Merge branch 'develop' into feature/glib-loop

Conflicts:
	mopidy/core.py
	mopidy/frontends/mpd/__init__.py
This commit is contained in:
Thomas Adamcik 2011-07-04 00:31:18 +02:00
commit 75984dda6a
30 changed files with 201 additions and 96 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

@ -8,9 +8,18 @@ This change log is used to track all major changes to Mopidy.
v0.6.0 (in development)
=======================
**Important changes**
- Pykka 0.12.3 or greater is required.
**Changes**
- None yet
- 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

@ -25,7 +25,7 @@ Otherwise, make sure you got the required dependencies installed.
- Python >= 2.6, < 3
- `Pykka <http://jodal.github.com/pykka/>`_ >= 0.12
- `Pykka <http://jodal.github.com/pykka/>`_ >= 0.12.3
- GStreamer >= 0.10, with Python bindings. See :doc:`gstreamer`.

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

@ -25,5 +25,5 @@ class Backend(object):
#: :class:`mopidy.backends.base.StoredPlaylistsController`.
stored_playlists = None
#: List of URI prefixes this backend can handle.
uri_handlers = []
#: List of URI schemes this backend can handle.
uri_schemes = []

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')
@ -461,38 +461,30 @@ class PlaybackController(object):
self.current_cp_track = None
def _trigger_started_playing_event(self):
"""
Notifies frontends that a track has started playing.
For internal use only. Should be called by the backend directly after a
track has started playing.
"""
logger.debug(u'Triggering started playing event')
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,
})
ActorRegistry.broadcast({
'command': 'pykka_call',
'attr_path': ('started_playing',),
'args': [],
'kwargs': {'track': self.current_track},
}, target_class=BackendListener)
def _trigger_stopped_playing_event(self):
"""
Notifies frontends 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
end-of-track.
"""
# TODO Test that this is called on next/prev/end-of-track
logger.debug(u'Triggering stopped playing event')
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',
ActorRegistry.broadcast({
'command': 'pykka_call',
'attr_path': ('stopped_playing',),
'args': [],
'kwargs': {
'track': self.current_track,
'stop_position': self.time_position,
})
'time_position': self.time_position,
},
}, target_class=BackendListener)
class BasePlaybackProvider(object):

View File

@ -32,7 +32,7 @@ class DummyBackend(ThreadingActor, Backend):
self.stored_playlists = StoredPlaylistsController(backend=self,
provider=stored_playlists_provider)
self.uri_handlers = [u'dummy:']
self.uri_schemes = [u'dummy']
class DummyLibraryProvider(BaseLibraryProvider):

View File

@ -52,7 +52,7 @@ class LocalBackend(ThreadingActor, Backend):
self.stored_playlists = StoredPlaylistsController(backend=self,
provider=stored_playlists_provider)
self.uri_handlers = [u'file://']
self.uri_schemes = [u'file']
self.gstreamer = None

View File

@ -67,7 +67,7 @@ class SpotifyBackend(ThreadingActor, Backend):
self.stored_playlists = StoredPlaylistsController(backend=self,
provider=stored_playlists_provider)
self.uri_handlers = [u'spotify:', u'http://open.spotify.com/']
self.uri_schemes = [u'spotify']
self.gstreamer = None
self.spotify = None

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

@ -25,7 +25,8 @@ from mopidy.gstreamer import GStreamer
from mopidy.utils import get_class
from mopidy.utils.log import setup_logging
from mopidy.utils.path import get_or_create_folder, get_or_create_file
from mopidy.utils.process import exit_handler, stop_all_actors
from mopidy.utils.process import (exit_handler, stop_remaining_actors,
stop_actors_by_class)
from mopidy.utils.settings import list_settings_optparse_callback
logger = logging.getLogger('mopidy.core')
@ -50,7 +51,11 @@ def main():
logger.exception(e)
finally:
loop.quit()
stop_all_actors()
stop_frontends()
stop_backend()
stop_mixer()
stop_gstreamer()
stop_remaining_actors()
def parse_options():
parser = optparse.OptionParser(version=u'Mopidy %s' % get_version())
@ -59,7 +64,7 @@ def parse_options():
help='show GStreamer help options')
parser.add_option('-i', '--interactive',
action='store_true', dest='interactive',
help='ask interactively for required settings which is missing')
help='ask interactively for required settings which are missing')
parser.add_option('-q', '--quiet',
action='store_const', const=0, dest='verbosity_level',
help='less output (warning level)')
@ -86,15 +91,31 @@ def setup_settings(interactive):
def setup_gstreamer():
GStreamer.start()
def stop_gstreamer():
stop_actors_by_class(GStreamer)
def setup_mixer():
get_class(settings.MIXER).start()
def stop_mixer():
stop_actors_by_class(get_class(settings.MIXER))
def setup_backend():
get_class(settings.BACKENDS[0]).start()
def stop_backend():
stop_actors_by_class(get_class(settings.BACKENDS[0]))
def setup_frontends():
for frontend_class_name in settings.FRONTENDS:
try:
get_class(frontend_class_name).start()
except OptionalDependencyError as e:
logger.info(u'Disabled: %s (%s)', frontend_class_name, e)
def stop_frontends():
for frontend_class_name in settings.FRONTENDS:
try:
stop_actors_by_class(get_class(frontend_class_name))
except OptionalDependencyError:
pass

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,7 +3,6 @@ import sys
from pykka.actor import ThreadingActor
from mopidy.frontends.base import BaseFrontend
from mopidy import settings
from mopidy.utils import network
from mopidy.frontends.mpd.dispatcher import MpdDispatcher
@ -11,8 +10,7 @@ from mopidy.frontends.mpd.protocol import ENCODING, VERSION, LINE_TERMINATOR
logger = logging.getLogger('mopidy.frontends.mpd')
# FIXME no real need for frontend to be threading actor
class MpdFrontend(ThreadingActor, BaseFrontend):
class MpdFrontend(ThreadingActor):
"""
The MPD frontend.

View File

@ -128,7 +128,7 @@ class MpdDispatcher(object):
return self._call_next_filter(request, response, filter_chain)
except ActorDeadError as e:
logger.warning(u'Tried to communicate with dead actor.')
raise exceptions.MpdSystemError(e.message)
raise exceptions.MpdSystemError(e)
def _call_handler(self, request):
(handler, kwargs) = self._find_handler(request)

View File

@ -19,8 +19,8 @@ def add(context, uri):
"""
if not uri:
return
for handler_prefix in context.backend.uri_handlers.get():
if uri.startswith(handler_prefix):
for uri_scheme in context.backend.uri_schemes.get():
if uri.startswith(uri_scheme):
track = context.backend.library.lookup(uri).get()
if track is not None:
context.backend.current_playlist.add(track)

View File

@ -95,4 +95,5 @@ def urlhandlers(context):
Gets a list of available URL handlers.
"""
return [(u'handler', uri) for uri in context.backend.uri_handlers.get()]
return [(u'handler', uri_scheme)
for uri_scheme in context.backend.uri_schemes.get()]

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, time_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 time_position: the time position in milliseconds
:type time_position: int
"""
pass

View File

@ -22,9 +22,17 @@ def exit_handler(signum, frame):
logger.info(u'Got %s signal', signals[signum])
exit_process()
def stop_all_actors():
def stop_actors_by_class(klass):
actors = ActorRegistry.get_by_class(klass)
logger.debug(u'Stopping %d instance(s) of %s', len(actors), klass.__name__)
for actor in actors:
actor.stop()
def stop_remaining_actors():
num_actors = len(ActorRegistry.get_all())
while num_actors:
logger.error(
u'There are actor threads still running, this is probably a bug')
logger.debug(u'Seeing %d actor and %d non-actor thread(s): %s',
num_actors, threading.active_count() - num_actors,
', '.join([t.name for t in threading.enumerate()]))

View File

@ -1 +1 @@
Pykka >= 0.12
Pykka >= 0.12.3

View File

@ -0,0 +1,45 @@
import threading
import unittest
from pykka.actor import ThreadingActor
from pykka.registry import ActorRegistry
from mopidy.backends.dummy import DummyBackend
from mopidy.listeners import BackendListener
from mopidy.models import Track
class BackendEventsTest(unittest.TestCase):
def setUp(self):
self.events = {
'started_playing': threading.Event(),
'stopped_playing': threading.Event(),
}
self.backend = DummyBackend.start().proxy()
self.listener = DummyBackendListener.start(self.events).proxy()
def tearDown(self):
ActorRegistry.stop_all()
def test_play_sends_started_playing_event(self):
self.backend.current_playlist.add([Track(uri='a')])
self.backend.playback.play()
self.events['started_playing'].wait(timeout=1)
self.assertTrue(self.events['started_playing'].is_set())
def test_stop_sends_stopped_playing_event(self):
self.backend.current_playlist.add([Track(uri='a')])
self.backend.playback.play()
self.backend.playback.stop()
self.events['stopped_playing'].wait(timeout=1)
self.assertTrue(self.events['stopped_playing'].is_set())
class DummyBackendListener(ThreadingActor, BackendListener):
def __init__(self, events):
self.events = events
def started_playing(self, track):
self.events['started_playing'].set()
def stopped_playing(self, track, time_position):
self.events['stopped_playing'].set()

View File

@ -36,8 +36,8 @@ class LocalPlaybackControllerTest(PlaybackControllerTest, unittest.TestCase):
track = Track(uri=uri, length=4464)
self.backend.current_playlist.add(track)
def test_uri_handler(self):
self.assert_('file://' in self.backend.uri_handlers)
def test_uri_scheme(self):
self.assertIn('file', self.backend.uri_schemes)
def test_play_mp3(self):
self.add_track('blank.mp3')

View File

@ -76,4 +76,4 @@ class ReflectionHandlerTest(unittest.TestCase):
def test_urlhandlers(self):
result = self.dispatcher.handle_request(u'urlhandlers')
self.assert_(u'OK' in result)
self.assert_(u'handler: dummy:' in result)
self.assert_(u'handler: dummy' in result)

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)