Merge pull request #287 from jodal/feature/search-result-model

Add SearchResult model
This commit is contained in:
Stein Magnus Jodal 2012-12-23 09:47:11 -08:00
commit 58cbbe8cb3
11 changed files with 235 additions and 117 deletions

View File

@ -76,8 +76,17 @@ v0.11.0 (in development)
- Specified that :attr:`mopidy.models.Playlist.last_modified` should be in UTC. - Specified that :attr:`mopidy.models.Playlist.last_modified` should be in UTC.
- Added :class:`mopidy.models.SearchResult` model to encapsulate search results
consisting of more than just tracks.
*Core API:* *Core API:*
- Change the following methods to return :class:`mopidy.models.SearchResult`
objects which can include both track results and other results:
- :meth:`mopidy.core.LibraryController.find_exact`
- :meth:`mopidy.core.LibraryController.search`
- Change the following methods to accept either a dict with filters or kwargs. - Change the following methods to accept either a dict with filters or kwargs.
Previously they only accepted kwargs, which made them impossible to use from Previously they only accepted kwargs, which made them impossible to use from
the Mopidy.js through JSON-RPC, which doesn't support kwargs. the Mopidy.js through JSON-RPC, which doesn't support kwargs.

View File

@ -19,7 +19,7 @@ from __future__ import unicode_literals
import pykka import pykka
from mopidy.backends import base from mopidy.backends import base
from mopidy.models import Playlist from mopidy.models import Playlist, SearchResult
class DummyBackend(pykka.ThreadingActor, base.Backend): class DummyBackend(pykka.ThreadingActor, base.Backend):
@ -37,8 +37,8 @@ class DummyLibraryProvider(base.BaseLibraryProvider):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(DummyLibraryProvider, self).__init__(*args, **kwargs) super(DummyLibraryProvider, self).__init__(*args, **kwargs)
self.dummy_library = [] self.dummy_library = []
self.dummy_find_exact_result = [] self.dummy_find_exact_result = SearchResult()
self.dummy_search_result = [] self.dummy_search_result = SearchResult()
def find_exact(self, **query): def find_exact(self, **query):
return self.dummy_find_exact_result return self.dummy_find_exact_result

View File

@ -4,7 +4,7 @@ import logging
from mopidy import settings from mopidy import settings
from mopidy.backends import base from mopidy.backends import base
from mopidy.models import Album from mopidy.models import Album, SearchResult
from .translator import parse_mpd_tag_cache from .translator import parse_mpd_tag_cache
@ -70,7 +70,7 @@ class LocalLibraryProvider(base.BaseLibraryProvider):
result_tracks = filter(any_filter, result_tracks) result_tracks = filter(any_filter, result_tracks)
else: else:
raise LookupError('Invalid lookup field: %s' % field) raise LookupError('Invalid lookup field: %s' % field)
return result_tracks return SearchResult(uri='file:search', tracks=result_tracks)
def search(self, **query): def search(self, **query):
self._validate_query(query) self._validate_query(query)
@ -107,7 +107,7 @@ class LocalLibraryProvider(base.BaseLibraryProvider):
result_tracks = filter(any_filter, result_tracks) result_tracks = filter(any_filter, result_tracks)
else: else:
raise LookupError('Invalid lookup field: %s' % field) raise LookupError('Invalid lookup field: %s' % field)
return result_tracks return SearchResult(uri='file:search', tracks=result_tracks)
def _validate_query(self, query): def _validate_query(self, query):
for (_, values) in query.iteritems(): for (_, values) in query.iteritems():

View File

@ -2,13 +2,14 @@ from __future__ import unicode_literals
import logging import logging
import time import time
import urllib
import pykka import pykka
from spotify import Link, SpotifyError from spotify import Link, SpotifyError
from mopidy import settings from mopidy import settings
from mopidy.backends import base from mopidy.backends import base
from mopidy.models import Track from mopidy.models import Track, SearchResult
from . import translator from . import translator
@ -123,12 +124,16 @@ class SpotifyLibraryProvider(base.BaseLibraryProvider):
if not query: if not query:
return self._get_all_tracks() return self._get_all_tracks()
if 'uri' in query.keys(): uris = query.get('uri', [])
result = [] if uris:
for uri in query['uri']: tracks = []
tracks = self.lookup(uri) for uri in uris:
result += tracks tracks += self.lookup(uri)
return result if len(uris) == 1:
uri = uris[0]
else:
uri = 'spotify:search'
return SearchResult(uri=uri, tracks=tracks)
spotify_query = self._translate_search_query(query) spotify_query = self._translate_search_query(query)
logger.debug('Spotify search query: %s' % spotify_query) logger.debug('Spotify search query: %s' % spotify_query)
@ -136,12 +141,16 @@ class SpotifyLibraryProvider(base.BaseLibraryProvider):
future = pykka.ThreadingFuture() future = pykka.ThreadingFuture()
def callback(results, userdata=None): def callback(results, userdata=None):
# TODO Include results from results.albums(), etc. too search_result = SearchResult(
# TODO Consider launching a second search if results.total_tracks() uri='spotify:search:%s' % (
# is larger than len(results.tracks()) urllib.quote(results.query().encode('utf-8'))),
tracks = [ albums=[
translator.to_mopidy_track(t) for t in results.tracks()] translator.to_mopidy_album(a) for a in results.albums()],
future.set(tracks) artists=[
translator.to_mopidy_artist(a) for a in results.artists()],
tracks=[
translator.to_mopidy_track(t) for t in results.tracks()])
future.set(search_result)
if not self.backend.spotify.connected.wait(settings.SPOTIFY_TIMEOUT): if not self.backend.spotify.connected.wait(settings.SPOTIFY_TIMEOUT):
logger.debug('Not connected: Spotify search cancelled') logger.debug('Not connected: Spotify search cancelled')
@ -149,7 +158,7 @@ class SpotifyLibraryProvider(base.BaseLibraryProvider):
self.backend.spotify.session.search( self.backend.spotify.session.search(
spotify_query, callback, spotify_query, callback,
track_count=200, album_count=0, artist_count=0) album_count=200, artist_count=200, track_count=200)
try: try:
return future.get(timeout=settings.SPOTIFY_TIMEOUT) return future.get(timeout=settings.SPOTIFY_TIMEOUT)
@ -157,7 +166,7 @@ class SpotifyLibraryProvider(base.BaseLibraryProvider):
logger.debug( logger.debug(
'Timeout: Spotify search did not return in %ds', 'Timeout: Spotify search did not return in %ds',
settings.SPOTIFY_TIMEOUT) settings.SPOTIFY_TIMEOUT)
return [] return SearchResult(uri='spotify:search')
def _get_all_tracks(self): def _get_all_tracks(self):
# Since we can't search for the entire Spotify library, we return # Since we can't search for the entire Spotify library, we return
@ -165,7 +174,7 @@ class SpotifyLibraryProvider(base.BaseLibraryProvider):
tracks = [] tracks = []
for playlist in self.backend.playlists.playlists: for playlist in self.backend.playlists.playlists:
tracks += playlist.tracks tracks += playlist.tracks
return tracks return SearchResult(uri='spotify:search', tracks=tracks)
def _translate_search_query(self, mopidy_query): def _translate_search_query(self, mopidy_query):
spotify_query = [] spotify_query = []

View File

@ -1,6 +1,5 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import itertools
import urlparse import urlparse
import pykka import pykka
@ -37,13 +36,12 @@ class LibraryController(object):
:param query: one or more queries to search for :param query: one or more queries to search for
:type query: dict :type query: dict
:rtype: list of :class:`mopidy.models.Track` :rtype: list of :class:`mopidy.models.SearchResult`
""" """
query = query or kwargs query = query or kwargs
futures = [ futures = [
b.library.find_exact(**query) for b in self.backends.with_library] b.library.find_exact(**query) for b in self.backends.with_library]
results = pykka.get_all(futures) return pykka.get_all(futures)
return list(itertools.chain(*results))
def lookup(self, uri): def lookup(self, uri):
""" """
@ -98,10 +96,9 @@ class LibraryController(object):
:param query: one or more queries to search for :param query: one or more queries to search for
:type query: dict :type query: dict
:rtype: list of :class:`mopidy.models.Track` :rtype: list of :class:`mopidy.models.SearchResult`
""" """
query = query or kwargs query = query or kwargs
futures = [ futures = [
b.library.search(**query) for b in self.backends.with_library] b.library.search(**query) for b in self.backends.with_library]
results = pykka.get_all(futures) return pykka.get_all(futures)
return list(itertools.chain(*results))

View File

@ -1,5 +1,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import itertools
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
@ -10,6 +12,10 @@ QUERY_RE = (
r'[Tt]itle|[Aa]ny)"? "[^"]*"\s?)+)$') r'[Tt]itle|[Aa]ny)"? "[^"]*"\s?)+)$')
def _get_tracks(search_results):
return list(itertools.chain(*[r.tracks for r in search_results]))
@handle_request(r'^count "(?P<tag>[^"]+)" "(?P<needle>[^"]*)"$') @handle_request(r'^count "(?P<tag>[^"]+)" "(?P<needle>[^"]*)"$')
def count(context, tag, needle): def count(context, tag, needle):
""" """
@ -55,8 +61,8 @@ def find(context, mpd_query):
query = translator.query_from_mpd_search_format(mpd_query) query = translator.query_from_mpd_search_format(mpd_query)
except ValueError: except ValueError:
return return
result = context.core.library.find_exact(**query).get() results = context.core.library.find_exact(**query).get()
return translator.tracks_to_mpd_format(result) return translator.tracks_to_mpd_format(_get_tracks(results))
@handle_request(r'^findadd ' + QUERY_RE) @handle_request(r'^findadd ' + QUERY_RE)
@ -73,8 +79,8 @@ def findadd(context, mpd_query):
query = translator.query_from_mpd_search_format(mpd_query) query = translator.query_from_mpd_search_format(mpd_query)
except ValueError: except ValueError:
return return
result = context.core.library.find_exact(**query).get() results = context.core.library.find_exact(**query).get()
context.core.tracklist.add(result) context.core.tracklist.add(_get_tracks(results))
@handle_request( @handle_request(
@ -179,8 +185,8 @@ def list_(context, field, mpd_query=None):
def _list_artist(context, query): def _list_artist(context, query):
artists = set() artists = set()
tracks = context.core.library.find_exact(**query).get() results = context.core.library.find_exact(**query).get()
for track in tracks: for track in _get_tracks(results):
for artist in track.artists: for artist in track.artists:
if artist.name: if artist.name:
artists.add(('Artist', artist.name)) artists.add(('Artist', artist.name))
@ -189,8 +195,8 @@ def _list_artist(context, query):
def _list_album(context, query): def _list_album(context, query):
albums = set() albums = set()
tracks = context.core.library.find_exact(**query).get() results = context.core.library.find_exact(**query).get()
for track in tracks: for track in _get_tracks(results):
if track.album and track.album.name: if track.album and track.album.name:
albums.add(('Album', track.album.name)) albums.add(('Album', track.album.name))
return albums return albums
@ -198,8 +204,8 @@ def _list_album(context, query):
def _list_date(context, query): def _list_date(context, query):
dates = set() dates = set()
tracks = context.core.library.find_exact(**query).get() results = context.core.library.find_exact(**query).get()
for track in tracks: for track in _get_tracks(results):
if track.date: if track.date:
dates.add(('Date', track.date)) dates.add(('Date', track.date))
return dates return dates
@ -297,8 +303,8 @@ def search(context, mpd_query):
query = translator.query_from_mpd_search_format(mpd_query) query = translator.query_from_mpd_search_format(mpd_query)
except ValueError: except ValueError:
return return
result = context.core.library.search(**query).get() results = context.core.library.search(**query).get()
return translator.tracks_to_mpd_format(result) return translator.tracks_to_mpd_format(_get_tracks(results))
@handle_request(r'^searchadd ' + QUERY_RE) @handle_request(r'^searchadd ' + QUERY_RE)
@ -318,8 +324,8 @@ def searchadd(context, mpd_query):
query = translator.query_from_mpd_search_format(mpd_query) query = translator.query_from_mpd_search_format(mpd_query)
except ValueError: except ValueError:
return return
result = context.core.library.search(**query).get() results = context.core.library.search(**query).get()
context.core.tracklist.add(result) context.core.tracklist.add(_get_tracks(results))
@handle_request(r'^searchaddpl "(?P<playlist_name>[^"]+)" ' + QUERY_RE) @handle_request(r'^searchaddpl "(?P<playlist_name>[^"]+)" ' + QUERY_RE)
@ -341,14 +347,14 @@ def searchaddpl(context, playlist_name, mpd_query):
query = translator.query_from_mpd_search_format(mpd_query) query = translator.query_from_mpd_search_format(mpd_query)
except ValueError: except ValueError:
return return
result = context.core.library.search(**query).get() results = context.core.library.search(**query).get()
playlists = context.core.playlists.filter(name=playlist_name).get() playlists = context.core.playlists.filter(name=playlist_name).get()
if playlists: if playlists:
playlist = playlists[0] playlist = playlists[0]
else: else:
playlist = context.core.playlists.create(playlist_name).get() playlist = context.core.playlists.create(playlist_name).get()
tracks = list(playlist.tracks) + result tracks = list(playlist.tracks) + _get_tracks(results)
playlist = playlist.copy(tracks=tracks) playlist = playlist.copy(tracks=tracks)
context.core.playlists.save(playlist) context.core.playlists.save(playlist)

View File

@ -318,3 +318,34 @@ class Playlist(ImmutableObject):
def length(self): def length(self):
"""The number of tracks in the playlist. Read-only.""" """The number of tracks in the playlist. Read-only."""
return len(self.tracks) return len(self.tracks)
class SearchResult(ImmutableObject):
"""
:param uri: search result URI
:type uri: string
:param tracks: matching tracks
:type tracks: list of :class:`Track` elements
:param artists: matching artists
:type artists: list of :class:`Artist` elements
:param albums: matching albums
:type albums: list of :class:`Album` elements
"""
# The search result URI. Read-only.
uri = None
# The tracks matching the search query. Read-only.
tracks = tuple()
# The artists matching the search query. Read-only.
artists = tuple()
# The albums matching the search query. Read-only.
albums = tuple()
def __init__(self, *args, **kwargs):
self.__dict__['tracks'] = tuple(kwargs.pop('tracks', []))
self.__dict__['artists'] = tuple(kwargs.pop('artists', []))
self.__dict__['albums'] = tuple(kwargs.pop('albums', []))
super(SearchResult, self).__init__(*args, **kwargs)

View File

@ -53,53 +53,53 @@ class LibraryControllerTest(object):
def test_find_exact_no_hits(self): 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, []) self.assertEqual(list(result[0].tracks), [])
result = self.library.find_exact(artist=['unknown artist']) result = self.library.find_exact(artist=['unknown artist'])
self.assertEqual(result, []) self.assertEqual(list(result[0].tracks), [])
result = self.library.find_exact(album=['unknown artist']) result = self.library.find_exact(album=['unknown artist'])
self.assertEqual(result, []) self.assertEqual(list(result[0].tracks), [])
def test_find_exact_uri(self): def test_find_exact_uri(self):
track_1_uri = 'file://' + path_to_data_dir('uri1') track_1_uri = 'file://' + path_to_data_dir('uri1')
result = self.library.find_exact(uri=track_1_uri) result = self.library.find_exact(uri=track_1_uri)
self.assertEqual(result, self.tracks[:1]) self.assertEqual(list(result[0].tracks), self.tracks[:1])
track_2_uri = 'file://' + path_to_data_dir('uri2') track_2_uri = 'file://' + path_to_data_dir('uri2')
result = self.library.find_exact(uri=track_2_uri) result = self.library.find_exact(uri=track_2_uri)
self.assertEqual(result, self.tracks[1:2]) self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_find_exact_track(self): def test_find_exact_track(self):
result = self.library.find_exact(track=['track1']) result = self.library.find_exact(track=['track1'])
self.assertEqual(result, self.tracks[:1]) self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.find_exact(track=['track2']) result = self.library.find_exact(track=['track2'])
self.assertEqual(result, self.tracks[1:2]) self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_find_exact_artist(self): def test_find_exact_artist(self):
result = self.library.find_exact(artist=['artist1']) result = self.library.find_exact(artist=['artist1'])
self.assertEqual(result, self.tracks[:1]) self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.find_exact(artist=['artist2']) result = self.library.find_exact(artist=['artist2'])
self.assertEqual(result, self.tracks[1:2]) self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_find_exact_album(self): def test_find_exact_album(self):
result = self.library.find_exact(album=['album1']) result = self.library.find_exact(album=['album1'])
self.assertEqual(result, self.tracks[:1]) self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.find_exact(album=['album2']) result = self.library.find_exact(album=['album2'])
self.assertEqual(result, self.tracks[1:2]) self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_find_exact_date(self): def test_find_exact_date(self):
result = self.library.find_exact(date=['2001']) result = self.library.find_exact(date=['2001'])
self.assertEqual(result, []) self.assertEqual(list(result[0].tracks), [])
result = self.library.find_exact(date=['2001-02-03']) result = self.library.find_exact(date=['2001-02-03'])
self.assertEqual(result, self.tracks[:1]) self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.find_exact(date=['2002']) result = self.library.find_exact(date=['2002'])
self.assertEqual(result, self.tracks[1:2]) self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_find_exact_wrong_type(self): def test_find_exact_wrong_type(self):
test = lambda: self.library.find_exact(wrong=['test']) test = lambda: self.library.find_exact(wrong=['test'])
@ -117,70 +117,70 @@ class LibraryControllerTest(object):
def test_search_no_hits(self): def test_search_no_hits(self):
result = self.library.search(track=['unknown track']) result = self.library.search(track=['unknown track'])
self.assertEqual(result, []) self.assertEqual(list(result[0].tracks), [])
result = self.library.search(artist=['unknown artist']) result = self.library.search(artist=['unknown artist'])
self.assertEqual(result, []) self.assertEqual(list(result[0].tracks), [])
result = self.library.search(album=['unknown artist']) result = self.library.search(album=['unknown artist'])
self.assertEqual(result, []) self.assertEqual(list(result[0].tracks), [])
result = self.library.search(uri=['unknown']) result = self.library.search(uri=['unknown'])
self.assertEqual(result, []) self.assertEqual(list(result[0].tracks), [])
result = self.library.search(any=['unknown']) result = self.library.search(any=['unknown'])
self.assertEqual(result, []) self.assertEqual(list(result[0].tracks), [])
def test_search_uri(self): def test_search_uri(self):
result = self.library.search(uri=['RI1']) result = self.library.search(uri=['RI1'])
self.assertEqual(result, self.tracks[:1]) self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.search(uri=['RI2']) result = self.library.search(uri=['RI2'])
self.assertEqual(result, self.tracks[1:2]) self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_search_track(self): def test_search_track(self):
result = self.library.search(track=['Rack1']) result = self.library.search(track=['Rack1'])
self.assertEqual(result, self.tracks[:1]) self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.search(track=['Rack2']) result = self.library.search(track=['Rack2'])
self.assertEqual(result, self.tracks[1:2]) self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_search_artist(self): def test_search_artist(self):
result = self.library.search(artist=['Tist1']) result = self.library.search(artist=['Tist1'])
self.assertEqual(result, self.tracks[:1]) self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.search(artist=['Tist2']) result = self.library.search(artist=['Tist2'])
self.assertEqual(result, self.tracks[1:2]) self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_search_album(self): def test_search_album(self):
result = self.library.search(album=['Bum1']) result = self.library.search(album=['Bum1'])
self.assertEqual(result, self.tracks[:1]) self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.search(album=['Bum2']) result = self.library.search(album=['Bum2'])
self.assertEqual(result, self.tracks[1:2]) self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_search_date(self): def test_search_date(self):
result = self.library.search(date=['2001']) result = self.library.search(date=['2001'])
self.assertEqual(result, self.tracks[:1]) self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.search(date=['2001-02-03']) result = self.library.search(date=['2001-02-03'])
self.assertEqual(result, self.tracks[:1]) self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.search(date=['2001-02-04']) result = self.library.search(date=['2001-02-04'])
self.assertEqual(result, []) self.assertEqual(list(result[0].tracks), [])
result = self.library.search(date=['2002']) result = self.library.search(date=['2002'])
self.assertEqual(result, self.tracks[1:2]) self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_search_any(self): def test_search_any(self):
result = self.library.search(any=['Tist1']) result = self.library.search(any=['Tist1'])
self.assertEqual(result, self.tracks[:1]) self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.search(any=['Rack1']) result = self.library.search(any=['Rack1'])
self.assertEqual(result, self.tracks[:1]) self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.search(any=['Bum1']) result = self.library.search(any=['Bum1'])
self.assertEqual(result, self.tracks[:1]) self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.search(any=['RI1']) result = self.library.search(any=['RI1'])
self.assertEqual(result, self.tracks[:1]) self.assertEqual(list(result[0].tracks), self.tracks[:1])
def test_search_wrong_type(self): def test_search_wrong_type(self):
test = lambda: self.library.search(wrong=['test']) test = lambda: self.library.search(wrong=['test'])

View File

@ -4,7 +4,7 @@ import mock
from mopidy.backends import base from mopidy.backends import base
from mopidy.core import Core from mopidy.core import Core
from mopidy.models import Track from mopidy.models import SearchResult, Track
from tests import unittest from tests import unittest
@ -75,59 +75,71 @@ class CoreLibraryTest(unittest.TestCase):
def test_find_exact_combines_results_from_all_backends(self): def test_find_exact_combines_results_from_all_backends(self):
track1 = Track(uri='dummy1:a') track1 = Track(uri='dummy1:a')
track2 = Track(uri='dummy2:a') track2 = Track(uri='dummy2:a')
self.library1.find_exact().get.return_value = [track1] result1 = SearchResult(tracks=[track1])
result2 = SearchResult(tracks=[track2])
self.library1.find_exact().get.return_value = result1
self.library1.find_exact.reset_mock() self.library1.find_exact.reset_mock()
self.library2.find_exact().get.return_value = [track2] self.library2.find_exact().get.return_value = result2
self.library2.find_exact.reset_mock() self.library2.find_exact.reset_mock()
result = self.core.library.find_exact(any=['a']) result = self.core.library.find_exact(any=['a'])
self.assertIn(track1, result) self.assertIn(result1, result)
self.assertIn(track2, result) self.assertIn(result2, result)
self.library1.find_exact.assert_called_once_with(any=['a']) self.library1.find_exact.assert_called_once_with(any=['a'])
self.library2.find_exact.assert_called_once_with(any=['a']) self.library2.find_exact.assert_called_once_with(any=['a'])
def test_find_accepts_query_dict_instead_of_kwargs(self): def test_find_accepts_query_dict_instead_of_kwargs(self):
track1 = Track(uri='dummy1:a') track1 = Track(uri='dummy1:a')
track2 = Track(uri='dummy2:a') track2 = Track(uri='dummy2:a')
self.library1.find_exact().get.return_value = [track1] result1 = SearchResult(tracks=[track1])
result2 = SearchResult(tracks=[track2])
self.library1.find_exact().get.return_value = result1
self.library1.find_exact.reset_mock() self.library1.find_exact.reset_mock()
self.library2.find_exact().get.return_value = [track2] self.library2.find_exact().get.return_value = result2
self.library2.find_exact.reset_mock() self.library2.find_exact.reset_mock()
result = self.core.library.find_exact(dict(any=['a'])) result = self.core.library.find_exact(dict(any=['a']))
self.assertIn(track1, result) self.assertIn(result1, result)
self.assertIn(track2, result) self.assertIn(result2, result)
self.library1.find_exact.assert_called_once_with(any=['a']) self.library1.find_exact.assert_called_once_with(any=['a'])
self.library2.find_exact.assert_called_once_with(any=['a']) self.library2.find_exact.assert_called_once_with(any=['a'])
def test_search_combines_results_from_all_backends(self): def test_search_combines_results_from_all_backends(self):
track1 = Track(uri='dummy1:a') track1 = Track(uri='dummy1:a')
track2 = Track(uri='dummy2:a') track2 = Track(uri='dummy2:a')
self.library1.search().get.return_value = [track1] result1 = SearchResult(tracks=[track1])
result2 = SearchResult(tracks=[track2])
self.library1.search().get.return_value = result1
self.library1.search.reset_mock() self.library1.search.reset_mock()
self.library2.search().get.return_value = [track2] self.library2.search().get.return_value = result2
self.library2.search.reset_mock() self.library2.search.reset_mock()
result = self.core.library.search(any=['a']) result = self.core.library.search(any=['a'])
self.assertIn(track1, result) self.assertIn(result1, result)
self.assertIn(track2, result) self.assertIn(result2, result)
self.library1.search.assert_called_once_with(any=['a']) self.library1.search.assert_called_once_with(any=['a'])
self.library2.search.assert_called_once_with(any=['a']) self.library2.search.assert_called_once_with(any=['a'])
def test_search_accepts_query_dict_instead_of_kwargs(self): def test_search_accepts_query_dict_instead_of_kwargs(self):
track1 = Track(uri='dummy1:a') track1 = Track(uri='dummy1:a')
track2 = Track(uri='dummy2:a') track2 = Track(uri='dummy2:a')
self.library1.search().get.return_value = [track1] result1 = SearchResult(tracks=[track1])
result2 = SearchResult(tracks=[track2])
self.library1.search().get.return_value = result1
self.library1.search.reset_mock() self.library1.search.reset_mock()
self.library2.search().get.return_value = [track2] self.library2.search().get.return_value = result2
self.library2.search.reset_mock() self.library2.search.reset_mock()
result = self.core.library.search(dict(any=['a'])) result = self.core.library.search(dict(any=['a']))
self.assertIn(track1, result) self.assertIn(result1, result)
self.assertIn(track2, result) self.assertIn(result2, result)
self.library1.search.assert_called_once_with(any=['a']) self.library1.search.assert_called_once_with(any=['a'])
self.library2.search.assert_called_once_with(any=['a']) self.library2.search.assert_called_once_with(any=['a'])

View File

@ -1,6 +1,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from mopidy.models import Album, Artist, Track from mopidy.models import Album, Artist, SearchResult, Track
from tests.frontends.mpd import protocol from tests.frontends.mpd import protocol
@ -13,9 +13,8 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase):
self.assertInResponse('OK') self.assertInResponse('OK')
def test_findadd(self): def test_findadd(self):
self.backend.library.dummy_find_exact_result = [ self.backend.library.dummy_find_exact_result = SearchResult(
Track(uri='dummy:a', name='A'), tracks=[Track(uri='dummy:a', name='A')])
]
self.assertEqual(self.core.tracklist.length.get(), 0) self.assertEqual(self.core.tracklist.length.get(), 0)
self.sendRequest('findadd "title" "A"') self.sendRequest('findadd "title" "A"')
@ -25,9 +24,8 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase):
self.assertInResponse('OK') self.assertInResponse('OK')
def test_searchadd(self): def test_searchadd(self):
self.backend.library.dummy_search_result = [ self.backend.library.dummy_search_result = SearchResult(
Track(uri='dummy:a', name='A'), tracks=[Track(uri='dummy:a', name='A')])
]
self.assertEqual(self.core.tracklist.length.get(), 0) self.assertEqual(self.core.tracklist.length.get(), 0)
self.sendRequest('searchadd "title" "a"') self.sendRequest('searchadd "title" "a"')
@ -43,9 +41,8 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase):
Track(uri='dummy:y', name='y'), Track(uri='dummy:y', name='y'),
]) ])
self.core.playlists.save(playlist) self.core.playlists.save(playlist)
self.backend.library.dummy_search_result = [ self.backend.library.dummy_search_result = SearchResult(
Track(uri='dummy:a', name='A'), tracks=[Track(uri='dummy:a', name='A')])
]
playlists = self.core.playlists.filter(name='my favs').get() playlists = self.core.playlists.filter(name='my favs').get()
self.assertEqual(len(playlists), 1) self.assertEqual(len(playlists), 1)
self.assertEqual(len(playlists[0].tracks), 2) self.assertEqual(len(playlists[0].tracks), 2)
@ -61,9 +58,8 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase):
self.assertInResponse('OK') self.assertInResponse('OK')
def test_searchaddpl_creates_missing_playlist(self): def test_searchaddpl_creates_missing_playlist(self):
self.backend.library.dummy_search_result = [ self.backend.library.dummy_search_result = SearchResult(
Track(uri='dummy:a', name='A'), tracks=[Track(uri='dummy:a', name='A')])
]
self.assertEqual( self.assertEqual(
len(self.core.playlists.filter(name='my favs').get()), 0) len(self.core.playlists.filter(name='my favs').get()), 0)
@ -185,6 +181,17 @@ class MusicDatabaseFindTest(protocol.BaseTestCase):
class MusicDatabaseListTest(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.sendRequest('list "artist" "artist" "foo"')
self.assertInResponse('Artist: A Artist')
self.assertInResponse('OK')
def test_list_foo_returns_ack(self): def test_list_foo_returns_ack(self):
self.sendRequest('list "foo"') self.sendRequest('list "foo"')
self.assertEqualResponse('ACK [2@0] {list} incorrect arguments') self.assertEqualResponse('ACK [2@0] {list} incorrect arguments')
@ -242,8 +249,8 @@ class MusicDatabaseListTest(protocol.BaseTestCase):
self.assertInResponse('OK') self.assertInResponse('OK')
def test_list_artist_should_not_return_artists_without_names(self): def test_list_artist_should_not_return_artists_without_names(self):
self.backend.library.dummy_find_exact_result = [ self.backend.library.dummy_find_exact_result = SearchResult(
Track(artists=[Artist(name='')])] tracks=[Track(artists=[Artist(name='')])])
self.sendRequest('list "artist"') self.sendRequest('list "artist"')
self.assertNotInResponse('Artist: ') self.assertNotInResponse('Artist: ')
@ -301,8 +308,8 @@ class MusicDatabaseListTest(protocol.BaseTestCase):
self.assertInResponse('OK') self.assertInResponse('OK')
def test_list_album_should_not_return_albums_without_names(self): def test_list_album_should_not_return_albums_without_names(self):
self.backend.library.dummy_find_exact_result = [ self.backend.library.dummy_find_exact_result = SearchResult(
Track(album=Album(name=''))] tracks=[Track(album=Album(name=''))])
self.sendRequest('list "album"') self.sendRequest('list "album"')
self.assertNotInResponse('Album: ') self.assertNotInResponse('Album: ')
@ -356,7 +363,8 @@ class MusicDatabaseListTest(protocol.BaseTestCase):
self.assertInResponse('OK') self.assertInResponse('OK')
def test_list_date_should_not_return_blank_dates(self): def test_list_date_should_not_return_blank_dates(self):
self.backend.library.dummy_find_exact_result = [Track(date='')] self.backend.library.dummy_find_exact_result = SearchResult(
tracks=[Track(date='')])
self.sendRequest('list "date"') self.sendRequest('list "date"')
self.assertNotInResponse('Date: ') self.assertNotInResponse('Date: ')

View File

@ -4,7 +4,7 @@ import datetime
import json import json
from mopidy.models import ( from mopidy.models import (
Artist, Album, TlTrack, Track, Playlist, Artist, Album, TlTrack, Track, Playlist, SearchResult,
ModelJSONEncoder, model_json_decoder) ModelJSONEncoder, model_json_decoder)
from tests import unittest from tests import unittest
@ -862,10 +862,56 @@ class PlaylistTest(unittest.TestCase):
def test_ne(self): def test_ne(self):
playlist1 = Playlist( playlist1 = Playlist(
uri='uri1', name='name2', tracks=[Track(uri='uri1')], uri='uri1', name='name1', tracks=[Track(uri='uri1')],
last_modified=1) last_modified=1)
playlist2 = Playlist( playlist2 = Playlist(
uri='uri2', name='name2', tracks=[Track(uri='uri2')], uri='uri2', name='name2', tracks=[Track(uri='uri2')],
last_modified=2) last_modified=2)
self.assertNotEqual(playlist1, playlist2) self.assertNotEqual(playlist1, playlist2)
self.assertNotEqual(hash(playlist1), hash(playlist2)) self.assertNotEqual(hash(playlist1), hash(playlist2))
class SearchResultTest(unittest.TestCase):
def test_uri(self):
uri = 'an_uri'
result = SearchResult(uri=uri)
self.assertEqual(result.uri, uri)
self.assertRaises(AttributeError, setattr, result, 'uri', None)
def test_tracks(self):
tracks = [Track(), Track(), Track()]
result = SearchResult(tracks=tracks)
self.assertEqual(list(result.tracks), tracks)
self.assertRaises(AttributeError, setattr, result, 'tracks', None)
def test_artists(self):
artists = [Artist(), Artist(), Artist()]
result = SearchResult(artists=artists)
self.assertEqual(list(result.artists), artists)
self.assertRaises(AttributeError, setattr, result, 'artists', None)
def test_albums(self):
albums = [Album(), Album(), Album()]
result = SearchResult(albums=albums)
self.assertEqual(list(result.albums), albums)
self.assertRaises(AttributeError, setattr, result, 'albums', None)
def test_invalid_kwarg(self):
test = lambda: SearchResult(foo='baz')
self.assertRaises(TypeError, test)
def test_repr_without_results(self):
self.assertEquals(
"SearchResult(albums=[], artists=[], tracks=[], uri=u'uri')",
repr(SearchResult(uri='uri')))
def test_serialize_without_results(self):
self.assertDictEqual(
{'__model__': 'SearchResult', 'uri': 'uri'},
SearchResult(uri='uri').serialize())
def test_to_json_and_back(self):
result1 = SearchResult(uri='uri')
serialized = json.dumps(result1, cls=ModelJSONEncoder)
result2 = json.loads(serialized, object_hook=model_json_decoder)
self.assertEqual(result1, result2)