Merge pull request #288 from jodal/feature/mpd-album-artist-search-results
Include albums and artists in MPD search results
This commit is contained in:
commit
bbd8630d88
@ -70,6 +70,12 @@ v0.11.0 (in development)
|
|||||||
- Make ``seek`` and ``seekid`` not restart the current track before seeking in
|
- Make ``seek`` and ``seekid`` not restart the current track before seeking in
|
||||||
it.
|
it.
|
||||||
|
|
||||||
|
- Include fake tracks representing albums and artists in the search results.
|
||||||
|
When these are added to the tracklist, they expand to either all tracks in
|
||||||
|
the album or all tracks by the artist. This makes it easy to play full albums
|
||||||
|
in proper order, which is a feature that have been frequently requested.
|
||||||
|
(Fixes: :issue:`67`, :issue:`148`)
|
||||||
|
|
||||||
**Internal changes**
|
**Internal changes**
|
||||||
|
|
||||||
*Models:*
|
*Models:*
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import functools
|
||||||
import itertools
|
import itertools
|
||||||
|
|
||||||
|
from mopidy.models import Track
|
||||||
from mopidy.frontends.mpd import translator
|
from mopidy.frontends.mpd import translator
|
||||||
from mopidy.frontends.mpd.exceptions import MpdNotImplemented
|
from mopidy.frontends.mpd.exceptions import MpdNotImplemented
|
||||||
from mopidy.frontends.mpd.protocol import handle_request, stored_playlists
|
from mopidy.frontends.mpd.protocol import handle_request, stored_playlists
|
||||||
@ -12,8 +14,29 @@ QUERY_RE = (
|
|||||||
r'[Tt]itle|[Aa]ny)"? "[^"]*"\s?)+)$')
|
r'[Tt]itle|[Aa]ny)"? "[^"]*"\s?)+)$')
|
||||||
|
|
||||||
|
|
||||||
def _get_tracks(search_results):
|
def _get_field(field, search_results):
|
||||||
return list(itertools.chain(*[r.tracks for r in search_results]))
|
return list(itertools.chain(*[getattr(r, field) for r in search_results]))
|
||||||
|
|
||||||
|
|
||||||
|
_get_albums = functools.partial(_get_field, 'albums')
|
||||||
|
_get_artists = functools.partial(_get_field, 'artists')
|
||||||
|
_get_tracks = functools.partial(_get_field, 'tracks')
|
||||||
|
|
||||||
|
|
||||||
|
def _album_as_track(album):
|
||||||
|
return Track(
|
||||||
|
uri=album.uri,
|
||||||
|
name='Album: ' + album.name,
|
||||||
|
artists=album.artists,
|
||||||
|
album=album,
|
||||||
|
date=album.date)
|
||||||
|
|
||||||
|
|
||||||
|
def _artist_as_track(artist):
|
||||||
|
return Track(
|
||||||
|
uri=artist.uri,
|
||||||
|
name='Artist: ' + artist.name,
|
||||||
|
artists=[artist])
|
||||||
|
|
||||||
|
|
||||||
@handle_request(r'^count "(?P<tag>[^"]+)" "(?P<needle>[^"]*)"$')
|
@handle_request(r'^count "(?P<tag>[^"]+)" "(?P<needle>[^"]*)"$')
|
||||||
@ -62,7 +85,13 @@ def find(context, mpd_query):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
return
|
return
|
||||||
results = context.core.library.find_exact(**query).get()
|
results = context.core.library.find_exact(**query).get()
|
||||||
return translator.tracks_to_mpd_format(_get_tracks(results))
|
result_tracks = []
|
||||||
|
if 'artist' 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)]
|
||||||
|
result_tracks += _get_tracks(results)
|
||||||
|
return translator.tracks_to_mpd_format(result_tracks)
|
||||||
|
|
||||||
|
|
||||||
@handle_request(r'^findadd ' + QUERY_RE)
|
@handle_request(r'^findadd ' + QUERY_RE)
|
||||||
@ -304,7 +333,10 @@ def search(context, mpd_query):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
return
|
return
|
||||||
results = context.core.library.search(**query).get()
|
results = context.core.library.search(**query).get()
|
||||||
return translator.tracks_to_mpd_format(_get_tracks(results))
|
artists = [_artist_as_track(a) for a in _get_artists(results)]
|
||||||
|
albums = [_album_as_track(a) for a in _get_albums(results)]
|
||||||
|
tracks = _get_tracks(results)
|
||||||
|
return translator.tracks_to_mpd_format(artists + albums + tracks)
|
||||||
|
|
||||||
|
|
||||||
@handle_request(r'^searchadd ' + QUERY_RE)
|
@handle_request(r'^searchadd ' + QUERY_RE)
|
||||||
|
|||||||
@ -115,6 +115,66 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase):
|
|||||||
|
|
||||||
|
|
||||||
class MusicDatabaseFindTest(protocol.BaseTestCase):
|
class MusicDatabaseFindTest(protocol.BaseTestCase):
|
||||||
|
def test_find_includes_fake_artist_and_album_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 "any" "foo"')
|
||||||
|
|
||||||
|
self.assertInResponse('file: dummy:artist:b')
|
||||||
|
self.assertInResponse('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_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 "artist" "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')],
|
||||||
|
artists=[Artist(uri='dummy:artist:b', name='B')],
|
||||||
|
tracks=[Track(uri='dummy:track:c', name='C')])
|
||||||
|
|
||||||
|
self.sendRequest('find "artist" "foo" "album" "bar"')
|
||||||
|
|
||||||
|
self.assertNotInResponse('file: dummy:artist:b')
|
||||||
|
self.assertNotInResponse('Title: Artist: B')
|
||||||
|
|
||||||
|
self.assertNotInResponse('file: dummy:album:a')
|
||||||
|
self.assertNotInResponse('Title: Album: A')
|
||||||
|
self.assertNotInResponse('Date: 2001')
|
||||||
|
|
||||||
|
self.assertInResponse('file: dummy:track:c')
|
||||||
|
self.assertInResponse('Title: C')
|
||||||
|
|
||||||
|
self.assertInResponse('OK')
|
||||||
|
|
||||||
def test_find_album(self):
|
def test_find_album(self):
|
||||||
self.sendRequest('find "album" "what"')
|
self.sendRequest('find "album" "what"')
|
||||||
self.assertInResponse('OK')
|
self.assertInResponse('OK')
|
||||||
@ -420,6 +480,23 @@ class MusicDatabaseListTest(protocol.BaseTestCase):
|
|||||||
|
|
||||||
|
|
||||||
class MusicDatabaseSearchTest(protocol.BaseTestCase):
|
class MusicDatabaseSearchTest(protocol.BaseTestCase):
|
||||||
|
def test_search(self):
|
||||||
|
self.backend.library.dummy_search_result = SearchResult(
|
||||||
|
albums=[Album(uri='dummy:album:a', name='A')],
|
||||||
|
artists=[Artist(uri='dummy:artist:b', name='B')],
|
||||||
|
tracks=[Track(uri='dummy:track:c', name='C')])
|
||||||
|
|
||||||
|
self.sendRequest('search "any" "foo"')
|
||||||
|
|
||||||
|
self.assertInResponse('file: dummy:album:a')
|
||||||
|
self.assertInResponse('Title: Album: A')
|
||||||
|
self.assertInResponse('file: dummy:artist:b')
|
||||||
|
self.assertInResponse('Title: Artist: B')
|
||||||
|
self.assertInResponse('file: dummy:track:c')
|
||||||
|
self.assertInResponse('Title: C')
|
||||||
|
|
||||||
|
self.assertInResponse('OK')
|
||||||
|
|
||||||
def test_search_album(self):
|
def test_search_album(self):
|
||||||
self.sendRequest('search "album" "analbum"')
|
self.sendRequest('search "album" "analbum"')
|
||||||
self.assertInResponse('OK')
|
self.assertInResponse('OK')
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user