diff --git a/docs/api/backends/controllers.rst b/docs/api/backends/controllers.rst index 28112cf7..20dc2d61 100644 --- a/docs/api/backends/controllers.rst +++ b/docs/api/backends/controllers.rst @@ -15,7 +15,6 @@ The backend .. autoclass:: mopidy.backends.base.Backend :members: - :undoc-members: Playback controller @@ -26,7 +25,6 @@ seek. .. autoclass:: mopidy.backends.base.PlaybackController :members: - :undoc-members: Mixer controller @@ -42,7 +40,6 @@ Manages everything related to the currently loaded playlist. .. autoclass:: mopidy.backends.base.CurrentPlaylistController :members: - :undoc-members: Stored playlists controller @@ -52,7 +49,6 @@ Manages stored playlist. .. autoclass:: mopidy.backends.base.StoredPlaylistsController :members: - :undoc-members: Library controller @@ -62,4 +58,3 @@ Manages the music library, e.g. searching for tracks to be added to a playlist. .. autoclass:: mopidy.backends.base.LibraryController :members: - :undoc-members: diff --git a/docs/api/backends/providers.rst b/docs/api/backends/providers.rst index 903e220b..61e5f68a 100644 --- a/docs/api/backends/providers.rst +++ b/docs/api/backends/providers.rst @@ -14,7 +14,6 @@ Playback provider .. autoclass:: mopidy.backends.base.BasePlaybackProvider :members: - :undoc-members: Stored playlists provider @@ -22,7 +21,6 @@ Stored playlists provider .. autoclass:: mopidy.backends.base.BaseStoredPlaylistsProvider :members: - :undoc-members: Library provider @@ -30,7 +28,6 @@ Library provider .. autoclass:: mopidy.backends.base.BaseLibraryProvider :members: - :undoc-members: Backend provider implementations 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/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/api/mixers.rst b/docs/api/mixers.rst index 6daa7a4e..2459db8c 100644 --- a/docs/api/mixers.rst +++ b/docs/api/mixers.rst @@ -30,7 +30,6 @@ methods as described below. .. automodule:: mopidy.mixers.base :synopsis: Mixer API :members: - :undoc-members: Mixer implementations diff --git a/docs/api/models.rst b/docs/api/models.rst index ef11547e..5833e58c 100644 --- a/docs/api/models.rst +++ b/docs/api/models.rst @@ -25,4 +25,3 @@ Data model API .. automodule:: mopidy.models :synopsis: Data model API :members: - :undoc-members: diff --git a/docs/changes.rst b/docs/changes.rst index 07212cc7..db03498c 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/docs/modules/frontends/mpd.rst b/docs/modules/frontends/mpd.rst index 6120c2a6..6f69b2a9 100644 --- a/docs/modules/frontends/mpd.rst +++ b/docs/modules/frontends/mpd.rst @@ -5,9 +5,8 @@ .. inheritance-diagram:: mopidy.frontends.mpd .. automodule:: mopidy.frontends.mpd - :synopsis: MPD frontend + :synopsis: MPD server frontend :members: - :undoc-members: MPD server @@ -18,7 +17,6 @@ MPD server .. automodule:: mopidy.frontends.mpd.server :synopsis: MPD server :members: - :undoc-members: MPD session @@ -29,7 +27,6 @@ MPD session .. automodule:: mopidy.frontends.mpd.session :synopsis: MPD client session :members: - :undoc-members: MPD dispatcher @@ -40,7 +37,6 @@ MPD dispatcher .. automodule:: mopidy.frontends.mpd.dispatcher :synopsis: MPD request dispatcher :members: - :undoc-members: MPD protocol diff --git a/docs/settings.rst b/docs/settings.rst index f0888670..68adfd55 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -151,4 +151,3 @@ Available settings .. automodule:: mopidy.settings :synopsis: Available settings and their default values :members: - :undoc-members: diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index 29188f55..e94ddf4d 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') @@ -486,23 +486,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 @@ -510,13 +508,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): diff --git a/mopidy/backends/spotify/translator.py b/mopidy/backends/spotify/translator.py index 1bf7e5aa..95287d77 100644 --- a/mopidy/backends/spotify/translator.py +++ b/mopidy/backends/spotify/translator.py @@ -4,7 +4,7 @@ import logging from spotify import Link, SpotifyError from mopidy import settings -from mopidy.backends.spotify import ENCODING, BITRATES +from mopidy.backends.spotify import ENCODING from mopidy.models import Artist, Album, Track, Playlist logger = logging.getLogger('mopidy.backends.spotify.translator') @@ -44,7 +44,7 @@ class SpotifyTranslator(object): track_no=spotify_track.index(), date=date, length=spotify_track.duration(), - bitrate=BITRATES[settings.SPOTIFY_BITRATE], + bitrate=settings.SPOTIFY_BITRATE, ) @classmethod 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 04716c61..33e724c0 100644 --- a/mopidy/frontends/lastfm.py +++ b/mopidy/frontends/lastfm.py @@ -10,14 +10,14 @@ 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') API_KEY = '2236babefa8ebb3d93ea467560d00d04' API_SECRET = '94d9a09c0cd5be955c4afaeaffcaefcd' -class LastfmFrontend(ThreadingActor, BaseFrontend): +class LastfmFrontend(ThreadingActor, BackendListener): """ Frontend which scrobbles the music you play to your `Last.fm `_ profile. @@ -57,14 +57,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 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. 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)