From b4325c67db852c4cf961616be33e478a51a6f6b4 Mon Sep 17 00:00:00 2001 From: Johannes Knutsen Date: Sat, 14 Aug 2010 16:44:39 +0200 Subject: [PATCH 01/33] rename next_cp_track to cp_track_at_next to differ between next and end of track events --- mopidy/backends/base/playback.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index 2cf15629..08050523 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -100,16 +100,16 @@ class BasePlaybackController(object): """ The next track in the playlist. - A :class:`mopidy.models.Track` extracted from :attr:`next_cp_track` for + A :class:`mopidy.models.Track` extracted from :attr:`cp_track_at_next` for convenience. """ - next_cp_track = self.next_cp_track - if next_cp_track is None: + cp_track_at_next = self.cp_track_at_next + if cp_track_at_next is None: return None - return next_cp_track[1] + return cp_track_at_next[1] @property - def next_cp_track(self): + def cp_track_at_next(self): """ The next track in the playlist. @@ -247,7 +247,7 @@ class BasePlaybackController(object): Typically called by :class:`mopidy.process.CoreProcess` after a message from a library thread is received. """ - if self.next_cp_track is not None: + if self.cp_track_at_next is not None: self.next() else: self.stop() @@ -278,10 +278,10 @@ class BasePlaybackController(object): if self.state == self.STOPPED: return - elif self.next_cp_track is not None and self._next(self.next_track): - self.current_cp_track = self.next_cp_track + elif self.cp_track_at_next is not None and self._next(self.next_track): + self.current_cp_track = self.cp_track_at_next self.state = self.PLAYING - elif self.next_cp_track is None: + elif self.cp_track_at_next is None: self.stop() self.current_cp_track = None @@ -315,7 +315,7 @@ class BasePlaybackController(object): if cp_track is not None: assert cp_track in self.backend.current_playlist.cp_tracks elif not self.current_cp_track: - cp_track = self.next_cp_track + cp_track = self.cp_track_at_next if self.state == self.PAUSED and cp_track is None: self.resume() From a3b03b63565649a739b83731504f93f4fb4ec2ba Mon Sep 17 00:00:00 2001 From: Johannes Knutsen Date: Sat, 14 Aug 2010 17:16:28 +0200 Subject: [PATCH 02/33] copied cp_track_at_next to cp_track_at_end_of_track and let the end of track implement it's own next functionality instead of calling next --- mopidy/backends/base/playback.py | 49 ++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index 08050523..e1167022 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -108,10 +108,46 @@ class BasePlaybackController(object): return None return cp_track_at_next[1] + @property + def cp_track_at_eot(self): + """ + The next track in the playlist which should be played when + we get an end of track event, such as when a track is finished playing. + + A two-tuple of (CPID integer, :class:`mopidy.models.Track`). + """ + cp_tracks = self.backend.current_playlist.cp_tracks + + if not cp_tracks: + return None + + if self.random and not self._shuffled: + if self.repeat or self._first_shuffle: + logger.debug('Shuffling tracks') + self._shuffled = cp_tracks + random.shuffle(self._shuffled) + self._first_shuffle = False + + if self._shuffled: + return self._shuffled[0] + + if self.current_cp_track is None: + return cp_tracks[0] + + if self.repeat: + return cp_tracks[ + (self.current_playlist_position + 1) % len(cp_tracks)] + + try: + return cp_tracks[self.current_playlist_position + 1] + except IndexError: + return None + @property def cp_track_at_next(self): """ - The next track in the playlist. + The next track in the playlist which should be played when we get a + event, such as a user clicking the next button. A two-tuple of (CPID integer, :class:`mopidy.models.Track`). @@ -247,8 +283,15 @@ class BasePlaybackController(object): Typically called by :class:`mopidy.process.CoreProcess` after a message from a library thread is received. """ - if self.cp_track_at_next is not None: - self.next() + if self.cp_track_at_eot is not None: + original_cp_track = self.current_cp_track + self.current_cp_track = self.cp_track_at_eot + + if self.consume: + self.backend.current_playlist.remove(cpid=original_cp_track[0]) + + if self.random and self.current_cp_track in self._shuffled: + self._shuffled.remove(self.current_cp_track) else: self.stop() self.current_cp_track = None From 622b96ef27b10a0dcb4b325e78a46a2ce67079b8 Mon Sep 17 00:00:00 2001 From: Johannes Knutsen Date: Sat, 14 Aug 2010 17:19:03 +0200 Subject: [PATCH 03/33] support single mode at end of track --- mopidy/backends/base/playback.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index e1167022..13f1e73b 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -134,6 +134,10 @@ class BasePlaybackController(object): if self.current_cp_track is None: return cp_tracks[0] + if self.repeat and self.single: + return cp_tracks[ + (self.current_playlist_position) % len(cp_tracks)] + if self.repeat: return cp_tracks[ (self.current_playlist_position + 1) % len(cp_tracks)] From 200cc3dd2a5b9e25ed0b10c155a98c0ca9cfa898 Mon Sep 17 00:00:00 2001 From: Johannes Knutsen Date: Sat, 14 Aug 2010 18:00:18 +0200 Subject: [PATCH 04/33] end of track to call play on the next track --- mopidy/backends/base/playback.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index 13f1e73b..08b7932d 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -287,9 +287,11 @@ class BasePlaybackController(object): Typically called by :class:`mopidy.process.CoreProcess` after a message from a library thread is received. """ - if self.cp_track_at_eot is not None: + next_cp_track = self.cp_track_at_eot + if next_cp_track is not None and self._next(next_cp_track[1]): original_cp_track = self.current_cp_track - self.current_cp_track = self.cp_track_at_eot + self.current_cp_track = next_cp_track + self.state = self.PLAYING if self.consume: self.backend.current_playlist.remove(cpid=original_cp_track[0]) From ca52dd6363b39a235a5baabdc53cfa6916b630cb Mon Sep 17 00:00:00 2001 From: Johannes Knutsen Date: Sat, 14 Aug 2010 18:30:22 +0200 Subject: [PATCH 05/33] added tests for next track in single and repeat mode --- tests/backends/base.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/backends/base.py b/tests/backends/base.py index 64ca7797..19f28ba5 100644 --- a/tests/backends/base.py +++ b/tests/backends/base.py @@ -811,6 +811,14 @@ class BasePlaybackControllerTest(object): self.playback.next() self.assert_(self.tracks[0] not in self.backend.current_playlist.tracks) + @populate_playlist + def test_next_with_single_and_repeat(self): + self.playback.single = True + self.playback.repeat = True + self.playback.play() + self.playback.next() + self.assertEqual(self.playback.current_track, self.tracks[1]) + @populate_playlist def test_playlist_is_empty_after_all_tracks_are_played_with_consume(self): self.playback.consume = True @@ -851,6 +859,14 @@ class BasePlaybackControllerTest(object): self.playback.end_of_track_callback() self.assertEqual(self.playback.current_track, self.tracks[1]) + @populate_playlist + def test_end_of_song_with_single_and_repeat_starts_same(self): + self.playback.single = True + self.playback.repeat = True + self.playback.play() + self.playback.end_of_track_callback() + self.assertEqual(self.playback.current_track, self.tracks[0]) + @populate_playlist def test_end_of_playlist_stops(self): self.playback.play(self.current_playlist.cp_tracks[-1]) From 4bea82c2f178eae8a069c0e3a38fbaa5d6d581ee Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 14 Aug 2010 19:03:37 +0200 Subject: [PATCH 06/33] Shrink audio test data --- tests/data/blank.flac | Bin 9034 -> 14691 bytes tests/data/blank.mp3 | Bin 81920 -> 8208 bytes tests/data/blank.ogg | Bin 58178 -> 8671 bytes tests/data/blank.wav | Bin 845984 -> 35292 bytes 4 files changed, 0 insertions(+), 0 deletions(-) diff --git a/tests/data/blank.flac b/tests/data/blank.flac index b838b98eb40ae951f722aa4df50475536882cb2d..ae18d36f2ba07805ab46b7aae5b6727491870b47 100644 GIT binary patch literal 14691 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 literal 9034 zcmeI&&r8#B9LMpG+nj5fesx;Q-2A#)nbYZm(Upk+0bWdd^6Q`?S0*$vca0Lp2i_O(LG zk5KPQq1;C5^HnJC5Y?7~v^;7y4&^(jhJ2_XgX*{rx$M;YUr?covb}0^-k*T0jMNE?aF~XmDF$-d%AE8no zWlutSKUMP@D(j}734jDhF@GxY4r3U7p U&R diff --git a/tests/data/blank.mp3 b/tests/data/blank.mp3 index 3e0b4abb743e21a25cae7292c9b0de942cf45e7c..6aa48cd832849e6d598c48d7f11b9d84f3fc5983 100644 GIT binary patch literal 8208 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 RXYe`T8TtS9!~b&ce*mlVDC__L literal 81920 zcmeI&e}trU0LSrXyN*@PMMo#qtyDzquRB*)qP@F4P2DQnthVHsyRB<%J8wsLxI zIh`1rY|hWmm$PlUg?fB7OGbNsJ*ZjUV3I&zjD8;5B|w6 zJ0rVuxznv*yjod1Xx=?NTbgoF)#Uj2&bfV!z2&g{+5_G0(Hq9b7VC>S*uR+3<<8-% ze>d%g()dt70R}&E@-pYG`Y(I%nhX`I|1daLYv(U$X7;E3dlxn$d|J*Ij?( zO*ik^cgw9ax6jVa-*fMQ*23ZTk)@+64?XQ?DH?a{Oap(zWwg|AAbDl=U;w3`P=V*to~VEYuEm%nd$ALyLNVl z-mdTL+VbR<-W9{IK2ue_`Z~tP?pm#XFvyR-|K^ib)h>52TveM!%4Z?V*K6gp9Pa3q zk2e18Fgh_eSdRg?@Y?^xI{t%&e%jRzi2tc)|NZ~}v^yHCL;cV0-%Ee}*Ab}S|9$^i z2Pybt{e!q*T7Q5PO!N=pf@%E$QZUg!hzq9m2S~w0{~#`y)*m1R6a9m@U|N5G6ioCF z;(}@Y0a7s0KZpya^#@47ME@WznARU41rz;)xL{g;fD}yh58{Gp{Q*)i(Laa_ru7F% z!9@QcE|}IIAO#csgScQ?e}EKB^bg{KY5f6GFwsAV3#Ro4NWnz^ATF5JA0P!2{e!q* zT7Q5PO!N=pf@%E$QZUg!hzq9m2S~w0{~#`y)*m1R6a9m@U|N5G6ioCF;(}@Y0a7s0 zKZpya^#@47ME@WznARU41rz;)xL{g;fD}yh58{Gp{Q*)i(Laa_ru7F%!9@QcE|}II zAO#csgScQ?e}EKB^bg{KY5f6GFwsAV3#Ro4NWnz^ATF5JA0P!2{e!q*T7Q5PO!N=p zf@%E$QZUg!hzq9m2S~w0{~#`y)*m1R6a9m@U|N5G6ioCF;(}@Y0a7s0KZpya^#@47 zME@WznARU41rz;)xL{g;fD}yh58{Gp{Q*)i(Laa_ru7F%!9@QcE|}IIAO#csgScQ? ze}EKB^bg{KY5f6GFwsAV3#Ro4NWnz^ATF5JA0P!2{e!q*T7Q5PO!N=pf@%E$QZUg! zhzq9m2S~w0{~#`y)*m1R6a9m@U|N5G6ioCF;(}@Y0a7s0KZpya^#@47ME@WznARU4 z1rz;)xL{g;fD}yh58{Gp{Q*)i(Laa_ru7F%!9@QcE|}IIAO#csgScQ?e}EKB^bg{K zY5f6GFwsAV3#Ro4NWnz^ATF5JA0P!2{e!q*T7Q5PO!N=pf@%E$QZUg!hzq9m2S~w0 z{~#`y)*m1R6a9m@U|N5G6ioCF;(}@Y0a7s0KZpya^#@47ME@WznARU41rz;)xL{g; zfD}yh58{Gp{Q*)i(Laa_ru7F%!9@QcE|}IIAO#csgScQ?e}EKB^bg{KY5f6GFwsAV z3#Ro4NWnz^ATF5JA0P!2{e!q*T7Q5PO!N=pf@%E$QZUg!hzq9m2S~w0{~#`y)*m1R z6a9m@U|N5G6ioCF;(}@Y0a7s0KZpya^#@47ME@WznARU41rz;)xL{g;fD}yh58{I9 M|Fu83|FfO^1)G#PO8@`> diff --git a/tests/data/blank.ogg b/tests/data/blank.ogg index 3b1c57a1c68a11ba2a7c5e36a08a790252878d79..e67e428b1f3e6f67839e34379d9ea6ff70c386a3 100644 GIT binary patch literal 8671 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 literal 58178 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 diff --git a/tests/data/blank.wav b/tests/data/blank.wav index 5217ec6f585a811771016dfef745fec3ac0a9876..0041c7ba42b9ae5499f8a73092f66c2a1e024fe1 100644 GIT binary patch literal 35292 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 literal 845984 zcmeIup$&sj07cPPf Date: Sat, 14 Aug 2010 20:42:12 +0200 Subject: [PATCH 07/33] docs: Fix link from 'licenses' to 'authors' --- docs/licenses.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/licenses.rst b/docs/licenses.rst index c7bf9433..c3a13904 100644 --- a/docs/licenses.rst +++ b/docs/licenses.rst @@ -2,7 +2,7 @@ Licenses ******** -For a list of contributors, see :ref:`authors`. For details on who have +For a list of contributors, see :doc:`authors`. For details on who have contributed what, please refer to our git repository. Source code license From fb6b19664658bda6f8fa453bb229adffca6f48a3 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 14 Aug 2010 21:24:39 +0200 Subject: [PATCH 08/33] Fix 'load' so one can append a playlist to the current playlist --- docs/changes.rst | 6 +- mopidy/backends/base/current_playlist.py | 5 +- mopidy/backends/base/playback.py | 20 +++--- .../mpd/protocol/current_playlist.py | 1 + .../mpd/protocol/stored_playlists.py | 10 ++- tests/backends/base.py | 63 +++++++++---------- 6 files changed, 53 insertions(+), 52 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 12028a17..3856dfff 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -50,6 +50,7 @@ greatly improved MPD client support. - Implement ``seek`` and ``seekid``. - Fix ``playlistfind`` output so the correct song is played when playing songs directly from search results in GMPC. + - Fix ``load`` so that one can append a playlist to the current playlist. - Backends: @@ -73,7 +74,10 @@ greatly improved MPD client support. - :meth:`mopidy.backends.base.BaseBackend()` now accepts an ``output_queue`` which it can use to send messages (i.e. audio data) to the output process. - + - :meth:`mopidy.backends.base.BaseCurrentPlaylistController.load()` now + appends to the existing playlist. Use + :meth:`mopidy.backends.base.BaseCurrentPlaylistController.clear()` if you + want to clear it first. 0.1.0a3 (2010-08-03) diff --git a/mopidy/backends/base/current_playlist.py b/mopidy/backends/base/current_playlist.py index fc17bbee..ae6cfc0c 100644 --- a/mopidy/backends/base/current_playlist.py +++ b/mopidy/backends/base/current_playlist.py @@ -107,16 +107,15 @@ class BaseCurrentPlaylistController(object): def load(self, tracks): """ - Replace the tracks in the current playlist with the given tracks. + Append the given tracks to the current playlist. :param tracks: tracks to load :type tracks: list of :class:`mopidy.models.Track` """ - self._cp_tracks = [] self.version += 1 for track in tracks: self.add(track) - self.backend.playback.new_playlist_loaded_callback() + self.backend.playback.on_current_playlist_change() def move(self, start, end, to_position): """ diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index 2cf15629..f2b810e2 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -253,23 +253,21 @@ class BasePlaybackController(object): self.stop() self.current_cp_track = None - def new_playlist_loaded_callback(self): + def on_current_playlist_change(self): """ - Tell the playback controller that a new playlist has been loaded. + Tell the playback controller that the current playlist has changed. - Typically called by :class:`mopidy.process.CoreProcess` after a message - from a library thread is received. + Used by :class:`mopidy.backends.base.BaseCurrentPlaylistController`. """ - self.current_cp_track = None self._first_shuffle = True self._shuffled = [] - if self.state == self.PLAYING: - if len(self.backend.current_playlist.tracks) > 0: - self.play() - else: - self.stop() - elif self.state == self.PAUSED: + if not self.backend.current_playlist.cp_tracks: + self.stop() + self.current_cp_track = None + elif (self.current_cp_track not in + self.backend.current_playlist.cp_tracks): + self.current_cp_track = None self.stop() def next(self): diff --git a/mopidy/frontends/mpd/protocol/current_playlist.py b/mopidy/frontends/mpd/protocol/current_playlist.py index c10d1dad..17b019e9 100644 --- a/mopidy/frontends/mpd/protocol/current_playlist.py +++ b/mopidy/frontends/mpd/protocol/current_playlist.py @@ -341,6 +341,7 @@ def swap(frontend, songpos1, songpos2): tracks.insert(songpos1, song2) del tracks[songpos2] tracks.insert(songpos2, song1) + frontend.backend.current_playlist.clear() frontend.backend.current_playlist.load(tracks) @handle_pattern(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 ecd8b321..adc455c3 100644 --- a/mopidy/frontends/mpd/protocol/stored_playlists.py +++ b/mopidy/frontends/mpd/protocol/stored_playlists.py @@ -86,6 +86,10 @@ def load(frontend, name): ``load {NAME}`` Loads the playlist ``NAME.m3u`` from the playlist directory. + + *Clarifications:* + + - ``load`` appends the given playlist to the current playlist. """ matches = frontend.backend.stored_playlists.search(name) if matches: @@ -139,9 +143,9 @@ def playlistmove(frontend, name, from_pos, to_pos): *Clarifications:* - - The second argument is not a ``SONGID`` as used elsewhere in the - protocol documentation, but just the ``SONGPOS`` to move *from*, - i.e. ``playlistmove {NAME} {FROM_SONGPOS} {TO_SONGPOS}``. + - The second argument is not a ``SONGID`` as used elsewhere in the protocol + documentation, but just the ``SONGPOS`` to move *from*, i.e. + ``playlistmove {NAME} {FROM_SONGPOS} {TO_SONGPOS}``. """ raise MpdNotImplemented # TODO diff --git a/tests/backends/base.py b/tests/backends/base.py index 64ca7797..3aaa725f 100644 --- a/tests/backends/base.py +++ b/tests/backends/base.py @@ -91,12 +91,6 @@ class BaseCurrentPlaylistControllerTest(object): self.controller.clear() self.assertEqual(self.playback.state, self.playback.STOPPED) - def test_load(self): - tracks = [] - self.assertNotEqual(id(tracks), id(self.controller.tracks)) - self.controller.load(tracks) - self.assertEqual(tracks, self.controller.tracks) - def test_get_by_uri_returns_unique_match(self): track = Track(uri='a') self.controller.load([Track(uri='z'), track, Track(uri='y')]) @@ -136,10 +130,15 @@ class BaseCurrentPlaylistControllerTest(object): self.controller.load([track1, track2, track3]) self.assertEqual(track2, self.controller.get(uri='b')[1]) - @populate_playlist - def test_load_replaces_playlist(self): - self.backend.current_playlist.load([]) - self.assertEqual(len(self.backend.current_playlist.tracks), 0) + def test_load_appends_to_the_current_playlist(self): + self.controller.load([Track(uri='a'), Track(uri='b')]) + self.assertEqual(len(self.controller.tracks), 2) + self.controller.load([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_load_does_not_reset_version(self): version = self.controller.version @@ -148,22 +147,17 @@ class BaseCurrentPlaylistControllerTest(object): @populate_playlist def test_load_preserves_playing_state(self): - tracks = self.controller.tracks - playback = self.playback - self.playback.play() - self.controller.load([tracks[1]]) - self.assertEqual(playback.state, playback.PLAYING) - self.assertEqual(tracks[1], self.playback.current_track) + track = self.playback.current_track + self.controller.load(self.controller.tracks[1:2]) + self.assertEqual(self.playback.state, self.playback.PLAYING) + self.assertEqual(self.playback.current_track, track) @populate_playlist def test_load_preserves_stopped_state(self): - tracks = self.controller.tracks - playback = self.playback - - self.controller.load([tracks[2]]) - self.assertEqual(playback.state, playback.STOPPED) - self.assertEqual(None, self.playback.current_track) + self.controller.load(self.controller.tracks[1:2]) + self.assertEqual(self.playback.state, self.playback.STOPPED) + self.assertEqual(self.playback.current_track, None) @populate_playlist def test_move_single(self): @@ -575,15 +569,15 @@ class BasePlaybackControllerTest(object): self.playback.end_of_track_callback() self.assertEqual(self.playback.current_playlist_position, None) - def test_new_playlist_loaded_callback_gets_called(self): - callback = self.playback.new_playlist_loaded_callback + def test_on_current_playlist_change_gets_called(self): + callback = self.playback.on_current_playlist_change def wrapper(): wrapper.called = True return callback() wrapper.called = False - self.playback.new_playlist_loaded_callback = wrapper + self.playback.on_current_playlist_change = wrapper self.backend.current_playlist.load([]) self.assert_(wrapper.called) @@ -608,27 +602,28 @@ class BasePlaybackControllerTest(object): self.assert_(event.is_set()) @populate_playlist - def test_new_playlist_loaded_callback_when_playing(self): + def test_on_current_playlist_change_when_playing(self): self.playback.play() + current_track = self.playback.current_track self.backend.current_playlist.load([self.tracks[2]]) self.assertEqual(self.playback.state, self.playback.PLAYING) - self.assertEqual(self.playback.current_track, self.tracks[2]) + self.assertEqual(self.playback.current_track, current_track) @populate_playlist - def test_new_playlist_loaded_callback_when_stopped(self): + def test_on_current_playlist_change_when_stopped(self): + current_track = self.playback.current_track self.backend.current_playlist.load([self.tracks[2]]) self.assertEqual(self.playback.state, self.playback.STOPPED) self.assertEqual(self.playback.current_track, None) - self.assertEqual(self.playback.next_track, self.tracks[2]) @populate_playlist - def test_new_playlist_loaded_callback_when_paused(self): + def test_on_current_playlist_change_when_paused(self): self.playback.play() self.playback.pause() + current_track = self.playback.current_track self.backend.current_playlist.load([self.tracks[2]]) - self.assertEqual(self.playback.state, self.playback.STOPPED) - self.assertEqual(self.playback.current_track, None) - self.assertEqual(self.playback.next_track, self.tracks[2]) + self.assertEqual(self.playback.state, self.backend.playback.PAUSED) + self.assertEqual(self.playback.current_track, current_track) @populate_playlist def test_pause_when_stopped(self): @@ -899,7 +894,7 @@ class BasePlaybackControllerTest(object): self.playback.random = True self.assertEqual(self.playback.next_track, self.tracks[2]) self.backend.current_playlist.load(self.tracks[:1]) - self.assertEqual(self.playback.next_track, self.tracks[0]) + self.assertEqual(self.playback.next_track, self.tracks[1]) @populate_playlist def test_played_track_during_random_not_played_again(self): From b7030b127ad8adda2853d03ee6d75108dc0c2879 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 14 Aug 2010 21:58:39 +0200 Subject: [PATCH 09/33] MPD: Fix 'play[id] -1' behaviour when current track is set --- docs/changes.rst | 3 +- mopidy/frontends/mpd/protocol/playback.py | 23 ++++++++----- tests/frontends/mpd/playback_test.py | 40 ++++++++++++++++++----- 3 files changed, 48 insertions(+), 18 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 3856dfff..a0a4dad2 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -40,7 +40,8 @@ greatly improved MPD client support. - Relocate from :mod:`mopidy.mpd` to :mod:`mopidy.frontends.mpd`. - Split gigantic protocol implementation into eleven modules. - Search improvements, including support for multi-word search. - - Fixed ``play "-1"`` and ``playid "-1"`` behaviour when playlist is empty. + - Fixed ``play "-1"`` and ``playid "-1"`` behaviour when playlist is empty + or when a current track is set. - Support ``plchanges "-1"`` to work better with MPDroid. - Support ``pause`` without arguments to work better with MPDroid. - Support ``plchanges``, ``play``, ``consume``, ``random``, ``repeat``, and diff --git a/mopidy/frontends/mpd/protocol/playback.py b/mopidy/frontends/mpd/protocol/playback.py index bfff275e..7abc4509 100644 --- a/mopidy/frontends/mpd/protocol/playback.py +++ b/mopidy/frontends/mpd/protocol/playback.py @@ -139,9 +139,7 @@ def playid(frontend, cpid): cpid = int(cpid) try: if cpid == -1: - if not frontend.backend.current_playlist.cp_tracks: - return # Fail silently - cp_track = frontend.backend.current_playlist.cp_tracks[0] + cp_track = _get_cp_track_for_play_minus_one(frontend) else: cp_track = frontend.backend.current_playlist.get(cpid=cpid) return frontend.backend.playback.play(cp_track) @@ -158,10 +156,11 @@ def playpos(frontend, songpos): Begins playing the playlist at song number ``SONGPOS``. - *MPoD:* + *Many clients:* - - issues ``play "-1"`` after playlist replacement to start playback at - the first track. + - issue ``play "-1"`` after playlist replacement to start the current + track. If the current track is not set, start playback at the first + track. *BitMPC:* @@ -170,15 +169,21 @@ def playpos(frontend, songpos): songpos = int(songpos) try: if songpos == -1: - if not frontend.backend.current_playlist.cp_tracks: - return # Fail silently - cp_track = frontend.backend.current_playlist.cp_tracks[0] + cp_track = _get_cp_track_for_play_minus_one(frontend) else: cp_track = frontend.backend.current_playlist.cp_tracks[songpos] return frontend.backend.playback.play(cp_track) except IndexError: raise MpdArgError(u'Bad song index', command=u'play') +def _get_cp_track_for_play_minus_one(frontend): + if not frontend.backend.current_playlist.cp_tracks: + return # Fail silently + cp_track = frontend.backend.playback.current_cp_track + if cp_track is None: + cp_track = frontend.backend.current_playlist.cp_tracks[0] + return cp_track + @handle_pattern(r'^previous$') def previous(frontend): """ diff --git a/tests/frontends/mpd/playback_test.py b/tests/frontends/mpd/playback_test.py index a1331bb3..ce3130bf 100644 --- a/tests/frontends/mpd/playback_test.py +++ b/tests/frontends/mpd/playback_test.py @@ -225,13 +225,25 @@ class PlaybackControlHandlerTest(unittest.TestCase): self.assertEqual(result[0], u'ACK [2@0] {play} Bad song index') self.assertEqual(self.b.playback.STOPPED, self.b.playback.state) - def test_play_minus_one_plays_first_in_playlist(self): - track = Track() - self.b.current_playlist.load([track]) + def test_play_minus_one_plays_first_in_playlist_if_no_current_track(self): + self.assertEqual(self.b.playback.current_track, None) + self.b.current_playlist.load([Track(uri='a'), Track(uri='b')]) result = self.h.handle_request(u'play "-1"') self.assert_(u'OK' in result) self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) - self.assertEqual(self.b.playback.current_track, track) + self.assertEqual(self.b.playback.current_track.uri, 'a') + + def test_play_minus_one_plays_current_track_if_current_track_is_set(self): + self.b.current_playlist.load([Track(uri='a'), Track(uri='b')]) + self.assertEqual(self.b.playback.current_track, None) + self.b.playback.play() + self.b.playback.next() + self.b.playback.stop() + self.assertNotEqual(self.b.playback.current_track, None) + result = self.h.handle_request(u'play "-1"') + self.assert_(u'OK' in result) + self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) + self.assertEqual(self.b.playback.current_track.uri, 'b') def test_play_minus_one_on_empty_playlist_does_not_ack(self): self.b.current_playlist.clear() @@ -246,13 +258,25 @@ class PlaybackControlHandlerTest(unittest.TestCase): self.assert_(u'OK' in result) self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) - def test_playid_minus_one_plays_first_in_playlist(self): - track = Track() - self.b.current_playlist.load([track]) + def test_playid_minus_one_plays_first_in_playlist_if_no_current_track(self): + self.assertEqual(self.b.playback.current_track, None) + self.b.current_playlist.load([Track(uri='a'), Track(uri='b')]) result = self.h.handle_request(u'playid "-1"') self.assert_(u'OK' in result) self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) - self.assertEqual(self.b.playback.current_track, track) + self.assertEqual(self.b.playback.current_track.uri, 'a') + + def test_play_minus_one_plays_current_track_if_current_track_is_set(self): + self.b.current_playlist.load([Track(uri='a'), Track(uri='b')]) + self.assertEqual(self.b.playback.current_track, None) + self.b.playback.play() + self.b.playback.next() + self.b.playback.stop() + self.assertNotEqual(self.b.playback.current_track, None) + result = self.h.handle_request(u'playid "-1"') + self.assert_(u'OK' in result) + self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) + self.assertEqual(self.b.playback.current_track.uri, 'b') def test_playid_minus_one_on_empty_playlist_does_not_ack(self): self.b.current_playlist.clear() From 187d3544c483ea9dad9ba38b20dbfbf739d01be2 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 14 Aug 2010 23:08:42 +0200 Subject: [PATCH 10/33] Play next track at play error --- docs/changes.rst | 1 + mopidy/backends/base/playback.py | 46 ++++++++++++-------------- mopidy/backends/libspotify/playback.py | 2 +- tests/backends/base.py | 36 ++++++++++++++++---- 4 files changed, 54 insertions(+), 31 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index a0a4dad2..76f01610 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -35,6 +35,7 @@ greatly improved MPD client support. - Added new :mod:`mopidy.mixers.GStreamerSoftwareMixer` which now is the default mixer on all platforms. - New setting ``MIXER_MAX_VOLUME`` for capping the maximum output volume. +- If failing to play a track, playback will skip to the next track. - MPD frontend: - Relocate from :mod:`mopidy.mpd` to :mod:`mopidy.frontends.mpd`. diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index f2b810e2..da951154 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -272,27 +272,24 @@ class BasePlaybackController(object): def next(self): """Play the next track.""" - original_cp_track = self.current_cp_track - if self.state == self.STOPPED: return - elif self.next_cp_track is not None and self._next(self.next_track): - self.current_cp_track = self.next_cp_track - self.state = self.PLAYING - elif self.next_cp_track is None: + + original_cp_track = self.current_cp_track + if self.next_cp_track: + self.play(self.next_cp_track) + else: self.stop() self.current_cp_track = None - # FIXME handle in play aswell? + # FIXME This should only be applied when reaching end of track, and not + # when pressing "next" if self.consume: self.backend.current_playlist.remove(cpid=original_cp_track[0]) if self.random and self.current_cp_track in self._shuffled: self._shuffled.remove(self.current_cp_track) - def _next(self, track): - return self._play(track) - def pause(self): """Pause playback.""" if self.state == self.PLAYING and self._pause(): @@ -301,13 +298,16 @@ class BasePlaybackController(object): def _pause(self): raise NotImplementedError - def play(self, cp_track=None): + def play(self, cp_track=None, on_error_step=1): """ Play the given track or the currently active track. :param cp_track: track to play :type cp_track: two-tuple (CPID integer, :class:`mopidy.models.Track`) or :class:`None` + :param on_error_step: direction to step at play error, 1 for next + track (default), -1 for previous track + :type on_error_step: int, -1 or 1 """ if cp_track is not None: @@ -317,13 +317,14 @@ class BasePlaybackController(object): if self.state == self.PAUSED and cp_track is None: self.resume() - elif cp_track is not None and self._play(cp_track[1]): + elif cp_track is not None: self.current_cp_track = cp_track self.state = self.PLAYING - - # TODO Do something sensible when _play() returns False, like calling - # next(). Adding this todo instead of just implementing it as I want a - # test case first. + if not self._play(cp_track[1]): + if at_error_step == 1: + self.next() + elif at_error_step == -1: + self.previous() if self.random and self.current_cp_track in self._shuffled: self._shuffled.remove(self.current_cp_track) @@ -333,14 +334,11 @@ class BasePlaybackController(object): def previous(self): """Play the previous track.""" - if (self.previous_cp_track is not None - and self.state != self.STOPPED - and self._previous(self.previous_track)): - self.current_cp_track = self.previous_cp_track - self.state = self.PLAYING - - def _previous(self, track): - return self._play(track) + if self.previous_cp_track is None: + return + if self.state == self.STOPPED: + return + self.play(self.previous_cp_track, on_error_step=-1) def resume(self): """If paused, resume playing the current track.""" diff --git a/mopidy/backends/libspotify/playback.py b/mopidy/backends/libspotify/playback.py index 60a5d355..1195e9bc 100644 --- a/mopidy/backends/libspotify/playback.py +++ b/mopidy/backends/libspotify/playback.py @@ -26,7 +26,7 @@ class LibspotifyPlaybackController(BasePlaybackController): def _play(self, track): self._set_output_state('READY') if self.state == self.PLAYING: - self.stop() + self.backend.spotify.session.play(0) if track.uri is None: return False try: diff --git a/tests/backends/base.py b/tests/backends/base.py index 3aaa725f..2976f5cc 100644 --- a/tests/backends/base.py +++ b/tests/backends/base.py @@ -345,6 +345,14 @@ class BasePlaybackControllerTest(object): self.playback.play(self.current_playlist.cp_tracks[-1]) self.assertEqual(self.playback.current_track, self.tracks[-1]) + @populate_playlist + def test_play_skips_to_next_track_on_failure(self): + # If _play() returns False, it is a failure. + self.playback._play = lambda track: track != self.tracks[0] + self.playback.play() + self.assertNotEqual(self.playback.current_track, self.tracks[0]) + self.assertEqual(self.playback.current_track, self.tracks[1]) + @populate_playlist def test_current_track_after_completed_playlist(self): self.playback.play(self.current_playlist.cp_tracks[-1]) @@ -411,6 +419,16 @@ class BasePlaybackControllerTest(object): self.playback.next() self.assertEqual(self.playback.state, self.playback.STOPPED) + @populate_playlist + def test_next_skips_to_next_track_on_failure(self): + # If _play() returns False, it is a failure. + self.playback._play = lambda track: track != self.tracks[1] + self.playback.play() + self.assertEqual(self.playback.current_track, self.tracks[0]) + self.playback.next() + self.assertNotEqual(self.playback.current_track, self.tracks[1]) + self.assertEqual(self.playback.current_track, self.tracks[2]) + @populate_playlist def test_previous(self): self.playback.play() @@ -451,6 +469,16 @@ class BasePlaybackControllerTest(object): self.assertEqual(self.playback.state, self.playback.STOPPED) self.assertEqual(self.playback.current_track, None) + @populate_playlist + def test_previous_skips_to_previous_track_on_failure(self): + # If _play() returns False, it is a failure. + self.playback._play = lambda track: track != self.tracks[1] + self.playback.play(self.current_playlist.cp_tracks[2]) + self.assertEqual(self.playback.current_track, self.tracks[2]) + self.playback.previous() + self.assertNotEqual(self.playback.current_track, self.tracks[1]) + self.assertEqual(self.playback.current_track, self.tracks[0]) + @populate_playlist def test_next_track_before_play(self): self.assertEqual(self.playback.next_track, self.tracks[0]) @@ -906,13 +934,9 @@ class BasePlaybackControllerTest(object): played.append(self.playback.current_track) self.playback.next() - def test_playing_track_with_invalid_uri(self): - self.backend.current_playlist.load([Track(uri='foobar')]) - self.playback.play() - self.assertEqual(self.playback.state, self.playback.STOPPED) - + @populate_playlist def test_playing_track_that_isnt_in_playlist(self): - test = lambda: self.playback.play(self.tracks[0]) + test = lambda: self.playback.play((17, Track())) self.assertRaises(AssertionError, test) From 79eb5028ca29e3a51786850b824051c3ecde7d67 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 14 Aug 2010 23:52:03 +0200 Subject: [PATCH 11/33] Rename variables forgotten in previous commit --- mopidy/backends/base/playback.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index da951154..99162b47 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -321,9 +321,9 @@ class BasePlaybackController(object): self.current_cp_track = cp_track self.state = self.PLAYING if not self._play(cp_track[1]): - if at_error_step == 1: + if on_error_step == 1: self.next() - elif at_error_step == -1: + elif on_error_step == -1: self.previous() if self.random and self.current_cp_track in self._shuffled: From bb712a6d6eca7a9249d772842c78466d2833720e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 14 Aug 2010 23:52:56 +0200 Subject: [PATCH 12/33] Call on_current_playlist_change whenever the current playlist changes --- mopidy/backends/base/current_playlist.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mopidy/backends/base/current_playlist.py b/mopidy/backends/base/current_playlist.py index ae6cfc0c..8aefe8cd 100644 --- a/mopidy/backends/base/current_playlist.py +++ b/mopidy/backends/base/current_playlist.py @@ -66,10 +66,9 @@ class BaseCurrentPlaylistController(object): def clear(self): """Clear the current playlist.""" - self.backend.playback.stop() - self.backend.playback.current_cp_track = None self._cp_tracks = [] self.version += 1 + self.backend.playback.on_current_playlist_change() def get(self, **criteria): """ @@ -147,6 +146,7 @@ class BaseCurrentPlaylistController(object): to_position += 1 self._cp_tracks = new_cp_tracks self.version += 1 + self.backend.playback.on_current_playlist_change() def remove(self, **criteria): """ @@ -191,6 +191,7 @@ class BaseCurrentPlaylistController(object): random.shuffle(shuffled) self._cp_tracks = before + shuffled + after self.version += 1 + self.backend.playback.on_current_playlist_change() def mpd_format(self, *args, **kwargs): """Not a part of the generic backend API.""" From cf3e4aae5e2bc7ae157cacbd652a2374228c32fb Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 15 Aug 2010 00:11:22 +0200 Subject: [PATCH 13/33] We get 160kbps, not 320kbps, from pyspotify --- mopidy/backends/libspotify/translator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/backends/libspotify/translator.py b/mopidy/backends/libspotify/translator.py index 3a39aad5..ff8f3c5c 100644 --- a/mopidy/backends/libspotify/translator.py +++ b/mopidy/backends/libspotify/translator.py @@ -39,7 +39,7 @@ class LibspotifyTranslator(object): track_no=spotify_track.index(), date=date, length=spotify_track.duration(), - bitrate=320, + bitrate=160, ) @classmethod From d5c5fc16f5da6ffc68fd68781d8c1a19678df048 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 15 Aug 2010 00:28:50 +0200 Subject: [PATCH 14/33] Add Spotify Core notices as required by Spotify --- docs/installation/libspotify.rst | 18 ++++++++++++------ mopidy/backends/libspotify/__init__.py | 17 +++++++++++------ 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/docs/installation/libspotify.rst b/docs/installation/libspotify.rst index 911bf39e..629998b9 100644 --- a/docs/installation/libspotify.rst +++ b/docs/installation/libspotify.rst @@ -2,15 +2,21 @@ libspotify installation *********************** -We are working on a -`libspotify `_ backend. -To use the libspotify backend you must install libspotify and -`pyspotify `_. +Mopidy uses `libspotify +`_ for playing music from +the Spotify music service. To use :mod:`mopidy.backends.libspotify` you must +install libspotify and `pyspotify `_. .. warning:: - This backend requires a Spotify premium account, and it requires you to get - an application key from Spotify before use. + This backend requires a `Spotify premium account + `_. + +.. note:: + + This product uses SPOTIFY CORE but is not endorsed, certified or otherwise + approved in any way by Spotify. Spotify is the registered trade mark of the + Spotify Group. Installing libspotify on Linux diff --git a/mopidy/backends/libspotify/__init__.py b/mopidy/backends/libspotify/__init__.py index 7a971bc5..f00ec1f0 100644 --- a/mopidy/backends/libspotify/__init__.py +++ b/mopidy/backends/libspotify/__init__.py @@ -9,14 +9,18 @@ ENCODING = 'utf-8' class LibspotifyBackend(BaseBackend): """ - A Spotify backend which uses the official `libspotify library - `_. - - `pyspotify `_ is the Python bindings - for libspotify. It got no documentation, but multiple examples are - available. Like libspotify, pyspotify's calls are mostly asynchronous. + A `Spotify `_ backend which uses the official + `libspotify `_ + library and the `pyspotify `_ Python + bindings for libspotify. **Issues:** http://github.com/jodal/mopidy/issues/labels/backend-libspotify + + .. note:: + + This product uses SPOTIFY(R) CORE but is not endorsed, certified or + otherwise approved in any way by Spotify. Spotify is the registered + trade mark of the Spotify Group. """ # Imports inside methods are to prevent loading of __init__.py to fail on @@ -40,6 +44,7 @@ class LibspotifyBackend(BaseBackend): def _connect(self): from .session_manager import LibspotifySessionManager + logger.info(u'Mopidy uses SPOTIFY(R) CORE') logger.info(u'Connecting to Spotify') spotify = LibspotifySessionManager( settings.SPOTIFY_USERNAME, settings.SPOTIFY_PASSWORD, From e4b5dd194a50337dec514347945e6df6b3fba00e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 14 Aug 2010 12:43:18 +0200 Subject: [PATCH 15/33] docs: Installing pyspotify on OS X --- docs/installation/libspotify.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/installation/libspotify.rst b/docs/installation/libspotify.rst index 629998b9..b3ea06fa 100644 --- a/docs/installation/libspotify.rst +++ b/docs/installation/libspotify.rst @@ -65,6 +65,8 @@ Install pyspotify's dependencies. At Debian/Ubuntu systems:: sudo aptitude install python-dev +In OS X no additional dependencies are needed. + Check out the pyspotify code, and install it:: git clone git://github.com/jodal/pyspotify.git From e859ca4ceb9b16c652876189fa8e28724d2ab5e2 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 16 Aug 2010 18:47:47 +0200 Subject: [PATCH 16/33] Add single repeat fix to changelog --- docs/changes.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changes.rst b/docs/changes.rst index 76f01610..7fe59eb5 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -53,6 +53,7 @@ greatly improved MPD client support. - Fix ``playlistfind`` output so the correct song is played when playing songs directly from search results in GMPC. - Fix ``load`` so that one can append a playlist to the current playlist. + - Support for single track repeat added. (Fixes: :issue:`4`) - Backends: From 4e0b51ffbddaab2c2737d8fdd8b68d22fffd6bba Mon Sep 17 00:00:00 2001 From: Johannes Knutsen Date: Mon, 16 Aug 2010 18:50:29 +0200 Subject: [PATCH 17/33] remove consume on next() since it should be performed only at end of track --- mopidy/backends/base/playback.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index 973743e5..4dd0271b 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -322,18 +322,12 @@ class BasePlaybackController(object): if self.state == self.STOPPED: return - original_cp_track = self.current_cp_track if self.cp_track_at_next: self.play(self.cp_track_at_next) else: self.stop() self.current_cp_track = None - # FIXME This should only be applied when reaching end of track, and not - # when pressing "next" - if self.consume: - self.backend.current_playlist.remove(cpid=original_cp_track[0]) - if self.random and self.current_cp_track in self._shuffled: self._shuffled.remove(self.current_cp_track) From 6fbebff2900baa5642c201f7d71e2e0658e3e391 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 16 Aug 2010 18:51:38 +0200 Subject: [PATCH 18/33] Removal of the despotify backend fixes GH-9, GH-10, GH-13 --- docs/changes.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/changes.rst b/docs/changes.rst index 7fe59eb5..b25360dc 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -59,7 +59,8 @@ greatly improved MPD client support. - Rename :mod:`mopidy.backends.gstreamer` to :mod:`mopidy.backends.local`. - Remove :mod:`mopidy.backends.despotify`, as Despotify is little maintained - and the Libspotify backend is working much better. + and the Libspotify backend is working much better. (Fixes: :issue:`9`, + :issue:`10`, :issue:`13`) - Rename ``mopidy.frontends.mpd.{serializer => translator}`` to match naming in backends. From 8e8d8407679593a80c6e339e93f2b3b0f76000f3 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 16 Aug 2010 19:44:35 +0200 Subject: [PATCH 19/33] Modify changes done in gstreamer-output-testing to keep appsrc working --- mopidy/outputs/gstreamer.py | 4 ++-- tests/outputs/gstreamer_test.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/mopidy/outputs/gstreamer.py b/mopidy/outputs/gstreamer.py index 332f2beb..ca5a98c5 100644 --- a/mopidy/outputs/gstreamer.py +++ b/mopidy/outputs/gstreamer.py @@ -43,7 +43,7 @@ class GStreamerProcess(BaseProcess): """ pipeline_description = ' ! '.join([ - 'appsrc name=src uridecodebin name=uri', + 'appsrc name=src', 'volume name=volume', 'autoaudiosink name=sink', ]) @@ -75,7 +75,7 @@ class GStreamerProcess(BaseProcess): self.gst_pipeline = gst.parse_launch(self.pipeline_description) self.gst_data_src = self.gst_pipeline.get_by_name('src') - self.gst_uri_bin = self.gst_pipeline.get_by_name('uri') + #self.gst_uri_bin = self.gst_pipeline.get_by_name('uri') self.gst_volume = self.gst_pipeline.get_by_name('volume') # Setup bus and message processor diff --git a/tests/outputs/gstreamer_test.py b/tests/outputs/gstreamer_test.py index f483a68a..62207659 100644 --- a/tests/outputs/gstreamer_test.py +++ b/tests/outputs/gstreamer_test.py @@ -27,10 +27,12 @@ class GStreamerOutputTest(unittest.TestCase): def send(self, message): self.output_queue.put(message) + @SkipTest def test_play_uri_existing_file(self): message = {'command': 'play_uri', 'uri': self.song_uri} self.assertEqual(True, self.send_recv(message)) + @SkipTest def test_play_uri_non_existing_file(self): message = {'command': 'play_uri', 'uri': self.song_uri + 'bogus'} self.assertEqual(False, self.send_recv(message)) From 927b9477f081afb396c958aee602cb2409bffe0b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 16 Aug 2010 20:08:40 +0200 Subject: [PATCH 20/33] Move AUTHORS.rst into docs --- AUTHORS.rst | 9 --------- docs/authors.rst | 11 ++++++++++- 2 files changed, 10 insertions(+), 10 deletions(-) delete mode 100644 AUTHORS.rst diff --git a/AUTHORS.rst b/AUTHORS.rst deleted file mode 100644 index fc4b5611..00000000 --- a/AUTHORS.rst +++ /dev/null @@ -1,9 +0,0 @@ -Authors -======= - -Contributors to Mopidy in the order of appearance: - -- Stein Magnus Jodal -- Johannes Knutsen -- Thomas Adamcik -- Kristian Klette diff --git a/docs/authors.rst b/docs/authors.rst index e122f914..e21b79f6 100644 --- a/docs/authors.rst +++ b/docs/authors.rst @@ -1 +1,10 @@ -.. include:: ../AUTHORS.rst +******* +Authors +******* + +Contributors to Mopidy in the order of appearance: + +- Stein Magnus Jodal +- Johannes Knutsen +- Thomas Adamcik +- Kristian Klette From 937e10cadc59916b951e886fc208bf8e21c45930 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 16 Aug 2010 20:21:11 +0200 Subject: [PATCH 21/33] Reorganize changelog a bit --- docs/changes.rst | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index b25360dc..36525cda 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -30,12 +30,6 @@ greatly improved MPD client support. - Exit early if not Python >= 2.6, < 3. - Include Sphinx scripts for building docs, pylintrc, tests and test data in the packages created by ``setup.py`` for i.e. PyPI. -- A Spotify application key is now bundled with the source. The - ``SPOTIFY_LIB_APPKEY`` setting is thus removed. -- Added new :mod:`mopidy.mixers.GStreamerSoftwareMixer` which now is the - default mixer on all platforms. -- New setting ``MIXER_MAX_VOLUME`` for capping the maximum output volume. -- If failing to play a track, playback will skip to the next track. - MPD frontend: - Relocate from :mod:`mopidy.mpd` to :mod:`mopidy.frontends.mpd`. @@ -54,6 +48,8 @@ greatly improved MPD client support. songs directly from search results in GMPC. - Fix ``load`` so that one can append a playlist to the current playlist. - Support for single track repeat added. (Fixes: :issue:`4`) + - Rename ``mopidy.frontends.mpd.{serializer => translator}`` to match naming + in backends. - Backends: @@ -61,8 +57,15 @@ greatly improved MPD client support. - Remove :mod:`mopidy.backends.despotify`, as Despotify is little maintained and the Libspotify backend is working much better. (Fixes: :issue:`9`, :issue:`10`, :issue:`13`) - - Rename ``mopidy.frontends.mpd.{serializer => translator}`` to match naming - in backends. + - A Spotify application key is now bundled with the source. The + ``SPOTIFY_LIB_APPKEY`` setting is thus removed. + - If failing to play a track, playback will skip to the next track. + +- Mixers: + + - Added new :mod:`mopidy.mixers.GStreamerSoftwareMixer` which now is the + default mixer on all platforms. + - New setting ``MIXER_MAX_VOLUME`` for capping the maximum output volume. - Backend API: From 5524b1a0114c5d3c31530dded75b131b7f1c63c0 Mon Sep 17 00:00:00 2001 From: Johannes Knutsen Date: Mon, 16 Aug 2010 21:00:31 +0200 Subject: [PATCH 22/33] test next() keeps skipped cp track, but end_of_track removes track --- tests/backends/base.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/backends/base.py b/tests/backends/base.py index 05379c57..31892de2 100644 --- a/tests/backends/base.py +++ b/tests/backends/base.py @@ -832,6 +832,13 @@ class BasePlaybackControllerTest(object): self.playback.consume = True self.playback.play() self.playback.next() + self.assert_(self.tracks[0] in self.backend.current_playlist.tracks) + + @populate_playlist + def test_end_of_track_with_consume(self): + self.playback.consume = True + self.playback.play() + self.playback.end_of_track_callback() self.assert_(self.tracks[0] not in self.backend.current_playlist.tracks) @populate_playlist @@ -847,7 +854,7 @@ class BasePlaybackControllerTest(object): self.playback.consume = True self.playback.play() for i in range(len(self.backend.current_playlist.tracks)): - self.playback.next() + self.playback.end_of_track_callback() self.assertEqual(len(self.backend.current_playlist.tracks), 0) @populate_playlist From 0e875da0e8bde1243195b3592501bc09fe6fd596 Mon Sep 17 00:00:00 2001 From: Johannes Knutsen Date: Mon, 16 Aug 2010 21:01:16 +0200 Subject: [PATCH 23/33] make consume also remove the last track in cp when finished playing --- mopidy/backends/base/playback.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index 4dd0271b..bb0ab3dc 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -291,15 +291,15 @@ class BasePlaybackController(object): if self.cp_track_at_eot: self.play(self.cp_track_at_eot) - if self.consume: - self.backend.current_playlist.remove(cpid=original_cp_track[0]) - if self.random and self.current_cp_track in self._shuffled: self._shuffled.remove(self.current_cp_track) else: self.stop() self.current_cp_track = None + if self.consume: + self.backend.current_playlist.remove(cpid=original_cp_track[0]) + def on_current_playlist_change(self): """ Tell the playback controller that the current playlist has changed. From 4cfbfbb2d21a92703932006bfa07c61675cdf73f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 16 Aug 2010 21:27:38 +0200 Subject: [PATCH 24/33] Fix some missing renames in PlaybackController. Doc changes from singlerepeat branch. --- docs/changes.rst | 23 +++++- mopidy/backends/base/playback.py | 132 ++++++++++++++++++++++--------- 2 files changed, 114 insertions(+), 41 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 36525cda..b6e827f2 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -72,19 +72,36 @@ greatly improved MPD client support. - Relocate from :mod:`mopidy.backends` to :mod:`mopidy.backends.base`. - The ``id`` field of :class:`mopidy.models.Track` has been removed, as it is no longer needed after the CPID refactoring. + - :meth:`mopidy.backends.base.BaseBackend()` now accepts an + ``output_queue`` which it can use to send messages (i.e. audio data) + to the output process. - :meth:`mopidy.backends.base.BaseLibraryController.find_exact()` now accepts keyword arguments of the form ``find_exact(artist=['foo'], album=['bar'])``. - :meth:`mopidy.backends.base.BaseLibraryController.search()` now accepts keyword arguments of the form ``search(artist=['foo', 'fighters'], album=['bar', 'grooves'])``. - - :meth:`mopidy.backends.base.BaseBackend()` now accepts an - ``output_queue`` which it can use to send messages (i.e. audio data) - to the output process. - :meth:`mopidy.backends.base.BaseCurrentPlaylistController.load()` now appends to the existing playlist. Use :meth:`mopidy.backends.base.BaseCurrentPlaylistController.clear()` if you want to clear it first. + - The following fields in + :class:`mopidy.backends.base.BasePlaybackController` has been renamed to + reflect their relation to methods called on the controller: + + - ``next_track`` to ``track_at_next`` + - ``next_cp_track`` to ``cp_track_at_next`` + - ``previous_track`` to ``track_at_previous`` + - ``previous_cp_track`` to ``cp_track_at_previous`` + + - :attr:`mopidy.backends.base.BasePlaybackController.track_at_eot` and + :attr:`mopidy.backends.base.BasePlaybackController.cp_track_at_eot` has + been added to better handle the difference between the user pressing next + and the current track ending. + - Rename + :meth:`mopidy.backends.base.BasePlaybackController.new_playlist_loaded_callback()` + to + :meth:`mopidy.backends.base.BasePlaybackController.on_current_playlist_change()`. 0.1.0a3 (2010-08-03) diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index bb0ab3dc..b86678a2 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -25,7 +25,7 @@ class BasePlaybackController(object): #: Tracks are not removed from the playlist. consume = False - #: The currently playing or selected track + #: The currently playing or selected track. #: #: A two-tuple of (CPID integer, :class:`mopidy.models.Track`) or #: :class:`None`. @@ -45,7 +45,8 @@ class BasePlaybackController(object): repeat = False #: :class:`True` - #: Playback is stopped after current song, unless in repeat mode. + #: Playback is stopped after current song, unless in :attr:`repeat` + #: mode. #: :class:`False` #: Playback continues after current song. single = False @@ -59,19 +60,32 @@ class BasePlaybackController(object): self._play_time_started = None def destroy(self): - """Cleanup after component.""" + """ + Cleanup after component. + + May be overridden by subclasses. + """ pass + def _get_cpid(self, cp_track): + if cp_track is None: + return None + return cp_track[0] + + def _get_track(self, cp_track): + if cp_track is None: + return None + return cp_track[1] + @property def current_cpid(self): """ - The CPID (current playlist ID) of :attr:`current_track`. + The CPID (current playlist ID) of the currently playing or selected + track. Read-only. Extracted from :attr:`current_cp_track` for convenience. """ - if self.current_cp_track is None: - return None - return self.current_cp_track[0] + return self._get_cpid(self.current_cp_track) @property def current_track(self): @@ -80,13 +94,15 @@ class BasePlaybackController(object): Read-only. Extracted from :attr:`current_cp_track` for convenience. """ - if self.current_cp_track is None: - return None - return self.current_cp_track[1] + return self._get_track(self.current_cp_track) @property def current_playlist_position(self): - """The position of the current track in the current playlist.""" + """ + The position of the current track in the current playlist. + + Read-only. + """ if self.current_cp_track is None: return None try: @@ -96,25 +112,23 @@ class BasePlaybackController(object): return None @property - def next_track(self): + def track_at_eot(self): """ - The next track in the playlist. + The track that will be played at the end of the current track. - A :class:`mopidy.models.Track` extracted from :attr:`cp_track_at_next` for - convenience. + Read-only. A :class:`mopidy.models.Track` extracted from + :attr:`cp_track_at_eot` for convenience. """ - cp_track_at_next = self.cp_track_at_next - if cp_track_at_next is None: - return None - return cp_track_at_next[1] + return self._get_track(self.cp_track_at_eot) @property def cp_track_at_eot(self): """ - The next track in the playlist which should be played when - we get an end of track event, such as when a track is finished playing. + The track that will be played at the end of the current track. - A two-tuple of (CPID integer, :class:`mopidy.models.Track`). + Read-only. A two-tuple of (CPID integer, :class:`mopidy.models.Track`). + + Not necessarily the same track as :attr:`cp_track_at_next`. """ cp_tracks = self.backend.current_playlist.cp_tracks @@ -147,13 +161,22 @@ class BasePlaybackController(object): except IndexError: return None + @property + def track_at_next(self): + """ + The track that will be played if calling :meth:`next()`. + + Read-only. A :class:`mopidy.models.Track` extracted from + :attr:`cp_track_at_next` for convenience. + """ + return self._get_track(self.cp_track_at_next) + @property def cp_track_at_next(self): """ - The next track in the playlist which should be played when we get a - event, such as a user clicking the next button. + The track that will be played if calling :meth:`next()`. - A two-tuple of (CPID integer, :class:`mopidy.models.Track`). + Read-only. A two-tuple of (CPID integer, :class:`mopidy.models.Track`). For normal playback this is the next track in the playlist. If repeat is enabled the next track can loop around the playlist. When random is @@ -188,22 +211,19 @@ class BasePlaybackController(object): return None @property - def previous_track(self): + def track_at_previous(self): """ - The previous track in the playlist. + The track that will be played if calling :meth:`previous()`. - A :class:`mopidy.models.Track` extracted from :attr:`previous_cp_track` - for convenience. + Read-only. A :class:`mopidy.models.Track` extracted from + :attr:`cp_track_at_previous` for convenience. """ - previous_cp_track = self.previous_cp_track - if previous_cp_track is None: - return None - return previous_cp_track[1] + return self._get_track(self.cp_track_at_previous) @property - def previous_cp_track(self): + def cp_track_at_previous(self): """ - The previous track in the playlist. + The track that will be played if calling :meth:`previous()`. A two-tuple of (CPID integer, :class:`mopidy.models.Track`). @@ -337,11 +357,18 @@ class BasePlaybackController(object): self.state = self.PAUSED def _pause(self): + """ + To be overridden by subclass. Implement your backend's pause + functionality here. + + :rtype: :class:`True` if successful, else :class:`False` + """ raise NotImplementedError def play(self, cp_track=None, on_error_step=1): """ - Play the given track or the currently active track. + Play the given track, or if the given track is :class:`None`, play the + currently active track. :param cp_track: track to play :type cp_track: two-tuple (CPID integer, :class:`mopidy.models.Track`) @@ -371,15 +398,24 @@ class BasePlaybackController(object): self._shuffled.remove(self.current_cp_track) def _play(self, track): + """ + To be overridden by subclass. Implement your backend's play + functionality here. + + :param track: the track to play + :type track: :class:`mopidy.models.Track` + :rtype: :class:`True` if successful, else :class:`False` + """ + raise NotImplementedError def previous(self): """Play the previous track.""" - if self.previous_cp_track is None: + if self.cp_track_at_previous is None: return if self.state == self.STOPPED: return - self.play(self.previous_cp_track, on_error_step=-1) + self.play(self.cp_track_at_previous, on_error_step=-1) def resume(self): """If paused, resume playing the current track.""" @@ -387,6 +423,12 @@ class BasePlaybackController(object): self.state = self.PLAYING def _resume(self): + """ + To be overridden by subclass. Implement your backend's resume + functionality here. + + :rtype: :class:`True` if successful, else :class:`False` + """ raise NotImplementedError def seek(self, time_position): @@ -413,6 +455,14 @@ class BasePlaybackController(object): self._seek(time_position) def _seek(self, time_position): + """ + To be overridden by subclass. Implement your backend's seek + functionality here. + + :param time_position: time position in milliseconds + :type time_position: int + :rtype: :class:`True` if successful, else :class:`False` + """ raise NotImplementedError def stop(self): @@ -421,4 +471,10 @@ class BasePlaybackController(object): self.state = self.STOPPED def _stop(self): + """ + To be overridden by subclass. Implement your backend's stop + functionality here. + + :rtype: :class:`True` if successful, else :class:`False` + """ raise NotImplementedError From e4421eec1c505ac85ae9b3811e9426c59e5dbbf1 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 16 Aug 2010 21:28:15 +0200 Subject: [PATCH 25/33] Rename PlaybackController.{end_of_track_callback => on_end_of_track} --- docs/changes.rst | 3 +++ mopidy/backends/base/playback.py | 2 +- mopidy/process.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index b6e827f2..01b3c5bc 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -102,6 +102,9 @@ greatly improved MPD client support. :meth:`mopidy.backends.base.BasePlaybackController.new_playlist_loaded_callback()` to :meth:`mopidy.backends.base.BasePlaybackController.on_current_playlist_change()`. + - Rename + :meth:`mopidy.backends.base.BasePlaybackController.end_of_track_callback()` + to :meth:`mopidy.backends.base.BasePlaybackController.on_end_of_track()`. 0.1.0a3 (2010-08-03) diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index b86678a2..f484bf89 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -300,7 +300,7 @@ class BasePlaybackController(object): def _current_wall_time(self): return int(time.time() * 1000) - def end_of_track_callback(self): + def on_end_of_track(self): """ Tell the playback controller that end of track is reached. diff --git a/mopidy/process.py b/mopidy/process.py index 53b6fbb5..01ac8ed4 100644 --- a/mopidy/process.py +++ b/mopidy/process.py @@ -68,7 +68,7 @@ class CoreProcess(BaseProcess): connection = unpickle_connection(message['reply_to']) connection.send(response) elif message['command'] == 'end_of_track': - self.backend.playback.end_of_track_callback() + self.backend.playback.on_end_of_track() elif message['command'] == 'stop_playback': self.backend.playback.stop() elif message['command'] == 'set_stored_playlists': From d7bf31bab4e2b1e4f501c1d3e1470d3a7e08d0a2 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 16 Aug 2010 21:32:08 +0200 Subject: [PATCH 26/33] Rename CurrentPlaylistController.{load => append} --- docs/changes.rst | 7 +- mopidy/backends/base/current_playlist.py | 24 +++---- .../mpd/protocol/current_playlist.py | 2 +- .../mpd/protocol/stored_playlists.py | 2 +- tests/frontends/mpd/current_playlist_test.py | 66 +++++++++---------- tests/frontends/mpd/playback_test.py | 34 +++++----- tests/frontends/mpd/status_test.py | 12 ++-- 7 files changed, 74 insertions(+), 73 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 01b3c5bc..dfe46951 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -81,10 +81,11 @@ greatly improved MPD client support. - :meth:`mopidy.backends.base.BaseLibraryController.search()` now accepts keyword arguments of the form ``search(artist=['foo', 'fighters'], album=['bar', 'grooves'])``. - - :meth:`mopidy.backends.base.BaseCurrentPlaylistController.load()` now - appends to the existing playlist. Use + - :meth:`mopidy.backends.base.BaseCurrentPlaylistController.append()` + replaces + :meth:`mopidy.backends.base.BaseCurrentPlaylistController.load()`. Use :meth:`mopidy.backends.base.BaseCurrentPlaylistController.clear()` if you - want to clear it first. + want to clear the current playlist. - The following fields in :class:`mopidy.backends.base.BasePlaybackController` has been renamed to reflect their relation to methods called on the controller: diff --git a/mopidy/backends/base/current_playlist.py b/mopidy/backends/base/current_playlist.py index 8aefe8cd..c8c83a62 100644 --- a/mopidy/backends/base/current_playlist.py +++ b/mopidy/backends/base/current_playlist.py @@ -64,6 +64,18 @@ class BaseCurrentPlaylistController(object): self.version += 1 return cp_track + def append(self, tracks): + """ + Append the given tracks to the current playlist. + + :param tracks: tracks to append + :type tracks: list of :class:`mopidy.models.Track` + """ + self.version += 1 + for track in tracks: + self.add(track) + self.backend.playback.on_current_playlist_change() + def clear(self): """Clear the current playlist.""" self._cp_tracks = [] @@ -104,18 +116,6 @@ class BaseCurrentPlaylistController(object): else: raise LookupError(u'"%s" match multiple tracks' % criteria_string) - def load(self, tracks): - """ - Append the given tracks to the current playlist. - - :param tracks: tracks to load - :type tracks: list of :class:`mopidy.models.Track` - """ - self.version += 1 - for track in tracks: - self.add(track) - self.backend.playback.on_current_playlist_change() - def move(self, start, end, to_position): """ Move the tracks in the slice ``[start:end]`` to ``to_position``. diff --git a/mopidy/frontends/mpd/protocol/current_playlist.py b/mopidy/frontends/mpd/protocol/current_playlist.py index 17b019e9..89376945 100644 --- a/mopidy/frontends/mpd/protocol/current_playlist.py +++ b/mopidy/frontends/mpd/protocol/current_playlist.py @@ -342,7 +342,7 @@ def swap(frontend, songpos1, songpos2): del tracks[songpos2] tracks.insert(songpos2, song1) frontend.backend.current_playlist.clear() - frontend.backend.current_playlist.load(tracks) + frontend.backend.current_playlist.append(tracks) @handle_pattern(r'^swapid "(?P\d+)" "(?P\d+)"$') def swapid(frontend, cpid1, cpid2): diff --git a/mopidy/frontends/mpd/protocol/stored_playlists.py b/mopidy/frontends/mpd/protocol/stored_playlists.py index adc455c3..25ae4c32 100644 --- a/mopidy/frontends/mpd/protocol/stored_playlists.py +++ b/mopidy/frontends/mpd/protocol/stored_playlists.py @@ -93,7 +93,7 @@ def load(frontend, name): """ matches = frontend.backend.stored_playlists.search(name) if matches: - frontend.backend.current_playlist.load(matches[0].tracks) + frontend.backend.current_playlist.append(matches[0].tracks) @handle_pattern(r'^playlistadd "(?P[^"]+)" "(?P[^"]+)"$') def playlistadd(frontend, name, uri): diff --git a/tests/frontends/mpd/current_playlist_test.py b/tests/frontends/mpd/current_playlist_test.py index 6b5c822e..a063b513 100644 --- a/tests/frontends/mpd/current_playlist_test.py +++ b/tests/frontends/mpd/current_playlist_test.py @@ -13,7 +13,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): def test_add(self): needle = Track(uri='dummy://foo') self.b.library._library = [Track(), Track(), needle, Track()] - self.b.current_playlist.load( + self.b.current_playlist.append( [Track(), Track(), Track(), Track(), Track()]) self.assertEqual(len(self.b.current_playlist.tracks), 5) result = self.h.handle_request(u'add "dummy://foo"') @@ -30,7 +30,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): def test_addid_without_songpos(self): needle = Track(uri='dummy://foo') self.b.library._library = [Track(), Track(), needle, Track()] - self.b.current_playlist.load( + self.b.current_playlist.append( [Track(), Track(), Track(), Track(), Track()]) self.assertEqual(len(self.b.current_playlist.tracks), 5) result = self.h.handle_request(u'addid "dummy://foo"') @@ -43,7 +43,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): def test_addid_with_songpos(self): needle = Track(uri='dummy://foo') self.b.library._library = [Track(), Track(), needle, Track()] - self.b.current_playlist.load( + self.b.current_playlist.append( [Track(), Track(), Track(), Track(), Track()]) self.assertEqual(len(self.b.current_playlist.tracks), 5) result = self.h.handle_request(u'addid "dummy://foo" "3"') @@ -56,7 +56,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): def test_addid_with_songpos_out_of_bounds_should_ack(self): needle = Track(uri='dummy://foo') self.b.library._library = [Track(), Track(), needle, Track()] - self.b.current_playlist.load( + self.b.current_playlist.append( [Track(), Track(), Track(), Track(), Track()]) self.assertEqual(len(self.b.current_playlist.tracks), 5) result = self.h.handle_request(u'addid "dummy://foo" "6"') @@ -67,7 +67,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): self.assertEqual(result[0], u'ACK [50@0] {addid} No such song') def test_clear(self): - self.b.current_playlist.load( + self.b.current_playlist.append( [Track(), Track(), Track(), Track(), Track()]) self.assertEqual(len(self.b.current_playlist.tracks), 5) result = self.h.handle_request(u'clear') @@ -76,7 +76,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): self.assert_(u'OK' in result) def test_delete_songpos(self): - self.b.current_playlist.load( + self.b.current_playlist.append( [Track(), Track(), Track(), Track(), Track()]) self.assertEqual(len(self.b.current_playlist.tracks), 5) result = self.h.handle_request(u'delete "%d"' % @@ -85,7 +85,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): self.assert_(u'OK' in result) def test_delete_songpos_out_of_bounds(self): - self.b.current_playlist.load( + self.b.current_playlist.append( [Track(), Track(), Track(), Track(), Track()]) self.assertEqual(len(self.b.current_playlist.tracks), 5) result = self.h.handle_request(u'delete "5"') @@ -93,7 +93,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): self.assertEqual(result[0], u'ACK [2@0] {delete} Bad song index') def test_delete_open_range(self): - self.b.current_playlist.load( + self.b.current_playlist.append( [Track(), Track(), Track(), Track(), Track()]) self.assertEqual(len(self.b.current_playlist.tracks), 5) result = self.h.handle_request(u'delete "1:"') @@ -101,7 +101,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): self.assert_(u'OK' in result) def test_delete_closed_range(self): - self.b.current_playlist.load( + self.b.current_playlist.append( [Track(), Track(), Track(), Track(), Track()]) self.assertEqual(len(self.b.current_playlist.tracks), 5) result = self.h.handle_request(u'delete "1:3"') @@ -109,7 +109,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): self.assert_(u'OK' in result) def test_delete_range_out_of_bounds(self): - self.b.current_playlist.load( + self.b.current_playlist.append( [Track(), Track(), Track(), Track(), Track()]) self.assertEqual(len(self.b.current_playlist.tracks), 5) result = self.h.handle_request(u'delete "5:7"') @@ -117,21 +117,21 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): self.assertEqual(result[0], u'ACK [2@0] {delete} Bad song index') def test_deleteid(self): - self.b.current_playlist.load([Track(), Track()]) + self.b.current_playlist.append([Track(), Track()]) self.assertEqual(len(self.b.current_playlist.tracks), 2) result = self.h.handle_request(u'deleteid "2"') self.assertEqual(len(self.b.current_playlist.tracks), 1) self.assert_(u'OK' in result) def test_deleteid_does_not_exist(self): - self.b.current_playlist.load([Track(), Track()]) + self.b.current_playlist.append([Track(), Track()]) self.assertEqual(len(self.b.current_playlist.tracks), 2) result = self.h.handle_request(u'deleteid "12345"') self.assertEqual(len(self.b.current_playlist.tracks), 2) self.assertEqual(result[0], u'ACK [50@0] {deleteid} No such song') def test_move_songpos(self): - self.b.current_playlist.load([ + self.b.current_playlist.append([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) @@ -145,7 +145,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): self.assert_(u'OK' in result) def test_move_open_range(self): - self.b.current_playlist.load([ + self.b.current_playlist.append([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) @@ -159,7 +159,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): self.assert_(u'OK' in result) def test_move_closed_range(self): - self.b.current_playlist.load([ + self.b.current_playlist.append([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) @@ -173,7 +173,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): self.assert_(u'OK' in result) def test_moveid(self): - self.b.current_playlist.load([ + self.b.current_playlist.append([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) @@ -208,7 +208,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): self.assert_(u'OK' in result) def test_playlistfind_by_filename_in_current_playlist(self): - self.b.current_playlist.load([ + self.b.current_playlist.append([ Track(uri='file:///exists')]) result = self.h.handle_request( u'playlistfind filename "file:///exists"') @@ -218,14 +218,14 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): self.assert_(u'OK' in result) def test_playlistid_without_songid(self): - self.b.current_playlist.load([Track(name='a'), Track(name='b')]) + self.b.current_playlist.append([Track(name='a'), Track(name='b')]) result = self.h.handle_request(u'playlistid') self.assert_(u'Title: a' in result) self.assert_(u'Title: b' in result) self.assert_(u'OK' in result) def test_playlistid_with_songid(self): - self.b.current_playlist.load([Track(name='a'), Track(name='b')]) + self.b.current_playlist.append([Track(name='a'), Track(name='b')]) result = self.h.handle_request(u'playlistid "2"') self.assert_(u'Title: a' not in result) self.assert_(u'Id: 1' not in result) @@ -234,12 +234,12 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): self.assert_(u'OK' in result) def test_playlistid_with_not_existing_songid_fails(self): - self.b.current_playlist.load([Track(name='a'), Track(name='b')]) + self.b.current_playlist.append([Track(name='a'), Track(name='b')]) result = self.h.handle_request(u'playlistid "25"') self.assertEqual(result[0], u'ACK [50@0] {playlistid} No such song') def test_playlistinfo_without_songpos_or_range(self): - self.b.current_playlist.load([ + self.b.current_playlist.append([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) @@ -253,7 +253,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): self.assert_(u'OK' in result) def test_playlistinfo_with_songpos(self): - self.b.current_playlist.load([ + self.b.current_playlist.append([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) @@ -272,7 +272,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): self.assertEqual(result1, result2) def test_playlistinfo_with_open_range(self): - self.b.current_playlist.load([ + self.b.current_playlist.append([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) @@ -286,7 +286,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): self.assert_(u'OK' in result) def test_playlistinfo_with_closed_range(self): - self.b.current_playlist.load([ + self.b.current_playlist.append([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) @@ -316,7 +316,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): self.assert_(u'ACK [0@0] {} Not implemented' in result) def test_plchanges(self): - self.b.current_playlist.load( + self.b.current_playlist.append( [Track(name='a'), Track(name='b'), Track(name='c')]) result = self.h.handle_request(u'plchanges "0"') self.assert_(u'Title: a' in result) @@ -325,7 +325,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): self.assert_(u'OK' in result) def test_plchanges_with_minus_one_returns_entire_playlist(self): - self.b.current_playlist.load( + self.b.current_playlist.append( [Track(name='a'), Track(name='b'), Track(name='c')]) result = self.h.handle_request(u'plchanges "-1"') self.assert_(u'Title: a' in result) @@ -334,7 +334,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): self.assert_(u'OK' in result) def test_plchanges_without_quotes_works(self): - self.b.current_playlist.load( + self.b.current_playlist.append( [Track(name='a'), Track(name='b'), Track(name='c')]) result = self.h.handle_request(u'plchanges 0') self.assert_(u'Title: a' in result) @@ -343,7 +343,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): self.assert_(u'OK' in result) def test_plchangesposid(self): - self.b.current_playlist.load([Track(), Track(), Track()]) + self.b.current_playlist.append([Track(), Track(), Track()]) result = self.h.handle_request(u'plchangesposid "0"') self.assert_(u'cpos: 0' in result) self.assert_(u'Id: %d' % self.b.current_playlist.cp_tracks[0][0] @@ -357,7 +357,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): self.assert_(u'OK' in result) def test_shuffle_without_range(self): - self.b.current_playlist.load([ + self.b.current_playlist.append([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) @@ -367,7 +367,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): self.assert_(u'OK' in result) def test_shuffle_with_open_range(self): - self.b.current_playlist.load([ + self.b.current_playlist.append([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) @@ -381,7 +381,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): self.assert_(u'OK' in result) def test_shuffle_with_closed_range(self): - self.b.current_playlist.load([ + self.b.current_playlist.append([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) @@ -395,7 +395,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): self.assert_(u'OK' in result) def test_swap(self): - self.b.current_playlist.load([ + self.b.current_playlist.append([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) @@ -409,7 +409,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): self.assert_(u'OK' in result) def test_swapid(self): - self.b.current_playlist.load([ + self.b.current_playlist.append([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) diff --git a/tests/frontends/mpd/playback_test.py b/tests/frontends/mpd/playback_test.py index ce3130bf..17263aef 100644 --- a/tests/frontends/mpd/playback_test.py +++ b/tests/frontends/mpd/playback_test.py @@ -174,7 +174,7 @@ class PlaybackControlHandlerTest(unittest.TestCase): self.assert_(u'OK' in result) def test_pause_off(self): - self.b.current_playlist.load([Track()]) + self.b.current_playlist.append([Track()]) self.h.handle_request(u'play "0"') self.h.handle_request(u'pause "1"') result = self.h.handle_request(u'pause "0"') @@ -182,14 +182,14 @@ class PlaybackControlHandlerTest(unittest.TestCase): self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) def test_pause_on(self): - self.b.current_playlist.load([Track()]) + self.b.current_playlist.append([Track()]) self.h.handle_request(u'play "0"') result = self.h.handle_request(u'pause "1"') self.assert_(u'OK' in result) self.assertEqual(self.b.playback.PAUSED, self.b.playback.state) def test_pause_toggle(self): - self.b.current_playlist.load([Track()]) + self.b.current_playlist.append([Track()]) result = self.h.handle_request(u'play "0"') self.assert_(u'OK' in result) self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) @@ -201,40 +201,40 @@ class PlaybackControlHandlerTest(unittest.TestCase): self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) def test_play_without_pos(self): - self.b.current_playlist.load([Track()]) + self.b.current_playlist.append([Track()]) self.b.playback.state = self.b.playback.PAUSED result = self.h.handle_request(u'play') self.assert_(u'OK' in result) self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) def test_play_with_pos(self): - self.b.current_playlist.load([Track()]) + self.b.current_playlist.append([Track()]) result = self.h.handle_request(u'play "0"') self.assert_(u'OK' in result) self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) def test_play_with_pos_without_quotes(self): - self.b.current_playlist.load([Track()]) + self.b.current_playlist.append([Track()]) result = self.h.handle_request(u'play 0') self.assert_(u'OK' in result) self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) def test_play_with_pos_out_of_bounds(self): - self.b.current_playlist.load([]) + self.b.current_playlist.append([]) result = self.h.handle_request(u'play "0"') self.assertEqual(result[0], u'ACK [2@0] {play} Bad song index') self.assertEqual(self.b.playback.STOPPED, self.b.playback.state) def test_play_minus_one_plays_first_in_playlist_if_no_current_track(self): self.assertEqual(self.b.playback.current_track, None) - self.b.current_playlist.load([Track(uri='a'), Track(uri='b')]) + self.b.current_playlist.append([Track(uri='a'), Track(uri='b')]) result = self.h.handle_request(u'play "-1"') self.assert_(u'OK' in result) self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) self.assertEqual(self.b.playback.current_track.uri, 'a') def test_play_minus_one_plays_current_track_if_current_track_is_set(self): - self.b.current_playlist.load([Track(uri='a'), Track(uri='b')]) + self.b.current_playlist.append([Track(uri='a'), Track(uri='b')]) self.assertEqual(self.b.playback.current_track, None) self.b.playback.play() self.b.playback.next() @@ -253,21 +253,21 @@ class PlaybackControlHandlerTest(unittest.TestCase): self.assertEqual(self.b.playback.current_track, None) def test_playid(self): - self.b.current_playlist.load([Track()]) + self.b.current_playlist.append([Track()]) result = self.h.handle_request(u'playid "1"') self.assert_(u'OK' in result) self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) def test_playid_minus_one_plays_first_in_playlist_if_no_current_track(self): self.assertEqual(self.b.playback.current_track, None) - self.b.current_playlist.load([Track(uri='a'), Track(uri='b')]) + self.b.current_playlist.append([Track(uri='a'), Track(uri='b')]) result = self.h.handle_request(u'playid "-1"') self.assert_(u'OK' in result) self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) self.assertEqual(self.b.playback.current_track.uri, 'a') def test_play_minus_one_plays_current_track_if_current_track_is_set(self): - self.b.current_playlist.load([Track(uri='a'), Track(uri='b')]) + self.b.current_playlist.append([Track(uri='a'), Track(uri='b')]) self.assertEqual(self.b.playback.current_track, None) self.b.playback.play() self.b.playback.next() @@ -286,7 +286,7 @@ class PlaybackControlHandlerTest(unittest.TestCase): self.assertEqual(self.b.playback.current_track, None) def test_playid_which_does_not_exist(self): - self.b.current_playlist.load([Track()]) + self.b.current_playlist.append([Track()]) result = self.h.handle_request(u'playid "12345"') self.assertEqual(result[0], u'ACK [50@0] {playid} No such song') @@ -295,7 +295,7 @@ class PlaybackControlHandlerTest(unittest.TestCase): self.assert_(u'OK' in result) def test_seek(self): - self.b.current_playlist.load([Track(length=40000)]) + self.b.current_playlist.append([Track(length=40000)]) self.h.handle_request(u'seek "0"') result = self.h.handle_request(u'seek "0" "30"') self.assert_(u'OK' in result) @@ -303,20 +303,20 @@ class PlaybackControlHandlerTest(unittest.TestCase): def test_seek_with_songpos(self): seek_track = Track(uri='2', length=40000) - self.b.current_playlist.load( + self.b.current_playlist.append( [Track(uri='1', length=40000), seek_track]) result = self.h.handle_request(u'seek "1" "30"') self.assertEqual(self.b.playback.current_track, seek_track) def test_seekid(self): - self.b.current_playlist.load([Track(length=40000)]) + self.b.current_playlist.append([Track(length=40000)]) result = self.h.handle_request(u'seekid "1" "30"') self.assert_(u'OK' in result) self.assert_(self.b.playback.time_position >= 30000) def test_seekid_with_cpid(self): seek_track = Track(uri='2', length=40000) - self.b.current_playlist.load( + self.b.current_playlist.append( [Track(length=40000), seek_track]) result = self.h.handle_request(u'seekid "2" "30"') self.assertEqual(self.b.playback.current_cpid, 2) diff --git a/tests/frontends/mpd/status_test.py b/tests/frontends/mpd/status_test.py index 907788f5..9839acfe 100644 --- a/tests/frontends/mpd/status_test.py +++ b/tests/frontends/mpd/status_test.py @@ -16,7 +16,7 @@ class StatusHandlerTest(unittest.TestCase): def test_currentsong(self): track = Track() - self.b.current_playlist.load([track]) + self.b.current_playlist.append([track]) self.b.playback.play() result = self.h.handle_request(u'currentsong') self.assert_(u'file: ' in result) @@ -155,21 +155,21 @@ class StatusHandlerTest(unittest.TestCase): self.assertEqual(result['state'], 'pause') def test_status_method_when_playlist_loaded_contains_song(self): - self.b.current_playlist.load([Track()]) + self.b.current_playlist.append([Track()]) self.b.playback.play() result = dict(frontend.status.status(self.h)) self.assert_('song' in result) self.assert_(int(result['song']) >= 0) def test_status_method_when_playlist_loaded_contains_cpid_as_songid(self): - self.b.current_playlist.load([Track()]) + self.b.current_playlist.append([Track()]) self.b.playback.play() result = dict(frontend.status.status(self.h)) self.assert_('songid' in result) self.assertEqual(int(result['songid']), 1) def test_status_method_when_playing_contains_time_with_no_length(self): - self.b.current_playlist.load([Track(length=None)]) + self.b.current_playlist.append([Track(length=None)]) self.b.playback.play() result = dict(frontend.status.status(self.h)) self.assert_('time' in result) @@ -179,7 +179,7 @@ class StatusHandlerTest(unittest.TestCase): self.assert_(position <= total) def test_status_method_when_playing_contains_time_with_length(self): - self.b.current_playlist.load([Track(length=10000)]) + self.b.current_playlist.append([Track(length=10000)]) self.b.playback.play() result = dict(frontend.status.status(self.h)) self.assert_('time' in result) @@ -196,7 +196,7 @@ class StatusHandlerTest(unittest.TestCase): self.assertEqual(int(result['elapsed']), 59123) def test_status_method_when_playing_contains_bitrate(self): - self.b.current_playlist.load([Track(bitrate=320)]) + self.b.current_playlist.append([Track(bitrate=320)]) self.b.playback.play() result = dict(frontend.status.status(self.h)) self.assert_('bitrate' in result) From b41629658a02a577c526b896c2a82b71cbab1f9d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 16 Aug 2010 21:53:09 +0200 Subject: [PATCH 27/33] Update roadmap --- docs/development/roadmap.rst | 72 ++++++++++++++++++++++-------------- 1 file changed, 44 insertions(+), 28 deletions(-) diff --git a/docs/development/roadmap.rst b/docs/development/roadmap.rst index 243243ab..f9588cb8 100644 --- a/docs/development/roadmap.rst +++ b/docs/development/roadmap.rst @@ -28,35 +28,51 @@ released when we reach the other goal. possible to have both Spotify tracks and local tracks in the same playlist. -Stuff we really want to do, but just not right now -================================================== +Stuff we want to do, but not right now, and maybe never +======================================================= -- **[PENDING]** Create `Homebrew `_ recipies - for all our dependencies and Mopidy itself to make OS X installation a - breeze. See `Homebrew's issue #1612 - `_. -- Create `Debian packages `_ of all our - dependencies and Mopidy itself (hosted in our own Debian repo until we get - stuff into the various distros) to make Debian/Ubuntu installation a breeze. -- Run frontend tests against a real MPD server to ensure we are in sync. -- Start working with MPD client maintainers to get rid of weird assumptions - like only searching for first two letters and doing the rest of the filtering - locally in the client, etc. +- Packaging and distribution: + - **[PENDING]** Create `Homebrew `_ + recipies for all our dependencies and Mopidy itself to make OS X + installation a breeze. See `Homebrew's issue #1612 + `_. + - Create `Debian packages `_ of all + our dependencies and Mopidy itself (hosted in our own Debian repo until we + get stuff into the various distros) to make Debian/Ubuntu installation a + breeze. -Crazy stuff we had to write down somewhere -========================================== +- Compatability: -- Add an `XMMS2 `_ frontend, so Mopidy can serve XMMS2 - clients. -- Add support for serving the music as an `Icecast `_ - stream instead of playing it locally. -- Integrate with `Squeezebox `_ in some - way. -- AirPort Express support, like in - `PulseAudio `_. -- DNLA and/or UPnP support. Maybe using - `Coherence `_. -- `Media Player Remote Interfacing Specification - `_ - support. + - Run frontend tests against a real MPD server to ensure we are in sync. + - Start working with MPD client maintainers to get rid of weird assumptions + like only searching for first two letters and doing the rest of the + filtering locally in the client (:issue:`1`), etc. + +- Backends: + + - `Last.fm `_ + - `WIMP `_ + - DNLA/UPnP to Mopidy can play music from other DNLA MediaServers. + +- Frontends: + + - D-Bus/`MPRIS `_ + - REST/JSON web service with a jQuery client as example application. Maybe + based upon `Tornado `_ and `jQuery + Mobile `_. + - DNLA/UPnP to Mopidy can be controlled from i.e. TVs. + - `XMMS2 `_ + - LIRC frontend for controlling Mopidy with a remote. + +- Mixers: + + - LIRC mixer for controlling arbitrary amplifiers remotely. + +- Audio streaming: + + - Ogg Vorbis/MP3 audio stream over HTTP, to MPD clients, `Squeezeboxes + `_, etc. + - Feed audio to an `Icecast `_ server. + - Stream to AirPort Express using `RAOP + `_. From b067c25b0c0afa9b2da6a4ce6efb2ea739b39e78 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 16 Aug 2010 22:05:25 +0200 Subject: [PATCH 28/33] Update README --- README.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 350f959b..1e4430e2 100644 --- a/README.rst +++ b/README.rst @@ -2,10 +2,11 @@ Mopidy ****** -Mopidy is an `Music Player Daemon `_ (MPD) server with a -`Spotify `_ backend. Using a standard MPD client you -can search for music in Spotify's vast archive, manage Spotify playlists and -play music from Spotify. +Mopidy is a music server which can play music from `Spotify +`_ or from your local hard drive. To search for music +in Spotify's vast archive, manage playlists, and play music, you can use most +`MPD clients `_. MPD clients are available for most +platforms, including Windows, Mac OS X, Linux, and iPhone and Android phones. To install Mopidy, check out `the installation docs `_. @@ -14,4 +15,3 @@ To install Mopidy, check out * `Source code `_ * `Issue tracker `_ * IRC: ``#mopidy`` at `irc.freenode.net `_ -* `Presentation of Mopidy `_ From 4efc51e524b0df503c19094af9d1214292c0596f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 16 Aug 2010 22:13:22 +0200 Subject: [PATCH 29/33] Add link to donations page at Pledgie --- docs/authors.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/authors.rst b/docs/authors.rst index e21b79f6..f56242a5 100644 --- a/docs/authors.rst +++ b/docs/authors.rst @@ -8,3 +8,15 @@ Contributors to Mopidy in the order of appearance: - Johannes Knutsen - Thomas Adamcik - Kristian Klette + + +Donations +========= + +If you already enjoy Mopidy, or don't enjoy it and want to help us making +Mopidy better, you can `donate money `_ to +Mopidy's development. + +Any donated money will be used to cover service subscriptions (e.g. Spotify +and Last.fm) and hardware devices (e.g. an used iPod Touch for testing Mopidy +with MPod) needed for developing Mopidy. From 73258d6f9d412d2977ed4dedf084a4a95cb3d2e8 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 16 Aug 2010 22:33:04 +0200 Subject: [PATCH 30/33] Reimplement 'load' using SPC.get() instead of search() --- docs/changes.rst | 7 ++++--- .../mpd/protocol/stored_playlists.py | 8 +++++--- tests/frontends/mpd/stored_playlists_test.py | 20 +++++++++++++++---- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index dfe46951..d5428a68 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -41,12 +41,13 @@ greatly improved MPD client support. - Support ``pause`` without arguments to work better with MPDroid. - Support ``plchanges``, ``play``, ``consume``, ``random``, ``repeat``, and ``single`` without quotes to work better with BitMPC. - - Fixed delete current playing track from playlist, which crashed several - clients. + - Fixed deletion of the currently playing track from the current playlist, + which crashed several clients. - Implement ``seek`` and ``seekid``. - Fix ``playlistfind`` output so the correct song is played when playing songs directly from search results in GMPC. - - Fix ``load`` so that one can append a playlist to the current playlist. + - Fix ``load`` so that one can append a playlist to the current playlist, and + make it return the correct error message if the playlist is not found. - Support for single track repeat added. (Fixes: :issue:`4`) - Rename ``mopidy.frontends.mpd.{serializer => translator}`` to match naming in backends. diff --git a/mopidy/frontends/mpd/protocol/stored_playlists.py b/mopidy/frontends/mpd/protocol/stored_playlists.py index 25ae4c32..39a2e150 100644 --- a/mopidy/frontends/mpd/protocol/stored_playlists.py +++ b/mopidy/frontends/mpd/protocol/stored_playlists.py @@ -91,9 +91,11 @@ def load(frontend, name): - ``load`` appends the given playlist to the current playlist. """ - matches = frontend.backend.stored_playlists.search(name) - if matches: - frontend.backend.current_playlist.append(matches[0].tracks) + try: + playlist = frontend.backend.stored_playlists.get(name=name) + frontend.backend.current_playlist.append(playlist.tracks) + except LookupError as e: + raise MpdNoExistError(u'No such playlist', command=u'load') @handle_pattern(r'^playlistadd "(?P[^"]+)" "(?P[^"]+)"$') def playlistadd(frontend, name, uri): diff --git a/tests/frontends/mpd/stored_playlists_test.py b/tests/frontends/mpd/stored_playlists_test.py index 9babc670..b49ccce1 100644 --- a/tests/frontends/mpd/stored_playlists_test.py +++ b/tests/frontends/mpd/stored_playlists_test.py @@ -49,12 +49,24 @@ class StoredPlaylistsHandlerTest(unittest.TestCase): self.assert_(u'Last-Modified: 2001-03-17T13:41:17Z' in result) self.assert_(u'OK' in result) - def test_load(self): - result = self.h.handle_request(u'load "name"') + def test_load_known_playlist_appends_to_current_playlist(self): + self.b.current_playlist.append([Track(uri='a'), Track(uri='b')]) + self.assertEqual(len(self.b.current_playlist.tracks), 2) + self.b.stored_playlists.playlists = [Playlist(name='A-list', + tracks=[Track(uri='c'), Track(uri='d'), Track(uri='e')])] + result = self.h.handle_request(u'load "A-list"') self.assert_(u'OK' in result) + self.assertEqual(len(self.b.current_playlist.tracks), 5) + self.assertEqual(self.b.current_playlist.tracks[0].uri, 'a') + self.assertEqual(self.b.current_playlist.tracks[1].uri, 'b') + self.assertEqual(self.b.current_playlist.tracks[2].uri, 'c') + self.assertEqual(self.b.current_playlist.tracks[3].uri, 'd') + self.assertEqual(self.b.current_playlist.tracks[4].uri, 'e') - def test_load_appends(self): - raise SkipTest + def test_load_unknown_playlist_acks(self): + result = self.h.handle_request(u'load "unknown playlist"') + self.assert_(u'ACK [50@0] {load} No such playlist' in result) + self.assertEqual(len(self.b.current_playlist.tracks), 0) def test_playlistadd(self): result = self.h.handle_request( From 7674775718f54b49b765f19d45202637ec41c569 Mon Sep 17 00:00:00 2001 From: Johannes Knutsen Date: Mon, 16 Aug 2010 22:34:00 +0200 Subject: [PATCH 31/33] Don't call lookup on backends with uris they don't support --- mopidy/frontends/mpd/protocol/current_playlist.py | 14 +++++++++----- tests/frontends/mpd/current_playlist_test.py | 6 ++++++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/mopidy/frontends/mpd/protocol/current_playlist.py b/mopidy/frontends/mpd/protocol/current_playlist.py index 17b019e9..0dab4b79 100644 --- a/mopidy/frontends/mpd/protocol/current_playlist.py +++ b/mopidy/frontends/mpd/protocol/current_playlist.py @@ -11,11 +11,15 @@ def add(frontend, uri): Adds the file ``URI`` to the playlist (directories add recursively). ``URI`` can also be a single file. """ - track = frontend.backend.library.lookup(uri) - if track is None: - raise MpdNoExistError( - u'directory or file not found', command=u'add') - frontend.backend.current_playlist.add(track) + for handler_prefix in frontend.backend.uri_handlers: + if uri.startswith(handler_prefix): + track = frontend.backend.library.lookup(uri) + if track is not None: + frontend.backend.current_playlist.add(track) + return + + raise MpdNoExistError( + u'directory or file not found', command=u'add') @handle_pattern(r'^addid "(?P[^"]*)"( "(?P\d+)")*$') def addid(frontend, uri, songpos=None): diff --git a/tests/frontends/mpd/current_playlist_test.py b/tests/frontends/mpd/current_playlist_test.py index 6b5c822e..5bd110e0 100644 --- a/tests/frontends/mpd/current_playlist_test.py +++ b/tests/frontends/mpd/current_playlist_test.py @@ -22,6 +22,12 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): self.assertEqual(len(result), 1) self.assert_(u'OK' in result) + def test_add_with_uri_not_found_in_library_should_not_call_lookup(self): + self.b.library.lookup = lambda uri: self.fail("Shouldn't run") + result = self.h.handle_request(u'add "foo"') + self.assertEqual(result[0], + u'ACK [50@0] {add} directory or file not found') + def test_add_with_uri_not_found_in_library_should_ack(self): result = self.h.handle_request(u'add "dummy://foo"') self.assertEqual(result[0], From c4dc6164d5c3301ac7a8af75a0e7b9698c8acc55 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 16 Aug 2010 22:36:02 +0200 Subject: [PATCH 32/33] Remove SPC.search() --- docs/changes.rst | 4 ++++ mopidy/backends/base/stored_playlists.py | 10 ---------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index d5428a68..37e0d202 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -107,6 +107,10 @@ greatly improved MPD client support. - Rename :meth:`mopidy.backends.base.BasePlaybackController.end_of_track_callback()` to :meth:`mopidy.backends.base.BasePlaybackController.on_end_of_track()`. + - Remove :meth:`mopidy.backends.base.BaseStoredPlaylistsController.search()` + since it was barely used, untested, and we got no use case for non-exact + search in stored playlists yet. Use + :meth:`mopidy.backends.base.BaseStoredPlaylistsController.get()` instead. 0.1.0a3 (2010-08-03) diff --git a/mopidy/backends/base/stored_playlists.py b/mopidy/backends/base/stored_playlists.py index 31185cd4..61722c81 100644 --- a/mopidy/backends/base/stored_playlists.py +++ b/mopidy/backends/base/stored_playlists.py @@ -107,13 +107,3 @@ class BaseStoredPlaylistsController(object): :type playlist: :class:`mopidy.models.Playlist` """ raise NotImplementedError - - def search(self, query): - """ - Search for playlists whose name contains ``query``. - - :param query: query to search for - :type query: string - :rtype: list of :class:`mopidy.models.Playlist` - """ - return filter(lambda p: query in p.name, self._playlists) From b313d6bf74c07e0852989e97d56c2a13d34ba5d1 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 16 Aug 2010 22:56:24 +0200 Subject: [PATCH 33/33] Mark settings in changelog properly, to get links to settings reference docs --- docs/changes.rst | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 37e0d202..323f899e 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -22,8 +22,11 @@ greatly improved MPD client support. `. - If you used :mod:`mopidy.backends.libspotify` previously, pyspotify must be updated when updating to this release, to get working seek functionality. -- The settings ``SERVER_HOSTNAME`` and ``SERVER_PORT`` has been renamed to - ``MPD_SERVER_HOSTNAME`` and ``MPD_SERVER_PORT``. +- :attr:`mopidy.settings.SERVER_HOSTNAME` and + :attr:`mopidy.settings.SERVER_PORT` has been renamed to + :attr:`mopidy.settings.MPD_SERVER_HOSTNAME` and + :attr:`mopidy.settings.MPD_SERVER_PORT` to allow for multiple frontends in + the future. **Changes** @@ -58,15 +61,16 @@ greatly improved MPD client support. - Remove :mod:`mopidy.backends.despotify`, as Despotify is little maintained and the Libspotify backend is working much better. (Fixes: :issue:`9`, :issue:`10`, :issue:`13`) - - A Spotify application key is now bundled with the source. The - ``SPOTIFY_LIB_APPKEY`` setting is thus removed. + - A Spotify application key is now bundled with the source. + :attr:`mopidy.settings.SPOTIFY_LIB_APPKEY` is thus removed. - If failing to play a track, playback will skip to the next track. - Mixers: - - Added new :mod:`mopidy.mixers.GStreamerSoftwareMixer` which now is the - default mixer on all platforms. - - New setting ``MIXER_MAX_VOLUME`` for capping the maximum output volume. + - Added new :mod:`mopidy.mixers.gstreamer_software.GStreamerSoftwareMixer` + which now is the default mixer on all platforms. + - New setting :attr:`mopidy.settings.MIXER_MAX_VOLUME` for capping the + maximum output volume. - Backend API: