Merge pull request #1022 from adamcik/fix/913
Add list_distinct command for more effective library lookups.
This commit is contained in:
commit
2179bf06d3
@ -36,6 +36,9 @@ v0.20.0 (UNRELEASED)
|
||||
- When seeking in paused state, do not change to playing state. (Fixed
|
||||
:issue:`939`)
|
||||
|
||||
- Add :meth:`mopidy.core.LibraryController.get_distinct` for getting unique
|
||||
values for a given field. (Fixes: :issue:`913`)
|
||||
|
||||
**Commands**
|
||||
|
||||
- Make the ``mopidy`` command print a friendly error message if the
|
||||
@ -98,6 +101,9 @@ v0.20.0 (UNRELEASED)
|
||||
"database". If you insist on using a client that needs these commands change
|
||||
:confval:`mpd/command_blacklist`.
|
||||
|
||||
- Switch the ``list`` command over to using
|
||||
:meth:`mopidy.core.LibraryController.get_distinct`. (Fixes: :issue:`913`)
|
||||
|
||||
**HTTP frontend**
|
||||
|
||||
- Prevent race condition in webservice broadcast from breaking the server.
|
||||
|
||||
@ -92,6 +92,16 @@ class LibraryProvider(object):
|
||||
"""
|
||||
return []
|
||||
|
||||
def get_distinct(self, field, query=None):
|
||||
"""
|
||||
See :meth:`mopidy.core.LibraryController.get_distinct`.
|
||||
|
||||
*MAY be implemented by subclass.*
|
||||
|
||||
Default implementation will simply return an empty set.
|
||||
"""
|
||||
return set()
|
||||
|
||||
def get_images(self, uris):
|
||||
"""
|
||||
See :meth:`mopidy.core.LibraryController.get_images`.
|
||||
|
||||
@ -72,6 +72,27 @@ class LibraryController(object):
|
||||
return []
|
||||
return backend.library.browse(uri).get()
|
||||
|
||||
def get_distinct(self, field, query=None):
|
||||
"""
|
||||
List distinct values for a given field from the library.
|
||||
|
||||
This has mainly been added to support the list commands the MPD
|
||||
protocol supports in a more sane fashion. Other frontends are not
|
||||
recommended to use this method.
|
||||
|
||||
:param string field: One of ``artist``, ``albumartist``, ``album``,
|
||||
``composer``, ``performer``, ``date``or ``genre``.
|
||||
:param dict query: Query to use for limiting results, see
|
||||
:method:`search` for details about the query format.
|
||||
:rtype: set of values corresponding to the requested field type.
|
||||
"""
|
||||
futures = [b.library.get_distinct(field, query)
|
||||
for b in self.backends.with_library.values()]
|
||||
result = set()
|
||||
for r in pykka.get_all(futures):
|
||||
result.update(r)
|
||||
return result
|
||||
|
||||
def get_images(self, uris):
|
||||
"""Lookup the images for the given URIs
|
||||
|
||||
|
||||
@ -89,6 +89,18 @@ class Library(object):
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_distinct(self, field, query=None):
|
||||
"""
|
||||
List distinct values for a given field from the library.
|
||||
|
||||
:param string field: One of ``artist``, ``albumartist``, ``album``,
|
||||
``composer``, ``performer``, ``date``or ``genre``.
|
||||
:param dict query: Query to use for limiting results, see
|
||||
:method:`search` for details about the query format.
|
||||
:rtype: set of values corresponding to the requested field type.
|
||||
"""
|
||||
return set()
|
||||
|
||||
def load(self):
|
||||
"""
|
||||
(Re)load any tracks stored in memory, if any, otherwise just return
|
||||
|
||||
@ -155,6 +155,38 @@ class JsonLibrary(local.Library):
|
||||
except KeyError:
|
||||
return []
|
||||
|
||||
def get_distinct(self, field, query=None):
|
||||
if field == 'artist':
|
||||
def distinct(track):
|
||||
return {a.name for a in track.artists}
|
||||
elif field == 'albumartist':
|
||||
def distinct(track):
|
||||
album = track.album or models.Album()
|
||||
return {a.name for a in album.artists}
|
||||
elif field == 'album':
|
||||
def distinct(track):
|
||||
album = track.album or models.Album()
|
||||
return {album.name}
|
||||
elif field == 'composer':
|
||||
def distinct(track):
|
||||
return {a.name for a in track.composers}
|
||||
elif field == 'performer':
|
||||
def distinct(track):
|
||||
return {a.name for a in track.performers}
|
||||
elif field == 'date':
|
||||
def distinct(track):
|
||||
return {track.date}
|
||||
elif field == 'genre':
|
||||
def distinct(track):
|
||||
return {track.genre}
|
||||
else:
|
||||
return set()
|
||||
|
||||
result = set()
|
||||
for track in search.search(self._tracks.values(), query).tracks:
|
||||
result.update(distinct(track))
|
||||
return result
|
||||
|
||||
def search(self, query=None, limit=100, offset=0, uris=None, exact=False):
|
||||
tracks = self._tracks.values()
|
||||
# TODO: pass limit and offset into search helpers
|
||||
|
||||
@ -23,6 +23,11 @@ class LocalLibraryProvider(backend.LibraryProvider):
|
||||
return []
|
||||
return self._library.browse(uri)
|
||||
|
||||
def get_distinct(self, field, query=None):
|
||||
if not self._library:
|
||||
return set()
|
||||
return self._library.get_distinct(field, query)
|
||||
|
||||
def refresh(self, uri=None):
|
||||
if not self._library:
|
||||
return 0
|
||||
|
||||
@ -30,6 +30,15 @@ _LIST_MAPPING = {
|
||||
'genre': 'genre',
|
||||
'performer': 'performer'}
|
||||
|
||||
_LIST_NAME_MAPPING = {
|
||||
'album': 'Album',
|
||||
'albumartist': 'AlbumArtist',
|
||||
'artist': 'Artist',
|
||||
'composer': 'Composer',
|
||||
'date': 'Date',
|
||||
'genre': 'Genre',
|
||||
'performer': 'Performer'}
|
||||
|
||||
|
||||
def _query_from_mpd_search_parameters(parameters, mapping):
|
||||
query = {}
|
||||
@ -246,109 +255,30 @@ def list_(context, *args):
|
||||
- does not add quotes around the field argument.
|
||||
- capitalizes the field argument.
|
||||
"""
|
||||
parameters = list(args)
|
||||
if not parameters:
|
||||
params = list(args)
|
||||
if not params:
|
||||
raise exceptions.MpdArgError('incorrect arguments')
|
||||
field = parameters.pop(0).lower()
|
||||
field = params.pop(0).lower()
|
||||
|
||||
if field not in _LIST_MAPPING:
|
||||
raise exceptions.MpdArgError('incorrect arguments')
|
||||
|
||||
if len(parameters) == 1:
|
||||
if len(params) == 1:
|
||||
if field != 'album':
|
||||
raise exceptions.MpdArgError('should be "Album" for 3 arguments')
|
||||
return _list_album(context, {'artist': parameters})
|
||||
query = {'artist': params}
|
||||
else:
|
||||
try:
|
||||
query = _query_from_mpd_search_parameters(params, _LIST_MAPPING)
|
||||
except exceptions.MpdArgError as e:
|
||||
e.message = 'not able to parse args'
|
||||
raise
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
try:
|
||||
query = _query_from_mpd_search_parameters(parameters, _LIST_MAPPING)
|
||||
except exceptions.MpdArgError as e:
|
||||
e.message = 'not able to parse args'
|
||||
raise
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
if field == 'artist':
|
||||
return _list_artist(context, query)
|
||||
if field == 'albumartist':
|
||||
return _list_albumartist(context, query)
|
||||
elif field == 'album':
|
||||
return _list_album(context, query)
|
||||
elif field == 'composer':
|
||||
return _list_composer(context, query)
|
||||
elif field == 'performer':
|
||||
return _list_performer(context, query)
|
||||
elif field == 'date':
|
||||
return _list_date(context, query)
|
||||
elif field == 'genre':
|
||||
return _list_genre(context, query)
|
||||
|
||||
|
||||
def _list_artist(context, query):
|
||||
artists = set()
|
||||
results = context.core.library.find_exact(**query).get()
|
||||
for track in _get_tracks(results):
|
||||
for artist in track.artists:
|
||||
if artist.name:
|
||||
artists.add(('Artist', artist.name))
|
||||
return artists
|
||||
|
||||
|
||||
def _list_albumartist(context, query):
|
||||
albumartists = set()
|
||||
results = context.core.library.find_exact(**query).get()
|
||||
for track in _get_tracks(results):
|
||||
if track.album:
|
||||
for artist in track.album.artists:
|
||||
if artist.name:
|
||||
albumartists.add(('AlbumArtist', artist.name))
|
||||
return albumartists
|
||||
|
||||
|
||||
def _list_album(context, query):
|
||||
albums = set()
|
||||
results = context.core.library.find_exact(**query).get()
|
||||
for track in _get_tracks(results):
|
||||
if track.album and track.album.name:
|
||||
albums.add(('Album', track.album.name))
|
||||
return albums
|
||||
|
||||
|
||||
def _list_composer(context, query):
|
||||
composers = set()
|
||||
results = context.core.library.find_exact(**query).get()
|
||||
for track in _get_tracks(results):
|
||||
for composer in track.composers:
|
||||
if composer.name:
|
||||
composers.add(('Composer', composer.name))
|
||||
return composers
|
||||
|
||||
|
||||
def _list_performer(context, query):
|
||||
performers = set()
|
||||
results = context.core.library.find_exact(**query).get()
|
||||
for track in _get_tracks(results):
|
||||
for performer in track.performers:
|
||||
if performer.name:
|
||||
performers.add(('Performer', performer.name))
|
||||
return performers
|
||||
|
||||
|
||||
def _list_date(context, query):
|
||||
dates = set()
|
||||
results = context.core.library.find_exact(**query).get()
|
||||
for track in _get_tracks(results):
|
||||
if track.date:
|
||||
dates.add(('Date', track.date))
|
||||
return dates
|
||||
|
||||
|
||||
def _list_genre(context, query):
|
||||
genres = set()
|
||||
results = context.core.library.find_exact(**query).get()
|
||||
for track in _get_tracks(results):
|
||||
if track.genre:
|
||||
genres.add(('Genre', track.genre))
|
||||
return genres
|
||||
name = _LIST_NAME_MAPPING[field]
|
||||
result = context.core.library.get_distinct(field, query)
|
||||
return [(name, value) for value in result.get()]
|
||||
|
||||
|
||||
@protocol.commands.add('listall')
|
||||
|
||||
@ -33,6 +33,7 @@ class DummyLibraryProvider(backend.LibraryProvider):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(DummyLibraryProvider, self).__init__(*args, **kwargs)
|
||||
self.dummy_library = []
|
||||
self.dummy_get_distinct_result = {}
|
||||
self.dummy_browse_result = {}
|
||||
self.dummy_find_exact_result = SearchResult()
|
||||
self.dummy_search_result = SearchResult()
|
||||
@ -40,6 +41,9 @@ class DummyLibraryProvider(backend.LibraryProvider):
|
||||
def browse(self, path):
|
||||
return self.dummy_browse_result.get(path, [])
|
||||
|
||||
def get_distinct(self, field, query=None):
|
||||
return self.dummy_get_distinct_result.get(field, set())
|
||||
|
||||
def find_exact(self, **query):
|
||||
return self.dummy_find_exact_result
|
||||
|
||||
|
||||
@ -55,7 +55,7 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase):
|
||||
# Count the lone track
|
||||
self.backend.library.dummy_find_exact_result = SearchResult(
|
||||
tracks=[
|
||||
Track(uri='dummy:a', name="foo", date="2001", length=4000),
|
||||
Track(uri='dummy:a', name='foo', date='2001', length=4000),
|
||||
])
|
||||
self.send_request('count "title" "foo"')
|
||||
self.assertInResponse('songs: 1')
|
||||
@ -613,11 +613,8 @@ class MusicDatabaseFindTest(protocol.BaseTestCase):
|
||||
|
||||
class MusicDatabaseListTest(protocol.BaseTestCase):
|
||||
def test_list(self):
|
||||
self.backend.library.dummy_find_exact_result = SearchResult(
|
||||
tracks=[
|
||||
Track(uri='dummy:a', name='A', artists=[
|
||||
Artist(name='A Artist')])])
|
||||
|
||||
self.backend.library.dummy_get_distinct_result = {
|
||||
'artist': set(['A Artist'])}
|
||||
self.send_request('list "artist" "artist" "foo"')
|
||||
|
||||
self.assertInResponse('Artist: A Artist')
|
||||
@ -891,8 +888,8 @@ class MusicDatabaseListTest(protocol.BaseTestCase):
|
||||
self.assertInResponse('OK')
|
||||
|
||||
def test_list_album_with_artist_name(self):
|
||||
self.backend.library.dummy_find_exact_result = SearchResult(
|
||||
tracks=[Track(album=Album(name='foo'))])
|
||||
self.backend.library.dummy_get_distinct_result = {
|
||||
'album': set(['foo'])}
|
||||
|
||||
self.send_request('list "album" "anartist"')
|
||||
self.assertInResponse('Album: foo')
|
||||
|
||||
Loading…
Reference in New Issue
Block a user