Skip to content

Commit

Permalink
Merge branch 'release-0.1.7'
Browse files Browse the repository at this point in the history
  • Loading branch information
DavisNT committed Mar 17, 2018
2 parents 4b68396 + c1bba16 commit 404fb7f
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 14 deletions.
7 changes: 6 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
language: python
sudo: true

python:
- "2.7_with_system_site_packages"
Expand All @@ -7,11 +8,15 @@ env:
- TOX_ENV=py27
- TOX_ENV=flake8

before_install:
- sudo apt-get update
- sudo apt-get install -y python-gi python-gst-1.0 gir1.2-gstreamer-1.0 gir1.2-gst-plugins-base-1.0 gstreamer1.0-plugins-good gstreamer1.0-plugins-ugly gstreamer1.0-tools

install:
- "pip install tox"

script:
- "tox -e $TOX_ENV"

after_success:
- "if [ $TOX_ENV == 'py27' ]; then pip install coveralls; coveralls; fi"
- "if [ $TOX_ENV == 'py27' ]; then pip install coveralls requests==2.2.1; coveralls; fi"
34 changes: 30 additions & 4 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@ Mopidy-AlarmClock
:target: https://pypi.python.org/pypi/Mopidy-AlarmClock/
:alt: Latest PyPI version

.. image:: https://img.shields.io/pypi/dm/Mopidy-AlarmClock.svg?style=flat
:target: https://pypi.python.org/pypi/Mopidy-AlarmClock/
:alt: Number of PyPI downloads

.. image:: https://travis-ci.org/DavisNT/mopidy-alarmclock.svg?branch=develop
:target: https://travis-ci.org/DavisNT/mopidy-alarmclock
:alt: Travis-CI build status
Expand Down Expand Up @@ -57,6 +53,30 @@ Usage

Make sure that the `HTTP extension <http://docs.mopidy.com/en/latest/ext/http/>`_ is enabled. Then browse to the app on the Mopidy server (for instance, http://localhost:6680/alarmclock/).

**WARNING! It is strongly recommended to use only local playlists with local media (files) for alarm clock.**

Althrough Mopidy-AlarmClock contains some safety measures against playlist/track inaccessibility (e.g. upon network outage) it is still much safer to use local media.

License
=============
::

Copyright 2014 Mathieu Xhonneux
Copyright 2015-2018 Davis Mosenkovs

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.


Project resources
=================

Expand All @@ -68,6 +88,12 @@ Project resources
Changelog
=========

v0.1.7
----------------------------------------

- Play `backup alarm sound <http://soundbible.com/1787-Annoying-Alarm-Clock.html>`_ when playback cannot be started (within 30 seconds or more).
- Added warning to readme that only local playlists/media should be used for alarm clock.

v0.1.6
----------------------------------------

Expand Down
3 changes: 1 addition & 2 deletions mopidy_alarmclock/__init__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
from __future__ import unicode_literals

import os

from http import MessageStore, factory_decorator

from alarm_manager import AlarmManager

from mopidy import config, ext


__version__ = '0.1.6'
__version__ = '0.1.7'


class Extension(ext.Extension):
Expand Down
42 changes: 37 additions & 5 deletions mopidy_alarmclock/alarm_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@
from __future__ import unicode_literals

import datetime
import logging
import os
import time
from threading import Timer

import monotonic

from mopidy.core import PlaybackState


# Enum of states
class states:
Expand All @@ -23,6 +28,7 @@ class AlarmManager(object):
core = None
state = states.DISABLED
idle_timer = None
logger = logging.getLogger(__name__)

def get_core(self, core):
self.core = core
Expand Down Expand Up @@ -80,15 +86,19 @@ def set_alarm(self, clock_datetime, playlist, random_mode, volume, volume_increa

self.idle()

def play(self):
def play(self, fallback=False):
self.logger.info("AlarmClock alarm started (fallback %s)", fallback)
self.core.playback.stop()
self.core.tracklist.clear()

try:
if fallback:
raise Exception('Fallback')
self.core.tracklist.add(self.get_playlist().tracks)
if self.core.tracklist.length.get() < 1:
raise Exception('Tracklist empty')
except:
except Exception as e:
self.logger.info("AlarmClock using backup alarm, reason: %s", e)
self.core.tracklist.add(None, 0, 'file://' + os.path.join(os.path.dirname(__file__), 'backup-alarm.mp3'))

self.core.tracklist.consume = False
Expand All @@ -100,11 +110,31 @@ def play(self):
self.core.playback.next()

self.core.playback.mute = False

self.adjust_volume(self.volume, self.volume_increase_seconds, 0)
self.core.playback.volume = 0

self.core.playback.play()

if not fallback: # do fallback only once
self.logger.info("AlarmClock waiting for playback to start")
waited = 0.5
starttime = 0
try:
starttime = monotonic.monotonic()
time.sleep(0.5)
while self.core.playback.state.get() != PlaybackState.PLAYING or self.core.playback.time_position.get() < 100: # in some cases this check will cause a notable delay
self.logger.info("AlarmClock has been waiting for %.2f seconds (waited inside AlarmClock %.2f sec)", monotonic.monotonic() - starttime, waited)
if waited > 30 or (waited > 0.5 and monotonic.monotonic() - starttime > 30): # ensure EITHER delay is more than 30 seconds OR at least 2 times above line has been executed
raise Exception("Timeout")
time.sleep(1)
waited += 1
self.logger.info("AlarmClock playback started within %.2f seconds (waited inside AlarmClock %.2f sec)", monotonic.monotonic() - starttime, waited)
except Exception as e:
self.logger.info("AlarmClock playback FAILED to start (waited inside AlarmClock %.2f sec), reason: %s", waited, e)
self.play(True)
return

self.adjust_volume(self.volume, self.volume_increase_seconds, 0)

self.reset()
self.state = states.DISABLED

Expand All @@ -122,12 +152,14 @@ def adjust_volume(self, target_volume, increase_duration, step_no):
current_volume = None
try:
current_volume = self.core.playback.volume.get()
except:
except Exception:
pass
if step_no == 0 or not isinstance(current_volume, int) or current_volume == int(round(target_volume * (step_no) / (number_of_steps + 1))):
if step_no >= number_of_steps: # this design should prevent floating-point edge-case bugs (in case such bugs could be possible here)
self.logger.info("AlarmClock increasing volume to target volume %d", target_volume)
self.core.playback.volume = target_volume
else:
self.logger.info("AlarmClock increasing volume to %d", int(round(target_volume * (step_no + 1) / (number_of_steps + 1))))
self.core.playback.volume = int(round(target_volume * (step_no + 1) / (number_of_steps + 1)))
t = Timer(increase_duration / number_of_steps, self.adjust_volume, [target_volume, increase_duration, step_no + 1])
t.start()
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def get_version(filename):
'setuptools',
'Mopidy >= 0.19',
'Pykka >= 1.1',
'monotonic >= 1.4',
],
test_suite='nose.collector',
tests_require=[
Expand Down
76 changes: 75 additions & 1 deletion tests/test_alarm_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

import mock

from mopidy.core import PlaybackState

from mopidy_alarmclock.alarm_manager import AlarmManager


Expand Down Expand Up @@ -98,6 +100,8 @@ def test02_set_alarm__threading(self):
def test02_set_alarm__empty_playlist(self):
core = mock.Mock()
playlist = 'Playlist URI'
core.playback.state.get.side_effect = lambda: PlaybackState.PLAYING
core.playback.time_position.get.side_effect = lambda: 234
core.playlists.lookup('Playlist URI').get().tracks = 'Tracks 811, 821, 823, 827, 829, 839'
core.tracklist.length.get.side_effect = lambda: 4
self.assertEqual(core.playlists.lookup.call_count, 1) # First call when setting up the Mock
Expand All @@ -121,13 +125,79 @@ def test02_set_alarm__empty_playlist(self):
# Set alarm to PAST
am.set_alarm(datetime.datetime(2000, 4, 28, 7, 59, 15, 324341), playlist, False, 83, 0)

# Ensure that tracks were added
# Ensure that tracks (including backup alarm) were added
self.assertEqual(core.playlists.lookup.call_count, 3)
self.assertEqual(core.tracklist.add.call_count, 2)
core.tracklist.add.assert_any_call('Tracks 811, 821, 823, 827, 829, 839')
core.tracklist.add.assert_called_with(None, 0, 'file://' + os.path.dirname(os.path.dirname(__file__)) + '/mopidy_alarmclock/backup-alarm.mp3')
core.playback.play.assert_called_once_with()

def test02_set_alarm__broken_playback(self):
core = mock.Mock()
playlist = 'Playlist URI'
core.playback.state.get.side_effect = lambda: PlaybackState.PLAYING
core.playback.time_position.get.side_effect = lambda: 234
core.playlists.lookup('Playlist URI').get().tracks = 'Tracks 811, 821, 823, 827, 829, 839'
core.tracklist.length.get.side_effect = lambda: 4
self.assertEqual(core.playlists.lookup.call_count, 1) # First call when setting up the Mock

am = AlarmManager()
am.get_core(core)

# Set alarm to PAST
am.set_alarm(datetime.datetime(2000, 4, 28, 7, 59, 15, 324341), playlist, False, 83, 0)

# Ensure that tracks were added
self.assertEqual(core.playlists.lookup.call_count, 2)
core.tracklist.add.assert_called_once_with('Tracks 811, 821, 823, 827, 829, 839')
core.playback.play.assert_called_once_with()

# Cleanup and re-setup (part 2 starts here)
core.tracklist.add.reset_mock()
core.playback.play.reset_mock()
core.playback.state.get.reset_mock()
core.playback.time_position.get.reset_mock()
core.playback.state.get.side_effect = lambda: PlaybackState.PLAYING
core.playback.time_position.get.side_effect = lambda: 0 # simulate broken playback (stuck at 0 milliseconds)

# Set alarm to PAST
am.set_alarm(datetime.datetime(2000, 4, 28, 7, 59, 15, 324341), playlist, False, 83, 0)

# Ensure that tracks (including backup alarm) were added and playback started
self.assertEqual(core.playlists.lookup.call_count, 3)
self.assertEqual(core.tracklist.add.call_count, 2)
core.tracklist.add.assert_any_call('Tracks 811, 821, 823, 827, 829, 839')
core.tracklist.add.assert_called_with(None, 0, 'file://' + os.path.dirname(os.path.dirname(__file__)) + '/mopidy_alarmclock/backup-alarm.mp3')
self.assertEqual(core.playback.play.call_count, 2)

# Ensure playback was checked around 31 times (tolerate 1 sec possible slowness of build env)
self.assertGreaterEqual(core.playback.state.get.call_count, 30)
self.assertLess(core.playback.state.get.call_count, 32)
self.assertGreaterEqual(core.playback.time_position.get.call_count, 30)
self.assertLess(core.playback.time_position.get.call_count, 32)

# Cleanup and re-setup (part 3 starts here)
core.tracklist.add.reset_mock()
core.playback.play.reset_mock()
core.playback.state.get.reset_mock()
core.playback.time_position.get.reset_mock()
core.playback.state.get.side_effect = lambda: time.sleep(31) # simulate broken playback (return invalid state after 31 second delay)
core.playback.time_position.get.side_effect = lambda: 234

# Set alarm to PAST
am.set_alarm(datetime.datetime(2000, 4, 28, 7, 59, 15, 324341), playlist, False, 83, 0)

# Ensure that tracks (including backup alarm) were added and playback started
self.assertEqual(core.playlists.lookup.call_count, 4)
self.assertEqual(core.tracklist.add.call_count, 2)
core.tracklist.add.assert_any_call('Tracks 811, 821, 823, 827, 829, 839')
core.tracklist.add.assert_called_with(None, 0, 'file://' + os.path.dirname(os.path.dirname(__file__)) + '/mopidy_alarmclock/backup-alarm.mp3')
self.assertEqual(core.playback.play.call_count, 2)

# Ensure playback was checked exactly 2 times (due to delay during checking)
self.assertEqual(core.playback.state.get.call_count, 2)
self.assertEqual(core.playback.time_position.get.call_count, 0) # actually this does not get called (because it is 2 operand in or)

def test02_get_ring_time(self):
playlist = 'Playlist URI'

Expand All @@ -142,6 +212,8 @@ def test02_get_ring_time(self):
def test03_cancel(self):
core = mock.Mock()
playlist = 'Playlist URI'
core.playback.state.get.side_effect = lambda: PlaybackState.PLAYING
core.playback.time_position.get.side_effect = lambda: 234
threadcount = threading.active_count()

am = AlarmManager()
Expand Down Expand Up @@ -436,6 +508,8 @@ def test03_adjust_volume__100_30_intervened(self):
def test04__integration_1(self):
core = mock.Mock()
playlist = 'Playlist URI'
core.playback.state.get.side_effect = lambda: PlaybackState.PLAYING
core.playback.time_position.get.side_effect = lambda: 234
core.playlists.lookup('Playlist URI').get().tracks = 'Tracks 811, 821, 823, 827, 829, 839'
self.assertEqual(core.playlists.lookup.call_count, 1) # First call when setting up the Mock
threadcount = threading.active_count()
Expand Down
3 changes: 2 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ deps =
nose
freezegun
mopidy
install_command = pip install --allow-unverified=mopidy --pre {opts} {packages}
tornado<5.0
commands = nosetests -v --with-xunit --xunit-file=xunit-{envname}.xml --with-coverage --cover-package=mopidy_alarmclock

[testenv:flake8]
deps =
flake8
flake8-import-order
tornado<5.0
commands = flake8

[flake8]
Expand Down

0 comments on commit 404fb7f

Please sign in to comment.