Skip to content

Commit

Permalink
PoC
Browse files Browse the repository at this point in the history
  • Loading branch information
kingosticks committed Jun 28, 2017
1 parent ecbe024 commit 9b892df
Show file tree
Hide file tree
Showing 4 changed files with 205 additions and 44 deletions.
34 changes: 20 additions & 14 deletions mopidy_spotify/lookup.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,28 @@

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

try:
if web_link.type == 'playlist':
return list(_lookup_playlist(web_client, config, web_link))
elif web_link.type == 'starred':
return list(reversed(_lookup_starred(web_client, config, web_link)))
if 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))))
#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 @@ -89,13 +94,14 @@ def _lookup_artist(config, sp_link):
if track is not None:
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
_API_BASE_URI = 'https://api.spotify.com/v1'

def _lookup_playlist(web_client, config, link):
uri = '%s/users/%s/playlists/%s' % (_API_BASE_URI, link.owner, link.id)
fields = ['name', 'owner', 'type', 'uri', 'tracks']
result = web_client.get(uri, params={'fields': ','.join(fields)})
if result:
for item in result.get('tracks', {}).get('items', []):
track = item.get('track', None)
if track:
yield translator.web_to_track(track, bitrate=config['bitrate'])
101 changes: 75 additions & 26 deletions mopidy_spotify/playlists.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,40 +9,61 @@
from mopidy_spotify import translator, utils


_API_BASE_URI = 'https://api.spotify.com/v1'

logger = logging.getLogger(__name__)


class SpotifyPlaylistsProvider(backend.PlaylistsProvider):

def __init__(self, backend):
self._backend = backend
self._timeout = self._backend._config['spotify']['timeout']
self._cache = None
self._cache2 = {}

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

def _get_all_items(self, first_result, params=None):
if params is None:
params = {}
items = first_result['items']
uri = first_result['next']
while uri is not None:
logger.error("DOING NEXT")
next_result = self._backend._web_client.get(uri, params=params)
#for item in next_result.get('items', []):
#yield item
items.extend(next_result['items'])
uri = next_result.get('next', None)
return items

def _get_flattened_playlist_refs(self):
logger.error("_get_flattened_playlist_refs")
if self._backend._session is None:
logger.info("NO session")
return

if self._backend._session.playlist_container is None:
username = self._backend._session.user_name

if self._cache is not None:
logger.info("USING CACHE")
result = self._cache
else:
result = self._backend._web_client.get('me/playlists', params={
'limit': 50})
self._cache = result

if result is None:
logger.error("No playlists found")
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

playlist_ref = translator.to_playlist_ref(
sp_playlist, folders=folders, username=username)
for web_playlist in self._get_all_items(result):
playlist_ref = translator.web_to_playlist_ref(
web_playlist, username=username)
if playlist_ref is not None:
logger.info("Got playlist %s %s" % (playlist_ref.name, playlist_ref.uri))
yield playlist_ref

def get_items(self, uri):
Expand All @@ -54,20 +75,48 @@ 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)
def gen_fields(name, fields=[]):
fields = ['uri', 'name'] + fields
return '%s(%s)' % (name, ','.join(fields))


fields = ['name', 'owner', 'type', 'uri']
if as_items:
fields.append('tracks')
#artists_fields = gen_fields('artists')
#album_fields = gen_fields('album', [artists_fields])
#track_fields = ['duration_ms', 'disc_number', 'track_number',
#album_fields, artists_fields]
#fields = 'items(%s)' % gen_fields('track', track_fields)
#items(track(uri,name,duration_ms,disc_number,track_number,album(uri,name,artists(uri,name)),artists(uri,name))

link = translator.parse_uri(uri)

web_playlist = self._cache2.get(uri, None)
if web_playlist is not None:
logger.info('found %s in cache', uri)
if as_items and 'tracks' not in web_playlist:
logger.info('cached copy without needed tracks so re-requesting')
web_playlist = None

if web_playlist is None:
params = {'fields': ','.join(fields), 'market': 'from_token'}
web_playlist = self._backend._web_client.get(
'users/%s/playlists/%s' % (link.owner, link.id),
params=params)
if web_playlist is not None:
if as_items and 'tracks' in web_playlist:
all_tracks = self._get_all_items(web_playlist['tracks'])
web_playlist['tracks'] = [t['track'] for t in all_tracks]
self._cache2[uri] = web_playlist

if web_playlist is None:
logger.debug('Failed to lookup Spotify URI %s', uri)
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,
return translator.web_to_playlist(
web_playlist, username=username, bitrate=self._backend._bitrate,
as_items=as_items)

def refresh(self):
Expand Down
92 changes: 89 additions & 3 deletions mopidy_spotify/translator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import collections
import logging
import urlparse

from mopidy import models

Expand Down Expand Up @@ -248,10 +249,11 @@ def web_to_album(web_album):
artists=artists)


def web_to_track(web_track):
def web_to_track(web_track, album=None, bitrate=None):
artists = [
web_to_artist(web_artist) for web_artist in web_track['artists']]
album = web_to_album(web_track['album'])
if not album:
album = web_to_album(web_track['album'])

return models.Track(
uri=web_track['uri'],
Expand All @@ -260,4 +262,88 @@ def web_to_track(web_track):
album=album,
length=web_track['duration_ms'],
disc_no=web_track['disc_number'],
track_no=web_track['track_number'])
track_no=web_track['track_number'],
bitrate=bitrate)


def web_to_track_ref(web_track):
return models.Ref.track(uri=web_track['uri'], name=web_track['name'])


def web_to_track_refs(web_tracks):
for web_track in web_tracks:
ref = web_to_track_ref(web_track)
if ref is not None:
yield ref


def web_to_playlist_ref(web_playlist, folders=None, username=None):
return web_to_playlist(
web_playlist, folders=folders, username=username, as_ref=True)


def web_to_playlist(web_playlist, folders=None, username=None, bitrate=None,
as_ref=False, as_items=False):
if web_playlist['type'] != 'playlist':
return

web_tracks = web_playlist.get('tracks', [])

This comment has been minimized.

Copy link
@kingosticks

kingosticks May 25, 2018

Author Owner

@blacklight It looks like it's the difference between simplified and full playlist objects. The former returned from https://api.spotify.com/v1/me/playlists and the latter from https://api.spotify.com/v1/users/{user_id}/playlists/{playlist_id}. The full objects are a list whereas the simplified ones are the hash, so I messed this up. Good spot.

This comment has been minimized.

Copy link
@blacklight

blacklight May 25, 2018

I've sent you a pull request with a possible fix: #1
The fix might be a bit over-zealous, as the new translator.web_to_playlist() method tries now to get the tracks in any possible way (is tracks a list? a dict with a href reference to /user/<user_id>/playlists/<playlist_id>/tracks? a dict with the exploded list of tracks under items?), but it makes playlists usable again in my case.

Please review and simplify the logic if appropriate, I'm not a big expert of the Spotify APIs.


if as_items:
return list(web_to_track_refs(web_tracks))

name = web_playlist['name']

if not as_ref:
tracks = [
web_to_track(web_track, bitrate=bitrate)
for web_track in web_tracks]
tracks = filter(None, tracks)
if name is None:
# Use same starred order as the Spotify client
tracks = list(reversed(tracks))

#if name is None:
#name = 'Starred' # Not supported by Web API
#if folders is not None: # Not supported by Web API
#name = '/'.join(folders + [name])
if username is not None and web_playlist['owner']['id'] != username:
name = '%s (by %s)' % (name, web_playlist['owner']['id'])

if as_ref:
return models.Ref.playlist(uri=web_playlist['uri'], name=name)
else:
return models.Playlist(
uri=web_playlist['uri'], name=name, tracks=tracks)


_result = collections.namedtuple('Link', ['uri', 'type', 'id', 'owner'])


def parse_uri(uri):
parsed_uri = urlparse.urlparse(uri)

schemes = ('http', 'https')
netlocs = ('open.spotify.com', 'play.spotify.com')

if parsed_uri.scheme == 'spotify':
parts = parsed_uri.path.split(':')
elif parsed_uri.scheme in schemes and parsed_uri.netloc in netlocs:
parts = parsed_uri.path[1:].split('/')
else:
parts = []

# Strip out empty parts to ensure we are strict about URI parsing.
parts = [p for p in parts if p.strip()]

if len(parts) == 2 and parts[0] in ('track', 'album', 'artist'):
return _result(uri, parts[0], parts[1], None)
elif len(parts) == 3 and parts[0] == 'user' and parts[2] == 'starred':
if parsed_uri.scheme == 'spotify':
return _result(uri, 'starred', None, parts[1])
elif len(parts) == 3 and parts[0] == 'playlist':
return _result(uri, 'playlist', parts[2], parts[1])
elif len(parts) == 4 and parts[0] == 'user' and parts[2] == 'playlist':
return _result(uri, 'playlist', parts[3], parts[1])

raise ValueError('Could not parse %r as a Spotify URI' % uri)
22 changes: 21 additions & 1 deletion mopidy_spotify/web.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,33 @@
import urllib
import urlparse

import collections
import requests

from mopidy_spotify import utils
from mopidy_spotify import utils, translator

logger = logging.getLogger(__name__)


class memoized(object):
def __init__(self, func):
self.func = func
self.cache = {}

def __call__(self, *args, **kwargs):
# NOTE Only args, not kwargs, are part of the memoization key.
if not isinstance(args, collections.Hashable):
return self.func(*args, **kwargs)
if args in self.cache:
logger.info("Cache hit for %S" % ','.join(args))
return self.cache[args]
else:
value = self.func(*args, **kwargs)
if value is not None:
self.cache[args] = value
return value


class OAuthTokenRefreshError(Exception):
def __init__(self, reason):
message = 'OAuth token refresh failed: %s' % reason
Expand Down

0 comments on commit 9b892df

Please sign in to comment.