implemented multi-word search by making a query as a list of (field, what)-tuples.

This commit is contained in:
Johannes Knutsen 2010-07-26 10:44:21 +02:00
parent 7176a8db0e
commit 9c08f54514
10 changed files with 215 additions and 126 deletions

View File

@ -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``.

View File

@ -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()

View File

@ -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

View File

@ -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)

View File

@ -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)

66
mopidy/backends/mock.py Normal file
View File

@ -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'

View File

@ -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<field>([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny))"?\s'
r'"(?P<what>[^"]+)"')
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<outputid>\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<field>([Aa]lbum|[Aa]rtist|[Tt]itle)) '
r'"(?P<what>[^"]+)"$')
@handle_pattern(r'^find "(?P<field>(album|artist|title))" '
r'"(?P<what>[^"]+)"$')
@handle_pattern(r'^find (?P<field>(album)) '
r'"(?P<what>[^"]+)" artist "([^"]+)"$')
def _music_db_find(self, field, what):
@handle_pattern(r'^find '
r'(?P<mpd_query>("?([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<field>(album|artist|title))" '
r'"(?P<what>[^"]+)"$')
def _music_db_findadd(self, field, what):
@handle_pattern(r'^findadd '
r'(?P<query>("?([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<field>([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny)) '
r'"(?P<what>[^"]+)"$')
@handle_pattern(r'^search "(?P<field>(album|artist|filename|title|any))" '
r'"(?P<what>[^"]+)"$')
def _music_db_search(self, field, what):
r'(?P<mpd_query>("?([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<uri>[^"]+)")*$')

View File

@ -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)

Binary file not shown.

View File

@ -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')