diff --git a/mopidy/backends/__init__.py b/mopidy/backends/__init__.py index 9b08250b..56526eb8 100644 --- a/mopidy/backends/__init__.py +++ b/mopidy/backends/__init__.py @@ -259,14 +259,12 @@ class BaseLibraryController(object): """Cleanup after component.""" pass - def find_exact(self, field, query): + def find_exact(self, query): """ - Find tracks in the library where ``field`` matches ``query`` exactly. + Find tracks in the library where ``field`` matches ``what`` exactly. - :param field: 'track', 'artist', or 'album' - :type field: string - :param query: the search query - :type query: string + :param query: Example: [(u'artist', u'anArtist'), (u'album', u'anAlbum')] + :type query: list of (field, what) tuples. :rtype: :class:`mopidy.models.Playlist` """ raise NotImplementedError @@ -290,7 +288,7 @@ class BaseLibraryController(object): """ raise NotImplementedError - def search(self, field, query): + def search(self, query): """ Search the library for tracks where ``field`` contains ``query``. diff --git a/mopidy/backends/despotify.py b/mopidy/backends/despotify.py index 49809620..e15aacf5 100644 --- a/mopidy/backends/despotify.py +++ b/mopidy/backends/despotify.py @@ -58,14 +58,17 @@ class DespotifyLibraryController(BaseLibraryController): track = self.backend.spotify.lookup(uri.encode(ENCODING)) return DespotifyTranslator.to_mopidy_track(track) - def search(self, field, what): - if field == u'track': - field = u'title' - if field == u'any': - query = what - else: - query = u'%s:%s' % (field, what) - result = self.backend.spotify.search(query.encode(ENCODING)) + def search(self, query): + spotify_query = [] + for (field, what) in query: + if field == u'track': + field = u'title' + if field is u'any': + spotify_query.append(what) + else: + spotify_query.append(u'%s:"%s"' % (field, what)) + spotify_query = u' '.join(query) + result = self.backend.spotify.search(spotify_query.encode(ENCODING)) if (result is None or result.playlist.tracks[0].get_uri() == 'spotify:track:0000000000000000000000'): return Playlist() diff --git a/mopidy/backends/dummy.py b/mopidy/backends/dummy.py index fd79cb23..8d8a595e 100644 --- a/mopidy/backends/dummy.py +++ b/mopidy/backends/dummy.py @@ -30,7 +30,7 @@ class DummyLibraryController(BaseLibraryController): if matches: return matches[0] - def search(self, field, query): + def search(self, query): return Playlist() find_exact = search diff --git a/mopidy/backends/gstreamer.py b/mopidy/backends/gstreamer.py index ef7579f4..9a8fef5a 100644 --- a/mopidy/backends/gstreamer.py +++ b/mopidy/backends/gstreamer.py @@ -217,49 +217,54 @@ class GStreamerLibraryController(BaseLibraryController): except KeyError: raise LookupError('%s not found.' % uri) - def find_exact(self, field, query): - if not query: - raise LookupError('Missing query') + def find_exact(self, query): + for (field, what) in query: + if not what: + raise LookupError('Missing query') - if field == 'track': - filter_func = lambda t: t.name == query - elif field == 'album': - filter_func = lambda t: getattr(t, 'album', Album()).name == query - elif field == 'artist': - filter_func = lambda t: filter(lambda a: a.name == query, t.artists) - else: - raise LookupError('Invalid lookup field: %s' % field) + result_tracks = self._uri_mapping.values() + for (field, what) in query: + if field == 'track': + filter_func = lambda t: t.name == what + elif field == 'album': + filter_func = lambda t: getattr(t, 'album', Album()).name == what + elif field == 'artist': + filter_func = lambda t: filter(lambda a: a.name == what, t.artists) + else: + raise LookupError('Invalid lookup field: %s' % field) - tracks = filter(filter_func, self._uri_mapping.values()) - return Playlist(tracks=tracks) + result_tracks = filter(filter_func, result_tracks) + return Playlist(tracks=result_tracks) - def search(self, field, query): - if not query: - raise LookupError('Missing query') + def search(self, query): + for (field, what) in query: + if not what: + raise LookupError('Missing query') - q = query.strip().lower() - library_tracks = self._uri_mapping.values() + result_tracks = self._uri_mapping.values() + for (field, what) in query: + q = what.strip().lower() - # FIXME this is bound to be slow for large libraries - track_filter = lambda t: q in t.name.lower() - album_filter = lambda t: q in getattr(t, 'album', Album()).name.lower() - artist_filter = lambda t: filter(lambda a: q in a.name.lower(), - t.artists) - uri_filter = lambda t: q in t.uri.lower() - any_filter = lambda t: track_filter(t) or album_filter(t) or \ - artist_filter(t) or uri_filter(t) + # FIXME this is bound to be slow for large libraries + track_filter = lambda t: q in t.name.lower() + album_filter = lambda t: q in getattr(t, 'album', Album()).name.lower() + artist_filter = lambda t: filter(lambda a: q in a.name.lower(), + t.artists) + uri_filter = lambda t: q in t.uri.lower() + any_filter = lambda t: track_filter(t) or album_filter(t) or \ + artist_filter(t) or uri_filter(t) - if field == 'track': - tracks = filter(track_filter, library_tracks) - elif field == 'album': - tracks = filter(album_filter, library_tracks) - elif field == 'artist': - tracks = filter(artist_filter, library_tracks) - elif field == 'uri': - tracks = filter(uri_filter, library_tracks) - elif field == 'any': - tracks = filter(any_filter, library_tracks) - else: - raise LookupError('Invalid lookup field: %s' % field) + if field == 'track': + result_tracks = filter(track_filter, result_tracks) + elif field == 'album': + result_tracks = filter(album_filter, result_tracks) + elif field == 'artist': + result_tracks = filter(artist_filter, result_tracks) + elif field == 'uri': + result_tracks = filter(uri_filter, result_tracks) + elif field == 'any': + result_tracks = filter(any_filter, result_tracks) + else: + raise LookupError('Invalid lookup field: %s' % field) - return Playlist(tracks=tracks) + return Playlist(tracks=result_tracks) diff --git a/mopidy/backends/libspotify.py b/mopidy/backends/libspotify.py index ada857ad..086930d6 100644 --- a/mopidy/backends/libspotify.py +++ b/mopidy/backends/libspotify.py @@ -69,11 +69,15 @@ class LibspotifyLibraryController(BaseLibraryController): spotify_track = Link.from_string(uri).as_track() return LibspotifyTranslator.to_mopidy_track(spotify_track) - def search(self, field, what): - if field is u'any': - query = what - else: - query = u'%s:%s' % (field, what) + def search(self, query): + spotify_query = [] + for (field, what) in query: + if field is u'any': + spotify_query.append(what) + else: + spotify_query.append(u'%s:"%s"' % (field, what)) + spotify_query = u' '.join(query) + logger.debug(u'In search method, search for: %s' % query) my_end, other_end = multiprocessing.Pipe() self.backend.spotify.search(query.encode(ENCODING), other_end) my_end.poll(None) diff --git a/mopidy/backends/mock.py b/mopidy/backends/mock.py new file mode 100644 index 00000000..f6cc9dbc --- /dev/null +++ b/mopidy/backends/mock.py @@ -0,0 +1,66 @@ +from mopidy.backends import (BaseBackend, BaseCurrentPlaylistController, + BasePlaybackController, BaseLibraryController, + BaseStoredPlaylistsController) +from mopidy.models import Playlist, Track + +class MockBackend(BaseBackend): + """ + A backend which implements the backend API in the simplest way possible. + Used in tests of the frontends. + + Handles URIs starting with ``mock:``. + """ + + def __init__(self, *args, **kwargs): + super(MockBackend, self).__init__(*args, **kwargs) + self.current_playlist = MockCurrentPlaylistController(backend=self) + self.library = MockLibraryController(backend=self) + self.playback = MockPlaybackController(backend=self) + self.stored_playlists = MockStoredPlaylistsController(backend=self) + self.uri_handlers = [u'dummy:'] + +class MockCurrentPlaylistController(BaseCurrentPlaylistController): + pass + +class MockLibraryController(BaseLibraryController): + _library = [] + + def lookup(self, uri): + matches = filter(lambda t: uri == t.uri, self._library) + if matches: + return matches[0] + + def search(self, field, query): + return Playlist() + + find_exact = search + +class MockPlaybackController(BasePlaybackController): + def _next(self, track): + return True + + def _pause(self): + return True + + def _play(self, track): + return True + + def _previous(self, track): + return True + + def _resume(self): + return True + +class MockStoredPlaylistsController(BaseStoredPlaylistsController): + def __init__(self, backend): + self.backend = backend + playlist = Playlist(name=u'A playlist') + track = Track(name=u'test', uri=u'mock:asdf', id=u'2') + playlist._tracks = [track, ] + self._playlists = [playlist, ] + + def search(self, query): + return [Playlist(name=query)] + + def _stored_playlists_listplaylists(self): + return u'playlist: A playlist\nLast-Modified: 2010-07-18T23:05:35Z' diff --git a/mopidy/mpd/frontend.py b/mopidy/mpd/frontend.py index eba6dcd3..1a04514f 100644 --- a/mopidy/mpd/frontend.py +++ b/mopidy/mpd/frontend.py @@ -106,6 +106,26 @@ class MpdFrontend(object): response.append(u'OK') return response + def _build_query(self, mpd_query): + """ + Parses a mpd query string and converts the MPD query to a list of + (field, what) tuples. + """ + query_pattern = r'"?(?:[Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny)"? "[^"]+"' + query_parts = re.findall(query_pattern, mpd_query) + query_part_pattern = ( + r'"?(?P([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny))"?\s' + r'"(?P[^"]+)"') + query = [] + for query_part in query_parts: + m = re.match(query_part_pattern, query_part) + field = m.groupdict()['field'].lower() + if field == u'title': + field = u'track' + what = m.groupdict()['what'].lower() + query.append((field, what)) + return query + @handle_pattern(r'^disableoutput "(?P\d+)"$') def _audio_output_disableoutput(self, outputid): """ @@ -592,13 +612,9 @@ class MpdFrontend(object): """ return [('songs', 0), ('playtime', 0)] # TODO - @handle_pattern(r'^find (?P([Aa]lbum|[Aa]rtist|[Tt]itle)) ' - r'"(?P[^"]+)"$') - @handle_pattern(r'^find "(?P(album|artist|title))" ' - r'"(?P[^"]+)"$') - @handle_pattern(r'^find (?P(album)) ' - r'"(?P[^"]+)" artist "([^"]+)"$') - def _music_db_find(self, field, what): + @handle_pattern(r'^find ' + r'(?P("?([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny)"? "[^"]+"\s?)+)$') + def _music_db_find(self, mpd_query): """ *musicpd.org, music database section:* @@ -618,15 +634,13 @@ class MpdFrontend(object): - does not add quotes around the field argument. - capitalizes the type argument. """ - field = field.lower() - if field == u'title': - field = u'track' - return self.backend.library.find_exact(field, what).mpd_format( + query = self._build_query(mpd_query) + return self.backend.library.find_exact(query).mpd_format( search_result=True) - @handle_pattern(r'^findadd "(?P(album|artist|title))" ' - r'"(?P[^"]+)"$') - def _music_db_findadd(self, field, what): + @handle_pattern(r'^findadd ' + r'(?P("?([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny)"? "[^"]+"\s?)+)$') + def _music_db_findadd(self, query): """ *musicpd.org, music database section:* @@ -636,7 +650,7 @@ class MpdFrontend(object): current playlist. ``TYPE`` can be any tag supported by MPD. ``WHAT`` is what to find. """ - result = self._music_db_find(field, what) + result = self._music_db_find(query) # TODO Add result to current playlist #return result @@ -653,7 +667,7 @@ class MpdFrontend(object): return u'\n'.join(artists) def _music_db_list_album_artist(self, artist): - playlist = self.backend.library.find_exact(u'artist', artist) + playlist = self.backend.library.find_exact([(u'artist', artist)]) albums = set() for track in playlist.tracks: albums.add(u'Album: %s' % track.album.name) @@ -669,11 +683,11 @@ class MpdFrontend(object): ``list {TYPE} [ARTIST]`` - Lists all tags of the specified type. ``TYPE`` should be ``album`` - or ``artist``. + Lists all tags of the specified type. ``TYPE`` should be ``album``, + ``artist``, ``date``, or ``genre``. - ``ARTIST`` is an optional parameter when type is ``album``, this - specifies to list albums by an artist. + ``ARTIST`` is an optional parameter when type is ``album``, ``date``, or ``genre`` + This filters the result list by an artist. *GMPC:* @@ -760,11 +774,8 @@ class MpdFrontend(object): return self._music_db_update(uri, rescan_unmodified_files=True) @handle_pattern(r'^search ' - r'(?P([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny)) ' - r'"(?P[^"]+)"$') - @handle_pattern(r'^search "(?P(album|artist|filename|title|any))" ' - r'"(?P[^"]+)"$') - def _music_db_search(self, field, what): + r'(?P("?([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny)"? "[^"]+"\s?)+)$') + def _music_db_search(self, mpd_query): """ *musicpd.org, music database section:* @@ -787,11 +798,8 @@ class MpdFrontend(object): - does not add quotes around the field argument. - capitalizes the field argument. """ - # TODO Support GMPC multi-word search - field = field.lower() - if field == u'title': - field = u'track' - return self.backend.library.search(field, what).mpd_format( + query = self._build_query(mpd_query) + return self.backend.library.search(query).mpd_format( search_result=True) @handle_pattern(r'^update( "(?P[^"]+)")*$') diff --git a/tests/backends/base.py b/tests/backends/base.py index 129ac4ff..562e3eee 100644 --- a/tests/backends/base.py +++ b/tests/backends/base.py @@ -1114,120 +1114,120 @@ class BaseLibraryControllerTest(object): self.assertRaises(LookupError, test) def test_find_exact_no_hits(self): - result = self.library.find_exact('track', 'unknown track') + result = self.library.find_exact([('track', 'unknown track')]) self.assertEqual(result, Playlist()) - result = self.library.find_exact('artist', 'unknown artist') + result = self.library.find_exact([('artist', 'unknown artist')]) self.assertEqual(result, Playlist()) - result = self.library.find_exact('album', 'unknown artist') + result = self.library.find_exact([('album', 'unknown artist')]) self.assertEqual(result, Playlist()) def test_find_exact_artist(self): - result = self.library.find_exact('artist', 'artist1') + result = self.library.find_exact([('artist', 'artist1')]) self.assertEqual(result, Playlist(tracks=self.tracks[:1])) - result = self.library.find_exact('artist', 'artist2') + result = self.library.find_exact([('artist', 'artist2')]) self.assertEqual(result, Playlist(tracks=self.tracks[1:2])) def test_find_exact_track(self): - result = self.library.find_exact('track', 'track1') + result = self.library.find_exact([('track', 'track1')]) self.assertEqual(result, Playlist(tracks=self.tracks[:1])) - result = self.library.find_exact('track', 'track2') + result = self.library.find_exact([('track', 'track2')]) self.assertEqual(result, Playlist(tracks=self.tracks[1:2])) def test_find_exact_album(self): - result = self.library.find_exact('album', 'album1') + result = self.library.find_exact([('album', 'album1')]) self.assertEqual(result, Playlist(tracks=self.tracks[:1])) - result = self.library.find_exact('album', 'album2') + result = self.library.find_exact([('album', 'album2')]) self.assertEqual(result, Playlist(tracks=self.tracks[1:2])) def test_find_exact_wrong_type(self): - test = lambda: self.library.find_exact('wrong', 'test') + test = lambda: self.library.find_exact([('wrong', 'test')]) self.assertRaises(LookupError, test) def test_find_exact_with_empty_query(self): - test = lambda: self.library.find_exact('artist', '') + test = lambda: self.library.find_exact([('artist', '')]) self.assertRaises(LookupError, test) - test = lambda: self.library.find_exact('track', '') + test = lambda: self.library.find_exact([('track', '')]) self.assertRaises(LookupError, test) - test = lambda: self.library.find_exact('album', '') + test = lambda: self.library.find_exact([('album', '')]) self.assertRaises(LookupError, test) def test_search_no_hits(self): - result = self.library.search('track', 'unknown track') + result = self.library.search([('track', 'unknown track')]) self.assertEqual(result, Playlist()) - result = self.library.search('artist', 'unknown artist') + result = self.library.search([('artist', 'unknown artist')]) self.assertEqual(result, Playlist()) - result = self.library.search('album', 'unknown artist') + result = self.library.search([('album', 'unknown artist')]) self.assertEqual(result, Playlist()) - result = self.library.search('uri', 'unknown') + result = self.library.search([('uri', 'unknown')]) self.assertEqual(result, Playlist()) - result = self.library.search('any', 'unknown') + result = self.library.search([('any', 'unknown')]) self.assertEqual(result, Playlist()) def test_search_artist(self): - result = self.library.search('artist', 'Tist1') + result = self.library.search([('artist', 'Tist1')]) self.assertEqual(result, Playlist(tracks=self.tracks[:1])) - result = self.library.search('artist', 'Tist2') + result = self.library.search([('artist', 'Tist2')]) self.assertEqual(result, Playlist(tracks=self.tracks[1:2])) def test_search_track(self): - result = self.library.search('track', 'Rack1') + result = self.library.search([('track', 'Rack1')]) self.assertEqual(result, Playlist(tracks=self.tracks[:1])) - result = self.library.search('track', 'Rack2') + result = self.library.search([('track', 'Rack2')]) self.assertEqual(result, Playlist(tracks=self.tracks[1:2])) def test_search_album(self): - result = self.library.search('album', 'Bum1') + result = self.library.search([('album', 'Bum1')]) self.assertEqual(result, Playlist(tracks=self.tracks[:1])) - result = self.library.search('album', 'Bum2') + result = self.library.search([('album', 'Bum2')]) self.assertEqual(result, Playlist(tracks=self.tracks[1:2])) def test_search_uri(self): - result = self.library.search('uri', 'RI1') + result = self.library.search([('uri', 'RI1')]) self.assertEqual(result, Playlist(tracks=self.tracks[:1])) - result = self.library.search('uri', 'RI2') + result = self.library.search([('uri', 'RI2')]) self.assertEqual(result, Playlist(tracks=self.tracks[1:2])) def test_search_any(self): - result = self.library.search('any', 'Tist1') + result = self.library.search([('any', 'Tist1')]) self.assertEqual(result, Playlist(tracks=self.tracks[:1])) - result = self.library.search('any', 'Rack1') + result = self.library.search([('any', 'Rack1')]) self.assertEqual(result, Playlist(tracks=self.tracks[:1])) - result = self.library.search('any', 'Bum1') + result = self.library.search([('any', 'Bum1')]) self.assertEqual(result, Playlist(tracks=self.tracks[:1])) - result = self.library.search('any', 'RI1') + result = self.library.search([('any', 'RI1')]) self.assertEqual(result, Playlist(tracks=self.tracks[:1])) def test_search_wrong_type(self): - test = lambda: self.library.search('wrong', 'test') + test = lambda: self.library.search([('wrong', 'test')]) self.assertRaises(LookupError, test) def test_search_with_empty_query(self): - test = lambda: self.library.search('artist', '') + test = lambda: self.library.search([('artist', '')]) self.assertRaises(LookupError, test) - test = lambda: self.library.search('track', '') + test = lambda: self.library.search([('track', '')]) self.assertRaises(LookupError, test) - test = lambda: self.library.search('album', '') + test = lambda: self.library.search([('album', '')]) self.assertRaises(LookupError, test) - test = lambda: self.library.search('uri', '') + test = lambda: self.library.search([('uri', '')]) self.assertRaises(LookupError, test) - test = lambda: self.library.search('any', '') + test = lambda: self.library.search([('any', '')]) self.assertRaises(LookupError, test) diff --git a/tests/mpd/.frontend_test.py.swo b/tests/mpd/.frontend_test.py.swo new file mode 100644 index 00000000..6607a658 Binary files /dev/null and b/tests/mpd/.frontend_test.py.swo differ diff --git a/tests/mpd/frontend_test.py b/tests/mpd/frontend_test.py index 10950ccb..b5f61b07 100644 --- a/tests/mpd/frontend_test.py +++ b/tests/mpd/frontend_test.py @@ -1085,6 +1085,11 @@ class MusicDatabaseHandlerTest(unittest.TestCase): result = self.h.handle_request(u'search any "anything"') self.assert_(u'OK' in result) + def test_search_multi_word(self): + result = self.h.handle_request(u'search any "test1" any "test2"') + print result + self.assert_(u'OK' in result) + def test_search_else_should_fail(self): result = self.h.handle_request(u'search "sometype" "something"') self.assertEqual(result[0], u'ACK [2@0] {search} incorrect arguments')