From 0794a8792a1eb66f287d63be346c2328fda1846d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 27 Jun 2011 18:43:19 +0300 Subject: [PATCH 1/4] Add BackendListener interface to be impl by any who wants events from the backend --- docs/api/listeners.rst | 7 +++++++ docs/changes.rst | 3 +++ mopidy/listeners.py | 34 ++++++++++++++++++++++++++++++++++ tests/listeners_test.py | 14 ++++++++++++++ 4 files changed, 58 insertions(+) create mode 100644 docs/api/listeners.rst create mode 100644 mopidy/listeners.py create mode 100644 tests/listeners_test.py diff --git a/docs/api/listeners.rst b/docs/api/listeners.rst new file mode 100644 index 00000000..609dc3c7 --- /dev/null +++ b/docs/api/listeners.rst @@ -0,0 +1,7 @@ +************ +Listener API +************ + +.. automodule:: mopidy.listeners + :synopsis: Listener API + :members: diff --git a/docs/changes.rst b/docs/changes.rst index 2e5b6659..2347ddb0 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -13,6 +13,9 @@ v0.6.0 (in development) - Replace :attr:`mopidy.backends.base.Backend.uri_handlers` with :attr:`mopidy.backends.base.Backend.uri_schemes`, which just takes the part up to the colon of an URI, and not any prefix. +- Add Listener API, :mod:`mopidy.listeners`, to be implemented by actors + wanting to receive events from the backend. This is a formalization of the + ad hoc events the Last.fm scrobbler has already been using for some time. v0.5.0 (2011-06-15) diff --git a/mopidy/listeners.py b/mopidy/listeners.py new file mode 100644 index 00000000..f6d1c67e --- /dev/null +++ b/mopidy/listeners.py @@ -0,0 +1,34 @@ +class BackendListener(object): + """ + Marker interface for recipients of events sent by the backend. + + Any Pykka actor that mixes in this class will receive calls to the methods + defined here when the corresponding events happen in the backend. This + interface is used both for looking up what actors to notify of the events, + and for providing default implementations for those listeners that are not + interested in all events. + """ + + def started_playing(self, track): + """ + Called whenever a new track starts playing. + + *MAY* be implemented by actor. + + :param track: the track that just started playing + :type track: :class:`mopidy.models.Track` + """ + pass + + def stopped_playing(self, track, stop_position): + """ + Called whenever playback is stopped. + + *MAY* be implemented by actor. + + :param track: the track that was played before playback stopped + :type track: :class:`mopidy.models.Track` + :param stop_position: the time position when stopped in milliseconds + :type stop_position: int + """ + pass diff --git a/tests/listeners_test.py b/tests/listeners_test.py new file mode 100644 index 00000000..761aff4f --- /dev/null +++ b/tests/listeners_test.py @@ -0,0 +1,14 @@ +import unittest + +from mopidy.listeners import BackendListener +from mopidy.models import Track + +class BackendListenerTest(unittest.TestCase): + def setUp(self): + self.listener = BackendListener() + + def test_listener_has_default_impl_for_the_started_playing_event(self): + self.listener.started_playing(Track()) + + def test_listener_has_default_impl_for_the_stopped_playing_event(self): + self.listener.stopped_playing(Track(), 0) From 4fd83d3641ea6e1193ae03ca53330c6d376d132e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 27 Jun 2011 18:58:32 +0300 Subject: [PATCH 2/4] Update PlaybackController to use new BackendListener --- mopidy/backends/base/playback.py | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index 530c4840..07e286fa 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -4,7 +4,7 @@ import time from pykka.registry import ActorRegistry -from mopidy.frontends.base import BaseFrontend +from mopidy.listeners import BackendListener logger = logging.getLogger('mopidy.backends.base') @@ -462,23 +462,21 @@ class PlaybackController(object): def _trigger_started_playing_event(self): """ - Notifies frontends that a track has started playing. + Notifies implementors of :class:`mopidy.listeners.BackendListener` that + a track has started playing. For internal use only. Should be called by the backend directly after a track has started playing. """ if self.current_track is None: return - frontend_refs = ActorRegistry.get_by_class(BaseFrontend) - for frontend_ref in frontend_refs: - frontend_ref.send_one_way({ - 'command': 'started_playing', - 'track': self.current_track, - }) + for listener_ref in ActorRegistry.get_by_class(BackendListener): + listener_ref.proxy().started_playing(track=self.current_track) def _trigger_stopped_playing_event(self): """ - Notifies frontends that a track has stopped playing. + Notifies implementors of :class:`mopidy.listeners.BackendListener` that + a track has stopped playing. For internal use only. Should be called by the backend before a track is stopped playing, e.g. at the next, previous, and stop actions and at @@ -486,13 +484,9 @@ class PlaybackController(object): """ if self.current_track is None: return - frontend_refs = ActorRegistry.get_by_class(BaseFrontend) - for frontend_ref in frontend_refs: - frontend_ref.send_one_way({ - 'command': 'stopped_playing', - 'track': self.current_track, - 'stop_position': self.time_position, - }) + for listener_ref in ActorRegistry.get_by_class(BackendListener): + listener_ref.proxy().stopped_playing( + track=self.current_track, stop_position=self.time_position) class BasePlaybackProvider(object): From 32bf7af73ff7c5eaca94507f5153a1087ee4e04b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 27 Jun 2011 18:58:42 +0300 Subject: [PATCH 3/4] Update LastfmFrontend to use new BackendListener --- mopidy/frontends/lastfm.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/mopidy/frontends/lastfm.py b/mopidy/frontends/lastfm.py index 04716c61..79ce286d 100644 --- a/mopidy/frontends/lastfm.py +++ b/mopidy/frontends/lastfm.py @@ -11,13 +11,14 @@ from pykka.actor import ThreadingActor from mopidy import settings, SettingsError from mopidy.frontends.base import BaseFrontend +from mopidy.listeners import BackendListener logger = logging.getLogger('mopidy.frontends.lastfm') API_KEY = '2236babefa8ebb3d93ea467560d00d04' API_SECRET = '94d9a09c0cd5be955c4afaeaffcaefcd' -class LastfmFrontend(ThreadingActor, BaseFrontend): +class LastfmFrontend(ThreadingActor, BaseFrontend, BackendListener): """ Frontend which scrobbles the music you play to your `Last.fm `_ profile. @@ -57,14 +58,6 @@ class LastfmFrontend(ThreadingActor, BaseFrontend): logger.error(u'Error during Last.fm setup: %s', e) self.stop() - def on_receive(self, message): - if message.get('command') == 'started_playing': - self.started_playing(message['track']) - elif message.get('command') == 'stopped_playing': - self.stopped_playing(message['track'], message['stop_position']) - else: - pass # Ignore any other messages - def started_playing(self, track): artists = ', '.join([a.name for a in track.artists]) duration = track.length and track.length // 1000 or 0 From 83d2601f680331041ef54bb4960b6c8cf72587e9 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 27 Jun 2011 19:30:57 +0300 Subject: [PATCH 4/4] Replace redundant BaseFrontend class with list of requirements for frontend implementations --- docs/api/frontends.rst | 34 ++++++++++++++++++-------------- mopidy/frontends/base.py | 5 ----- mopidy/frontends/lastfm.py | 3 +-- mopidy/frontends/mpd/__init__.py | 3 +-- 4 files changed, 21 insertions(+), 24 deletions(-) delete mode 100644 mopidy/frontends/base.py diff --git a/docs/api/frontends.rst b/docs/api/frontends.rst index 0c1e32a3..792e4bc9 100644 --- a/docs/api/frontends.rst +++ b/docs/api/frontends.rst @@ -2,22 +2,26 @@ Frontend API ************ -A frontend may do whatever it wants to, including creating threads, opening TCP -ports and exposing Mopidy for a type of clients. - -Frontends got one main limitation: they are restricted to passing messages -through the ``core_queue`` for all communication with the rest of Mopidy. Thus, -the frontend API is very small and reveals little of what a frontend may do. - -.. warning:: - - A stable frontend API is not available yet, as we've only implemented a - couple of frontend modules. - -.. automodule:: mopidy.frontends.base - :synopsis: Base class for frontends - :members: +The following requirements applies to any frontend implementation: +- A frontend MAY do mostly whatever it wants to, including creating threads, + opening TCP ports and exposing Mopidy for a group of clients. +- A frontend MUST implement at least one `Pykka + `_ actor, called the "main actor" from here + on. +- It MAY use additional actors to implement whatever it does, and using actors + in frontend implementations is encouraged. +- The frontend is activated by including its main actor in the + :attr:`mopidy.settings.FRONTENDS` setting. +- The main actor MUST be able to start and stop the frontend when the main + actor is started and stopped. +- The frontend MAY require additional settings to be set for it to + work. +- Such settings MUST be documented. +- The main actor MUST stop itself if the defined settings are not adequate for + the frontend to work properly. +- Any actor which is part of the frontend MAY implement any listener interface + from :mod:`mopidy.listeners` to receive notification of the specified events. Frontend implementations ======================== diff --git a/mopidy/frontends/base.py b/mopidy/frontends/base.py deleted file mode 100644 index 811644b1..00000000 --- a/mopidy/frontends/base.py +++ /dev/null @@ -1,5 +0,0 @@ -class BaseFrontend(object): - """ - Base class for frontends. - """ - pass diff --git a/mopidy/frontends/lastfm.py b/mopidy/frontends/lastfm.py index 79ce286d..33e724c0 100644 --- a/mopidy/frontends/lastfm.py +++ b/mopidy/frontends/lastfm.py @@ -10,7 +10,6 @@ except ImportError as import_error: from pykka.actor import ThreadingActor from mopidy import settings, SettingsError -from mopidy.frontends.base import BaseFrontend from mopidy.listeners import BackendListener logger = logging.getLogger('mopidy.frontends.lastfm') @@ -18,7 +17,7 @@ logger = logging.getLogger('mopidy.frontends.lastfm') API_KEY = '2236babefa8ebb3d93ea467560d00d04' API_SECRET = '94d9a09c0cd5be955c4afaeaffcaefcd' -class LastfmFrontend(ThreadingActor, BaseFrontend, BackendListener): +class LastfmFrontend(ThreadingActor, BackendListener): """ Frontend which scrobbles the music you play to your `Last.fm `_ profile. diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index 175aa0ee..f37b2deb 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -3,13 +3,12 @@ import logging from pykka.actor import ThreadingActor -from mopidy.frontends.base import BaseFrontend from mopidy.frontends.mpd.server import MpdServer from mopidy.utils.process import BaseThread logger = logging.getLogger('mopidy.frontends.mpd') -class MpdFrontend(ThreadingActor, BaseFrontend): +class MpdFrontend(ThreadingActor): """ The MPD frontend.