Merge branch 'feature/event-listeners' into feature/mpris-frontend

Conflicts:
	docs/changes.rst
This commit is contained in:
Stein Magnus Jodal 2011-06-27 19:49:07 +03:00
commit 232d81aab8
16 changed files with 93 additions and 66 deletions

View File

@ -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:

View File

@ -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

View File

@ -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
<http://jodal.github.com/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
========================

7
docs/api/listeners.rst Normal file
View File

@ -0,0 +1,7 @@
************
Listener API
************
.. automodule:: mopidy.listeners
:synopsis: Listener API
:members:

View File

@ -30,7 +30,6 @@ methods as described below.
.. automodule:: mopidy.mixers.base
:synopsis: Mixer API
:members:
:undoc-members:
Mixer implementations

View File

@ -25,4 +25,3 @@ Data model API
.. automodule:: mopidy.models
:synopsis: Data model API
:members:
:undoc-members:

View File

@ -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)

View File

@ -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

View File

@ -151,4 +151,3 @@ Available settings
.. automodule:: mopidy.settings
:synopsis: Available settings and their default values
:members:
:undoc-members:

View File

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

View File

@ -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

View File

@ -1,5 +0,0 @@
class BaseFrontend(object):
"""
Base class for frontends.
"""
pass

View File

@ -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
<http://www.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

View File

@ -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.

34
mopidy/listeners.py Normal file
View File

@ -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

14
tests/listeners_test.py Normal file
View File

@ -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)