From 9c08f54514ee6e28ed05d36c22efa3b8d3b903f1 Mon Sep 17 00:00:00 2001 From: Johannes Knutsen Date: Mon, 26 Jul 2010 10:44:21 +0200 Subject: [PATCH] implemented multi-word search by making a query as a list of (field, what)-tuples. --- mopidy/backends/__init__.py | 12 ++--- mopidy/backends/despotify.py | 19 +++++--- mopidy/backends/dummy.py | 2 +- mopidy/backends/gstreamer.py | 83 +++++++++++++++++--------------- mopidy/backends/libspotify.py | 14 ++++-- mopidy/backends/mock.py | 66 +++++++++++++++++++++++++ mopidy/mpd/frontend.py | 68 ++++++++++++++------------ tests/backends/base.py | 72 +++++++++++++-------------- tests/mpd/.frontend_test.py.swo | Bin 0 -> 73728 bytes tests/mpd/frontend_test.py | 5 ++ 10 files changed, 215 insertions(+), 126 deletions(-) create mode 100644 mopidy/backends/mock.py create mode 100644 tests/mpd/.frontend_test.py.swo 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 0000000000000000000000000000000000000000..6607a65809cdfd4e713b8b9ba8c9b04ea1f4f116 GIT binary patch literal 73728 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

4L(h)l?{{LM3{acOyZ_oex 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 H*+l(6(YLef literal 0 HcmV?d00001 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')