From ac537a63c75145ee3bf60d35c74b6d3e3dda1895 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 14 Dec 2012 11:51:55 +0100 Subject: [PATCH 1/9] mpd: Add 'seekcur' command --- docs/changes.rst | 4 ++- mopidy/frontends/mpd/protocol/playback.py | 20 ++++++++++++ tests/frontends/mpd/protocol/playback_test.py | 31 +++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/docs/changes.rst b/docs/changes.rst index 3720ccf4..8de66e45 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -8,7 +8,9 @@ This change log is used to track all major changes to Mopidy. v0.11.0 (in development) ======================== -- No changes yet. +**MPD frontend** + +- Add support for ``seekcur`` command added in MPD 0.17. v0.10.0 (2012-12-12) diff --git a/mopidy/frontends/mpd/protocol/playback.py b/mopidy/frontends/mpd/protocol/playback.py index 5a4569e1..68c49ca0 100644 --- a/mopidy/frontends/mpd/protocol/playback.py +++ b/mopidy/frontends/mpd/protocol/playback.py @@ -349,6 +349,26 @@ def seekid(context, tlid, seconds): context.core.playback.seek(int(seconds) * 1000).get() +@handle_request(r'^seekcur "(?P\d+)"$') +@handle_request(r'^seekcur "(?P[-+]\d+)"$') +def seekcur(context, position=None, diff=None): + """ + *musicpd.org, playback section:* + + ``seekcur {TIME}`` + + Seeks to the position ``TIME`` within the current song. If prefixed by + '+' or '-', then the time is relative to the current playing position. + """ + if position is not None: + position = int(position) * 1000 + context.core.playback.seek(position).get() + elif diff is not None: + position = context.core.playback.time_position.get() + position += int(diff) * 1000 + context.core.playback.seek(position).get() + + @handle_request(r'^setvol (?P[-+]*\d+)$') @handle_request(r'^setvol "(?P[-+]*\d+)"$') def setvol(context, volume): diff --git a/tests/frontends/mpd/protocol/playback_test.py b/tests/frontends/mpd/protocol/playback_test.py index 14168a35..063493ec 100644 --- a/tests/frontends/mpd/protocol/playback_test.py +++ b/tests/frontends/mpd/protocol/playback_test.py @@ -414,6 +414,37 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): self.assertEqual(seek_track, self.core.playback.current_track.get()) self.assertInResponse('OK') + def test_seekcur_absolute_value(self): + self.core.tracklist.add([Track(uri='dummy:a', length=40000)]) + self.core.playback.play() + + self.sendRequest('seekcur "30"') + + self.assertGreaterEqual(self.core.playback.time_position.get(), 30000) + self.assertInResponse('OK') + + def test_seekcur_positive_diff(self): + self.core.tracklist.add([Track(uri='dummy:a', length=40000)]) + self.core.playback.play() + self.core.playback.seek(10000) + self.assertGreaterEqual(self.core.playback.time_position.get(), 10000) + + self.sendRequest('seekcur "+20"') + + self.assertGreaterEqual(self.core.playback.time_position.get(), 30000) + self.assertInResponse('OK') + + def test_seekcur_negative_diff(self): + self.core.tracklist.add([Track(uri='dummy:a', length=40000)]) + self.core.playback.play() + self.core.playback.seek(30000) + self.assertGreaterEqual(self.core.playback.time_position.get(), 30000) + + self.sendRequest('seekcur "-20"') + + self.assertLessEqual(self.core.playback.time_position.get(), 15000) + self.assertInResponse('OK') + def test_stop(self): self.sendRequest('stop') self.assertEqual(STOPPED, self.core.playback.state.get()) From 49d585a97c7e13d3e39c315a13bddbfe58206141 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 14 Dec 2012 12:08:17 +0100 Subject: [PATCH 2/9] mpd: Add 'config' command --- docs/changes.rst | 2 ++ mopidy/frontends/mpd/protocol/reflection.py | 22 ++++++++++++++++--- .../frontends/mpd/protocol/reflection_test.py | 11 ++++++++-- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 8de66e45..6f15ff20 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -12,6 +12,8 @@ v0.11.0 (in development) - Add support for ``seekcur`` command added in MPD 0.17. +- Add support for ``config`` command added in MPD 0.17. + v0.10.0 (2012-12-12) ==================== diff --git a/mopidy/frontends/mpd/protocol/reflection.py b/mopidy/frontends/mpd/protocol/reflection.py index d9c35743..cc1c7222 100644 --- a/mopidy/frontends/mpd/protocol/reflection.py +++ b/mopidy/frontends/mpd/protocol/reflection.py @@ -1,8 +1,23 @@ from __future__ import unicode_literals +from mopidy.frontends.mpd.exceptions import MpdPermissionError from mopidy.frontends.mpd.protocol import handle_request, mpd_commands +@handle_request(r'^config$', auth_required=False) +def config(context): + """ + *musicpd.org, reflection section:* + + ``config`` + + Dumps configuration values that may be interesting for the client. This + command is only permitted to "local" clients (connected via UNIX domain + socket). + """ + raise MpdPermissionError(command='config') + + @handle_request(r'^commands$', auth_required=False) def commands(context): """ @@ -19,10 +34,10 @@ def commands(context): command.name for command in mpd_commands if not command.auth_required]) - # No one is permited to use kill, rest of commands are not listed by MPD, - # so we shouldn't either. + # No one is permited to use 'config' or 'kill', rest of commands are not + # listed by MPD, so we shouldn't either. command_names = command_names - set([ - 'kill', 'command_list_begin', 'command_list_ok_begin', + 'config', 'kill', 'command_list_begin', 'command_list_ok_begin', 'command_list_ok_begin', 'command_list_end', 'idle', 'noidle', 'sticker']) @@ -73,6 +88,7 @@ def notcommands(context): command.name for command in mpd_commands if command.auth_required] # No permission to use + command_names.append('config') command_names.append('kill') return [ diff --git a/tests/frontends/mpd/protocol/reflection_test.py b/tests/frontends/mpd/protocol/reflection_test.py index 9c07f104..f2720473 100644 --- a/tests/frontends/mpd/protocol/reflection_test.py +++ b/tests/frontends/mpd/protocol/reflection_test.py @@ -6,6 +6,11 @@ from tests.frontends.mpd import protocol class ReflectionHandlerTest(protocol.BaseTestCase): + def test_config_is_not_allowed_across_the_network(self): + self.sendRequest('config') + self.assertEqualResponse( + 'ACK [4@0] {config} you don\'t have permission for "config"') + def test_commands_returns_list_of_all_commands(self): self.sendRequest('commands') # Check if some random commands are included @@ -13,6 +18,7 @@ class ReflectionHandlerTest(protocol.BaseTestCase): self.assertInResponse('command: play') self.assertInResponse('command: status') # Check if commands you do not have access to are not present + self.assertNotInResponse('command: config') self.assertNotInResponse('command: kill') # Check if the blacklisted commands are not present self.assertNotInResponse('command: command_list_begin') @@ -40,9 +46,10 @@ class ReflectionHandlerTest(protocol.BaseTestCase): self.sendRequest('decoders') self.assertInResponse('OK') - def test_notcommands_returns_only_kill_and_ok(self): + def test_notcommands_returns_only_config_and_kill_and_ok(self): response = self.sendRequest('notcommands') - self.assertEqual(2, len(response)) + self.assertEqual(3, len(response)) + self.assertInResponse('command: config') self.assertInResponse('command: kill') self.assertInResponse('OK') From 50cbe5f3841e2801d573483f41ed1b8b174a2a58 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 14 Dec 2012 22:12:24 +0100 Subject: [PATCH 3/9] mpd: Add range support to 'load' command --- docs/changes.rst | 3 ++ .../mpd/protocol/stored_playlists.py | 23 +++++++++--- .../mpd/protocol/stored_playlists_test.py | 36 ++++++++++++++++++- 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 6f15ff20..300af3d3 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -14,6 +14,9 @@ v0.11.0 (in development) - Add support for ``config`` command added in MPD 0.17. +- Add support for loading a range of tracks from a playlist to the ``load`` + command, as added in MPD 0.17. + v0.10.0 (2012-12-12) ==================== diff --git a/mopidy/frontends/mpd/protocol/stored_playlists.py b/mopidy/frontends/mpd/protocol/stored_playlists.py index eef1f3d1..034403ec 100644 --- a/mopidy/frontends/mpd/protocol/stored_playlists.py +++ b/mopidy/frontends/mpd/protocol/stored_playlists.py @@ -92,23 +92,36 @@ def listplaylists(context): return result -@handle_request(r'^load "(?P[^"]+)"$') -def load(context, name): +@handle_request(r'^load "(?P[^"]+)"( "(?P\d+):(?P\d+)*")*$') +def load(context, name, start=None, end=None): """ *musicpd.org, stored playlists section:* - ``load {NAME}`` + ``load {NAME} [START:END]`` - Loads the playlist ``NAME.m3u`` from the playlist directory. + Loads the playlist into the current queue. Playlist plugins are + supported. A range may be specified to load only a part of the + playlist. *Clarifications:* - ``load`` appends the given playlist to the current playlist. + + - MPD 0.17.1 does not support open-ended ranges, i.e. without end + specified, for the ``load`` command, even though MPD's general range docs + allows open-ended ranges. + + - MPD 0.17.1 does not fail if the specified range is outside the playlist, + in either or both ends. """ playlists = context.core.playlists.filter(name=name).get() if not playlists: raise MpdNoExistError('No such playlist', command='load') - context.core.tracklist.add(playlists[0].tracks) + if start is not None: + start = int(start) + if end is not None: + end = int(end) + context.core.tracklist.add(playlists[0].tracks[start:end]) @handle_request(r'^playlistadd "(?P[^"]+)" "(?P[^"]+)"$') diff --git a/tests/frontends/mpd/protocol/stored_playlists_test.py b/tests/frontends/mpd/protocol/stored_playlists_test.py index be2afd4c..49da5d0b 100644 --- a/tests/frontends/mpd/protocol/stored_playlists_test.py +++ b/tests/frontends/mpd/protocol/stored_playlists_test.py @@ -73,7 +73,7 @@ class PlaylistsHandlerTest(protocol.BaseTestCase): self.assertNotInResponse('playlist: ') self.assertInResponse('OK') - def test_load_known_playlist_appends_to_tracklist(self): + def test_load_appends_to_tracklist(self): self.core.tracklist.add([Track(uri='a'), Track(uri='b')]) self.assertEqual(len(self.core.tracklist.tracks.get()), 2) self.backend.playlists.playlists = [ @@ -81,6 +81,7 @@ class PlaylistsHandlerTest(protocol.BaseTestCase): Track(uri='c'), Track(uri='d'), Track(uri='e')])] self.sendRequest('load "A-list"') + tracks = self.core.tracklist.tracks.get() self.assertEqual(5, len(tracks)) self.assertEqual('a', tracks[0].uri) @@ -90,6 +91,39 @@ class PlaylistsHandlerTest(protocol.BaseTestCase): self.assertEqual('e', tracks[4].uri) self.assertInResponse('OK') + def test_load_with_range_loads_part_of_playlist(self): + self.core.tracklist.add([Track(uri='a'), Track(uri='b')]) + self.assertEqual(len(self.core.tracklist.tracks.get()), 2) + self.backend.playlists.playlists = [ + Playlist(name='A-list', tracks=[ + Track(uri='c'), Track(uri='d'), Track(uri='e')])] + + self.sendRequest('load "A-list" "1:2"') + + tracks = self.core.tracklist.tracks.get() + self.assertEqual(3, len(tracks)) + self.assertEqual('a', tracks[0].uri) + self.assertEqual('b', tracks[1].uri) + self.assertEqual('d', tracks[2].uri) + self.assertInResponse('OK') + + def test_load_with_range_without_end_loads_rest_of_playlist(self): + self.core.tracklist.add([Track(uri='a'), Track(uri='b')]) + self.assertEqual(len(self.core.tracklist.tracks.get()), 2) + self.backend.playlists.playlists = [ + Playlist(name='A-list', tracks=[ + Track(uri='c'), Track(uri='d'), Track(uri='e')])] + + self.sendRequest('load "A-list" "1:"') + + tracks = self.core.tracklist.tracks.get() + self.assertEqual(4, len(tracks)) + self.assertEqual('a', tracks[0].uri) + self.assertEqual('b', tracks[1].uri) + self.assertEqual('d', tracks[2].uri) + self.assertEqual('e', tracks[3].uri) + self.assertInResponse('OK') + def test_load_unknown_playlist_acks(self): self.sendRequest('load "unknown playlist"') self.assertEqual(0, len(self.core.tracklist.tracks.get())) From 6ac2c249b52d1a076aca3308f9caf52601bc2221 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 15 Dec 2012 00:33:11 +0100 Subject: [PATCH 4/9] mpd: Add 'findadd' command --- docs/changes.rst | 2 ++ mopidy/frontends/mpd/protocol/music_db.py | 30 +++++++++++-------- tests/frontends/mpd/protocol/music_db_test.py | 10 ++++++- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 300af3d3..4224c5d2 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -17,6 +17,8 @@ v0.11.0 (in development) - Add support for loading a range of tracks from a playlist to the ``load`` command, as added in MPD 0.17. +- Add support for the ``findadd`` command. + v0.10.0 (2012-12-12) ==================== diff --git a/mopidy/frontends/mpd/protocol/music_db.py b/mopidy/frontends/mpd/protocol/music_db.py index 00b9ec00..91f0dcf4 100644 --- a/mopidy/frontends/mpd/protocol/music_db.py +++ b/mopidy/frontends/mpd/protocol/music_db.py @@ -51,7 +51,8 @@ def count(context, tag, needle): @handle_request( - r'^find (?P("?([Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ile[name]*|' + r'^find ' + r'(?P("?([Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ile[name]*|' r'[Tt]itle|[Aa]ny)"? "[^"]*"\s?)+)$') def find(context, mpd_query): """ @@ -59,8 +60,10 @@ def find(context, mpd_query): ``find {TYPE} {WHAT}`` - Finds songs in the db that are exactly ``WHAT``. ``TYPE`` should be - ``album``, ``artist``, or ``title``. ``WHAT`` is what to find. + Finds songs in the db that are exactly ``WHAT``. ``TYPE`` can be any + tag supported by MPD, or one of the two special parameters - ``file`` + to search by full path (relative to database root), and ``any`` to + match against all available tags. ``WHAT`` is what to find. *GMPC:* @@ -82,26 +85,29 @@ def find(context, mpd_query): query = _build_query(mpd_query) except ValueError: return - return tracks_to_mpd_format( - context.core.library.find_exact(**query).get()) + result = context.core.library.find_exact(**query).get() + return tracks_to_mpd_format(result) @handle_request( r'^findadd ' - r'(?P("?([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny)"? ' - r'"[^"]+"\s?)+)$') -def findadd(context, query): + r'(?P("?([Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ile[name]*|' + r'[Tt]itle|[Aa]ny)"? "[^"]*"\s?)+)$') +def findadd(context, mpd_query): """ *musicpd.org, music database section:* ``findadd {TYPE} {WHAT}`` Finds songs in the db that are exactly ``WHAT`` and adds them to - current playlist. ``TYPE`` can be any tag supported by MPD. - ``WHAT`` is what to find. + current playlist. Parameters have the same meaning as for ``find``. """ - # TODO Add result to current playlist - #result = context.find(query) + try: + query = _build_query(mpd_query) + except ValueError: + return + result = context.core.library.find_exact(**query).get() + context.core.tracklist.add(result) @handle_request( diff --git a/tests/frontends/mpd/protocol/music_db_test.py b/tests/frontends/mpd/protocol/music_db_test.py index 4539eb4c..7f50d169 100644 --- a/tests/frontends/mpd/protocol/music_db_test.py +++ b/tests/frontends/mpd/protocol/music_db_test.py @@ -13,7 +13,15 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_findadd(self): - self.sendRequest('findadd "album" "what"') + self.backend.library.dummy_find_exact_result = [ + Track(uri='dummy:a', name='A'), + ] + self.assertEqual(self.core.tracklist.length.get(), 0) + + self.sendRequest('findadd "title" "A"') + + self.assertEqual(self.core.tracklist.length.get(), 1) + self.assertEqual(self.core.tracklist.tracks.get()[0].uri, 'dummy:a') self.assertInResponse('OK') def test_listall(self): From 9b1dfa69784412d84695dbab34c828c7b542ef8b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 15 Dec 2012 00:40:33 +0100 Subject: [PATCH 5/9] mpd: Add 'searchadd' command --- docs/changes.rst | 2 + mopidy/frontends/mpd/protocol/music_db.py | 38 +++++++++++++++---- tests/frontends/mpd/protocol/music_db_test.py | 12 ++++++ 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 4224c5d2..83252a9c 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -19,6 +19,8 @@ v0.11.0 (in development) - Add support for the ``findadd`` command. +- Add support for ``searchadd`` command added in MPD 0.17. + v0.10.0 (2012-12-12) ==================== diff --git a/mopidy/frontends/mpd/protocol/music_db.py b/mopidy/frontends/mpd/protocol/music_db.py index 91f0dcf4..2c0a2c32 100644 --- a/mopidy/frontends/mpd/protocol/music_db.py +++ b/mopidy/frontends/mpd/protocol/music_db.py @@ -340,17 +340,17 @@ def rescan(context, uri=None): @handle_request( - r'^search (?P("?([Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ile[name]*|' + r'^search ' + r'(?P("?([Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ile[name]*|' r'[Tt]itle|[Aa]ny)"? "[^"]*"\s?)+)$') def search(context, mpd_query): """ *musicpd.org, music database section:* - ``search {TYPE} {WHAT}`` + ``search {TYPE} {WHAT} [...]`` - Searches for any song that contains ``WHAT``. ``TYPE`` can be - ``title``, ``artist``, ``album`` or ``filename``. Search is not - case sensitive. + Searches for any song that contains ``WHAT``. Parameters have the same + meaning as for ``find``, except that search is not case sensitive. *GMPC:* @@ -374,8 +374,32 @@ def search(context, mpd_query): query = _build_query(mpd_query) except ValueError: return - return tracks_to_mpd_format( - context.core.library.search(**query).get()) + result = context.core.library.search(**query).get() + return tracks_to_mpd_format(result) + + +@handle_request( + r'^searchadd ' + r'(?P("?([Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ile[name]*|' + r'[Tt]itle|[Aa]ny)"? "[^"]*"\s?)+)$') +def searchadd(context, mpd_query): + """ + *musicpd.org, music database section:* + + ``searchadd {TYPE} {WHAT} [...]`` + + Searches for any song that contains ``WHAT`` in tag ``TYPE`` and adds + them to current playlist. + + Parameters have the same meaning as for ``find``, except that search is + not case sensitive. + """ + try: + query = _build_query(mpd_query) + except ValueError: + return + result = context.core.library.search(**query).get() + context.core.tracklist.add(result) @handle_request(r'^update( "(?P[^"]+)")*$') diff --git a/tests/frontends/mpd/protocol/music_db_test.py b/tests/frontends/mpd/protocol/music_db_test.py index 7f50d169..13f0759b 100644 --- a/tests/frontends/mpd/protocol/music_db_test.py +++ b/tests/frontends/mpd/protocol/music_db_test.py @@ -24,6 +24,18 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase): self.assertEqual(self.core.tracklist.tracks.get()[0].uri, 'dummy:a') self.assertInResponse('OK') + def test_searchadd(self): + self.backend.library.dummy_search_result = [ + Track(uri='dummy:a', name='A'), + ] + self.assertEqual(self.core.tracklist.length.get(), 0) + + self.sendRequest('searchadd "title" "a"') + + self.assertEqual(self.core.tracklist.length.get(), 1) + self.assertEqual(self.core.tracklist.tracks.get()[0].uri, 'dummy:a') + self.assertInResponse('OK') + def test_listall(self): self.sendRequest('listall "file:///dev/urandom"') self.assertEqualResponse('ACK [0@0] {} Not implemented') From b95c8032de14cdb7a92620eb45bf7a7259532497 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 15 Dec 2012 01:18:13 +0100 Subject: [PATCH 6/9] mpd: Add 'searchaddpl' command --- docs/changes.rst | 2 + mopidy/frontends/mpd/protocol/music_db.py | 35 +++++++++++++++++ tests/frontends/mpd/protocol/music_db_test.py | 38 +++++++++++++++++++ 3 files changed, 75 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index 83252a9c..8add66e1 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -21,6 +21,8 @@ v0.11.0 (in development) - Add support for ``searchadd`` command added in MPD 0.17. +- Add support for ``searchaddpl`` command added in MPD 0.17. + v0.10.0 (2012-12-12) ==================== diff --git a/mopidy/frontends/mpd/protocol/music_db.py b/mopidy/frontends/mpd/protocol/music_db.py index 2c0a2c32..66735538 100644 --- a/mopidy/frontends/mpd/protocol/music_db.py +++ b/mopidy/frontends/mpd/protocol/music_db.py @@ -402,6 +402,41 @@ def searchadd(context, mpd_query): context.core.tracklist.add(result) +@handle_request( + r'^searchaddpl ' + r'"(?P[^"]+)" ' + r'(?P("?([Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ile[name]*|' + r'[Tt]itle|[Aa]ny)"? "[^"]*"\s?)+)$') +def searchaddpl(context, playlist_name, mpd_query): + """ + *musicpd.org, music database section:* + + ``searchaddpl {NAME} {TYPE} {WHAT} [...]`` + + Searches for any song that contains ``WHAT`` in tag ``TYPE`` and adds + them to the playlist named ``NAME``. + + If a playlist by that name doesn't exist it is created. + + Parameters have the same meaning as for ``find``, except that search is + not case sensitive. + """ + try: + query = _build_query(mpd_query) + except ValueError: + return + result = context.core.library.search(**query).get() + + playlists = context.core.playlists.filter(name=playlist_name).get() + if playlists: + playlist = playlists[0] + else: + playlist = context.core.playlists.create(playlist_name).get() + tracks = list(playlist.tracks) + result + playlist = playlist.copy(tracks=tracks) + context.core.playlists.save(playlist) + + @handle_request(r'^update( "(?P[^"]+)")*$') def update(context, uri=None, rescan_unmodified_files=False): """ diff --git a/tests/frontends/mpd/protocol/music_db_test.py b/tests/frontends/mpd/protocol/music_db_test.py index 13f0759b..5c887958 100644 --- a/tests/frontends/mpd/protocol/music_db_test.py +++ b/tests/frontends/mpd/protocol/music_db_test.py @@ -36,6 +36,44 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase): self.assertEqual(self.core.tracklist.tracks.get()[0].uri, 'dummy:a') self.assertInResponse('OK') + def test_searchaddpl_appends_to_existing_playlist(self): + playlist = self.core.playlists.create('my favs').get() + playlist = playlist.copy(tracks=[ + Track(uri='dummy:x', name='X'), + Track(uri='dummy:y', name='y'), + ]) + self.core.playlists.save(playlist) + self.backend.library.dummy_search_result = [ + Track(uri='dummy:a', name='A'), + ] + playlists = self.core.playlists.filter(name='my favs').get() + self.assertEqual(len(playlists), 1) + self.assertEqual(len(playlists[0].tracks), 2) + + self.sendRequest('searchaddpl "my favs" "title" "a"') + + playlists = self.core.playlists.filter(name='my favs').get() + self.assertEqual(len(playlists), 1) + self.assertEqual(len(playlists[0].tracks), 3) + self.assertEqual(playlists[0].tracks[0].uri, 'dummy:x') + self.assertEqual(playlists[0].tracks[1].uri, 'dummy:y') + self.assertEqual(playlists[0].tracks[2].uri, 'dummy:a') + self.assertInResponse('OK') + + def test_searchaddpl_creates_missing_playlist(self): + self.backend.library.dummy_search_result = [ + Track(uri='dummy:a', name='A'), + ] + self.assertEqual( + len(self.core.playlists.filter(name='my favs').get()), 0) + + self.sendRequest('searchaddpl "my favs" "title" "a"') + + playlists = self.core.playlists.filter(name='my favs').get() + self.assertEqual(len(playlists), 1) + self.assertEqual(playlists[0].tracks[0].uri, 'dummy:a') + self.assertInResponse('OK') + def test_listall(self): self.sendRequest('listall "file:///dev/urandom"') self.assertEqualResponse('ACK [0@0] {} Not implemented') From b43fc2ebe98cf0e2712c4d0e50748b8f9a79e386 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 15 Dec 2012 01:44:20 +0100 Subject: [PATCH 7/9] mpd: Stub channel commands --- docs/changes.rst | 3 + docs/modules/frontends/mpd.rst | 8 +++ mopidy/frontends/mpd/protocol/__init__.py | 5 +- mopidy/frontends/mpd/protocol/channels.py | 69 +++++++++++++++++++ tests/frontends/mpd/protocol/channels_test.py | 25 +++++++ 5 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 mopidy/frontends/mpd/protocol/channels.py create mode 100644 tests/frontends/mpd/protocol/channels_test.py diff --git a/docs/changes.rst b/docs/changes.rst index 8add66e1..e313c540 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -23,6 +23,9 @@ v0.11.0 (in development) - Add support for ``searchaddpl`` command added in MPD 0.17. +- Add empty stubs for channel commands for client to client communication, + which was added in MPD 0.17. + v0.10.0 (2012-12-12) ==================== diff --git a/docs/modules/frontends/mpd.rst b/docs/modules/frontends/mpd.rst index 090ca5cd..f25b90f2 100644 --- a/docs/modules/frontends/mpd.rst +++ b/docs/modules/frontends/mpd.rst @@ -30,6 +30,14 @@ Audio output :members: +Channels +-------- + +.. automodule:: mopidy.frontends.mpd.protocol.channels + :synopsis: MPD protocol: channels -- client to client communication + :members: + + Command list ------------ diff --git a/mopidy/frontends/mpd/protocol/__init__.py b/mopidy/frontends/mpd/protocol/__init__.py index a8bdc2c7..6afde4b9 100644 --- a/mopidy/frontends/mpd/protocol/__init__.py +++ b/mopidy/frontends/mpd/protocol/__init__.py @@ -74,6 +74,7 @@ def load_protocol_modules(): """ # pylint: disable = W0612 from . import ( # noqa - audio_output, command_list, connection, current_playlist, empty, - music_db, playback, reflection, status, stickers, stored_playlists) + audio_output, channels, command_list, connection, current_playlist, + empty, music_db, playback, reflection, status, stickers, + stored_playlists) # pylint: enable = W0612 diff --git a/mopidy/frontends/mpd/protocol/channels.py b/mopidy/frontends/mpd/protocol/channels.py new file mode 100644 index 00000000..11ac6fda --- /dev/null +++ b/mopidy/frontends/mpd/protocol/channels.py @@ -0,0 +1,69 @@ +from __future__ import unicode_literals + +from mopidy.frontends.mpd.protocol import handle_request +from mopidy.frontends.mpd.exceptions import MpdNotImplemented + + +@handle_request(r'^subscribe "(?P[A-Za-z0-9:._-]+)"$') +def subscribe(context, channel): + """ + *musicpd.org, client to client section:* + + ``subscribe {NAME}`` + + Subscribe to a channel. The channel is created if it does not exist + already. The name may consist of alphanumeric ASCII characters plus + underscore, dash, dot and colon. + """ + raise MpdNotImplemented # TODO + + +@handle_request(r'^unsubscribe "(?P[A-Za-z0-9:._-]+)"$') +def unsubscribe(context, channel): + """ + *musicpd.org, client to client section:* + + ``unsubscribe {NAME}`` + + Unsubscribe from a channel. + """ + raise MpdNotImplemented # TODO + + +@handle_request(r'^channels$') +def channels(context): + """ + *musicpd.org, client to client section:* + + ``channels`` + + Obtain a list of all channels. The response is a list of "channel:" + lines. + """ + raise MpdNotImplemented # TODO + + +@handle_request(r'^readmessages$') +def readmessages(context): + """ + *musicpd.org, client to client section:* + + ``readmessages`` + + Reads messages for this client. The response is a list of "channel:" + and "message:" lines. + """ + raise MpdNotImplemented # TODO + + +@handle_request( + r'^sendmessage "(?P[A-Za-z0-9:._-]+)" "(?P[^"]*)"$') +def sendmessage(context, channel, text): + """ + *musicpd.org, client to client section:* + + ``sendmessage {CHANNEL} {TEXT}`` + + Send a message to the specified channel. + """ + raise MpdNotImplemented # TODO diff --git a/tests/frontends/mpd/protocol/channels_test.py b/tests/frontends/mpd/protocol/channels_test.py new file mode 100644 index 00000000..86cf8197 --- /dev/null +++ b/tests/frontends/mpd/protocol/channels_test.py @@ -0,0 +1,25 @@ +from __future__ import unicode_literals + +from tests.frontends.mpd import protocol + + +class ChannelsHandlerTest(protocol.BaseTestCase): + def test_subscribe(self): + self.sendRequest('subscribe "topic"') + self.assertEqualResponse('ACK [0@0] {} Not implemented') + + def test_unsubscribe(self): + self.sendRequest('unsubscribe "topic"') + self.assertEqualResponse('ACK [0@0] {} Not implemented') + + def test_channels(self): + self.sendRequest('channels') + self.assertEqualResponse('ACK [0@0] {} Not implemented') + + def test_readmessages(self): + self.sendRequest('readmessages') + self.assertEqualResponse('ACK [0@0] {} Not implemented') + + def test_sendmessage(self): + self.sendRequest('sendmessage "topic" "a message"') + self.assertEqualResponse('ACK [0@0] {} Not implemented') From 0b6673e7f59a193e77b9647b9a78ecc6c6dfefef Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 15 Dec 2012 01:46:59 +0100 Subject: [PATCH 8/9] mpd: Bump protocol version to 0.17.0 --- docs/changes.rst | 23 ++++++++++++----------- mopidy/frontends/mpd/protocol/__init__.py | 4 ++-- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index e313c540..6a5aa572 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -10,21 +10,22 @@ v0.11.0 (in development) **MPD frontend** -- Add support for ``seekcur`` command added in MPD 0.17. - -- Add support for ``config`` command added in MPD 0.17. - -- Add support for loading a range of tracks from a playlist to the ``load`` - command, as added in MPD 0.17. - - Add support for the ``findadd`` command. -- Add support for ``searchadd`` command added in MPD 0.17. +- Updated to match the MPD 0.17 protocol (Fixes: :issue:`228`): -- Add support for ``searchaddpl`` command added in MPD 0.17. + - Add support for ``seekcur`` command. -- Add empty stubs for channel commands for client to client communication, - which was added in MPD 0.17. + - Add support for ``config`` command. + + - Add support for loading a range of tracks from a playlist to the ``load`` + command. + + - Add support for ``searchadd`` command. + + - Add support for ``searchaddpl`` command. + + - Add empty stubs for channel commands for client to client communication. v0.10.0 (2012-12-12) diff --git a/mopidy/frontends/mpd/protocol/__init__.py b/mopidy/frontends/mpd/protocol/__init__.py index 6afde4b9..1827624b 100644 --- a/mopidy/frontends/mpd/protocol/__init__.py +++ b/mopidy/frontends/mpd/protocol/__init__.py @@ -21,8 +21,8 @@ ENCODING = 'UTF-8' #: The MPD protocol uses ``\n`` as line terminator. LINE_TERMINATOR = '\n' -#: The MPD protocol version is 0.16.0. -VERSION = '0.16.0' +#: The MPD protocol version is 0.17.0. +VERSION = '0.17.0' MpdCommand = namedtuple('MpdCommand', ['name', 'auth_required']) From b1f0a67dd42e8fdf041250514611003f5130f642 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 15 Dec 2012 23:43:45 +0100 Subject: [PATCH 9/9] mpd: Reuse query regexp. Fix 'filename' expression --- mopidy/frontends/mpd/protocol/music_db.py | 31 ++++++++--------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/mopidy/frontends/mpd/protocol/music_db.py b/mopidy/frontends/mpd/protocol/music_db.py index 66735538..7cdfc5e0 100644 --- a/mopidy/frontends/mpd/protocol/music_db.py +++ b/mopidy/frontends/mpd/protocol/music_db.py @@ -8,6 +8,11 @@ from mopidy.frontends.mpd.protocol import handle_request, stored_playlists from mopidy.frontends.mpd.translator import tracks_to_mpd_format +QUERY_RE = ( + r'(?P("?([Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ile|[Ff]ilename|' + r'[Tt]itle|[Aa]ny)"? "[^"]*"\s?)+)$') + + def _build_query(mpd_query): """ Parses a MPD query string and converts it to the Mopidy query format. @@ -50,10 +55,7 @@ def count(context, tag, needle): return [('songs', 0), ('playtime', 0)] # TODO -@handle_request( - r'^find ' - r'(?P("?([Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ile[name]*|' - r'[Tt]itle|[Aa]ny)"? "[^"]*"\s?)+)$') +@handle_request(r'^find ' + QUERY_RE) def find(context, mpd_query): """ *musicpd.org, music database section:* @@ -89,10 +91,7 @@ def find(context, mpd_query): return tracks_to_mpd_format(result) -@handle_request( - r'^findadd ' - r'(?P("?([Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ile[name]*|' - r'[Tt]itle|[Aa]ny)"? "[^"]*"\s?)+)$') +@handle_request(r'^findadd ' + QUERY_RE) def findadd(context, mpd_query): """ *musicpd.org, music database section:* @@ -339,10 +338,7 @@ def rescan(context, uri=None): return update(context, uri, rescan_unmodified_files=True) -@handle_request( - r'^search ' - r'(?P("?([Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ile[name]*|' - r'[Tt]itle|[Aa]ny)"? "[^"]*"\s?)+)$') +@handle_request(r'^search ' + QUERY_RE) def search(context, mpd_query): """ *musicpd.org, music database section:* @@ -378,10 +374,7 @@ def search(context, mpd_query): return tracks_to_mpd_format(result) -@handle_request( - r'^searchadd ' - r'(?P("?([Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ile[name]*|' - r'[Tt]itle|[Aa]ny)"? "[^"]*"\s?)+)$') +@handle_request(r'^searchadd ' + QUERY_RE) def searchadd(context, mpd_query): """ *musicpd.org, music database section:* @@ -402,11 +395,7 @@ def searchadd(context, mpd_query): context.core.tracklist.add(result) -@handle_request( - r'^searchaddpl ' - r'"(?P[^"]+)" ' - r'(?P("?([Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ile[name]*|' - r'[Tt]itle|[Aa]ny)"? "[^"]*"\s?)+)$') +@handle_request(r'^searchaddpl "(?P[^"]+)" ' + QUERY_RE) def searchaddpl(context, playlist_name, mpd_query): """ *musicpd.org, music database section:*