Merge branch 'develop' into feature/limit-spotify-data-pushing

This commit is contained in:
Stein Magnus Jodal 2012-12-24 12:13:29 +01:00
commit 610a04bf6c
17 changed files with 414 additions and 134 deletions

View File

@ -5,9 +5,33 @@ Changes
This change log is used to track all major changes to Mopidy.
v0.11.0 (in development)
v0.12.0 (in development)
========================
(in development)
**Spotify**
- Let GStreamer handle time position tracking and seeks. (Fixes: :issue:`191`)
v0.11.0 (2012-12-24)
====================
In celebration of Mopidy's three year anniversary December 23, we're releasing
Mopidy 0.11. This release brings several improvements, most notably better
search which now includes matching artists and albums from Spotify in the
search results.
**Settings**
- The settings validator now complains if a setting which expects a tuple of
values (e.g. :attr:`mopidy.settings.BACKENDS`,
:attr:`mopidy.settings.FRONTENDS`) has a non-iterable value. This typically
happens because the setting value contains a single value and one has
forgotten to add a comma after the string, making the value a tuple. (Fixes:
:issue:`278`)
**Spotify backend**
- Add :attr:`mopidy.settings.SPOTIFY_TIMEOUT` setting which allows you to
@ -61,14 +85,29 @@ v0.11.0 (in development)
- Make ``seek`` and ``seekid`` not restart the current track before seeking in
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**
*Models:*
- 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:*
- 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.
Previously they only accepted kwargs, which made them impossible to use from
the Mopidy.js through JSON-RPC, which doesn't support kwargs.

9
fabfile.py vendored
View File

@ -1,14 +1,15 @@
from fabric.api import local
def test():
local('nosetests tests/')
def test(path=None):
path = path or 'tests/'
local('nosetests ' + path)
def autotest():
def autotest(path=None):
while True:
local('clear')
test()
test(path)
local(
'inotifywait -q -e create -e modify -e delete '
'--exclude ".*\.(pyc|sw.)" -r mopidy/ tests/')

View File

@ -23,7 +23,7 @@ if (isinstance(pykka.__version__, basestring)
warnings.filterwarnings('ignore', 'could not open display')
__version__ = '0.10.0'
__version__ = '0.11.0'
from mopidy import settings as default_settings_module

View File

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

View File

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

View File

@ -2,18 +2,21 @@ from __future__ import unicode_literals
import logging
import time
import urllib
import pykka
from spotify import Link, SpotifyError
from mopidy import settings
from mopidy.backends import base
from mopidy.models import Track
from mopidy.models import Track, SearchResult
from . import translator
logger = logging.getLogger('mopidy.backends.spotify')
TRACK_AVAILABLE = 1
class SpotifyTrack(Track):
"""Proxy object for unloaded Spotify tracks."""
@ -83,7 +86,10 @@ class SpotifyLibraryProvider(base.BaseLibraryProvider):
track = Link.from_string(uri).as_track()
self._wait_for_object_to_load(track)
if track.is_loaded():
return [SpotifyTrack(track=track)]
if track.availability() == TRACK_AVAILABLE:
return [SpotifyTrack(track=track)]
else:
return []
else:
return [SpotifyTrack(uri=uri)]
@ -91,18 +97,24 @@ class SpotifyLibraryProvider(base.BaseLibraryProvider):
album = Link.from_string(uri).as_album()
album_browser = self.backend.spotify.session.browse_album(album)
self._wait_for_object_to_load(album_browser)
return [SpotifyTrack(track=t) for t in album_browser]
return [
SpotifyTrack(track=t)
for t in album_browser if t.availability() == TRACK_AVAILABLE]
def _lookup_artist(self, uri):
artist = Link.from_string(uri).as_artist()
artist_browser = self.backend.spotify.session.browse_artist(artist)
self._wait_for_object_to_load(artist_browser)
return [SpotifyTrack(track=t) for t in artist_browser]
return [
SpotifyTrack(track=t)
for t in artist_browser if t.availability() == TRACK_AVAILABLE]
def _lookup_playlist(self, uri):
playlist = Link.from_string(uri).as_playlist()
self._wait_for_object_to_load(playlist)
return [SpotifyTrack(track=t) for t in playlist]
return [
SpotifyTrack(track=t)
for t in playlist if t.availability() == TRACK_AVAILABLE]
def _wait_for_object_to_load(
self, spotify_obj, timeout=settings.SPOTIFY_TIMEOUT):
@ -123,12 +135,16 @@ class SpotifyLibraryProvider(base.BaseLibraryProvider):
if not query:
return self._get_all_tracks()
if 'uri' in query.keys():
result = []
for uri in query['uri']:
tracks = self.lookup(uri)
result += tracks
return result
uris = query.get('uri', [])
if uris:
tracks = []
for uri in uris:
tracks += self.lookup(uri)
if len(uris) == 1:
uri = uris[0]
else:
uri = 'spotify:search'
return SearchResult(uri=uri, tracks=tracks)
spotify_query = self._translate_search_query(query)
logger.debug('Spotify search query: %s' % spotify_query)
@ -136,20 +152,24 @@ class SpotifyLibraryProvider(base.BaseLibraryProvider):
future = pykka.ThreadingFuture()
def callback(results, userdata=None):
# TODO Include results from results.albums(), etc. too
# TODO Consider launching a second search if results.total_tracks()
# is larger than len(results.tracks())
tracks = [
translator.to_mopidy_track(t) for t in results.tracks()]
future.set(tracks)
search_result = SearchResult(
uri='spotify:search:%s' % (
urllib.quote(results.query().encode('utf-8'))),
albums=[
translator.to_mopidy_album(a) for a in results.albums()],
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):
logger.debug('Not connected: Spotify search cancelled')
return []
return SearchResult(uri='spotify:search')
self.backend.spotify.session.search(
spotify_query, callback,
track_count=200, album_count=0, artist_count=0)
album_count=200, artist_count=200, track_count=200)
try:
return future.get(timeout=settings.SPOTIFY_TIMEOUT)
@ -157,7 +177,7 @@ class SpotifyLibraryProvider(base.BaseLibraryProvider):
logger.debug(
'Timeout: Spotify search did not return in %ds',
settings.SPOTIFY_TIMEOUT)
return []
return SearchResult(uri='spotify:search')
def _get_all_tracks(self):
# Since we can't search for the entire Spotify library, we return
@ -165,7 +185,7 @@ class SpotifyLibraryProvider(base.BaseLibraryProvider):
tracks = []
for playlist in self.backend.playlists.playlists:
tracks += playlist.tracks
return tracks
return SearchResult(uri='spotify:search', tracks=tracks)
def _translate_search_query(self, mopidy_query):
spotify_query = []

View File

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

View File

@ -1,5 +1,9 @@
from __future__ import unicode_literals
import functools
import itertools
from mopidy.models import Track
from mopidy.frontends.mpd import translator
from mopidy.frontends.mpd.exceptions import MpdNotImplemented
from mopidy.frontends.mpd.protocol import handle_request, stored_playlists
@ -10,6 +14,31 @@ QUERY_RE = (
r'[Tt]itle|[Aa]ny)"? "[^"]*"\s?)+)$')
def _get_field(field, 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>[^"]*)"$')
def count(context, tag, needle):
"""
@ -55,8 +84,14 @@ def find(context, mpd_query):
query = translator.query_from_mpd_search_format(mpd_query)
except ValueError:
return
result = context.core.library.find_exact(**query).get()
return translator.tracks_to_mpd_format(result)
results = context.core.library.find_exact(**query).get()
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)
@ -73,8 +108,8 @@ def findadd(context, mpd_query):
query = translator.query_from_mpd_search_format(mpd_query)
except ValueError:
return
result = context.core.library.find_exact(**query).get()
context.core.tracklist.add(result)
results = context.core.library.find_exact(**query).get()
context.core.tracklist.add(_get_tracks(results))
@handle_request(
@ -179,8 +214,8 @@ def list_(context, field, mpd_query=None):
def _list_artist(context, query):
artists = set()
tracks = context.core.library.find_exact(**query).get()
for track in tracks:
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))
@ -189,8 +224,8 @@ def _list_artist(context, query):
def _list_album(context, query):
albums = set()
tracks = context.core.library.find_exact(**query).get()
for track in tracks:
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
@ -198,8 +233,8 @@ def _list_album(context, query):
def _list_date(context, query):
dates = set()
tracks = context.core.library.find_exact(**query).get()
for track in tracks:
results = context.core.library.find_exact(**query).get()
for track in _get_tracks(results):
if track.date:
dates.add(('Date', track.date))
return dates
@ -297,8 +332,11 @@ def search(context, mpd_query):
query = translator.query_from_mpd_search_format(mpd_query)
except ValueError:
return
result = context.core.library.search(**query).get()
return translator.tracks_to_mpd_format(result)
results = context.core.library.search(**query).get()
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)
@ -318,8 +356,8 @@ def searchadd(context, mpd_query):
query = translator.query_from_mpd_search_format(mpd_query)
except ValueError:
return
result = context.core.library.search(**query).get()
context.core.tracklist.add(result)
results = context.core.library.search(**query).get()
context.core.tracklist.add(_get_tracks(results))
@handle_request(r'^searchaddpl "(?P<playlist_name>[^"]+)" ' + QUERY_RE)
@ -341,14 +379,14 @@ def searchaddpl(context, playlist_name, mpd_query):
query = translator.query_from_mpd_search_format(mpd_query)
except ValueError:
return
result = context.core.library.search(**query).get()
results = context.core.library.search(**query).get()
playlists = context.core.playlists.filter(name=playlist_name).get()
if playlists:
playlist = playlists[0]
else:
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)
context.core.playlists.save(playlist)

View File

@ -329,8 +329,7 @@ def seek(context, songpos, seconds):
- issues ``seek 1 120`` without quotes around the arguments.
"""
songpos = int(songpos)
if context.core.playback.tracklist_position.get() != songpos:
if context.core.playback.tracklist_position.get() != int(songpos):
playpos(context, songpos)
context.core.playback.seek(int(seconds) * 1000).get()
@ -344,9 +343,8 @@ def seekid(context, tlid, seconds):
Seeks to the position ``TIME`` (in seconds) of song ``SONGID``.
"""
tlid = int(tlid)
tl_track = context.core.playback.current_tl_track.get()
if not tl_track or tl_track.tlid != tlid:
if not tl_track or tl_track.tlid != int(tlid):
playid(context, tlid)
context.core.playback.seek(int(seconds) * 1000).get()

View File

@ -318,3 +318,34 @@ class Playlist(ImmutableObject):
def length(self):
"""The number of tracks in the playlist. Read-only."""
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

@ -172,6 +172,10 @@ def validate_settings(defaults, settings):
'bin in OUTPUT.')
elif setting in list_of_one_or_more:
if not hasattr(value, '__iter__'):
errors[setting] = (
'Must be a tuple. '
"Remember the comma after single values: (u'value',)")
if not value:
errors[setting] = 'Must contain at least one value.'

View File

@ -53,53 +53,53 @@ class LibraryControllerTest(object):
def test_find_exact_no_hits(self):
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'])
self.assertEqual(result, [])
self.assertEqual(list(result[0].tracks), [])
result = self.library.find_exact(album=['unknown artist'])
self.assertEqual(result, [])
self.assertEqual(list(result[0].tracks), [])
def test_find_exact_uri(self):
track_1_uri = 'file://' + path_to_data_dir('uri1')
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')
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):
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'])
self.assertEqual(result, self.tracks[1:2])
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_find_exact_artist(self):
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'])
self.assertEqual(result, self.tracks[1:2])
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_find_exact_album(self):
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'])
self.assertEqual(result, self.tracks[1:2])
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_find_exact_date(self):
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'])
self.assertEqual(result, self.tracks[:1])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
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):
test = lambda: self.library.find_exact(wrong=['test'])
@ -117,70 +117,70 @@ class LibraryControllerTest(object):
def test_search_no_hits(self):
result = self.library.search(track=['unknown track'])
self.assertEqual(result, [])
self.assertEqual(list(result[0].tracks), [])
result = self.library.search(artist=['unknown artist'])
self.assertEqual(result, [])
self.assertEqual(list(result[0].tracks), [])
result = self.library.search(album=['unknown artist'])
self.assertEqual(result, [])
self.assertEqual(list(result[0].tracks), [])
result = self.library.search(uri=['unknown'])
self.assertEqual(result, [])
self.assertEqual(list(result[0].tracks), [])
result = self.library.search(any=['unknown'])
self.assertEqual(result, [])
self.assertEqual(list(result[0].tracks), [])
def test_search_uri(self):
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'])
self.assertEqual(result, self.tracks[1:2])
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_search_track(self):
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'])
self.assertEqual(result, self.tracks[1:2])
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_search_artist(self):
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'])
self.assertEqual(result, self.tracks[1:2])
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_search_album(self):
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'])
self.assertEqual(result, self.tracks[1:2])
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_search_date(self):
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'])
self.assertEqual(result, self.tracks[:1])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.search(date=['2001-02-04'])
self.assertEqual(result, [])
self.assertEqual(list(result[0].tracks), [])
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):
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'])
self.assertEqual(result, self.tracks[:1])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
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'])
self.assertEqual(result, self.tracks[:1])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
def test_search_wrong_type(self):
test = lambda: self.library.search(wrong=['test'])

View File

@ -4,7 +4,7 @@ import mock
from mopidy.backends import base
from mopidy.core import Core
from mopidy.models import Track
from mopidy.models import SearchResult, Track
from tests import unittest
@ -75,59 +75,71 @@ class CoreLibraryTest(unittest.TestCase):
def test_find_exact_combines_results_from_all_backends(self):
track1 = Track(uri='dummy1: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.library2.find_exact().get.return_value = [track2]
self.library2.find_exact().get.return_value = result2
self.library2.find_exact.reset_mock()
result = self.core.library.find_exact(any=['a'])
self.assertIn(track1, result)
self.assertIn(track2, result)
self.assertIn(result1, result)
self.assertIn(result2, result)
self.library1.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):
track1 = Track(uri='dummy1: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.library2.find_exact().get.return_value = [track2]
self.library2.find_exact().get.return_value = result2
self.library2.find_exact.reset_mock()
result = self.core.library.find_exact(dict(any=['a']))
self.assertIn(track1, result)
self.assertIn(track2, result)
self.assertIn(result1, result)
self.assertIn(result2, result)
self.library1.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):
track1 = Track(uri='dummy1: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.library2.search().get.return_value = [track2]
self.library2.search().get.return_value = result2
self.library2.search.reset_mock()
result = self.core.library.search(any=['a'])
self.assertIn(track1, result)
self.assertIn(track2, result)
self.assertIn(result1, result)
self.assertIn(result2, result)
self.library1.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):
track1 = Track(uri='dummy1: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.library2.search().get.return_value = [track2]
self.library2.search().get.return_value = result2
self.library2.search.reset_mock()
result = self.core.library.search(dict(any=['a']))
self.assertIn(track1, result)
self.assertIn(track2, result)
self.assertIn(result1, result)
self.assertIn(result2, result)
self.library1.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 mopidy.models import Album, Artist, Track
from mopidy.models import Album, Artist, SearchResult, Track
from tests.frontends.mpd import protocol
@ -13,9 +13,8 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase):
self.assertInResponse('OK')
def test_findadd(self):
self.backend.library.dummy_find_exact_result = [
Track(uri='dummy:a', name='A'),
]
self.backend.library.dummy_find_exact_result = SearchResult(
tracks=[Track(uri='dummy:a', name='A')])
self.assertEqual(self.core.tracklist.length.get(), 0)
self.sendRequest('findadd "title" "A"')
@ -25,9 +24,8 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase):
self.assertInResponse('OK')
def test_searchadd(self):
self.backend.library.dummy_search_result = [
Track(uri='dummy:a', name='A'),
]
self.backend.library.dummy_search_result = SearchResult(
tracks=[Track(uri='dummy:a', name='A')])
self.assertEqual(self.core.tracklist.length.get(), 0)
self.sendRequest('searchadd "title" "a"')
@ -43,9 +41,8 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase):
Track(uri='dummy:y', name='y'),
])
self.core.playlists.save(playlist)
self.backend.library.dummy_search_result = [
Track(uri='dummy:a', name='A'),
]
self.backend.library.dummy_search_result = SearchResult(
tracks=[Track(uri='dummy:a', name='A')])
playlists = self.core.playlists.filter(name='my favs').get()
self.assertEqual(len(playlists), 1)
self.assertEqual(len(playlists[0].tracks), 2)
@ -61,9 +58,8 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase):
self.assertInResponse('OK')
def test_searchaddpl_creates_missing_playlist(self):
self.backend.library.dummy_search_result = [
Track(uri='dummy:a', name='A'),
]
self.backend.library.dummy_search_result = SearchResult(
tracks=[Track(uri='dummy:a', name='A')])
self.assertEqual(
len(self.core.playlists.filter(name='my favs').get()), 0)
@ -119,6 +115,66 @@ class MusicDatabaseHandlerTest(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):
self.sendRequest('find "album" "what"')
self.assertInResponse('OK')
@ -185,6 +241,17 @@ 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.sendRequest('list "artist" "artist" "foo"')
self.assertInResponse('Artist: A Artist')
self.assertInResponse('OK')
def test_list_foo_returns_ack(self):
self.sendRequest('list "foo"')
self.assertEqualResponse('ACK [2@0] {list} incorrect arguments')
@ -242,8 +309,8 @@ class MusicDatabaseListTest(protocol.BaseTestCase):
self.assertInResponse('OK')
def test_list_artist_should_not_return_artists_without_names(self):
self.backend.library.dummy_find_exact_result = [
Track(artists=[Artist(name='')])]
self.backend.library.dummy_find_exact_result = SearchResult(
tracks=[Track(artists=[Artist(name='')])])
self.sendRequest('list "artist"')
self.assertNotInResponse('Artist: ')
@ -301,8 +368,8 @@ class MusicDatabaseListTest(protocol.BaseTestCase):
self.assertInResponse('OK')
def test_list_album_should_not_return_albums_without_names(self):
self.backend.library.dummy_find_exact_result = [
Track(album=Album(name=''))]
self.backend.library.dummy_find_exact_result = SearchResult(
tracks=[Track(album=Album(name=''))])
self.sendRequest('list "album"')
self.assertNotInResponse('Album: ')
@ -356,7 +423,8 @@ class MusicDatabaseListTest(protocol.BaseTestCase):
self.assertInResponse('OK')
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.assertNotInResponse('Date: ')
@ -412,6 +480,23 @@ class MusicDatabaseListTest(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):
self.sendRequest('search "album" "analbum"')
self.assertInResponse('OK')

View File

@ -4,7 +4,7 @@ import datetime
import json
from mopidy.models import (
Artist, Album, TlTrack, Track, Playlist,
Artist, Album, TlTrack, Track, Playlist, SearchResult,
ModelJSONEncoder, model_json_decoder)
from tests import unittest
@ -862,10 +862,56 @@ class PlaylistTest(unittest.TestCase):
def test_ne(self):
playlist1 = Playlist(
uri='uri1', name='name2', tracks=[Track(uri='uri1')],
uri='uri1', name='name1', tracks=[Track(uri='uri1')],
last_modified=1)
playlist2 = Playlist(
uri='uri2', name='name2', tracks=[Track(uri='uri2')],
last_modified=2)
self.assertNotEqual(playlist1, 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)

View File

@ -87,6 +87,14 @@ class ValidateSettingsTest(unittest.TestCase):
self.assertEqual(
result['BACKENDS'], 'Must contain at least one value.')
def test_noniterable_multivalue_setting_returns_error(self):
result = setting_utils.validate_settings(
self.defaults, {'FRONTENDS': ('this is not a tuple')})
self.assertEqual(
result['FRONTENDS'],
'Must be a tuple. '
"Remember the comma after single values: (u'value',)")
class SettingsProxyTest(unittest.TestCase):
def setUp(self):

View File

@ -32,5 +32,6 @@ class VersionTest(unittest.TestCase):
self.assertLess(SV('0.7.3'), SV('0.8.0'))
self.assertLess(SV('0.8.0'), SV('0.8.1'))
self.assertLess(SV('0.8.1'), SV('0.9.0'))
self.assertLess(SV('0.9.0'), SV(__version__))
self.assertLess(SV(__version__), SV('0.10.1'))
self.assertLess(SV('0.9.0'), SV('0.10.0'))
self.assertLess(SV('0.10.0'), SV(__version__))
self.assertLess(SV(__version__), SV('0.11.1'))