Merge branch 'develop' into feature/mpd-album-artist-search-results

Conflicts:
	docs/changes.rst
This commit is contained in:
Stein Magnus Jodal 2012-12-23 18:49:51 +01:00
commit a1cfc74d29
10 changed files with 76 additions and 21 deletions

View File

@ -8,6 +8,15 @@ This change log is used to track all major changes to Mopidy.
v0.11.0 (in development)
========================
**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
@ -58,6 +67,9 @@ v0.11.0 (in development)
- Add support for search by date.
- 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

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

@ -20,4 +20,4 @@ class LocalBackend(pykka.ThreadingActor, base.Backend):
self.playback = base.BasePlaybackProvider(audio=audio, backend=self)
self.playlists = LocalPlaylistsProvider(backend=self)
self.uri_schemes = ['file', 'local']
self.uri_schemes = ['file']

View File

@ -70,7 +70,7 @@ class LocalLibraryProvider(base.BaseLibraryProvider):
result_tracks = filter(any_filter, result_tracks)
else:
raise LookupError('Invalid lookup field: %s' % field)
return SearchResult(uri='local:search', tracks=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 SearchResult(uri='local:search', tracks=result_tracks)
return SearchResult(uri='file:search', tracks=result_tracks)
def _validate_query(self, query):
for (_, values) in query.iteritems():

View File

@ -83,7 +83,10 @@ class SpotifyLibraryProvider(base.BaseLibraryProvider):
def _lookup_track(self, uri):
track = Link.from_string(uri).as_track()
self._wait_for_object_to_load(track)
return [SpotifyTrack(track=track)]
if track.is_loaded():
return [SpotifyTrack(track=track)]
else:
return [SpotifyTrack(uri=uri)]
def _lookup_album(self, uri):
album = Link.from_string(uri).as_album()
@ -121,12 +124,13 @@ class SpotifyLibraryProvider(base.BaseLibraryProvider):
if not query:
return self._get_all_tracks()
if 'uri' in query.keys():
uris = query.get('uri', [])
if uris:
tracks = []
for uri in query['uri']:
for uri in uris:
tracks += self.lookup(uri)
if len(query['uri']) == 1:
uri = query['uri']
if len(uris) == 1:
uri = uris[0]
else:
uri = 'spotify:search'
return SearchResult(uri=uri, tracks=tracks)
@ -170,7 +174,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

@ -329,6 +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:
playpos(context, songpos)
context.core.playback.seek(int(seconds) * 1000).get()
@ -343,6 +344,7 @@ 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:
playid(context, tlid)

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

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

View File

@ -371,45 +371,58 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase):
self.sendRequest('previous')
self.assertInResponse('OK')
def test_seek(self):
self.core.tracklist.add([Track(uri='dummy:a', length=40000)])
def test_seek_in_current_track(self):
seek_track = Track(uri='dummy:a', length=40000)
self.core.tracklist.add([seek_track])
self.core.playback.play()
self.sendRequest('seek "0"')
self.sendRequest('seek "0" "30"')
self.assertEqual(self.core.playback.current_track.get(), seek_track)
self.assertGreaterEqual(self.core.playback.time_position, 30000)
self.assertInResponse('OK')
def test_seek_with_songpos(self):
def test_seek_in_another_track(self):
seek_track = Track(uri='dummy:b', length=40000)
self.core.tracklist.add(
[Track(uri='dummy:a', length=40000), seek_track])
self.core.playback.play()
self.assertNotEqual(self.core.playback.current_track.get(), seek_track)
self.sendRequest('seek "1" "30"')
self.assertEqual(self.core.playback.current_track.get(), seek_track)
self.assertInResponse('OK')
def test_seek_without_quotes(self):
self.core.tracklist.add([Track(uri='dummy:a', length=40000)])
self.core.playback.play()
self.sendRequest('seek 0')
self.sendRequest('seek 0 30')
self.assertGreaterEqual(
self.core.playback.time_position.get(), 30000)
self.assertInResponse('OK')
def test_seekid(self):
self.core.tracklist.add([Track(uri='dummy:a', length=40000)])
def test_seekid_in_current_track(self):
seek_track = Track(uri='dummy:a', length=40000)
self.core.tracklist.add([seek_track])
self.core.playback.play()
self.sendRequest('seekid "0" "30"')
self.assertEqual(self.core.playback.current_track.get(), seek_track)
self.assertGreaterEqual(
self.core.playback.time_position.get(), 30000)
self.assertInResponse('OK')
def test_seekid_with_tlid(self):
def test_seekid_in_another_track(self):
seek_track = Track(uri='dummy:b', length=40000)
self.core.tracklist.add(
[Track(uri='dummy:a', length=40000), seek_track])
self.core.playback.play()
self.sendRequest('seekid "1" "30"')
self.assertEqual(1, self.core.playback.current_tl_track.get().tlid)
self.assertEqual(seek_track, self.core.playback.current_track.get())
self.assertInResponse('OK')

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