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/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/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):
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)