diff --git a/mopidy/frontends/mpd/protocol/music_db.py b/mopidy/frontends/mpd/protocol/music_db.py index e56c123e..6dd43d68 100644 --- a/mopidy/frontends/mpd/protocol/music_db.py +++ b/mopidy/frontends/mpd/protocol/music_db.py @@ -10,8 +10,8 @@ from mopidy.frontends.mpd.protocol import handle_request, stored_playlists QUERY_RE = ( - r'(?P("?([Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ile|[Ff]ilename|' - r'[Tt]itle|[Tt]rack|[Aa]ny)"? "[^"]*"\s?)+)$') + r'(?P("?([Aa]lbum|[Aa]rtist|[Aa]lbumartist|[Dd]ate|[Ff]ile|' + r'[Ff]ilename|[Tt]itle|[Tt]rack|[Aa]ny)"? "[^"]*"\s?)+)$') def _get_field(field, search_results): @@ -100,7 +100,7 @@ def find(context, mpd_query): return results = context.core.library.find_exact(**query).get() result_tracks = [] - if 'artist' not in query: + if 'artist' not in query and 'albumartist' not in query: result_tracks += [_artist_as_track(a) for a in _get_artists(results)] if 'album' not in query: result_tracks += [_album_as_track(a) for a in _get_albums(results)] diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 80bfb60a..9b331395 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -179,6 +179,48 @@ def query_from_mpd_list_format(field, mpd_query): raise MpdArgError('not able to parse args', command='list') +# XXX The regexps below should be refactored to reuse common patterns here +# and in mopidy.frontends.mpd.protocol.music_db.QUERY_RE. + +MPD_SEARCH_QUERY_RE = re.compile(r""" + \b # Only begin matching at word bundaries + "? # Optional quote around the field type + (?: # A non-capturing group for the field type + [Aa]lbum + | [Aa]rtist + | [Aa]lbumartist + | [Dd]ate + | [Ff]ile + | [Ff]ilename + | [Tt]itle + | [Tt]rack + | [Aa]ny + ) + "? # End of optional quote around the field type + \s # A single space + "[^"]+" # Matching a quoted search string +""", re.VERBOSE) + +MPD_SEARCH_QUERY_PART_RE = re.compile(r""" + \b # Only begin matching at word bundaries + "? # Optional quote around the field type + (?P( # A capturing group for the field type + [Aa]lbum + | [Aa]rtist + | [Aa]lbumartist + | [Dd]ate + | [Ff]ile + | [Ff]ilename + | [Tt]itle + | [Tt]rack + | [Aa]ny + )) + "? # End of optional quote around the field type + \s # A single space + "(?P[^"]+)" # Capturing a quoted search string +""", re.VERBOSE) + + def query_from_mpd_search_format(mpd_query): """ Parses an MPD ``search`` or ``find`` query and converts it to the Mopidy @@ -187,18 +229,10 @@ def query_from_mpd_search_format(mpd_query): :param mpd_query: the MPD search query :type mpd_query: string """ - # XXX The regexps below should be refactored to reuse common patterns here - # and in mopidy.frontends.mpd.protocol.music_db. - query_pattern = ( - r'"?(?:[Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ile|[Ff]ilename|' - r'[Tt]itle|[Aa]ny)"? "[^"]+"') - query_parts = re.findall(query_pattern, mpd_query) - query_part_pattern = ( - r'"?(?P([Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ile|[Ff]ilename|' - r'[Tt]itle|[Tt]rack|[Aa]ny))"? "(?P[^"]+)"') + query_parts = MPD_SEARCH_QUERY_RE.findall(mpd_query) query = {} for query_part in query_parts: - m = re.match(query_part_pattern, query_part) + m = MPD_SEARCH_QUERY_PART_RE.match(query_part) field = m.groupdict()['field'].lower() if field == 'title': field = 'track' @@ -206,7 +240,6 @@ def query_from_mpd_search_format(mpd_query): field = 'track_no' elif field in ('file', 'filename'): field = 'uri' - field = str(field) # Needed for kwargs keys on OS X and Windows what = m.groupdict()['what'] if not what: raise ValueError diff --git a/tests/frontends/mpd/protocol/music_db_test.py b/tests/frontends/mpd/protocol/music_db_test.py index 1b115bd6..0114340b 100644 --- a/tests/frontends/mpd/protocol/music_db_test.py +++ b/tests/frontends/mpd/protocol/music_db_test.py @@ -197,6 +197,26 @@ class MusicDatabaseFindTest(protocol.BaseTestCase): self.assertInResponse('OK') + def test_find_albumartist_does_not_include_fake_artist_tracks(self): + self.backend.library.dummy_find_exact_result = SearchResult( + albums=[Album(uri='dummy:album:a', name='A', date='2001')], + artists=[Artist(uri='dummy:artist:b', name='B')], + tracks=[Track(uri='dummy:track:c', name='C')]) + + self.sendRequest('find "albumartist" "foo"') + + self.assertNotInResponse('file: dummy:artist:b') + self.assertNotInResponse('Title: Artist: B') + + self.assertInResponse('file: dummy:album:a') + self.assertInResponse('Title: Album: A') + self.assertInResponse('Date: 2001') + + self.assertInResponse('file: dummy:track:c') + self.assertInResponse('Title: C') + + self.assertInResponse('OK') + def test_find_artist_and_album_does_not_include_fake_tracks(self): self.backend.library.dummy_find_exact_result = SearchResult( albums=[Album(uri='dummy:album:a', name='A', date='2001')], @@ -233,6 +253,14 @@ class MusicDatabaseFindTest(protocol.BaseTestCase): self.sendRequest('find artist "what"') self.assertInResponse('OK') + def test_find_albumartist(self): + self.sendRequest('find "albumartist" "what"') + self.assertInResponse('OK') + + def test_find_albumartist_without_quotes(self): + self.sendRequest('find albumartist "what"') + self.assertInResponse('OK') + def test_find_filename(self): self.sendRequest('find "filename" "afilename"') self.assertInResponse('OK') @@ -579,6 +607,18 @@ class MusicDatabaseSearchTest(protocol.BaseTestCase): self.sendRequest('search "artist" ""') self.assertInResponse('OK') + def test_search_albumartist(self): + self.sendRequest('search "albumartist" "analbumartist"') + self.assertInResponse('OK') + + def test_search_albumartist_without_quotes(self): + self.sendRequest('search albumartist "analbumartist"') + self.assertInResponse('OK') + + def test_search_albumartist_without_filter_value(self): + self.sendRequest('search "albumartist" ""') + self.assertInResponse('OK') + def test_search_filename(self): self.sendRequest('search "filename" "afilename"') self.assertInResponse('OK')