From f17852c98f3d5993f5dd801fa7b40bd16d86a073 Mon Sep 17 00:00:00 2001 From: David C Date: Mon, 19 Nov 2012 14:18:42 +0100 Subject: [PATCH 01/39] Add proxy support --- mopidy/backends/spotify/actor.py | 7 +++- mopidy/backends/spotify/session_manager.py | 8 +++-- mopidy/settings.py | 42 ++++++++++++++++++++++ 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/mopidy/backends/spotify/actor.py b/mopidy/backends/spotify/actor.py index 5fc5cc4f..5e90205b 100644 --- a/mopidy/backends/spotify/actor.py +++ b/mopidy/backends/spotify/actor.py @@ -31,9 +31,14 @@ class SpotifyBackend(pykka.ThreadingActor, base.Backend): # Fail early if settings are not present username = settings.SPOTIFY_USERNAME password = settings.SPOTIFY_PASSWORD + proxy = settings.SPOTIFY_PROXY_HOST + proxy_username = settings.SPOTIFY_PROXY_USERNAME + proxy_password = settings.SPOTIFY_PROXY_PASSWORD self.spotify = SpotifySessionManager( - username, password, audio=audio, backend_ref=self.actor_ref) + username, password, audio=audio, backend_ref=self.actor_ref, + proxy=proxy, proxy_username=proxy_username, + proxy_password=proxy_password) def on_start(self): logger.info('Mopidy uses SPOTIFY(R) CORE') diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index b46fd659..09df3102 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -33,8 +33,12 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager): appkey_file = os.path.join(os.path.dirname(__file__), 'spotify_appkey.key') user_agent = 'Mopidy %s' % versioning.get_version() - def __init__(self, username, password, audio, backend_ref): - PyspotifySessionManager.__init__(self, username, password) + def __init__(self, username, password, audio, backend_ref, proxy=None, + proxy_username=None, proxy_password=None): + PyspotifySessionManager.__init__( + self, username, password, proxy=proxy, + proxy_username=proxy_username, + proxy_password=proxy_password) process.BaseThread.__init__(self) self.name = 'SpotifyThread' diff --git a/mopidy/settings.py b/mopidy/settings.py index 897745d7..22df5d2d 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -218,3 +218,45 @@ SPOTIFY_PASSWORD = '' #: #: SPOTIFY_BITRATE = 160 SPOTIFY_BITRATE = 160 + +#: Spotify proxy host +#: +#: Example:: +#: +#: SPOTIFY_PROXY_HOST = u'protocol://host:port' +#: +#: Used by :mod:`mopidy.backends.spotify` +#: +#: Default :: +#: +#: SPOTIFY_PROXY_HOST = None +#: +SPOTIFY_PROXY_HOST = None + +#: Spotify proxy username +#: +#: Example:: +#: +#: SPOTIFY_PROXY_HOST = u'username' +#: +#: Used by :mod:`mopidy.backends.spotify` +#: +#: Default :: +#: +#: SPOTIFY_PROXY_USERNAME = None +#: +SPOTIFY_PROXY_USERNAME = None + +#: Spotify proxy password +#: +#: Example:: +#: +#: SPOTIFY_PROXY_HOST = u'password' +#: +#: Used by :mod:`mopidy.backends.spotify` +#: +#: Default :: +#: +#: SPOTIFY_PROXY_PASSWORD = None +#: +SPOTIFY_PROXY_PASSWORD = None From 7da2058b656b98b012b7bf3daa315cc3359ec553 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 20 Nov 2012 00:31:41 +0100 Subject: [PATCH 02/39] mpd: Test response for bad 'list' requests --- tests/frontends/mpd/protocol/music_db_test.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/frontends/mpd/protocol/music_db_test.py b/tests/frontends/mpd/protocol/music_db_test.py index 7059c855..08b36332 100644 --- a/tests/frontends/mpd/protocol/music_db_test.py +++ b/tests/frontends/mpd/protocol/music_db_test.py @@ -173,6 +173,10 @@ class MusicDatabaseListTest(protocol.BaseTestCase): 'list "artist" "artist" "anartist" "album" "analbum"') self.assertInResponse('OK') + def test_list_artist_without_filter_value(self): + self.sendRequest('list "artist" "artist" ""') + self.assertInResponse('OK') + ### Album def test_list_album_with_quotes(self): @@ -191,6 +195,10 @@ class MusicDatabaseListTest(protocol.BaseTestCase): self.sendRequest('list "album" "anartist"') self.assertInResponse('OK') + def test_list_album_with_artist_name_without_filter_value(self): + self.sendRequest('list "album" ""') + self.assertInResponse('OK') + def test_list_album_by_artist(self): self.sendRequest('list "album" "artist" "anartist"') self.assertInResponse('OK') @@ -216,6 +224,10 @@ class MusicDatabaseListTest(protocol.BaseTestCase): 'list "album" "artist" "anartist" "album" "analbum"') self.assertInResponse('OK') + def test_list_album_without_filter_value(self): + self.sendRequest('list "album" "artist" ""') + self.assertInResponse('OK') + ### Date def test_list_date_with_quotes(self): @@ -259,6 +271,10 @@ class MusicDatabaseListTest(protocol.BaseTestCase): self.sendRequest('list "date" "artist" "anartist" "album" "analbum"') self.assertInResponse('OK') + def test_list_date_without_filter_value(self): + self.sendRequest('list "date" "artist" ""') + self.assertInResponse('OK') + ### Genre def test_list_genre_with_quotes(self): @@ -303,6 +319,10 @@ class MusicDatabaseListTest(protocol.BaseTestCase): 'list "genre" "artist" "anartist" "album" "analbum"') self.assertInResponse('OK') + def test_list_genre_without_filter_value(self): + self.sendRequest('list "genre" "artist" ""') + self.assertInResponse('OK') + class MusicDatabaseSearchTest(protocol.BaseTestCase): def test_search_album(self): From bec91284be7c2554c238d2d25cd35986fdac3a6a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 20 Nov 2012 00:38:53 +0100 Subject: [PATCH 03/39] mpd: Allow bad 'find' requests --- mopidy/frontends/mpd/protocol/music_db.py | 2 +- tests/frontends/mpd/protocol/music_db_test.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/mopidy/frontends/mpd/protocol/music_db.py b/mopidy/frontends/mpd/protocol/music_db.py index 4d6433f1..26371364 100644 --- a/mopidy/frontends/mpd/protocol/music_db.py +++ b/mopidy/frontends/mpd/protocol/music_db.py @@ -52,7 +52,7 @@ def count(context, tag, needle): @handle_request( r'^find (?P("?([Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ile[name]*|' - r'[Tt]itle|[Aa]ny)"? "[^"]+"\s?)+)$') + r'[Tt]itle|[Aa]ny)"? "[^"]*"\s?)+)$') def find(context, mpd_query): """ *musicpd.org, music database section:* diff --git a/tests/frontends/mpd/protocol/music_db_test.py b/tests/frontends/mpd/protocol/music_db_test.py index 08b36332..9a233e40 100644 --- a/tests/frontends/mpd/protocol/music_db_test.py +++ b/tests/frontends/mpd/protocol/music_db_test.py @@ -119,6 +119,10 @@ class MusicDatabaseFindTest(protocol.BaseTestCase): self.sendRequest('find album "album_what" artist "artist_what"') self.assertInResponse('OK') + def test_find_without_filter_value(self): + self.sendRequest('find "album" ""') + self.assertInResponse('OK') + class MusicDatabaseListTest(protocol.BaseTestCase): def test_list_foo_returns_ack(self): From d226db90397d5a021eaefe9fba07268356b54112 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 20 Nov 2012 08:49:55 +0100 Subject: [PATCH 04/39] docs: Fix docstring errors --- mopidy/core/playback.py | 4 +++- mopidy/core/tracklist.py | 20 +++++++------------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/mopidy/core/playback.py b/mopidy/core/playback.py index 94b4af9c..0cd2b3e8 100644 --- a/mopidy/core/playback.py +++ b/mopidy/core/playback.py @@ -324,6 +324,8 @@ class PlaybackController(object): def on_end_of_track(self): """ Tell the playback controller that end of track is reached. + + Used by event handler in :class:`mopidy.core.Core`. """ if self.state == PlaybackState.STOPPED: return @@ -343,7 +345,7 @@ class PlaybackController(object): """ Tell the playback controller that the current playlist has changed. - Used by :class:`mopidy.core.CurrentPlaylistController`. + Used by :class:`mopidy.core.TracklistController`. """ self._first_shuffle = True self._shuffled = [] diff --git a/mopidy/core/tracklist.py b/mopidy/core/tracklist.py index e00a42f9..fb84c112 100644 --- a/mopidy/core/tracklist.py +++ b/mopidy/core/tracklist.py @@ -63,8 +63,7 @@ class TracklistController(object): """ Add the track to the end of, or at the given position in the tracklist. - Triggers the :method:`mopidy.core.CoreListener.tracklist_changed` - event. + Triggers the :meth:`mopidy.core.CoreListener.tracklist_changed` event. :param track: track to add :type track: :class:`mopidy.models.Track` @@ -90,12 +89,11 @@ class TracklistController(object): """ Append the given tracks to the tracklist. - Triggers the :method:`mopidy.core.CoreListener.tracklist_changed` - event. + Triggers the :meth:`mopidy.core.CoreListener.tracklist_changed` event. :param tracks: tracks to append :type tracks: list of :class:`mopidy.models.Track` - :rtype: list of class:`mopidy.models.TlTrack` + :rtype: list of :class:`mopidy.models.TlTrack` """ tl_tracks = [] for track in tracks: @@ -110,8 +108,7 @@ class TracklistController(object): """ Clear the tracklist. - Triggers the :method:`mopidy.core.CoreListener.tracklist_changed` - event. + Triggers the :meth:`mopidy.core.CoreListener.tracklist_changed` event. """ self._tl_tracks = [] self.version += 1 @@ -156,8 +153,7 @@ class TracklistController(object): """ Move the tracks in the slice ``[start:end]`` to ``to_position``. - Triggers the :method:`mopidy.core.CoreListener.tracklist_changed` - event. + Triggers the :meth:`mopidy.core.CoreListener.tracklist_changed` event. :param start: position of first track to move :type start: int @@ -192,8 +188,7 @@ class TracklistController(object): Uses :meth:`filter()` to lookup the tracks to remove. - Triggers the :method:`mopidy.core.CoreListener.tracklist_changed` - event. + Triggers the :meth:`mopidy.core.CoreListener.tracklist_changed` event. :param criteria: on or more criteria to match by :type criteria: dict @@ -211,8 +206,7 @@ class TracklistController(object): Shuffles the entire tracklist. If ``start`` and ``end`` is given only shuffles the slice ``[start:end]``. - Triggers the :method:`mopidy.core.CoreListener.tracklist_changed` - event. + Triggers the :meth:`mopidy.core.CoreListener.tracklist_changed` event. :param start: position of first track to shuffle :type start: int or :class:`None` From e9658453b002daa5d0e59fac8c74f1b62e955d02 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 20 Nov 2012 08:51:19 +0100 Subject: [PATCH 05/39] core: Make tracklist.version read-only --- mopidy/core/tracklist.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/mopidy/core/tracklist.py b/mopidy/core/tracklist.py index fb84c112..7e26709e 100644 --- a/mopidy/core/tracklist.py +++ b/mopidy/core/tracklist.py @@ -53,9 +53,8 @@ class TracklistController(object): """ return self._version - @version.setter # noqa - def version(self, version): - self._version = version + def _increase_version(self): + self._version += 1 self._core.playback.on_tracklist_change() self._trigger_tracklist_changed() @@ -81,7 +80,7 @@ class TracklistController(object): else: self._tl_tracks.append(tl_track) if increase_version: - self.version += 1 + self._increase_version() self._next_tlid += 1 return tl_track @@ -100,7 +99,7 @@ class TracklistController(object): tl_tracks.append(self.add(track, increase_version=False)) if tracks: - self.version += 1 + self._increase_version() return tl_tracks @@ -111,7 +110,7 @@ class TracklistController(object): Triggers the :meth:`mopidy.core.CoreListener.tracklist_changed` event. """ self._tl_tracks = [] - self.version += 1 + self._increase_version() def filter(self, **criteria): """ @@ -180,7 +179,7 @@ class TracklistController(object): new_tl_tracks.insert(to_position, tl_track) to_position += 1 self._tl_tracks = new_tl_tracks - self.version += 1 + self._increase_version() def remove(self, **criteria): """ @@ -198,7 +197,7 @@ class TracklistController(object): for tl_track in tl_tracks: position = self._tl_tracks.index(tl_track) del self._tl_tracks[position] - self.version += 1 + self._increase_version() return tl_tracks def shuffle(self, start=None, end=None): @@ -230,7 +229,7 @@ class TracklistController(object): after = tl_tracks[end or len(tl_tracks):] random.shuffle(shuffled) self._tl_tracks = before + shuffled + after - self.version += 1 + self._increase_version() def slice(self, start, end): """ From f588787ac3d1ce5cbf8a7caa6629881b521b40d7 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 20 Nov 2012 09:13:34 +0100 Subject: [PATCH 06/39] core: Expose getters/setters for all properties This will be useful when exposing the core API over various protocols, e.g. JSON-RPC. --- mopidy/core/actor.py | 7 +- mopidy/core/playback.py | 210 +++++++++++++++++++-------------------- mopidy/core/playlists.py | 15 +-- mopidy/core/tracklist.py | 52 +++++----- 4 files changed, 145 insertions(+), 139 deletions(-) diff --git a/mopidy/core/actor.py b/mopidy/core/actor.py index a4f184bf..cd4ba180 100644 --- a/mopidy/core/actor.py +++ b/mopidy/core/actor.py @@ -46,14 +46,15 @@ class Core(pykka.ThreadingActor, AudioListener, BackendListener): self.tracklist = TracklistController(core=self) - @property - def uri_schemes(self): - """List of URI schemes we can handle""" + def get_uri_schemes(self): futures = [b.uri_schemes for b in self.backends] results = pykka.get_all(futures) uri_schemes = itertools.chain(*results) return sorted(uri_schemes) + uri_schemes = property(get_uri_schemes) + """List of URI schemes we can handle""" + def reached_end_of_stream(self): self.playback.on_end_of_track() diff --git a/mopidy/core/playback.py b/mopidy/core/playback.py index 0cd2b3e8..e562a9b1 100644 --- a/mopidy/core/playback.py +++ b/mopidy/core/playback.py @@ -79,42 +79,28 @@ class PlaybackController(object): uri_scheme = urlparse.urlparse(uri).scheme return self.backends.with_playback_by_uri_scheme.get(uri_scheme, None) - def _get_tlid(self, tl_track): - if tl_track is None: - return None - return tl_track.tlid + def get_current_tlid(self): + return self.current_tl_track and self.current_tl_track.tlid - def _get_track(self, tl_track): - if tl_track is None: - return None - return tl_track.track + current_tlid = property(get_current_tlid) + """ + The TLID (tracklist ID) of the currently playing or selected + track. - @property - def current_tlid(self): - """ - The TLID (tracklist ID) of the currently playing or selected - track. + Read-only. Extracted from :attr:`current_tl_track` for convenience. + """ - Read-only. Extracted from :attr:`current_tl_track` for convenience. - """ - return self._get_tlid(self.current_tl_track) + def get_current_track(self): + return self.current_tl_track and self.current_tl_track.track - @property - def current_track(self): - """ - The currently playing or selected :class:`mopidy.models.Track`. + current_track = property(get_current_track) + """ + The currently playing or selected :class:`mopidy.models.Track`. - Read-only. Extracted from :attr:`current_tl_track` for convenience. - """ - return self._get_track(self.current_tl_track) + Read-only. Extracted from :attr:`current_tl_track` for convenience. + """ - @property - def tracklist_position(self): - """ - The position of the current track in the tracklist. - - Read-only. - """ + def get_tracklist_position(self): if self.current_tl_track is None: return None try: @@ -122,25 +108,25 @@ class PlaybackController(object): except ValueError: return None - @property - def track_at_eot(self): - """ - The track that will be played at the end of the current track. + tracklist_position = property(get_tracklist_position) + """ + The position of the current track in the tracklist. - Read-only. A :class:`mopidy.models.Track` extracted from - :attr:`tl_track_at_eot` for convenience. - """ - return self._get_track(self.tl_track_at_eot) + Read-only. + """ - @property - def tl_track_at_eot(self): - """ - The track that will be played at the end of the current track. + def get_track_at_eot(self): + return self.tl_track_at_eot and self.tl_track_at_eot.track - Read-only. A :class:`mopidy.models.TlTrack`. + track_at_eot = property(get_track_at_eot) + """ + The track that will be played at the end of the current track. - Not necessarily the same track as :attr:`tl_track_at_next`. - """ + Read-only. A :class:`mopidy.models.Track` extracted from + :attr:`tl_track_at_eot` for convenience. + """ + + def get_tl_track_at_eot(self): # pylint: disable = R0911 # Too many return statements @@ -173,28 +159,27 @@ class PlaybackController(object): except IndexError: return None - @property - def track_at_next(self): - """ - The track that will be played if calling :meth:`next()`. + tl_track_at_eot = property(get_tl_track_at_eot) + """ + The track that will be played at the end of the current track. - Read-only. A :class:`mopidy.models.Track` extracted from - :attr:`tl_track_at_next` for convenience. - """ - return self._get_track(self.tl_track_at_next) + Read-only. A :class:`mopidy.models.TlTrack`. - @property - def tl_track_at_next(self): - """ - The track that will be played if calling :meth:`next()`. + Not necessarily the same track as :attr:`tl_track_at_next`. + """ - Read-only. A :class:`mopidy.models.TlTrack`. + def get_track_at_next(self): + return self.tl_track_at_next and self.tl_track_at_next.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. - """ + track_at_next = property(get_track_at_next) + """ + The track that will be played if calling :meth:`next()`. + + Read-only. A :class:`mopidy.models.Track` extracted from + :attr:`tl_track_at_next` for convenience. + """ + + def get_tl_track_at_next(self): tl_tracks = self.core.tracklist.tl_tracks if not tl_tracks: @@ -221,27 +206,30 @@ class PlaybackController(object): except IndexError: return None - @property - def track_at_previous(self): - """ - The track that will be played if calling :meth:`previous()`. + tl_track_at_next = property(get_tl_track_at_next) + """ + The track that will be played if calling :meth:`next()`. - Read-only. A :class:`mopidy.models.Track` extracted from - :attr:`tl_track_at_previous` for convenience. - """ - return self._get_track(self.tl_track_at_previous) + Read-only. A :class:`mopidy.models.TlTrack`. - @property - def tl_track_at_previous(self): - """ - The track that will be played if calling :meth:`previous()`. + 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. + """ - A :class:`mopidy.models.TlTrack`. + def get_track_at_previous(self): + return self.tl_track_at_previous and self.tl_track_at_previous.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. - """ + track_at_previous = property(get_track_at_previous) + """ + The track that will be played if calling :meth:`previous()`. + + Read-only. A :class:`mopidy.models.Track` extracted from + :attr:`tl_track_at_previous` for convenience. + """ + + def get_tl_track_at_previous(self): if self.repeat or self.consume or self.random: return self.current_tl_track @@ -250,59 +238,71 @@ class PlaybackController(object): return self.core.tracklist.tl_tracks[self.tracklist_position - 1] - @property - def state(self): - """ - The playback state. Must be :attr:`PLAYING`, :attr:`PAUSED`, or - :attr:`STOPPED`. + tl_track_at_previous = property(get_tl_track_at_previous) + """ + The track that will be played if calling :meth:`previous()`. - Possible states and transitions: + A :class:`mopidy.models.TlTrack`. - .. digraph:: state_transitions + 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. + """ - "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" ] - """ + def get_state(self): return self._state - @state.setter # noqa - def state(self, new_state): + def set_state(self, new_state): (old_state, self._state) = (self.state, new_state) logger.debug('Changing state: %s -> %s', old_state, new_state) self._trigger_playback_state_changed(old_state, new_state) - @property - def time_position(self): - """Time position in milliseconds.""" + state = property(get_state, set_state) + """ + 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" ] + """ + + def get_time_position(self): backend = self._get_backend() if backend: return backend.playback.get_time_position().get() else: return 0 - @property - def volume(self): - """Volume as int in range [0..100] or :class:`None`""" + time_position = property(get_time_position) + """Time position in milliseconds.""" + + def get_volume(self): if self.audio: return self.audio.get_volume().get() else: # For testing return self._volume - @volume.setter # noqa - def volume(self, volume): + def set_volume(self, volume): if self.audio: self.audio.set_volume(volume) else: # For testing self._volume = volume + volume = property(get_volume, set_volume) + """Volume as int in range [0..100] or :class:`None`""" + def change_track(self, tl_track, on_error_step=1): """ Change to the given track, keeping the current playback state. diff --git a/mopidy/core/playlists.py b/mopidy/core/playlists.py index dcdc665f..6a368ac6 100644 --- a/mopidy/core/playlists.py +++ b/mopidy/core/playlists.py @@ -15,18 +15,19 @@ class PlaylistsController(object): self.backends = backends self.core = core - @property - def playlists(self): - """ - The available playlists. - - Read-only. List of :class:`mopidy.models.Playlist`. - """ + def get_playlists(self): futures = [ b.playlists.playlists for b in self.backends.with_playlists] results = pykka.get_all(futures) return list(itertools.chain(*results)) + playlists = property(get_playlists) + """ + The available playlists. + + Read-only. List of :class:`mopidy.models.Playlist`. + """ + def create(self, name, uri_scheme=None): """ Create a new playlist. diff --git a/mopidy/core/tracklist.py b/mopidy/core/tracklist.py index 7e26709e..05d551fe 100644 --- a/mopidy/core/tracklist.py +++ b/mopidy/core/tracklist.py @@ -20,37 +20,33 @@ class TracklistController(object): self._tl_tracks = [] self._version = 0 - @property - def tl_tracks(self): - """ - List of :class:`mopidy.models.TlTrack`. - - Read-only. - """ + def get_tl_tracks(self): return self._tl_tracks[:] - @property - def tracks(self): - """ - List of :class:`mopidy.models.Track` in the tracklist. + tl_tracks = property(get_tl_tracks) + """ + List of :class:`mopidy.models.TlTrack`. - Read-only. - """ + Read-only. + """ + + def get_tracks(self): return [tl_track.track for tl_track in self._tl_tracks] - @property - def length(self): - """ - Length of the tracklist. - """ + tracks = property(get_tracks) + """ + List of :class:`mopidy.models.Track` in the tracklist. + + Read-only. + """ + + def get_length(self): return len(self._tl_tracks) - @property - def version(self): - """ - The tracklist version. Integer which is increased every time the - tracklist is changed. Is not reset before Mopidy is restarted. - """ + length = property(get_length) + """Length of the tracklist.""" + + def get_version(self): return self._version def _increase_version(self): @@ -58,6 +54,14 @@ class TracklistController(object): self._core.playback.on_tracklist_change() self._trigger_tracklist_changed() + version = property(get_version) + """ + The tracklist version. + + Read-only. Integer which is increased every time the tracklist is changed. + Is not reset before Mopidy is restarted. + """ + def add(self, track, at_position=None, increase_version=True): """ Add the track to the end of, or at the given position in the tracklist. From ee8c2ca58911e92f0dfd867fe5be8a112793d21e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 20 Nov 2012 09:24:37 +0100 Subject: [PATCH 07/39] tests: Rename populate_playlist() to populate_tracklist() --- tests/backends/base/__init__.py | 2 +- tests/backends/base/playback.py | 212 +++++++++++++++---------------- tests/backends/base/tracklist.py | 50 ++++---- 3 files changed, 132 insertions(+), 132 deletions(-) diff --git a/tests/backends/base/__init__.py b/tests/backends/base/__init__.py index ec3ec1df..c415ef23 100644 --- a/tests/backends/base/__init__.py +++ b/tests/backends/base/__init__.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals -def populate_playlist(func): +def populate_tracklist(func): def wrapper(self): for track in self.tracks: self.core.tracklist.add(track) diff --git a/tests/backends/base/playback.py b/tests/backends/base/playback.py index 21e377d9..94fc7759 100644 --- a/tests/backends/base/playback.py +++ b/tests/backends/base/playback.py @@ -9,7 +9,7 @@ from mopidy.core import PlaybackState from mopidy.models import Track from tests import unittest -from tests.backends.base import populate_playlist +from tests.backends.base import populate_tracklist # TODO Test 'playlist repeat', e.g. repeat=1,single=0 @@ -40,35 +40,35 @@ class PlaybackControllerTest(object): def test_play_with_empty_playlist_return_value(self): self.assertEqual(self.playback.play(), None) - @populate_playlist + @populate_tracklist def test_play_state(self): self.assertEqual(self.playback.state, PlaybackState.STOPPED) self.playback.play() self.assertEqual(self.playback.state, PlaybackState.PLAYING) - @populate_playlist + @populate_tracklist def test_play_return_value(self): self.assertEqual(self.playback.play(), None) - @populate_playlist + @populate_tracklist def test_play_track_state(self): self.assertEqual(self.playback.state, PlaybackState.STOPPED) self.playback.play(self.tracklist.tl_tracks[-1]) self.assertEqual(self.playback.state, PlaybackState.PLAYING) - @populate_playlist + @populate_tracklist def test_play_track_return_value(self): self.assertEqual(self.playback.play( self.tracklist.tl_tracks[-1]), None) - @populate_playlist + @populate_tracklist def test_play_when_playing(self): self.playback.play() track = self.playback.current_track self.playback.play() self.assertEqual(track, self.playback.current_track) - @populate_playlist + @populate_tracklist def test_play_when_paused(self): self.playback.play() track = self.playback.current_track @@ -77,7 +77,7 @@ class PlaybackControllerTest(object): self.assertEqual(self.playback.state, PlaybackState.PLAYING) self.assertEqual(track, self.playback.current_track) - @populate_playlist + @populate_tracklist def test_play_when_pause_after_next(self): self.playback.play() self.playback.next() @@ -88,17 +88,17 @@ class PlaybackControllerTest(object): self.assertEqual(self.playback.state, PlaybackState.PLAYING) self.assertEqual(track, self.playback.current_track) - @populate_playlist + @populate_tracklist def test_play_sets_current_track(self): self.playback.play() self.assertEqual(self.playback.current_track, self.tracks[0]) - @populate_playlist + @populate_tracklist def test_play_track_sets_current_track(self): self.playback.play(self.tracklist.tl_tracks[-1]) self.assertEqual(self.playback.current_track, self.tracks[-1]) - @populate_playlist + @populate_tracklist def test_play_skips_to_next_track_on_failure(self): # If backend's play() returns False, it is a failure. self.backend.playback.play = lambda track: track != self.tracks[0] @@ -106,7 +106,7 @@ class PlaybackControllerTest(object): self.assertNotEqual(self.playback.current_track, self.tracks[0]) self.assertEqual(self.playback.current_track, self.tracks[1]) - @populate_playlist + @populate_tracklist def test_current_track_after_completed_playlist(self): self.playback.play(self.tracklist.tl_tracks[-1]) self.playback.on_end_of_track() @@ -118,14 +118,14 @@ class PlaybackControllerTest(object): self.assertEqual(self.playback.state, PlaybackState.STOPPED) self.assertEqual(self.playback.current_track, None) - @populate_playlist + @populate_tracklist def test_previous(self): self.playback.play() self.playback.next() self.playback.previous() self.assertEqual(self.playback.current_track, self.tracks[0]) - @populate_playlist + @populate_tracklist def test_previous_more(self): self.playback.play() # At track 0 self.playback.next() # At track 1 @@ -133,13 +133,13 @@ class PlaybackControllerTest(object): self.playback.previous() # At track 1 self.assertEqual(self.playback.current_track, self.tracks[1]) - @populate_playlist + @populate_tracklist def test_previous_return_value(self): self.playback.play() self.playback.next() self.assertEqual(self.playback.previous(), None) - @populate_playlist + @populate_tracklist def test_previous_does_not_trigger_playback(self): self.playback.play() self.playback.next() @@ -147,7 +147,7 @@ class PlaybackControllerTest(object): self.playback.previous() self.assertEqual(self.playback.state, PlaybackState.STOPPED) - @populate_playlist + @populate_tracklist def test_previous_at_start_of_playlist(self): self.playback.previous() self.assertEqual(self.playback.state, PlaybackState.STOPPED) @@ -158,7 +158,7 @@ class PlaybackControllerTest(object): self.assertEqual(self.playback.state, PlaybackState.STOPPED) self.assertEqual(self.playback.current_track, None) - @populate_playlist + @populate_tracklist def test_previous_skips_to_previous_track_on_failure(self): # If backend's play() returns False, it is a failure. self.backend.playback.play = lambda track: track != self.tracks[1] @@ -168,7 +168,7 @@ class PlaybackControllerTest(object): self.assertNotEqual(self.playback.current_track, self.tracks[1]) self.assertEqual(self.playback.current_track, self.tracks[0]) - @populate_playlist + @populate_tracklist def test_next(self): self.playback.play() @@ -181,17 +181,17 @@ class PlaybackControllerTest(object): self.playback.tracklist_position, old_position + 1) self.assertNotEqual(self.playback.current_track.uri, old_uri) - @populate_playlist + @populate_tracklist def test_next_return_value(self): self.playback.play() self.assertEqual(self.playback.next(), None) - @populate_playlist + @populate_tracklist def test_next_does_not_trigger_playback(self): self.playback.next() self.assertEqual(self.playback.state, PlaybackState.STOPPED) - @populate_playlist + @populate_tracklist def test_next_at_end_of_playlist(self): self.playback.play() @@ -204,7 +204,7 @@ class PlaybackControllerTest(object): self.assertEqual(self.playback.state, PlaybackState.STOPPED) - @populate_playlist + @populate_tracklist def test_next_until_end_of_playlist_and_play_from_start(self): self.playback.play() @@ -222,7 +222,7 @@ class PlaybackControllerTest(object): self.playback.next() self.assertEqual(self.playback.state, PlaybackState.STOPPED) - @populate_playlist + @populate_tracklist def test_next_skips_to_next_track_on_failure(self): # If backend's play() returns False, it is a failure. self.backend.playback.play = lambda track: track != self.tracks[1] @@ -232,16 +232,16 @@ class PlaybackControllerTest(object): self.assertNotEqual(self.playback.current_track, self.tracks[1]) self.assertEqual(self.playback.current_track, self.tracks[2]) - @populate_playlist + @populate_tracklist def test_next_track_before_play(self): self.assertEqual(self.playback.track_at_next, self.tracks[0]) - @populate_playlist + @populate_tracklist def test_next_track_during_play(self): self.playback.play() self.assertEqual(self.playback.track_at_next, self.tracks[1]) - @populate_playlist + @populate_tracklist def test_next_track_after_previous(self): self.playback.play() self.playback.next() @@ -251,14 +251,14 @@ class PlaybackControllerTest(object): def test_next_track_empty_playlist(self): self.assertEqual(self.playback.track_at_next, None) - @populate_playlist + @populate_tracklist def test_next_track_at_end_of_playlist(self): self.playback.play() for _ in self.tracklist.tl_tracks[1:]: self.playback.next() self.assertEqual(self.playback.track_at_next, None) - @populate_playlist + @populate_tracklist def test_next_track_at_end_of_playlist_with_repeat(self): self.playback.repeat = True self.playback.play() @@ -266,20 +266,20 @@ class PlaybackControllerTest(object): self.playback.next() self.assertEqual(self.playback.track_at_next, self.tracks[0]) - @populate_playlist + @populate_tracklist def test_next_track_with_random(self): random.seed(1) self.playback.random = True self.assertEqual(self.playback.track_at_next, self.tracks[2]) - @populate_playlist + @populate_tracklist def test_next_with_consume(self): self.playback.consume = True self.playback.play() self.playback.next() self.assertIn(self.tracks[0], self.tracklist.tracks) - @populate_playlist + @populate_tracklist def test_next_with_single_and_repeat(self): self.playback.single = True self.playback.repeat = True @@ -287,7 +287,7 @@ class PlaybackControllerTest(object): self.playback.next() self.assertEqual(self.playback.current_track, self.tracks[1]) - @populate_playlist + @populate_tracklist def test_next_with_random(self): # FIXME feels very fragile random.seed(1) @@ -296,7 +296,7 @@ class PlaybackControllerTest(object): self.playback.next() self.assertEqual(self.playback.current_track, self.tracks[1]) - @populate_playlist + @populate_tracklist def test_next_track_with_random_after_append_playlist(self): random.seed(1) self.playback.random = True @@ -304,7 +304,7 @@ class PlaybackControllerTest(object): self.tracklist.append(self.tracks[:1]) self.assertEqual(self.playback.track_at_next, self.tracks[1]) - @populate_playlist + @populate_tracklist def test_end_of_track(self): self.playback.play() @@ -317,17 +317,17 @@ class PlaybackControllerTest(object): self.playback.tracklist_position, old_position + 1) self.assertNotEqual(self.playback.current_track.uri, old_uri) - @populate_playlist + @populate_tracklist def test_end_of_track_return_value(self): self.playback.play() self.assertEqual(self.playback.on_end_of_track(), None) - @populate_playlist + @populate_tracklist def test_end_of_track_does_not_trigger_playback(self): self.playback.on_end_of_track() self.assertEqual(self.playback.state, PlaybackState.STOPPED) - @populate_playlist + @populate_tracklist def test_end_of_track_at_end_of_playlist(self): self.playback.play() @@ -340,7 +340,7 @@ class PlaybackControllerTest(object): self.assertEqual(self.playback.state, PlaybackState.STOPPED) - @populate_playlist + @populate_tracklist def test_end_of_track_until_end_of_playlist_and_play_from_start(self): self.playback.play() @@ -358,7 +358,7 @@ class PlaybackControllerTest(object): self.playback.on_end_of_track() self.assertEqual(self.playback.state, PlaybackState.STOPPED) - @populate_playlist + @populate_tracklist def test_end_of_track_skips_to_next_track_on_failure(self): # If backend's play() returns False, it is a failure. self.backend.playback.play = lambda track: track != self.tracks[1] @@ -368,16 +368,16 @@ class PlaybackControllerTest(object): self.assertNotEqual(self.playback.current_track, self.tracks[1]) self.assertEqual(self.playback.current_track, self.tracks[2]) - @populate_playlist + @populate_tracklist def test_end_of_track_track_before_play(self): self.assertEqual(self.playback.track_at_next, self.tracks[0]) - @populate_playlist + @populate_tracklist def test_end_of_track_track_during_play(self): self.playback.play() self.assertEqual(self.playback.track_at_next, self.tracks[1]) - @populate_playlist + @populate_tracklist def test_end_of_track_track_after_previous(self): self.playback.play() self.playback.on_end_of_track() @@ -387,14 +387,14 @@ class PlaybackControllerTest(object): def test_end_of_track_track_empty_playlist(self): self.assertEqual(self.playback.track_at_next, None) - @populate_playlist + @populate_tracklist def test_end_of_track_track_at_end_of_playlist(self): self.playback.play() for _ in self.tracklist.tl_tracks[1:]: self.playback.on_end_of_track() self.assertEqual(self.playback.track_at_next, None) - @populate_playlist + @populate_tracklist def test_end_of_track_track_at_end_of_playlist_with_repeat(self): self.playback.repeat = True self.playback.play() @@ -402,20 +402,20 @@ class PlaybackControllerTest(object): self.playback.on_end_of_track() self.assertEqual(self.playback.track_at_next, self.tracks[0]) - @populate_playlist + @populate_tracklist def test_end_of_track_track_with_random(self): random.seed(1) self.playback.random = True self.assertEqual(self.playback.track_at_next, self.tracks[2]) - @populate_playlist + @populate_tracklist def test_end_of_track_with_consume(self): self.playback.consume = True self.playback.play() self.playback.on_end_of_track() self.assertNotIn(self.tracks[0], self.tracklist.tracks) - @populate_playlist + @populate_tracklist def test_end_of_track_with_random(self): # FIXME feels very fragile random.seed(1) @@ -424,7 +424,7 @@ class PlaybackControllerTest(object): self.playback.on_end_of_track() self.assertEqual(self.playback.current_track, self.tracks[1]) - @populate_playlist + @populate_tracklist def test_end_of_track_track_with_random_after_append_playlist(self): random.seed(1) self.playback.random = True @@ -432,22 +432,22 @@ class PlaybackControllerTest(object): self.tracklist.append(self.tracks[:1]) self.assertEqual(self.playback.track_at_next, self.tracks[1]) - @populate_playlist + @populate_tracklist def test_previous_track_before_play(self): self.assertEqual(self.playback.track_at_previous, None) - @populate_playlist + @populate_tracklist def test_previous_track_after_play(self): self.playback.play() self.assertEqual(self.playback.track_at_previous, None) - @populate_playlist + @populate_tracklist def test_previous_track_after_next(self): self.playback.play() self.playback.next() self.assertEqual(self.playback.track_at_previous, self.tracks[0]) - @populate_playlist + @populate_tracklist def test_previous_track_after_previous(self): self.playback.play() # At track 0 self.playback.next() # At track 1 @@ -458,7 +458,7 @@ class PlaybackControllerTest(object): def test_previous_track_empty_playlist(self): self.assertEqual(self.playback.track_at_previous, None) - @populate_playlist + @populate_tracklist def test_previous_track_with_consume(self): self.playback.consume = True for _ in self.tracks: @@ -466,7 +466,7 @@ class PlaybackControllerTest(object): self.assertEqual( self.playback.track_at_previous, self.playback.current_track) - @populate_playlist + @populate_tracklist def test_previous_track_with_random(self): self.playback.random = True for _ in self.tracks: @@ -474,37 +474,37 @@ class PlaybackControllerTest(object): self.assertEqual( self.playback.track_at_previous, self.playback.current_track) - @populate_playlist + @populate_tracklist def test_initial_current_track(self): self.assertEqual(self.playback.current_track, None) - @populate_playlist + @populate_tracklist def test_current_track_during_play(self): self.playback.play() self.assertEqual(self.playback.current_track, self.tracks[0]) - @populate_playlist + @populate_tracklist def test_current_track_after_next(self): self.playback.play() self.playback.next() self.assertEqual(self.playback.current_track, self.tracks[1]) - @populate_playlist + @populate_tracklist def test_initial_tracklist_position(self): self.assertEqual(self.playback.tracklist_position, None) - @populate_playlist + @populate_tracklist def test_tracklist_position_during_play(self): self.playback.play() self.assertEqual(self.playback.tracklist_position, 0) - @populate_playlist + @populate_tracklist def test_tracklist_position_after_next(self): self.playback.play() self.playback.next() self.assertEqual(self.playback.tracklist_position, 1) - @populate_playlist + @populate_tracklist def test_tracklist_position_at_end_of_playlist(self): self.playback.play(self.tracklist.tl_tracks[-1]) self.playback.on_end_of_track() @@ -524,7 +524,7 @@ class PlaybackControllerTest(object): self.assert_(wrapper.called) @unittest.SkipTest # Blocks for 10ms - @populate_playlist + @populate_tracklist def test_end_of_track_callback_gets_called(self): self.playback.play() result = self.playback.seek(self.tracks[0].length - 10) @@ -532,7 +532,7 @@ class PlaybackControllerTest(object): message = self.core_queue.get(True, 1) self.assertEqual('end_of_track', message['command']) - @populate_playlist + @populate_tracklist def test_on_tracklist_change_when_playing(self): self.playback.play() current_track = self.playback.current_track @@ -540,13 +540,13 @@ class PlaybackControllerTest(object): self.assertEqual(self.playback.state, PlaybackState.PLAYING) self.assertEqual(self.playback.current_track, current_track) - @populate_playlist + @populate_tracklist def test_on_tracklist_change_when_stopped(self): self.tracklist.append([self.tracks[2]]) self.assertEqual(self.playback.state, PlaybackState.STOPPED) self.assertEqual(self.playback.current_track, None) - @populate_playlist + @populate_tracklist def test_on_tracklist_change_when_paused(self): self.playback.play() self.playback.pause() @@ -555,55 +555,55 @@ class PlaybackControllerTest(object): self.assertEqual(self.playback.state, PlaybackState.PAUSED) self.assertEqual(self.playback.current_track, current_track) - @populate_playlist + @populate_tracklist def test_pause_when_stopped(self): self.playback.pause() self.assertEqual(self.playback.state, PlaybackState.PAUSED) - @populate_playlist + @populate_tracklist def test_pause_when_playing(self): self.playback.play() self.playback.pause() self.assertEqual(self.playback.state, PlaybackState.PAUSED) - @populate_playlist + @populate_tracklist def test_pause_when_paused(self): self.playback.play() self.playback.pause() self.playback.pause() self.assertEqual(self.playback.state, PlaybackState.PAUSED) - @populate_playlist + @populate_tracklist def test_pause_return_value(self): self.playback.play() self.assertEqual(self.playback.pause(), None) - @populate_playlist + @populate_tracklist def test_resume_when_stopped(self): self.playback.resume() self.assertEqual(self.playback.state, PlaybackState.STOPPED) - @populate_playlist + @populate_tracklist def test_resume_when_playing(self): self.playback.play() self.playback.resume() self.assertEqual(self.playback.state, PlaybackState.PLAYING) - @populate_playlist + @populate_tracklist def test_resume_when_paused(self): self.playback.play() self.playback.pause() self.playback.resume() self.assertEqual(self.playback.state, PlaybackState.PLAYING) - @populate_playlist + @populate_tracklist def test_resume_return_value(self): self.playback.play() self.playback.pause() self.assertEqual(self.playback.resume(), None) @unittest.SkipTest # Uses sleep and might not work with LocalBackend - @populate_playlist + @populate_tracklist def test_resume_continues_from_right_position(self): self.playback.play() time.sleep(0.2) @@ -611,12 +611,12 @@ class PlaybackControllerTest(object): self.playback.resume() self.assertNotEqual(self.playback.time_position, 0) - @populate_playlist + @populate_tracklist def test_seek_when_stopped(self): result = self.playback.seek(1000) self.assert_(result, 'Seek return value was %s' % result) - @populate_playlist + @populate_tracklist def test_seek_when_stopped_updates_position(self): self.playback.seek(1000) position = self.playback.time_position @@ -629,18 +629,18 @@ class PlaybackControllerTest(object): self.playback.seek(0) self.assertEqual(self.playback.state, PlaybackState.STOPPED) - @populate_playlist + @populate_tracklist def test_seek_when_stopped_triggers_play(self): self.playback.seek(0) self.assertEqual(self.playback.state, PlaybackState.PLAYING) - @populate_playlist + @populate_tracklist def test_seek_when_playing(self): self.playback.play() result = self.playback.seek(self.tracks[0].length - 1000) self.assert_(result, 'Seek return value was %s' % result) - @populate_playlist + @populate_tracklist def test_seek_when_playing_updates_position(self): length = self.tracklist.tracks[0].length self.playback.play() @@ -648,14 +648,14 @@ class PlaybackControllerTest(object): position = self.playback.time_position self.assertGreaterEqual(position, length - 1010) - @populate_playlist + @populate_tracklist def test_seek_when_paused(self): self.playback.play() self.playback.pause() result = self.playback.seek(self.tracks[0].length - 1000) self.assert_(result, 'Seek return value was %s' % result) - @populate_playlist + @populate_tracklist def test_seek_when_paused_updates_position(self): length = self.tracklist.tracks[0].length self.playback.play() @@ -664,7 +664,7 @@ class PlaybackControllerTest(object): position = self.playback.time_position self.assertGreaterEqual(position, length - 1010) - @populate_playlist + @populate_tracklist def test_seek_when_paused_triggers_play(self): self.playback.play() self.playback.pause() @@ -672,34 +672,34 @@ class PlaybackControllerTest(object): self.assertEqual(self.playback.state, PlaybackState.PLAYING) @unittest.SkipTest - @populate_playlist + @populate_tracklist def test_seek_beyond_end_of_song(self): # FIXME need to decide return value self.playback.play() result = self.playback.seek(self.tracks[0].length * 100) self.assert_(not result, 'Seek return value was %s' % result) - @populate_playlist + @populate_tracklist def test_seek_beyond_end_of_song_jumps_to_next_song(self): self.playback.play() self.playback.seek(self.tracks[0].length * 100) self.assertEqual(self.playback.current_track, self.tracks[1]) - @populate_playlist + @populate_tracklist def test_seek_beyond_end_of_song_for_last_track(self): self.playback.play(self.tracklist.tl_tracks[-1]) self.playback.seek(self.tracklist.tracks[-1].length * 100) self.assertEqual(self.playback.state, PlaybackState.STOPPED) @unittest.SkipTest - @populate_playlist + @populate_tracklist def test_seek_beyond_start_of_song(self): # FIXME need to decide return value self.playback.play() result = self.playback.seek(-1000) self.assert_(not result, 'Seek return value was %s' % result) - @populate_playlist + @populate_tracklist def test_seek_beyond_start_of_song_update_postion(self): self.playback.play() self.playback.seek(-1000) @@ -707,18 +707,18 @@ class PlaybackControllerTest(object): self.assertGreaterEqual(position, 0) self.assertEqual(self.playback.state, PlaybackState.PLAYING) - @populate_playlist + @populate_tracklist def test_stop_when_stopped(self): self.playback.stop() self.assertEqual(self.playback.state, PlaybackState.STOPPED) - @populate_playlist + @populate_tracklist def test_stop_when_playing(self): self.playback.play() self.playback.stop() self.assertEqual(self.playback.state, PlaybackState.STOPPED) - @populate_playlist + @populate_tracklist def test_stop_when_paused(self): self.playback.play() self.playback.pause() @@ -736,7 +736,7 @@ class PlaybackControllerTest(object): self.assertEqual(self.playback.time_position, 0) - @populate_playlist + @populate_tracklist def test_time_position_when_stopped_with_playlist(self): future = mock.Mock() future.get = mock.Mock(return_value=0) @@ -745,7 +745,7 @@ class PlaybackControllerTest(object): self.assertEqual(self.playback.time_position, 0) @unittest.SkipTest # Uses sleep and does might not work with LocalBackend - @populate_playlist + @populate_tracklist def test_time_position_when_playing(self): self.playback.play() first = self.playback.time_position @@ -754,7 +754,7 @@ class PlaybackControllerTest(object): self.assertGreater(second, first) @unittest.SkipTest # Uses sleep - @populate_playlist + @populate_tracklist def test_time_position_when_paused(self): self.playback.play() time.sleep(0.2) @@ -764,13 +764,13 @@ class PlaybackControllerTest(object): second = self.playback.time_position self.assertEqual(first, second) - @populate_playlist + @populate_tracklist def test_play_with_consume(self): self.playback.consume = True self.playback.play() self.assertEqual(self.playback.current_track, self.tracks[0]) - @populate_playlist + @populate_tracklist def test_playlist_is_empty_after_all_tracks_are_played_with_consume(self): self.playback.consume = True self.playback.play() @@ -778,14 +778,14 @@ class PlaybackControllerTest(object): self.playback.on_end_of_track() self.assertEqual(len(self.tracklist.tracks), 0) - @populate_playlist + @populate_tracklist def test_play_with_random(self): random.seed(1) self.playback.random = True self.playback.play() self.assertEqual(self.playback.current_track, self.tracks[2]) - @populate_playlist + @populate_tracklist def test_previous_with_random(self): random.seed(1) self.playback.random = True @@ -795,13 +795,13 @@ class PlaybackControllerTest(object): self.playback.previous() self.assertEqual(self.playback.current_track, current_track) - @populate_playlist + @populate_tracklist def test_end_of_song_starts_next_track(self): self.playback.play() self.playback.on_end_of_track() self.assertEqual(self.playback.current_track, self.tracks[1]) - @populate_playlist + @populate_tracklist def test_end_of_song_with_single_and_repeat_starts_same(self): self.playback.single = True self.playback.repeat = True @@ -809,7 +809,7 @@ class PlaybackControllerTest(object): self.playback.on_end_of_track() self.assertEqual(self.playback.current_track, self.tracks[0]) - @populate_playlist + @populate_tracklist def test_end_of_playlist_stops(self): self.playback.play(self.tracklist.tl_tracks[-1]) self.playback.on_end_of_track() @@ -824,7 +824,7 @@ class PlaybackControllerTest(object): def test_consume_off_by_default(self): self.assertEqual(self.playback.consume, False) - @populate_playlist + @populate_tracklist def test_random_until_end_of_playlist(self): self.playback.random = True self.playback.play() @@ -832,7 +832,7 @@ class PlaybackControllerTest(object): self.playback.next() self.assertEqual(self.playback.track_at_next, None) - @populate_playlist + @populate_tracklist def test_random_until_end_of_playlist_and_play_from_start(self): self.playback.repeat = True for _ in self.tracks: @@ -842,7 +842,7 @@ class PlaybackControllerTest(object): self.playback.play() self.assertEqual(self.playback.state, PlaybackState.PLAYING) - @populate_playlist + @populate_tracklist def test_random_until_end_of_playlist_with_repeat(self): self.playback.repeat = True self.playback.random = True @@ -851,7 +851,7 @@ class PlaybackControllerTest(object): self.playback.next() self.assertNotEqual(self.playback.track_at_next, None) - @populate_playlist + @populate_tracklist def test_played_track_during_random_not_played_again(self): self.playback.random = True self.playback.play() @@ -861,7 +861,7 @@ class PlaybackControllerTest(object): played.append(self.playback.current_track) self.playback.next() - @populate_playlist + @populate_tracklist def test_playing_track_that_isnt_in_playlist(self): test = lambda: self.playback.play((17, Track())) self.assertRaises(AssertionError, test) diff --git a/tests/backends/base/tracklist.py b/tests/backends/base/tracklist.py index a5fbbcb5..52ddfa46 100644 --- a/tests/backends/base/tracklist.py +++ b/tests/backends/base/tracklist.py @@ -9,7 +9,7 @@ from mopidy import audio, core from mopidy.core import PlaybackState from mopidy.models import TlTrack, Playlist, Track -from tests.backends.base import populate_playlist +from tests.backends.base import populate_tracklist class TracklistControllerTest(object): @@ -48,25 +48,25 @@ class TracklistControllerTest(object): self.assertEqual(tl_track, self.controller.tl_tracks[0]) self.assertEqual(track, tl_track.track) - @populate_playlist + @populate_tracklist def test_add_at_position_outside_of_playlist(self): test = lambda: self.controller.add( self.tracks[0], len(self.tracks) + 2) self.assertRaises(AssertionError, test) - @populate_playlist + @populate_tracklist def test_filter_by_tlid(self): tl_track = self.controller.tl_tracks[1] self.assertEqual( [tl_track], self.controller.filter(tlid=tl_track.tlid)) - @populate_playlist + @populate_tracklist def test_filter_by_uri(self): tl_track = self.controller.tl_tracks[1] self.assertEqual( [tl_track], self.controller.filter(uri=tl_track.track.uri)) - @populate_playlist + @populate_tracklist def test_filter_by_uri_returns_nothing_for_invalid_uri(self): self.assertEqual([], self.controller.filter(uri='foobar')) @@ -106,7 +106,7 @@ class TracklistControllerTest(object): self.controller.append([track1, track2, track3]) self.assertEqual(track2, self.controller.filter(uri='b')[0].track) - @populate_playlist + @populate_tracklist def test_clear(self): self.controller.clear() self.assertEqual(len(self.controller.tracks), 0) @@ -115,7 +115,7 @@ class TracklistControllerTest(object): self.controller.clear() self.assertEqual(len(self.controller.tracks), 0) - @populate_playlist + @populate_tracklist def test_clear_when_playing(self): self.playback.play() self.assertEqual(self.playback.state, PlaybackState.PLAYING) @@ -137,7 +137,7 @@ class TracklistControllerTest(object): self.controller.append([]) self.assertEqual(self.controller.version, version) - @populate_playlist + @populate_tracklist def test_append_preserves_playing_state(self): self.playback.play() track = self.playback.current_track @@ -145,13 +145,13 @@ class TracklistControllerTest(object): self.assertEqual(self.playback.state, PlaybackState.PLAYING) self.assertEqual(self.playback.current_track, track) - @populate_playlist + @populate_tracklist def test_append_preserves_stopped_state(self): self.controller.append(self.controller.tracks[1:2]) self.assertEqual(self.playback.state, PlaybackState.STOPPED) self.assertEqual(self.playback.current_track, None) - @populate_playlist + @populate_tracklist def test_append_returns_the_tl_tracks_that_was_added(self): tl_tracks = self.controller.append(self.controller.tracks[1:2]) self.assertEqual(tl_tracks[0].track, self.controller.tracks[1]) @@ -166,14 +166,14 @@ class TracklistControllerTest(object): test = lambda: self.controller.index(TlTrack(0, Track())) self.assertRaises(ValueError, test) - @populate_playlist + @populate_tracklist def test_move_single(self): self.controller.move(0, 0, 2) tracks = self.controller.tracks self.assertEqual(tracks[2], self.tracks[0]) - @populate_playlist + @populate_tracklist def test_move_group(self): self.controller.move(0, 2, 1) @@ -181,25 +181,25 @@ class TracklistControllerTest(object): self.assertEqual(tracks[1], self.tracks[0]) self.assertEqual(tracks[2], self.tracks[1]) - @populate_playlist + @populate_tracklist def test_moving_track_outside_of_playlist(self): tracks = len(self.controller.tracks) test = lambda: self.controller.move(0, 0, tracks + 5) self.assertRaises(AssertionError, test) - @populate_playlist + @populate_tracklist def test_move_group_outside_of_playlist(self): tracks = len(self.controller.tracks) test = lambda: self.controller.move(0, 2, tracks + 5) self.assertRaises(AssertionError, test) - @populate_playlist + @populate_tracklist def test_move_group_out_of_range(self): tracks = len(self.controller.tracks) test = lambda: self.controller.move(tracks + 2, tracks + 3, 0) self.assertRaises(AssertionError, test) - @populate_playlist + @populate_tracklist def test_move_group_invalid_group(self): test = lambda: self.controller.move(2, 1, 0) self.assertRaises(AssertionError, test) @@ -209,7 +209,7 @@ class TracklistControllerTest(object): tracks2 = self.controller.tracks self.assertNotEqual(id(tracks1), id(tracks2)) - @populate_playlist + @populate_tracklist def test_remove(self): track1 = self.controller.tracks[1] track2 = self.controller.tracks[2] @@ -219,14 +219,14 @@ class TracklistControllerTest(object): self.assertNotIn(track1, self.controller.tracks) self.assertEqual(track2, self.controller.tracks[1]) - @populate_playlist + @populate_tracklist def test_removing_track_that_does_not_exist_does_nothing(self): self.controller.remove(uri='/nonexistant') def test_removing_from_empty_playlist_does_nothing(self): self.controller.remove(uri='/nonexistant') - @populate_playlist + @populate_tracklist def test_shuffle(self): random.seed(1) self.controller.shuffle() @@ -236,7 +236,7 @@ class TracklistControllerTest(object): self.assertNotEqual(self.tracks, shuffled_tracks) self.assertEqual(set(self.tracks), set(shuffled_tracks)) - @populate_playlist + @populate_tracklist def test_shuffle_subset(self): random.seed(1) self.controller.shuffle(1, 3) @@ -247,18 +247,18 @@ class TracklistControllerTest(object): self.assertEqual(self.tracks[0], shuffled_tracks[0]) self.assertEqual(set(self.tracks), set(shuffled_tracks)) - @populate_playlist + @populate_tracklist def test_shuffle_invalid_subset(self): test = lambda: self.controller.shuffle(3, 1) self.assertRaises(AssertionError, test) - @populate_playlist + @populate_tracklist def test_shuffle_superset(self): tracks = len(self.controller.tracks) test = lambda: self.controller.shuffle(1, tracks + 5) self.assertRaises(AssertionError, test) - @populate_playlist + @populate_tracklist def test_shuffle_open_subset(self): random.seed(1) self.controller.shuffle(1) @@ -269,14 +269,14 @@ class TracklistControllerTest(object): self.assertEqual(self.tracks[0], shuffled_tracks[0]) self.assertEqual(set(self.tracks), set(shuffled_tracks)) - @populate_playlist + @populate_tracklist def test_slice_returns_a_subset_of_tracks(self): track_slice = self.controller.slice(1, 3) self.assertEqual(2, len(track_slice)) self.assertEqual(self.tracks[1], track_slice[0].track) self.assertEqual(self.tracks[2], track_slice[1].track) - @populate_playlist + @populate_tracklist def test_slice_returns_empty_list_if_indexes_outside_tracks_list(self): self.assertEqual(0, len(self.controller.slice(7, 8))) self.assertEqual(0, len(self.controller.slice(-1, 1))) From f8bd291d5f6d868bfac6ed8a7e4656ea80f0e067 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 20 Nov 2012 14:26:06 +0100 Subject: [PATCH 08/39] spotify: Require pyspotify 1.9 --- docs/changes.rst | 4 ++++ mopidy/backends/spotify/__init__.py | 2 +- requirements/spotify.txt | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 requirements/spotify.txt diff --git a/docs/changes.rst b/docs/changes.rst index ffb7fbf6..ba4718df 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -8,6 +8,10 @@ This change log is used to track all major changes to Mopidy. v0.9.0 (in development) ======================= +**Dependencies** + +- pyspotify >= 1.9, < 1.10 is now required for Spotify support. + **Multiple backends support** Support for using the local and Spotify backends simultaneously have for a very diff --git a/mopidy/backends/spotify/__init__.py b/mopidy/backends/spotify/__init__.py index fa6feb99..141656cc 100644 --- a/mopidy/backends/spotify/__init__.py +++ b/mopidy/backends/spotify/__init__.py @@ -21,7 +21,7 @@ https://github.com/mopidy/mopidy/issues?labels=Spotify+backend **Dependencies:** - libspotify >= 12, < 13 (libspotify12 package from apt.mopidy.com) -- pyspotify >= 1.8, < 1.9 (python-spotify package from apt.mopidy.com) +- pyspotify >= 1.9, < 1.10 (python-spotify package from apt.mopidy.com) **Settings:** diff --git a/requirements/spotify.txt b/requirements/spotify.txt new file mode 100644 index 00000000..c37d4674 --- /dev/null +++ b/requirements/spotify.txt @@ -0,0 +1 @@ +pyspotify >= 1.9, < 1.10 From 4b6272037180a1f12511d7e1567ee2cb84aa31a6 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 20 Nov 2012 14:30:25 +0100 Subject: [PATCH 09/39] settings: Tweak docstrings --- mopidy/settings.py | 29 +++++++++-------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/mopidy/settings.py b/mopidy/settings.py index 22df5d2d..2e022bc2 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -219,44 +219,33 @@ SPOTIFY_PASSWORD = '' #: SPOTIFY_BITRATE = 160 SPOTIFY_BITRATE = 160 -#: Spotify proxy host +#: Spotify proxy host. +#: +#: Used by :mod:`mopidy.backends.spotify`. #: #: Example:: #: #: SPOTIFY_PROXY_HOST = u'protocol://host:port' #: -#: Used by :mod:`mopidy.backends.spotify` -#: -#: Default :: +#: Default:: #: #: SPOTIFY_PROXY_HOST = None -#: SPOTIFY_PROXY_HOST = None -#: Spotify proxy username +#: Spotify proxy username. #: -#: Example:: +#: Used by :mod:`mopidy.backends.spotify`. #: -#: SPOTIFY_PROXY_HOST = u'username' -#: -#: Used by :mod:`mopidy.backends.spotify` -#: -#: Default :: +#: Default:: #: #: SPOTIFY_PROXY_USERNAME = None -#: SPOTIFY_PROXY_USERNAME = None -#: Spotify proxy password -#: -#: Example:: -#: -#: SPOTIFY_PROXY_HOST = u'password' +#: Spotify proxy password. #: #: Used by :mod:`mopidy.backends.spotify` #: -#: Default :: +#: Default:: #: #: SPOTIFY_PROXY_PASSWORD = None -#: SPOTIFY_PROXY_PASSWORD = None From e87f6f70b17acde2a93476e82c7c0ffb80593401 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 20 Nov 2012 14:30:41 +0100 Subject: [PATCH 10/39] docs: Add Spotify proxy support to changelog --- docs/changes.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index ba4718df..5ed689a3 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -149,6 +149,9 @@ backends: This makes it possible to support lookup of artist or album URIs which then can expand to a list of tracks. +- Added support for connecting to the Spotify service through an HTTP or SOCKS + proxy, which is supported by pyspotify >= 1.9. + **Bug fixes** - :issue:`218`: The MPD commands ``listplaylist`` and ``listplaylistinfo`` now From 16518697c8cecbd17e1928493576357d12da19c0 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 20 Nov 2012 15:08:17 +0100 Subject: [PATCH 11/39] spotify: Only pause on connection error if playing Spotify has availability issues today, which makes this easy to reproduce and improve. Before this patch, the following was logged on a Spotify connection error when not playing: ERROR Spotify connection error: Can not connect to Spotify WARNING Setting GStreamer state to GST_STATE_PAUSED failed ERROR Resource not found. gstplaybin2.c(3824): setup_next_source (): /GstPlayBin2:playbin20 With this patch, only the first and relevant error message is logged. --- mopidy/backends/spotify/session_manager.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index 00d45e19..cfe4e433 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -10,7 +10,7 @@ import threading from spotify.manager import SpotifySessionManager as PyspotifySessionManager -from mopidy import settings +from mopidy import audio, settings from mopidy.backends.listener import BackendListener from mopidy.utils import process, versioning @@ -92,7 +92,8 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager): logger.info('Spotify connection OK') else: logger.error('Spotify connection error: %s', error) - self.backend.playback.pause() + if self.audio.state.get() == audio.PlaybackState.PLAYING: + self.backend.playback.pause() def message_to_user(self, session, message): """Callback used by pyspotify""" From fd49faeed3ef10c4f909dd073bd1d2b8a46f641b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 20 Nov 2012 15:11:29 +0100 Subject: [PATCH 12/39] spotify: Fix resume which was broken by fix for #227 --- mopidy/backends/spotify/playback.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mopidy/backends/spotify/playback.py b/mopidy/backends/spotify/playback.py index d3585021..e4534172 100644 --- a/mopidy/backends/spotify/playback.py +++ b/mopidy/backends/spotify/playback.py @@ -46,10 +46,11 @@ class SpotifyPlaybackProvider(base.BasePlaybackProvider): def resume(self): time_position = self.get_time_position() - self._timer.resume() - - return self.seek(time_position) + self.audio.prepare_change() + result = self.seek(time_position) + self.audio.start_playback() + return result def seek(self, time_position): self.backend.spotify.session.seek(time_position) From 70d4dba7aac8ebc654cfe06ced9c2e2ffe06496d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 20 Nov 2012 09:40:39 +0100 Subject: [PATCH 13/39] core: Remove playback.track_at_next --- docs/changes.rst | 3 +++ mopidy/core/playback.py | 11 -------- mopidy/frontends/mpris/objects.py | 4 +-- tests/backends/base/__init__.py | 3 ++- tests/backends/base/playback.py | 42 +++++++++++++++---------------- 5 files changed, 28 insertions(+), 35 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 5ed689a3..4bdacd78 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -149,6 +149,9 @@ backends: This makes it possible to support lookup of artist or album URIs which then can expand to a list of tracks. +- Remove :attr:`mopidy.core.PlaybackController.track_at_next`. Use + :attr:`mopidy.core.PlaybackController.tl_track_at_next` instead. + - Added support for connecting to the Spotify service through an HTTP or SOCKS proxy, which is supported by pyspotify >= 1.9. diff --git a/mopidy/core/playback.py b/mopidy/core/playback.py index e562a9b1..613f21ba 100644 --- a/mopidy/core/playback.py +++ b/mopidy/core/playback.py @@ -168,17 +168,6 @@ class PlaybackController(object): Not necessarily the same track as :attr:`tl_track_at_next`. """ - def get_track_at_next(self): - return self.tl_track_at_next and self.tl_track_at_next.track - - track_at_next = property(get_track_at_next) - """ - The track that will be played if calling :meth:`next()`. - - Read-only. A :class:`mopidy.models.Track` extracted from - :attr:`tl_track_at_next` for convenience. - """ - def get_tl_track_at_next(self): tl_tracks = self.core.tracklist.tl_tracks diff --git a/mopidy/frontends/mpris/objects.py b/mopidy/frontends/mpris/objects.py index 51b0d7e8..93f14a2d 100644 --- a/mopidy/frontends/mpris/objects.py +++ b/mopidy/frontends/mpris/objects.py @@ -419,8 +419,8 @@ class MprisObject(dbus.service.Object): if not self.get_CanControl(): return False return ( - self.core.playback.current_track.get() is not None or - self.core.playback.track_at_next.get() is not None) + self.core.playback.current_tl_track.get() is not None or + self.core.playback.tl_track_at_next.get() is not None) def get_CanPause(self): if not self.get_CanControl(): diff --git a/tests/backends/base/__init__.py b/tests/backends/base/__init__.py index c415ef23..477f8cc1 100644 --- a/tests/backends/base/__init__.py +++ b/tests/backends/base/__init__.py @@ -3,8 +3,9 @@ from __future__ import unicode_literals def populate_tracklist(func): def wrapper(self): + self.tl_tracks = [] for track in self.tracks: - self.core.tracklist.add(track) + self.tl_tracks.append(self.core.tracklist.add(track)) return func(self) wrapper.__name__ = func.__name__ diff --git a/tests/backends/base/playback.py b/tests/backends/base/playback.py index 94fc7759..619efbb2 100644 --- a/tests/backends/base/playback.py +++ b/tests/backends/base/playback.py @@ -234,29 +234,29 @@ class PlaybackControllerTest(object): @populate_tracklist def test_next_track_before_play(self): - self.assertEqual(self.playback.track_at_next, self.tracks[0]) + self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[0]) @populate_tracklist def test_next_track_during_play(self): self.playback.play() - self.assertEqual(self.playback.track_at_next, self.tracks[1]) + self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[1]) @populate_tracklist def test_next_track_after_previous(self): self.playback.play() self.playback.next() self.playback.previous() - self.assertEqual(self.playback.track_at_next, self.tracks[1]) + self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[1]) def test_next_track_empty_playlist(self): - self.assertEqual(self.playback.track_at_next, None) + self.assertEqual(self.playback.tl_track_at_next, None) @populate_tracklist def test_next_track_at_end_of_playlist(self): self.playback.play() for _ in self.tracklist.tl_tracks[1:]: self.playback.next() - self.assertEqual(self.playback.track_at_next, None) + self.assertEqual(self.playback.tl_track_at_next, None) @populate_tracklist def test_next_track_at_end_of_playlist_with_repeat(self): @@ -264,13 +264,13 @@ class PlaybackControllerTest(object): self.playback.play() for _ in self.tracks[1:]: self.playback.next() - self.assertEqual(self.playback.track_at_next, self.tracks[0]) + self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[0]) @populate_tracklist def test_next_track_with_random(self): random.seed(1) self.playback.random = True - self.assertEqual(self.playback.track_at_next, self.tracks[2]) + self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[2]) @populate_tracklist def test_next_with_consume(self): @@ -300,9 +300,9 @@ class PlaybackControllerTest(object): def test_next_track_with_random_after_append_playlist(self): random.seed(1) self.playback.random = True - self.assertEqual(self.playback.track_at_next, self.tracks[2]) + self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[2]) self.tracklist.append(self.tracks[:1]) - self.assertEqual(self.playback.track_at_next, self.tracks[1]) + self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[1]) @populate_tracklist def test_end_of_track(self): @@ -370,29 +370,29 @@ class PlaybackControllerTest(object): @populate_tracklist def test_end_of_track_track_before_play(self): - self.assertEqual(self.playback.track_at_next, self.tracks[0]) + self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[0]) @populate_tracklist def test_end_of_track_track_during_play(self): self.playback.play() - self.assertEqual(self.playback.track_at_next, self.tracks[1]) + self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[1]) @populate_tracklist def test_end_of_track_track_after_previous(self): self.playback.play() self.playback.on_end_of_track() self.playback.previous() - self.assertEqual(self.playback.track_at_next, self.tracks[1]) + self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[1]) def test_end_of_track_track_empty_playlist(self): - self.assertEqual(self.playback.track_at_next, None) + self.assertEqual(self.playback.tl_track_at_next, None) @populate_tracklist def test_end_of_track_track_at_end_of_playlist(self): self.playback.play() for _ in self.tracklist.tl_tracks[1:]: self.playback.on_end_of_track() - self.assertEqual(self.playback.track_at_next, None) + self.assertEqual(self.playback.tl_track_at_next, None) @populate_tracklist def test_end_of_track_track_at_end_of_playlist_with_repeat(self): @@ -400,13 +400,13 @@ class PlaybackControllerTest(object): self.playback.play() for _ in self.tracks[1:]: self.playback.on_end_of_track() - self.assertEqual(self.playback.track_at_next, self.tracks[0]) + self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[0]) @populate_tracklist def test_end_of_track_track_with_random(self): random.seed(1) self.playback.random = True - self.assertEqual(self.playback.track_at_next, self.tracks[2]) + self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[2]) @populate_tracklist def test_end_of_track_with_consume(self): @@ -428,9 +428,9 @@ class PlaybackControllerTest(object): def test_end_of_track_track_with_random_after_append_playlist(self): random.seed(1) self.playback.random = True - self.assertEqual(self.playback.track_at_next, self.tracks[2]) + self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[2]) self.tracklist.append(self.tracks[:1]) - self.assertEqual(self.playback.track_at_next, self.tracks[1]) + self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[1]) @populate_tracklist def test_previous_track_before_play(self): @@ -830,14 +830,14 @@ class PlaybackControllerTest(object): self.playback.play() for _ in self.tracks[1:]: self.playback.next() - self.assertEqual(self.playback.track_at_next, None) + self.assertEqual(self.playback.tl_track_at_next, None) @populate_tracklist def test_random_until_end_of_playlist_and_play_from_start(self): self.playback.repeat = True for _ in self.tracks: self.playback.next() - self.assertNotEqual(self.playback.track_at_next, None) + self.assertNotEqual(self.playback.tl_track_at_next, None) self.assertEqual(self.playback.state, PlaybackState.STOPPED) self.playback.play() self.assertEqual(self.playback.state, PlaybackState.PLAYING) @@ -849,7 +849,7 @@ class PlaybackControllerTest(object): self.playback.play() for _ in self.tracks: self.playback.next() - self.assertNotEqual(self.playback.track_at_next, None) + self.assertNotEqual(self.playback.tl_track_at_next, None) @populate_tracklist def test_played_track_during_random_not_played_again(self): From 4c19321500bebb67eba3dec9639482345b90c732 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 20 Nov 2012 09:41:46 +0100 Subject: [PATCH 14/39] core: Remove playback.track_at_eot --- docs/changes.rst | 3 +++ mopidy/core/playback.py | 11 ----------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 4bdacd78..a3891424 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -152,6 +152,9 @@ backends: - Remove :attr:`mopidy.core.PlaybackController.track_at_next`. Use :attr:`mopidy.core.PlaybackController.tl_track_at_next` instead. +- Remove :attr:`mopidy.core.PlaybackController.track_at_eot`. Use + :attr:`mopidy.core.PlaybackController.tl_track_at_eot` instead. + - Added support for connecting to the Spotify service through an HTTP or SOCKS proxy, which is supported by pyspotify >= 1.9. diff --git a/mopidy/core/playback.py b/mopidy/core/playback.py index 613f21ba..cab2c392 100644 --- a/mopidy/core/playback.py +++ b/mopidy/core/playback.py @@ -115,17 +115,6 @@ class PlaybackController(object): Read-only. """ - def get_track_at_eot(self): - return self.tl_track_at_eot and self.tl_track_at_eot.track - - track_at_eot = property(get_track_at_eot) - """ - The track that will be played at the end of the current track. - - Read-only. A :class:`mopidy.models.Track` extracted from - :attr:`tl_track_at_eot` for convenience. - """ - def get_tl_track_at_eot(self): # pylint: disable = R0911 # Too many return statements From 2f2716767791cd21e51570b1d5a3d19115180419 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 20 Nov 2012 09:44:09 +0100 Subject: [PATCH 15/39] core: Remove playback.track_at_previous --- docs/changes.rst | 3 +++ mopidy/core/playback.py | 11 ----------- tests/backends/base/playback.py | 16 +++++++++------- 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index a3891424..77c418bf 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -149,6 +149,9 @@ backends: This makes it possible to support lookup of artist or album URIs which then can expand to a list of tracks. +- Remove :attr:`mopidy.core.PlaybackController.track_at_previous`. Use + :attr:`mopidy.core.PlaybackController.tl_track_at_previous` instead. + - Remove :attr:`mopidy.core.PlaybackController.track_at_next`. Use :attr:`mopidy.core.PlaybackController.tl_track_at_next` instead. diff --git a/mopidy/core/playback.py b/mopidy/core/playback.py index cab2c392..901c7e34 100644 --- a/mopidy/core/playback.py +++ b/mopidy/core/playback.py @@ -196,17 +196,6 @@ class PlaybackController(object): before the list repeats. """ - def get_track_at_previous(self): - return self.tl_track_at_previous and self.tl_track_at_previous.track - - track_at_previous = property(get_track_at_previous) - """ - The track that will be played if calling :meth:`previous()`. - - Read-only. A :class:`mopidy.models.Track` extracted from - :attr:`tl_track_at_previous` for convenience. - """ - def get_tl_track_at_previous(self): if self.repeat or self.consume or self.random: return self.current_tl_track diff --git a/tests/backends/base/playback.py b/tests/backends/base/playback.py index 619efbb2..fffe09da 100644 --- a/tests/backends/base/playback.py +++ b/tests/backends/base/playback.py @@ -434,18 +434,18 @@ class PlaybackControllerTest(object): @populate_tracklist def test_previous_track_before_play(self): - self.assertEqual(self.playback.track_at_previous, None) + self.assertEqual(self.playback.tl_track_at_previous, None) @populate_tracklist def test_previous_track_after_play(self): self.playback.play() - self.assertEqual(self.playback.track_at_previous, None) + self.assertEqual(self.playback.tl_track_at_previous, None) @populate_tracklist def test_previous_track_after_next(self): self.playback.play() self.playback.next() - self.assertEqual(self.playback.track_at_previous, self.tracks[0]) + self.assertEqual(self.playback.tl_track_at_previous, self.tl_tracks[0]) @populate_tracklist def test_previous_track_after_previous(self): @@ -453,10 +453,10 @@ class PlaybackControllerTest(object): self.playback.next() # At track 1 self.playback.next() # At track 2 self.playback.previous() # At track 1 - self.assertEqual(self.playback.track_at_previous, self.tracks[0]) + self.assertEqual(self.playback.tl_track_at_previous, self.tl_tracks[0]) def test_previous_track_empty_playlist(self): - self.assertEqual(self.playback.track_at_previous, None) + self.assertEqual(self.playback.tl_track_at_previous, None) @populate_tracklist def test_previous_track_with_consume(self): @@ -464,7 +464,8 @@ class PlaybackControllerTest(object): for _ in self.tracks: self.playback.next() self.assertEqual( - self.playback.track_at_previous, self.playback.current_track) + self.playback.tl_track_at_previous, + self.playback.current_tl_track) @populate_tracklist def test_previous_track_with_random(self): @@ -472,7 +473,8 @@ class PlaybackControllerTest(object): for _ in self.tracks: self.playback.next() self.assertEqual( - self.playback.track_at_previous, self.playback.current_track) + self.playback.tl_track_at_previous, + self.playback.current_tl_track) @populate_tracklist def test_initial_current_track(self): From d107b13fcbb16b2fb7fc0210f1607d6a1abad222 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 20 Nov 2012 10:09:46 +0100 Subject: [PATCH 16/39] core: Remove playback.current_tlid --- docs/changes.rst | 3 +++ mopidy/core/playback.py | 11 ----------- mopidy/frontends/mpd/protocol/current_playlist.py | 3 ++- mopidy/frontends/mpd/protocol/playback.py | 9 +++++---- tests/frontends/mpd/protocol/playback_test.py | 2 +- 5 files changed, 11 insertions(+), 17 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 77c418bf..d664872b 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -158,6 +158,9 @@ backends: - Remove :attr:`mopidy.core.PlaybackController.track_at_eot`. Use :attr:`mopidy.core.PlaybackController.tl_track_at_eot` instead. +- Remove :attr:`mopidy.core.PlaybackController.current_tlid`. Use + :attr:`mopidy.core.PlaybackController.current_tl_track` instead. + - Added support for connecting to the Spotify service through an HTTP or SOCKS proxy, which is supported by pyspotify >= 1.9. diff --git a/mopidy/core/playback.py b/mopidy/core/playback.py index 901c7e34..94fd7d4e 100644 --- a/mopidy/core/playback.py +++ b/mopidy/core/playback.py @@ -79,17 +79,6 @@ class PlaybackController(object): uri_scheme = urlparse.urlparse(uri).scheme return self.backends.with_playback_by_uri_scheme.get(uri_scheme, None) - def get_current_tlid(self): - return self.current_tl_track and self.current_tl_track.tlid - - current_tlid = property(get_current_tlid) - """ - The TLID (tracklist ID) of the currently playing or selected - track. - - Read-only. Extracted from :attr:`current_tl_track` for convenience. - """ - def get_current_track(self): return self.current_tl_track and self.current_tl_track.track diff --git a/mopidy/frontends/mpd/protocol/current_playlist.py b/mopidy/frontends/mpd/protocol/current_playlist.py index da950078..69e04d4b 100644 --- a/mopidy/frontends/mpd/protocol/current_playlist.py +++ b/mopidy/frontends/mpd/protocol/current_playlist.py @@ -110,7 +110,8 @@ def deleteid(context, tlid): Deletes the song ``SONGID`` from the playlist """ tlid = int(tlid) - if context.core.playback.current_tlid.get() == tlid: + tl_track = context.core.playback.current_tl_track.get() + if tl_track and tl_track.tlid == tlid: context.core.playback.next() tl_tracks = context.core.tracklist.remove(tlid=tlid).get() if not tl_tracks: diff --git a/mopidy/frontends/mpd/protocol/playback.py b/mopidy/frontends/mpd/protocol/playback.py index d166f982..5a4569e1 100644 --- a/mopidy/frontends/mpd/protocol/playback.py +++ b/mopidy/frontends/mpd/protocol/playback.py @@ -329,9 +329,9 @@ def seek(context, songpos, seconds): - issues ``seek 1 120`` without quotes around the arguments. """ - if context.core.playback.tracklist_position != songpos: + if context.core.playback.tracklist_position.get() != songpos: playpos(context, songpos) - context.core.playback.seek(int(seconds) * 1000) + context.core.playback.seek(int(seconds) * 1000).get() @handle_request(r'^seekid "(?P\d+)" "(?P\d+)"$') @@ -343,9 +343,10 @@ def seekid(context, tlid, seconds): Seeks to the position ``TIME`` (in seconds) of song ``SONGID``. """ - if context.core.playback.current_tlid != tlid: + tl_track = context.core.playback.current_tl_track.get() + if not tl_track or tl_track.tlid != tlid: playid(context, tlid) - context.core.playback.seek(int(seconds) * 1000) + context.core.playback.seek(int(seconds) * 1000).get() @handle_request(r'^setvol (?P[-+]*\d+)$') diff --git a/tests/frontends/mpd/protocol/playback_test.py b/tests/frontends/mpd/protocol/playback_test.py index f81be241..9bf467f5 100644 --- a/tests/frontends/mpd/protocol/playback_test.py +++ b/tests/frontends/mpd/protocol/playback_test.py @@ -424,7 +424,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): [Track(uri='dummy:a', length=40000), seek_track]) self.sendRequest('seekid "1" "30"') - self.assertEqual(1, self.core.playback.current_tlid.get()) + self.assertEqual(1, self.core.playback.current_tl_track.get().tlid) self.assertEqual(seek_track, self.core.playback.current_track.get()) self.assertInResponse('OK') From efe7247407ce25ebff0b180f889a422f275373ed Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 20 Nov 2012 11:51:59 +0100 Subject: [PATCH 17/39] core: Merge functionality of tracklist.append into tracklist.add --- mopidy/core/tracklist.py | 63 ++++++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/mopidy/core/tracklist.py b/mopidy/core/tracklist.py index 05d551fe..57c9de63 100644 --- a/mopidy/core/tracklist.py +++ b/mopidy/core/tracklist.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals import logging import random -from mopidy.models import TlTrack +from mopidy.models import TlTrack, Track from . import listener @@ -62,9 +62,17 @@ class TracklistController(object): Is not reset before Mopidy is restarted. """ - def add(self, track, at_position=None, increase_version=True): + def add(self, tracks, at_position=None): """ - Add the track to the end of, or at the given position in the tracklist. + Add the track or list of tracks to the tracklist. + + If ``at_position`` is given, the tracks placed at the given position in + the tracklist. If ``at_position`` is not given, the tracks are appended + to the end of the tracklist. + + If ``tracks`` is a track object, a single + :class:`mopidy.models.TlTrack` object is returned. If ``tracks`` is a + list, a list of :class:`mopidy.models.TlTrack` is returned. Triggers the :meth:`mopidy.core.CoreListener.tracklist_changed` event. @@ -72,40 +80,39 @@ class TracklistController(object): :type track: :class:`mopidy.models.Track` :param at_position: position in tracklist to add track :type at_position: int or :class:`None` - :param increase_version: if the tracklist version should be increased - :type increase_version: :class:`True` or :class:`False` - :rtype: :class:`mopidy.models.TlTrack` that was added to the tracklist + :rtype: a single or a list of :class:`mopidy.models.TlTrack` """ - assert at_position <= len(self._tl_tracks), \ + assert at_position is None or at_position <= len(self._tl_tracks), \ 'at_position can not be greater than tracklist length' - tl_track = TlTrack(self._next_tlid, track) - if at_position is not None: - self._tl_tracks.insert(at_position, tl_track) - else: - self._tl_tracks.append(tl_track) - if increase_version: - self._increase_version() - self._next_tlid += 1 - return tl_track - def append(self, tracks): - """ - Append the given tracks to the tracklist. + single_add = False + if isinstance(tracks, Track): + tracks = [tracks] + single_add = True - Triggers the :meth:`mopidy.core.CoreListener.tracklist_changed` event. - - :param tracks: tracks to append - :type tracks: list of :class:`mopidy.models.Track` - :rtype: list of :class:`mopidy.models.TlTrack` - """ tl_tracks = [] for track in tracks: - tl_tracks.append(self.add(track, increase_version=False)) + tl_track = TlTrack(self._next_tlid, track) + self._next_tlid += 1 + if at_position is not None: + self._tl_tracks.insert(at_position, tl_track) + at_position += 1 + else: + self._tl_tracks.append(tl_track) + tl_tracks.append(tl_track) - if tracks: + if tl_tracks: self._increase_version() - return tl_tracks + if single_add: + return tl_tracks[0] + else: + return tl_tracks + + append = add + """ + Alias for :meth:`add`. + """ def clear(self): """ From 70dbf81191ee4a1f8c9136f1d5f1ed64f048855e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 20 Nov 2012 11:53:08 +0100 Subject: [PATCH 18/39] mpd: Simplify 'addid' implementation using improved tracklist.add() --- mopidy/frontends/mpd/protocol/current_playlist.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/mopidy/frontends/mpd/protocol/current_playlist.py b/mopidy/frontends/mpd/protocol/current_playlist.py index 69e04d4b..81df2827 100644 --- a/mopidy/frontends/mpd/protocol/current_playlist.py +++ b/mopidy/frontends/mpd/protocol/current_playlist.py @@ -57,14 +57,8 @@ def addid(context, uri, songpos=None): raise MpdNoExistError('No such song', command='addid') if songpos and songpos > context.core.tracklist.length.get(): raise MpdArgError('Bad song index', command='addid') - first_tl_track = None - for track in tracks: - tl_track = context.core.tracklist.add(track, at_position=songpos).get() - if songpos is not None: - songpos += 1 - if first_tl_track is None: - first_tl_track = tl_track - return ('Id', first_tl_track.tlid) + tl_tracks = context.core.tracklist.add(tracks, at_position=songpos).get() + return ('Id', tl_tracks[0].tlid) @handle_request(r'^delete "(?P\d+):(?P\d+)*"$') From 1ed56c9ed71f52db267c97979194eaee5889cb43 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 20 Nov 2012 12:14:47 +0100 Subject: [PATCH 19/39] Use tracklist.add() instead of tracklist.append() --- .../mpd/protocol/current_playlist.py | 4 +- .../mpd/protocol/stored_playlists.py | 2 +- mopidy/frontends/mpris/objects.py | 4 +- tests/backends/base/playback.py | 12 +- tests/backends/base/tracklist.py | 42 +++--- tests/core/events_test.py | 13 +- tests/core/playback_test.py | 2 +- .../mpd/protocol/current_playlist_test.py | 75 +++++----- tests/frontends/mpd/protocol/playback_test.py | 60 +++----- .../frontends/mpd/protocol/regression_test.py | 8 +- tests/frontends/mpd/protocol/status_test.py | 2 +- .../mpd/protocol/stored_playlists_test.py | 2 +- tests/frontends/mpd/status_test.py | 14 +- .../frontends/mpris/player_interface_test.py | 139 +++++++----------- .../mpris/playlists_interface_test.py | 2 +- 15 files changed, 165 insertions(+), 216 deletions(-) diff --git a/mopidy/frontends/mpd/protocol/current_playlist.py b/mopidy/frontends/mpd/protocol/current_playlist.py index 81df2827..fbc92b46 100644 --- a/mopidy/frontends/mpd/protocol/current_playlist.py +++ b/mopidy/frontends/mpd/protocol/current_playlist.py @@ -24,7 +24,7 @@ def add(context, uri): return tracks = context.core.library.lookup(uri).get() if tracks: - context.core.tracklist.append(tracks) + context.core.tracklist.add(tracks) return raise MpdNoExistError('directory or file not found', command='add') @@ -371,7 +371,7 @@ def swap(context, songpos1, songpos2): del tracks[songpos2] tracks.insert(songpos2, song1) context.core.tracklist.clear() - context.core.tracklist.append(tracks) + context.core.tracklist.add(tracks) @handle_request(r'^swapid "(?P\d+)" "(?P\d+)"$') diff --git a/mopidy/frontends/mpd/protocol/stored_playlists.py b/mopidy/frontends/mpd/protocol/stored_playlists.py index d5d6b2a6..de2b267e 100644 --- a/mopidy/frontends/mpd/protocol/stored_playlists.py +++ b/mopidy/frontends/mpd/protocol/stored_playlists.py @@ -101,7 +101,7 @@ def load(context, name): playlists = context.core.playlists.filter(name=name).get() if not playlists: raise MpdNoExistError('No such playlist', command='load') - context.core.tracklist.append(playlists[0].tracks) + context.core.tracklist.add(playlists[0].tracks) @handle_request(r'^playlistadd "(?P[^"]+)" "(?P[^"]+)"$') diff --git a/mopidy/frontends/mpris/objects.py b/mopidy/frontends/mpris/objects.py index 93f14a2d..15ef9383 100644 --- a/mopidy/frontends/mpris/objects.py +++ b/mopidy/frontends/mpris/objects.py @@ -281,7 +281,7 @@ class MprisObject(dbus.service.Object): # is added to the backend. tracks = self.core.library.lookup(uri).get() if tracks: - tl_tracks = self.core.tracklist.append(tracks).get() + tl_tracks = self.core.tracklist.add(tracks).get() self.core.playback.play(tl_tracks[0]) else: logger.debug('Track with URI "%s" not found in library.', uri) @@ -449,7 +449,7 @@ class MprisObject(dbus.service.Object): playlist_uri = self.get_playlist_uri(playlist_id) playlist = self.core.playlists.lookup(playlist_uri).get() if playlist and playlist.tracks: - tl_tracks = self.core.tracklist.append(playlist.tracks).get() + tl_tracks = self.core.tracklist.add(playlist.tracks).get() self.core.playback.play(tl_tracks[0]) @dbus.service.method(dbus_interface=PLAYLISTS_IFACE) diff --git a/tests/backends/base/playback.py b/tests/backends/base/playback.py index fffe09da..09dffbab 100644 --- a/tests/backends/base/playback.py +++ b/tests/backends/base/playback.py @@ -301,7 +301,7 @@ class PlaybackControllerTest(object): random.seed(1) self.playback.random = True self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[2]) - self.tracklist.append(self.tracks[:1]) + self.tracklist.add(self.tracks[:1]) self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[1]) @populate_tracklist @@ -429,7 +429,7 @@ class PlaybackControllerTest(object): random.seed(1) self.playback.random = True self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[2]) - self.tracklist.append(self.tracks[:1]) + self.tracklist.add(self.tracks[:1]) self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[1]) @populate_tracklist @@ -521,7 +521,7 @@ class PlaybackControllerTest(object): wrapper.called = False self.playback.on_tracklist_change = wrapper - self.tracklist.append([Track()]) + self.tracklist.add([Track()]) self.assert_(wrapper.called) @@ -538,13 +538,13 @@ class PlaybackControllerTest(object): def test_on_tracklist_change_when_playing(self): self.playback.play() current_track = self.playback.current_track - self.tracklist.append([self.tracks[2]]) + self.tracklist.add([self.tracks[2]]) self.assertEqual(self.playback.state, PlaybackState.PLAYING) self.assertEqual(self.playback.current_track, current_track) @populate_tracklist def test_on_tracklist_change_when_stopped(self): - self.tracklist.append([self.tracks[2]]) + self.tracklist.add([self.tracks[2]]) self.assertEqual(self.playback.state, PlaybackState.STOPPED) self.assertEqual(self.playback.current_track, None) @@ -553,7 +553,7 @@ class PlaybackControllerTest(object): self.playback.play() self.playback.pause() current_track = self.playback.current_track - self.tracklist.append([self.tracks[2]]) + self.tracklist.add([self.tracks[2]]) self.assertEqual(self.playback.state, PlaybackState.PAUSED) self.assertEqual(self.playback.current_track, current_track) diff --git a/tests/backends/base/tracklist.py b/tests/backends/base/tracklist.py index 52ddfa46..53b3288a 100644 --- a/tests/backends/base/tracklist.py +++ b/tests/backends/base/tracklist.py @@ -30,7 +30,7 @@ class TracklistControllerTest(object): def test_length(self): self.assertEqual(0, len(self.controller.tl_tracks)) self.assertEqual(0, self.controller.length) - self.controller.append(self.tracks) + self.controller.add(self.tracks) self.assertEqual(3, len(self.controller.tl_tracks)) self.assertEqual(3, self.controller.length) @@ -72,12 +72,12 @@ class TracklistControllerTest(object): def test_filter_by_uri_returns_single_match(self): track = Track(uri='a') - self.controller.append([Track(uri='z'), track, Track(uri='y')]) + self.controller.add([Track(uri='z'), track, Track(uri='y')]) self.assertEqual(track, self.controller.filter(uri='a')[0].track) def test_filter_by_uri_returns_multiple_matches(self): track = Track(uri='a') - self.controller.append([Track(uri='z'), track, track]) + self.controller.add([Track(uri='z'), track, track]) tl_tracks = self.controller.filter(uri='a') self.assertEqual(track, tl_tracks[0].track) self.assertEqual(track, tl_tracks[1].track) @@ -91,7 +91,7 @@ class TracklistControllerTest(object): track1 = Track(uri='a', name='x') track2 = Track(uri='b', name='x') track3 = Track(uri='b', name='y') - self.controller.append([track1, track2, track3]) + self.controller.add([track1, track2, track3]) self.assertEqual( track1, self.controller.filter(uri='a', name='x')[0].track) self.assertEqual( @@ -103,7 +103,7 @@ class TracklistControllerTest(object): track1 = Track() track2 = Track(uri='b') track3 = Track() - self.controller.append([track1, track2, track3]) + self.controller.add([track1, track2, track3]) self.assertEqual(track2, self.controller.filter(uri='b')[0].track) @populate_tracklist @@ -122,42 +122,42 @@ class TracklistControllerTest(object): self.controller.clear() self.assertEqual(self.playback.state, PlaybackState.STOPPED) - def test_append_appends_to_the_tracklist(self): - self.controller.append([Track(uri='a'), Track(uri='b')]) + def test_add_appends_to_the_tracklist(self): + self.controller.add([Track(uri='a'), Track(uri='b')]) self.assertEqual(len(self.controller.tracks), 2) - self.controller.append([Track(uri='c'), Track(uri='d')]) + self.controller.add([Track(uri='c'), Track(uri='d')]) self.assertEqual(len(self.controller.tracks), 4) self.assertEqual(self.controller.tracks[0].uri, 'a') self.assertEqual(self.controller.tracks[1].uri, 'b') self.assertEqual(self.controller.tracks[2].uri, 'c') self.assertEqual(self.controller.tracks[3].uri, 'd') - def test_append_does_not_reset_version(self): + def test_add_does_not_reset_version(self): version = self.controller.version - self.controller.append([]) + self.controller.add([]) self.assertEqual(self.controller.version, version) @populate_tracklist - def test_append_preserves_playing_state(self): + def test_add_preserves_playing_state(self): self.playback.play() track = self.playback.current_track - self.controller.append(self.controller.tracks[1:2]) + self.controller.add(self.controller.tracks[1:2]) self.assertEqual(self.playback.state, PlaybackState.PLAYING) self.assertEqual(self.playback.current_track, track) @populate_tracklist - def test_append_preserves_stopped_state(self): - self.controller.append(self.controller.tracks[1:2]) + def test_add_preserves_stopped_state(self): + self.controller.add(self.controller.tracks[1:2]) self.assertEqual(self.playback.state, PlaybackState.STOPPED) self.assertEqual(self.playback.current_track, None) @populate_tracklist - def test_append_returns_the_tl_tracks_that_was_added(self): - tl_tracks = self.controller.append(self.controller.tracks[1:2]) + def test_add_returns_the_tl_tracks_that_was_added(self): + tl_tracks = self.controller.add(self.controller.tracks[1:2]) self.assertEqual(tl_tracks[0].track, self.controller.tracks[1]) def test_index_returns_index_of_track(self): - tl_tracks = self.controller.append(self.tracks) + tl_tracks = self.controller.add(self.tracks) self.assertEquals(0, self.controller.index(tl_tracks[0])) self.assertEquals(1, self.controller.index(tl_tracks[1])) self.assertEquals(2, self.controller.index(tl_tracks[2])) @@ -281,12 +281,12 @@ class TracklistControllerTest(object): self.assertEqual(0, len(self.controller.slice(7, 8))) self.assertEqual(0, len(self.controller.slice(-1, 1))) - def test_version_does_not_change_when_appending_nothing(self): + def test_version_does_not_change_when_adding_nothing(self): version = self.controller.version - self.controller.append([]) + self.controller.add([]) self.assertEquals(version, self.controller.version) - def test_version_increases_when_appending_something(self): + def test_version_increases_when_adding_something(self): version = self.controller.version - self.controller.append([Track()]) + self.controller.add([Track()]) self.assertLess(version, self.controller.version) diff --git a/tests/core/events_test.py b/tests/core/events_test.py index 8f969b0d..b0ae2081 100644 --- a/tests/core/events_test.py +++ b/tests/core/events_test.py @@ -65,32 +65,27 @@ class BackendEventsTest(unittest.TestCase): self.core.tracklist.add(Track(uri='dummy:a')).get() self.assertEqual(send.call_args[0][0], 'tracklist_changed') - def test_tracklist_append_sends_tracklist_changed_event(self, send): - send.reset_mock() - self.core.tracklist.append([Track(uri='dummy:a')]).get() - self.assertEqual(send.call_args[0][0], 'tracklist_changed') - def test_tracklist_clear_sends_tracklist_changed_event(self, send): - self.core.tracklist.append([Track(uri='dummy:a')]).get() + self.core.tracklist.add([Track(uri='dummy:a')]).get() send.reset_mock() self.core.tracklist.clear().get() self.assertEqual(send.call_args[0][0], 'tracklist_changed') def test_tracklist_move_sends_tracklist_changed_event(self, send): - self.core.tracklist.append( + self.core.tracklist.add( [Track(uri='dummy:a'), Track(uri='dummy:b')]).get() send.reset_mock() self.core.tracklist.move(0, 1, 1).get() self.assertEqual(send.call_args[0][0], 'tracklist_changed') def test_tracklist_remove_sends_tracklist_changed_event(self, send): - self.core.tracklist.append([Track(uri='dummy:a')]).get() + self.core.tracklist.add([Track(uri='dummy:a')]).get() send.reset_mock() self.core.tracklist.remove(uri='dummy:a').get() self.assertEqual(send.call_args[0][0], 'tracklist_changed') def test_tracklist_shuffle_sends_tracklist_changed_event(self, send): - self.core.tracklist.append( + self.core.tracklist.add( [Track(uri='dummy:a'), Track(uri='dummy:b')]).get() send.reset_mock() self.core.tracklist.shuffle().get() diff --git a/tests/core/playback_test.py b/tests/core/playback_test.py index bb3d359f..ffbca506 100644 --- a/tests/core/playback_test.py +++ b/tests/core/playback_test.py @@ -35,7 +35,7 @@ class CorePlaybackTest(unittest.TestCase): self.core = Core(audio=None, backends=[ self.backend1, self.backend2, self.backend3]) - self.core.tracklist.append(self.tracks) + self.core.tracklist.add(self.tracks) self.tl_tracks = self.core.tracklist.tl_tracks self.unplayable_tl_track = self.tl_tracks[2] diff --git a/tests/frontends/mpd/protocol/current_playlist_test.py b/tests/frontends/mpd/protocol/current_playlist_test.py index dd1ba57e..fc4640b1 100644 --- a/tests/frontends/mpd/protocol/current_playlist_test.py +++ b/tests/frontends/mpd/protocol/current_playlist_test.py @@ -10,7 +10,7 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase): needle = Track(uri='dummy://foo') self.backend.library.dummy_library = [ Track(), Track(), needle, Track()] - self.core.tracklist.append( + self.core.tracklist.add( [Track(), Track(), Track(), Track(), Track()]) self.assertEqual(len(self.core.tracklist.tracks.get()), 5) @@ -33,7 +33,7 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase): needle = Track(uri='dummy://foo') self.backend.library.dummy_library = [ Track(), Track(), needle, Track()] - self.core.tracklist.append( + self.core.tracklist.add( [Track(), Track(), Track(), Track(), Track()]) self.assertEqual(len(self.core.tracklist.tracks.get()), 5) @@ -52,7 +52,7 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase): needle = Track(uri='dummy://foo') self.backend.library.dummy_library = [ Track(), Track(), needle, Track()] - self.core.tracklist.append( + self.core.tracklist.add( [Track(), Track(), Track(), Track(), Track()]) self.assertEqual(len(self.core.tracklist.tracks.get()), 5) @@ -67,7 +67,7 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase): needle = Track(uri='dummy://foo') self.backend.library.dummy_library = [ Track(), Track(), needle, Track()] - self.core.tracklist.append( + self.core.tracklist.add( [Track(), Track(), Track(), Track(), Track()]) self.assertEqual(len(self.core.tracklist.tracks.get()), 5) @@ -79,7 +79,7 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase): self.assertEqualResponse('ACK [50@0] {addid} No such song') def test_clear(self): - self.core.tracklist.append( + self.core.tracklist.add( [Track(), Track(), Track(), Track(), Track()]) self.assertEqual(len(self.core.tracklist.tracks.get()), 5) @@ -89,7 +89,7 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_delete_songpos(self): - self.core.tracklist.append( + self.core.tracklist.add( [Track(), Track(), Track(), Track(), Track()]) self.assertEqual(len(self.core.tracklist.tracks.get()), 5) @@ -99,7 +99,7 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_delete_songpos_out_of_bounds(self): - self.core.tracklist.append( + self.core.tracklist.add( [Track(), Track(), Track(), Track(), Track()]) self.assertEqual(len(self.core.tracklist.tracks.get()), 5) @@ -108,7 +108,7 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase): self.assertEqualResponse('ACK [2@0] {delete} Bad song index') def test_delete_open_range(self): - self.core.tracklist.append( + self.core.tracklist.add( [Track(), Track(), Track(), Track(), Track()]) self.assertEqual(len(self.core.tracklist.tracks.get()), 5) @@ -117,7 +117,7 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_delete_closed_range(self): - self.core.tracklist.append( + self.core.tracklist.add( [Track(), Track(), Track(), Track(), Track()]) self.assertEqual(len(self.core.tracklist.tracks.get()), 5) @@ -126,7 +126,7 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_delete_range_out_of_bounds(self): - self.core.tracklist.append( + self.core.tracklist.add( [Track(), Track(), Track(), Track(), Track()]) self.assertEqual(len(self.core.tracklist.tracks.get()), 5) @@ -135,7 +135,7 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase): self.assertEqualResponse('ACK [2@0] {delete} Bad song index') def test_deleteid(self): - self.core.tracklist.append([Track(), Track()]) + self.core.tracklist.add([Track(), Track()]) self.assertEqual(len(self.core.tracklist.tracks.get()), 2) self.sendRequest('deleteid "1"') @@ -143,7 +143,7 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_deleteid_does_not_exist(self): - self.core.tracklist.append([Track(), Track()]) + self.core.tracklist.add([Track(), Track()]) self.assertEqual(len(self.core.tracklist.tracks.get()), 2) self.sendRequest('deleteid "12345"') @@ -151,7 +151,7 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase): self.assertEqualResponse('ACK [50@0] {deleteid} No such song') def test_move_songpos(self): - self.core.tracklist.append([ + self.core.tracklist.add([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) @@ -167,7 +167,7 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_move_open_range(self): - self.core.tracklist.append([ + self.core.tracklist.add([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) @@ -183,7 +183,7 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_move_closed_range(self): - self.core.tracklist.append([ + self.core.tracklist.add([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) @@ -199,7 +199,7 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_moveid(self): - self.core.tracklist.append([ + self.core.tracklist.add([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) @@ -237,8 +237,7 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase): self.assertEqualResponse('OK') def test_playlistfind_by_filename_in_tracklist(self): - self.core.tracklist.append([ - Track(uri='file:///exists')]) + self.core.tracklist.add([Track(uri='file:///exists')]) self.sendRequest('playlistfind filename "file:///exists"') self.assertInResponse('file: file:///exists') @@ -247,7 +246,7 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_playlistid_without_songid(self): - self.core.tracklist.append([Track(name='a'), Track(name='b')]) + self.core.tracklist.add([Track(name='a'), Track(name='b')]) self.sendRequest('playlistid') self.assertInResponse('Title: a') @@ -255,7 +254,7 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_playlistid_with_songid(self): - self.core.tracklist.append([Track(name='a'), Track(name='b')]) + self.core.tracklist.add([Track(name='a'), Track(name='b')]) self.sendRequest('playlistid "1"') self.assertNotInResponse('Title: a') @@ -265,13 +264,13 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_playlistid_with_not_existing_songid_fails(self): - self.core.tracklist.append([Track(name='a'), Track(name='b')]) + self.core.tracklist.add([Track(name='a'), Track(name='b')]) self.sendRequest('playlistid "25"') self.assertEqualResponse('ACK [50@0] {playlistid} No such song') def test_playlistinfo_without_songpos_or_range(self): - self.core.tracklist.append([ + self.core.tracklist.add([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) @@ -294,7 +293,7 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase): def test_playlistinfo_with_songpos(self): # Make the track's CPID not match the playlist position self.core.tracklist.tlid = 17 - self.core.tracklist.append([ + self.core.tracklist.add([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) @@ -320,7 +319,7 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase): self.assertEqual(response1, response2) def test_playlistinfo_with_open_range(self): - self.core.tracklist.append([ + self.core.tracklist.add([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) @@ -341,7 +340,7 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_playlistinfo_with_closed_range(self): - self.core.tracklist.append([ + self.core.tracklist.add([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) @@ -372,7 +371,7 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase): self.assertEqualResponse('ACK [0@0] {} Not implemented') def test_plchanges_with_lower_version_returns_changes(self): - self.core.tracklist.append( + self.core.tracklist.add( [Track(name='a'), Track(name='b'), Track(name='c')]) self.sendRequest('plchanges "0"') @@ -382,7 +381,7 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_plchanges_with_equal_version_returns_nothing(self): - self.core.tracklist.append( + self.core.tracklist.add( [Track(name='a'), Track(name='b'), Track(name='c')]) self.assertEqual(self.core.tracklist.version.get(), 1) @@ -393,7 +392,7 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_plchanges_with_greater_version_returns_nothing(self): - self.core.tracklist.append( + self.core.tracklist.add( [Track(name='a'), Track(name='b'), Track(name='c')]) self.assertEqual(self.core.tracklist.version.get(), 1) @@ -404,7 +403,7 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_plchanges_with_minus_one_returns_entire_playlist(self): - self.core.tracklist.append( + self.core.tracklist.add( [Track(name='a'), Track(name='b'), Track(name='c')]) self.sendRequest('plchanges "-1"') @@ -414,7 +413,7 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_plchanges_without_quotes_works(self): - self.core.tracklist.append( + self.core.tracklist.add( [Track(name='a'), Track(name='b'), Track(name='c')]) self.sendRequest('plchanges 0') @@ -424,7 +423,7 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_plchangesposid(self): - self.core.tracklist.append([Track(), Track(), Track()]) + self.core.tracklist.add([Track(), Track(), Track()]) self.sendRequest('plchangesposid "0"') tl_tracks = self.core.tracklist.tl_tracks.get() @@ -437,7 +436,7 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_shuffle_without_range(self): - self.core.tracklist.append([ + self.core.tracklist.add([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) @@ -448,7 +447,7 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_shuffle_with_open_range(self): - self.core.tracklist.append([ + self.core.tracklist.add([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) @@ -464,7 +463,7 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_shuffle_with_closed_range(self): - self.core.tracklist.append([ + self.core.tracklist.add([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) @@ -480,7 +479,7 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_swap(self): - self.core.tracklist.append([ + self.core.tracklist.add([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) @@ -496,7 +495,7 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_swapid(self): - self.core.tracklist.append([ + self.core.tracklist.add([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) @@ -512,13 +511,13 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_swapid_with_first_id_unknown_should_ack(self): - self.core.tracklist.append([Track()]) + self.core.tracklist.add([Track()]) self.sendRequest('swapid "0" "4"') self.assertEqualResponse( 'ACK [50@0] {swapid} No such song') def test_swapid_with_second_id_unknown_should_ack(self): - self.core.tracklist.append([Track()]) + self.core.tracklist.add([Track()]) self.sendRequest('swapid "4" "0"') self.assertEqualResponse( 'ACK [50@0] {swapid} No such song') diff --git a/tests/frontends/mpd/protocol/playback_test.py b/tests/frontends/mpd/protocol/playback_test.py index 9bf467f5..14168a35 100644 --- a/tests/frontends/mpd/protocol/playback_test.py +++ b/tests/frontends/mpd/protocol/playback_test.py @@ -168,7 +168,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_pause_off(self): - self.core.tracklist.append([Track(uri='dummy:a')]) + self.core.tracklist.add([Track(uri='dummy:a')]) self.sendRequest('play "0"') self.sendRequest('pause "1"') @@ -177,7 +177,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_pause_on(self): - self.core.tracklist.append([Track(uri='dummy:a')]) + self.core.tracklist.add([Track(uri='dummy:a')]) self.sendRequest('play "0"') self.sendRequest('pause "1"') @@ -185,7 +185,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_pause_toggle(self): - self.core.tracklist.append([Track(uri='dummy:a')]) + self.core.tracklist.add([Track(uri='dummy:a')]) self.sendRequest('play "0"') self.assertEqual(PLAYING, self.core.playback.state.get()) @@ -200,28 +200,28 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_play_without_pos(self): - self.core.tracklist.append([Track(uri='dummy:a')]) + self.core.tracklist.add([Track(uri='dummy:a')]) self.sendRequest('play') self.assertEqual(PLAYING, self.core.playback.state.get()) self.assertInResponse('OK') def test_play_with_pos(self): - self.core.tracklist.append([Track(uri='dummy:a')]) + self.core.tracklist.add([Track(uri='dummy:a')]) self.sendRequest('play "0"') self.assertEqual(PLAYING, self.core.playback.state.get()) self.assertInResponse('OK') def test_play_with_pos_without_quotes(self): - self.core.tracklist.append([Track(uri='dummy:a')]) + self.core.tracklist.add([Track(uri='dummy:a')]) self.sendRequest('play 0') self.assertEqual(PLAYING, self.core.playback.state.get()) self.assertInResponse('OK') def test_play_with_pos_out_of_bounds(self): - self.core.tracklist.append([]) + self.core.tracklist.add([]) self.sendRequest('play "0"') self.assertEqual(STOPPED, self.core.playback.state.get()) @@ -229,10 +229,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): def test_play_minus_one_plays_first_in_playlist_if_no_current_track(self): self.assertEqual(self.core.playback.current_track.get(), None) - self.core.tracklist.append([ - Track(uri='dummy:a'), - Track(uri='dummy:b'), - ]) + self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.sendRequest('play "-1"') self.assertEqual(PLAYING, self.core.playback.state.get()) @@ -241,10 +238,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_play_minus_one_plays_current_track_if_current_track_is_set(self): - self.core.tracklist.append([ - Track(uri='dummy:a'), - Track(uri='dummy:b'), - ]) + self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.assertEqual(self.core.playback.current_track.get(), None) self.core.playback.play() self.core.playback.next() @@ -266,8 +260,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_play_minus_is_ignored_if_playing(self): - self.core.tracklist.append([ - Track(uri='dummy:a', length=40000)]) + self.core.tracklist.add([Track(uri='dummy:a', length=40000)]) self.core.playback.seek(30000) self.assertGreaterEqual( self.core.playback.time_position.get(), 30000) @@ -280,8 +273,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_play_minus_one_resumes_if_paused(self): - self.core.tracklist.append([ - Track(uri='dummy:a', length=40000)]) + self.core.tracklist.add([Track(uri='dummy:a', length=40000)]) self.core.playback.seek(30000) self.assertGreaterEqual( self.core.playback.time_position.get(), 30000) @@ -296,14 +288,14 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_playid(self): - self.core.tracklist.append([Track(uri='dummy:a')]) + self.core.tracklist.add([Track(uri='dummy:a')]) self.sendRequest('playid "0"') self.assertEqual(PLAYING, self.core.playback.state.get()) self.assertInResponse('OK') def test_playid_without_quotes(self): - self.core.tracklist.append([Track(uri='dummy:a')]) + self.core.tracklist.add([Track(uri='dummy:a')]) self.sendRequest('playid 0') self.assertEqual(PLAYING, self.core.playback.state.get()) @@ -311,10 +303,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): def test_playid_minus_1_plays_first_in_playlist_if_no_current_track(self): self.assertEqual(self.core.playback.current_track.get(), None) - self.core.tracklist.append([ - Track(uri='dummy:a'), - Track(uri='dummy:b'), - ]) + self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.sendRequest('playid "-1"') self.assertEqual(PLAYING, self.core.playback.state.get()) @@ -323,10 +312,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_playid_minus_1_plays_current_track_if_current_track_is_set(self): - self.core.tracklist.append([ - Track(uri='dummy:a'), - Track(uri='dummy:b'), - ]) + self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.assertEqual(self.core.playback.current_track.get(), None) self.core.playback.play() self.core.playback.next() @@ -348,7 +334,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_playid_minus_is_ignored_if_playing(self): - self.core.tracklist.append([Track(uri='dummy:a', length=40000)]) + self.core.tracklist.add([Track(uri='dummy:a', length=40000)]) self.core.playback.seek(30000) self.assertGreaterEqual( self.core.playback.time_position.get(), 30000) @@ -361,7 +347,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_playid_minus_one_resumes_if_paused(self): - self.core.tracklist.append([Track(uri='dummy:a', length=40000)]) + self.core.tracklist.add([Track(uri='dummy:a', length=40000)]) self.core.playback.seek(30000) self.assertGreaterEqual( self.core.playback.time_position.get(), 30000) @@ -376,7 +362,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_playid_which_does_not_exist(self): - self.core.tracklist.append([Track(uri='dummy:a')]) + self.core.tracklist.add([Track(uri='dummy:a')]) self.sendRequest('playid "12345"') self.assertInResponse('ACK [50@0] {playid} No such song') @@ -386,7 +372,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_seek(self): - self.core.tracklist.append([Track(uri='dummy:a', length=40000)]) + self.core.tracklist.add([Track(uri='dummy:a', length=40000)]) self.sendRequest('seek "0"') self.sendRequest('seek "0" "30"') @@ -395,7 +381,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): def test_seek_with_songpos(self): seek_track = Track(uri='dummy:b', length=40000) - self.core.tracklist.append( + self.core.tracklist.add( [Track(uri='dummy:a', length=40000), seek_track]) self.sendRequest('seek "1" "30"') @@ -403,7 +389,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_seek_without_quotes(self): - self.core.tracklist.append([Track(uri='dummy:a', length=40000)]) + self.core.tracklist.add([Track(uri='dummy:a', length=40000)]) self.sendRequest('seek 0') self.sendRequest('seek 0 30') @@ -412,7 +398,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_seekid(self): - self.core.tracklist.append([Track(uri='dummy:a', length=40000)]) + self.core.tracklist.add([Track(uri='dummy:a', length=40000)]) self.sendRequest('seekid "0" "30"') self.assertGreaterEqual( self.core.playback.time_position.get(), 30000) @@ -420,7 +406,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): def test_seekid_with_tlid(self): seek_track = Track(uri='dummy:b', length=40000) - self.core.tracklist.append( + self.core.tracklist.add( [Track(uri='dummy:a', length=40000), seek_track]) self.sendRequest('seekid "1" "30"') diff --git a/tests/frontends/mpd/protocol/regression_test.py b/tests/frontends/mpd/protocol/regression_test.py index 6b8832e4..0bc488fd 100644 --- a/tests/frontends/mpd/protocol/regression_test.py +++ b/tests/frontends/mpd/protocol/regression_test.py @@ -18,7 +18,7 @@ class IssueGH17RegressionTest(protocol.BaseTestCase): - Press next until you get to the unplayable track """ def test(self): - self.core.tracklist.append([ + self.core.tracklist.add([ Track(uri='dummy:a'), Track(uri='dummy:b'), Track(uri='dummy:error'), @@ -59,7 +59,7 @@ class IssueGH18RegressionTest(protocol.BaseTestCase): """ def test(self): - self.core.tracklist.append([ + self.core.tracklist.add([ Track(uri='dummy:a'), Track(uri='dummy:b'), Track(uri='dummy:c'), Track(uri='dummy:d'), Track(uri='dummy:e'), Track(uri='dummy:f')]) random.seed(1) @@ -95,7 +95,7 @@ class IssueGH22RegressionTest(protocol.BaseTestCase): """ def test(self): - self.core.tracklist.append([ + self.core.tracklist.add([ Track(uri='dummy:a'), Track(uri='dummy:b'), Track(uri='dummy:c'), Track(uri='dummy:d'), Track(uri='dummy:e'), Track(uri='dummy:f')]) random.seed(1) @@ -124,7 +124,7 @@ class IssueGH69RegressionTest(protocol.BaseTestCase): def test(self): self.core.playlists.create('foo') - self.core.tracklist.append([ + self.core.tracklist.add([ Track(uri='dummy:a'), Track(uri='dummy:b'), Track(uri='dummy:c'), Track(uri='dummy:d'), Track(uri='dummy:e'), Track(uri='dummy:f')]) diff --git a/tests/frontends/mpd/protocol/status_test.py b/tests/frontends/mpd/protocol/status_test.py index ef3cf7b2..24f24ab2 100644 --- a/tests/frontends/mpd/protocol/status_test.py +++ b/tests/frontends/mpd/protocol/status_test.py @@ -12,7 +12,7 @@ class StatusHandlerTest(protocol.BaseTestCase): def test_currentsong(self): track = Track() - self.core.tracklist.append([track]) + self.core.tracklist.add([track]) self.core.playback.play() self.sendRequest('currentsong') self.assertInResponse('file: ') diff --git a/tests/frontends/mpd/protocol/stored_playlists_test.py b/tests/frontends/mpd/protocol/stored_playlists_test.py index 6bac95e5..cf1b8cd0 100644 --- a/tests/frontends/mpd/protocol/stored_playlists_test.py +++ b/tests/frontends/mpd/protocol/stored_playlists_test.py @@ -65,7 +65,7 @@ class PlaylistsHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_load_known_playlist_appends_to_tracklist(self): - self.core.tracklist.append([Track(uri='a'), Track(uri='b')]) + self.core.tracklist.add([Track(uri='a'), Track(uri='b')]) self.assertEqual(len(self.core.tracklist.tracks.get()), 2) self.backend.playlists.playlists = [ Playlist(name='A-list', tracks=[ diff --git a/tests/frontends/mpd/status_test.py b/tests/frontends/mpd/status_test.py index 6afa5541..d508cbf0 100644 --- a/tests/frontends/mpd/status_test.py +++ b/tests/frontends/mpd/status_test.py @@ -131,21 +131,21 @@ class StatusHandlerTest(unittest.TestCase): self.assertEqual(result['state'], 'pause') def test_status_method_when_playlist_loaded_contains_song(self): - self.core.tracklist.append([Track(uri='dummy:a')]) + self.core.tracklist.add([Track(uri='dummy:a')]) self.core.playback.play() result = dict(status.status(self.context)) self.assertIn('song', result) self.assertGreaterEqual(int(result['song']), 0) def test_status_method_when_playlist_loaded_contains_tlid_as_songid(self): - self.core.tracklist.append([Track(uri='dummy:a')]) + self.core.tracklist.add([Track(uri='dummy:a')]) self.core.playback.play() result = dict(status.status(self.context)) self.assertIn('songid', result) self.assertEqual(int(result['songid']), 0) def test_status_method_when_playing_contains_time_with_no_length(self): - self.core.tracklist.append([Track(uri='dummy:a', length=None)]) + self.core.tracklist.add([Track(uri='dummy:a', length=None)]) self.core.playback.play() result = dict(status.status(self.context)) self.assertIn('time', result) @@ -155,7 +155,7 @@ class StatusHandlerTest(unittest.TestCase): self.assertLessEqual(position, total) def test_status_method_when_playing_contains_time_with_length(self): - self.core.tracklist.append([Track(uri='dummy:a', length=10000)]) + self.core.tracklist.add([Track(uri='dummy:a', length=10000)]) self.core.playback.play() result = dict(status.status(self.context)) self.assertIn('time', result) @@ -165,7 +165,7 @@ class StatusHandlerTest(unittest.TestCase): self.assertLessEqual(position, total) def test_status_method_when_playing_contains_elapsed(self): - self.core.tracklist.append([Track(uri='dummy:a', length=60000)]) + self.core.tracklist.add([Track(uri='dummy:a', length=60000)]) self.core.playback.play() self.core.playback.pause() self.core.playback.seek(59123) @@ -174,7 +174,7 @@ class StatusHandlerTest(unittest.TestCase): self.assertEqual(result['elapsed'], '59.123') def test_status_method_when_starting_playing_contains_elapsed_zero(self): - self.core.tracklist.append([Track(uri='dummy:a', length=10000)]) + self.core.tracklist.add([Track(uri='dummy:a', length=10000)]) self.core.playback.play() self.core.playback.pause() result = dict(status.status(self.context)) @@ -182,7 +182,7 @@ class StatusHandlerTest(unittest.TestCase): self.assertEqual(result['elapsed'], '0.000') def test_status_method_when_playing_contains_bitrate(self): - self.core.tracklist.append([Track(uri='dummy:a', bitrate=320)]) + self.core.tracklist.add([Track(uri='dummy:a', bitrate=320)]) self.core.playback.play() result = dict(status.status(self.context)) self.assertIn('bitrate', result) diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index 39b77093..c48ffa98 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -101,16 +101,14 @@ class PlayerInterfaceTest(unittest.TestCase): def test_set_rate_is_ignored_if_can_control_is_false(self): self.mpris.get_CanControl = lambda *_: False - self.core.tracklist.append([ - Track(uri='dummy:a'), Track(uri='dummy:b')]) + self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.assertEqual(self.core.playback.state.get(), PLAYING) self.mpris.Set(objects.PLAYER_IFACE, 'Rate', 0) self.assertEqual(self.core.playback.state.get(), PLAYING) def test_set_rate_to_zero_pauses_playback(self): - self.core.tracklist.append([ - Track(uri='dummy:a'), Track(uri='dummy:b')]) + self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.assertEqual(self.core.playback.state.get(), PLAYING) self.mpris.Set(objects.PLAYER_IFACE, 'Rate', 0) @@ -150,7 +148,7 @@ class PlayerInterfaceTest(unittest.TestCase): self.assertEqual(result['mpris:trackid'], '') def test_get_metadata_has_trackid_based_on_tlid(self): - self.core.tracklist.append([Track(uri='dummy:a')]) + self.core.tracklist.add([Track(uri='dummy:a')]) self.core.playback.play() (tlid, track) = self.core.playback.current_tl_track.get() result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata') @@ -159,28 +157,28 @@ class PlayerInterfaceTest(unittest.TestCase): result['mpris:trackid'], '/com/mopidy/track/%d' % tlid) def test_get_metadata_has_track_length(self): - self.core.tracklist.append([Track(uri='dummy:a', length=40000)]) + self.core.tracklist.add([Track(uri='dummy:a', length=40000)]) self.core.playback.play() result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata') self.assertIn('mpris:length', result.keys()) self.assertEqual(result['mpris:length'], 40000000) def test_get_metadata_has_track_uri(self): - self.core.tracklist.append([Track(uri='dummy:a')]) + self.core.tracklist.add([Track(uri='dummy:a')]) self.core.playback.play() result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata') self.assertIn('xesam:url', result.keys()) self.assertEqual(result['xesam:url'], 'dummy:a') def test_get_metadata_has_track_title(self): - self.core.tracklist.append([Track(name='a')]) + self.core.tracklist.add([Track(name='a')]) self.core.playback.play() result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata') self.assertIn('xesam:title', result.keys()) self.assertEqual(result['xesam:title'], 'a') def test_get_metadata_has_track_artists(self): - self.core.tracklist.append([Track(artists=[ + self.core.tracklist.add([Track(artists=[ Artist(name='a'), Artist(name='b'), Artist(name=None)])]) self.core.playback.play() result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata') @@ -188,14 +186,14 @@ class PlayerInterfaceTest(unittest.TestCase): self.assertEqual(result['xesam:artist'], ['a', 'b']) def test_get_metadata_has_track_album(self): - self.core.tracklist.append([Track(album=Album(name='a'))]) + self.core.tracklist.add([Track(album=Album(name='a'))]) self.core.playback.play() result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata') self.assertIn('xesam:album', result.keys()) self.assertEqual(result['xesam:album'], 'a') def test_get_metadata_has_track_album_artists(self): - self.core.tracklist.append([Track(album=Album(artists=[ + self.core.tracklist.add([Track(album=Album(artists=[ Artist(name='a'), Artist(name='b'), Artist(name=None)]))]) self.core.playback.play() result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata') @@ -203,7 +201,7 @@ class PlayerInterfaceTest(unittest.TestCase): self.assertEqual(result['xesam:albumArtist'], ['a', 'b']) def test_get_metadata_has_track_number_in_album(self): - self.core.tracklist.append([Track(track_no=7)]) + self.core.tracklist.add([Track(track_no=7)]) self.core.playback.play() result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata') self.assertIn('xesam:trackNumber', result.keys()) @@ -246,7 +244,7 @@ class PlayerInterfaceTest(unittest.TestCase): self.assertEqual(self.core.playback.volume.get(), 10) def test_get_position_returns_time_position_in_microseconds(self): - self.core.tracklist.append([Track(uri='dummy:a', length=40000)]) + self.core.tracklist.add([Track(uri='dummy:a', length=40000)]) self.core.playback.play() self.core.playback.seek(10000) result_in_microseconds = self.mpris.Get( @@ -270,15 +268,14 @@ class PlayerInterfaceTest(unittest.TestCase): def test_can_go_next_is_true_if_can_control_and_other_next_track(self): self.mpris.get_CanControl = lambda *_: True - self.core.tracklist.append([ - Track(uri='dummy:a'), Track(uri='dummy:b')]) + self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() result = self.mpris.Get(objects.PLAYER_IFACE, 'CanGoNext') self.assertTrue(result) def test_can_go_next_is_false_if_next_track_is_the_same(self): self.mpris.get_CanControl = lambda *_: True - self.core.tracklist.append([Track(uri='dummy:a')]) + self.core.tracklist.add([Track(uri='dummy:a')]) self.core.playback.repeat = True self.core.playback.play() result = self.mpris.Get(objects.PLAYER_IFACE, 'CanGoNext') @@ -286,16 +283,14 @@ class PlayerInterfaceTest(unittest.TestCase): def test_can_go_next_is_false_if_can_control_is_false(self): self.mpris.get_CanControl = lambda *_: False - self.core.tracklist.append([ - Track(uri='dummy:a'), Track(uri='dummy:b')]) + self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() result = self.mpris.Get(objects.PLAYER_IFACE, 'CanGoNext') self.assertFalse(result) def test_can_go_previous_is_true_if_can_control_and_previous_track(self): self.mpris.get_CanControl = lambda *_: True - self.core.tracklist.append([ - Track(uri='dummy:a'), Track(uri='dummy:b')]) + self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.core.playback.next() result = self.mpris.Get(objects.PLAYER_IFACE, 'CanGoPrevious') @@ -303,7 +298,7 @@ class PlayerInterfaceTest(unittest.TestCase): def test_can_go_previous_is_false_if_previous_track_is_the_same(self): self.mpris.get_CanControl = lambda *_: True - self.core.tracklist.append([Track(uri='dummy:a')]) + self.core.tracklist.add([Track(uri='dummy:a')]) self.core.playback.repeat = True self.core.playback.play() result = self.mpris.Get(objects.PLAYER_IFACE, 'CanGoPrevious') @@ -311,8 +306,7 @@ class PlayerInterfaceTest(unittest.TestCase): def test_can_go_previous_is_false_if_can_control_is_false(self): self.mpris.get_CanControl = lambda *_: False - self.core.tracklist.append([ - Track(uri='dummy:a'), Track(uri='dummy:b')]) + self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.core.playback.next() result = self.mpris.Get(objects.PLAYER_IFACE, 'CanGoPrevious') @@ -320,7 +314,7 @@ class PlayerInterfaceTest(unittest.TestCase): def test_can_play_is_true_if_can_control_and_current_track(self): self.mpris.get_CanControl = lambda *_: True - self.core.tracklist.append([Track(uri='dummy:a')]) + self.core.tracklist.add([Track(uri='dummy:a')]) self.core.playback.play() self.assertTrue(self.core.playback.current_track.get()) result = self.mpris.Get(objects.PLAYER_IFACE, 'CanPlay') @@ -363,16 +357,14 @@ class PlayerInterfaceTest(unittest.TestCase): def test_next_is_ignored_if_can_go_next_is_false(self): self.mpris.get_CanGoNext = lambda *_: False - self.core.tracklist.append([ - Track(uri='dummy:a'), Track(uri='dummy:b')]) + self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a') self.mpris.Next() self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a') def test_next_when_playing_skips_to_next_track_and_keep_playing(self): - self.core.tracklist.append([ - Track(uri='dummy:a'), Track(uri='dummy:b')]) + self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a') self.assertEqual(self.core.playback.state.get(), PLAYING) @@ -381,8 +373,7 @@ class PlayerInterfaceTest(unittest.TestCase): self.assertEqual(self.core.playback.state.get(), PLAYING) def test_next_when_at_end_of_list_should_stop_playback(self): - self.core.tracklist.append([ - Track(uri='dummy:a'), Track(uri='dummy:b')]) + self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.core.playback.next() self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:b') @@ -391,8 +382,7 @@ class PlayerInterfaceTest(unittest.TestCase): self.assertEqual(self.core.playback.state.get(), STOPPED) def test_next_when_paused_should_skip_to_next_track_and_stay_paused(self): - self.core.tracklist.append([ - Track(uri='dummy:a'), Track(uri='dummy:b')]) + self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.core.playback.pause() self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a') @@ -402,8 +392,7 @@ class PlayerInterfaceTest(unittest.TestCase): self.assertEqual(self.core.playback.state.get(), PAUSED) def test_next_when_stopped_skips_to_next_track_and_stay_stopped(self): - self.core.tracklist.append([ - Track(uri='dummy:a'), Track(uri='dummy:b')]) + self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.core.playback.stop() self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a') @@ -414,8 +403,7 @@ class PlayerInterfaceTest(unittest.TestCase): def test_previous_is_ignored_if_can_go_previous_is_false(self): self.mpris.get_CanGoPrevious = lambda *_: False - self.core.tracklist.append([ - Track(uri='dummy:a'), Track(uri='dummy:b')]) + self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.core.playback.next() self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:b') @@ -423,8 +411,7 @@ class PlayerInterfaceTest(unittest.TestCase): self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:b') def test_previous_when_playing_skips_to_prev_track_and_keep_playing(self): - self.core.tracklist.append([ - Track(uri='dummy:a'), Track(uri='dummy:b')]) + self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.core.playback.next() self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:b') @@ -434,8 +421,7 @@ class PlayerInterfaceTest(unittest.TestCase): self.assertEqual(self.core.playback.state.get(), PLAYING) def test_previous_when_at_start_of_list_should_stop_playback(self): - self.core.tracklist.append([ - Track(uri='dummy:a'), Track(uri='dummy:b')]) + self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a') self.assertEqual(self.core.playback.state.get(), PLAYING) @@ -443,8 +429,7 @@ class PlayerInterfaceTest(unittest.TestCase): self.assertEqual(self.core.playback.state.get(), STOPPED) def test_previous_when_paused_skips_to_previous_track_and_pause(self): - self.core.tracklist.append([ - Track(uri='dummy:a'), Track(uri='dummy:b')]) + self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.core.playback.next() self.core.playback.pause() @@ -455,8 +440,7 @@ class PlayerInterfaceTest(unittest.TestCase): self.assertEqual(self.core.playback.state.get(), PAUSED) def test_previous_when_stopped_skips_to_previous_track_and_stops(self): - self.core.tracklist.append([ - Track(uri='dummy:a'), Track(uri='dummy:b')]) + self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.core.playback.next() self.core.playback.stop() @@ -468,24 +452,21 @@ class PlayerInterfaceTest(unittest.TestCase): def test_pause_is_ignored_if_can_pause_is_false(self): self.mpris.get_CanPause = lambda *_: False - self.core.tracklist.append([ - Track(uri='dummy:a'), Track(uri='dummy:b')]) + self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.assertEqual(self.core.playback.state.get(), PLAYING) self.mpris.Pause() self.assertEqual(self.core.playback.state.get(), PLAYING) def test_pause_when_playing_should_pause_playback(self): - self.core.tracklist.append([ - Track(uri='dummy:a'), Track(uri='dummy:b')]) + self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.assertEqual(self.core.playback.state.get(), PLAYING) self.mpris.Pause() self.assertEqual(self.core.playback.state.get(), PAUSED) def test_pause_when_paused_has_no_effect(self): - self.core.tracklist.append([ - Track(uri='dummy:a'), Track(uri='dummy:b')]) + self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.core.playback.pause() self.assertEqual(self.core.playback.state.get(), PAUSED) @@ -494,24 +475,21 @@ class PlayerInterfaceTest(unittest.TestCase): def test_playpause_is_ignored_if_can_pause_is_false(self): self.mpris.get_CanPause = lambda *_: False - self.core.tracklist.append([ - Track(uri='dummy:a'), Track(uri='dummy:b')]) + self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.assertEqual(self.core.playback.state.get(), PLAYING) self.mpris.PlayPause() self.assertEqual(self.core.playback.state.get(), PLAYING) def test_playpause_when_playing_should_pause_playback(self): - self.core.tracklist.append([ - Track(uri='dummy:a'), Track(uri='dummy:b')]) + self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.assertEqual(self.core.playback.state.get(), PLAYING) self.mpris.PlayPause() self.assertEqual(self.core.playback.state.get(), PAUSED) def test_playpause_when_paused_should_resume_playback(self): - self.core.tracklist.append([ - Track(uri='dummy:a'), Track(uri='dummy:b')]) + self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.core.playback.pause() @@ -526,32 +504,28 @@ class PlayerInterfaceTest(unittest.TestCase): self.assertGreaterEqual(after_pause, at_pause) def test_playpause_when_stopped_should_start_playback(self): - self.core.tracklist.append([ - Track(uri='dummy:a'), Track(uri='dummy:b')]) + self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.assertEqual(self.core.playback.state.get(), STOPPED) self.mpris.PlayPause() self.assertEqual(self.core.playback.state.get(), PLAYING) def test_stop_is_ignored_if_can_control_is_false(self): self.mpris.get_CanControl = lambda *_: False - self.core.tracklist.append([ - Track(uri='dummy:a'), Track(uri='dummy:b')]) + self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.assertEqual(self.core.playback.state.get(), PLAYING) self.mpris.Stop() self.assertEqual(self.core.playback.state.get(), PLAYING) def test_stop_when_playing_should_stop_playback(self): - self.core.tracklist.append([ - Track(uri='dummy:a'), Track(uri='dummy:b')]) + self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.assertEqual(self.core.playback.state.get(), PLAYING) self.mpris.Stop() self.assertEqual(self.core.playback.state.get(), STOPPED) def test_stop_when_paused_should_stop_playback(self): - self.core.tracklist.append([ - Track(uri='dummy:a'), Track(uri='dummy:b')]) + self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.core.playback.pause() self.assertEqual(self.core.playback.state.get(), PAUSED) @@ -560,21 +534,19 @@ class PlayerInterfaceTest(unittest.TestCase): def test_play_is_ignored_if_can_play_is_false(self): self.mpris.get_CanPlay = lambda *_: False - self.core.tracklist.append([ - Track(uri='dummy:a'), Track(uri='dummy:b')]) + self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.assertEqual(self.core.playback.state.get(), STOPPED) self.mpris.Play() self.assertEqual(self.core.playback.state.get(), STOPPED) def test_play_when_stopped_starts_playback(self): - self.core.tracklist.append([ - Track(uri='dummy:a'), Track(uri='dummy:b')]) + self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.assertEqual(self.core.playback.state.get(), STOPPED) self.mpris.Play() self.assertEqual(self.core.playback.state.get(), PLAYING) def test_play_after_pause_resumes_from_same_position(self): - self.core.tracklist.append([Track(uri='dummy:a', length=40000)]) + self.core.tracklist.add([Track(uri='dummy:a', length=40000)]) self.core.playback.play() before_pause = self.core.playback.time_position.get() @@ -598,7 +570,7 @@ class PlayerInterfaceTest(unittest.TestCase): def test_seek_is_ignored_if_can_seek_is_false(self): self.mpris.get_CanSeek = lambda *_: False - self.core.tracklist.append([Track(uri='dummy:a', length=40000)]) + self.core.tracklist.add([Track(uri='dummy:a', length=40000)]) self.core.playback.play() before_seek = self.core.playback.time_position.get() @@ -614,7 +586,7 @@ class PlayerInterfaceTest(unittest.TestCase): self.assertLess(after_seek, before_seek + milliseconds_to_seek) def test_seek_seeks_given_microseconds_forward_in_the_current_track(self): - self.core.tracklist.append([Track(uri='dummy:a', length=40000)]) + self.core.tracklist.add([Track(uri='dummy:a', length=40000)]) self.core.playback.play() before_seek = self.core.playback.time_position.get() @@ -631,7 +603,7 @@ class PlayerInterfaceTest(unittest.TestCase): self.assertGreaterEqual(after_seek, before_seek + milliseconds_to_seek) def test_seek_seeks_given_microseconds_backward_if_negative(self): - self.core.tracklist.append([Track(uri='dummy:a', length=40000)]) + self.core.tracklist.add([Track(uri='dummy:a', length=40000)]) self.core.playback.play() self.core.playback.seek(20000) @@ -650,7 +622,7 @@ class PlayerInterfaceTest(unittest.TestCase): self.assertLess(after_seek, before_seek) def test_seek_seeks_to_start_of_track_if_new_position_is_negative(self): - self.core.tracklist.append([Track(uri='dummy:a', length=40000)]) + self.core.tracklist.add([Track(uri='dummy:a', length=40000)]) self.core.playback.play() self.core.playback.seek(20000) @@ -670,7 +642,7 @@ class PlayerInterfaceTest(unittest.TestCase): self.assertGreaterEqual(after_seek, 0) def test_seek_skips_to_next_track_if_new_position_gt_track_length(self): - self.core.tracklist.append([ + self.core.tracklist.add([ Track(uri='dummy:a', length=40000), Track(uri='dummy:b')]) self.core.playback.play() @@ -695,7 +667,7 @@ class PlayerInterfaceTest(unittest.TestCase): def test_set_position_is_ignored_if_can_seek_is_false(self): self.mpris.get_CanSeek = lambda *_: False - self.core.tracklist.append([Track(uri='dummy:a', length=40000)]) + self.core.tracklist.add([Track(uri='dummy:a', length=40000)]) self.core.playback.play() before_set_position = self.core.playback.time_position.get() @@ -713,7 +685,7 @@ class PlayerInterfaceTest(unittest.TestCase): self.assertLess(after_set_position, position_to_set_in_millisec) def test_set_position_sets_the_current_track_position_in_microsecs(self): - self.core.tracklist.append([Track(uri='dummy:a', length=40000)]) + self.core.tracklist.add([Track(uri='dummy:a', length=40000)]) self.core.playback.play() before_set_position = self.core.playback.time_position.get() @@ -734,7 +706,7 @@ class PlayerInterfaceTest(unittest.TestCase): after_set_position, position_to_set_in_millisec) def test_set_position_does_nothing_if_the_position_is_negative(self): - self.core.tracklist.append([Track(uri='dummy:a', length=40000)]) + self.core.tracklist.add([Track(uri='dummy:a', length=40000)]) self.core.playback.play() self.core.playback.seek(20000) @@ -757,7 +729,7 @@ class PlayerInterfaceTest(unittest.TestCase): self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a') def test_set_position_does_nothing_if_position_is_gt_track_length(self): - self.core.tracklist.append([Track(uri='dummy:a', length=40000)]) + self.core.tracklist.add([Track(uri='dummy:a', length=40000)]) self.core.playback.play() self.core.playback.seek(20000) @@ -780,7 +752,7 @@ class PlayerInterfaceTest(unittest.TestCase): self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a') def test_set_position_is_noop_if_track_id_isnt_current_track(self): - self.core.tracklist.append([Track(uri='dummy:a', length=40000)]) + self.core.tracklist.add([Track(uri='dummy:a', length=40000)]) self.core.playback.play() self.core.playback.seek(20000) @@ -826,8 +798,7 @@ class PlayerInterfaceTest(unittest.TestCase): def test_open_uri_starts_playback_of_new_track_if_stopped(self): self.mpris.get_CanPlay = lambda *_: True self.backend.library.dummy_library = [Track(uri='dummy:/test/uri')] - self.core.tracklist.append([ - Track(uri='dummy:a'), Track(uri='dummy:b')]) + self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.assertEqual(self.core.playback.state.get(), STOPPED) self.mpris.OpenUri('dummy:/test/uri') @@ -839,8 +810,7 @@ class PlayerInterfaceTest(unittest.TestCase): def test_open_uri_starts_playback_of_new_track_if_paused(self): self.mpris.get_CanPlay = lambda *_: True self.backend.library.dummy_library = [Track(uri='dummy:/test/uri')] - self.core.tracklist.append([ - Track(uri='dummy:a'), Track(uri='dummy:b')]) + self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.core.playback.pause() self.assertEqual(self.core.playback.state.get(), PAUSED) @@ -855,8 +825,7 @@ class PlayerInterfaceTest(unittest.TestCase): def test_open_uri_starts_playback_of_new_track_if_playing(self): self.mpris.get_CanPlay = lambda *_: True self.backend.library.dummy_library = [Track(uri='dummy:/test/uri')] - self.core.tracklist.append([ - Track(uri='dummy:a'), Track(uri='dummy:b')]) + self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.assertEqual(self.core.playback.state.get(), PLAYING) self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a') diff --git a/tests/frontends/mpris/playlists_interface_test.py b/tests/frontends/mpris/playlists_interface_test.py index 21038d4b..2adffaf3 100644 --- a/tests/frontends/mpris/playlists_interface_test.py +++ b/tests/frontends/mpris/playlists_interface_test.py @@ -44,7 +44,7 @@ class PlayerInterfaceTest(unittest.TestCase): pykka.ActorRegistry.stop_all() def test_activate_playlist_appends_tracks_to_tracklist(self): - self.core.tracklist.append([ + self.core.tracklist.add([ Track(uri='dummy:old-a'), Track(uri='dummy:old-b'), ]) From 3dc15862130c9c568a807d369af916e97d967380 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 20 Nov 2012 12:21:43 +0100 Subject: [PATCH 20/39] core: Remove tracklist.append() --- docs/changes.rst | 3 +++ mopidy/core/tracklist.py | 5 ----- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index d664872b..f1eebad9 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -161,6 +161,9 @@ backends: - Remove :attr:`mopidy.core.PlaybackController.current_tlid`. Use :attr:`mopidy.core.PlaybackController.current_tl_track` instead. +- Remove :meth:`mopidy.core.TracklistController.append`. Use + :meth:`mopidy.core.TracklistController.add` instead. + - Added support for connecting to the Spotify service through an HTTP or SOCKS proxy, which is supported by pyspotify >= 1.9. diff --git a/mopidy/core/tracklist.py b/mopidy/core/tracklist.py index 57c9de63..a5ea6c11 100644 --- a/mopidy/core/tracklist.py +++ b/mopidy/core/tracklist.py @@ -109,11 +109,6 @@ class TracklistController(object): else: return tl_tracks - append = add - """ - Alias for :meth:`add`. - """ - def clear(self): """ Clear the tracklist. From ae9a25709173ba9512ff25569ece74f41de61f7b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 20 Nov 2012 22:12:09 +0100 Subject: [PATCH 21/39] Make tracklist.add() only take and return lists --- mopidy/core/tracklist.py | 22 +++++----------------- tests/backends/base/__init__.py | 4 +--- tests/backends/base/tracklist.py | 12 ++++++------ tests/backends/local/playback_test.py | 2 +- tests/core/events_test.py | 12 ++++++------ 5 files changed, 19 insertions(+), 33 deletions(-) diff --git a/mopidy/core/tracklist.py b/mopidy/core/tracklist.py index a5ea6c11..0337828c 100644 --- a/mopidy/core/tracklist.py +++ b/mopidy/core/tracklist.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals import logging import random -from mopidy.models import TlTrack, Track +from mopidy.models import TlTrack from . import listener @@ -70,26 +70,17 @@ class TracklistController(object): the tracklist. If ``at_position`` is not given, the tracks are appended to the end of the tracklist. - If ``tracks`` is a track object, a single - :class:`mopidy.models.TlTrack` object is returned. If ``tracks`` is a - list, a list of :class:`mopidy.models.TlTrack` is returned. - Triggers the :meth:`mopidy.core.CoreListener.tracklist_changed` event. - :param track: track to add - :type track: :class:`mopidy.models.Track` + :param tracks: tracks to add + :type tracks: list of :class:`mopidy.models.Track` :param at_position: position in tracklist to add track :type at_position: int or :class:`None` - :rtype: a single or a list of :class:`mopidy.models.TlTrack` + :rtype: list of :class:`mopidy.models.TlTrack` """ assert at_position is None or at_position <= len(self._tl_tracks), \ 'at_position can not be greater than tracklist length' - single_add = False - if isinstance(tracks, Track): - tracks = [tracks] - single_add = True - tl_tracks = [] for track in tracks: tl_track = TlTrack(self._next_tlid, track) @@ -104,10 +95,7 @@ class TracklistController(object): if tl_tracks: self._increase_version() - if single_add: - return tl_tracks[0] - else: - return tl_tracks + return tl_tracks def clear(self): """ diff --git a/tests/backends/base/__init__.py b/tests/backends/base/__init__.py index 477f8cc1..7dc4bcf6 100644 --- a/tests/backends/base/__init__.py +++ b/tests/backends/base/__init__.py @@ -3,9 +3,7 @@ from __future__ import unicode_literals def populate_tracklist(func): def wrapper(self): - self.tl_tracks = [] - for track in self.tracks: - self.tl_tracks.append(self.core.tracklist.add(track)) + self.tl_tracks = self.core.tracklist.add(self.tracks) return func(self) wrapper.__name__ = func.__name__ diff --git a/tests/backends/base/tracklist.py b/tests/backends/base/tracklist.py index 53b3288a..09b2b6a6 100644 --- a/tests/backends/base/tracklist.py +++ b/tests/backends/base/tracklist.py @@ -36,17 +36,17 @@ class TracklistControllerTest(object): def test_add(self): for track in self.tracks: - tl_track = self.controller.add(track) + tl_tracks = self.controller.add([track]) self.assertEqual(track, self.controller.tracks[-1]) - self.assertEqual(tl_track, self.controller.tl_tracks[-1]) - self.assertEqual(track, tl_track.track) + self.assertEqual(tl_tracks[0], self.controller.tl_tracks[-1]) + self.assertEqual(track, tl_tracks[0].track) def test_add_at_position(self): for track in self.tracks[:-1]: - tl_track = self.controller.add(track, 0) + tl_tracks = self.controller.add([track], 0) self.assertEqual(track, self.controller.tracks[0]) - self.assertEqual(tl_track, self.controller.tl_tracks[0]) - self.assertEqual(track, tl_track.track) + self.assertEqual(tl_tracks[0], self.controller.tl_tracks[0]) + self.assertEqual(track, tl_tracks[0].track) @populate_tracklist def test_add_at_position_outside_of_playlist(self): diff --git a/tests/backends/local/playback_test.py b/tests/backends/local/playback_test.py index 285270ce..9731f70d 100644 --- a/tests/backends/local/playback_test.py +++ b/tests/backends/local/playback_test.py @@ -27,7 +27,7 @@ class LocalPlaybackControllerTest(PlaybackControllerTest, unittest.TestCase): def add_track(self, path): uri = path_to_uri(path_to_data_dir(path)) track = Track(uri=uri, length=4464) - self.tracklist.add(track) + self.tracklist.add([track]) def test_uri_scheme(self): self.assertIn('file', self.core.uri_schemes) diff --git a/tests/core/events_test.py b/tests/core/events_test.py index b0ae2081..88f07de6 100644 --- a/tests/core/events_test.py +++ b/tests/core/events_test.py @@ -26,14 +26,14 @@ class BackendEventsTest(unittest.TestCase): self.assertEqual(send.call_args[0][0], 'playlists_loaded') def test_pause_sends_track_playback_paused_event(self, send): - self.core.tracklist.add(Track(uri='dummy:a')) + self.core.tracklist.add([Track(uri='dummy:a')]) self.core.playback.play().get() send.reset_mock() self.core.playback.pause().get() self.assertEqual(send.call_args[0][0], 'track_playback_paused') def test_resume_sends_track_playback_resumed(self, send): - self.core.tracklist.add(Track(uri='dummy:a')) + self.core.tracklist.add([Track(uri='dummy:a')]) self.core.playback.play() self.core.playback.pause().get() send.reset_mock() @@ -41,20 +41,20 @@ class BackendEventsTest(unittest.TestCase): self.assertEqual(send.call_args[0][0], 'track_playback_resumed') def test_play_sends_track_playback_started_event(self, send): - self.core.tracklist.add(Track(uri='dummy:a')) + self.core.tracklist.add([Track(uri='dummy:a')]) send.reset_mock() self.core.playback.play().get() self.assertEqual(send.call_args[0][0], 'track_playback_started') def test_stop_sends_track_playback_ended_event(self, send): - self.core.tracklist.add(Track(uri='dummy:a')) + self.core.tracklist.add([Track(uri='dummy:a')]) self.core.playback.play().get() send.reset_mock() self.core.playback.stop().get() self.assertEqual(send.call_args_list[0][0][0], 'track_playback_ended') def test_seek_sends_seeked_event(self, send): - self.core.tracklist.add(Track(uri='dummy:a', length=40000)) + self.core.tracklist.add([Track(uri='dummy:a', length=40000)]) self.core.playback.play().get() send.reset_mock() self.core.playback.seek(1000).get() @@ -62,7 +62,7 @@ class BackendEventsTest(unittest.TestCase): def test_tracklist_add_sends_tracklist_changed_event(self, send): send.reset_mock() - self.core.tracklist.add(Track(uri='dummy:a')).get() + self.core.tracklist.add([Track(uri='dummy:a')]).get() self.assertEqual(send.call_args[0][0], 'tracklist_changed') def test_tracklist_clear_sends_tracklist_changed_event(self, send): From acbde530c2b7d414ebc6b4cc9623930254db7c24 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 20 Nov 2012 22:23:48 +0100 Subject: [PATCH 22/39] core: Add getters/setters for consume/random/repeat/single Also, the properties and methods was sorted alphabetically. The `state` and `time_position` properties were out of order. --- mopidy/core/playback.py | 194 +++++++++++++++++++++++----------------- 1 file changed, 113 insertions(+), 81 deletions(-) diff --git a/mopidy/core/playback.py b/mopidy/core/playback.py index 94fd7d4e..e50de2e7 100644 --- a/mopidy/core/playback.py +++ b/mopidy/core/playback.py @@ -12,56 +12,12 @@ from . import listener logger = logging.getLogger('mopidy.core') -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: - # pylint: disable = W0212 - self._trigger_options_changed() - # pylint: enable = W0212 - return setattr(self, name, value) - - return property(get_option, set_option) - - class PlaybackController(object): # pylint: disable = R0902 # Too many instance attributes pykka_traversable = True - #: :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 :class:`mopidy.models.TlTrack`, or - #: :class:`None`. - current_tl_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, audio, backends, core): self.audio = audio self.backends = backends @@ -79,6 +35,30 @@ class PlaybackController(object): uri_scheme = urlparse.urlparse(uri).scheme return self.backends.with_playback_by_uri_scheme.get(uri_scheme, None) + ### Properties + + def get_consume(self): + return getattr(self, '_consume', False) + + def set_consume(self, value): + if self.get_consume() != value: + self._trigger_options_changed() + return setattr(self, '_consume', value) + + consume = property(get_consume, set_consume) + """ + :class:`True` + Tracks are removed from the playlist when they have been played. + :class:`False` + Tracks are not removed from the playlist. + """ + + current_tl_track = None + """ + The currently playing or selected :class:`mopidy.models.TlTrack`, or + :class:`None`. + """ + def get_current_track(self): return self.current_tl_track and self.current_tl_track.track @@ -89,6 +69,93 @@ class PlaybackController(object): Read-only. Extracted from :attr:`current_tl_track` for convenience. """ + def get_random(self): + return getattr(self, '_random', False) + + def set_random(self, value): + if self.get_random() != value: + self._trigger_options_changed() + return setattr(self, '_random', value) + + random = property(get_random, set_random) + """ + :class:`True` + Tracks are selected at random from the playlist. + :class:`False` + Tracks are played in the order of the playlist. + """ + + def get_repeat(self): + return getattr(self, '_repeat', False) + + def set_repeat(self, value): + if self.get_repeat() != value: + self._trigger_options_changed() + return setattr(self, '_repeat', value) + + repeat = property(get_repeat, set_repeat) + """ + :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. + """ + + def get_single(self): + return getattr(self, '_single', False) + + def set_single(self, value): + if self.get_single() != value: + self._trigger_options_changed() + return setattr(self, '_single', value) + + single = property(get_single, set_single) + """ + :class:`True` + Playback is stopped after current song, unless in :attr:`repeat` + mode. + :class:`False` + Playback continues after current song. + """ + + def get_state(self): + return self._state + + def set_state(self, new_state): + (old_state, self._state) = (self.state, new_state) + logger.debug('Changing state: %s -> %s', old_state, new_state) + + self._trigger_playback_state_changed(old_state, new_state) + + state = property(get_state, set_state) + """ + 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" ] + """ + + def get_time_position(self): + backend = self._get_backend() + if backend: + return backend.playback.get_time_position().get() + else: + return 0 + + time_position = property(get_time_position) + """Time position in milliseconds.""" + def get_tracklist_position(self): if self.current_tl_track is None: return None @@ -205,43 +272,6 @@ class PlaybackController(object): instead. """ - def get_state(self): - return self._state - - def set_state(self, new_state): - (old_state, self._state) = (self.state, new_state) - logger.debug('Changing state: %s -> %s', old_state, new_state) - - self._trigger_playback_state_changed(old_state, new_state) - - state = property(get_state, set_state) - """ - 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" ] - """ - - def get_time_position(self): - backend = self._get_backend() - if backend: - return backend.playback.get_time_position().get() - else: - return 0 - - time_position = property(get_time_position) - """Time position in milliseconds.""" - def get_volume(self): if self.audio: return self.audio.get_volume().get() @@ -259,6 +289,8 @@ class PlaybackController(object): volume = property(get_volume, set_volume) """Volume as int in range [0..100] or :class:`None`""" + ### Methods + def change_track(self, tl_track, on_error_step=1): """ Change to the given track, keeping the current playback state. From 8f1b98b30652ee1a764d14473f159a07d052e139 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 20 Nov 2012 22:36:39 +0100 Subject: [PATCH 23/39] core: Don't fail when adding tracks after end of tracklist --- mopidy/core/tracklist.py | 3 --- tests/backends/base/tracklist.py | 8 +++++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/mopidy/core/tracklist.py b/mopidy/core/tracklist.py index 0337828c..656e15b1 100644 --- a/mopidy/core/tracklist.py +++ b/mopidy/core/tracklist.py @@ -78,9 +78,6 @@ class TracklistController(object): :type at_position: int or :class:`None` :rtype: list of :class:`mopidy.models.TlTrack` """ - assert at_position is None or at_position <= len(self._tl_tracks), \ - 'at_position can not be greater than tracklist length' - tl_tracks = [] for track in tracks: tl_track = TlTrack(self._next_tlid, track) diff --git a/tests/backends/base/tracklist.py b/tests/backends/base/tracklist.py index 09b2b6a6..71f44018 100644 --- a/tests/backends/base/tracklist.py +++ b/tests/backends/base/tracklist.py @@ -50,9 +50,11 @@ class TracklistControllerTest(object): @populate_tracklist def test_add_at_position_outside_of_playlist(self): - test = lambda: self.controller.add( - self.tracks[0], len(self.tracks) + 2) - self.assertRaises(AssertionError, test) + for track in self.tracks: + tl_tracks = self.controller.add([track], len(self.tracks) + 2) + self.assertEqual(track, self.controller.tracks[-1]) + self.assertEqual(tl_tracks[0], self.controller.tl_tracks[-1]) + self.assertEqual(track, tl_tracks[0].track) @populate_tracklist def test_filter_by_tlid(self): From 174d38b790bb5a1eb05f2ee122c958d9a4a32424 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 20 Nov 2012 23:34:21 +0100 Subject: [PATCH 24/39] docs: Clean changelog for v0.9 --- docs/changes.rst | 262 ++++++++++++++++++++++++++--------------------- 1 file changed, 146 insertions(+), 116 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index f1eebad9..4cfe2972 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -8,15 +8,15 @@ This change log is used to track all major changes to Mopidy. v0.9.0 (in development) ======================= +Support for using the local and Spotify backends simultaneously have for a very +long time been our most requested feature. Finally, it's here! + **Dependencies** - pyspotify >= 1.9, < 1.10 is now required for Spotify support. **Multiple backends support** -Support for using the local and Spotify backends simultaneously have for a very -long time been our most requested feature. Finally, it's here! - - Both the local backend and the Spotify backend are now turned on by default. The local backend is listed first in the :attr:`mopidy.settings.BACKENDS` setting, and are thus given the highest priority in e.g. search results, @@ -28,8 +28,69 @@ long time been our most requested feature. Finally, it's here! As always, see :mod:`mopidy.settings` for the full list of available settings. +**Spotify backend** + +- The Spotify backend now includes release year and artist on albums. + +- :issue:`233`: The Spotify backend now returns the track if you search for the + Spotify track URI. + +- Added support for connecting to the Spotify service through an HTTP or SOCKS + proxy, which is supported by pyspotify >= 1.9. + +**Local backend** + +- :issue:`236`: The ``mopidy-scan`` command failed to include tags from ALAC + files (Apple lossless) because it didn't support multiple tag messages from + GStreamer per track it scanned. + +- Added support for search by filename to local backend. + +**MPD frontend** + +- :issue:`218`: The MPD commands ``listplaylist`` and ``listplaylistinfo`` now + accepts unquoted playlist names if they don't contain spaces. + +- :issue:`246`: The MPD command ``list album artist ""`` and similar + ``search``, ``find``, and ``list`` commands with empty filter values caused a + :exc:`LookupError`, but should have been ignored by the MPD server. + +- The MPD frontend no longer lowercases search queries. This broke e.g. search + by URI, where casing may be essential. + +- The MPD command ``plchanges`` always returned the entire playlist. It now + returns an empty response when the client has seen the latest version. + +- The MPD commands ``search`` and ``find`` now allows the key ``file``, which + is used by ncmpcpp instead of ``filename``. + +**MPRIS frontend** + +- The MPRIS playlists interface is now supported by our MPRIS frontend. This + means that you now can select playlists to queue and play from the Ubuntu + Sound Menu. + +**Audio mixers** + +- Made the :mod:`NAD mixer ` responsive to interrupts + during amplifier calibration. It will now quit immediately, while previously + it completed the calibration first, and then quit, which could take more than + 15 seconds. + +**Developer support** + +- Added optional background thread for debugging deadlocks. When the feature is + enabled via the ``--debug-thread`` option or + :attr:`mopidy.settings.DEBUG_THREAD` setting a ``SIGUSR1`` signal will dump + the traceback for all running threads. + +- The settings validator will now allow any setting prefixed with ``CUSTOM_`` + to exist in the settings file. + +**Internal changes** + Internally, Mopidy have seen a lot of changes to pave the way for multiple -backends: +backends and the future HTTP frontend. - A new layer and actor, "core", has been added to our stack, inbetween the frontends and the backends. The responsibility of the core layer and actor is @@ -40,12 +101,6 @@ backends: Frontends no longer know anything about the backends. They just use the :ref:`core-api`. -- The base playback provider has been updated with sane default behavior - instead of empty functions. By default, the playback provider now lets - GStreamer keep track of the current track's time position. The local backend - simply uses the base playback provider without any changes. The same applies - to any future backend that just needs GStreamer to play an URI for it. - - The dependency graph between the core controllers and the backend providers have been straightened out, so that we don't have any circular dependencies. The frontend, core, backend, and audio layers are now strictly separate. The @@ -60,131 +115,106 @@ backends: dummy/mocked lower layers easier than with the old variant, where dependencies where looked up in Pykka's actor registry. -- Renamed "current playlist" to "tracklist" everywhere, including the core API - used by frontends. +- All properties in the core API now got getters, and setters if setting them + is allowed. They are not explictly listed in the docs as they have the same + behavior as the documented properties, but they are available and may be + used. This is useful for the future HTTP frontend. -- Renamed "stored playlists" to "playlists" everywhere, including the core API - used by frontends. - -- The playlists part of the core API has been revised to be more focused around - the playlist URI, and some redundant functionality has been removed: - - - :attr:`mopidy.core.PlaylistsController.playlists` no longer supports - assignment to it. The `playlists` property on the backend layer still does, - and all functionality is maintained by assigning to the playlists - collections at the backend level. - - - :meth:`mopidy.core.PlaylistsController.delete` now accepts an URI, and not - a playlist object. - - - :meth:`mopidy.core.PlaylistsController.save` now returns the saved - playlist. The returned playlist may differ from the saved playlist, and - should thus be used instead of the playlist passed to ``save()``. - - - :meth:`mopidy.core.PlaylistsController.rename` has been removed, since - renaming can be done with ``save()``. - -**Changes** - -- Made the :mod:`NAD mixer ` responsive to interrupts - during amplifier calibration. It will now quit immediately, while previously - it completed the calibration first, and then quit, which could take more than - 15 seconds. +*Models:* - Added :attr:`mopidy.models.Album.date` attribute. It has the same format as the existing :attr:`mopidy.models.Track.date`. -- The Spotify backend now includes release year and artist on albums. +- Added :class:`mopidy.models.ModelJSONEncoder` and + :func:`mopidy.models.model_json_decoder` for automatic JSON serialization and + deserialization of data structures which contains Mopidy models. This is + useful for the future HTTP frontend. -- Added support for search by filename to local backend. +*Library:* -- Added optional background thread for debugging deadlocks. When the feature is - enabled via the ``--debug-thread`` option or - :attr:`mopidy.settings.DEBUG_THREAD` setting a ``SIGUSR1`` signal will dump - the traceback for all running threads. +- :meth:`mopidy.core.LibraryController.find_exact` and + :meth:`mopidy.core.LibraryController.search` now returns plain lists of + tracks instead of playlist objects. -- Make the entire code base use unicode strings by default, and only fall back - to bytestrings where it is required. Another step closer to Python 3. +- :meth:`mopidy.core.LibraryController.lookup` now returns a list of tracks + instead of a single track. This makes it possible to support lookup of + artist or album URIs which then can expand to a list of tracks. -- The settings validator will now allow any setting prefixed with ``CUSTOM_`` - to exist in the settings file. +*Playback:* -- The MPD commands ``search`` and ``find`` now allows the key ``file``, which - is used by ncmpcpp instead of ``filename``. +- The base playback provider has been updated with sane default behavior + instead of empty functions. By default, the playback provider now lets + GStreamer keep track of the current track's time position. The local backend + simply uses the base playback provider without any changes. Any future + backend that just feeds URIs to GStreamer to play can also use the base + playback provider without any changes. -- The Spotify backend now returns the track if you search for the Spotify track - URI. (Fixes: :issue:`233`) +- Removed :attr:`mopidy.core.PlaybackController.track_at_previous`. Use + :attr:`mopidy.core.PlaybackController.tl_track_at_previous` instead. -- :meth:`mopidy.core.TracklistController.append` now returns a list of the - :class:`mopidy.models.TlTrack` instances that was added to the tracklist. - This makes it easier to start playing one of the tracks that was just - appended to the tracklist. +- Removed :attr:`mopidy.core.PlaybackController.track_at_next`. Use + :attr:`mopidy.core.PlaybackController.tl_track_at_next` instead. + +- Removed :attr:`mopidy.core.PlaybackController.track_at_eot`. Use + :attr:`mopidy.core.PlaybackController.tl_track_at_eot` instead. + +- Removed :attr:`mopidy.core.PlaybackController.current_tlid`. Use + :attr:`mopidy.core.PlaybackController.current_tl_track` instead. + +*Playlists:* + +The playlists part of the core API has been revised to be more focused around +the playlist URI, and some redundant functionality has been removed: + +- Renamed "stored playlists" to "playlists" everywhere, including the core API + used by frontends. + +- :attr:`mopidy.core.PlaylistsController.playlists` no longer supports + assignment to it. The `playlists` property on the backend layer still does, + and all functionality is maintained by assigning to the playlists collections + at the backend level. + +- :meth:`mopidy.core.PlaylistsController.delete` now accepts an URI, and not a + playlist object. + +- :meth:`mopidy.core.PlaylistsController.save` now returns the saved playlist. + The returned playlist may differ from the saved playlist, and should thus be + used instead of the playlist passed to + :meth:`mopidy.core.PlaylistsController.save`. + +- :meth:`mopidy.core.PlaylistsController.rename` has been removed, since + renaming can be done with :meth:`mopidy.core.PlaylistsController.save`. + +- :meth:`mopidy.core.PlaylistsController.get` has been replaced by + :meth:`mopidy.core.PlaylistsController.filter`. + +- The event :meth:`mopidy.core.CoreListener.playlist_changed` has been changed + to include the playlist that was changed. + +*Tracklist:* + +- Renamed "current playlist" to "tracklist" everywhere, including the core API + used by frontends. + +- Removed :meth:`mopidy.core.TracklistController.append`. Use + :meth:`mopidy.core.TracklistController.add` instead, which is now capable of + adding multiple tracks. + +- :meth:`mopidy.core.TracklistController.get` has been replaced by + :meth:`mopidy.core.TracklistController.filter`. + +- :meth:`mopidy.core.TracklistController.remove` can now remove multiple + tracks, and returns the tracks it removed. - When the tracklist is changed, we now trigger the new :meth:`mopidy.core.CoreListener.tracklist_changed` event. Previously we triggered :meth:`mopidy.core.CoreListener.playlist_changed`, which is intended for stored playlists, not the tracklist. -- The event :meth:`mopidy.core.CoreListener.playlist_changed` has been changed - to include the playlist that was changed. +*Towards Python 3 support:* -- The MPRIS playlists interface is now supported by our MPRIS frontend. This - means that you now can select playlists to queue and play from the Ubuntu - Sound Menu. - -- :meth:`mopidy.core.LibraryController.find_exact` and - :meth:`mopidy.core.LibraryController.search` now returns plain lists of - tracks instead of playlist objects. - -- :meth:`mopidy.core.TracklistController.get` has been replaced by - :meth:`mopidy.core.TracklistController.filter`. - -- :meth:`mopidy.core.PlaylistsController.get` has been replaced by - :meth:`mopidy.core.PlaylistsController.filter`. - -- :meth:`mopidy.core.TracklistController.remove` can now remove multiple - tracks, and returns the tracks it removed. - -- :meth:`mopidy.core.LibraryController.lookup` now returns a list of tracks. - This makes it possible to support lookup of artist or album URIs which then - can expand to a list of tracks. - -- Remove :attr:`mopidy.core.PlaybackController.track_at_previous`. Use - :attr:`mopidy.core.PlaybackController.tl_track_at_previous` instead. - -- Remove :attr:`mopidy.core.PlaybackController.track_at_next`. Use - :attr:`mopidy.core.PlaybackController.tl_track_at_next` instead. - -- Remove :attr:`mopidy.core.PlaybackController.track_at_eot`. Use - :attr:`mopidy.core.PlaybackController.tl_track_at_eot` instead. - -- Remove :attr:`mopidy.core.PlaybackController.current_tlid`. Use - :attr:`mopidy.core.PlaybackController.current_tl_track` instead. - -- Remove :meth:`mopidy.core.TracklistController.append`. Use - :meth:`mopidy.core.TracklistController.add` instead. - -- Added support for connecting to the Spotify service through an HTTP or SOCKS - proxy, which is supported by pyspotify >= 1.9. - -**Bug fixes** - -- :issue:`218`: The MPD commands ``listplaylist`` and ``listplaylistinfo`` now - accepts unquotes playlist names if they don't contain spaces. - -- The MPD command ``plchanges`` always returned the entire playlist. It now - returns an empty response when the client has seen the latest version. - -- MPD no longer lowercases search queries. This broke e.g. search by URI, where - casing may be essential. - -- :issue:`236`: The ``mopidy-scan`` command failed to include tags from ALAC - files (Apple lossless) because it didn't support multiple tag messages from - GStreamer per track it scanned. - -- :issue:`246`: The MPD command ``list album artist ""`` and similar - ``search``, ``find``, and ``list`` commands with empty filter values caused a - :exc:`LookupError`, but should have been ignored by the MPD server. +- Make the entire code base use unicode strings by default, and only fall back + to bytestrings where it is required. Another step closer to Python 3. v0.8.1 (2012-10-30) From 02345beb0e805c3ae17f186eb9dece9a7e69b83e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 20 Nov 2012 23:57:06 +0100 Subject: [PATCH 25/39] docs: Add major docs changes to the changelog --- docs/changes.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index 4cfe2972..47d1ea2d 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -15,6 +15,18 @@ long time been our most requested feature. Finally, it's here! - pyspotify >= 1.9, < 1.10 is now required for Spotify support. +**Documentation** + +- New :ref:`installation` guides, organized by OS and distribution so that you + can follow one concise list of instructions instead of jumping around the + docs to look for instructions for each dependency. + +- Moved :ref:`raspberrypi-installation` howto from the wiki to the docs. + +- Updated :ref:`mpd-clients` overview. + +- Added :ref:`mpris-clients` and :ref:`upnp-clients` overview. + **Multiple backends support** - Both the local backend and the Spotify backend are now turned on by default. @@ -110,6 +122,8 @@ backends and the future HTTP frontend. broadcasting of events to listeners, through e.g. :class:`mopidy.core.CoreListener` and :class:`mopidy.audio.AudioListener`. + See :ref:`concepts` for more details and illustrations of all the relations. + - All dependencies are now explicitly passed to the constructors of the frontends, core, and the backends. This makes testing each layer with dummy/mocked lower layers easier than with the old variant, where From f313d9d44681e0795b237edfc8cd89af09408068 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 21 Nov 2012 00:23:10 +0100 Subject: [PATCH 26/39] spotify: Ignore playlists without a name --- docs/changes.rst | 3 +++ mopidy/backends/spotify/translator.py | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index 47d1ea2d..eef62a1f 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -50,6 +50,9 @@ long time been our most requested feature. Finally, it's here! - Added support for connecting to the Spotify service through an HTTP or SOCKS proxy, which is supported by pyspotify >= 1.9. +- Subscriptions to other Spotify user's "starred" playlists are ignored, as + they currently isn't fully supported by pyspotify. + **Local backend** - :issue:`236`: The ``mopidy-scan`` command failed to include tags from ALAC diff --git a/mopidy/backends/spotify/translator.py b/mopidy/backends/spotify/translator.py index 834b34d8..92b4514e 100644 --- a/mopidy/backends/spotify/translator.py +++ b/mopidy/backends/spotify/translator.py @@ -56,6 +56,10 @@ def to_mopidy_playlist(spotify_playlist): uri = str(Link.from_playlist(spotify_playlist)) if not spotify_playlist.is_loaded(): return Playlist(uri=uri, name='[loading...]') + if not spotify_playlist.name(): + # Other user's "starred" playlists isn't handled properly by pyspotify + # See https://github.com/mopidy/pyspotify/issues/81 + return return Playlist( uri=uri, name=spotify_playlist.name(), From 72574c1ae0c57e77a97eb585a252e3a62a0e553d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 21 Nov 2012 00:32:46 +0100 Subject: [PATCH 27/39] mpd: listplaylists should not return playlists without a name --- docs/changes.rst | 3 +++ mopidy/frontends/mpd/protocol/stored_playlists.py | 7 +++++++ tests/frontends/mpd/protocol/stored_playlists_test.py | 9 +++++++++ 3 files changed, 19 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index eef62a1f..295bf8fd 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -79,6 +79,9 @@ long time been our most requested feature. Finally, it's here! - The MPD commands ``search`` and ``find`` now allows the key ``file``, which is used by ncmpcpp instead of ``filename``. +- The MPD command ``listplaylists`` will no longer return playlists without a + name. This could crash ncmpcpp. + **MPRIS frontend** - The MPRIS playlists interface is now supported by our MPRIS frontend. This diff --git a/mopidy/frontends/mpd/protocol/stored_playlists.py b/mopidy/frontends/mpd/protocol/stored_playlists.py index de2b267e..a7be2399 100644 --- a/mopidy/frontends/mpd/protocol/stored_playlists.py +++ b/mopidy/frontends/mpd/protocol/stored_playlists.py @@ -70,9 +70,16 @@ def listplaylists(context): Last-Modified: 2010-02-06T02:10:25Z playlist: b Last-Modified: 2010-02-06T02:11:08Z + + *Clarifications:* + + - ncmpcpp 0.5.10 segfaults if we return 'playlist: ' on a line, so we must + ignore playlists without names, which isn't very useful anyway. """ result = [] for playlist in context.core.playlists.playlists.get(): + if not playlist.name: + continue result.append(('playlist', playlist.name)) last_modified = ( playlist.last_modified or dt.datetime.now()).isoformat() diff --git a/tests/frontends/mpd/protocol/stored_playlists_test.py b/tests/frontends/mpd/protocol/stored_playlists_test.py index cf1b8cd0..414f0b25 100644 --- a/tests/frontends/mpd/protocol/stored_playlists_test.py +++ b/tests/frontends/mpd/protocol/stored_playlists_test.py @@ -64,6 +64,15 @@ class PlaylistsHandlerTest(protocol.BaseTestCase): self.assertInResponse('Last-Modified: 2001-03-17T13:41:17Z') self.assertInResponse('OK') + def test_listplaylists_ignores_playlists_without_name(self): + last_modified = datetime.datetime(2001, 3, 17, 13, 41, 17, 12345) + self.backend.playlists.playlists = [ + Playlist(name='', last_modified=last_modified)] + + self.sendRequest('listplaylists') + self.assertNotInResponse('playlist: ') + self.assertInResponse('OK') + def test_load_known_playlist_appends_to_tracklist(self): self.core.tracklist.add([Track(uri='a'), Track(uri='b')]) self.assertEqual(len(self.core.tracklist.tracks.get()), 2) From 5fbb6328d64c8a540dc3670c7b5e115f585c9467 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 21 Nov 2012 00:48:08 +0100 Subject: [PATCH 28/39] mpd: list shouldn't return blank artist names, album names, or dates --- docs/changes.rst | 3 +++ mopidy/backends/dummy.py | 6 +++-- mopidy/frontends/mpd/protocol/music_db.py | 7 +++--- tests/frontends/mpd/protocol/music_db_test.py | 25 +++++++++++++++++++ 4 files changed, 36 insertions(+), 5 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 295bf8fd..485ac0fd 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -82,6 +82,9 @@ long time been our most requested feature. Finally, it's here! - The MPD command ``listplaylists`` will no longer return playlists without a name. This could crash ncmpcpp. +- The MPD command ``list`` will no longer return artist names, album names, or + dates that are blank. + **MPRIS frontend** - The MPRIS playlists interface is now supported by our MPRIS frontend. This diff --git a/mopidy/backends/dummy.py b/mopidy/backends/dummy.py index 62ac8e8f..39180bbb 100644 --- a/mopidy/backends/dummy.py +++ b/mopidy/backends/dummy.py @@ -37,9 +37,11 @@ class DummyLibraryProvider(base.BaseLibraryProvider): def __init__(self, *args, **kwargs): super(DummyLibraryProvider, self).__init__(*args, **kwargs) self.dummy_library = [] + self.dummy_find_exact_result = [] + self.dummy_search_result = [] def find_exact(self, **query): - return [] + return self.dummy_find_exact_result def lookup(self, uri): return filter(lambda t: uri == t.uri, self.dummy_library) @@ -48,7 +50,7 @@ class DummyLibraryProvider(base.BaseLibraryProvider): pass def search(self, **query): - return [] + return self.dummy_search_result class DummyPlaybackProvider(base.BasePlaybackProvider): diff --git a/mopidy/frontends/mpd/protocol/music_db.py b/mopidy/frontends/mpd/protocol/music_db.py index 26371364..8f41b199 100644 --- a/mopidy/frontends/mpd/protocol/music_db.py +++ b/mopidy/frontends/mpd/protocol/music_db.py @@ -250,7 +250,8 @@ def _list_artist(context, query): tracks = context.core.library.find_exact(**query).get() for track in tracks: for artist in track.artists: - artists.add(('Artist', artist.name)) + if artist.name: + artists.add(('Artist', artist.name)) return artists @@ -258,7 +259,7 @@ def _list_album(context, query): albums = set() tracks = context.core.library.find_exact(**query).get() for track in tracks: - if track.album is not None: + if track.album and track.album.name: albums.add(('Album', track.album.name)) return albums @@ -267,7 +268,7 @@ def _list_date(context, query): dates = set() tracks = context.core.library.find_exact(**query).get() for track in tracks: - if track.date is not None: + if track.date: dates.add(('Date', track.date)) return dates diff --git a/tests/frontends/mpd/protocol/music_db_test.py b/tests/frontends/mpd/protocol/music_db_test.py index 9a233e40..44999a4f 100644 --- a/tests/frontends/mpd/protocol/music_db_test.py +++ b/tests/frontends/mpd/protocol/music_db_test.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals +from mopidy.models import Album, Artist, Track + from tests.frontends.mpd import protocol @@ -181,6 +183,14 @@ class MusicDatabaseListTest(protocol.BaseTestCase): self.sendRequest('list "artist" "artist" ""') self.assertInResponse('OK') + def test_list_artist_should_not_return_artists_without_names(self): + self.backend.library.dummy_find_exact_result = [ + Track(artists=[Artist(name='')])] + + self.sendRequest('list "artist"') + self.assertNotInResponse('Artist: ') + self.assertInResponse('OK') + ### Album def test_list_album_with_quotes(self): @@ -232,6 +242,14 @@ class MusicDatabaseListTest(protocol.BaseTestCase): self.sendRequest('list "album" "artist" ""') self.assertInResponse('OK') + def test_list_album_should_not_return_albums_without_names(self): + self.backend.library.dummy_find_exact_result = [ + Track(album=Album(name=''))] + + self.sendRequest('list "album"') + self.assertNotInResponse('Album: ') + self.assertInResponse('OK') + ### Date def test_list_date_with_quotes(self): @@ -279,6 +297,13 @@ class MusicDatabaseListTest(protocol.BaseTestCase): self.sendRequest('list "date" "artist" ""') self.assertInResponse('OK') + def test_list_date_should_not_return_blank_dates(self): + self.backend.library.dummy_find_exact_result = [Track(date='')] + + self.sendRequest('list "date"') + self.assertNotInResponse('Date: ') + self.assertInResponse('OK') + ### Genre def test_list_genre_with_quotes(self): From dc24876f66dcf824d0a96b378764ff902f059b68 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 20 Nov 2012 00:44:15 +0100 Subject: [PATCH 29/39] mpd: Allow bad 'search' requests --- mopidy/frontends/mpd/protocol/music_db.py | 2 +- tests/frontends/mpd/protocol/music_db_test.py | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/mopidy/frontends/mpd/protocol/music_db.py b/mopidy/frontends/mpd/protocol/music_db.py index 8f41b199..00b9ec00 100644 --- a/mopidy/frontends/mpd/protocol/music_db.py +++ b/mopidy/frontends/mpd/protocol/music_db.py @@ -335,7 +335,7 @@ def rescan(context, uri=None): @handle_request( r'^search (?P("?([Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ile[name]*|' - r'[Tt]itle|[Aa]ny)"? "[^"]+"\s?)+)$') + r'[Tt]itle|[Aa]ny)"? "[^"]*"\s?)+)$') def search(context, mpd_query): """ *musicpd.org, music database section:* diff --git a/tests/frontends/mpd/protocol/music_db_test.py b/tests/frontends/mpd/protocol/music_db_test.py index 44999a4f..4539eb4c 100644 --- a/tests/frontends/mpd/protocol/music_db_test.py +++ b/tests/frontends/mpd/protocol/music_db_test.py @@ -362,6 +362,10 @@ class MusicDatabaseSearchTest(protocol.BaseTestCase): self.sendRequest('search album "analbum"') self.assertInResponse('OK') + def test_search_album_without_filter_value(self): + self.sendRequest('search "album" ""') + self.assertInResponse('OK') + def test_search_artist(self): self.sendRequest('search "artist" "anartist"') self.assertInResponse('OK') @@ -370,6 +374,10 @@ class MusicDatabaseSearchTest(protocol.BaseTestCase): self.sendRequest('search artist "anartist"') self.assertInResponse('OK') + def test_search_artist_without_filter_value(self): + self.sendRequest('search "artist" ""') + self.assertInResponse('OK') + def test_search_filename(self): self.sendRequest('search "filename" "afilename"') self.assertInResponse('OK') @@ -378,6 +386,10 @@ class MusicDatabaseSearchTest(protocol.BaseTestCase): self.sendRequest('search filename "afilename"') self.assertInResponse('OK') + def test_search_filename_without_filter_value(self): + self.sendRequest('search "filename" ""') + self.assertInResponse('OK') + def test_search_file(self): self.sendRequest('search "file" "afilename"') self.assertInResponse('OK') @@ -386,6 +398,10 @@ class MusicDatabaseSearchTest(protocol.BaseTestCase): self.sendRequest('search file "afilename"') self.assertInResponse('OK') + def test_search_file_without_filter_value(self): + self.sendRequest('search "file" ""') + self.assertInResponse('OK') + def test_search_title(self): self.sendRequest('search "title" "atitle"') self.assertInResponse('OK') @@ -394,6 +410,10 @@ class MusicDatabaseSearchTest(protocol.BaseTestCase): self.sendRequest('search title "atitle"') self.assertInResponse('OK') + def test_search_title_without_filter_value(self): + self.sendRequest('search "title" ""') + self.assertInResponse('OK') + def test_search_any(self): self.sendRequest('search "any" "anything"') self.assertInResponse('OK') @@ -402,6 +422,10 @@ class MusicDatabaseSearchTest(protocol.BaseTestCase): self.sendRequest('search any "anything"') self.assertInResponse('OK') + def test_search_any_without_filter_value(self): + self.sendRequest('search "any" ""') + self.assertInResponse('OK') + def test_search_date(self): self.sendRequest('search "date" "2002-01-01"') self.assertInResponse('OK') @@ -414,6 +438,10 @@ class MusicDatabaseSearchTest(protocol.BaseTestCase): self.sendRequest('search Date "2005"') self.assertInResponse('OK') + def test_search_date_without_filter_value(self): + self.sendRequest('search "date" ""') + self.assertInResponse('OK') + def test_search_else_should_fail(self): self.sendRequest('search "sometype" "something"') self.assertEqualResponse('ACK [2@0] {search} incorrect arguments') From 39b9429dfcd0e88ad3cd65a8a360662177bb39df Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 21 Nov 2012 01:03:53 +0100 Subject: [PATCH 30/39] tests: Use track URIs matching the backend in use --- .../mpd/protocol/stored_playlists_test.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/frontends/mpd/protocol/stored_playlists_test.py b/tests/frontends/mpd/protocol/stored_playlists_test.py index 414f0b25..be2afd4c 100644 --- a/tests/frontends/mpd/protocol/stored_playlists_test.py +++ b/tests/frontends/mpd/protocol/stored_playlists_test.py @@ -10,18 +10,18 @@ from tests.frontends.mpd import protocol class PlaylistsHandlerTest(protocol.BaseTestCase): def test_listplaylist(self): self.backend.playlists.playlists = [ - Playlist(name='name', tracks=[Track(uri='file:///dev/urandom')])] + Playlist(name='name', tracks=[Track(uri='dummy:a')])] self.sendRequest('listplaylist "name"') - self.assertInResponse('file: file:///dev/urandom') + self.assertInResponse('file: dummy:a') self.assertInResponse('OK') def test_listplaylist_without_quotes(self): self.backend.playlists.playlists = [ - Playlist(name='name', tracks=[Track(uri='file:///dev/urandom')])] + Playlist(name='name', tracks=[Track(uri='dummy:a')])] self.sendRequest('listplaylist name') - self.assertInResponse('file: file:///dev/urandom') + self.assertInResponse('file: dummy:a') self.assertInResponse('OK') def test_listplaylist_fails_if_no_playlist_is_found(self): @@ -30,20 +30,20 @@ class PlaylistsHandlerTest(protocol.BaseTestCase): def test_listplaylistinfo(self): self.backend.playlists.playlists = [ - Playlist(name='name', tracks=[Track(uri='file:///dev/urandom')])] + Playlist(name='name', tracks=[Track(uri='dummy:a')])] self.sendRequest('listplaylistinfo "name"') - self.assertInResponse('file: file:///dev/urandom') + self.assertInResponse('file: dummy:a') self.assertInResponse('Track: 0') self.assertNotInResponse('Pos: 0') self.assertInResponse('OK') def test_listplaylistinfo_without_quotes(self): self.backend.playlists.playlists = [ - Playlist(name='name', tracks=[Track(uri='file:///dev/urandom')])] + Playlist(name='name', tracks=[Track(uri='dummy:a')])] self.sendRequest('listplaylistinfo name') - self.assertInResponse('file: file:///dev/urandom') + self.assertInResponse('file: dummy:a') self.assertInResponse('Track: 0') self.assertNotInResponse('Pos: 0') self.assertInResponse('OK') @@ -96,7 +96,7 @@ class PlaylistsHandlerTest(protocol.BaseTestCase): self.assertEqualResponse('ACK [50@0] {load} No such playlist') def test_playlistadd(self): - self.sendRequest('playlistadd "name" "file:///dev/urandom"') + self.sendRequest('playlistadd "name" "dummy:a"') self.assertEqualResponse('ACK [0@0] {} Not implemented') def test_playlistclear(self): From 09d7279b6ba44326af7778ee004571daab48e8d4 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 21 Nov 2012 01:13:05 +0100 Subject: [PATCH 31/39] mpd: Compile protocol matching regexpes This caused a single test failure, which was fixed. --- mopidy/frontends/mpd/protocol/__init__.py | 5 +++-- mopidy/frontends/mpd/protocol/current_playlist.py | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/mopidy/frontends/mpd/protocol/__init__.py b/mopidy/frontends/mpd/protocol/__init__.py index 3a9f3674..ded65315 100644 --- a/mopidy/frontends/mpd/protocol/__init__.py +++ b/mopidy/frontends/mpd/protocol/__init__.py @@ -56,10 +56,11 @@ def handle_request(pattern, auth_required=True): if match is not None: mpd_commands.add( MpdCommand(name=match.group(), auth_required=auth_required)) - if pattern in request_handlers: + compiled_pattern = re.compile(pattern) + if compiled_pattern in request_handlers: raise ValueError('Tried to redefine handler for %s with %s' % ( pattern, func)) - request_handlers[pattern] = func + request_handlers[compiled_pattern] = func func.__doc__ = ' - *Pattern:* ``%s``\n\n%s' % ( pattern, func.__doc__ or '') return func diff --git a/mopidy/frontends/mpd/protocol/current_playlist.py b/mopidy/frontends/mpd/protocol/current_playlist.py index fbc92b46..f0d2e8f9 100644 --- a/mopidy/frontends/mpd/protocol/current_playlist.py +++ b/mopidy/frontends/mpd/protocol/current_playlist.py @@ -232,7 +232,6 @@ def playlistid(context, tlid=None): @handle_request(r'^playlistinfo$') -@handle_request(r'^playlistinfo "-1"$') @handle_request(r'^playlistinfo "(?P-?\d+)"$') @handle_request(r'^playlistinfo "(?P\d+):(?P\d+)*"$') def playlistinfo(context, songpos=None, start=None, end=None): @@ -250,6 +249,8 @@ def playlistinfo(context, songpos=None, start=None, end=None): - uses negative indexes, like ``playlistinfo "-1"``, to request the entire playlist """ + if songpos == '-1': + songpos = None if songpos is not None: songpos = int(songpos) tl_track = context.core.tracklist.tl_tracks.get()[songpos] From 0bc8fc6bf17e1a6db691241eb481aeb7a231a0b0 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 21 Nov 2012 01:14:12 +0100 Subject: [PATCH 32/39] mpd: Interpret regexp groups with unicode semantics Compiling the regexpes with either re.UNICODE or re.LOCALE both seems to fix the mystical failure of test_listplaylistinfo. --- mopidy/frontends/mpd/protocol/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/frontends/mpd/protocol/__init__.py b/mopidy/frontends/mpd/protocol/__init__.py index ded65315..a8bdc2c7 100644 --- a/mopidy/frontends/mpd/protocol/__init__.py +++ b/mopidy/frontends/mpd/protocol/__init__.py @@ -56,7 +56,7 @@ def handle_request(pattern, auth_required=True): if match is not None: mpd_commands.add( MpdCommand(name=match.group(), auth_required=auth_required)) - compiled_pattern = re.compile(pattern) + compiled_pattern = re.compile(pattern, flags=re.UNICODE) if compiled_pattern in request_handlers: raise ValueError('Tried to redefine handler for %s with %s' % ( pattern, func)) From 512b95fdb06e74ee49462bb0f50dcda3b50ea520 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 21 Nov 2012 01:18:19 +0100 Subject: [PATCH 33/39] docs: Update changelog with MPD search/find change --- docs/changes.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index 485ac0fd..583e5c46 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -79,6 +79,9 @@ long time been our most requested feature. Finally, it's here! - The MPD commands ``search`` and ``find`` now allows the key ``file``, which is used by ncmpcpp instead of ``filename``. +- The MPD commands ``search`` and ``find`` now allow search query values to be + empty strings. + - The MPD command ``listplaylists`` will no longer return playlists without a name. This could crash ncmpcpp. From 3af3eb5127d5e15c4b04d96c34e4e55c743de784 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 21 Nov 2012 01:29:14 +0100 Subject: [PATCH 34/39] mpd: Make 'decoders' return an empty response to please ncmpcpp --- docs/changes.rst | 4 ++++ mopidy/frontends/mpd/protocol/reflection.py | 10 ++++++++-- tests/frontends/mpd/protocol/reflection_test.py | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 583e5c46..d62faf8e 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -88,6 +88,10 @@ long time been our most requested feature. Finally, it's here! - The MPD command ``list`` will no longer return artist names, album names, or dates that are blank. +- The MPD command ``decoders`` will now return an empty response instead of a + "not implemented" error to make the ncmpcpp browse view work the first time + it is opened. + **MPRIS frontend** - The MPRIS playlists interface is now supported by our MPRIS frontend. This diff --git a/mopidy/frontends/mpd/protocol/reflection.py b/mopidy/frontends/mpd/protocol/reflection.py index 5af86a1a..d9c35743 100644 --- a/mopidy/frontends/mpd/protocol/reflection.py +++ b/mopidy/frontends/mpd/protocol/reflection.py @@ -1,7 +1,6 @@ from __future__ import unicode_literals from mopidy.frontends.mpd.protocol import handle_request, mpd_commands -from mopidy.frontends.mpd.exceptions import MpdNotImplemented @handle_request(r'^commands$', auth_required=False) @@ -47,8 +46,15 @@ def decoders(context): mime_type: audio/mpeg plugin: mpcdec suffix: mpc + + *Clarifications:* + + - ncmpcpp asks for decoders the first time you open the browse view. By + returning nothing and OK instead of an not implemented error, we avoid + "Not implemented" showing up in the ncmpcpp interface, and we get the + list of playlists without having to enter the browse interface twice. """ - raise MpdNotImplemented # TODO + return # TODO @handle_request(r'^notcommands$', auth_required=False) diff --git a/tests/frontends/mpd/protocol/reflection_test.py b/tests/frontends/mpd/protocol/reflection_test.py index 33032d73..9c07f104 100644 --- a/tests/frontends/mpd/protocol/reflection_test.py +++ b/tests/frontends/mpd/protocol/reflection_test.py @@ -38,7 +38,7 @@ class ReflectionHandlerTest(protocol.BaseTestCase): def test_decoders(self): self.sendRequest('decoders') - self.assertInResponse('ACK [0@0] {} Not implemented') + self.assertInResponse('OK') def test_notcommands_returns_only_kill_and_ok(self): response = self.sendRequest('notcommands') From 50708f9fd77b82460cd8543b1ab0cdbb13d630b5 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 21 Nov 2012 01:30:07 +0100 Subject: [PATCH 35/39] mpd: Change to interpret regexp groups with the old locale semantics --- mopidy/frontends/mpd/protocol/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/frontends/mpd/protocol/__init__.py b/mopidy/frontends/mpd/protocol/__init__.py index a8bdc2c7..df321d94 100644 --- a/mopidy/frontends/mpd/protocol/__init__.py +++ b/mopidy/frontends/mpd/protocol/__init__.py @@ -56,7 +56,7 @@ def handle_request(pattern, auth_required=True): if match is not None: mpd_commands.add( MpdCommand(name=match.group(), auth_required=auth_required)) - compiled_pattern = re.compile(pattern, flags=re.UNICODE) + compiled_pattern = re.compile(pattern, flags=re.LOCALE) if compiled_pattern in request_handlers: raise ValueError('Tried to redefine handler for %s with %s' % ( pattern, func)) From ab906c5684b9cb3977debbd58c1a8cac65e10ea4 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 21 Nov 2012 01:34:49 +0100 Subject: [PATCH 36/39] Revert "mpd: Change to interpret regexp groups with the old locale semantics" This reverts commit 50708f9fd77b82460cd8543b1ab0cdbb13d630b5. --- mopidy/frontends/mpd/protocol/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/frontends/mpd/protocol/__init__.py b/mopidy/frontends/mpd/protocol/__init__.py index df321d94..a8bdc2c7 100644 --- a/mopidy/frontends/mpd/protocol/__init__.py +++ b/mopidy/frontends/mpd/protocol/__init__.py @@ -56,7 +56,7 @@ def handle_request(pattern, auth_required=True): if match is not None: mpd_commands.add( MpdCommand(name=match.group(), auth_required=auth_required)) - compiled_pattern = re.compile(pattern, flags=re.LOCALE) + compiled_pattern = re.compile(pattern, flags=re.UNICODE) if compiled_pattern in request_handlers: raise ValueError('Tried to redefine handler for %s with %s' % ( pattern, func)) From bb32ff6b6bb422b74e9f851dfb50b3d5078cffb8 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 21 Nov 2012 01:35:21 +0100 Subject: [PATCH 37/39] mpd: Don't use the \S regexp group --- mopidy/frontends/mpd/protocol/current_playlist.py | 2 +- mopidy/frontends/mpd/protocol/stored_playlists.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mopidy/frontends/mpd/protocol/current_playlist.py b/mopidy/frontends/mpd/protocol/current_playlist.py index f0d2e8f9..d1b0e59a 100644 --- a/mopidy/frontends/mpd/protocol/current_playlist.py +++ b/mopidy/frontends/mpd/protocol/current_playlist.py @@ -270,7 +270,7 @@ def playlistinfo(context, songpos=None, start=None, end=None): @handle_request(r'^playlistsearch "(?P[^"]+)" "(?P[^"]+)"$') -@handle_request(r'^playlistsearch (?P\S+) "(?P[^"]+)"$') +@handle_request(r'^playlistsearch (?P\w+) "(?P[^"]+)"$') def playlistsearch(context, tag, needle): """ *musicpd.org, current playlist section:* diff --git a/mopidy/frontends/mpd/protocol/stored_playlists.py b/mopidy/frontends/mpd/protocol/stored_playlists.py index a7be2399..eef1f3d1 100644 --- a/mopidy/frontends/mpd/protocol/stored_playlists.py +++ b/mopidy/frontends/mpd/protocol/stored_playlists.py @@ -7,7 +7,7 @@ from mopidy.frontends.mpd.protocol import handle_request from mopidy.frontends.mpd.translator import playlist_to_mpd_format -@handle_request(r'^listplaylist (?P\S+)$') +@handle_request(r'^listplaylist (?P\w+)$') @handle_request(r'^listplaylist "(?P[^"]+)"$') def listplaylist(context, name): """ @@ -29,7 +29,7 @@ def listplaylist(context, name): return ['file: %s' % t.uri for t in playlists[0].tracks] -@handle_request(r'^listplaylistinfo (?P\S+)$') +@handle_request(r'^listplaylistinfo (?P\w+)$') @handle_request(r'^listplaylistinfo "(?P[^"]+)"$') def listplaylistinfo(context, name): """ From 88eef7de49937e0d9434a38f590db40cbb98ec1f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 21 Nov 2012 01:42:51 +0100 Subject: [PATCH 38/39] Bump version number to 0.9.0 --- mopidy/__init__.py | 2 +- tests/version_test.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/mopidy/__init__.py b/mopidy/__init__.py index 072a604c..918e1459 100644 --- a/mopidy/__init__.py +++ b/mopidy/__init__.py @@ -23,7 +23,7 @@ if (isinstance(pykka.__version__, basestring) warnings.filterwarnings('ignore', 'could not open display') -__version__ = '0.8.1' +__version__ = '0.9.0' from mopidy import settings as default_settings_module diff --git a/tests/version_test.py b/tests/version_test.py index 978660b0..966b8b94 100644 --- a/tests/version_test.py +++ b/tests/version_test.py @@ -30,5 +30,6 @@ class VersionTest(unittest.TestCase): self.assertLess(SV('0.7.1'), SV('0.7.2')) self.assertLess(SV('0.7.2'), SV('0.7.3')) self.assertLess(SV('0.7.3'), SV('0.8.0')) - self.assertLess(SV('0.8.0'), SV(__version__)) - self.assertLess(SV(__version__), SV('0.8.2')) + self.assertLess(SV('0.8.0'), SV('0.8.1')) + self.assertLess(SV('0.8.1'), SV(__version__)) + self.assertLess(SV(__version__), SV('0.9.1')) From a5d222dee31da2bdde60602dc8978b14234eed9b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 21 Nov 2012 01:43:16 +0100 Subject: [PATCH 39/39] Update changelog for v0.9.0 --- docs/changes.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index d62faf8e..64fe1ad6 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -5,8 +5,8 @@ Changes This change log is used to track all major changes to Mopidy. -v0.9.0 (in development) -======================= +v0.9.0 (2012-11-21) +=================== Support for using the local and Spotify backends simultaneously have for a very long time been our most requested feature. Finally, it's here!