From 0aeb11b22ebd94b0263e209a517afae46eb1c9da Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 7 Sep 2012 00:51:08 +0200 Subject: [PATCH 01/44] Create a Track proxy for spotify (Fixes #72) --- mopidy/backends/spotify/library.py | 45 ++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/mopidy/backends/spotify/library.py b/mopidy/backends/spotify/library.py index a080c7bd..181dc19d 100644 --- a/mopidy/backends/spotify/library.py +++ b/mopidy/backends/spotify/library.py @@ -5,21 +5,54 @@ from spotify import Link, SpotifyError from mopidy.backends.base import BaseLibraryProvider from mopidy.backends.spotify.translator import SpotifyTranslator -from mopidy.models import Playlist +from mopidy.models import Track, Playlist logger = logging.getLogger('mopidy.backends.spotify.library') + +class SpotifyTrack(Track): + def __init__(self, uri): + self._spotify_track = Link.from_string(uri).as_track() + self._unloaded_track = Track(uri=uri, name=u'[loading...]') + self._track = None + + @property + def _proxy(self): + if self._track: + return self._track + elif self._spotify_track.is_loaded(): + self._track = SpotifyTranslator.to_mopidy_track(self._spotify_track) + return self._track + else: + return self._unloaded_track + + def __getattribute__(self, name): + if name.startswith('_'): + return super(SpotifyTrack, self).__getattribute__(name) + return self._proxy.__getattribute__(name) + + def __repr__(self): + return self._proxy.__repr__() + + def __hash__(self): # hash on just uri for consistency? + return hash(self._proxy.uri) + + def __eq__(self, other): # compare on just uri for consistency? + if not isinstance(other, Track): + return False + return self._proxy.uri == other.uri + + def copy(self, **values): # is it okay to return a plain track? + return self._proxy.copy(**values) + + class SpotifyLibraryProvider(BaseLibraryProvider): def find_exact(self, **query): return self.search(**query) def lookup(self, uri): try: - spotify_track = Link.from_string(uri).as_track() - # TODO Block until metadata_updated callback is called. Before that - # the track will be unloaded, unless it's already in the stored - # playlists. - return SpotifyTranslator.to_mopidy_track(spotify_track) + return SpotifyTrack(uri) except SpotifyError as e: logger.debug(u'Failed to lookup "%s": %s', uri, e) return None From 911b45dce8f83b1abbb24df927de61bafef5eaf6 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 9 Sep 2012 23:27:03 +0200 Subject: [PATCH 02/44] Document debug-proxy's existance. --- docs/development/contributing.rst | 41 +++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/docs/development/contributing.rst b/docs/development/contributing.rst index 373da1a0..74e2f0b5 100644 --- a/docs/development/contributing.rst +++ b/docs/development/contributing.rst @@ -125,6 +125,47 @@ statistics and uses pylint to check for errors and possible improvements in our code. So, if you're out of work, the code coverage and pylint data at the CI server should give you a place to start. +Protocol debugging +================== + +Since the main interface provided to Mopidy is through the MPD protocol, it is +crucial that we try and stay in sync with protocol developments. In an attempt +to make it easier to debug differences Mopidy and MPD protocol handling we have +created ``tools/debug-proxy.py``. + +This tool is proxy that sits in front of two MPD protocol aware servers and +sends all requests to both, returning the primary response to the client and +then printing any diff in the two responses. + +Note that this tool depends on ``gevent`` unlike the rest of Mopidy at the time +of writing. See ``--help`` for available options. Sample session:: + + [127.0.0.1]:59714 + listallinfo + --- Reference response + +++ Actual response + @@ -1,16 +1,1 @@ + -file: uri1 + -Time: 4 + -Artist: artist1 + -Title: track1 + -Album: album1 + -file: uri2 + -Time: 4 + -Artist: artist2 + -Title: track2 + -Album: album2 + -file: uri3 + -Time: 4 + -Artist: artist3 + -Title: track3 + -Album: album3 + -OK + +ACK [2@0] {listallinfo} incorrect arguments + +To ensure that Mopidy and MPD have comparable state it is suggested you setup +both to use ``tests/data/library_tag_cache`` for their tag cache and +``tests/data`` for music/playlist folders. Writing documentation ===================== From ee599b6235aff2572604580e5ea0c57438571e29 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 9 Sep 2012 23:57:06 +0200 Subject: [PATCH 03/44] Add changelog entry for #72 and remove old comments. --- docs/changes.rst | 3 +++ mopidy/backends/spotify/library.py | 7 ++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 57224300..082ad136 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -68,6 +68,9 @@ v0.8 (in development) cleanup code would wait for an response that would never come inside the event loop, blocking everything else. +- Created a Spotify track proxy that will switch to using loaded data as soon + as it becomes available. Fixes :issue:`72`. + v0.7.3 (2012-08-11) =================== diff --git a/mopidy/backends/spotify/library.py b/mopidy/backends/spotify/library.py index 181dc19d..18276ecd 100644 --- a/mopidy/backends/spotify/library.py +++ b/mopidy/backends/spotify/library.py @@ -11,6 +11,7 @@ logger = logging.getLogger('mopidy.backends.spotify.library') class SpotifyTrack(Track): + """Proxy object for unloaded Spotify tracks.""" def __init__(self, uri): self._spotify_track = Link.from_string(uri).as_track() self._unloaded_track = Track(uri=uri, name=u'[loading...]') @@ -34,15 +35,15 @@ class SpotifyTrack(Track): def __repr__(self): return self._proxy.__repr__() - def __hash__(self): # hash on just uri for consistency? + def __hash__(self): return hash(self._proxy.uri) - def __eq__(self, other): # compare on just uri for consistency? + def __eq__(self, other): if not isinstance(other, Track): return False return self._proxy.uri == other.uri - def copy(self, **values): # is it okay to return a plain track? + def copy(self, **values): return self._proxy.copy(**values) From 94fdac04a12348ca1efa1427d6a18414008b8f24 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 10 Sep 2012 00:31:03 +0200 Subject: [PATCH 04/44] Cleanup after GStreamer actor Unregister callbacks and release pipeline resources when GStreamer actor shuts down. Fixes #185. --- mopidy/gstreamer.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index d9157a02..c25dde47 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -47,6 +47,10 @@ class GStreamer(ThreadingActor): self._setup_mixer() self._setup_message_processor() + def on_stop(self): + self._teardown_message_processor() + self._teardown_pipeline() + def _setup_pipeline(self): # TODO: replace with and input bin so we simply have an input bin we # connect to an output bin with a mixer on the side. set_uri on bin? @@ -65,6 +69,9 @@ class GStreamer(ThreadingActor): self._uridecodebin.connect('pad-added', self._on_new_pad, self._pipeline.get_by_name('queue').get_pad('sink')) + def _teardown_pipeline(self): + self._pipeline.set_state(gst.STATE_NULL) + def _setup_output(self): # This will raise a gobject.GError if the description is bad. self._output = gst.parse_bin_from_description( @@ -118,6 +125,10 @@ class GStreamer(ThreadingActor): bus.add_signal_watch() bus.connect('message', self._on_message) + def _teardown_message_processor(self): + bus = self._pipeline.get_bus() + bus.remove_signal_watch() + def _on_new_source(self, element, pad): self._source = element.get_property('source') try: From 1c4ee46c4cf382bcc111e09b4c511743d4d8dbab Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 10 Sep 2012 00:31:41 +0200 Subject: [PATCH 05/44] Move GStreamer setup back into the actor thread Make sure to terminate the whole process on GError exceptions, so that we fail quickly on non-working output pipelines. --- mopidy/gstreamer.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index c25dde47..a1ddc93e 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -1,6 +1,7 @@ import pygst pygst.require('0.10') import gst +import gobject import logging @@ -9,7 +10,10 @@ from pykka.registry import ActorRegistry from mopidy import settings, utils from mopidy.backends.base import Backend -from mopidy import mixers # Trigger install of gst mixer plugins. +from mopidy.utils import process + +# Trigger install of gst mixer plugins +from mopidy import mixers logger = logging.getLogger('mopidy.gstreamer') @@ -42,10 +46,15 @@ class GStreamer(ThreadingActor): self._output = None self._mixer = None - self._setup_pipeline() - self._setup_output() - self._setup_mixer() - self._setup_message_processor() + def on_start(self): + try: + self._setup_pipeline() + self._setup_output() + self._setup_mixer() + self._setup_message_processor() + except gobject.GError as ex: + logger.exception(ex) + process.exit_process() def on_stop(self): self._teardown_message_processor() From 4bffea8b1f44c21e5e10ba9b2029d669791642bb Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 10 Sep 2012 00:32:36 +0200 Subject: [PATCH 06/44] Test the GStreamer class as an actor The test should use the same interface and code paths as production code. --- tests/gstreamer_test.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/tests/gstreamer_test.py b/tests/gstreamer_test.py index 790394f5..ce20d2b4 100644 --- a/tests/gstreamer_test.py +++ b/tests/gstreamer_test.py @@ -14,9 +14,10 @@ class GStreamerTest(unittest.TestCase): settings.MIXER = 'fakemixer track_max_volume=65536' settings.OUTPUT = 'fakesink' self.song_uri = path_to_uri(path_to_data_dir('song1.wav')) - self.gstreamer = GStreamer() + self.gstreamer = GStreamer.start().proxy() def tearDown(self): + self.gstreamer.stop() settings.runtime.clear() def prepare_uri(self, uri): @@ -25,21 +26,21 @@ class GStreamerTest(unittest.TestCase): def test_start_playback_existing_file(self): self.prepare_uri(self.song_uri) - self.assertTrue(self.gstreamer.start_playback()) + self.assertTrue(self.gstreamer.start_playback().get()) def test_start_playback_non_existing_file(self): self.prepare_uri(self.song_uri + 'bogus') - self.assertFalse(self.gstreamer.start_playback()) + self.assertFalse(self.gstreamer.start_playback().get()) def test_pause_playback_while_playing(self): self.prepare_uri(self.song_uri) self.gstreamer.start_playback() - self.assertTrue(self.gstreamer.pause_playback()) + self.assertTrue(self.gstreamer.pause_playback().get()) def test_stop_playback_while_playing(self): self.prepare_uri(self.song_uri) self.gstreamer.start_playback() - self.assertTrue(self.gstreamer.stop_playback()) + self.assertTrue(self.gstreamer.stop_playback().get()) @unittest.SkipTest def test_deliver_data(self): @@ -51,8 +52,8 @@ class GStreamerTest(unittest.TestCase): def test_set_volume(self): for value in range(0, 101): - self.assertTrue(self.gstreamer.set_volume(value)) - self.assertEqual(value, self.gstreamer.get_volume()) + self.assertTrue(self.gstreamer.set_volume(value).get()) + self.assertEqual(value, self.gstreamer.get_volume().get()) @unittest.SkipTest def test_set_state_encapsulation(self): From 2f33a6c4ffd780ac9c2fe4d881f516e265032d9a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 10 Sep 2012 23:19:40 +0200 Subject: [PATCH 07/44] Only remove signal watch if it has been added If removing unconditionally, we get the following error message: GStreamer-CRITICAL **: Bus bus2 has no signal watches attached --- mopidy/gstreamer.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index a1ddc93e..b8b30d14 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -32,6 +32,7 @@ class GStreamer(ThreadingActor): def __init__(self): super(GStreamer, self).__init__() + self._default_caps = gst.Caps(""" audio/x-raw-int, endianness=(int)1234, @@ -40,12 +41,15 @@ class GStreamer(ThreadingActor): depth=(int)16, signed=(boolean)true, rate=(int)44100""") + self._pipeline = None self._source = None self._uridecodebin = None self._output = None self._mixer = None + self._message_processor_set_up = False + def on_start(self): try: self._setup_pipeline() @@ -133,10 +137,12 @@ class GStreamer(ThreadingActor): bus = self._pipeline.get_bus() bus.add_signal_watch() bus.connect('message', self._on_message) + self._message_processor_set_up = True def _teardown_message_processor(self): - bus = self._pipeline.get_bus() - bus.remove_signal_watch() + if self._message_processor_set_up: + bus = self._pipeline.get_bus() + bus.remove_signal_watch() def _on_new_source(self, element, pad): self._source = element.get_property('source') From 0e56b15fccbfeadfca13e459197e7102861b6386 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 11 Sep 2012 10:08:59 +0200 Subject: [PATCH 08/44] Teardown GStreamer mixer --- mopidy/gstreamer.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index b8b30d14..5bdb7b39 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -62,6 +62,7 @@ class GStreamer(ThreadingActor): def on_stop(self): self._teardown_message_processor() + self._teardown_mixer() self._teardown_pipeline() def _setup_pipeline(self): @@ -133,6 +134,11 @@ class GStreamer(ThreadingActor): gst.interfaces.MIXER_TRACK_OUTPUT): return track + def _teardown_mixer(self): + if self._mixer is not None: + (mixer, track) = self._mixer + mixer.set_state(gst.STATE_NULL) + def _setup_message_processor(self): bus = self._pipeline.get_bus() bus.add_signal_watch() From 51256e18949bbb2acdc9fac97e934511bcfc831b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 11 Sep 2012 11:18:08 +0200 Subject: [PATCH 09/44] Log warning message if mixer creation fails --- mopidy/gstreamer.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index 5bdb7b39..2ebe2d71 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -101,8 +101,12 @@ class GStreamer(ThreadingActor): logger.info('Not setting up mixer.') return - # This will raise a gobject.GError if the description is bad. - mixerbin = gst.parse_bin_from_description(settings.MIXER, False) + try: + mixerbin = gst.parse_bin_from_description(settings.MIXER, False) + except gobject.GError as ex: + logger.warning('Failed to create mixer "%s": %s', + settings.MIXER, ex) + return # We assume that the bin will contain a single mixer. mixer = mixerbin.get_by_interface('GstMixer') From a1146c2964162ef4cb71e297688dfe34c7ec9797 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 11 Sep 2012 13:52:43 +0200 Subject: [PATCH 10/44] Log error and exit if output creation fails --- mopidy/gstreamer.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index 2ebe2d71..f131fdfc 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -87,9 +87,14 @@ class GStreamer(ThreadingActor): self._pipeline.set_state(gst.STATE_NULL) def _setup_output(self): - # This will raise a gobject.GError if the description is bad. - self._output = gst.parse_bin_from_description( - settings.OUTPUT, ghost_unconnected_pads=True) + try: + self._output = gst.parse_bin_from_description( + settings.OUTPUT, ghost_unconnected_pads=True) + except gobject.GError as ex: + logger.error('Failed to create output "%s": %s', + settings.OUTPUT, ex) + process.exit_process() + return self._pipeline.add(self._output) gst.element_link_many(self._pipeline.get_by_name('queue'), From a3ea1bc97a0781c3cb845244b1fd042c1767633f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 11 Sep 2012 15:37:46 +0200 Subject: [PATCH 11/44] Use kwarg to make meaning obvious --- mopidy/gstreamer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index f131fdfc..6dc7b0aa 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -107,7 +107,8 @@ class GStreamer(ThreadingActor): return try: - mixerbin = gst.parse_bin_from_description(settings.MIXER, False) + mixerbin = gst.parse_bin_from_description(settings.MIXER, + ghost_unconnected_pads=False) except gobject.GError as ex: logger.warning('Failed to create mixer "%s": %s', settings.MIXER, ex) From dbf7030d5b5b4969272ee66d22bb5759efe0b4d8 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 11 Sep 2012 23:23:36 +0200 Subject: [PATCH 12/44] Fix crash in local backend when looking up unknown path --- docs/changes.rst | 2 ++ mopidy/backends/local/__init__.py | 3 ++- tests/backends/base/library.py | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 082ad136..77d72383 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -71,6 +71,8 @@ v0.8 (in development) - Created a Spotify track proxy that will switch to using loaded data as soon as it becomes available. Fixes :issue:`72`. +- Fixed crash on lookup of unknown path when using local backend. + v0.7.3 (2012-08-11) =================== diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index 1b1f9730..263d2fc2 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -203,7 +203,8 @@ class LocalLibraryProvider(BaseLibraryProvider): try: return self._uri_mapping[uri] except KeyError: - raise LookupError('%s not found.' % uri) + logger.debug(u'Failed to lookup "%s"', uri) + return None def find_exact(self, **query): self._validate_query(query) diff --git a/tests/backends/base/library.py b/tests/backends/base/library.py index 4b3ef5c0..f76d9d75 100644 --- a/tests/backends/base/library.py +++ b/tests/backends/base/library.py @@ -34,8 +34,8 @@ class LibraryControllerTest(object): self.assertEqual(track, self.tracks[0]) def test_lookup_unknown_track(self): - test = lambda: self.library.lookup('fake uri') - self.assertRaises(LookupError, test) + track = self.library.lookup('fake uri') + self.assertEquals(track, None) def test_find_exact_no_hits(self): result = self.library.find_exact(track=['unknown track']) From 85d90bbabb628f858889709874d52fb07788121e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 12 Sep 2012 11:50:56 +0200 Subject: [PATCH 13/44] Cleanup settings docstrings --- mopidy/settings.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/mopidy/settings.py b/mopidy/settings.py index 72e805bf..0612fc24 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -1,5 +1,5 @@ """ -Available settings and their default values. +All available settings and their default values. .. warning:: @@ -14,6 +14,10 @@ Available settings and their default values. #: #: BACKENDS = (u'mopidy.backends.spotify.SpotifyBackend',) #: +#: Other typical values:: +#: +#: BACKENDS = (u'mopidy.backends.local.LocalBackend',) +#: #: .. note:: #: Currently only the first backend in the list is used. BACKENDS = ( @@ -106,9 +110,9 @@ LOCAL_TAG_CACHE_FILE = None #: Sound mixer to use. #: #: Expects a GStreamer mixer to use, typical values are: -#: alsamixer, pulsemixer, oss4mixer, ossmixer. +#: ``alsamixer``, ``pulsemixer``, ``ossmixer``, and ``oss4mixer``. #: -#: Setting this to ``None`` means no volume control. +#: Setting this to :class:`None` turns off volume control. #: #: Default:: #: @@ -118,7 +122,7 @@ MIXER = u'autoaudiomixer' #: Sound mixer track to use. #: #: Name of the mixer track to use. If this is not set we will try to find the -#: output track with master set. As an example, using ``alsamixer`` you would +#: master output track. As an example, using ``alsamixer`` you would #: typically set this to ``Master`` or ``PCM``. #: #: Default:: @@ -128,7 +132,9 @@ MIXER_TRACK = None #: Which address Mopidy's MPD server should bind to. #: -#:Examples: +#: Used by :mod:`mopidy.frontends.mpd`. +#: +#: Examples: #: #: ``127.0.0.1`` #: Listens only on the IPv4 loopback interface. Default. @@ -142,16 +148,22 @@ MPD_SERVER_HOSTNAME = u'127.0.0.1' #: Which TCP port Mopidy's MPD server should listen to. #: +#: Used by :mod:`mopidy.frontends.mpd`. +#: #: Default: 6600 MPD_SERVER_PORT = 6600 #: The password required for connecting to the MPD server. #: +#: Used by :mod:`mopidy.frontends.mpd`. +#: #: Default: :class:`None`, which means no password required. MPD_SERVER_PASSWORD = None #: The maximum number of concurrent connections the MPD server will accept. #: +#: Used by :mod:`mopidy.frontends.mpd`. +#: #: Default: 20 MPD_SERVER_MAX_CONNECTIONS = 20 From 414e7774a961d08bbb5df4900ef91df8cd615026 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 12 Sep 2012 12:06:30 +0200 Subject: [PATCH 14/44] Add .mailmap for mapping of git Author tags belonging to the same person --- .mailmap | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .mailmap diff --git a/.mailmap b/.mailmap new file mode 100644 index 00000000..15d8f359 --- /dev/null +++ b/.mailmap @@ -0,0 +1,4 @@ +Kristian Klette +Johannes Knutsen +Johannes Knutsen +John Bäckstrand From 5ad75b18dea7f50aebaec6a119e3a5b471844004 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 12 Sep 2012 12:08:19 +0200 Subject: [PATCH 15/44] Add link to contributors list at GitHub --- docs/authors.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/authors.rst b/docs/authors.rst index af84f842..04795ee6 100644 --- a/docs/authors.rst +++ b/docs/authors.rst @@ -9,6 +9,9 @@ Contributors to Mopidy in the order of appearance: - Thomas Adamcik - Kristian Klette +A complete list of persons with commits accepted into the Mopidy repo can be +found at `GitHub `_. + Showing your appreciation ========================= From c8b24303ca4d83b41c34f436e28e8b3212127dfa Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 12 Sep 2012 12:08:35 +0200 Subject: [PATCH 16/44] Remove sections about donations --- docs/authors.rst | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/docs/authors.rst b/docs/authors.rst index 04795ee6..822abc15 100644 --- a/docs/authors.rst +++ b/docs/authors.rst @@ -20,13 +20,3 @@ If you already enjoy Mopidy, or don't enjoy it and want to help us making Mopidy better, the best way to do so is to contribute back to the community. You can contribute code, documentation, tests, bug reports, or help other users, spreading the word, etc. - -If you want to show your appreciation in a less time consuming way, you can -`flattr us `_, or `donate money -`_ to Mopidy's development. - -We promise that any money donated -- to Pledgie, not Flattr, due to the size of -the amounts -- will be used to cover costs related to Mopidy development, like -service subscriptions (Spotify, Last.fm, etc.) and hardware devices like an -used iPod Touch for testing Mopidy with MPod. - From 91b2c0f43033655359ab2a6fd9c07a5012088a73 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 12 Sep 2012 13:41:46 +0200 Subject: [PATCH 17/44] Update Homebrew installation guide --- docs/installation/gstreamer.rst | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/docs/installation/gstreamer.rst b/docs/installation/gstreamer.rst index 546b53ba..74b5e66b 100644 --- a/docs/installation/gstreamer.rst +++ b/docs/installation/gstreamer.rst @@ -54,15 +54,8 @@ Python bindings on OS X using Homebrew. #. Install `Homebrew `_. -#. Download our Homebrew formulas for ``pycairo``, ``pygobject``, ``pygtk``, - and ``gst-python``:: +#. Download our Homebrew formula for ``gst-python``:: - curl -o $(brew --prefix)/Library/Formula/pycairo.rb \ - https://raw.github.com/jodal/homebrew/gst-python/Library/Formula/pycairo.rb - curl -o $(brew --prefix)/Library/Formula/pygobject.rb \ - https://raw.github.com/jodal/homebrew/gst-python/Library/Formula/pygobject.rb - curl -o $(brew --prefix)/Library/Formula/pygtk.rb \ - https://raw.github.com/jodal/homebrew/gst-python/Library/Formula/pygtk.rb curl -o $(brew --prefix)/Library/Formula/gst-python.rb \ https://raw.github.com/jodal/homebrew/gst-python/Library/Formula/gst-python.rb @@ -77,13 +70,13 @@ Python bindings on OS X using Homebrew. You can either amend your ``PYTHONPATH`` permanently, by adding the following statement to your shell's init file, e.g. ``~/.bashrc``:: - export PYTHONPATH=$(brew --prefix)/lib/python2.6/site-packages:$PYTHONPATH + export PYTHONPATH=$(brew --prefix)/lib/python2.7/site-packages:$PYTHONPATH Or, you can prefix the Mopidy command every time you run it:: - PYTHONPATH=$(brew --prefix)/lib/python2.6/site-packages mopidy + PYTHONPATH=$(brew --prefix)/lib/python2.7/site-packages mopidy - Note that you need to replace ``python2.6`` with ``python2.7`` if that's + Note that you need to replace ``python2.7`` with ``python2.6`` if that's the Python version you are using. To find your Python version, run:: python --version From 8d44f4697bd82e80adfcee0511e6a5d68c42966f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 10 Sep 2012 18:54:28 +0200 Subject: [PATCH 18/44] Fix typos, clarify docs --- docs/settings.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/settings.rst b/docs/settings.rst index 94f3c63b..0c1a3c7e 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -166,9 +166,9 @@ server simultaneously. To use the SHOUTcast output, do the following: example, to set the username and password, use: ``lame ! shout2send username="foobar" password="s3cret"``. -Other advanced setups are also possible for outputs. Basically anything you can -get a ``gst-lauch`` command to output to can be plugged into -:attr:`mopidy.settings.OUTPUT``. +Other advanced setups are also possible for outputs. Basically, anything you +can use with the ``gst-launch-0.10`` command can be plugged into +:attr:`mopidy.settings.OUTPUT`. Available settings From 3f923907cab105c3586592c08469e5987e5585bb Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 12 Sep 2012 20:26:59 +0200 Subject: [PATCH 19/44] docs: Fix capitalization of GStreamer --- docs/changes.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 77d72383..7f67973d 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -951,7 +951,7 @@ Since the previous release Mopidy has seen about 300 commits, more than 200 new tests, a libspotify release, and major feature additions to Spotify. The new releases from Spotify have lead to updates to our dependencies, and also to new bugs in Mopidy. Thus, this is primarily a bugfix release, even though the not -yet finished work on a Gstreamer backend have been merged. +yet finished work on a GStreamer backend have been merged. All users are recommended to upgrade to 0.1.0a1, and should at the same time ensure that they have the latest versions of our dependencies: Despotify r508 @@ -976,7 +976,7 @@ As always, report problems at our IRC channel or our issue tracker. Thanks! - Several new generic features, like shuffle, consume, and playlist repeat. (Fixes: :issue:`3`) - **[Work in Progress]** A new backend for playing music from a local music - archive using the Gstreamer library. + archive using the GStreamer library. - Made :class:`mopidy.mixers.alsa.AlsaMixer` work on machines without a mixer named "Master". From 061adbddd0f9941d964997480102bca0030c2983 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 12 Sep 2012 20:27:12 +0200 Subject: [PATCH 20/44] docs: Remove superflous word --- docs/installation/gstreamer.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/gstreamer.rst b/docs/installation/gstreamer.rst index 74b5e66b..42685ad0 100644 --- a/docs/installation/gstreamer.rst +++ b/docs/installation/gstreamer.rst @@ -2,7 +2,7 @@ GStreamer installation ********************** -To use the Mopidy, you first need to install GStreamer and the GStreamer Python +To use Mopidy, you first need to install GStreamer and the GStreamer Python bindings. From a08b1461ad884ef80da5e24693dcad91926cfc67 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 12 Sep 2012 20:34:00 +0200 Subject: [PATCH 21/44] docs: Remove note about Ubuntu 10.04 and older --- docs/clients/mpd.rst | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/docs/clients/mpd.rst b/docs/clients/mpd.rst index 844eaee7..64bd304b 100644 --- a/docs/clients/mpd.rst +++ b/docs/clients/mpd.rst @@ -30,20 +30,13 @@ ncmpcpp A console client that generally works well with Mopidy, and is regularly used by Mopidy developers. -Search -^^^^^^ - -Search only works for ncmpcpp versions 0.5.1 and higher, and in two of the -three search modes: +Search only works in two of the three search modes: - "Match if tag contains search phrase (regexes supported)" -- Does not work. The client tries to fetch all known metadata and do the search client side. - "Match if tag contains searched phrase (no regexes)" -- Works. - "Match only if both values are the same" -- Works. -If you run Ubuntu 10.04 or older, you can fetch an updated version of ncmpcpp -from `Launchpad `_. - Communication mode ^^^^^^^^^^^^^^^^^^ From e304b85c87398387782a25491b14a81ff61d8ed6 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 12 Sep 2012 20:35:25 +0200 Subject: [PATCH 22/44] docs: Remove note about how to use ncmpcpp with Mopidy < 0.6, which is almost a year ago --- docs/clients/mpd.rst | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/docs/clients/mpd.rst b/docs/clients/mpd.rst index 64bd304b..d35b87bc 100644 --- a/docs/clients/mpd.rst +++ b/docs/clients/mpd.rst @@ -37,19 +37,6 @@ Search only works in two of the three search modes: - "Match if tag contains searched phrase (no regexes)" -- Works. - "Match only if both values are the same" -- Works. -Communication mode -^^^^^^^^^^^^^^^^^^ - -In newer versions of ncmpcpp, like ncmpcpp 0.5.5 shipped with Ubuntu 11.04, -ncmcpp defaults to "notifications" mode for MPD communications, which Mopidy -did not support before Mopidy 0.6. To workaround this limitation in earlier -versions of Mopidy, edit the ncmpcpp configuration file at -``~/.ncmpcpp/config`` and add the following setting:: - - mpd_communication_mode = "polling" - -If you use Mopidy 0.6 or newer, you don't need to change anything. - Graphical clients ================= From 5b284af399858a9c545ccefb3194e67c333e6668 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 12 Sep 2012 21:52:23 +0200 Subject: [PATCH 23/44] Update Android MPD client review (fixes #155) --- docs/clients/mpd.rst | 206 +++++++++++++++++++++++++------------------ 1 file changed, 119 insertions(+), 87 deletions(-) diff --git a/docs/clients/mpd.rst b/docs/clients/mpd.rst index d35b87bc..e6137d7d 100644 --- a/docs/clients/mpd.rst +++ b/docs/clients/mpd.rst @@ -82,8 +82,8 @@ It generally works well with Mopidy. Android clients =============== -We've tested all six MPD clients we could find for Android with Mopidy 0.3 on a -HTC Hero with Android 2.1, using the following test procedure: +We've tested all four MPD clients we could find for Android with Mopidy 0.7.3 on +a Samsung Galaxy Nexus with Android 4.1.1, using the following test procedure: #. Connect to Mopidy #. Search for ``foo``, with search type "any" if it can be selected @@ -107,133 +107,165 @@ HTC Hero with Android 2.1, using the following test procedure: #. Check if the app got support for single mode and consume mode #. Kill Mopidy and confirm that the app handles it without crashing -In summary: +We found that all four apps crashed on Android 4.1.1. -- BitMPC lacks finishing touches on its user interface but supports all - features tested. -- Droid MPD Client works well, but got a couple of bugs one can live with and - does not expose stored playlist anywhere. -- IcyBeats is not usable yet. -- MPDroid is working well and looking good, but does not have search - functionality. -- PMix is just a lesser MPDroid, so use MPDroid instead. -- ThreeMPD is too buggy to even get connected to Mopidy. +Combining what we managed to find before the apps crashed with our experience +from an older version of this review, using Android 2.1, we can say that: -Our recommendation: +- PMix can be ignored, because it is unmaintained and its fork MPDroid is + better on all fronts. -- If you do not care about looks, use BitMPC. -- If you do not care about stored playlists, use Droid MPD Client. -- If you do not care about searching, use MPDroid. +- Droid MPD Client was to buggy to get an impression from. Unclear if the bugs + are due to the app or that it hasn't been updated for Android 4.x. + +- BitMPC is in our experience feature complete, but ugly. + +- MPDroid, now that search is in place, is probably feature complete as well, + and looks nicer than BitMPC. + +In conclusion: MPD clients on Android 4.x is a sad affair. If you want to try +anyway, try BitMPC and MPDroid. BitMPC ------ -We tested version 1.0.0, which at the time had 1k-5k downloads, <100 ratings, -3.5 stars. +Test date: + 2012-09-12 +Tested version: + 1.0.0 (released 2010-04-12) +Downloads: + 5,000+ +Rating: + 3.7 stars from about 100 ratings -The user interface lacks some finishing touches. E.g. you can't enter a -hostname for the server. Only IPv4 addresses are allowed. -All features exercised in the test procedure works. BitMPC lacks support for -single mode and consume mode. BitMPC crashes if Mopidy is killed or crash. +- The user interface lacks some finishing touches. E.g. you can't enter a + hostname for the server. Only IPv4 addresses are allowed. + +- When we last tested the same version of BitMPC using Android 2.1: + + - All features exercised in the test procedure worked. + + - BitMPC lacked support for single mode and consume mode. + + - BitMPC crashed if Mopidy was killed or crashed. + +- When we tried to test using Android 4.1.1, BitMPC started and connected to + Mopidy without problems, but the app crashed as soon as fire off our search, + and continued to crash on startup after that. + +In conclusion, BitMPC is usable if you got an older Android phone and don't +care about looks. For newer Android versions, BitMPC will probably not work as +it hasn't been maintained for 2.5 years. Droid MPD Client ---------------- -We tested version 0.4.0, which at the time had 5k-10k downloads, >200 ratings, -4 stars. +Test date: + 2012-09-12 +Tested version: + 1.4.0 (released 2011-12-20) +Downloads: + 10,000+ +Rating: + 4.2 stars from 400+ ratings -To find the search functionality, you have to select the menu, then "Playlist -manager", then the search tab. I do not understand why search is hidden inside -"Playlist manager". +- No intutive way to ask the app to connect to the server after adding the + server hostname to the settings. -The user interface have some French remnants, like "Rechercher" in the search -field. +- To find the search functionality, you have to select the menu, + then "Playlist manager", then the search tab. I do not understand why search + is hidden inside "Playlist manager". -When selecting the artist tab, it issues the ``list Artist`` command and -becomes stuck waiting for the results. Same thing happens for the album tab, -which issues ``list Album``, and the folder tab, which issues ``lsinfo``. -Mopidy returned zero hits immediately on all three commands. If Mopidy has -loaded your stored playlists and returns more than zero hits on these commands, -they artist and album tabs do not hang. The folder tab still freezes when -``lsinfo`` returns a list of stored playlists, though zero files. Thus, we've -discovered a couple of bugs in Droid MPD Client. +- The tabs "Artists" and "Albums" did not contain anything, and did not cause + any requests. -Even though ``lsinfo`` returns the stored playlists for the folder tab, they -are not displayed anywhere. Thus, we had to select an album in the album tab to -complete the test procedure. +- The tab "Folders" showed a spinner and said "Updating data..." but did not + send any requests. -At one point, I had problems turning off repeat mode. After I adjusted the -volume and tried again, it worked. +- Searching for "foo" did nothing. No request was sent to the server. -Droid MPD client does not support single mode or consume mode. It does not -detect that the server is killed/crashed. You'll only notice it by no actions -having any effect, e.g. you can't turn the volume knob any more. +- Once, I managed to get a list of stored playlists in the "Search" tab, but I + never managed to reproduce this. Opening the stored playlists doesn't work, + because Mopidy haven't implemented ``lsinfo "Playlist name"`` (see + :issue:`193`). -In conclusion, some bugs and caveats, but most of the test procedure was -possible to perform. +- Droid MPD client does not support single mode or consume mode. +- Not able to complete the test procedure, due to the above problems. -IcyBeats --------- - -We tested version 0.2, which at the time had 50-100 downloads, no ratings. -The app was still in beta when we tried it. - -IcyBeats successfully connected to Mopidy and I was able to adjust volume. When -I was searching for some tracks, I could not figure out how to actually start -the search, as there was no search button and pressing enter in the input field -just added a new line. I was stuck. In other words, IcyBeats 0.2 is not usable -with Mopidy. - -IcyBeats does have something going for it: IcyBeats uses IPv6 to connect to -Mopidy. The future is just around the corner! +In conclusion, not a client we can recommend. MPDroid ------- -We tested version 0.6.9, which at the time had 5k-10k downloads, <200 ratings, -4.5 stars. MPDroid started out as a fork of PMix. +Test date: + 2012-09-12 +Tested version: + 0.7 (released 2011-06-19) +Downloads: + 10,000+ +Rating: + 4.5 stars from ~500 ratings -First of all, MPDroid's user interface looks nice. +- MPDroid started out as a fork of PMix. -I couldn't find any search functionality, so I added the initial track using -another client. Other than the missing search functionality, everything in the -test procedure worked out flawlessly. Like all other Android clients, MPDroid -does not support single mode or consume mode. When Mopidy is killed, MPDroid -handles it gracefully and asks if you want to try to reconnect. +- First of all, MPDroid's user interface looks nice. -All in all, MPDroid is a good MPD client without search support. +- Last time we tested MPDroid (v0.6.9), we couldn't find any search + functionality. Now we found it, and it worked. + +- Last time we tested MPDroid (v0.6.9) everything in the test procedure worked + out flawlessly. + +- Like all other Android clients, MPDroid does not support single mode or + consume mode. + +- When Mopidy is killed, MPDroid handles it gracefully and asks if you want to + try to reconnect. + +- When using Android 4.1.1, MPDroid crashes here and there, e.g. when having an + empty current playlist and pressing play. + +Disregarding Adnroid 4.x problems, MPDroid is a good MPD client. PMix ---- -We tested version 0.4.0, which at the time had 10k-50k downloads, >200 ratings, -4 stars. +Test date: + 2012-09-12 +Tested version: + 0.4.0 (released 2010-03-06) +Downloads: + 10,000+ +Rating: + 3.8 stars from >200 ratings -Add MPDroid is a fork from PMix, it is no surprise that PMix does not support -search either. In addition, I could not find stored playlists. Other than that, -I was able to complete the test procedure. PMix crashed once during testing, -but handled the killing of Mopidy just as nicely as MPDroid. It does not -support single mode or consume mode. +- Using Android 4.1.1, PMix, which haven't been updated for 2.5 years, crashes + as soon as it connects to Mopidy. + +- Last time we tested the same version of PMix using Android 2.1, we found + that: + + - PMix does not support search. + + - I could not find stored playlists. + + - Other than that, I was able to complete the test procedure. + + - PMix crashed once during testing. + + - PMix handled the killing of Mopidy just as nicely as MPDroid. + + - It does not support single mode or consume mode. All in all, PMix works but can do less than MPDroid. Use MPDroid instead. -ThreeMPD --------- - -We tested version 0.3.0, which at the time had 1k-5k downloads, <25 ratings, -2.5 average. The developer request users to use MPDroid instead, due to limited -time for maintenance. Does not support password authentication. - -ThreeMPD froze during startup, so we were not able to test it. - - .. _ios_mpd_clients: iPhone/iPod Touch clients From fd76b46d5ea072f8ec37599dc032cf0e4bdafc01 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 12 Sep 2012 22:11:29 +0200 Subject: [PATCH 24/44] docs: Fix typo --- docs/clients/mpd.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/clients/mpd.rst b/docs/clients/mpd.rst index e6137d7d..6d0cc6a7 100644 --- a/docs/clients/mpd.rst +++ b/docs/clients/mpd.rst @@ -230,7 +230,7 @@ Rating: - When using Android 4.1.1, MPDroid crashes here and there, e.g. when having an empty current playlist and pressing play. -Disregarding Adnroid 4.x problems, MPDroid is a good MPD client. +Disregarding Android 4.x problems, MPDroid is a good MPD client. PMix From a703b1986260fe12fd2ec3a1c9f19fff6077e9b1 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 12 Sep 2012 22:20:00 +0200 Subject: [PATCH 25/44] docs: Update iOS clients section --- docs/clients/mpd.rst | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/docs/clients/mpd.rst b/docs/clients/mpd.rst index 6d0cc6a7..c7dc3799 100644 --- a/docs/clients/mpd.rst +++ b/docs/clients/mpd.rst @@ -268,23 +268,19 @@ All in all, PMix works but can do less than MPDroid. Use MPDroid instead. .. _ios_mpd_clients: -iPhone/iPod Touch clients -========================= - -impdclient ----------- - -There's an open source MPD client for iOS called `impdclient -`_ which has not seen any updates since -August 2008. So far, we've not heard of users trying it with Mopidy. Please -notify us of your successes and/or problems if you do try it out. - +iOS clients +=========== MPod ---- -The `MPoD `_ client can be -installed from the `iTunes Store +Test date: + 2011-01-19 +Tested version: + 1.5.1 + +The `MPoD `_ iPhone/iPod Touch +app can be installed from the `iTunes Store `_. Users have reported varying success in using MPoD together with Mopidy. Thus, @@ -328,3 +324,10 @@ we've tested a fresh install of MPoD 1.5.1 with Mopidy as of revision e7ed28d - **Wishlist:** MPoD supports autodetection/-configuration of MPD servers through the use of Bonjour. Mopidy does not currently support this, but there is a wishlist bug at :issue:`39`. + + +MPaD +---- + +The `MPaD `_ iPad app works +with Mopidy. A complete review may appear here in the future. From 0ed59a884564c1a6ba15a69e34782b9c43c801a4 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 12 Sep 2012 22:54:34 +0200 Subject: [PATCH 26/44] gstreamer: Fix typo --- mopidy/gstreamer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index 6dc7b0aa..9d0cb97c 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -381,7 +381,7 @@ class GStreamer(ThreadingActor): deliver raw audio data to GStreamer. :param track: the current track - :type track: :class:`mopidy.modes.Track` + :type track: :class:`mopidy.models.Track` """ taglist = gst.TagList() artists = [a for a in (track.artists or []) if a.name] From 7525cad94c1084d9d2cdd4bb20c361bbe588d8d1 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 12 Sep 2012 23:06:06 +0200 Subject: [PATCH 27/44] Let Track.date be an ISO-8601 string This lets us have less precision than full dates. E.g. Spotify tracks only got release year, not full release date. The original MPD server regularly expose data like this as "Date: 1977", so we don't need to fake more precision for MPD's sake. --- docs/changes.rst | 3 +++ mopidy/backends/spotify/translator.py | 6 ++---- mopidy/frontends/mpd/protocol/music_db.py | 2 +- mopidy/models.py | 4 ++-- tests/models_test.py | 16 ++++++++-------- 5 files changed, 16 insertions(+), 15 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 7f67973d..de447dee 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -73,6 +73,9 @@ v0.8 (in development) - Fixed crash on lookup of unknown path when using local backend. +- Support tracks with only release year, and not a full release date, like e.g. + Spotify tracks. + v0.7.3 (2012-08-11) =================== diff --git a/mopidy/backends/spotify/translator.py b/mopidy/backends/spotify/translator.py index 2f47a42b..1a8f048d 100644 --- a/mopidy/backends/spotify/translator.py +++ b/mopidy/backends/spotify/translator.py @@ -1,4 +1,3 @@ -import datetime as dt import logging from spotify import Link, SpotifyError @@ -31,9 +30,8 @@ class SpotifyTranslator(object): if not spotify_track.is_loaded(): return Track(uri=uri, name=u'[loading...]') spotify_album = spotify_track.album() - if (spotify_album is not None and spotify_album.is_loaded() - and dt.MINYEAR <= int(spotify_album.year()) <= dt.MAXYEAR): - date = dt.date(spotify_album.year(), 1, 1) + if spotify_album is not None and spotify_album.is_loaded(): + date = spotify_album.year() else: date = None return Track( diff --git a/mopidy/frontends/mpd/protocol/music_db.py b/mopidy/frontends/mpd/protocol/music_db.py index 3cf20c5d..d0128a1e 100644 --- a/mopidy/frontends/mpd/protocol/music_db.py +++ b/mopidy/frontends/mpd/protocol/music_db.py @@ -242,7 +242,7 @@ def _list_date(context, query): playlist = context.backend.library.find_exact(**query).get() for track in playlist.tracks: if track.date is not None: - dates.add((u'Date', track.date.strftime('%Y-%m-%d'))) + dates.add((u'Date', track.date)) return dates @handle_request(r'^listall "(?P[^"]+)"') diff --git a/mopidy/models.py b/mopidy/models.py index 3363a429..6a2af914 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -157,8 +157,8 @@ class Track(ImmutableObject): :type album: :class:`Album` :param track_no: track number in album :type track_no: integer - :param date: track release date - :type date: :class:`datetime.date` + :param date: track release date (YYYY or YYYY-MM-DD) + :type date: string :param length: track length in milliseconds :type length: integer :param bitrate: bitrate in kbit/s diff --git a/tests/models_test.py b/tests/models_test.py index 231587e4..af90c5bd 100644 --- a/tests/models_test.py +++ b/tests/models_test.py @@ -338,7 +338,7 @@ class TrackTest(unittest.TestCase): self.assertRaises(AttributeError, setattr, track, 'track_no', None) def test_date(self): - date = datetime.date(1977, 1, 1) + date = '1977-01-01' track = Track(date=date) self.assertEqual(track.date, date) self.assertRaises(AttributeError, setattr, track, 'date', None) @@ -434,7 +434,7 @@ class TrackTest(unittest.TestCase): self.assertEqual(hash(track1), hash(track2)) def test_eq_date(self): - date = datetime.date.today() + date = '1977-01-01' track1 = Track(date=date) track2 = Track(date=date) self.assertEqual(track1, track2) @@ -459,7 +459,7 @@ class TrackTest(unittest.TestCase): self.assertEqual(hash(track1), hash(track2)) def test_eq(self): - date = datetime.date.today() + date = '1977-01-01' artists = [Artist()] album = Album() track1 = Track(uri=u'uri', name=u'name', artists=artists, album=album, @@ -508,8 +508,8 @@ class TrackTest(unittest.TestCase): self.assertNotEqual(hash(track1), hash(track2)) def test_ne_date(self): - track1 = Track(date=datetime.date.today()) - track2 = Track(date=datetime.date.today()-datetime.timedelta(days=1)) + track1 = Track(date='1977-01-01') + track2 = Track(date='1977-01-02') self.assertNotEqual(track1, track2) self.assertNotEqual(hash(track1), hash(track2)) @@ -534,12 +534,12 @@ class TrackTest(unittest.TestCase): def test_ne(self): track1 = Track(uri=u'uri1', name=u'name1', artists=[Artist(name=u'name1')], album=Album(name=u'name1'), - track_no=1, date=datetime.date.today(), length=100, bitrate=100, + track_no=1, date='1977-01-01', length=100, bitrate=100, musicbrainz_id='id1') track2 = Track(uri=u'uri2', name=u'name2', artists=[Artist(name=u'name2')], album=Album(name=u'name2'), - track_no=2, date=datetime.date.today()-datetime.timedelta(days=1), - length=200, bitrate=200, musicbrainz_id='id2') + track_no=2, date='1977-01-02', length=200, bitrate=200, + musicbrainz_id='id2') self.assertNotEqual(track1, track2) self.assertNotEqual(hash(track1), hash(track2)) From 0a0c7c59b7186df72eea3f2b099532ca05bc0bac Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 13 Sep 2012 15:17:10 +0200 Subject: [PATCH 28/44] docs: Merge development docs into a single document --- docs/changes.rst | 4 +- .../contributing.rst => development.rst} | 39 +++++++++++++++++-- docs/development/index.rst | 9 ----- docs/development/roadmap.rst | 34 ---------------- docs/index.rst | 2 +- docs/installation/index.rst | 2 +- 6 files changed, 40 insertions(+), 50 deletions(-) rename docs/{development/contributing.rst => development.rst} (81%) delete mode 100644 docs/development/index.rst delete mode 100644 docs/development/roadmap.rst diff --git a/docs/changes.rst b/docs/changes.rst index de447dee..c6b7e0ac 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -860,8 +860,8 @@ In the last two months, Mopidy's MPD frontend has gotten lots of stability fixes and error handling improvements, proper support for having the same track multiple times in a playlist, and support for IPv6. We have also fixed the choppy playback on the libspotify backend. For the road ahead of us, we got an -updated :doc:`release roadmap ` with our goals for the 0.1 -to 0.3 releases. +updated :doc:`release roadmap ` with our goals for the 0.1 to 0.3 +releases. Enjoy the best alpha relase of Mopidy ever :-) diff --git a/docs/development/contributing.rst b/docs/development.rst similarity index 81% rename from docs/development/contributing.rst rename to docs/development.rst index 74e2f0b5..c5020bd9 100644 --- a/docs/development/contributing.rst +++ b/docs/development.rst @@ -1,11 +1,42 @@ -***************** -How to contribute -***************** +*********** +Development +*********** Development of Mopidy is coordinated through the IRC channel ``#mopidy`` at ``irc.freenode.net`` and through `GitHub `_. +Release schedule +================ + +We intend to have about one timeboxed feature release every month +in periods of active development. The feature releases are numbered 0.x.0. The +features added is a mix of what we feel is most important/requested of the +missing features, and features we develop just because we find them fun to +make, even though they may be useful for very few users or for a limited use +case. + +Bugfix releases, numbered 0.x.y, will be released whenever we discover bugs +that are too serious to wait for the next feature release. We will only release +bugfix releases for the last feature release. E.g. when 0.3.0 is released, we +will no longer provide bugfix releases for the 0.2 series. In other words, +there will be just a single supported release at any point in time. + + +Feature wishlist +================ + +We maintain our collection of sane or less sane ideas for future Mopidy +features as `issues `_ at GitHub +labeled with `the "wishlist" label +`_. Feel free to vote +up any feature you would love to see in Mopidy, but please refrain from adding +a comment just to say "I want this too!". You are of course free to add +comments if you have suggestions for how the feature should work or be +implemented, and you may add new wishlist issues if your ideas are not already +represented. + + Code style ========== @@ -125,6 +156,7 @@ statistics and uses pylint to check for errors and possible improvements in our code. So, if you're out of work, the code coverage and pylint data at the CI server should give you a place to start. + Protocol debugging ================== @@ -167,6 +199,7 @@ To ensure that Mopidy and MPD have comparable state it is suggested you setup both to use ``tests/data/library_tag_cache`` for their tag cache and ``tests/data`` for music/playlist folders. + Writing documentation ===================== diff --git a/docs/development/index.rst b/docs/development/index.rst deleted file mode 100644 index 321b3242..00000000 --- a/docs/development/index.rst +++ /dev/null @@ -1,9 +0,0 @@ -*********** -Development -*********** - -.. toctree:: - :maxdepth: 3 - - roadmap - contributing diff --git a/docs/development/roadmap.rst b/docs/development/roadmap.rst deleted file mode 100644 index 6280762c..00000000 --- a/docs/development/roadmap.rst +++ /dev/null @@ -1,34 +0,0 @@ -******* -Roadmap -******* - - -Release schedule -================ - -We intend to have about one timeboxed feature release every month -in periods of active development. The feature releases are numbered 0.x.0. The -features added is a mix of what we feel is most important/requested of the -missing features, and features we develop just because we find them fun to -make, even though they may be useful for very few users or for a limited use -case. - -Bugfix releases, numbered 0.x.y, will be released whenever we discover bugs -that are too serious to wait for the next feature release. We will only release -bugfix releases for the last feature release. E.g. when 0.3.0 is released, we -will no longer provide bugfix releases for the 0.2 series. In other words, -there will be just a single supported release at any point in time. - - -Feature wishlist -================ - -We maintain our collection of sane or less sane ideas for future Mopidy -features as `issues `_ at GitHub -labeled with `the "wishlist" label -`_. Feel free to vote -up any feature you would love to see in Mopidy, but please refrain from adding -a comment just to say "I want this too!". You are of course free to add -comments if you have suggestions for how the feature should work or be -implemented, and you may add new wishlist issues if your ideas are not already -represented. diff --git a/docs/index.rst b/docs/index.rst index 7e757de0..0af510d0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -54,7 +54,7 @@ Development documentation .. toctree:: :maxdepth: 3 - development/index + development Indices and tables ================== diff --git a/docs/installation/index.rst b/docs/installation/index.rst index 766616ac..66b920f8 100644 --- a/docs/installation/index.rst +++ b/docs/installation/index.rst @@ -173,7 +173,7 @@ If you want to contribute to Mopidy, you should install Mopidy using Git. For an introduction to ``git``, please visit `git-scm.com `_. Also, please read our :doc:`developer documentation -`. +`. From AUR on ArchLinux From 0559213da326cbb4ccd5e2aa5bc5e547ef30f444 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 13 Sep 2012 23:40:45 +0200 Subject: [PATCH 29/44] Move backend controllers to mopidy.core --- mopidy/backends/base/__init__.py | 11 +- mopidy/backends/base/library.py | 76 --- mopidy/backends/base/playback.py | 547 ----------------- mopidy/backends/base/stored_playlists.py | 116 ---- mopidy/backends/dummy/__init__.py | 22 +- mopidy/backends/local/__init__.py | 25 +- mopidy/backends/spotify/__init__.py | 15 +- mopidy/core/__init__.py | 4 + .../base => core}/current_playlist.py | 4 +- mopidy/core/library.py | 70 +++ mopidy/core/playback.py | 548 ++++++++++++++++++ mopidy/core/stored_playlists.py | 113 ++++ mopidy/frontends/mpd/protocol/playback.py | 12 +- mopidy/frontends/mpd/protocol/status.py | 12 +- mopidy/frontends/mpris/objects.py | 17 +- tests/core/__init__.py | 0 tests/frontends/mpd/protocol/playback_test.py | 8 +- tests/frontends/mpd/status_test.py | 11 +- .../frontends/mpris/player_interface_test.py | 9 +- 19 files changed, 803 insertions(+), 817 deletions(-) create mode 100644 mopidy/core/__init__.py rename mopidy/{backends/base => core}/current_playlist.py (99%) create mode 100644 mopidy/core/library.py create mode 100644 mopidy/core/playback.py create mode 100644 mopidy/core/stored_playlists.py create mode 100644 tests/core/__init__.py diff --git a/mopidy/backends/base/__init__.py b/mopidy/backends/base/__init__.py index 76c7f078..e6c8b70a 100644 --- a/mopidy/backends/base/__init__.py +++ b/mopidy/backends/base/__init__.py @@ -1,12 +1,7 @@ -import logging +from .library import BaseLibraryProvider +from .playback import BasePlaybackProvider +from .stored_playlists import BaseStoredPlaylistsProvider -from .current_playlist import CurrentPlaylistController -from .library import LibraryController, BaseLibraryProvider -from .playback import PlaybackController, BasePlaybackProvider -from .stored_playlists import (StoredPlaylistsController, - BaseStoredPlaylistsProvider) - -logger = logging.getLogger('mopidy.backends.base') class Backend(object): #: The current playlist controller. An instance of diff --git a/mopidy/backends/base/library.py b/mopidy/backends/base/library.py index 9e3afe9a..837eef49 100644 --- a/mopidy/backends/base/library.py +++ b/mopidy/backends/base/library.py @@ -1,79 +1,3 @@ -import logging - -logger = logging.getLogger('mopidy.backends.base') - -class LibraryController(object): - """ - :param backend: backend the controller is a part of - :type backend: :class:`mopidy.backends.base.Backend` - :param provider: provider the controller should use - :type provider: instance of :class:`BaseLibraryProvider` - """ - - pykka_traversable = True - - def __init__(self, backend, provider): - self.backend = backend - self.provider = provider - - def find_exact(self, **query): - """ - Search the library for tracks where ``field`` is ``values``. - - Examples:: - - # Returns results matching 'a' - find_exact(any=['a']) - # Returns results matching artist 'xyz' - find_exact(artist=['xyz']) - # Returns results matching 'a' and 'b' and artist 'xyz' - find_exact(any=['a', 'b'], artist=['xyz']) - - :param query: one or more queries to search for - :type query: dict - :rtype: :class:`mopidy.models.Playlist` - """ - return self.provider.find_exact(**query) - - def lookup(self, uri): - """ - Lookup track with given URI. Returns :class:`None` if not found. - - :param uri: track URI - :type uri: string - :rtype: :class:`mopidy.models.Track` or :class:`None` - """ - return self.provider.lookup(uri) - - def refresh(self, uri=None): - """ - Refresh library. Limit to URI and below if an URI is given. - - :param uri: directory or track URI - :type uri: string - """ - return self.provider.refresh(uri) - - def search(self, **query): - """ - Search the library for tracks where ``field`` contains ``values``. - - Examples:: - - # Returns results matching 'a' - search(any=['a']) - # Returns results matching artist 'xyz' - search(artist=['xyz']) - # Returns results matching 'a' and 'b' and artist 'xyz' - search(any=['a', 'b'], artist=['xyz']) - - :param query: one or more queries to search for - :type query: dict - :rtype: :class:`mopidy.models.Playlist` - """ - return self.provider.search(**query) - - class BaseLibraryProvider(object): """ :param backend: backend the controller is a part of diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index dfcbe8bb..d2b9edd9 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -1,550 +1,3 @@ -import logging -import random -import time - -from mopidy.listeners import BackendListener - -logger = logging.getLogger('mopidy.backends.base') - - -def option_wrapper(name, default): - def get_option(self): - return getattr(self, name, default) - def set_option(self, value): - if getattr(self, name, default) != value: - self._trigger_options_changed() - return setattr(self, name, value) - return property(get_option, set_option) - - -class PlaybackController(object): - """ - :param backend: the backend - :type backend: :class:`mopidy.backends.base.Backend` - :param provider: provider the controller should use - :type provider: instance of :class:`BasePlaybackProvider` - """ - - # pylint: disable = R0902 - # Too many instance attributes - - pykka_traversable = True - - #: Constant representing the paused state. - PAUSED = u'paused' - - #: Constant representing the playing state. - PLAYING = u'playing' - - #: Constant representing the stopped state. - STOPPED = u'stopped' - - #: :class:`True` - #: Tracks are removed from the playlist when they have been played. - #: :class:`False` - #: Tracks are not removed from the playlist. - consume = option_wrapper('_consume', False) - - #: The currently playing or selected track. - #: - #: A two-tuple of (CPID integer, :class:`mopidy.models.Track`) or - #: :class:`None`. - current_cp_track = None - - #: :class:`True` - #: Tracks are selected at random from the playlist. - #: :class:`False` - #: Tracks are played in the order of the playlist. - random = option_wrapper('_random', False) - - #: :class:`True` - #: The current playlist is played repeatedly. To repeat a single track, - #: select both :attr:`repeat` and :attr:`single`. - #: :class:`False` - #: The current playlist is played once. - repeat = option_wrapper('_repeat', False) - - #: :class:`True` - #: Playback is stopped after current song, unless in :attr:`repeat` - #: mode. - #: :class:`False` - #: Playback continues after current song. - single = option_wrapper('_single', False) - - def __init__(self, backend, provider): - self.backend = backend - self.provider = provider - self._state = self.STOPPED - self._shuffled = [] - self._first_shuffle = True - self.play_time_accumulated = 0 - self.play_time_started = None - - def _get_cpid(self, cp_track): - if cp_track is None: - return None - return cp_track.cpid - - def _get_track(self, cp_track): - if cp_track is None: - return None - return cp_track.track - - @property - def current_cpid(self): - """ - The CPID (current playlist ID) of the currently playing or selected - track. - - Read-only. Extracted from :attr:`current_cp_track` for convenience. - """ - return self._get_cpid(self.current_cp_track) - - @property - def current_track(self): - """ - The currently playing or selected :class:`mopidy.models.Track`. - - Read-only. Extracted from :attr:`current_cp_track` for convenience. - """ - return self._get_track(self.current_cp_track) - - @property - def current_playlist_position(self): - """ - The position of the current track in the current playlist. - - Read-only. - """ - if self.current_cp_track is None: - return None - try: - return self.backend.current_playlist.cp_tracks.index( - self.current_cp_track) - except ValueError: - return None - - @property - def track_at_eot(self): - """ - The track that will be played at the end of the current track. - - Read-only. A :class:`mopidy.models.Track` extracted from - :attr:`cp_track_at_eot` for convenience. - """ - return self._get_track(self.cp_track_at_eot) - - @property - def cp_track_at_eot(self): - """ - The track that will be played at the end of the current track. - - Read-only. A two-tuple of (CPID integer, :class:`mopidy.models.Track`). - - Not necessarily the same track as :attr:`cp_track_at_next`. - """ - # pylint: disable = R0911 - # Too many return statements - - cp_tracks = self.backend.current_playlist.cp_tracks - - if not cp_tracks: - return None - - if self.random and not self._shuffled: - if self.repeat or self._first_shuffle: - logger.debug('Shuffling tracks') - self._shuffled = cp_tracks - random.shuffle(self._shuffled) - self._first_shuffle = False - - if self.random and self._shuffled: - return self._shuffled[0] - - if self.current_cp_track is None: - return cp_tracks[0] - - if self.repeat and self.single: - return cp_tracks[self.current_playlist_position] - - if self.repeat and not self.single: - return cp_tracks[ - (self.current_playlist_position + 1) % len(cp_tracks)] - - try: - return cp_tracks[self.current_playlist_position + 1] - except IndexError: - return None - - @property - def track_at_next(self): - """ - The track that will be played if calling :meth:`next()`. - - Read-only. A :class:`mopidy.models.Track` extracted from - :attr:`cp_track_at_next` for convenience. - """ - return self._get_track(self.cp_track_at_next) - - @property - def cp_track_at_next(self): - """ - The track that will be played if calling :meth:`next()`. - - Read-only. A two-tuple of (CPID integer, :class:`mopidy.models.Track`). - - For normal playback this is the next track in the playlist. If repeat - is enabled the next track can loop around the playlist. When random is - enabled this should be a random track, all tracks should be played once - before the list repeats. - """ - cp_tracks = self.backend.current_playlist.cp_tracks - - if not cp_tracks: - return None - - if self.random and not self._shuffled: - if self.repeat or self._first_shuffle: - logger.debug('Shuffling tracks') - self._shuffled = cp_tracks - random.shuffle(self._shuffled) - self._first_shuffle = False - - if self.random and self._shuffled: - return self._shuffled[0] - - if self.current_cp_track is None: - return cp_tracks[0] - - if self.repeat: - return cp_tracks[ - (self.current_playlist_position + 1) % len(cp_tracks)] - - try: - return cp_tracks[self.current_playlist_position + 1] - except IndexError: - return None - - @property - def track_at_previous(self): - """ - The track that will be played if calling :meth:`previous()`. - - Read-only. A :class:`mopidy.models.Track` extracted from - :attr:`cp_track_at_previous` for convenience. - """ - return self._get_track(self.cp_track_at_previous) - - @property - def cp_track_at_previous(self): - """ - The track that will be played if calling :meth:`previous()`. - - A two-tuple of (CPID integer, :class:`mopidy.models.Track`). - - For normal playback this is the previous track in the playlist. If - random and/or consume is enabled it should return the current track - instead. - """ - if self.repeat or self.consume or self.random: - return self.current_cp_track - - if self.current_playlist_position in (None, 0): - return None - - return self.backend.current_playlist.cp_tracks[ - self.current_playlist_position - 1] - - @property - def state(self): - """ - The playback state. Must be :attr:`PLAYING`, :attr:`PAUSED`, or - :attr:`STOPPED`. - - Possible states and transitions: - - .. digraph:: state_transitions - - "STOPPED" -> "PLAYING" [ label="play" ] - "STOPPED" -> "PAUSED" [ label="pause" ] - "PLAYING" -> "STOPPED" [ label="stop" ] - "PLAYING" -> "PAUSED" [ label="pause" ] - "PLAYING" -> "PLAYING" [ label="play" ] - "PAUSED" -> "PLAYING" [ label="resume" ] - "PAUSED" -> "STOPPED" [ label="stop" ] - """ - return self._state - - @state.setter - def state(self, new_state): - (old_state, self._state) = (self.state, new_state) - logger.debug(u'Changing state: %s -> %s', old_state, new_state) - - self._trigger_playback_state_changed() - - # FIXME play_time stuff assumes backend does not have a better way of - # handeling this stuff :/ - if (old_state in (self.PLAYING, self.STOPPED) - and new_state == self.PLAYING): - self._play_time_start() - elif old_state == self.PLAYING and new_state == self.PAUSED: - self._play_time_pause() - elif old_state == self.PAUSED and new_state == self.PLAYING: - self._play_time_resume() - - @property - def time_position(self): - """Time position in milliseconds.""" - if self.state == self.PLAYING: - time_since_started = (self._current_wall_time - - self.play_time_started) - return self.play_time_accumulated + time_since_started - elif self.state == self.PAUSED: - return self.play_time_accumulated - elif self.state == self.STOPPED: - return 0 - - def _play_time_start(self): - self.play_time_accumulated = 0 - self.play_time_started = self._current_wall_time - - def _play_time_pause(self): - time_since_started = self._current_wall_time - self.play_time_started - self.play_time_accumulated += time_since_started - - def _play_time_resume(self): - self.play_time_started = self._current_wall_time - - @property - def _current_wall_time(self): - return int(time.time() * 1000) - - @property - def volume(self): - return self.provider.get_volume() - - @volume.setter - def volume(self, volume): - self.provider.set_volume(volume) - - def change_track(self, cp_track, on_error_step=1): - """ - Change to the given track, keeping the current playback state. - - :param cp_track: track to change to - :type cp_track: two-tuple (CPID integer, :class:`mopidy.models.Track`) - or :class:`None` - :param on_error_step: direction to step at play error, 1 for next - track (default), -1 for previous track - :type on_error_step: int, -1 or 1 - - """ - old_state = self.state - self.stop() - self.current_cp_track = cp_track - if old_state == self.PLAYING: - self.play(on_error_step=on_error_step) - elif old_state == self.PAUSED: - self.pause() - - def on_end_of_track(self): - """ - Tell the playback controller that end of track is reached. - """ - if self.state == self.STOPPED: - return - - original_cp_track = self.current_cp_track - - if self.cp_track_at_eot: - self._trigger_track_playback_ended() - self.play(self.cp_track_at_eot) - else: - self.stop(clear_current_track=True) - - if self.consume: - self.backend.current_playlist.remove(cpid=original_cp_track.cpid) - - def on_current_playlist_change(self): - """ - Tell the playback controller that the current playlist has changed. - - Used by :class:`mopidy.backends.base.CurrentPlaylistController`. - """ - self._first_shuffle = True - self._shuffled = [] - - if (not self.backend.current_playlist.cp_tracks or - self.current_cp_track not in - self.backend.current_playlist.cp_tracks): - self.stop(clear_current_track=True) - - def next(self): - """ - Change to the next track. - - The current playback state will be kept. If it was playing, playing - will continue. If it was paused, it will still be paused, etc. - """ - if self.cp_track_at_next: - self._trigger_track_playback_ended() - self.change_track(self.cp_track_at_next) - else: - self.stop(clear_current_track=True) - - def pause(self): - """Pause playback.""" - if self.provider.pause(): - self.state = self.PAUSED - self._trigger_track_playback_paused() - - def play(self, cp_track=None, on_error_step=1): - """ - Play the given track, or if the given track is :class:`None`, play the - currently active track. - - :param cp_track: track to play - :type cp_track: two-tuple (CPID integer, :class:`mopidy.models.Track`) - or :class:`None` - :param on_error_step: direction to step at play error, 1 for next - track (default), -1 for previous track - :type on_error_step: int, -1 or 1 - """ - - if cp_track is not None: - assert cp_track in self.backend.current_playlist.cp_tracks - elif cp_track is None: - if self.state == self.PAUSED: - return self.resume() - elif self.current_cp_track is not None: - cp_track = self.current_cp_track - elif self.current_cp_track is None and on_error_step == 1: - cp_track = self.cp_track_at_next - elif self.current_cp_track is None and on_error_step == -1: - cp_track = self.cp_track_at_previous - - if cp_track is not None: - self.current_cp_track = cp_track - self.state = self.PLAYING - if not self.provider.play(cp_track.track): - # Track is not playable - if self.random and self._shuffled: - self._shuffled.remove(cp_track) - if on_error_step == 1: - self.next() - elif on_error_step == -1: - self.previous() - - if self.random and self.current_cp_track in self._shuffled: - self._shuffled.remove(self.current_cp_track) - - self._trigger_track_playback_started() - - def previous(self): - """ - Change to the previous track. - - The current playback state will be kept. If it was playing, playing - will continue. If it was paused, it will still be paused, etc. - """ - self._trigger_track_playback_ended() - self.change_track(self.cp_track_at_previous, on_error_step=-1) - - def resume(self): - """If paused, resume playing the current track.""" - if self.state == self.PAUSED and self.provider.resume(): - self.state = self.PLAYING - self._trigger_track_playback_resumed() - - def seek(self, time_position): - """ - Seeks to time position given in milliseconds. - - :param time_position: time position in milliseconds - :type time_position: int - :rtype: :class:`True` if successful, else :class:`False` - """ - if not self.backend.current_playlist.tracks: - return False - - if self.state == self.STOPPED: - self.play() - elif self.state == self.PAUSED: - self.resume() - - if time_position < 0: - time_position = 0 - elif time_position > self.current_track.length: - self.next() - return True - - self.play_time_started = self._current_wall_time - self.play_time_accumulated = time_position - - success = self.provider.seek(time_position) - if success: - self._trigger_seeked() - return success - - def stop(self, clear_current_track=False): - """ - Stop playing. - - :param clear_current_track: whether to clear the current track _after_ - stopping - :type clear_current_track: boolean - """ - if self.state != self.STOPPED: - if self.provider.stop(): - self._trigger_track_playback_ended() - self.state = self.STOPPED - if clear_current_track: - self.current_cp_track = None - - def _trigger_track_playback_paused(self): - logger.debug(u'Triggering track playback paused event') - if self.current_track is None: - return - BackendListener.send('track_playback_paused', - track=self.current_track, - time_position=self.time_position) - - def _trigger_track_playback_resumed(self): - logger.debug(u'Triggering track playback resumed event') - if self.current_track is None: - return - BackendListener.send('track_playback_resumed', - track=self.current_track, - time_position=self.time_position) - - def _trigger_track_playback_started(self): - logger.debug(u'Triggering track playback started event') - if self.current_track is None: - return - BackendListener.send('track_playback_started', - track=self.current_track) - - def _trigger_track_playback_ended(self): - logger.debug(u'Triggering track playback ended event') - if self.current_track is None: - return - BackendListener.send('track_playback_ended', - track=self.current_track, - time_position=self.time_position) - - def _trigger_playback_state_changed(self): - logger.debug(u'Triggering playback state change event') - BackendListener.send('playback_state_changed') - - def _trigger_options_changed(self): - logger.debug(u'Triggering options changed event') - BackendListener.send('options_changed') - - def _trigger_seeked(self): - logger.debug(u'Triggering seeked event') - BackendListener.send('seeked') - - class BasePlaybackProvider(object): """ :param backend: the backend diff --git a/mopidy/backends/base/stored_playlists.py b/mopidy/backends/base/stored_playlists.py index 0ce2e196..d1d52c9a 100644 --- a/mopidy/backends/base/stored_playlists.py +++ b/mopidy/backends/base/stored_playlists.py @@ -1,120 +1,4 @@ from copy import copy -import logging - -logger = logging.getLogger('mopidy.backends.base') - -class StoredPlaylistsController(object): - """ - :param backend: backend the controller is a part of - :type backend: :class:`mopidy.backends.base.Backend` - :param provider: provider the controller should use - :type provider: instance of :class:`BaseStoredPlaylistsProvider` - """ - - pykka_traversable = True - - def __init__(self, backend, provider): - self.backend = backend - self.provider = provider - - @property - def playlists(self): - """ - Currently stored playlists. - - Read/write. List of :class:`mopidy.models.Playlist`. - """ - return self.provider.playlists - - @playlists.setter - def playlists(self, playlists): - self.provider.playlists = playlists - - def create(self, name): - """ - Create a new playlist. - - :param name: name of the new playlist - :type name: string - :rtype: :class:`mopidy.models.Playlist` - """ - return self.provider.create(name) - - def delete(self, playlist): - """ - Delete playlist. - - :param playlist: the playlist to delete - :type playlist: :class:`mopidy.models.Playlist` - """ - return self.provider.delete(playlist) - - def get(self, **criteria): - """ - Get playlist by given criterias from the set of stored playlists. - - Raises :exc:`LookupError` if a unique match is not found. - - Examples:: - - get(name='a') # Returns track with name 'a' - get(uri='xyz') # Returns track with URI 'xyz' - get(name='a', uri='xyz') # Returns track with name 'a' and URI - # 'xyz' - - :param criteria: one or more criteria to match by - :type criteria: dict - :rtype: :class:`mopidy.models.Playlist` - """ - matches = self.playlists - for (key, value) in criteria.iteritems(): - matches = filter(lambda p: getattr(p, key) == value, matches) - if len(matches) == 1: - return matches[0] - criteria_string = ', '.join( - ['%s=%s' % (k, v) for (k, v) in criteria.iteritems()]) - if len(matches) == 0: - raise LookupError('"%s" match no playlists' % criteria_string) - else: - raise LookupError('"%s" match multiple playlists' % criteria_string) - - def lookup(self, uri): - """ - Lookup playlist with given URI in both the set of stored playlists and - in any other playlist sources. - - :param uri: playlist URI - :type uri: string - :rtype: :class:`mopidy.models.Playlist` - """ - return self.provider.lookup(uri) - - def refresh(self): - """ - Refresh the stored playlists in - :attr:`mopidy.backends.base.StoredPlaylistsController.playlists`. - """ - return self.provider.refresh() - - def rename(self, playlist, new_name): - """ - Rename playlist. - - :param playlist: the playlist - :type playlist: :class:`mopidy.models.Playlist` - :param new_name: the new name - :type new_name: string - """ - return self.provider.rename(playlist, new_name) - - def save(self, playlist): - """ - Save the playlist to the set of stored playlists. - - :param playlist: the playlist - :type playlist: :class:`mopidy.models.Playlist` - """ - return self.provider.save(playlist) class BaseStoredPlaylistsProvider(object): diff --git a/mopidy/backends/dummy/__init__.py b/mopidy/backends/dummy/__init__.py index 2234242c..3ada0052 100644 --- a/mopidy/backends/dummy/__init__.py +++ b/mopidy/backends/dummy/__init__.py @@ -1,13 +1,11 @@ from pykka.actor import ThreadingActor -from mopidy.backends.base import (Backend, CurrentPlaylistController, - PlaybackController, BasePlaybackProvider, LibraryController, - BaseLibraryProvider, StoredPlaylistsController, - BaseStoredPlaylistsProvider) +from mopidy import core +from mopidy.backends import base from mopidy.models import Playlist -class DummyBackend(ThreadingActor, Backend): +class DummyBackend(ThreadingActor, base.Backend): """ A backend which implements the backend API in the simplest way possible. Used in tests of the frontends. @@ -18,24 +16,24 @@ class DummyBackend(ThreadingActor, Backend): def __init__(self, *args, **kwargs): super(DummyBackend, self).__init__(*args, **kwargs) - self.current_playlist = CurrentPlaylistController(backend=self) + self.current_playlist = core.CurrentPlaylistController(backend=self) library_provider = DummyLibraryProvider(backend=self) - self.library = LibraryController(backend=self, + self.library = core.LibraryController(backend=self, provider=library_provider) playback_provider = DummyPlaybackProvider(backend=self) - self.playback = PlaybackController(backend=self, + self.playback = core.PlaybackController(backend=self, provider=playback_provider) stored_playlists_provider = DummyStoredPlaylistsProvider(backend=self) - self.stored_playlists = StoredPlaylistsController(backend=self, + self.stored_playlists = core.StoredPlaylistsController(backend=self, provider=stored_playlists_provider) self.uri_schemes = [u'dummy'] -class DummyLibraryProvider(BaseLibraryProvider): +class DummyLibraryProvider(base.BaseLibraryProvider): def __init__(self, *args, **kwargs): super(DummyLibraryProvider, self).__init__(*args, **kwargs) self.dummy_library = [] @@ -55,7 +53,7 @@ class DummyLibraryProvider(BaseLibraryProvider): return Playlist() -class DummyPlaybackProvider(BasePlaybackProvider): +class DummyPlaybackProvider(base.BasePlaybackProvider): def __init__(self, *args, **kwargs): super(DummyPlaybackProvider, self).__init__(*args, **kwargs) self._volume = None @@ -83,7 +81,7 @@ class DummyPlaybackProvider(BasePlaybackProvider): self._volume = volume -class DummyStoredPlaylistsProvider(BaseStoredPlaylistsProvider): +class DummyStoredPlaylistsProvider(base.BaseStoredPlaylistsProvider): def create(self, name): playlist = Playlist(name=name) self._playlists.append(playlist) diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index 263d2fc2..e8d918b0 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -7,11 +7,8 @@ import shutil from pykka.actor import ThreadingActor from pykka.registry import ActorRegistry -from mopidy import settings, DATA_PATH -from mopidy.backends.base import (Backend, CurrentPlaylistController, - LibraryController, BaseLibraryProvider, PlaybackController, - BasePlaybackProvider, StoredPlaylistsController, - BaseStoredPlaylistsProvider) +from mopidy import core, settings, DATA_PATH +from mopidy.backends import base from mopidy.models import Playlist, Track, Album from mopidy.gstreamer import GStreamer @@ -27,12 +24,10 @@ if not DEFAULT_MUSIC_PATH or DEFAULT_MUSIC_PATH == os.path.expanduser(u'~'): DEFAULT_MUSIC_PATH = os.path.expanduser(u'~/music') -class LocalBackend(ThreadingActor, Backend): +class LocalBackend(ThreadingActor, base.Backend): """ A backend for playing music from a local music archive. - **Issues:** https://github.com/mopidy/mopidy/issues?labels=backend-local - **Dependencies:** - None @@ -47,10 +42,10 @@ class LocalBackend(ThreadingActor, Backend): def __init__(self, *args, **kwargs): super(LocalBackend, self).__init__(*args, **kwargs) - self.current_playlist = CurrentPlaylistController(backend=self) + self.current_playlist = core.CurrentPlaylistController(backend=self) library_provider = LocalLibraryProvider(backend=self) - self.library = LibraryController(backend=self, + self.library = core.LibraryController(backend=self, provider=library_provider) playback_provider = LocalPlaybackProvider(backend=self) @@ -58,7 +53,7 @@ class LocalBackend(ThreadingActor, Backend): provider=playback_provider) stored_playlists_provider = LocalStoredPlaylistsProvider(backend=self) - self.stored_playlists = StoredPlaylistsController(backend=self, + self.stored_playlists = core.StoredPlaylistsController(backend=self, provider=stored_playlists_provider) self.uri_schemes = [u'file'] @@ -72,7 +67,7 @@ class LocalBackend(ThreadingActor, Backend): self.gstreamer = gstreamer_refs[0].proxy() -class LocalPlaybackController(PlaybackController): +class LocalPlaybackController(core.PlaybackController): def __init__(self, *args, **kwargs): super(LocalPlaybackController, self).__init__(*args, **kwargs) @@ -84,7 +79,7 @@ class LocalPlaybackController(PlaybackController): return self.backend.gstreamer.get_position().get() -class LocalPlaybackProvider(BasePlaybackProvider): +class LocalPlaybackProvider(base.BasePlaybackProvider): def pause(self): return self.backend.gstreamer.pause_playback().get() @@ -109,7 +104,7 @@ class LocalPlaybackProvider(BasePlaybackProvider): self.backend.gstreamer.set_volume(volume).get() -class LocalStoredPlaylistsProvider(BaseStoredPlaylistsProvider): +class LocalStoredPlaylistsProvider(base.BaseStoredPlaylistsProvider): def __init__(self, *args, **kwargs): super(LocalStoredPlaylistsProvider, self).__init__(*args, **kwargs) self._folder = settings.LOCAL_PLAYLIST_PATH or DEFAULT_PLAYLIST_PATH @@ -182,7 +177,7 @@ class LocalStoredPlaylistsProvider(BaseStoredPlaylistsProvider): self._playlists.append(playlist) -class LocalLibraryProvider(BaseLibraryProvider): +class LocalLibraryProvider(base.BaseLibraryProvider): def __init__(self, *args, **kwargs): super(LocalLibraryProvider, self).__init__(*args, **kwargs) self._uri_mapping = {} diff --git a/mopidy/backends/spotify/__init__.py b/mopidy/backends/spotify/__init__.py index 56775926..fef86280 100644 --- a/mopidy/backends/spotify/__init__.py +++ b/mopidy/backends/spotify/__init__.py @@ -3,16 +3,15 @@ import logging from pykka.actor import ThreadingActor from pykka.registry import ActorRegistry -from mopidy import settings -from mopidy.backends.base import (Backend, CurrentPlaylistController, - LibraryController, PlaybackController, StoredPlaylistsController) +from mopidy import core, settings +from mopidy.backends import base from mopidy.gstreamer import GStreamer logger = logging.getLogger('mopidy.backends.spotify') BITRATES = {96: 2, 160: 0, 320: 1} -class SpotifyBackend(ThreadingActor, Backend): +class SpotifyBackend(ThreadingActor, base.Backend): """ A backend for playing music from the `Spotify `_ music streaming service. The backend uses the official `libspotify @@ -51,19 +50,19 @@ class SpotifyBackend(ThreadingActor, Backend): super(SpotifyBackend, self).__init__(*args, **kwargs) - self.current_playlist = CurrentPlaylistController(backend=self) + self.current_playlist = core.CurrentPlaylistController(backend=self) library_provider = SpotifyLibraryProvider(backend=self) - self.library = LibraryController(backend=self, + self.library = core.LibraryController(backend=self, provider=library_provider) playback_provider = SpotifyPlaybackProvider(backend=self) - self.playback = PlaybackController(backend=self, + self.playback = core.PlaybackController(backend=self, provider=playback_provider) stored_playlists_provider = SpotifyStoredPlaylistsProvider( backend=self) - self.stored_playlists = StoredPlaylistsController(backend=self, + self.stored_playlists = core.StoredPlaylistsController(backend=self, provider=stored_playlists_provider) self.uri_schemes = [u'spotify'] diff --git a/mopidy/core/__init__.py b/mopidy/core/__init__.py new file mode 100644 index 00000000..16c09665 --- /dev/null +++ b/mopidy/core/__init__.py @@ -0,0 +1,4 @@ +from .current_playlist import CurrentPlaylistController +from .library import LibraryController +from .playback import PlaybackController +from .stored_playlists import StoredPlaylistsController diff --git a/mopidy/backends/base/current_playlist.py b/mopidy/core/current_playlist.py similarity index 99% rename from mopidy/backends/base/current_playlist.py rename to mopidy/core/current_playlist.py index d7e6c331..af06e05e 100644 --- a/mopidy/backends/base/current_playlist.py +++ b/mopidy/core/current_playlist.py @@ -5,7 +5,9 @@ import random from mopidy.listeners import BackendListener from mopidy.models import CpTrack -logger = logging.getLogger('mopidy.backends.base') + +logger = logging.getLogger('mopidy.core') + class CurrentPlaylistController(object): """ diff --git a/mopidy/core/library.py b/mopidy/core/library.py new file mode 100644 index 00000000..fc55aaeb --- /dev/null +++ b/mopidy/core/library.py @@ -0,0 +1,70 @@ +class LibraryController(object): + """ + :param backend: backend the controller is a part of + :type backend: :class:`mopidy.backends.base.Backend` + :param provider: provider the controller should use + :type provider: instance of :class:`BaseLibraryProvider` + """ + + pykka_traversable = True + + def __init__(self, backend, provider): + self.backend = backend + self.provider = provider + + def find_exact(self, **query): + """ + Search the library for tracks where ``field`` is ``values``. + + Examples:: + + # Returns results matching 'a' + find_exact(any=['a']) + # Returns results matching artist 'xyz' + find_exact(artist=['xyz']) + # Returns results matching 'a' and 'b' and artist 'xyz' + find_exact(any=['a', 'b'], artist=['xyz']) + + :param query: one or more queries to search for + :type query: dict + :rtype: :class:`mopidy.models.Playlist` + """ + return self.provider.find_exact(**query) + + def lookup(self, uri): + """ + Lookup track with given URI. Returns :class:`None` if not found. + + :param uri: track URI + :type uri: string + :rtype: :class:`mopidy.models.Track` or :class:`None` + """ + return self.provider.lookup(uri) + + def refresh(self, uri=None): + """ + Refresh library. Limit to URI and below if an URI is given. + + :param uri: directory or track URI + :type uri: string + """ + return self.provider.refresh(uri) + + def search(self, **query): + """ + Search the library for tracks where ``field`` contains ``values``. + + Examples:: + + # Returns results matching 'a' + search(any=['a']) + # Returns results matching artist 'xyz' + search(artist=['xyz']) + # Returns results matching 'a' and 'b' and artist 'xyz' + search(any=['a', 'b'], artist=['xyz']) + + :param query: one or more queries to search for + :type query: dict + :rtype: :class:`mopidy.models.Playlist` + """ + return self.provider.search(**query) diff --git a/mopidy/core/playback.py b/mopidy/core/playback.py new file mode 100644 index 00000000..a0c3ef30 --- /dev/null +++ b/mopidy/core/playback.py @@ -0,0 +1,548 @@ +import logging +import random +import time + +from mopidy.listeners import BackendListener + + +logger = logging.getLogger('mopidy.backends.base') + + +def option_wrapper(name, default): + def get_option(self): + return getattr(self, name, default) + + def set_option(self, value): + if getattr(self, name, default) != value: + self._trigger_options_changed() + return setattr(self, name, value) + + return property(get_option, set_option) + + +class PlaybackController(object): + """ + :param backend: the backend + :type backend: :class:`mopidy.backends.base.Backend` + :param provider: provider the controller should use + :type provider: instance of :class:`BasePlaybackProvider` + """ + + # pylint: disable = R0902 + # Too many instance attributes + + pykka_traversable = True + + #: Constant representing the paused state. + PAUSED = u'paused' + + #: Constant representing the playing state. + PLAYING = u'playing' + + #: Constant representing the stopped state. + STOPPED = u'stopped' + + #: :class:`True` + #: Tracks are removed from the playlist when they have been played. + #: :class:`False` + #: Tracks are not removed from the playlist. + consume = option_wrapper('_consume', False) + + #: The currently playing or selected track. + #: + #: A two-tuple of (CPID integer, :class:`mopidy.models.Track`) or + #: :class:`None`. + current_cp_track = None + + #: :class:`True` + #: Tracks are selected at random from the playlist. + #: :class:`False` + #: Tracks are played in the order of the playlist. + random = option_wrapper('_random', False) + + #: :class:`True` + #: The current playlist is played repeatedly. To repeat a single track, + #: select both :attr:`repeat` and :attr:`single`. + #: :class:`False` + #: The current playlist is played once. + repeat = option_wrapper('_repeat', False) + + #: :class:`True` + #: Playback is stopped after current song, unless in :attr:`repeat` + #: mode. + #: :class:`False` + #: Playback continues after current song. + single = option_wrapper('_single', False) + + def __init__(self, backend, provider): + self.backend = backend + self.provider = provider + self._state = self.STOPPED + self._shuffled = [] + self._first_shuffle = True + self.play_time_accumulated = 0 + self.play_time_started = None + + def _get_cpid(self, cp_track): + if cp_track is None: + return None + return cp_track.cpid + + def _get_track(self, cp_track): + if cp_track is None: + return None + return cp_track.track + + @property + def current_cpid(self): + """ + The CPID (current playlist ID) of the currently playing or selected + track. + + Read-only. Extracted from :attr:`current_cp_track` for convenience. + """ + return self._get_cpid(self.current_cp_track) + + @property + def current_track(self): + """ + The currently playing or selected :class:`mopidy.models.Track`. + + Read-only. Extracted from :attr:`current_cp_track` for convenience. + """ + return self._get_track(self.current_cp_track) + + @property + def current_playlist_position(self): + """ + The position of the current track in the current playlist. + + Read-only. + """ + if self.current_cp_track is None: + return None + try: + return self.backend.current_playlist.cp_tracks.index( + self.current_cp_track) + except ValueError: + return None + + @property + def track_at_eot(self): + """ + The track that will be played at the end of the current track. + + Read-only. A :class:`mopidy.models.Track` extracted from + :attr:`cp_track_at_eot` for convenience. + """ + return self._get_track(self.cp_track_at_eot) + + @property + def cp_track_at_eot(self): + """ + The track that will be played at the end of the current track. + + Read-only. A two-tuple of (CPID integer, :class:`mopidy.models.Track`). + + Not necessarily the same track as :attr:`cp_track_at_next`. + """ + # pylint: disable = R0911 + # Too many return statements + + cp_tracks = self.backend.current_playlist.cp_tracks + + if not cp_tracks: + return None + + if self.random and not self._shuffled: + if self.repeat or self._first_shuffle: + logger.debug('Shuffling tracks') + self._shuffled = cp_tracks + random.shuffle(self._shuffled) + self._first_shuffle = False + + if self.random and self._shuffled: + return self._shuffled[0] + + if self.current_cp_track is None: + return cp_tracks[0] + + if self.repeat and self.single: + return cp_tracks[self.current_playlist_position] + + if self.repeat and not self.single: + return cp_tracks[ + (self.current_playlist_position + 1) % len(cp_tracks)] + + try: + return cp_tracks[self.current_playlist_position + 1] + except IndexError: + return None + + @property + def track_at_next(self): + """ + The track that will be played if calling :meth:`next()`. + + Read-only. A :class:`mopidy.models.Track` extracted from + :attr:`cp_track_at_next` for convenience. + """ + return self._get_track(self.cp_track_at_next) + + @property + def cp_track_at_next(self): + """ + The track that will be played if calling :meth:`next()`. + + Read-only. A two-tuple of (CPID integer, :class:`mopidy.models.Track`). + + For normal playback this is the next track in the playlist. If repeat + is enabled the next track can loop around the playlist. When random is + enabled this should be a random track, all tracks should be played once + before the list repeats. + """ + cp_tracks = self.backend.current_playlist.cp_tracks + + if not cp_tracks: + return None + + if self.random and not self._shuffled: + if self.repeat or self._first_shuffle: + logger.debug('Shuffling tracks') + self._shuffled = cp_tracks + random.shuffle(self._shuffled) + self._first_shuffle = False + + if self.random and self._shuffled: + return self._shuffled[0] + + if self.current_cp_track is None: + return cp_tracks[0] + + if self.repeat: + return cp_tracks[ + (self.current_playlist_position + 1) % len(cp_tracks)] + + try: + return cp_tracks[self.current_playlist_position + 1] + except IndexError: + return None + + @property + def track_at_previous(self): + """ + The track that will be played if calling :meth:`previous()`. + + Read-only. A :class:`mopidy.models.Track` extracted from + :attr:`cp_track_at_previous` for convenience. + """ + return self._get_track(self.cp_track_at_previous) + + @property + def cp_track_at_previous(self): + """ + The track that will be played if calling :meth:`previous()`. + + A two-tuple of (CPID integer, :class:`mopidy.models.Track`). + + For normal playback this is the previous track in the playlist. If + random and/or consume is enabled it should return the current track + instead. + """ + if self.repeat or self.consume or self.random: + return self.current_cp_track + + if self.current_playlist_position in (None, 0): + return None + + return self.backend.current_playlist.cp_tracks[ + self.current_playlist_position - 1] + + @property + def state(self): + """ + The playback state. Must be :attr:`PLAYING`, :attr:`PAUSED`, or + :attr:`STOPPED`. + + Possible states and transitions: + + .. digraph:: state_transitions + + "STOPPED" -> "PLAYING" [ label="play" ] + "STOPPED" -> "PAUSED" [ label="pause" ] + "PLAYING" -> "STOPPED" [ label="stop" ] + "PLAYING" -> "PAUSED" [ label="pause" ] + "PLAYING" -> "PLAYING" [ label="play" ] + "PAUSED" -> "PLAYING" [ label="resume" ] + "PAUSED" -> "STOPPED" [ label="stop" ] + """ + return self._state + + @state.setter + def state(self, new_state): + (old_state, self._state) = (self.state, new_state) + logger.debug(u'Changing state: %s -> %s', old_state, new_state) + + self._trigger_playback_state_changed() + + # FIXME play_time stuff assumes backend does not have a better way of + # handeling this stuff :/ + if (old_state in (self.PLAYING, self.STOPPED) + and new_state == self.PLAYING): + self._play_time_start() + elif old_state == self.PLAYING and new_state == self.PAUSED: + self._play_time_pause() + elif old_state == self.PAUSED and new_state == self.PLAYING: + self._play_time_resume() + + @property + def time_position(self): + """Time position in milliseconds.""" + if self.state == self.PLAYING: + time_since_started = (self._current_wall_time - + self.play_time_started) + return self.play_time_accumulated + time_since_started + elif self.state == self.PAUSED: + return self.play_time_accumulated + elif self.state == self.STOPPED: + return 0 + + def _play_time_start(self): + self.play_time_accumulated = 0 + self.play_time_started = self._current_wall_time + + def _play_time_pause(self): + time_since_started = self._current_wall_time - self.play_time_started + self.play_time_accumulated += time_since_started + + def _play_time_resume(self): + self.play_time_started = self._current_wall_time + + @property + def _current_wall_time(self): + return int(time.time() * 1000) + + @property + def volume(self): + return self.provider.get_volume() + + @volume.setter + def volume(self, volume): + self.provider.set_volume(volume) + + def change_track(self, cp_track, on_error_step=1): + """ + Change to the given track, keeping the current playback state. + + :param cp_track: track to change to + :type cp_track: two-tuple (CPID integer, :class:`mopidy.models.Track`) + or :class:`None` + :param on_error_step: direction to step at play error, 1 for next + track (default), -1 for previous track + :type on_error_step: int, -1 or 1 + + """ + old_state = self.state + self.stop() + self.current_cp_track = cp_track + if old_state == self.PLAYING: + self.play(on_error_step=on_error_step) + elif old_state == self.PAUSED: + self.pause() + + def on_end_of_track(self): + """ + Tell the playback controller that end of track is reached. + """ + if self.state == self.STOPPED: + return + + original_cp_track = self.current_cp_track + + if self.cp_track_at_eot: + self._trigger_track_playback_ended() + self.play(self.cp_track_at_eot) + else: + self.stop(clear_current_track=True) + + if self.consume: + self.backend.current_playlist.remove(cpid=original_cp_track.cpid) + + def on_current_playlist_change(self): + """ + Tell the playback controller that the current playlist has changed. + + Used by :class:`mopidy.backends.base.CurrentPlaylistController`. + """ + self._first_shuffle = True + self._shuffled = [] + + if (not self.backend.current_playlist.cp_tracks or + self.current_cp_track not in + self.backend.current_playlist.cp_tracks): + self.stop(clear_current_track=True) + + def next(self): + """ + Change to the next track. + + The current playback state will be kept. If it was playing, playing + will continue. If it was paused, it will still be paused, etc. + """ + if self.cp_track_at_next: + self._trigger_track_playback_ended() + self.change_track(self.cp_track_at_next) + else: + self.stop(clear_current_track=True) + + def pause(self): + """Pause playback.""" + if self.provider.pause(): + self.state = self.PAUSED + self._trigger_track_playback_paused() + + def play(self, cp_track=None, on_error_step=1): + """ + Play the given track, or if the given track is :class:`None`, play the + currently active track. + + :param cp_track: track to play + :type cp_track: two-tuple (CPID integer, :class:`mopidy.models.Track`) + or :class:`None` + :param on_error_step: direction to step at play error, 1 for next + track (default), -1 for previous track + :type on_error_step: int, -1 or 1 + """ + + if cp_track is not None: + assert cp_track in self.backend.current_playlist.cp_tracks + elif cp_track is None: + if self.state == self.PAUSED: + return self.resume() + elif self.current_cp_track is not None: + cp_track = self.current_cp_track + elif self.current_cp_track is None and on_error_step == 1: + cp_track = self.cp_track_at_next + elif self.current_cp_track is None and on_error_step == -1: + cp_track = self.cp_track_at_previous + + if cp_track is not None: + self.current_cp_track = cp_track + self.state = self.PLAYING + if not self.provider.play(cp_track.track): + # Track is not playable + if self.random and self._shuffled: + self._shuffled.remove(cp_track) + if on_error_step == 1: + self.next() + elif on_error_step == -1: + self.previous() + + if self.random and self.current_cp_track in self._shuffled: + self._shuffled.remove(self.current_cp_track) + + self._trigger_track_playback_started() + + def previous(self): + """ + Change to the previous track. + + The current playback state will be kept. If it was playing, playing + will continue. If it was paused, it will still be paused, etc. + """ + self._trigger_track_playback_ended() + self.change_track(self.cp_track_at_previous, on_error_step=-1) + + def resume(self): + """If paused, resume playing the current track.""" + if self.state == self.PAUSED and self.provider.resume(): + self.state = self.PLAYING + self._trigger_track_playback_resumed() + + def seek(self, time_position): + """ + Seeks to time position given in milliseconds. + + :param time_position: time position in milliseconds + :type time_position: int + :rtype: :class:`True` if successful, else :class:`False` + """ + if not self.backend.current_playlist.tracks: + return False + + if self.state == self.STOPPED: + self.play() + elif self.state == self.PAUSED: + self.resume() + + if time_position < 0: + time_position = 0 + elif time_position > self.current_track.length: + self.next() + return True + + self.play_time_started = self._current_wall_time + self.play_time_accumulated = time_position + + success = self.provider.seek(time_position) + if success: + self._trigger_seeked() + return success + + def stop(self, clear_current_track=False): + """ + Stop playing. + + :param clear_current_track: whether to clear the current track _after_ + stopping + :type clear_current_track: boolean + """ + if self.state != self.STOPPED: + if self.provider.stop(): + self._trigger_track_playback_ended() + self.state = self.STOPPED + if clear_current_track: + self.current_cp_track = None + + def _trigger_track_playback_paused(self): + logger.debug(u'Triggering track playback paused event') + if self.current_track is None: + return + BackendListener.send('track_playback_paused', + track=self.current_track, + time_position=self.time_position) + + def _trigger_track_playback_resumed(self): + logger.debug(u'Triggering track playback resumed event') + if self.current_track is None: + return + BackendListener.send('track_playback_resumed', + track=self.current_track, + time_position=self.time_position) + + def _trigger_track_playback_started(self): + logger.debug(u'Triggering track playback started event') + if self.current_track is None: + return + BackendListener.send('track_playback_started', + track=self.current_track) + + def _trigger_track_playback_ended(self): + logger.debug(u'Triggering track playback ended event') + if self.current_track is None: + return + BackendListener.send('track_playback_ended', + track=self.current_track, + time_position=self.time_position) + + def _trigger_playback_state_changed(self): + logger.debug(u'Triggering playback state change event') + BackendListener.send('playback_state_changed') + + def _trigger_options_changed(self): + logger.debug(u'Triggering options changed event') + BackendListener.send('options_changed') + + def _trigger_seeked(self): + logger.debug(u'Triggering seeked event') + BackendListener.send('seeked') diff --git a/mopidy/core/stored_playlists.py b/mopidy/core/stored_playlists.py new file mode 100644 index 00000000..a29e34fc --- /dev/null +++ b/mopidy/core/stored_playlists.py @@ -0,0 +1,113 @@ +class StoredPlaylistsController(object): + """ + :param backend: backend the controller is a part of + :type backend: :class:`mopidy.backends.base.Backend` + :param provider: provider the controller should use + :type provider: instance of :class:`BaseStoredPlaylistsProvider` + """ + + pykka_traversable = True + + def __init__(self, backend, provider): + self.backend = backend + self.provider = provider + + @property + def playlists(self): + """ + Currently stored playlists. + + Read/write. List of :class:`mopidy.models.Playlist`. + """ + return self.provider.playlists + + @playlists.setter + def playlists(self, playlists): + self.provider.playlists = playlists + + def create(self, name): + """ + Create a new playlist. + + :param name: name of the new playlist + :type name: string + :rtype: :class:`mopidy.models.Playlist` + """ + return self.provider.create(name) + + def delete(self, playlist): + """ + Delete playlist. + + :param playlist: the playlist to delete + :type playlist: :class:`mopidy.models.Playlist` + """ + return self.provider.delete(playlist) + + def get(self, **criteria): + """ + Get playlist by given criterias from the set of stored playlists. + + Raises :exc:`LookupError` if a unique match is not found. + + Examples:: + + get(name='a') # Returns track with name 'a' + get(uri='xyz') # Returns track with URI 'xyz' + get(name='a', uri='xyz') # Returns track with name 'a' and URI + # 'xyz' + + :param criteria: one or more criteria to match by + :type criteria: dict + :rtype: :class:`mopidy.models.Playlist` + """ + matches = self.playlists + for (key, value) in criteria.iteritems(): + matches = filter(lambda p: getattr(p, key) == value, matches) + if len(matches) == 1: + return matches[0] + criteria_string = ', '.join( + ['%s=%s' % (k, v) for (k, v) in criteria.iteritems()]) + if len(matches) == 0: + raise LookupError('"%s" match no playlists' % criteria_string) + else: + raise LookupError('"%s" match multiple playlists' + % criteria_string) + + def lookup(self, uri): + """ + Lookup playlist with given URI in both the set of stored playlists and + in any other playlist sources. + + :param uri: playlist URI + :type uri: string + :rtype: :class:`mopidy.models.Playlist` + """ + return self.provider.lookup(uri) + + def refresh(self): + """ + Refresh the stored playlists in + :attr:`mopidy.backends.base.StoredPlaylistsController.playlists`. + """ + return self.provider.refresh() + + def rename(self, playlist, new_name): + """ + Rename playlist. + + :param playlist: the playlist + :type playlist: :class:`mopidy.models.Playlist` + :param new_name: the new name + :type new_name: string + """ + return self.provider.rename(playlist, new_name) + + def save(self, playlist): + """ + Save the playlist to the set of stored playlists. + + :param playlist: the playlist + :type playlist: :class:`mopidy.models.Playlist` + """ + return self.provider.save(playlist) diff --git a/mopidy/frontends/mpd/protocol/playback.py b/mopidy/frontends/mpd/protocol/playback.py index 4cf33266..e6bb6478 100644 --- a/mopidy/frontends/mpd/protocol/playback.py +++ b/mopidy/frontends/mpd/protocol/playback.py @@ -1,4 +1,4 @@ -from mopidy.backends.base import PlaybackController +from mopidy import core from mopidy.frontends.mpd.protocol import handle_request from mopidy.frontends.mpd.exceptions import (MpdArgError, MpdNoExistError, MpdNotImplemented) @@ -105,10 +105,10 @@ def pause(context, state=None): """ if state is None: if (context.backend.playback.state.get() == - PlaybackController.PLAYING): + core.PlaybackController.PLAYING): context.backend.playback.pause() elif (context.backend.playback.state.get() == - PlaybackController.PAUSED): + core.PlaybackController.PAUSED): context.backend.playback.resume() elif int(state): context.backend.playback.pause() @@ -185,9 +185,11 @@ def playpos(context, songpos): raise MpdArgError(u'Bad song index', command=u'play') def _play_minus_one(context): - if (context.backend.playback.state.get() == PlaybackController.PLAYING): + if (context.backend.playback.state.get() == + core.PlaybackController.PLAYING): return # Nothing to do - elif (context.backend.playback.state.get() == PlaybackController.PAUSED): + elif (context.backend.playback.state.get() == + core.PlaybackController.PAUSED): return context.backend.playback.resume().get() elif context.backend.playback.current_cp_track.get() is not None: cp_track = context.backend.playback.current_cp_track.get() diff --git a/mopidy/frontends/mpd/protocol/status.py b/mopidy/frontends/mpd/protocol/status.py index 4a9ad9a1..279978aa 100644 --- a/mopidy/frontends/mpd/protocol/status.py +++ b/mopidy/frontends/mpd/protocol/status.py @@ -1,6 +1,6 @@ import pykka.future -from mopidy.backends.base import PlaybackController +from mopidy import core from mopidy.frontends.mpd.exceptions import MpdNotImplemented from mopidy.frontends.mpd.protocol import handle_request from mopidy.frontends.mpd.translator import track_to_mpd_format @@ -194,8 +194,8 @@ def status(context): if futures['playback.current_cp_track'].get() is not None: result.append(('song', _status_songpos(futures))) result.append(('songid', _status_songid(futures))) - if futures['playback.state'].get() in (PlaybackController.PLAYING, - PlaybackController.PAUSED): + if futures['playback.state'].get() in (core.PlaybackController.PLAYING, + core.PlaybackController.PAUSED): result.append(('time', _status_time(futures))) result.append(('elapsed', _status_time_elapsed(futures))) result.append(('bitrate', _status_bitrate(futures))) @@ -239,11 +239,11 @@ def _status_songpos(futures): def _status_state(futures): state = futures['playback.state'].get() - if state == PlaybackController.PLAYING: + if state == core.PlaybackController.PLAYING: return u'play' - elif state == PlaybackController.STOPPED: + elif state == core.PlaybackController.STOPPED: return u'stop' - elif state == PlaybackController.PAUSED: + elif state == core.PlaybackController.PAUSED: return u'pause' def _status_time(futures): diff --git a/mopidy/frontends/mpris/objects.py b/mopidy/frontends/mpris/objects.py index 6815c0d2..bcd3de5c 100644 --- a/mopidy/frontends/mpris/objects.py +++ b/mopidy/frontends/mpris/objects.py @@ -14,9 +14,8 @@ except ImportError as import_error: from pykka.registry import ActorRegistry -from mopidy import settings +from mopidy import core, settings from mopidy.backends.base import Backend -from mopidy.backends.base.playback import PlaybackController from mopidy.utils.process import exit_process # Must be done before dbus.SessionBus() is called @@ -198,11 +197,11 @@ class MprisObject(dbus.service.Object): logger.debug(u'%s.PlayPause not allowed', PLAYER_IFACE) return state = self.backend.playback.state.get() - if state == PlaybackController.PLAYING: + if state == core.PlaybackController.PLAYING: self.backend.playback.pause().get() - elif state == PlaybackController.PAUSED: + elif state == core.PlaybackController.PAUSED: self.backend.playback.resume().get() - elif state == PlaybackController.STOPPED: + elif state == core.PlaybackController.STOPPED: self.backend.playback.play().get() @dbus.service.method(dbus_interface=PLAYER_IFACE) @@ -220,7 +219,7 @@ class MprisObject(dbus.service.Object): logger.debug(u'%s.Play not allowed', PLAYER_IFACE) return state = self.backend.playback.state.get() - if state == PlaybackController.PAUSED: + if state == core.PlaybackController.PAUSED: self.backend.playback.resume().get() else: self.backend.playback.play().get() @@ -287,11 +286,11 @@ class MprisObject(dbus.service.Object): def get_PlaybackStatus(self): state = self.backend.playback.state.get() - if state == PlaybackController.PLAYING: + if state == core.PlaybackController.PLAYING: return 'Playing' - elif state == PlaybackController.PAUSED: + elif state == core.PlaybackController.PAUSED: return 'Paused' - elif state == PlaybackController.STOPPED: + elif state == core.PlaybackController.STOPPED: return 'Stopped' def get_LoopStatus(self): diff --git a/tests/core/__init__.py b/tests/core/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/frontends/mpd/protocol/playback_test.py b/tests/frontends/mpd/protocol/playback_test.py index 87c9bbb8..514c1599 100644 --- a/tests/frontends/mpd/protocol/playback_test.py +++ b/tests/frontends/mpd/protocol/playback_test.py @@ -1,12 +1,12 @@ -from mopidy.backends import base as backend +from mopidy import core from mopidy.models import Track from tests import unittest from tests.frontends.mpd import protocol -PAUSED = backend.PlaybackController.PAUSED -PLAYING = backend.PlaybackController.PLAYING -STOPPED = backend.PlaybackController.STOPPED +PAUSED = core.PlaybackController.PAUSED +PLAYING = core.PlaybackController.PLAYING +STOPPED = core.PlaybackController.STOPPED class PlaybackOptionsHandlerTest(protocol.BaseTestCase): diff --git a/tests/frontends/mpd/status_test.py b/tests/frontends/mpd/status_test.py index 3701faaf..8fd8895d 100644 --- a/tests/frontends/mpd/status_test.py +++ b/tests/frontends/mpd/status_test.py @@ -1,13 +1,14 @@ -from mopidy.backends import dummy as backend +from mopidy import core +from mopidy.backends import dummy from mopidy.frontends.mpd import dispatcher from mopidy.frontends.mpd.protocol import status from mopidy.models import Track from tests import unittest -PAUSED = backend.PlaybackController.PAUSED -PLAYING = backend.PlaybackController.PLAYING -STOPPED = backend.PlaybackController.STOPPED +PAUSED = core.PlaybackController.PAUSED +PLAYING = core.PlaybackController.PLAYING +STOPPED = core.PlaybackController.STOPPED # FIXME migrate to using protocol.BaseTestCase instead of status.stats # directly? @@ -15,7 +16,7 @@ STOPPED = backend.PlaybackController.STOPPED class StatusHandlerTest(unittest.TestCase): def setUp(self): - self.backend = backend.DummyBackend.start().proxy() + self.backend = dummy.DummyBackend.start().proxy() self.dispatcher = dispatcher.MpdDispatcher() self.context = self.dispatcher.context diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index b7ad1b60..48be504f 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -2,9 +2,8 @@ import sys import mock -from mopidy import OptionalDependencyError +from mopidy import core, OptionalDependencyError from mopidy.backends.dummy import DummyBackend -from mopidy.backends.base.playback import PlaybackController from mopidy.models import Album, Artist, Track try: @@ -14,9 +13,9 @@ except OptionalDependencyError: from tests import unittest -PLAYING = PlaybackController.PLAYING -PAUSED = PlaybackController.PAUSED -STOPPED = PlaybackController.STOPPED +PLAYING = core.PlaybackController.PLAYING +PAUSED = core.PlaybackController.PAUSED +STOPPED = core.PlaybackController.STOPPED @unittest.skipUnless(sys.platform.startswith('linux'), 'requires Linux') From e0b26fcb814702657d2a03586e9486b49f01387c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 13 Sep 2012 23:50:09 +0200 Subject: [PATCH 30/44] docs: Move controllers out of the Backend API --- .../{backends/providers.rst => backends.rst} | 0 docs/api/backends/controllers.rst | 54 ------------------- docs/api/{backends => }/concepts.rst | 0 docs/api/core.rst | 49 +++++++++++++++++ docs/api/index.rst | 10 ++-- 5 files changed, 55 insertions(+), 58 deletions(-) rename docs/api/{backends/providers.rst => backends.rst} (100%) delete mode 100644 docs/api/backends/controllers.rst rename docs/api/{backends => }/concepts.rst (100%) create mode 100644 docs/api/core.rst diff --git a/docs/api/backends/providers.rst b/docs/api/backends.rst similarity index 100% rename from docs/api/backends/providers.rst rename to docs/api/backends.rst diff --git a/docs/api/backends/controllers.rst b/docs/api/backends/controllers.rst deleted file mode 100644 index 8d6687e2..00000000 --- a/docs/api/backends/controllers.rst +++ /dev/null @@ -1,54 +0,0 @@ -.. _backend-controller-api: - -********************** -Backend controller API -********************** - - -The backend controller API is the interface that is used by frontends like -:mod:`mopidy.frontends.mpd`. If you want to implement your own backend, see the -:ref:`backend-provider-api`. - - -The backend -=========== - -.. autoclass:: mopidy.backends.base.Backend - :members: - - -Playback controller -=================== - -Manages playback, with actions like play, pause, stop, next, previous, -seek, and volume control. - -.. autoclass:: mopidy.backends.base.PlaybackController - :members: - - -Current playlist controller -=========================== - -Manages everything related to the currently loaded playlist. - -.. autoclass:: mopidy.backends.base.CurrentPlaylistController - :members: - - -Stored playlists controller -=========================== - -Manages stored playlist. - -.. autoclass:: mopidy.backends.base.StoredPlaylistsController - :members: - - -Library controller -================== - -Manages the music library, e.g. searching for tracks to be added to a playlist. - -.. autoclass:: mopidy.backends.base.LibraryController - :members: diff --git a/docs/api/backends/concepts.rst b/docs/api/concepts.rst similarity index 100% rename from docs/api/backends/concepts.rst rename to docs/api/concepts.rst diff --git a/docs/api/core.rst b/docs/api/core.rst new file mode 100644 index 00000000..1852733f --- /dev/null +++ b/docs/api/core.rst @@ -0,0 +1,49 @@ +.. _backend-controller-api: + +******** +Core API +******** + + +The core API is the interface that is used by frontends like +:mod:`mopidy.frontends.mpd`. The core layer is inbetween the frontends and the +backends. + +If you want to implement your own backend, see the :ref:`backend-provider-api`. + + +Playback controller +=================== + +Manages playback, with actions like play, pause, stop, next, previous, +seek, and volume control. + +.. autoclass:: mopidy.core.PlaybackController + :members: + + +Current playlist controller +=========================== + +Manages everything related to the currently loaded playlist. + +.. autoclass:: mopidy.core.CurrentPlaylistController + :members: + + +Stored playlists controller +=========================== + +Manages stored playlist. + +.. autoclass:: mopidy.core.StoredPlaylistsController + :members: + + +Library controller +================== + +Manages the music library, e.g. searching for tracks to be added to a playlist. + +.. autoclass:: mopidy.core.LibraryController + :members: diff --git a/docs/api/index.rst b/docs/api/index.rst index 1f37e9ff..b5be8ed4 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -5,7 +5,9 @@ API reference .. toctree:: :glob: - backends/concepts - backends/controllers - backends/providers - * + concepts + models + backends + core + frontends + listeners From f3cb3036d4627255e6ac85d9527387d3bc17f83c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 13 Sep 2012 23:57:07 +0200 Subject: [PATCH 31/44] docs: Update a couple of references and titles --- docs/api/backends.rst | 18 +++++++++--------- docs/api/concepts.rst | 6 +++--- docs/api/core.rst | 4 +--- docs/changes.rst | 6 +++--- 4 files changed, 16 insertions(+), 18 deletions(-) diff --git a/docs/api/backends.rst b/docs/api/backends.rst index 61e5f68a..781723d6 100644 --- a/docs/api/backends.rst +++ b/docs/api/backends.rst @@ -1,12 +1,12 @@ -.. _backend-provider-api: +.. _backend-api: -******************** -Backend provider API -******************** +*********** +Backend API +*********** -The backend provider API is the interface that must be implemented when you -create a backend. If you are working on a frontend and need to access the -backend, see the :ref:`backend-controller-api`. +The backend API is the interface that must be implemented when you create a +backend. If you are working on a frontend and need to access the backend, see +the :ref:`core-api`. Playback provider @@ -30,8 +30,8 @@ Library provider :members: -Backend provider implementations -================================ +Backend implementations +======================= * :mod:`mopidy.backends.dummy` * :mod:`mopidy.backends.spotify` diff --git a/docs/api/concepts.rst b/docs/api/concepts.rst index 371e03bc..ae959237 100644 --- a/docs/api/concepts.rst +++ b/docs/api/concepts.rst @@ -1,4 +1,4 @@ -.. _backend-concepts: +.. _concepts: ********************************************** The backend, controller, and provider concepts @@ -12,11 +12,11 @@ Controllers: functionality. Most, but not all, controllers delegates some work to one or more providers. The controllers are responsible for choosing the right provider for any given task based upon i.e. the track's URI. See - :ref:`backend-controller-api` for more details. + :ref:`core-api` for more details. Providers: Anything specific to i.e. Spotify integration or local storage is contained in the providers. To integrate with new music sources, you just add new - providers. See :ref:`backend-provider-api` for more details. + providers. See :ref:`backend-api` for more details. .. digraph:: backend_relations diff --git a/docs/api/core.rst b/docs/api/core.rst index 1852733f..b37c8730 100644 --- a/docs/api/core.rst +++ b/docs/api/core.rst @@ -1,4 +1,4 @@ -.. _backend-controller-api: +.. _core-api: ******** Core API @@ -9,8 +9,6 @@ The core API is the interface that is used by frontends like :mod:`mopidy.frontends.mpd`. The core layer is inbetween the frontends and the backends. -If you want to implement your own backend, see the :ref:`backend-provider-api`. - Playback controller =================== diff --git a/docs/changes.rst b/docs/changes.rst index c6b7e0ac..3b77f61a 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -616,9 +616,9 @@ to this problem. :class:`mopidy.models.Album`, and :class:`mopidy.models.Track`. - Prepare for multi-backend support (see :issue:`40`) by introducing the - :ref:`provider concept `. Split the backend API into a - :ref:`backend controller API ` (for frontend use) - and a :ref:`backend provider API ` (for backend + :ref:`provider concept `. Split the backend API into a + :ref:`backend controller API ` (for frontend use) + and a :ref:`backend provider API ` (for backend implementation use), which includes the following changes: - Rename ``BaseBackend`` to :class:`mopidy.backends.base.Backend`. From aab37302a1096a19e0ed02fa1e77cf369772bf21 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 14 Sep 2012 00:53:23 +0200 Subject: [PATCH 32/44] Rename mopidy.gstreamer to mopidy.audio --- docs/modules/audio.rst | 7 +++++ docs/modules/gstreamer.rst | 7 ----- mopidy/__main__.py | 14 ++++----- mopidy/{gstreamer.py => audio/__init__.py} | 6 ++-- mopidy/backends/local/__init__.py | 33 +++++++++++----------- mopidy/backends/spotify/__init__.py | 13 ++++----- mopidy/backends/spotify/playback.py | 20 ++++++------- mopidy/backends/spotify/session_manager.py | 17 ++++++----- tests/{gstreamer_test.py => audio_test.py} | 30 +++++++++----------- tests/backends/base/current_playlist.py | 4 +-- tests/backends/base/playback.py | 8 +++--- 11 files changed, 77 insertions(+), 82 deletions(-) create mode 100644 docs/modules/audio.rst delete mode 100644 docs/modules/gstreamer.rst rename mopidy/{gstreamer.py => audio/__init__.py} (99%) rename tests/{gstreamer_test.py => audio_test.py} (64%) diff --git a/docs/modules/audio.rst b/docs/modules/audio.rst new file mode 100644 index 00000000..0f1c3bfb --- /dev/null +++ b/docs/modules/audio.rst @@ -0,0 +1,7 @@ +************************************* +:mod:`mopidy.audio` -- Audio playback +************************************* + +.. automodule:: mopidy.audio + :synopsis: Audio playback through GStreamer + :members: diff --git a/docs/modules/gstreamer.rst b/docs/modules/gstreamer.rst deleted file mode 100644 index 205b0a3e..00000000 --- a/docs/modules/gstreamer.rst +++ /dev/null @@ -1,7 +0,0 @@ -******************************************** -:mod:`mopidy.gstreamer` -- GStreamer adapter -******************************************** - -.. automodule:: mopidy.gstreamer - :synopsis: GStreamer adapter - :members: diff --git a/mopidy/__main__.py b/mopidy/__main__.py index 9bee390e..416429bc 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -30,7 +30,7 @@ sys.path.insert(0, from mopidy import (get_version, settings, OptionalDependencyError, SettingsError, DATA_PATH, SETTINGS_PATH, SETTINGS_FILE) -from mopidy.gstreamer import GStreamer +from mopidy.audio import Audio from mopidy.utils import get_class from mopidy.utils.deps import list_deps_optparse_callback from mopidy.utils.log import setup_logging @@ -51,7 +51,7 @@ def main(): setup_logging(options.verbosity_level, options.save_debug_log) check_old_folders() setup_settings(options.interactive) - setup_gstreamer() + setup_audio() setup_backend() setup_frontends() loop.run() @@ -65,7 +65,7 @@ def main(): loop.quit() stop_frontends() stop_backend() - stop_gstreamer() + stop_audio() stop_remaining_actors() @@ -117,12 +117,12 @@ def setup_settings(interactive): sys.exit(1) -def setup_gstreamer(): - GStreamer.start() +def setup_audio(): + Audio.start() -def stop_gstreamer(): - stop_actors_by_class(GStreamer) +def stop_audio(): + stop_actors_by_class(Audio) def setup_backend(): get_class(settings.BACKENDS[0]).start() diff --git a/mopidy/gstreamer.py b/mopidy/audio/__init__.py similarity index 99% rename from mopidy/gstreamer.py rename to mopidy/audio/__init__.py index 9d0cb97c..28abd75d 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/audio/__init__.py @@ -15,10 +15,10 @@ from mopidy.utils import process # Trigger install of gst mixer plugins from mopidy import mixers -logger = logging.getLogger('mopidy.gstreamer') +logger = logging.getLogger('mopidy.audio') -class GStreamer(ThreadingActor): +class Audio(ThreadingActor): """ Audio output through `GStreamer `_. @@ -31,7 +31,7 @@ class GStreamer(ThreadingActor): """ def __init__(self): - super(GStreamer, self).__init__() + super(Audio, self).__init__() self._default_caps = gst.Caps(""" audio/x-raw-int, diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index e8d918b0..022b253b 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -7,10 +7,9 @@ import shutil from pykka.actor import ThreadingActor from pykka.registry import ActorRegistry -from mopidy import core, settings, DATA_PATH +from mopidy import audio, core, settings, DATA_PATH from mopidy.backends import base from mopidy.models import Playlist, Track, Album -from mopidy.gstreamer import GStreamer from .translator import parse_m3u, parse_mpd_tag_cache @@ -58,13 +57,13 @@ class LocalBackend(ThreadingActor, base.Backend): self.uri_schemes = [u'file'] - self.gstreamer = None + self.audio = None def on_start(self): - gstreamer_refs = ActorRegistry.get_by_class(GStreamer) - assert len(gstreamer_refs) == 1, \ - 'Expected exactly one running GStreamer.' - self.gstreamer = gstreamer_refs[0].proxy() + audio_refs = ActorRegistry.get_by_class(audio.Audio) + assert len(audio_refs) == 1, \ + 'Expected exactly one running Audio instance.' + self.audio = audio_refs[0].proxy() class LocalPlaybackController(core.PlaybackController): @@ -76,32 +75,32 @@ class LocalPlaybackController(core.PlaybackController): @property def time_position(self): - return self.backend.gstreamer.get_position().get() + return self.backend.audio.get_position().get() class LocalPlaybackProvider(base.BasePlaybackProvider): def pause(self): - return self.backend.gstreamer.pause_playback().get() + return self.backend.audio.pause_playback().get() def play(self, track): - self.backend.gstreamer.prepare_change() - self.backend.gstreamer.set_uri(track.uri).get() - return self.backend.gstreamer.start_playback().get() + self.backend.audio.prepare_change() + self.backend.audio.set_uri(track.uri).get() + return self.backend.audio.start_playback().get() def resume(self): - return self.backend.gstreamer.start_playback().get() + return self.backend.audio.start_playback().get() def seek(self, time_position): - return self.backend.gstreamer.set_position(time_position).get() + return self.backend.audio.set_position(time_position).get() def stop(self): - return self.backend.gstreamer.stop_playback().get() + return self.backend.audio.stop_playback().get() def get_volume(self): - return self.backend.gstreamer.get_volume().get() + return self.backend.audio.get_volume().get() def set_volume(self, volume): - self.backend.gstreamer.set_volume(volume).get() + self.backend.audio.set_volume(volume).get() class LocalStoredPlaylistsProvider(base.BaseStoredPlaylistsProvider): diff --git a/mopidy/backends/spotify/__init__.py b/mopidy/backends/spotify/__init__.py index fef86280..1feb1c65 100644 --- a/mopidy/backends/spotify/__init__.py +++ b/mopidy/backends/spotify/__init__.py @@ -3,9 +3,8 @@ import logging from pykka.actor import ThreadingActor from pykka.registry import ActorRegistry -from mopidy import core, settings +from mopidy import audio, core, settings from mopidy.backends import base -from mopidy.gstreamer import GStreamer logger = logging.getLogger('mopidy.backends.spotify') @@ -67,7 +66,7 @@ class SpotifyBackend(ThreadingActor, base.Backend): self.uri_schemes = [u'spotify'] - self.gstreamer = None + self.audio = None self.spotify = None # Fail early if settings are not present @@ -75,10 +74,10 @@ class SpotifyBackend(ThreadingActor, base.Backend): self.password = settings.SPOTIFY_PASSWORD def on_start(self): - gstreamer_refs = ActorRegistry.get_by_class(GStreamer) - assert len(gstreamer_refs) == 1, \ - 'Expected exactly one running GStreamer.' - self.gstreamer = gstreamer_refs[0].proxy() + audio_refs = ActorRegistry.get_by_class(audio.Audio) + assert len(audio_refs) == 1, \ + 'Expected exactly one running Audio instance.' + self.audio = audio_refs[0].proxy() logger.info(u'Mopidy uses SPOTIFY(R) CORE') self.spotify = self._connect() diff --git a/mopidy/backends/spotify/playback.py b/mopidy/backends/spotify/playback.py index 70cc4617..307cf4bf 100644 --- a/mopidy/backends/spotify/playback.py +++ b/mopidy/backends/spotify/playback.py @@ -8,7 +8,7 @@ logger = logging.getLogger('mopidy.backends.spotify.playback') class SpotifyPlaybackProvider(BasePlaybackProvider): def pause(self): - return self.backend.gstreamer.pause_playback() + return self.backend.audio.pause_playback() def play(self, track): if self.backend.playback.state == self.backend.playback.PLAYING: @@ -19,10 +19,10 @@ class SpotifyPlaybackProvider(BasePlaybackProvider): self.backend.spotify.session.load( Link.from_string(track.uri).as_track()) self.backend.spotify.session.play(1) - self.backend.gstreamer.prepare_change() - self.backend.gstreamer.set_uri('appsrc://') - self.backend.gstreamer.start_playback() - self.backend.gstreamer.set_metadata(track) + self.backend.audio.prepare_change() + self.backend.audio.set_uri('appsrc://') + self.backend.audio.start_playback() + self.backend.audio.set_metadata(track) return True except SpotifyError as e: logger.info('Playback of %s failed: %s', track.uri, e) @@ -32,18 +32,18 @@ class SpotifyPlaybackProvider(BasePlaybackProvider): return self.seek(self.backend.playback.time_position) def seek(self, time_position): - self.backend.gstreamer.prepare_change() + self.backend.audio.prepare_change() self.backend.spotify.session.seek(time_position) - self.backend.gstreamer.start_playback() + self.backend.audio.start_playback() return True def stop(self): - result = self.backend.gstreamer.stop_playback() + result = self.backend.audio.stop_playback() self.backend.spotify.session.play(0) return result def get_volume(self): - return self.backend.gstreamer.get_volume().get() + return self.backend.audio.get_volume().get() def set_volume(self, volume): - self.backend.gstreamer.set_volume(volume) + self.backend.audio.set_volume(volume) diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index 481f7a94..aa3734ae 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -6,14 +6,13 @@ from spotify.manager import SpotifySessionManager as PyspotifySessionManager from pykka.registry import ActorRegistry -from mopidy import get_version, settings, CACHE_PATH +from mopidy import audio, get_version, settings, CACHE_PATH from mopidy.backends.base import Backend from mopidy.backends.spotify import BITRATES from mopidy.backends.spotify.container_manager import SpotifyContainerManager from mopidy.backends.spotify.playlist_manager import SpotifyPlaylistManager from mopidy.backends.spotify.translator import SpotifyTranslator from mopidy.models import Playlist -from mopidy.gstreamer import GStreamer from mopidy.utils.process import BaseThread logger = logging.getLogger('mopidy.backends.spotify.session_manager') @@ -34,7 +33,7 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager): BaseThread.__init__(self) self.name = 'SpotifyThread' - self.gstreamer = None + self.audio = None self.backend = None self.connected = threading.Event() @@ -50,10 +49,10 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager): self.connect() def setup(self): - gstreamer_refs = ActorRegistry.get_by_class(GStreamer) - assert len(gstreamer_refs) == 1, \ - 'Expected exactly one running gstreamer.' - self.gstreamer = gstreamer_refs[0].proxy() + audio_refs = ActorRegistry.get_by_class(audio.Audio) + assert len(audio_refs) == 1, \ + 'Expected exactly one running Audio instance.' + self.audio = audio_refs[0].proxy() backend_refs = ActorRegistry.get_by_class(Backend) assert len(backend_refs) == 1, 'Expected exactly one running backend.' @@ -117,7 +116,7 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager): 'sample_rate': sample_rate, 'channels': channels, } - self.gstreamer.emit_data(capabilites, bytes(frames)) + self.audio.emit_data(capabilites, bytes(frames)) return num_frames def play_token_lost(self, session): @@ -143,7 +142,7 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager): def end_of_track(self, session): """Callback used by pyspotify""" logger.debug(u'End of data stream reached') - self.gstreamer.emit_end_of_stream() + self.audio.emit_end_of_stream() def refresh_stored_playlists(self): """Refresh the stored playlists in the backend with fresh meta data diff --git a/tests/gstreamer_test.py b/tests/audio_test.py similarity index 64% rename from tests/gstreamer_test.py rename to tests/audio_test.py index ce20d2b4..fcafa75f 100644 --- a/tests/gstreamer_test.py +++ b/tests/audio_test.py @@ -1,7 +1,6 @@ import sys -from mopidy import settings -from mopidy.gstreamer import GStreamer +from mopidy import audio, settings from mopidy.utils.path import path_to_uri from tests import unittest, path_to_data_dir @@ -9,38 +8,38 @@ from tests import unittest, path_to_data_dir @unittest.skipIf(sys.platform == 'win32', 'Our Windows build server does not support GStreamer yet') -class GStreamerTest(unittest.TestCase): +class AudioTest(unittest.TestCase): def setUp(self): settings.MIXER = 'fakemixer track_max_volume=65536' settings.OUTPUT = 'fakesink' self.song_uri = path_to_uri(path_to_data_dir('song1.wav')) - self.gstreamer = GStreamer.start().proxy() + self.audio = audio.Audio.start().proxy() def tearDown(self): - self.gstreamer.stop() + self.audio.stop() settings.runtime.clear() def prepare_uri(self, uri): - self.gstreamer.prepare_change() - self.gstreamer.set_uri(uri) + self.audio.prepare_change() + self.audio.set_uri(uri) def test_start_playback_existing_file(self): self.prepare_uri(self.song_uri) - self.assertTrue(self.gstreamer.start_playback().get()) + self.assertTrue(self.audio.start_playback().get()) def test_start_playback_non_existing_file(self): self.prepare_uri(self.song_uri + 'bogus') - self.assertFalse(self.gstreamer.start_playback().get()) + self.assertFalse(self.audio.start_playback().get()) def test_pause_playback_while_playing(self): self.prepare_uri(self.song_uri) - self.gstreamer.start_playback() - self.assertTrue(self.gstreamer.pause_playback().get()) + self.audio.start_playback() + self.assertTrue(self.audio.pause_playback().get()) def test_stop_playback_while_playing(self): self.prepare_uri(self.song_uri) - self.gstreamer.start_playback() - self.assertTrue(self.gstreamer.stop_playback().get()) + self.audio.start_playback() + self.assertTrue(self.audio.stop_playback().get()) @unittest.SkipTest def test_deliver_data(self): @@ -52,8 +51,8 @@ class GStreamerTest(unittest.TestCase): def test_set_volume(self): for value in range(0, 101): - self.assertTrue(self.gstreamer.set_volume(value).get()) - self.assertEqual(value, self.gstreamer.get_volume().get()) + self.assertTrue(self.audio.set_volume(value).get()) + self.assertEqual(value, self.audio.get_volume().get()) @unittest.SkipTest def test_set_state_encapsulation(self): @@ -66,4 +65,3 @@ class GStreamerTest(unittest.TestCase): @unittest.SkipTest def test_invalid_output_raises_error(self): pass # TODO - diff --git a/tests/backends/base/current_playlist.py b/tests/backends/base/current_playlist.py index e99cd56c..44e9390e 100644 --- a/tests/backends/base/current_playlist.py +++ b/tests/backends/base/current_playlist.py @@ -1,8 +1,8 @@ import mock import random +from mopidy import audio from mopidy.models import CpTrack, Playlist, Track -from mopidy.gstreamer import GStreamer from tests.backends.base import populate_playlist @@ -12,7 +12,7 @@ class CurrentPlaylistControllerTest(object): def setUp(self): self.backend = self.backend_class() - self.backend.gstreamer = mock.Mock(spec=GStreamer) + self.backend.audio = mock.Mock(spec=audio.Audio) self.controller = self.backend.current_playlist self.playback = self.backend.playback diff --git a/tests/backends/base/playback.py b/tests/backends/base/playback.py index 40c49709..dcd43983 100644 --- a/tests/backends/base/playback.py +++ b/tests/backends/base/playback.py @@ -2,8 +2,8 @@ import mock import random import time +from mopidy import audio from mopidy.models import Track -from mopidy.gstreamer import GStreamer from tests import unittest from tests.backends.base import populate_playlist @@ -16,7 +16,7 @@ class PlaybackControllerTest(object): def setUp(self): self.backend = self.backend_class() - self.backend.gstreamer = mock.Mock(spec=GStreamer) + self.backend.audio = mock.Mock(spec=audio.Audio) self.playback = self.backend.playback self.current_playlist = self.backend.current_playlist @@ -729,7 +729,7 @@ class PlaybackControllerTest(object): def test_time_position_when_stopped(self): future = mock.Mock() future.get = mock.Mock(return_value=0) - self.backend.gstreamer.get_position = mock.Mock(return_value=future) + self.backend.audio.get_position = mock.Mock(return_value=future) self.assertEqual(self.playback.time_position, 0) @@ -737,7 +737,7 @@ class PlaybackControllerTest(object): def test_time_position_when_stopped_with_playlist(self): future = mock.Mock() future.get = mock.Mock(return_value=0) - self.backend.gstreamer.get_position = mock.Mock(return_value=future) + self.backend.audio.get_position = mock.Mock(return_value=future) self.assertEqual(self.playback.time_position, 0) From 4dd95804f25dd9dee2c260d8a23b2879cb50e2fa Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 14 Sep 2012 00:56:49 +0200 Subject: [PATCH 33/44] Rename mopidy.mixers to mopidy.audio.mixers --- mopidy/audio/__init__.py | 2 +- mopidy/{ => audio}/mixers/__init__.py | 6 +++--- mopidy/{ => audio}/mixers/auto.py | 2 +- mopidy/{ => audio}/mixers/fake.py | 2 +- mopidy/{ => audio}/mixers/nad.py | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) rename mopidy/{ => audio}/mixers/__init__.py (88%) rename mopidy/{ => audio}/mixers/auto.py (97%) rename mopidy/{ => audio}/mixers/fake.py (96%) rename mopidy/{ => audio}/mixers/nad.py (98%) diff --git a/mopidy/audio/__init__.py b/mopidy/audio/__init__.py index 28abd75d..78a53277 100644 --- a/mopidy/audio/__init__.py +++ b/mopidy/audio/__init__.py @@ -13,7 +13,7 @@ from mopidy.backends.base import Backend from mopidy.utils import process # Trigger install of gst mixer plugins -from mopidy import mixers +from mopidy.audio import mixers logger = logging.getLogger('mopidy.audio') diff --git a/mopidy/mixers/__init__.py b/mopidy/audio/mixers/__init__.py similarity index 88% rename from mopidy/mixers/__init__.py rename to mopidy/audio/mixers/__init__.py index 317188fc..a0247519 100644 --- a/mopidy/mixers/__init__.py +++ b/mopidy/audio/mixers/__init__.py @@ -38,6 +38,6 @@ def create_track(label, initial_volume, min_volume, max_volume, # # Keep these imports at the bottom of the file to avoid cyclic import problems # when mixers use the above code. -from mopidy.mixers.auto import AutoAudioMixer -from mopidy.mixers.fake import FakeMixer -from mopidy.mixers.nad import NadMixer +from .auto import AutoAudioMixer +from .fake import FakeMixer +from .nad import NadMixer diff --git a/mopidy/mixers/auto.py b/mopidy/audio/mixers/auto.py similarity index 97% rename from mopidy/mixers/auto.py rename to mopidy/audio/mixers/auto.py index f4bd0f92..1233afa3 100644 --- a/mopidy/mixers/auto.py +++ b/mopidy/audio/mixers/auto.py @@ -5,7 +5,7 @@ import gst import logging -logger = logging.getLogger('mopidy.mixers.auto') +logger = logging.getLogger('mopidy.audio.mixers.auto') # TODO: we might want to add some ranking to the mixers we know about? diff --git a/mopidy/mixers/fake.py b/mopidy/audio/mixers/fake.py similarity index 96% rename from mopidy/mixers/fake.py rename to mopidy/audio/mixers/fake.py index 3c47ef33..c5faa03f 100644 --- a/mopidy/mixers/fake.py +++ b/mopidy/audio/mixers/fake.py @@ -3,7 +3,7 @@ pygst.require('0.10') import gobject import gst -from mopidy.mixers import create_track +from mopidy.audio.mixers import create_track class FakeMixer(gst.Element, gst.ImplementsInterface, gst.interfaces.Mixer): diff --git a/mopidy/mixers/nad.py b/mopidy/audio/mixers/nad.py similarity index 98% rename from mopidy/mixers/nad.py rename to mopidy/audio/mixers/nad.py index de959d41..667dee53 100644 --- a/mopidy/mixers/nad.py +++ b/mopidy/audio/mixers/nad.py @@ -12,10 +12,10 @@ except ImportError: from pykka.actor import ThreadingActor -from mopidy.mixers import create_track +from mopidy.audio.mixers import create_track -logger = logging.getLogger('mopidy.mixers.nad') +logger = logging.getLogger('mopidy.audio.mixers.nad') class NadMixer(gst.Element, gst.ImplementsInterface, gst.interfaces.Mixer): From 2ba05f940599914f9ef49c4813b280a9206c4b80 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 14 Sep 2012 00:28:14 +0200 Subject: [PATCH 34/44] Add PlaybackState enum --- docs/api/core.rst | 3 + mopidy/backends/spotify/playback.py | 3 +- mopidy/core/__init__.py | 2 +- mopidy/core/playback.py | 67 +++++++------ mopidy/frontends/mpd/protocol/playback.py | 14 +-- mopidy/frontends/mpd/protocol/status.py | 12 +-- mopidy/frontends/mpris/objects.py | 17 ++-- mopidy/models.py | 1 + tests/backends/base/current_playlist.py | 9 +- tests/backends/base/playback.py | 93 ++++++++++--------- tests/backends/local/playback_test.py | 7 +- tests/frontends/mpd/protocol/playback_test.py | 9 +- tests/frontends/mpd/status_test.py | 9 +- .../frontends/mpris/player_interface_test.py | 9 +- 14 files changed, 136 insertions(+), 119 deletions(-) diff --git a/docs/api/core.rst b/docs/api/core.rst index b37c8730..e74d9f45 100644 --- a/docs/api/core.rst +++ b/docs/api/core.rst @@ -16,6 +16,9 @@ Playback controller Manages playback, with actions like play, pause, stop, next, previous, seek, and volume control. +.. autoclass:: mopidy.core.PlaybackState + :members: + .. autoclass:: mopidy.core.PlaybackController :members: diff --git a/mopidy/backends/spotify/playback.py b/mopidy/backends/spotify/playback.py index 307cf4bf..cf16c35e 100644 --- a/mopidy/backends/spotify/playback.py +++ b/mopidy/backends/spotify/playback.py @@ -3,6 +3,7 @@ import logging from spotify import Link, SpotifyError from mopidy.backends.base import BasePlaybackProvider +from mopidy.core import PlaybackState logger = logging.getLogger('mopidy.backends.spotify.playback') @@ -11,7 +12,7 @@ class SpotifyPlaybackProvider(BasePlaybackProvider): return self.backend.audio.pause_playback() def play(self, track): - if self.backend.playback.state == self.backend.playback.PLAYING: + if self.backend.playback.state == PlaybackState.PLAYING: self.backend.spotify.session.play(0) if track.uri is None: return False diff --git a/mopidy/core/__init__.py b/mopidy/core/__init__.py index 16c09665..87df96c9 100644 --- a/mopidy/core/__init__.py +++ b/mopidy/core/__init__.py @@ -1,4 +1,4 @@ from .current_playlist import CurrentPlaylistController from .library import LibraryController -from .playback import PlaybackController +from .playback import PlaybackController, PlaybackState from .stored_playlists import StoredPlaylistsController diff --git a/mopidy/core/playback.py b/mopidy/core/playback.py index a0c3ef30..dfd1676e 100644 --- a/mopidy/core/playback.py +++ b/mopidy/core/playback.py @@ -20,6 +20,22 @@ def option_wrapper(name, default): return property(get_option, set_option) + +class PlaybackState(object): + """ + Enum of playback states. + """ + + #: Constant representing the paused state. + PAUSED = u'paused' + + #: Constant representing the playing state. + PLAYING = u'playing' + + #: Constant representing the stopped state. + STOPPED = u'stopped' + + class PlaybackController(object): """ :param backend: the backend @@ -33,15 +49,6 @@ class PlaybackController(object): pykka_traversable = True - #: Constant representing the paused state. - PAUSED = u'paused' - - #: Constant representing the playing state. - PLAYING = u'playing' - - #: Constant representing the stopped state. - STOPPED = u'stopped' - #: :class:`True` #: Tracks are removed from the playlist when they have been played. #: :class:`False` @@ -77,7 +84,7 @@ class PlaybackController(object): def __init__(self, backend, provider): self.backend = backend self.provider = provider - self._state = self.STOPPED + self._state = PlaybackState.STOPPED self._shuffled = [] self._first_shuffle = True self.play_time_accumulated = 0 @@ -287,24 +294,26 @@ class PlaybackController(object): # FIXME play_time stuff assumes backend does not have a better way of # handeling this stuff :/ - if (old_state in (self.PLAYING, self.STOPPED) - and new_state == self.PLAYING): + if (old_state in (PlaybackState.PLAYING, PlaybackState.STOPPED) + and new_state == PlaybackState.PLAYING): self._play_time_start() - elif old_state == self.PLAYING and new_state == self.PAUSED: + elif (old_state == PlaybackState.PLAYING + and new_state == PlaybackState.PAUSED): self._play_time_pause() - elif old_state == self.PAUSED and new_state == self.PLAYING: + elif (old_state == PlaybackState.PAUSED + and new_state == PlaybackState.PLAYING): self._play_time_resume() @property def time_position(self): """Time position in milliseconds.""" - if self.state == self.PLAYING: + if self.state == PlaybackState.PLAYING: time_since_started = (self._current_wall_time - self.play_time_started) return self.play_time_accumulated + time_since_started - elif self.state == self.PAUSED: + elif self.state == PlaybackState.PAUSED: return self.play_time_accumulated - elif self.state == self.STOPPED: + elif self.state == PlaybackState.STOPPED: return 0 def _play_time_start(self): @@ -345,16 +354,16 @@ class PlaybackController(object): old_state = self.state self.stop() self.current_cp_track = cp_track - if old_state == self.PLAYING: + if old_state == PlaybackState.PLAYING: self.play(on_error_step=on_error_step) - elif old_state == self.PAUSED: + elif old_state == PlaybackState.PAUSED: self.pause() def on_end_of_track(self): """ Tell the playback controller that end of track is reached. """ - if self.state == self.STOPPED: + if self.state == PlaybackState.STOPPED: return original_cp_track = self.current_cp_track @@ -398,7 +407,7 @@ class PlaybackController(object): def pause(self): """Pause playback.""" if self.provider.pause(): - self.state = self.PAUSED + self.state = PlaybackState.PAUSED self._trigger_track_playback_paused() def play(self, cp_track=None, on_error_step=1): @@ -417,7 +426,7 @@ class PlaybackController(object): if cp_track is not None: assert cp_track in self.backend.current_playlist.cp_tracks elif cp_track is None: - if self.state == self.PAUSED: + if self.state == PlaybackState.PAUSED: return self.resume() elif self.current_cp_track is not None: cp_track = self.current_cp_track @@ -428,7 +437,7 @@ class PlaybackController(object): if cp_track is not None: self.current_cp_track = cp_track - self.state = self.PLAYING + self.state = PlaybackState.PLAYING if not self.provider.play(cp_track.track): # Track is not playable if self.random and self._shuffled: @@ -455,8 +464,8 @@ class PlaybackController(object): def resume(self): """If paused, resume playing the current track.""" - if self.state == self.PAUSED and self.provider.resume(): - self.state = self.PLAYING + if self.state == PlaybackState.PAUSED and self.provider.resume(): + self.state = PlaybackState.PLAYING self._trigger_track_playback_resumed() def seek(self, time_position): @@ -470,9 +479,9 @@ class PlaybackController(object): if not self.backend.current_playlist.tracks: return False - if self.state == self.STOPPED: + if self.state == PlaybackState.STOPPED: self.play() - elif self.state == self.PAUSED: + elif self.state == PlaybackState.PAUSED: self.resume() if time_position < 0: @@ -497,10 +506,10 @@ class PlaybackController(object): stopping :type clear_current_track: boolean """ - if self.state != self.STOPPED: + if self.state != PlaybackState.STOPPED: if self.provider.stop(): self._trigger_track_playback_ended() - self.state = self.STOPPED + self.state = PlaybackState.STOPPED if clear_current_track: self.current_cp_track = None diff --git a/mopidy/frontends/mpd/protocol/playback.py b/mopidy/frontends/mpd/protocol/playback.py index e6bb6478..b0c299c8 100644 --- a/mopidy/frontends/mpd/protocol/playback.py +++ b/mopidy/frontends/mpd/protocol/playback.py @@ -1,4 +1,4 @@ -from mopidy import core +from mopidy.core import PlaybackState from mopidy.frontends.mpd.protocol import handle_request from mopidy.frontends.mpd.exceptions import (MpdArgError, MpdNoExistError, MpdNotImplemented) @@ -104,11 +104,9 @@ def pause(context, state=None): - Calls ``pause`` without any arguments to toogle pause. """ if state is None: - if (context.backend.playback.state.get() == - core.PlaybackController.PLAYING): + if (context.backend.playback.state.get() == PlaybackState.PLAYING): context.backend.playback.pause() - elif (context.backend.playback.state.get() == - core.PlaybackController.PAUSED): + elif (context.backend.playback.state.get() == PlaybackState.PAUSED): context.backend.playback.resume() elif int(state): context.backend.playback.pause() @@ -185,11 +183,9 @@ def playpos(context, songpos): raise MpdArgError(u'Bad song index', command=u'play') def _play_minus_one(context): - if (context.backend.playback.state.get() == - core.PlaybackController.PLAYING): + if (context.backend.playback.state.get() == PlaybackState.PLAYING): return # Nothing to do - elif (context.backend.playback.state.get() == - core.PlaybackController.PAUSED): + elif (context.backend.playback.state.get() == PlaybackState.PAUSED): return context.backend.playback.resume().get() elif context.backend.playback.current_cp_track.get() is not None: cp_track = context.backend.playback.current_cp_track.get() diff --git a/mopidy/frontends/mpd/protocol/status.py b/mopidy/frontends/mpd/protocol/status.py index 279978aa..fc24e1e1 100644 --- a/mopidy/frontends/mpd/protocol/status.py +++ b/mopidy/frontends/mpd/protocol/status.py @@ -1,6 +1,6 @@ import pykka.future -from mopidy import core +from mopidy.core import PlaybackState from mopidy.frontends.mpd.exceptions import MpdNotImplemented from mopidy.frontends.mpd.protocol import handle_request from mopidy.frontends.mpd.translator import track_to_mpd_format @@ -194,8 +194,8 @@ def status(context): if futures['playback.current_cp_track'].get() is not None: result.append(('song', _status_songpos(futures))) result.append(('songid', _status_songid(futures))) - if futures['playback.state'].get() in (core.PlaybackController.PLAYING, - core.PlaybackController.PAUSED): + if futures['playback.state'].get() in (PlaybackState.PLAYING, + PlaybackState.PAUSED): result.append(('time', _status_time(futures))) result.append(('elapsed', _status_time_elapsed(futures))) result.append(('bitrate', _status_bitrate(futures))) @@ -239,11 +239,11 @@ def _status_songpos(futures): def _status_state(futures): state = futures['playback.state'].get() - if state == core.PlaybackController.PLAYING: + if state == PlaybackState.PLAYING: return u'play' - elif state == core.PlaybackController.STOPPED: + elif state == PlaybackState.STOPPED: return u'stop' - elif state == core.PlaybackController.PAUSED: + elif state == PlaybackState.PAUSED: return u'pause' def _status_time(futures): diff --git a/mopidy/frontends/mpris/objects.py b/mopidy/frontends/mpris/objects.py index bcd3de5c..93669977 100644 --- a/mopidy/frontends/mpris/objects.py +++ b/mopidy/frontends/mpris/objects.py @@ -14,8 +14,9 @@ except ImportError as import_error: from pykka.registry import ActorRegistry -from mopidy import core, settings +from mopidy import settings from mopidy.backends.base import Backend +from mopidy.core import PlaybackState from mopidy.utils.process import exit_process # Must be done before dbus.SessionBus() is called @@ -197,11 +198,11 @@ class MprisObject(dbus.service.Object): logger.debug(u'%s.PlayPause not allowed', PLAYER_IFACE) return state = self.backend.playback.state.get() - if state == core.PlaybackController.PLAYING: + if state == PlaybackState.PLAYING: self.backend.playback.pause().get() - elif state == core.PlaybackController.PAUSED: + elif state == PlaybackState.PAUSED: self.backend.playback.resume().get() - elif state == core.PlaybackController.STOPPED: + elif state == PlaybackState.STOPPED: self.backend.playback.play().get() @dbus.service.method(dbus_interface=PLAYER_IFACE) @@ -219,7 +220,7 @@ class MprisObject(dbus.service.Object): logger.debug(u'%s.Play not allowed', PLAYER_IFACE) return state = self.backend.playback.state.get() - if state == core.PlaybackController.PAUSED: + if state == PlaybackState.PAUSED: self.backend.playback.resume().get() else: self.backend.playback.play().get() @@ -286,11 +287,11 @@ class MprisObject(dbus.service.Object): def get_PlaybackStatus(self): state = self.backend.playback.state.get() - if state == core.PlaybackController.PLAYING: + if state == PlaybackState.PLAYING: return 'Playing' - elif state == core.PlaybackController.PAUSED: + elif state == PlaybackState.PAUSED: return 'Paused' - elif state == core.PlaybackController.STOPPED: + elif state == PlaybackState.STOPPED: return 'Stopped' def get_LoopStatus(self): diff --git a/mopidy/models.py b/mopidy/models.py index 6a2af914..507ca088 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -1,5 +1,6 @@ from collections import namedtuple + class ImmutableObject(object): """ Superclass for immutable objects whose fields can only be modified via the diff --git a/tests/backends/base/current_playlist.py b/tests/backends/base/current_playlist.py index 44e9390e..bfc0a254 100644 --- a/tests/backends/base/current_playlist.py +++ b/tests/backends/base/current_playlist.py @@ -2,6 +2,7 @@ import mock import random from mopidy import audio +from mopidy.core import PlaybackState from mopidy.models import CpTrack, Playlist, Track from tests.backends.base import populate_playlist @@ -71,9 +72,9 @@ class CurrentPlaylistControllerTest(object): @populate_playlist def test_clear_when_playing(self): self.playback.play() - self.assertEqual(self.playback.state, self.playback.PLAYING) + self.assertEqual(self.playback.state, PlaybackState.PLAYING) self.controller.clear() - self.assertEqual(self.playback.state, self.playback.STOPPED) + self.assertEqual(self.playback.state, PlaybackState.STOPPED) def test_get_by_uri_returns_unique_match(self): track = Track(uri='a') @@ -134,13 +135,13 @@ class CurrentPlaylistControllerTest(object): self.playback.play() track = self.playback.current_track self.controller.append(self.controller.tracks[1:2]) - self.assertEqual(self.playback.state, self.playback.PLAYING) + self.assertEqual(self.playback.state, PlaybackState.PLAYING) self.assertEqual(self.playback.current_track, track) @populate_playlist def test_append_preserves_stopped_state(self): self.controller.append(self.controller.tracks[1:2]) - self.assertEqual(self.playback.state, self.playback.STOPPED) + self.assertEqual(self.playback.state, PlaybackState.STOPPED) self.assertEqual(self.playback.current_track, None) def test_index_returns_index_of_track(self): diff --git a/tests/backends/base/playback.py b/tests/backends/base/playback.py index dcd43983..b9661df9 100644 --- a/tests/backends/base/playback.py +++ b/tests/backends/base/playback.py @@ -3,6 +3,7 @@ import random import time from mopidy import audio +from mopidy.core import PlaybackState from mopidy.models import Track from tests import unittest @@ -26,21 +27,21 @@ class PlaybackControllerTest(object): 'First song needs to be at least 2000 miliseconds' def test_initial_state_is_stopped(self): - self.assertEqual(self.playback.state, self.playback.STOPPED) + self.assertEqual(self.playback.state, PlaybackState.STOPPED) def test_play_with_empty_playlist(self): - self.assertEqual(self.playback.state, self.playback.STOPPED) + self.assertEqual(self.playback.state, PlaybackState.STOPPED) self.playback.play() - self.assertEqual(self.playback.state, self.playback.STOPPED) + self.assertEqual(self.playback.state, PlaybackState.STOPPED) def test_play_with_empty_playlist_return_value(self): self.assertEqual(self.playback.play(), None) @populate_playlist def test_play_state(self): - self.assertEqual(self.playback.state, self.playback.STOPPED) + self.assertEqual(self.playback.state, PlaybackState.STOPPED) self.playback.play() - self.assertEqual(self.playback.state, self.playback.PLAYING) + self.assertEqual(self.playback.state, PlaybackState.PLAYING) @populate_playlist def test_play_return_value(self): @@ -48,9 +49,9 @@ class PlaybackControllerTest(object): @populate_playlist def test_play_track_state(self): - self.assertEqual(self.playback.state, self.playback.STOPPED) + self.assertEqual(self.playback.state, PlaybackState.STOPPED) self.playback.play(self.current_playlist.cp_tracks[-1]) - self.assertEqual(self.playback.state, self.playback.PLAYING) + self.assertEqual(self.playback.state, PlaybackState.PLAYING) @populate_playlist def test_play_track_return_value(self): @@ -70,7 +71,7 @@ class PlaybackControllerTest(object): track = self.playback.current_track self.playback.pause() self.playback.play() - self.assertEqual(self.playback.state, self.playback.PLAYING) + self.assertEqual(self.playback.state, PlaybackState.PLAYING) self.assertEqual(track, self.playback.current_track) @populate_playlist @@ -81,7 +82,7 @@ class PlaybackControllerTest(object): track = self.playback.current_track self.playback.pause() self.playback.play() - self.assertEqual(self.playback.state, self.playback.PLAYING) + self.assertEqual(self.playback.state, PlaybackState.PLAYING) self.assertEqual(track, self.playback.current_track) @populate_playlist @@ -106,12 +107,12 @@ class PlaybackControllerTest(object): def test_current_track_after_completed_playlist(self): self.playback.play(self.current_playlist.cp_tracks[-1]) self.playback.on_end_of_track() - self.assertEqual(self.playback.state, self.playback.STOPPED) + self.assertEqual(self.playback.state, PlaybackState.STOPPED) self.assertEqual(self.playback.current_track, None) self.playback.play(self.current_playlist.cp_tracks[-1]) self.playback.next() - self.assertEqual(self.playback.state, self.playback.STOPPED) + self.assertEqual(self.playback.state, PlaybackState.STOPPED) self.assertEqual(self.playback.current_track, None) @populate_playlist @@ -141,17 +142,17 @@ class PlaybackControllerTest(object): self.playback.next() self.playback.stop() self.playback.previous() - self.assertEqual(self.playback.state, self.playback.STOPPED) + self.assertEqual(self.playback.state, PlaybackState.STOPPED) @populate_playlist def test_previous_at_start_of_playlist(self): self.playback.previous() - self.assertEqual(self.playback.state, self.playback.STOPPED) + self.assertEqual(self.playback.state, PlaybackState.STOPPED) self.assertEqual(self.playback.current_track, None) def test_previous_for_empty_playlist(self): self.playback.previous() - self.assertEqual(self.playback.state, self.playback.STOPPED) + self.assertEqual(self.playback.state, PlaybackState.STOPPED) self.assertEqual(self.playback.current_track, None) @populate_playlist @@ -185,20 +186,20 @@ class PlaybackControllerTest(object): @populate_playlist def test_next_does_not_trigger_playback(self): self.playback.next() - self.assertEqual(self.playback.state, self.playback.STOPPED) + self.assertEqual(self.playback.state, PlaybackState.STOPPED) @populate_playlist def test_next_at_end_of_playlist(self): self.playback.play() for i, track in enumerate(self.tracks): - self.assertEqual(self.playback.state, self.playback.PLAYING) + self.assertEqual(self.playback.state, PlaybackState.PLAYING) self.assertEqual(self.playback.current_track, track) self.assertEqual(self.playback.current_playlist_position, i) self.playback.next() - self.assertEqual(self.playback.state, self.playback.STOPPED) + self.assertEqual(self.playback.state, PlaybackState.STOPPED) @populate_playlist def test_next_until_end_of_playlist_and_play_from_start(self): @@ -208,15 +209,15 @@ class PlaybackControllerTest(object): self.playback.next() self.assertEqual(self.playback.current_track, None) - self.assertEqual(self.playback.state, self.playback.STOPPED) + self.assertEqual(self.playback.state, PlaybackState.STOPPED) self.playback.play() - self.assertEqual(self.playback.state, self.playback.PLAYING) + self.assertEqual(self.playback.state, PlaybackState.PLAYING) self.assertEqual(self.playback.current_track, self.tracks[0]) def test_next_for_empty_playlist(self): self.playback.next() - self.assertEqual(self.playback.state, self.playback.STOPPED) + self.assertEqual(self.playback.state, PlaybackState.STOPPED) @populate_playlist def test_next_skips_to_next_track_on_failure(self): @@ -321,20 +322,20 @@ class PlaybackControllerTest(object): @populate_playlist def test_end_of_track_does_not_trigger_playback(self): self.playback.on_end_of_track() - self.assertEqual(self.playback.state, self.playback.STOPPED) + self.assertEqual(self.playback.state, PlaybackState.STOPPED) @populate_playlist def test_end_of_track_at_end_of_playlist(self): self.playback.play() for i, track in enumerate(self.tracks): - self.assertEqual(self.playback.state, self.playback.PLAYING) + self.assertEqual(self.playback.state, PlaybackState.PLAYING) self.assertEqual(self.playback.current_track, track) self.assertEqual(self.playback.current_playlist_position, i) self.playback.on_end_of_track() - self.assertEqual(self.playback.state, self.playback.STOPPED) + self.assertEqual(self.playback.state, PlaybackState.STOPPED) @populate_playlist def test_end_of_track_until_end_of_playlist_and_play_from_start(self): @@ -344,15 +345,15 @@ class PlaybackControllerTest(object): self.playback.on_end_of_track() self.assertEqual(self.playback.current_track, None) - self.assertEqual(self.playback.state, self.playback.STOPPED) + self.assertEqual(self.playback.state, PlaybackState.STOPPED) self.playback.play() - self.assertEqual(self.playback.state, self.playback.PLAYING) + self.assertEqual(self.playback.state, PlaybackState.PLAYING) self.assertEqual(self.playback.current_track, self.tracks[0]) def test_end_of_track_for_empty_playlist(self): self.playback.on_end_of_track() - self.assertEqual(self.playback.state, self.playback.STOPPED) + self.assertEqual(self.playback.state, PlaybackState.STOPPED) @populate_playlist def test_end_of_track_skips_to_next_track_on_failure(self): @@ -534,13 +535,13 @@ class PlaybackControllerTest(object): self.playback.play() current_track = self.playback.current_track self.backend.current_playlist.append([self.tracks[2]]) - self.assertEqual(self.playback.state, self.playback.PLAYING) + self.assertEqual(self.playback.state, PlaybackState.PLAYING) self.assertEqual(self.playback.current_track, current_track) @populate_playlist def test_on_current_playlist_change_when_stopped(self): self.backend.current_playlist.append([self.tracks[2]]) - self.assertEqual(self.playback.state, self.playback.STOPPED) + self.assertEqual(self.playback.state, PlaybackState.STOPPED) self.assertEqual(self.playback.current_track, None) @populate_playlist @@ -549,26 +550,26 @@ class PlaybackControllerTest(object): self.playback.pause() current_track = self.playback.current_track self.backend.current_playlist.append([self.tracks[2]]) - self.assertEqual(self.playback.state, self.backend.playback.PAUSED) + self.assertEqual(self.playback.state, PlaybackState.PAUSED) self.assertEqual(self.playback.current_track, current_track) @populate_playlist def test_pause_when_stopped(self): self.playback.pause() - self.assertEqual(self.playback.state, self.playback.PAUSED) + self.assertEqual(self.playback.state, PlaybackState.PAUSED) @populate_playlist def test_pause_when_playing(self): self.playback.play() self.playback.pause() - self.assertEqual(self.playback.state, self.playback.PAUSED) + self.assertEqual(self.playback.state, PlaybackState.PAUSED) @populate_playlist def test_pause_when_paused(self): self.playback.play() self.playback.pause() self.playback.pause() - self.assertEqual(self.playback.state, self.playback.PAUSED) + self.assertEqual(self.playback.state, PlaybackState.PAUSED) @populate_playlist def test_pause_return_value(self): @@ -578,20 +579,20 @@ class PlaybackControllerTest(object): @populate_playlist def test_resume_when_stopped(self): self.playback.resume() - self.assertEqual(self.playback.state, self.playback.STOPPED) + self.assertEqual(self.playback.state, PlaybackState.STOPPED) @populate_playlist def test_resume_when_playing(self): self.playback.play() self.playback.resume() - self.assertEqual(self.playback.state, self.playback.PLAYING) + self.assertEqual(self.playback.state, PlaybackState.PLAYING) @populate_playlist def test_resume_when_paused(self): self.playback.play() self.playback.pause() self.playback.resume() - self.assertEqual(self.playback.state, self.playback.PLAYING) + self.assertEqual(self.playback.state, PlaybackState.PLAYING) @populate_playlist def test_resume_return_value(self): @@ -624,12 +625,12 @@ class PlaybackControllerTest(object): def test_seek_on_empty_playlist_updates_position(self): self.playback.seek(0) - self.assertEqual(self.playback.state, self.playback.STOPPED) + self.assertEqual(self.playback.state, PlaybackState.STOPPED) @populate_playlist def test_seek_when_stopped_triggers_play(self): self.playback.seek(0) - self.assertEqual(self.playback.state, self.playback.PLAYING) + self.assertEqual(self.playback.state, PlaybackState.PLAYING) @populate_playlist def test_seek_when_playing(self): @@ -666,7 +667,7 @@ class PlaybackControllerTest(object): self.playback.play() self.playback.pause() self.playback.seek(0) - self.assertEqual(self.playback.state, self.playback.PLAYING) + self.assertEqual(self.playback.state, PlaybackState.PLAYING) @unittest.SkipTest @populate_playlist @@ -686,7 +687,7 @@ class PlaybackControllerTest(object): def test_seek_beyond_end_of_song_for_last_track(self): self.playback.play(self.current_playlist.cp_tracks[-1]) self.playback.seek(self.current_playlist.tracks[-1].length * 100) - self.assertEqual(self.playback.state, self.playback.STOPPED) + self.assertEqual(self.playback.state, PlaybackState.STOPPED) @unittest.SkipTest @populate_playlist @@ -702,25 +703,25 @@ class PlaybackControllerTest(object): self.playback.seek(-1000) position = self.playback.time_position self.assert_(position >= 0, position) - self.assertEqual(self.playback.state, self.playback.PLAYING) + self.assertEqual(self.playback.state, PlaybackState.PLAYING) @populate_playlist def test_stop_when_stopped(self): self.playback.stop() - self.assertEqual(self.playback.state, self.playback.STOPPED) + self.assertEqual(self.playback.state, PlaybackState.STOPPED) @populate_playlist def test_stop_when_playing(self): self.playback.play() self.playback.stop() - self.assertEqual(self.playback.state, self.playback.STOPPED) + self.assertEqual(self.playback.state, PlaybackState.STOPPED) @populate_playlist def test_stop_when_paused(self): self.playback.play() self.playback.pause() self.playback.stop() - self.assertEqual(self.playback.state, self.playback.STOPPED) + self.assertEqual(self.playback.state, PlaybackState.STOPPED) def test_stop_return_value(self): self.playback.play() @@ -810,7 +811,7 @@ class PlaybackControllerTest(object): def test_end_of_playlist_stops(self): self.playback.play(self.current_playlist.cp_tracks[-1]) self.playback.on_end_of_track() - self.assertEqual(self.playback.state, self.playback.STOPPED) + self.assertEqual(self.playback.state, PlaybackState.STOPPED) def test_repeat_off_by_default(self): self.assertEqual(self.playback.repeat, False) @@ -835,9 +836,9 @@ class PlaybackControllerTest(object): for _ in self.tracks: self.playback.next() self.assertNotEqual(self.playback.track_at_next, None) - self.assertEqual(self.playback.state, self.playback.STOPPED) + self.assertEqual(self.playback.state, PlaybackState.STOPPED) self.playback.play() - self.assertEqual(self.playback.state, self.playback.PLAYING) + self.assertEqual(self.playback.state, PlaybackState.PLAYING) @populate_playlist def test_random_until_end_of_playlist_with_repeat(self): diff --git a/tests/backends/local/playback_test.py b/tests/backends/local/playback_test.py index 788fe33c..4dede6ad 100644 --- a/tests/backends/local/playback_test.py +++ b/tests/backends/local/playback_test.py @@ -2,6 +2,7 @@ import sys from mopidy import settings from mopidy.backends.local import LocalBackend +from mopidy.core import PlaybackState from mopidy.models import Track from mopidy.utils.path import path_to_uri @@ -39,14 +40,14 @@ class LocalPlaybackControllerTest(PlaybackControllerTest, unittest.TestCase): def test_play_mp3(self): self.add_track('blank.mp3') self.playback.play() - self.assertEqual(self.playback.state, self.playback.PLAYING) + self.assertEqual(self.playback.state, PlaybackState.PLAYING) def test_play_ogg(self): self.add_track('blank.ogg') self.playback.play() - self.assertEqual(self.playback.state, self.playback.PLAYING) + self.assertEqual(self.playback.state, PlaybackState.PLAYING) def test_play_flac(self): self.add_track('blank.flac') self.playback.play() - self.assertEqual(self.playback.state, self.playback.PLAYING) + self.assertEqual(self.playback.state, PlaybackState.PLAYING) diff --git a/tests/frontends/mpd/protocol/playback_test.py b/tests/frontends/mpd/protocol/playback_test.py index 514c1599..88452d3d 100644 --- a/tests/frontends/mpd/protocol/playback_test.py +++ b/tests/frontends/mpd/protocol/playback_test.py @@ -1,12 +1,13 @@ -from mopidy import core +from mopidy.core import PlaybackState from mopidy.models import Track from tests import unittest from tests.frontends.mpd import protocol -PAUSED = core.PlaybackController.PAUSED -PLAYING = core.PlaybackController.PLAYING -STOPPED = core.PlaybackController.STOPPED + +PAUSED = PlaybackState.PAUSED +PLAYING = PlaybackState.PLAYING +STOPPED = PlaybackState.STOPPED class PlaybackOptionsHandlerTest(protocol.BaseTestCase): diff --git a/tests/frontends/mpd/status_test.py b/tests/frontends/mpd/status_test.py index 8fd8895d..455dba45 100644 --- a/tests/frontends/mpd/status_test.py +++ b/tests/frontends/mpd/status_test.py @@ -1,14 +1,15 @@ -from mopidy import core from mopidy.backends import dummy +from mopidy.core import PlaybackState from mopidy.frontends.mpd import dispatcher from mopidy.frontends.mpd.protocol import status from mopidy.models import Track from tests import unittest -PAUSED = core.PlaybackController.PAUSED -PLAYING = core.PlaybackController.PLAYING -STOPPED = core.PlaybackController.STOPPED + +PAUSED = PlaybackState.PAUSED +PLAYING = PlaybackState.PLAYING +STOPPED = PlaybackState.STOPPED # FIXME migrate to using protocol.BaseTestCase instead of status.stats # directly? diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index 48be504f..a6415b2f 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -2,8 +2,9 @@ import sys import mock -from mopidy import core, OptionalDependencyError +from mopidy import OptionalDependencyError from mopidy.backends.dummy import DummyBackend +from mopidy.core import PlaybackState from mopidy.models import Album, Artist, Track try: @@ -13,9 +14,9 @@ except OptionalDependencyError: from tests import unittest -PLAYING = core.PlaybackController.PLAYING -PAUSED = core.PlaybackController.PAUSED -STOPPED = core.PlaybackController.STOPPED +PLAYING = PlaybackState.PLAYING +PAUSED = PlaybackState.PAUSED +STOPPED = PlaybackState.STOPPED @unittest.skipUnless(sys.platform.startswith('linux'), 'requires Linux') From 25b14cbfb315d901b2524377b35f3020bf4dd4dd Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 14 Sep 2012 01:43:45 +0200 Subject: [PATCH 35/44] docs: Fix broken autodocs --- docs/conf.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index d8aa118e..cd59d14d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -29,6 +29,8 @@ class Mock(object): def __getattr__(self, name): if name in ('__file__', '__path__'): return '/dev/null' + elif name[0] == name[0].upper() and not name.startswith('MIXER_TRACK'): + return type(name, (), {}) else: return Mock() From 2321a77e37c7eea9028f82ce041b050b712b6db5 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 14 Sep 2012 01:45:27 +0200 Subject: [PATCH 36/44] docs: Fix Sphinx warning --- mopidy/audio/__init__.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/mopidy/audio/__init__.py b/mopidy/audio/__init__.py index 78a53277..25c53c5a 100644 --- a/mopidy/audio/__init__.py +++ b/mopidy/audio/__init__.py @@ -331,9 +331,14 @@ class Audio(ThreadingActor): """ Get volume level of the installed mixer. - 0 == muted. - 100 == max volume for given system. - None == no mixer present, i.e. volume unknown. + Example values: + + 0: + Muted. + 100: + Max volume for given system. + :class:`None`: + No mixer present, so the volume is unknown. :rtype: int in range [0..100] or :class:`None` """ From fd60d42be6eacfa5a7e1811c78580ed8f8f909ad Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 14 Sep 2012 02:08:23 +0200 Subject: [PATCH 37/44] Make LocalPlaybackProvider the default implementation of BasePlaybackProvider --- mopidy/backends/base/playback.py | 30 +++++++++++++++-------------- mopidy/backends/local/__init__.py | 27 +------------------------- mopidy/backends/spotify/playback.py | 12 +----------- 3 files changed, 18 insertions(+), 51 deletions(-) diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index d2b9edd9..ae5a4383 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -13,73 +13,75 @@ class BasePlaybackProvider(object): """ Pause playback. - *MUST be implemented by subclass.* + *MAY be reimplemented by subclass.* :rtype: :class:`True` if successful, else :class:`False` """ - raise NotImplementedError + return self.backend.audio.pause_playback().get() def play(self, track): """ Play given track. - *MUST be implemented by subclass.* + *MAY be reimplemented by subclass.* :param track: the track to play :type track: :class:`mopidy.models.Track` :rtype: :class:`True` if successful, else :class:`False` """ - raise NotImplementedError + self.backend.audio.prepare_change() + self.backend.audio.set_uri(track.uri).get() + return self.backend.audio.start_playback().get() def resume(self): """ Resume playback at the same time position playback was paused. - *MUST be implemented by subclass.* + *MAY be reimplemented by subclass.* :rtype: :class:`True` if successful, else :class:`False` """ - raise NotImplementedError + return self.backend.audio.start_playback().get() def seek(self, time_position): """ Seek to a given time position. - *MUST be implemented by subclass.* + *MAY be reimplemented by subclass.* :param time_position: time position in milliseconds :type time_position: int :rtype: :class:`True` if successful, else :class:`False` """ - raise NotImplementedError + return self.backend.audio.set_position(time_position).get() def stop(self): """ Stop playback. - *MUST be implemented by subclass.* + *MAY be reimplemented by subclass.* :rtype: :class:`True` if successful, else :class:`False` """ - raise NotImplementedError + return self.backend.audio.stop_playback().get() def get_volume(self): """ Get current volume - *MUST be implemented by subclass.* + *MAY be reimplemented by subclass.* :rtype: int [0..100] or :class:`None` """ - raise NotImplementedError + return self.backend.audio.get_volume().get() def set_volume(self, volume): """ Get current volume - *MUST be implemented by subclass.* + *MAY be reimplemented by subclass.* :param: volume :type volume: int [0..100] """ - raise NotImplementedError + self.backend.audio.set_volume(volume) diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index 022b253b..b49406c6 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -47,7 +47,7 @@ class LocalBackend(ThreadingActor, base.Backend): self.library = core.LibraryController(backend=self, provider=library_provider) - playback_provider = LocalPlaybackProvider(backend=self) + playback_provider = base.BasePlaybackProvider(backend=self) self.playback = LocalPlaybackController(backend=self, provider=playback_provider) @@ -78,31 +78,6 @@ class LocalPlaybackController(core.PlaybackController): return self.backend.audio.get_position().get() -class LocalPlaybackProvider(base.BasePlaybackProvider): - def pause(self): - return self.backend.audio.pause_playback().get() - - def play(self, track): - self.backend.audio.prepare_change() - self.backend.audio.set_uri(track.uri).get() - return self.backend.audio.start_playback().get() - - def resume(self): - return self.backend.audio.start_playback().get() - - def seek(self, time_position): - return self.backend.audio.set_position(time_position).get() - - def stop(self): - return self.backend.audio.stop_playback().get() - - def get_volume(self): - return self.backend.audio.get_volume().get() - - def set_volume(self, volume): - self.backend.audio.set_volume(volume).get() - - class LocalStoredPlaylistsProvider(base.BaseStoredPlaylistsProvider): def __init__(self, *args, **kwargs): super(LocalStoredPlaylistsProvider, self).__init__(*args, **kwargs) diff --git a/mopidy/backends/spotify/playback.py b/mopidy/backends/spotify/playback.py index cf16c35e..1c20da87 100644 --- a/mopidy/backends/spotify/playback.py +++ b/mopidy/backends/spotify/playback.py @@ -8,9 +8,6 @@ from mopidy.core import PlaybackState logger = logging.getLogger('mopidy.backends.spotify.playback') class SpotifyPlaybackProvider(BasePlaybackProvider): - def pause(self): - return self.backend.audio.pause_playback() - def play(self, track): if self.backend.playback.state == PlaybackState.PLAYING: self.backend.spotify.session.play(0) @@ -39,12 +36,5 @@ class SpotifyPlaybackProvider(BasePlaybackProvider): return True def stop(self): - result = self.backend.audio.stop_playback() self.backend.spotify.session.play(0) - return result - - def get_volume(self): - return self.backend.audio.get_volume().get() - - def set_volume(self, volume): - self.backend.audio.set_volume(volume) + return super(SpotifyPlaybackProvider, self).stop() From e6485e4abea48c14adf1d8a3088f1861b5c1d06b Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 15 Sep 2012 14:01:05 +0200 Subject: [PATCH 38/44] Add basic docs for Audio API, fixes #177. --- docs/api/audio.rst | 19 +++++++++++++++++++ docs/api/index.rst | 1 + mopidy/audio/__init__.py | 2 ++ 3 files changed, 22 insertions(+) create mode 100644 docs/api/audio.rst diff --git a/docs/api/audio.rst b/docs/api/audio.rst new file mode 100644 index 00000000..d5fb5dd9 --- /dev/null +++ b/docs/api/audio.rst @@ -0,0 +1,19 @@ +.. _audio-api: + +********* +Audio API +********* + +The audio API is the interface we have built around GStreamer to support our +specific use cases. Most backends should be able to get by with simply setting +the URI of the resource they want to play, for these cases the default playback +provider should be used. + +For more advanced cases such as when the raw audio data is delivered outside of +GStreamer or the backend needs to add metadata to the currently playing resource, +developers should sub-class the base playback provider and implement the extra +behaviour that is needed through the following API: + + +.. autoclass:: mopidy.audio.Audio + :members: diff --git a/docs/api/index.rst b/docs/api/index.rst index b5be8ed4..618096ee 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -9,5 +9,6 @@ API reference models backends core + audio frontends listeners diff --git a/mopidy/audio/__init__.py b/mopidy/audio/__init__.py index 25c53c5a..dd98dfa8 100644 --- a/mopidy/audio/__init__.py +++ b/mopidy/audio/__init__.py @@ -208,6 +208,8 @@ class Audio(ThreadingActor): """ Call this to deliver raw audio data to be played. + Note that the uri must be set to ``appsrc://`` for this to work. + :param capabilities: a GStreamer capabilities string :type capabilities: string :param data: raw audio data to be played From e3bc0e79b976ae4903735d2bbca8cee1c6915d0f Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 15 Sep 2012 15:18:50 +0200 Subject: [PATCH 39/44] Fix bug in local playlist handling. --- mopidy/backends/local/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index b49406c6..c7126824 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -93,7 +93,7 @@ class LocalStoredPlaylistsProvider(base.BaseStoredPlaylistsProvider): logger.info('Loading playlists from %s', self._folder) for m3u in glob.glob(os.path.join(self._folder, '*.m3u')): - name = os.path.basename(m3u)[:len('.m3u')] + name = os.path.basename(m3u)[:-len('.m3u')] tracks = [] for uri in parse_m3u(m3u): try: From 144311420486c8087c9e0c0203431f968947f9f3 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 15 Sep 2012 17:45:28 +0200 Subject: [PATCH 40/44] docs: Remove duplicate API doc --- docs/modules/audio.rst | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 docs/modules/audio.rst diff --git a/docs/modules/audio.rst b/docs/modules/audio.rst deleted file mode 100644 index 0f1c3bfb..00000000 --- a/docs/modules/audio.rst +++ /dev/null @@ -1,7 +0,0 @@ -************************************* -:mod:`mopidy.audio` -- Audio playback -************************************* - -.. automodule:: mopidy.audio - :synopsis: Audio playback through GStreamer - :members: From a8d1d41ab3c7aef11754f387458038297ebbc549 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 14 Sep 2012 23:59:57 +0200 Subject: [PATCH 41/44] Use assertIn and assertNotIn in tests --- tests/backends/base/current_playlist.py | 2 +- tests/backends/base/playback.py | 6 +- tests/backends/base/stored_playlists.py | 4 +- tests/backends/local/playback_test.py | 2 +- tests/frontends/mpd/dispatcher_test.py | 6 +- tests/frontends/mpd/protocol/__init__.py | 4 +- .../mpd/protocol/command_list_test.py | 4 +- tests/frontends/mpd/serializer_test.py | 50 +++++++-------- tests/frontends/mpd/status_test.py | 62 +++++++++---------- .../frontends/mpris/player_interface_test.py | 2 +- tests/help_test.py | 18 +++--- tests/models_test.py | 4 +- tests/utils/init_test.py | 2 +- tests/utils/settings_test.py | 17 +++-- tests/version_test.py | 6 +- 15 files changed, 94 insertions(+), 95 deletions(-) diff --git a/tests/backends/base/current_playlist.py b/tests/backends/base/current_playlist.py index bfc0a254..430e4c40 100644 --- a/tests/backends/base/current_playlist.py +++ b/tests/backends/base/current_playlist.py @@ -206,7 +206,7 @@ class CurrentPlaylistControllerTest(object): version = self.controller.version self.controller.remove(uri=track1.uri) self.assert_(version < self.controller.version) - self.assert_(track1 not in self.controller.tracks) + self.assertNotIn(track1, self.controller.tracks) self.assertEqual(track2, self.controller.tracks[1]) @populate_playlist diff --git a/tests/backends/base/playback.py b/tests/backends/base/playback.py index b9661df9..1e434e35 100644 --- a/tests/backends/base/playback.py +++ b/tests/backends/base/playback.py @@ -274,7 +274,7 @@ class PlaybackControllerTest(object): self.playback.consume = True self.playback.play() self.playback.next() - self.assert_(self.tracks[0] in self.backend.current_playlist.tracks) + self.assertIn(self.tracks[0], self.backend.current_playlist.tracks) @populate_playlist def test_next_with_single_and_repeat(self): @@ -411,7 +411,7 @@ class PlaybackControllerTest(object): self.playback.consume = True self.playback.play() self.playback.on_end_of_track() - self.assert_(self.tracks[0] not in self.backend.current_playlist.tracks) + self.assertNotIn(self.tracks[0], self.backend.current_playlist.tracks) @populate_playlist def test_end_of_track_with_random(self): @@ -855,7 +855,7 @@ class PlaybackControllerTest(object): self.playback.play() played = [] for _ in self.tracks: - self.assert_(self.playback.current_track not in played) + self.assertNotIn(self.playback.current_track, played) played.append(self.playback.current_track) self.playback.next() diff --git a/tests/backends/base/stored_playlists.py b/tests/backends/base/stored_playlists.py index 54315e62..1e575b9e 100644 --- a/tests/backends/base/stored_playlists.py +++ b/tests/backends/base/stored_playlists.py @@ -30,7 +30,7 @@ class StoredPlaylistsControllerTest(object): def test_create_in_playlists(self): playlist = self.stored.create('test') self.assert_(self.stored.playlists) - self.assert_(playlist in self.stored.playlists) + self.assertIn(playlist, self.stored.playlists) def test_playlists_empty_to_start_with(self): self.assert_(not self.stored.playlists) @@ -101,7 +101,7 @@ class StoredPlaylistsControllerTest(object): # FIXME should we handle playlists without names? playlist = Playlist(name='test') self.stored.save(playlist) - self.assert_(playlist in self.stored.playlists) + self.assertIn(playlist, self.stored.playlists) @unittest.SkipTest def test_playlist_with_unknown_track(self): diff --git a/tests/backends/local/playback_test.py b/tests/backends/local/playback_test.py index 4dede6ad..c167fbcc 100644 --- a/tests/backends/local/playback_test.py +++ b/tests/backends/local/playback_test.py @@ -35,7 +35,7 @@ class LocalPlaybackControllerTest(PlaybackControllerTest, unittest.TestCase): self.backend.current_playlist.add(track) def test_uri_scheme(self): - self.assert_('file' in self.backend.uri_schemes) + self.assertIn('file', self.backend.uri_schemes) def test_play_mp3(self): self.add_track('blank.mp3') diff --git a/tests/frontends/mpd/dispatcher_test.py b/tests/frontends/mpd/dispatcher_test.py index 63f6d299..9f05d7dd 100644 --- a/tests/frontends/mpd/dispatcher_test.py +++ b/tests/frontends/mpd/dispatcher_test.py @@ -37,7 +37,7 @@ class MpdDispatcherTest(unittest.TestCase): expected_handler (handler, kwargs) = self.dispatcher._find_handler('known_command an_arg') self.assertEqual(handler, expected_handler) - self.assert_('arg1' in kwargs) + self.assertIn('arg1', kwargs) self.assertEqual(kwargs['arg1'], 'an_arg') def test_handling_unknown_request_yields_error(self): @@ -48,5 +48,5 @@ class MpdDispatcherTest(unittest.TestCase): expected = 'magic' request_handlers['known request'] = lambda x: expected result = self.dispatcher.handle_request('known request') - self.assert_(u'OK' in result) - self.assert_(expected in result) + self.assertIn(u'OK', result) + self.assertIn(expected, result) diff --git a/tests/frontends/mpd/protocol/__init__.py b/tests/frontends/mpd/protocol/__init__.py index b39ded01..3b8fbe33 100644 --- a/tests/frontends/mpd/protocol/__init__.py +++ b/tests/frontends/mpd/protocol/__init__.py @@ -42,7 +42,7 @@ class BaseTestCase(unittest.TestCase): self.assertEqual([], self.connection.response) def assertInResponse(self, value): - self.assert_(value in self.connection.response, u'Did not find %s ' + self.assertIn(value, self.connection.response, u'Did not find %s ' 'in %s' % (repr(value), repr(self.connection.response))) def assertOnceInResponse(self, value): @@ -51,7 +51,7 @@ class BaseTestCase(unittest.TestCase): (repr(value), repr(self.connection.response))) def assertNotInResponse(self, value): - self.assert_(value not in self.connection.response, u'Found %s in %s' % + self.assertNotIn(value, self.connection.response, u'Found %s in %s' % (repr(value), repr(self.connection.response))) def assertEqualResponse(self, value): diff --git a/tests/frontends/mpd/protocol/command_list_test.py b/tests/frontends/mpd/protocol/command_list_test.py index a81725ad..65b051d3 100644 --- a/tests/frontends/mpd/protocol/command_list_test.py +++ b/tests/frontends/mpd/protocol/command_list_test.py @@ -21,7 +21,7 @@ class CommandListsTest(protocol.BaseTestCase): self.assertEqual([], self.dispatcher.command_list) self.assertEqual(False, self.dispatcher.command_list_ok) self.sendRequest(u'ping') - self.assert_(u'ping' in self.dispatcher.command_list) + self.assertIn(u'ping', self.dispatcher.command_list) self.sendRequest(u'command_list_end') self.assertInResponse(u'OK') self.assertEqual(False, self.dispatcher.command_list) @@ -42,7 +42,7 @@ class CommandListsTest(protocol.BaseTestCase): self.assertEqual([], self.dispatcher.command_list) self.assertEqual(True, self.dispatcher.command_list_ok) self.sendRequest(u'ping') - self.assert_(u'ping' in self.dispatcher.command_list) + self.assertIn(u'ping', self.dispatcher.command_list) self.sendRequest(u'command_list_end') self.assertInResponse(u'list_OK') self.assertInResponse(u'OK') diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index a20abaed..e6cd80e2 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -31,66 +31,66 @@ class TrackMpdFormatTest(unittest.TestCase): def test_track_to_mpd_format_for_empty_track(self): result = translator.track_to_mpd_format(Track()) - self.assert_(('file', '') in result) - self.assert_(('Time', 0) in result) - self.assert_(('Artist', '') in result) - self.assert_(('Title', '') in result) - self.assert_(('Album', '') in result) - self.assert_(('Track', 0) in result) - self.assert_(('Date', '') in result) + self.assertIn(('file', ''), result) + self.assertIn(('Time', 0), result) + self.assertIn(('Artist', ''), result) + self.assertIn(('Title', ''), result) + self.assertIn(('Album', ''), result) + self.assertIn(('Track', 0), result) + self.assertIn(('Date', ''), result) self.assertEqual(len(result), 7) def test_track_to_mpd_format_with_position(self): result = translator.track_to_mpd_format(Track(), position=1) - self.assert_(('Pos', 1) not in result) + self.assertNotIn(('Pos', 1), result) def test_track_to_mpd_format_with_cpid(self): result = translator.track_to_mpd_format(CpTrack(1, Track())) - self.assert_(('Id', 1) not in result) + self.assertNotIn(('Id', 1), result) def test_track_to_mpd_format_with_position_and_cpid(self): result = translator.track_to_mpd_format(CpTrack(2, Track()), position=1) - self.assert_(('Pos', 1) in result) - self.assert_(('Id', 2) in result) + self.assertIn(('Pos', 1), result) + self.assertIn(('Id', 2), result) def test_track_to_mpd_format_for_nonempty_track(self): result = translator.track_to_mpd_format( CpTrack(122, self.track), position=9) - self.assert_(('file', 'a uri') in result) - self.assert_(('Time', 137) in result) - self.assert_(('Artist', 'an artist') in result) - self.assert_(('Title', 'a name') in result) - self.assert_(('Album', 'an album') in result) - self.assert_(('AlbumArtist', 'an other artist') in result) - self.assert_(('Track', '7/13') in result) - self.assert_(('Date', datetime.date(1977, 1, 1)) in result) - self.assert_(('Pos', 9) in result) - self.assert_(('Id', 122) in result) + self.assertIn(('file', 'a uri'), result) + self.assertIn(('Time', 137), result) + self.assertIn(('Artist', 'an artist'), result) + self.assertIn(('Title', 'a name'), result) + self.assertIn(('Album', 'an album'), result) + self.assertIn(('AlbumArtist', 'an other artist'), result) + self.assertIn(('Track', '7/13'), result) + self.assertIn(('Date', datetime.date(1977, 1, 1)), result) + self.assertIn(('Pos', 9), result) + self.assertIn(('Id', 122), result) self.assertEqual(len(result), 10) def test_track_to_mpd_format_musicbrainz_trackid(self): track = self.track.copy(musicbrainz_id='foo') result = translator.track_to_mpd_format(track) - self.assert_(('MUSICBRAINZ_TRACKID', 'foo') in result) + self.assertIn(('MUSICBRAINZ_TRACKID', 'foo'), result) def test_track_to_mpd_format_musicbrainz_albumid(self): album = self.track.album.copy(musicbrainz_id='foo') track = self.track.copy(album=album) result = translator.track_to_mpd_format(track) - self.assert_(('MUSICBRAINZ_ALBUMID', 'foo') in result) + self.assertIn(('MUSICBRAINZ_ALBUMID', 'foo'), result) def test_track_to_mpd_format_musicbrainz_albumid(self): artist = list(self.track.artists)[0].copy(musicbrainz_id='foo') album = self.track.album.copy(artists=[artist]) track = self.track.copy(album=album) result = translator.track_to_mpd_format(track) - self.assert_(('MUSICBRAINZ_ALBUMARTISTID', 'foo') in result) + self.assertIn(('MUSICBRAINZ_ALBUMARTISTID', 'foo'), result) def test_track_to_mpd_format_musicbrainz_artistid(self): artist = list(self.track.artists)[0].copy(musicbrainz_id='foo') track = self.track.copy(artists=[artist]) result = translator.track_to_mpd_format(track) - self.assert_(('MUSICBRAINZ_ARTISTID', 'foo') in result) + self.assertIn(('MUSICBRAINZ_ARTISTID', 'foo'), result) def test_artists_to_mpd_format(self): artists = [Artist(name=u'ABBA'), Artist(name=u'Beatles')] diff --git a/tests/frontends/mpd/status_test.py b/tests/frontends/mpd/status_test.py index 455dba45..2bc3488b 100644 --- a/tests/frontends/mpd/status_test.py +++ b/tests/frontends/mpd/status_test.py @@ -26,123 +26,123 @@ class StatusHandlerTest(unittest.TestCase): def test_stats_method(self): result = status.stats(self.context) - self.assert_('artists' in result) + self.assertIn('artists', result) self.assert_(int(result['artists']) >= 0) - self.assert_('albums' in result) + self.assertIn('albums', result) self.assert_(int(result['albums']) >= 0) - self.assert_('songs' in result) + self.assertIn('songs', result) self.assert_(int(result['songs']) >= 0) - self.assert_('uptime' in result) + self.assertIn('uptime', result) self.assert_(int(result['uptime']) >= 0) - self.assert_('db_playtime' in result) + self.assertIn('db_playtime', result) self.assert_(int(result['db_playtime']) >= 0) - self.assert_('db_update' in result) + self.assertIn('db_update', result) self.assert_(int(result['db_update']) >= 0) - self.assert_('playtime' in result) + self.assertIn('playtime', result) self.assert_(int(result['playtime']) >= 0) def test_status_method_contains_volume_with_na_value(self): result = dict(status.status(self.context)) - self.assert_('volume' in result) + self.assertIn('volume', result) self.assertEqual(int(result['volume']), -1) def test_status_method_contains_volume(self): self.backend.playback.volume = 17 result = dict(status.status(self.context)) - self.assert_('volume' in result) + self.assertIn('volume', result) self.assertEqual(int(result['volume']), 17) def test_status_method_contains_repeat_is_0(self): result = dict(status.status(self.context)) - self.assert_('repeat' in result) + self.assertIn('repeat', result) self.assertEqual(int(result['repeat']), 0) def test_status_method_contains_repeat_is_1(self): self.backend.playback.repeat = 1 result = dict(status.status(self.context)) - self.assert_('repeat' in result) + self.assertIn('repeat', result) self.assertEqual(int(result['repeat']), 1) def test_status_method_contains_random_is_0(self): result = dict(status.status(self.context)) - self.assert_('random' in result) + self.assertIn('random', result) self.assertEqual(int(result['random']), 0) def test_status_method_contains_random_is_1(self): self.backend.playback.random = 1 result = dict(status.status(self.context)) - self.assert_('random' in result) + self.assertIn('random', result) self.assertEqual(int(result['random']), 1) def test_status_method_contains_single(self): result = dict(status.status(self.context)) - self.assert_('single' in result) - self.assert_(int(result['single']) in (0, 1)) + self.assertIn('single', result) + self.assertIn(int(result['single']), (0, 1)) def test_status_method_contains_consume_is_0(self): result = dict(status.status(self.context)) - self.assert_('consume' in result) + self.assertIn('consume', result) self.assertEqual(int(result['consume']), 0) def test_status_method_contains_consume_is_1(self): self.backend.playback.consume = 1 result = dict(status.status(self.context)) - self.assert_('consume' in result) + self.assertIn('consume', result) self.assertEqual(int(result['consume']), 1) def test_status_method_contains_playlist(self): result = dict(status.status(self.context)) - self.assert_('playlist' in result) - self.assert_(int(result['playlist']) in xrange(0, 2**31 - 1)) + self.assertIn('playlist', result) + self.assertIn(int(result['playlist']), xrange(0, 2**31 - 1)) def test_status_method_contains_playlistlength(self): result = dict(status.status(self.context)) - self.assert_('playlistlength' in result) + self.assertIn('playlistlength', result) self.assert_(int(result['playlistlength']) >= 0) def test_status_method_contains_xfade(self): result = dict(status.status(self.context)) - self.assert_('xfade' in result) + self.assertIn('xfade', result) self.assert_(int(result['xfade']) >= 0) def test_status_method_contains_state_is_play(self): self.backend.playback.state = PLAYING result = dict(status.status(self.context)) - self.assert_('state' in result) + self.assertIn('state', result) self.assertEqual(result['state'], 'play') def test_status_method_contains_state_is_stop(self): self.backend.playback.state = STOPPED result = dict(status.status(self.context)) - self.assert_('state' in result) + self.assertIn('state', result) self.assertEqual(result['state'], 'stop') def test_status_method_contains_state_is_pause(self): self.backend.playback.state = PLAYING self.backend.playback.state = PAUSED result = dict(status.status(self.context)) - self.assert_('state' in result) + self.assertIn('state', result) self.assertEqual(result['state'], 'pause') def test_status_method_when_playlist_loaded_contains_song(self): self.backend.current_playlist.append([Track()]) self.backend.playback.play() result = dict(status.status(self.context)) - self.assert_('song' in result) + self.assertIn('song', result) self.assert_(int(result['song']) >= 0) def test_status_method_when_playlist_loaded_contains_cpid_as_songid(self): self.backend.current_playlist.append([Track()]) self.backend.playback.play() result = dict(status.status(self.context)) - self.assert_('songid' in result) + self.assertIn('songid', result) self.assertEqual(int(result['songid']), 0) def test_status_method_when_playing_contains_time_with_no_length(self): self.backend.current_playlist.append([Track(length=None)]) self.backend.playback.play() result = dict(status.status(self.context)) - self.assert_('time' in result) + self.assertIn('time', result) (position, total) = result['time'].split(':') position = int(position) total = int(total) @@ -152,7 +152,7 @@ class StatusHandlerTest(unittest.TestCase): self.backend.current_playlist.append([Track(length=10000)]) self.backend.playback.play() result = dict(status.status(self.context)) - self.assert_('time' in result) + self.assertIn('time', result) (position, total) = result['time'].split(':') position = int(position) total = int(total) @@ -162,19 +162,19 @@ class StatusHandlerTest(unittest.TestCase): self.backend.playback.state = PAUSED self.backend.playback.play_time_accumulated = 59123 result = dict(status.status(self.context)) - self.assert_('elapsed' in result) + self.assertIn('elapsed', result) self.assertEqual(result['elapsed'], '59.123') def test_status_method_when_starting_playing_contains_elapsed_zero(self): self.backend.playback.state = PAUSED self.backend.playback.play_time_accumulated = 123 # Less than 1000ms result = dict(status.status(self.context)) - self.assert_('elapsed' in result) + self.assertIn('elapsed', result) self.assertEqual(result['elapsed'], '0.123') def test_status_method_when_playing_contains_bitrate(self): self.backend.current_playlist.append([Track(bitrate=320)]) self.backend.playback.play() result = dict(status.status(self.context)) - self.assert_('bitrate' in result) + self.assertIn('bitrate', result) self.assertEqual(int(result['bitrate']), 320) diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index a6415b2f..db7f9265 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -141,7 +141,7 @@ class PlayerInterfaceTest(unittest.TestCase): def test_get_metadata_has_trackid_even_when_no_current_track(self): result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata') - self.assert_('mpris:trackid' in result.keys()) + self.assertIn('mpris:trackid', result.keys()) self.assertEquals(result['mpris:trackid'], '') def test_get_metadata_has_trackid_based_on_cpid(self): diff --git a/tests/help_test.py b/tests/help_test.py index 1fa22c2f..a2803b72 100644 --- a/tests/help_test.py +++ b/tests/help_test.py @@ -13,18 +13,18 @@ class HelpTest(unittest.TestCase): args = [sys.executable, mopidy_dir, '--help'] process = subprocess.Popen(args, stdout=subprocess.PIPE) output = process.communicate()[0] - self.assert_('--version' in output) - self.assert_('--help' in output) - self.assert_('--help-gst' in output) - self.assert_('--interactive' in output) - self.assert_('--quiet' in output) - self.assert_('--verbose' in output) - self.assert_('--save-debug-log' in output) - self.assert_('--list-settings' in output) + self.assertIn('--version', output) + self.assertIn('--help', output) + self.assertIn('--help-gst', output) + self.assertIn('--interactive', output) + self.assertIn('--quiet', output) + self.assertIn('--verbose', output) + self.assertIn('--save-debug-log', output) + self.assertIn('--list-settings', output) def test_help_gst_has_gstreamer_options(self): mopidy_dir = os.path.dirname(mopidy.__file__) args = [sys.executable, mopidy_dir, '--help-gst'] process = subprocess.Popen(args, stdout=subprocess.PIPE) output = process.communicate()[0] - self.assert_('--gst-version' in output) + self.assertIn('--gst-version', output) diff --git a/tests/models_test.py b/tests/models_test.py index af90c5bd..779d1a4b 100644 --- a/tests/models_test.py +++ b/tests/models_test.py @@ -43,7 +43,7 @@ class GenericCopyTets(unittest.TestCase): artist2 = Artist(name='bar') track = Track(artists=[artist1]) copy = track.copy(artists=[artist2]) - self.assert_(artist2 in copy.artists) + self.assertIn(artist2, copy.artists) def test_copying_track_with_invalid_key(self): test = lambda: Track().copy(invalid_key=True) @@ -155,7 +155,7 @@ class AlbumTest(unittest.TestCase): def test_artists(self): artist = Artist() album = Album(artists=[artist]) - self.assert_(artist in album.artists) + self.assertIn(artist, album.artists) self.assertRaises(AttributeError, setattr, album, 'artists', None) def test_num_tracks(self): diff --git a/tests/utils/init_test.py b/tests/utils/init_test.py index f232e2ef..bdd0adc5 100644 --- a/tests/utils/init_test.py +++ b/tests/utils/init_test.py @@ -20,7 +20,7 @@ class GetClassTest(unittest.TestCase): try: utils.get_class('foo.bar.Baz') except ImportError as e: - self.assert_('foo.bar.Baz' in str(e)) + self.assertIn('foo.bar.Baz', str(e)) def test_loading_existing_class(self): cls = utils.get_class('unittest.TestCase') diff --git a/tests/utils/settings_test.py b/tests/utils/settings_test.py index 7d104969..cf476c24 100644 --- a/tests/utils/settings_test.py +++ b/tests/utils/settings_test.py @@ -107,7 +107,7 @@ class SettingsProxyTest(unittest.TestCase): def test_setattr_updates_runtime_settings(self): self.settings.TEST = 'test' - self.assert_('TEST' in self.settings.runtime) + self.assertIn('TEST', self.settings.runtime) def test_setattr_updates_runtime_with_value(self): self.settings.TEST = 'test' @@ -181,34 +181,33 @@ class FormatSettingListTest(unittest.TestCase): def test_contains_the_setting_name(self): self.settings.TEST = u'test' result = setting_utils.format_settings_list(self.settings) - self.assert_('TEST:' in result, result) + self.assertIn('TEST:', result, result) def test_repr_of_a_string_value(self): self.settings.TEST = u'test' result = setting_utils.format_settings_list(self.settings) - self.assert_("TEST: u'test'" in result, result) + self.assertIn("TEST: u'test'", result, result) def test_repr_of_an_int_value(self): self.settings.TEST = 123 result = setting_utils.format_settings_list(self.settings) - self.assert_("TEST: 123" in result, result) + self.assertIn("TEST: 123", result, result) def test_repr_of_a_tuple_value(self): self.settings.TEST = (123, u'abc') result = setting_utils.format_settings_list(self.settings) - self.assert_("TEST: (123, u'abc')" in result, result) + self.assertIn("TEST: (123, u'abc')", result, result) def test_passwords_are_masked(self): self.settings.TEST_PASSWORD = u'secret' result = setting_utils.format_settings_list(self.settings) - self.assert_("TEST_PASSWORD: u'secret'" not in result, result) - self.assert_("TEST_PASSWORD: u'********'" in result, result) + self.assertNotIn("TEST_PASSWORD: u'secret'", result, result) + self.assertIn("TEST_PASSWORD: u'********'", result, result) def test_short_values_are_not_pretty_printed(self): self.settings.FRONTEND = (u'mopidy.frontends.mpd.MpdFrontend',) result = setting_utils.format_settings_list(self.settings) - self.assert_("FRONTEND: (u'mopidy.frontends.mpd.MpdFrontend',)" in result, - result) + self.assertIn("FRONTEND: (u'mopidy.frontends.mpd.MpdFrontend',)", result) def test_long_values_are_pretty_printed(self): self.settings.FRONTEND = (u'mopidy.frontends.mpd.MpdFrontend', diff --git a/tests/version_test.py b/tests/version_test.py index 26045ac1..85b182f0 100644 --- a/tests/version_test.py +++ b/tests/version_test.py @@ -31,10 +31,10 @@ class VersionTest(unittest.TestCase): self.assert_(SV(__version__) < SV('0.8.0')) def test_get_platform_contains_platform(self): - self.assert_(platform.platform() in get_platform()) + self.assertIn(platform.platform(), get_platform()) def test_get_python_contains_python_implementation(self): - self.assert_(platform.python_implementation() in get_python()) + self.assertIn(platform.python_implementation(), get_python()) def test_get_python_contains_python_version(self): - self.assert_(platform.python_version() in get_python()) + self.assertIn(platform.python_version(), get_python()) From 2cd729aa2f990154467f097565851a038f1c5da5 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 15 Sep 2012 18:50:43 +0200 Subject: [PATCH 42/44] docs: Include git revision in version number if we're in a git repo --- docs/conf.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index cd59d14d..8129adec 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -53,11 +53,6 @@ MOCK_MODULES = [ for mod_name in MOCK_MODULES: sys.modules[mod_name] = Mock() -def get_version(): - init_py = open('../mopidy/__init__.py').read() - metadata = dict(re.findall("__([a-z]+)__ = '([^']+)'", init_py)) - return metadata['version'] - # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. @@ -96,6 +91,7 @@ copyright = u'2010-2012, Stein Magnus Jodal and contributors' # built documents. # # The full version, including alpha/beta/rc tags. +from mopidy import get_version release = get_version() # The short X.Y version. version = '.'.join(release.split('.')[:2]) From e905fd8d8a8723efd8ea01dc138530daca47eb3f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 15 Sep 2012 19:19:45 +0200 Subject: [PATCH 43/44] Reorganize v0.8 changelog --- docs/changes.rst | 66 +++++++++++++++++++++++++----------------------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 3b77f61a..3eb5947c 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -7,24 +7,7 @@ This change log is used to track all major changes to Mopidy. v0.8 (in development) ===================== -**Changes** - -- Added tools/debug-proxy.py to tee client requests to two backends and diff - responses. Intended as a developer tool for checking for MPD protocol changes - and various client support. Requires gevent, which currently is not a - dependency of Mopidy. - -- Fixed bug when the MPD command `playlistinfo` is used with a track position. - Track position and CPID was intermixed, so it would cause a crash if a CPID - matching the track position didn't exist. (Fixes: :issue:`162`) - -- Added :option:`--list-deps` option to the `mopidy` command that lists - required and optional dependencies, their current versions, and some other - information useful for debugging. (Fixes: :issue:`74`) - -- When unknown settings are encountered, we now check if it's similar to a - known setting, and suggests to the user what we think the setting should have - been. +**Audio output and mixer changes** - Removed multiple outputs support. Having this feature currently seems to be more trouble than what it is worth. The :attr:`mopidy.settings.OUTPUTS` @@ -36,9 +19,9 @@ v0.8 (in development) - Switch to pure GStreamer based mixing. This implies that users setup a GStreamer bin with a mixer in it in :attr:`mopidy.setting.MIXER`. The default - value is ``autoaudiomixer``, a custom mixer that attempts to find a mixer that - will work on your system. If this picks the wrong mixer you can of course - override it. Setting the mixer to :class:`None` is also supported. MPD + value is ``autoaudiomixer``, a custom mixer that attempts to find a mixer + that will work on your system. If this picks the wrong mixer you can of + course override it. Setting the mixer to :class:`None` is also supported. MPD protocol support for volume has also been updated to return -1 when we have no mixer set. @@ -46,7 +29,7 @@ v0.8 (in development) - Updated the NAD hardware mixer to work in the new GStreamer based mixing regime. Settings are now passed as GStreamer element properties. In practice - that means that the following old-style config: + that means that the following old-style config:: MIXER = u'mopidy.mixers.nad.NadMixer' MIXER_EXT_PORT = u'/dev/ttyUSB0' @@ -54,7 +37,7 @@ v0.8 (in development) MIXER_EXT_SPEAKERS_A = u'On' MIXER_EXT_SPEAKERS_B = u'Off' - Now is reduced to simply: + Now is reduced to simply:: MIXER = u'nadmixer port=/dev/ttyUSB0 source=aux speakers-a=on speakers-b=off' @@ -62,20 +45,41 @@ v0.8 (in development) properties may be left out if you don't want the mixer to adjust the settings on your NAD amplifier when Mopidy is started. -- Fixed :issue:`150` which caused some clients to block Mopidy completely. Bug - was caused by some clients sending ``close`` and then shutting down the - connection right away. This trigged a situation in which the connection - cleanup code would wait for an response that would never come inside the - event loop, blocking everything else. +**Changes** -- Created a Spotify track proxy that will switch to using loaded data as soon - as it becomes available. Fixes :issue:`72`. +- When unknown settings are encountered, we now check if it's similar to a + known setting, and suggests to the user what we think the setting should have + been. -- Fixed crash on lookup of unknown path when using local backend. +- Added :option:`--list-deps` option to the ``mopidy`` command that lists + required and optional dependencies, their current versions, and some other + information useful for debugging. (Fixes: :issue:`74`) + +- Added ``tools/debug-proxy.py`` to tee client requests to two backends and + diff responses. Intended as a developer tool for checking for MPD protocol + changes and various client support. Requires gevent, which currently is not a + dependency of Mopidy. - Support tracks with only release year, and not a full release date, like e.g. Spotify tracks. +**Bug fixes** + +- :issue:`72`: Created a Spotify track proxy that will switch to using loaded + data as soon as it becomes available. + +- :issue:`162`: Fixed bug when the MPD command ``playlistinfo`` is used with a + track position. Track position and CPID was intermixed, so it would cause a + crash if a CPID matching the track position didn't exist. + +- :issue:`150`: Fix bug which caused some clients to block Mopidy completely. + The bug was caused by some clients sending ``close`` and then shutting down + the connection right away. This trigged a situation in which the connection + cleanup code would wait for an response that would never come inside the + event loop, blocking everything else. + +- Fixed crash on lookup of unknown path when using local backend. + v0.7.3 (2012-08-11) =================== From 31d015f9fd9af093b9e31a7493d140828da4472f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 15 Sep 2012 19:21:23 +0200 Subject: [PATCH 44/44] docs: Fix typo --- docs/changes.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 3eb5947c..43b930b8 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -18,9 +18,9 @@ v0.8 (in development) :issue:`159`) - Switch to pure GStreamer based mixing. This implies that users setup a - GStreamer bin with a mixer in it in :attr:`mopidy.setting.MIXER`. The default - value is ``autoaudiomixer``, a custom mixer that attempts to find a mixer - that will work on your system. If this picks the wrong mixer you can of + GStreamer bin with a mixer in it in :attr:`mopidy.settings.MIXER`. The + default value is ``autoaudiomixer``, a custom mixer that attempts to find a + mixer that will work on your system. If this picks the wrong mixer you can of course override it. Setting the mixer to :class:`None` is also supported. MPD protocol support for volume has also been updated to return -1 when we have no mixer set.