Merge branch 'develop' into feature/mpd-album-artist-search-results
Conflicts: docs/changes.rst
This commit is contained in:
commit
a1cfc74d29
@ -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
9
fabfile.py
vendored
@ -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/')
|
||||
|
||||
@ -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']
|
||||
|
||||
@ -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():
|
||||
|
||||
@ -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 = []
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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.'
|
||||
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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):
|
||||
|
||||
Loading…
Reference in New Issue
Block a user