Skip to content

Commit

Permalink
playlists: Use the Web API (Fixes mopidy#122, mopidy#182).
Browse files Browse the repository at this point in the history
Cache playlist web API responses in a simple dict.

playlists: Support Spotify's new playlist URI scheme (Fixes mopidy#215).

search: uses 'from_token' market.
  • Loading branch information
kingosticks committed Sep 12, 2019
1 parent 136f5cb commit 39ef8ab
Show file tree
Hide file tree
Showing 14 changed files with 781 additions and 464 deletions.
14 changes: 0 additions & 14 deletions mopidy_spotify/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ def on_start(self):
self._web_client = web.SpotifyOAuthClient(
self._config['spotify']['client_id'],
self._config['spotify']['client_secret'], self._config['proxy'])

self._web_client.login()

def on_stop(self):
Expand Down Expand Up @@ -122,19 +121,6 @@ def on_logged_in(self):
logger.info('Spotify private session activated')
self._session.social.private_session = True

self._session.playlist_container.on(
spotify.PlaylistContainerEvent.CONTAINER_LOADED,
playlists.on_container_loaded)
self._session.playlist_container.on(
spotify.PlaylistContainerEvent.PLAYLIST_ADDED,
playlists.on_playlist_added)
self._session.playlist_container.on(
spotify.PlaylistContainerEvent.PLAYLIST_REMOVED,
playlists.on_playlist_removed)
self._session.playlist_container.on(
spotify.PlaylistContainerEvent.PLAYLIST_MOVED,
playlists.on_playlist_moved)

def on_play_token_lost(self):
if self._session.player.state == spotify.PlayerState.PLAYING:
self.playback.pause()
Expand Down
4 changes: 3 additions & 1 deletion mopidy_spotify/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ def get_images(self, uris):
return images.get_images(self._backend._web_client, uris)

def lookup(self, uri):
return lookup.lookup(self._config, self._backend._session, uri)
return lookup.lookup(
self._config, self._backend._session, self._backend._web_client,
uri)

def search(self, query=None, uris=None, exact=False):
return search.search(
Expand Down
30 changes: 13 additions & 17 deletions mopidy_spotify/lookup.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import spotify

from mopidy_spotify import translator, utils
from mopidy_spotify import playlists, translator, utils, web


logger = logging.getLogger(__name__)
Expand All @@ -14,25 +14,25 @@
]


def lookup(config, session, uri):
def lookup(config, session, web_client, uri):
try:
sp_link = session.get_link(uri)
web_link = web.parse_uri(uri)
if web_link.type != 'playlist':
sp_link = session.get_link(uri)
except ValueError as exc:
logger.info('Failed to lookup "%s": %s', uri, exc)
return []

try:
if sp_link.type is spotify.LinkType.TRACK:
if web_link.type == 'playlist':
return _lookup_playlist(config, web_client, uri)
elif sp_link.type is spotify.LinkType.TRACK:
return list(_lookup_track(config, sp_link))
elif sp_link.type is spotify.LinkType.ALBUM:
return list(_lookup_album(config, sp_link))
elif sp_link.type is spotify.LinkType.ARTIST:
with utils.time_logger('Artist lookup'):
return list(_lookup_artist(config, sp_link))
elif sp_link.type is spotify.LinkType.PLAYLIST:
return list(_lookup_playlist(config, sp_link))
elif sp_link.type is spotify.LinkType.STARRED:
return list(reversed(list(_lookup_playlist(config, sp_link))))
else:
logger.info(
'Failed to lookup "%s": Cannot handle %r',
Expand Down Expand Up @@ -90,12 +90,8 @@ def _lookup_artist(config, sp_link):
yield track


def _lookup_playlist(config, sp_link):
sp_playlist = sp_link.as_playlist()
sp_playlist.load(config['timeout'])
for sp_track in sp_playlist.tracks:
sp_track.load(config['timeout'])
track = translator.to_track(
sp_track, bitrate=config['bitrate'])
if track is not None:
yield track
def _lookup_playlist(config, web_client, uri):
playlist = playlists.playlist_lookup(web_client, uri, config['bitrate'])
if playlist is None:
raise spotify.Error('Playlist Web API lookup failed')
return playlist.tracks
103 changes: 29 additions & 74 deletions mopidy_spotify/playlists.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@

from mopidy import backend

import spotify

from mopidy_spotify import translator, utils


_cache = {}

logger = logging.getLogger(__name__)


Expand All @@ -23,25 +23,16 @@ def as_list(self):
return list(self._get_flattened_playlist_refs())

def _get_flattened_playlist_refs(self):
if self._backend._session is None:
if self._backend._web_client is None:
return

if self._backend._session.playlist_container is None:
if self._backend._web_client.user_id is None:
return

username = self._backend._session.user_name
folders = []

for sp_playlist in self._backend._session.playlist_container:
if isinstance(sp_playlist, spotify.PlaylistFolder):
if sp_playlist.type is spotify.PlaylistType.START_FOLDER:
folders.append(sp_playlist.name)
elif sp_playlist.type is spotify.PlaylistType.END_FOLDER:
folders.pop()
continue

web_client = self._backend._web_client
for web_playlist in web_client.get_user_playlists(_cache):
playlist_ref = translator.to_playlist_ref(
sp_playlist, folders=folders, username=username)
web_playlist, web_client.user_id)
if playlist_ref is not None:
yield playlist_ref

Expand All @@ -54,38 +45,15 @@ def lookup(self, uri):
return self._get_playlist(uri)

def _get_playlist(self, uri, as_items=False):
try:
sp_playlist = self._backend._session.get_playlist(uri)
except spotify.Error as exc:
logger.debug('Failed to lookup Spotify URI %s: %s', uri, exc)
return

if not sp_playlist.is_loaded:
logger.debug(
'Waiting for Spotify playlist to load: %s', sp_playlist)
sp_playlist.load(self._timeout)

username = self._backend._session.user_name
return translator.to_playlist(
sp_playlist, username=username, bitrate=self._backend._bitrate,
as_items=as_items)
return playlist_lookup(
self._backend._web_client, uri,
self._backend._bitrate, as_items)

def refresh(self):
pass # Not needed as long as we don't cache anything.
pass # TODO: Clear/invalidate all caches on refresh.

def create(self, name):
try:
sp_playlist = (
self._backend._session.playlist_container
.add_new_playlist(name))
except ValueError as exc:
logger.warning(
'Failed creating new Spotify playlist "%s": %s', name, exc)
except spotify.Error:
logger.warning('Failed creating new Spotify playlist "%s"', name)
else:
username = self._backend._session.user_name
return translator.to_playlist(sp_playlist, username=username)
pass # TODO

def delete(self, uri):
pass # TODO
Expand All @@ -94,40 +62,27 @@ def save(self, playlist):
pass # TODO


def on_container_loaded(sp_playlist_container):
# Called from the pyspotify event loop, and not in an actor context.
logger.debug('Spotify playlist container loaded')

# This event listener is also called after playlists are added, removed and
# moved, so since Mopidy currently only supports the "playlists_loaded"
# event this is the only place we need to trigger a Mopidy backend event.
backend.BackendListener.send('playlists_loaded')


def on_playlist_added(sp_playlist_container, sp_playlist, index):
# Called from the pyspotify event loop, and not in an actor context.
logger.debug(
'Spotify playlist "%s" added to index %d', sp_playlist.name, index)

# XXX Should Mopidy support more fine grained playlist events which this
# event can trigger?
def playlist_lookup(web_client, uri, bitrate, as_items=False):
if web_client is None:
return

logger.info('Fetching Spotify playlist "%s"', uri)
web_playlist = web_client.get_playlist(uri, _cache)

def on_playlist_removed(sp_playlist_container, sp_playlist, index):
# Called from the pyspotify event loop, and not in an actor context.
logger.debug(
'Spotify playlist "%s" removed from index %d', sp_playlist.name, index)
if web_playlist == {}:
logger.error('Failed to lookup Spotify playlist URI %s', uri)
return

# XXX Should Mopidy support more fine grained playlist events which this
# event can trigger?
return translator.to_playlist(
web_playlist, username=web_client.user_id, bitrate=bitrate,
as_items=as_items)


def on_playlist_moved(
sp_playlist_container, sp_playlist, old_index, new_index):
def on_playlists_loaded():
# Called from the pyspotify event loop, and not in an actor context.
logger.debug(
'Spotify playlist "%s" moved from index %d to %d',
sp_playlist.name, old_index, new_index)
logger.debug('Spotify playlists loaded')

# XXX Should Mopidy support more fine grained playlist events which this
# event can trigger?
# This event listener is also called after playlists are added, removed and
# moved, so since Mopidy currently only supports the "playlists_loaded"
# event this is the only place we need to trigger a Mopidy backend event.
backend.BackendListener.send('playlists_loaded')
8 changes: 4 additions & 4 deletions mopidy_spotify/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def search(config, session, web_client,
return models.SearchResult(uri='spotify:search')

if 'uri' in query:
return _search_by_uri(config, session, query)
return _search_by_uri(config, session, web_client, query)

sp_query = translator.sp_search_query(query)
if not sp_query:
Expand Down Expand Up @@ -55,7 +55,7 @@ def search(config, session, web_client,
result = web_client.get('search', params={
'q': sp_query,
'limit': search_count,
'market': web_client.user_country,
'market': 'from_token',
'type': ','.join(types)})

albums = [
Expand All @@ -77,10 +77,10 @@ def search(config, session, web_client,
uri=uri, albums=albums, artists=artists, tracks=tracks)


def _search_by_uri(config, session, query):
def _search_by_uri(config, session, web_client, query):
tracks = []
for uri in query['uri']:
tracks += lookup.lookup(config, session, uri)
tracks += lookup.lookup(config, session, web_client, uri)

uri = 'spotify:search'
if len(query['uri']) == 1:
Expand Down
Loading

0 comments on commit 39ef8ab

Please sign in to comment.