diff --git a/mopidy/core/__init__.py b/mopidy/core/__init__.py index 720f9c38..1593d4ba 100644 --- a/mopidy/core/__init__.py +++ b/mopidy/core/__init__.py @@ -9,3 +9,4 @@ from .mixer import MixerController from .playback import PlaybackController, PlaybackState from .playlists import PlaylistsController from .tracklist import TracklistController +from .sleeptimer import SleepTimerController diff --git a/mopidy/core/actor.py b/mopidy/core/actor.py index ad9acba9..7d3bc245 100644 --- a/mopidy/core/actor.py +++ b/mopidy/core/actor.py @@ -17,6 +17,7 @@ from mopidy.core.mixer import MixerController from mopidy.core.playback import PlaybackController from mopidy.core.playlists import PlaylistsController from mopidy.core.tracklist import TracklistController +from mopidy.core.sleeptimer import SleepTimerController from mopidy.internal import path, storage, validation, versioning from mopidy.internal.deprecation import deprecated_property from mopidy.internal.models import CoreState @@ -44,6 +45,9 @@ class Core( playlists = None """An instance of :class:`~mopidy.core.PlaylistsController`""" + sleeptimer = None + """An instance of :class: ~mopidy.core.SleepTimerController`.""" + tracklist = None """An instance of :class:`~mopidy.core.TracklistController`""" @@ -61,6 +65,7 @@ class Core( audio=audio, backends=self.backends, core=self) self.playlists = PlaylistsController(backends=self.backends, core=self) self.tracklist = TracklistController(core=self) + self.sleeptimer = SleepTimerController(playback=self.playback, core=self) self.audio = audio diff --git a/mopidy/core/listener.py b/mopidy/core/listener.py index b8ef734d..041ff926 100644 --- a/mopidy/core/listener.py +++ b/mopidy/core/listener.py @@ -187,3 +187,40 @@ class CoreListener(listener.Listener): :type title: string """ pass + + def sleeptimer_started(self, was_running, duration, seconds_left): + """ + Called whenever the sleeptimer is started + *MAY* be implemented by actor. + :param was_running: indicates if the timer has been restarted while it was already running i.e. the end time has changed + :type was_running: boolean + :param duration: the length of time in seconds until the sleep timer will expire and stop playback + :type duration: int + :param seconds_left: the number of seconds left until the the sleep timer expire. may be slightly different to duration because of datetime calc rounding etc + :type seconds_left: float + """ + pass + + def sleeptimer_tick(self, seconds_left): + """ + Called roughly every 0.5 seconds when the sleeptimer is active + *MAY* be implemented by actor. + :param seconds_left: the number of seconds left until the the sleep timer expire + :type seconds_left: float + """ + pass + + def sleeptimer_expired(self): + """ + Called whenever the sleeptimer has reached the end time nd stopped playback + *MAY* be implemented by actor. + """ + pass + + def sleeptimer_cancelled(self): + """ + Called whenever the sleeptimer is running and is cancelled + *MAY* be implemented by actor. + """ + pass + diff --git a/mopidy/core/sleeptimer.py b/mopidy/core/sleeptimer.py new file mode 100644 index 00000000..2fb65ace --- /dev/null +++ b/mopidy/core/sleeptimer.py @@ -0,0 +1,128 @@ +from __future__ import absolute_import, unicode_literals + +import logging +import threading +import datetime +#import gobject +import time + +from mopidy.audio import PlaybackState +from mopidy.core import playback, listener + +logger = logging.getLogger(__name__) + + +class SleepTimerController(object): + pykka_traversable = True + + def __init__(self, playback, core): + logger.debug('Core.SleepTimer __init__') + self.playback = playback + self.core = core + + self._cancelevent = threading.Event() + self._timer = None + self._state = SleeptimerState() + self._state.__init__() + self._timer_id = None + + def get_state(self): + return {"running": self._state.running, + "duration": self._state.duration, + "seconds_left": self._get_seconds_left()} + + def _get_seconds_left(self): + now = datetime.datetime.now() + time_left = self._state.timerEndTime - now + seconds_left = time_left.total_seconds() + + if seconds_left < 0: + seconds_left = 0 + + return seconds_left + + def cancel(self, notify=True): + logger.debug('Cancel') + + self._cancelevent.set() + + if notify: + listener.CoreListener.send( + 'sleeptimer_cancelled') + + return True + + def start(self, duration): + old_state = self._state.running + logger.debug('Start - state = %s, duration = %d', old_state, duration) + + if self._state.running: + self.cancel(False) + + self._state.start(duration) + + if self._timer: + self._timer.cancel() + + #gobject.timeout_add(500, self._tick_handler) + self._timer=threading.Timer(1, self._tick_handler) + self._timer.start() + + self._cancelevent.clear() + + listener.CoreListener.send( + 'sleeptimer_started', + was_running=old_state, duration=self._state.duration, seconds_left=self._get_seconds_left()) + + return True + + def _tick_handler(self): + logger.debug('tick_handler, time left = %s', self._get_seconds_left()) + + if self._cancelevent.is_set(): + return False + + if datetime.datetime.now() > self._state.timerEndTime: + self._cancelevent.set() + + if self.playback.get_state() != PlaybackState.STOPPED: + #self.playback.stop() + self.playback.pause() + + listener.CoreListener.send( + 'sleeptimer_expired') + + self._state.clear() + + return False + else: + self._timer=threading.Timer(1, self._tick_handler) + self._timer.start() + listener.CoreListener.send( + 'sleeptimer_tick', + seconds_left=self._get_seconds_left()) + + return True + + +class SleeptimerState(object): + pykka_traversable = True + + def __init__(self): + #self.running = False + #self.duration = 0 + self.clear() + + def clear(self): + self.running = False + self.timerStartTime = datetime.datetime.now() + self.timerEndTime = self.timerStartTime + self.duration = 0 + + def start(self, duration): + self.running = True + self.timerStartTime = datetime.datetime.now() + self.timerEndTime = self.timerStartTime + datetime.timedelta(seconds=duration) + self.duration = duration + + logger.debug('SleepTimerState.start: running = %s, end time = %s', self.running, self.timerEndTime) diff --git a/mopidy/http/handlers.py b/mopidy/http/handlers.py index 654fad05..259453a9 100644 --- a/mopidy/http/handlers.py +++ b/mopidy/http/handlers.py @@ -59,6 +59,7 @@ def make_jsonrpc_wrapper(core_actor): 'core.playback': core.PlaybackController, 'core.playlists': core.PlaylistsController, 'core.tracklist': core.TracklistController, + 'core.sleeptimer': core.SleepTimerController, }) return jsonrpc.JsonRpcWrapper( objects={ @@ -71,6 +72,7 @@ def make_jsonrpc_wrapper(core_actor): 'core.playback': core_actor.playback, 'core.playlists': core_actor.playlists, 'core.tracklist': core_actor.tracklist, + 'core.sleeptimer': core_actor.sleeptimer, }, decoders=[models.model_json_decoder], encoders=[models.ModelJSONEncoder] diff --git a/mopidy/mpd/actor.py b/mopidy/mpd/actor.py index 00041bf3..9e7bceaa 100644 --- a/mopidy/mpd/actor.py +++ b/mopidy/mpd/actor.py @@ -26,6 +26,10 @@ _CORE_EVENTS_TO_IDLE_SUBSYSTEMS = { 'mute_changed': 'output', 'seeked': 'player', 'stream_title_changed': 'playlist', + 'sleeptimer_started': None, + 'sleeptimer_expired': None, + 'sleeptimer_tick': None, + 'sleeptimer_cancelled': None, }