From 524f22eff44638d00c2e8930b3bd2eeb79a15ee0 Mon Sep 17 00:00:00 2001 From: Wouter van Wijk Date: Wed, 19 Dec 2012 12:48:33 +0100 Subject: [PATCH 1/9] Added lookup for artists, albums --- mopidy/backends/spotify/library.py | 86 ++++++++++++++++++++++++++++-- 1 file changed, 81 insertions(+), 5 deletions(-) diff --git a/mopidy/backends/spotify/library.py b/mopidy/backends/spotify/library.py index df04058b..1179341f 100644 --- a/mopidy/backends/spotify/library.py +++ b/mopidy/backends/spotify/library.py @@ -2,11 +2,16 @@ from __future__ import unicode_literals import logging import Queue +import time + +TIME_OUT = 10 from spotify import Link, SpotifyError from mopidy.backends import base from mopidy.models import Track +from mopidy.backends.base import BaseLibraryProvider +from mopidy.models import Playlist from . import translator @@ -56,11 +61,82 @@ class SpotifyLibraryProvider(base.BaseLibraryProvider): return self.search(**query) def lookup(self, uri): - try: - return [SpotifyTrack(uri)] - except SpotifyError as e: - logger.debug('Failed to lookup "%s": %s', uri, e) - return [] + link = Link.from_string(uri) + #uri is an album + if link.type() == Link.LINK_ALBUM: + try: + spotify_album = Link.from_string(uri).as_album() + # TODO Block until metadata_updated callback is called. Before that + # the track will be unloaded, unless it's already in the stored + # playlists. + browser = self.backend.spotify.session.browse_album(spotify_album) + + #wait 5 seconds + start = time.time() + while not browser.is_loaded(): + time.sleep(0.1) + if time.time() > (start + TIME_OUT): + break + album = translator.to_mopidy_album(spotify_album) + + #for track in browser: + # track = translator.to_mopidy_track(track) + + #from translator + tracks=[translator.to_mopidy_track(t) for t in browser + if str(Link.from_track(t, 0))] + + playlist = Playlist(tracks=tracks, uri=uri, name=album.name) + return playlist + + except SpotifyError as e: + logger.debug(u'Failed to lookup album "%s": %s', uri, e) + return None + + #uri is an album + if link.type() == Link.LINK_ARTIST: + try: + spotify_artist = Link.from_string(uri).as_artist() + # TODO Block until metadata_updated callback is called. Before that + # the track will be unloaded, unless it's already in the stored + # playlists. + browser = self.backend.spotify.session.browse_artist(spotify_artist) + #wait 5 seconds + start = time.time() + while not browser.is_loaded(): + time.sleep(0.1) + if time.time() > (start + TIME_OUT): + break + artist = translator.to_mopidy_artist(spotify_artist) + + #for track in browser: + # track = translator.to_mopidy_track(track) + + #from translator + tracks=[translator.to_mopidy_track(t) for t in browser + if str(Link.from_track(t, 0))] + + playlist = Playlist(tracks=tracks, uri=uri, name=artist.name) + return playlist + + except SpotifyError as e: + logger.debug(u'Failed to lookup album "%s": %s', uri, e) + return None + + #uri is a playlist of another user + # if l.type() == Link.LINK_PLAYLIST: + # if l.type() == Link.LINK_USER: + + #uri is a track + try: + spotify_track = Link.from_string(uri).as_track() + # TODO Block until metadata_updated callback is called. Before that + # the track will be unloaded, unless it's already in the stored + # playlists. + return translator.to_mopidy_track(spotify_track) + except SpotifyError as e: + logger.debug(u'Failed to lookup track "%s": %s', uri, e) + return None def refresh(self, uri=None): pass # TODO From d5c401bd07bbd349c528d171d976ecf3fb0fddd9 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 19 Dec 2012 20:29:45 +0100 Subject: [PATCH 2/9] spotify: Fix flake8 warnings in lookup method --- mopidy/backends/spotify/library.py | 59 ++++++++++++++++-------------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/mopidy/backends/spotify/library.py b/mopidy/backends/spotify/library.py index 1179341f..884e9ac6 100644 --- a/mopidy/backends/spotify/library.py +++ b/mopidy/backends/spotify/library.py @@ -10,7 +10,6 @@ from spotify import Link, SpotifyError from mopidy.backends import base from mopidy.models import Track -from mopidy.backends.base import BaseLibraryProvider from mopidy.models import Playlist from . import translator @@ -66,10 +65,11 @@ class SpotifyLibraryProvider(base.BaseLibraryProvider): if link.type() == Link.LINK_ALBUM: try: spotify_album = Link.from_string(uri).as_album() - # TODO Block until metadata_updated callback is called. Before that - # the track will be unloaded, unless it's already in the stored - # playlists. - browser = self.backend.spotify.session.browse_album(spotify_album) + # TODO Block until metadata_updated callback is called. + # Before that the track will be unloaded, unless it's + # already in the stored playlists. + browser = self.backend.spotify.session.browse_album( + spotify_album) #wait 5 seconds start = time.time() @@ -81,26 +81,29 @@ class SpotifyLibraryProvider(base.BaseLibraryProvider): #for track in browser: # track = translator.to_mopidy_track(track) - + #from translator - tracks=[translator.to_mopidy_track(t) for t in browser - if str(Link.from_track(t, 0))] - - playlist = Playlist(tracks=tracks, uri=uri, name=album.name) + tracks = [ + translator.to_mopidy_track(t) + for t in browser if str(Link.from_track(t, 0))] + + playlist = Playlist( + tracks=tracks, uri=uri, name=album.name) return playlist - + except SpotifyError as e: logger.debug(u'Failed to lookup album "%s": %s', uri, e) return None - + #uri is an album if link.type() == Link.LINK_ARTIST: try: spotify_artist = Link.from_string(uri).as_artist() - # TODO Block until metadata_updated callback is called. Before that - # the track will be unloaded, unless it's already in the stored - # playlists. - browser = self.backend.spotify.session.browse_artist(spotify_artist) + # TODO Block until metadata_updated callback is called. + # Before that the track will be unloaded, unless it's + # already in the stored playlists. + browser = self.backend.spotify.session.browse_artist( + spotify_artist) #wait 5 seconds start = time.time() while not browser.is_loaded(): @@ -111,28 +114,30 @@ class SpotifyLibraryProvider(base.BaseLibraryProvider): #for track in browser: # track = translator.to_mopidy_track(track) - + #from translator - tracks=[translator.to_mopidy_track(t) for t in browser - if str(Link.from_track(t, 0))] - - playlist = Playlist(tracks=tracks, uri=uri, name=artist.name) + tracks = [ + translator.to_mopidy_track(t) + for t in browser if str(Link.from_track(t, 0))] + + playlist = Playlist( + tracks=tracks, uri=uri, name=artist.name) return playlist - + except SpotifyError as e: logger.debug(u'Failed to lookup album "%s": %s', uri, e) return None - + #uri is a playlist of another user # if l.type() == Link.LINK_PLAYLIST: # if l.type() == Link.LINK_USER: - + #uri is a track try: spotify_track = Link.from_string(uri).as_track() - # TODO Block until metadata_updated callback is called. Before that - # the track will be unloaded, unless it's already in the stored - # playlists. + # TODO Block until metadata_updated callback is called. Before + # that the track will be unloaded, unless it's already in the + # stored playlists. return translator.to_mopidy_track(spotify_track) except SpotifyError as e: logger.debug(u'Failed to lookup track "%s": %s', uri, e) From 699588b52530601b4d51ca5f35e558cea74c790a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 19 Dec 2012 21:54:09 +0100 Subject: [PATCH 3/9] spotify: Refactor lookup code, add playlist support --- mopidy/backends/spotify/library.py | 126 +++++++++++------------------ 1 file changed, 46 insertions(+), 80 deletions(-) diff --git a/mopidy/backends/spotify/library.py b/mopidy/backends/spotify/library.py index 884e9ac6..25c58a17 100644 --- a/mopidy/backends/spotify/library.py +++ b/mopidy/backends/spotify/library.py @@ -4,13 +4,10 @@ import logging import Queue import time -TIME_OUT = 10 - from spotify import Link, SpotifyError from mopidy.backends import base from mopidy.models import Track -from mopidy.models import Playlist from . import translator @@ -19,9 +16,14 @@ logger = logging.getLogger('mopidy.backends.spotify') class SpotifyTrack(Track): """Proxy object for unloaded Spotify tracks.""" - def __init__(self, uri): + def __init__(self, uri=None, track=None): super(SpotifyTrack, self).__init__() - self._spotify_track = Link.from_string(uri).as_track() + if uri: + self._spotify_track = Link.from_string(uri).as_track() + elif track: + self._spotify_track = track + else: + raise AttributeError('uri or track must be provided') self._unloaded_track = Track(uri=uri, name='[loading...]') self._track = None @@ -60,88 +62,52 @@ class SpotifyLibraryProvider(base.BaseLibraryProvider): return self.search(**query) def lookup(self, uri): + try: link = Link.from_string(uri) - #uri is an album + if link.type() == Link.LINK_TRACK: + return self._lookup_track(uri) if link.type() == Link.LINK_ALBUM: - try: - spotify_album = Link.from_string(uri).as_album() - # TODO Block until metadata_updated callback is called. - # Before that the track will be unloaded, unless it's - # already in the stored playlists. - browser = self.backend.spotify.session.browse_album( - spotify_album) + return self._lookup_album(uri) + elif link.type() == Link.LINK_ARTIST: + return self._lookup_artist(uri) + elif link.type() == Link.LINK_PLAYLIST: + return self._lookup_playlist(uri) + else: + return [] + except SpotifyError as error: + logger.debug(u'Failed to lookup "%s": %s', uri, error) + return [] - #wait 5 seconds - start = time.time() - while not browser.is_loaded(): - time.sleep(0.1) - if time.time() > (start + TIME_OUT): - break - album = translator.to_mopidy_album(spotify_album) + def _lookup_track(self, uri): + return [SpotifyTrack(uri)] - #for track in browser: - # track = translator.to_mopidy_track(track) + def _lookup_album(self, uri): + 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] - #from translator - tracks = [ - translator.to_mopidy_track(t) - for t in browser if str(Link.from_track(t, 0))] + 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] - playlist = Playlist( - tracks=tracks, uri=uri, name=album.name) - return playlist + 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] - except SpotifyError as e: - logger.debug(u'Failed to lookup album "%s": %s', uri, e) - return None - - #uri is an album - if link.type() == Link.LINK_ARTIST: - try: - spotify_artist = Link.from_string(uri).as_artist() - # TODO Block until metadata_updated callback is called. - # Before that the track will be unloaded, unless it's - # already in the stored playlists. - browser = self.backend.spotify.session.browse_artist( - spotify_artist) - #wait 5 seconds - start = time.time() - while not browser.is_loaded(): - time.sleep(0.1) - if time.time() > (start + TIME_OUT): - break - artist = translator.to_mopidy_artist(spotify_artist) - - #for track in browser: - # track = translator.to_mopidy_track(track) - - #from translator - tracks = [ - translator.to_mopidy_track(t) - for t in browser if str(Link.from_track(t, 0))] - - playlist = Playlist( - tracks=tracks, uri=uri, name=artist.name) - return playlist - - except SpotifyError as e: - logger.debug(u'Failed to lookup album "%s": %s', uri, e) - return None - - #uri is a playlist of another user - # if l.type() == Link.LINK_PLAYLIST: - # if l.type() == Link.LINK_USER: - - #uri is a track - try: - spotify_track = Link.from_string(uri).as_track() - # TODO Block until metadata_updated callback is called. Before - # that the track will be unloaded, unless it's already in the - # stored playlists. - return translator.to_mopidy_track(spotify_track) - except SpotifyError as e: - logger.debug(u'Failed to lookup track "%s": %s', uri, e) - return None + def _wait_for_object_to_load(self, spotify_obj, timeout=10): + # XXX Sleeping to wait for the Spotify object to load is an ugly hack, + # but it works. We should look into other solutions for this. + start = time.time() + while not spotify_obj.is_loaded(): + time.sleep(0.1) + if time.time() > (start + timeout): + logger.debug( + 'Timeout: Spotify object did not load in %ds', timeout) + return def refresh(self, uri=None): pass # TODO From e39d15399b989bd843a173c449a5825474ac3c1d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 19 Dec 2012 21:58:26 +0100 Subject: [PATCH 4/9] Update author list --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 91a9f6cf..d536c059 100644 --- a/AUTHORS +++ b/AUTHORS @@ -12,3 +12,4 @@ - Christian Johansen - Matt Bray - Trygve Aaberge +- Wouter van Wijk From e63e6f7bbb2adf1e89961731de417cae62a802f6 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 19 Dec 2012 21:58:36 +0100 Subject: [PATCH 5/9] docs: Update changelog --- docs/changes.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index 8d614f1d..97199291 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -8,6 +8,19 @@ This change log is used to track all major changes to Mopidy. v0.11.0 (in development) ======================== +**Spotify backend** + +- Add support for looking up albums, artists, and playlists by URI in addition + to tracks. (Fixes: :issue:`67`) + + As an example of how this can be used, you can try the the following MPD + commands which now all adds one or more tracks to your tracklist:: + + add "spotify:track:1mwt9hzaH7idmC5UCoOUkz" + add "spotify:album:3gpHG5MGwnipnap32lFYvI" + add "spotify:artist:5TgQ66WuWkoQ2xYxaSTnVP" + add "spotify:user:p3.no:playlist:0XX6tamRiqEgh3t6FPFEkw" + **MPD frontend** - Add support for the ``findadd`` command. From 4d67dd1353dc56b4dcfae4ba22c2e9440d5c7236 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 19 Dec 2012 23:37:43 +0100 Subject: [PATCH 6/9] spotify: Use SPOTIFY_TIMEOUT when waiting for objects to load --- mopidy/backends/spotify/library.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mopidy/backends/spotify/library.py b/mopidy/backends/spotify/library.py index d171ecae..dec13ced 100644 --- a/mopidy/backends/spotify/library.py +++ b/mopidy/backends/spotify/library.py @@ -99,7 +99,8 @@ class SpotifyLibraryProvider(base.BaseLibraryProvider): self._wait_for_object_to_load(playlist) return [SpotifyTrack(track=t) for t in playlist] - def _wait_for_object_to_load(self, spotify_obj, timeout=10): + def _wait_for_object_to_load( + self, spotify_obj, timeout=settings.SPOTIFY_TIMEOUT): # XXX Sleeping to wait for the Spotify object to load is an ugly hack, # but it works. We should look into other solutions for this. start = time.time() From 42faec8a3c8c2faaecdf90fddbf8bf79fcc8f357 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 20 Dec 2012 18:59:01 +0100 Subject: [PATCH 7/9] spotify: SpotifyTrack fails when both uri and track is provided --- mopidy/backends/spotify/library.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mopidy/backends/spotify/library.py b/mopidy/backends/spotify/library.py index dec13ced..28e9c61f 100644 --- a/mopidy/backends/spotify/library.py +++ b/mopidy/backends/spotify/library.py @@ -19,12 +19,12 @@ class SpotifyTrack(Track): """Proxy object for unloaded Spotify tracks.""" def __init__(self, uri=None, track=None): super(SpotifyTrack, self).__init__() - if uri: + if (uri and track) or (not uri and not track): + raise AttributeError('uri or track must be provided') + elif uri: self._spotify_track = Link.from_string(uri).as_track() elif track: self._spotify_track = track - else: - raise AttributeError('uri or track must be provided') self._unloaded_track = Track(uri=uri, name='[loading...]') self._track = None From e118c73aa36506e1f66eda027aedfdda98758821 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 20 Dec 2012 19:01:04 +0100 Subject: [PATCH 8/9] spotify: Refactor loading timeout logic --- mopidy/backends/spotify/library.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/backends/spotify/library.py b/mopidy/backends/spotify/library.py index 28e9c61f..bde1e3fb 100644 --- a/mopidy/backends/spotify/library.py +++ b/mopidy/backends/spotify/library.py @@ -103,10 +103,10 @@ class SpotifyLibraryProvider(base.BaseLibraryProvider): self, spotify_obj, timeout=settings.SPOTIFY_TIMEOUT): # XXX Sleeping to wait for the Spotify object to load is an ugly hack, # but it works. We should look into other solutions for this. - start = time.time() + wait_until = time.time() + timeout while not spotify_obj.is_loaded(): time.sleep(0.1) - if time.time() > (start + timeout): + if time.time() > wait_until: logger.debug( 'Timeout: Spotify object did not load in %ds', timeout) return From a3ab9567331f1072a79e3d1869a38e338ff8d2f9 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 20 Dec 2012 19:08:01 +0100 Subject: [PATCH 9/9] spotify: Block track lookups until we get data This makes track lookup behave consistently with lookup of artists, albums and playlists. I consider this "safe", since track lookup is only used for lookup of single tracks by URI. If you're e.g. loading a playlist full of unloaded tracks, you should still use SpotifyTrack to avoid blocking on track loading. --- mopidy/backends/spotify/library.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mopidy/backends/spotify/library.py b/mopidy/backends/spotify/library.py index bde1e3fb..af25fab2 100644 --- a/mopidy/backends/spotify/library.py +++ b/mopidy/backends/spotify/library.py @@ -80,7 +80,9 @@ class SpotifyLibraryProvider(base.BaseLibraryProvider): return [] def _lookup_track(self, uri): - return [SpotifyTrack(uri)] + track = Link.from_string(uri).as_track() + self._wait_for_object_to_load(track) + return [SpotifyTrack(track=track)] def _lookup_album(self, uri): album = Link.from_string(uri).as_album()