Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use Web API for Playlists #188

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions mopidy_spotify/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ def __init__(self, config, audio):
self._session = None
self._event_loop = None
self._bitrate = None
self._web_client = None
self._web_session = None
self.playlists_refresher = None

self.library = library.SpotifyLibraryProvider(backend=self)
self.playback = playback.SpotifyPlaybackProvider(
Expand All @@ -60,15 +61,20 @@ def on_start(self):
self._config['spotify']['username'],
self._config['spotify']['password'])

self._web_client = web.OAuthClient(
base_url='https://api.spotify.com/v1',
refresh_url='https://auth.mopidy.com/spotify/token',
client_id=self._config['spotify']['client_id'],
client_secret=self._config['spotify']['client_secret'],
proxy_config=self._config['proxy'])
self._web_session = web.WebSession(
self._config['spotify']['client_id'],
self._config['spotify']['client_secret'],self._config['proxy'])

if self.playlists:
self.playlists_refresher = threading.Thread(
target=self.playlists.refresh).start()

logger.info('Started spotify backend')

def on_stop(self):
logger.debug('Logging out of Spotify')
if self.playlists_refresher is not None:
self.playlists_refresher.join()
self._session.logout()
self._logged_out.wait()
self._event_loop.stop()
Expand Down
1 change: 1 addition & 0 deletions mopidy_spotify/images.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
logger = logging.getLogger(__name__)


# TODO: Merge some/all of this into WebSession
def get_images(web_client, uris):
result = {}
uri_type_getter = operator.itemgetter('type')
Expand Down
14 changes: 8 additions & 6 deletions mopidy_spotify/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,18 @@ def browse(self, uri):

def get_distinct(self, field, query=None):
return distinct.get_distinct(
self._config, self._backend._session, self._backend._web_client,
field, query)
self._config, self._backend._session,
self._backend._web_session._client, field, query)

def get_images(self, uris):
return images.get_images(self._backend._web_client, uris)
return images.get_images(self._backend._web_session._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_session,
uri)

def search(self, query=None, uris=None, exact=False):
return search.search(
self._config, self._backend._session, self._backend._web_client,
query, uris, exact)
self._config, self._backend._session,
self._backend._web_session, query, uris, exact)
29 changes: 13 additions & 16 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,9 +14,13 @@
]


def lookup(config, session, uri):
def lookup(config, session, web_session, uri):
try:
sp_link = session.get_link(uri)
web_link = web.parse_uri(uri)
if web.link_is_playlist(web_link):
return lookup_playlist(session, web_session, config, uri)
else:
sp_link = session.get_link(uri)
except ValueError as exc:
logger.info('Failed to lookup "%s": %s', uri, exc)
return []
Expand All @@ -29,10 +33,6 @@ def lookup(config, session, uri):
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,9 @@ 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(session, web_session, config, uri):
playlist = playlists.playlist_lookup(
session, web_session, uri, config['bitrate'])
if playlist is None:
raise spotify.Error('Playlist Web API lookup failed')
return playlist.tracks
80 changes: 47 additions & 33 deletions mopidy_spotify/playlists.py
Original file line number Diff line number Diff line change
@@ -1,47 +1,36 @@
from __future__ import unicode_literals

import logging
import time

from mopidy import backend

import spotify

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


_sp_links = {}
logger = logging.getLogger(__name__)


class SpotifyPlaylistsProvider(backend.PlaylistsProvider):

def __init__(self, backend):
self._backend = backend
self._timeout = self._backend._config['spotify']['timeout']

def as_list(self):
with utils.time_logger('playlists.as_list()'):
return list(self._get_flattened_playlist_refs())

def _get_flattened_playlist_refs(self):
if self._backend._session is None:
if not self._backend._web_session.playlists_loaded:
return

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

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

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

playlist_ref = translator.to_playlist_ref(
sp_playlist, folders=folders, username=username)
for web_playlist in self._backend._web_session.get_user_playlists(include_tracks=False):
playlist_ref = translator.to_playlist_ref(web_playlist, username)
if playlist_ref is not None:
yield playlist_ref

Expand All @@ -54,24 +43,21 @@ 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
return playlist_lookup(
self._backend._session, self._backend._web_session, uri,
self._backend._bitrate, as_items)

if not sp_playlist.is_loaded:
logger.debug(
'Waiting for Spotify playlist to load: %s', sp_playlist)
sp_playlist.load(self._timeout)
def refresh(self):
# TODO: Clear/invalidate memoize caches on refresh?
_sp_links = {}
with utils.time_logger('Refresh Playlists', logging.INFO):
self._backend._web_session.load_playlists()

username = self._backend._session.user_name
return translator.to_playlist(
sp_playlist, username=username, bitrate=self._backend._bitrate,
as_items=as_items)
# Allow libspotify to get track links so they load in the background.
for playlist_ref in self.as_list():
self.get_items(playlist_ref.uri)

def refresh(self):
pass # Not needed as long as we don't cache anything.
on_container_loaded(None)

def create(self, name):
try:
Expand All @@ -94,6 +80,34 @@ def save(self, playlist):
pass # TODO


def playlist_lookup(session, web_session, uri, bitrate, as_items=False):
if not web_session.playlists_loaded:
return

web_playlist = web_session.get_playlist(uri)
playlist = translator.to_playlist(
web_playlist, username=web_session.user_name, bitrate=bitrate,
as_items=as_items)
if playlist is None:
return

if session.connection.state is spotify.ConnectionState.LOGGED_IN:
if as_items:
tracks = playlist
else:
tracks = playlist.tracks

for track in tracks:
if track.uri in _sp_links:
continue
try:
_sp_links[track.uri] = session.get_link(track.uri)
except ValueError as exc:
logger.info('Failed to get link "%s": %s', track.uri, exc)

return playlist


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')
Expand Down
13 changes: 7 additions & 6 deletions mopidy_spotify/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
logger = logging.getLogger(__name__)


def search(config, session, web_client,
def search(config, session, web_session,
query=None, uris=None, exact=False, types=_SEARCH_TYPES):
# TODO Respect `uris` argument
# TODO Support `exact` search
Expand All @@ -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_session, query)

sp_query = translator.sp_search_query(query)
if not sp_query:
Expand All @@ -35,7 +35,8 @@ def search(config, session, web_client,
uri = 'spotify:search:%s' % urllib.quote(sp_query.encode('utf-8'))
logger.info('Searching Spotify for: %s', sp_query)

if session.connection.state is not spotify.ConnectionState.LOGGED_IN:
if web_session is None:
# TODO: Verify auth status?
logger.info('Spotify search aborted: Spotify is offline')
return models.SearchResult(uri=uri)

Expand All @@ -52,7 +53,7 @@ def search(config, session, web_client,
'to at most 50.')
search_count = 50

result = web_client.get('search', params={
result = web_session._client.get('search', params={
'q': sp_query,
'limit': search_count,
'market': session.user_country,
Expand All @@ -77,10 +78,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_session, query):
tracks = []
for uri in query['uri']:
tracks += lookup.lookup(config, session, uri)
tracks += lookup.lookup(config, session, web_session, uri)

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