Merge branch 'master' into proxy

This commit is contained in:
Sebastian Bischoff 2017-01-02 14:43:40 +01:00 committed by GitHub
commit 1ef89cfd2c
20 changed files with 516 additions and 133 deletions

View File

@ -6,7 +6,7 @@ Spotipy is a thin client library for the Spotify Web API.
## Documentation
Spotipy's full documentation is online at [Spotipy Documentation](http://spotipy.readthedocs.org/)
Spotipy's full documentation is online at [Spotipy Documentation](http://spotipy.readthedocs.org/)
## Installation
@ -14,7 +14,7 @@ If you already have [Python](http://www.python.org/) on your system you can inst
python setup.py install
You can also install it using a popular package manager with
You can also install it using a popular package manager with
`pip install spotipy`
@ -29,7 +29,7 @@ or
## Quick Start
To get started, simply install spotipy, reate a Spotify object and call methods:
To get started, simply install spotipy, create a Spotify object and call methods:
import spotipy
sp = spotipy.Spotify()
@ -39,7 +39,7 @@ To get started, simply install spotipy, reate a Spotify object and call methods:
print ' ', i, t['name']
A full set of examples can be found in the [online documentation](http://spotipy.readthedocs.org/) and in the [Spotipy examples directory](https://github.com/plamere/spotipy/tree/master/examples).
## Reporting Issues
@ -64,10 +64,11 @@ If you have suggestions, bugs or other issues specific to this library, file the
- v2.0.2 - August 25, 2014 -- Moved to spotipy at pypi
- v2.1.0 - October 25, 2014 -- Added support for new_releases and featured_playlists
- v2.2.0 - November 15, 2014 -- Added support for user_playlist_tracks
- v2.3.0 - January 5, 2015 -- Added session support added by akx.
- v2.3.0 - January 5, 2015 -- Added session support added by akx.
- v2.3.2 - March 31, 2015 -- Added auto retry logic
- v2.3.3 - April 1, 2015 -- added client credential flow
- v2.3.5 - April 28, 2015 -- Fixed bug in auto retry logic
- v2.3.6 - June 3, 2015 -- Support for offset/limit with album_tracks API
- v2.3.7 - August 10, 2015 -- Added current_user_followed_artists
- v2.3.8 - March 30, 2016 -- Added recs, audio features, user top lists
- v2.4.0 - December 31, 2016 -- Incorporated a number of PRs

View File

@ -98,12 +98,32 @@ Many methods require user authentication. For these requests you will need to
generate an authorization token that indicates that the user has granted
permission for your application to perform the given task. You will need to
register your app to get the credentials necessary to make authorized calls.
Even if your script does not have an accessible URL you need to specify one
when registering your application where the spotify authentication API will
redirect to after successful login. The URL doesn't need to work or be
accessible, you can specify "http://localhost/", after successful login you
just need to copy the "http://localhost/?code=..." URL from your browser
and paste it to the console where your script is running.
Register your app at
`My Applications
<https://developer.spotify.com/my-applications/#!/applications>`_.
*Spotipy* provides a
*spotipy* supports two authorization flows:
- The **Authorization Code flow** This method is suitable for long-running applications
which the user logs into once. It provides an access token that can be refreshed.
- The **Client Credentials flow** The method makes it possible
to authenticate your requests to the Spotify Web API and to obtain
a higher rate limit than you would
Authorization Code Flow
=======================
To support the **Authorization Code Flow** *Spotipy* provides a
utility method ``util.prompt_for_user_token`` that will attempt to authorize the
user. You can pass your app credentials directly into the method as arguments,
or if you are reluctant to immortalize your app credentials in your source code,
@ -117,8 +137,9 @@ Call ``util.prompt_for_user_token`` method with the username and the
desired scope (see `Using
Scopes <https://developer.spotify.com/web-api/using-scopes/>`_ for information
about scopes) and credentials. This will coordinate the user authorization via
your web browser. The credentials are cached locally and are used to automatically
re-authorized expired tokens.
your web browser and ask for the SPOTIPY_REDIRECT_URI you were redirected to
with the authorization token appended. The credentials are cached locally and
are used to automatically re-authorized expired tokens.
Here's an example of getting user authorization to read a user's saved tracks::
@ -145,6 +166,31 @@ Here's an example of getting user authorization to read a user's saved tracks::
else:
print "Can't get token for", username
Client Credentials Flow
=======================
To support the **Client Credentials Flow** *Spotipy* provides a
class SpotifyClientCredentials that can be used to authenticate requests like so::
import spotipy
from spotipy.oauth2 import SpotifyClientCredentials
client_credentials_manager = SpotifyClientCredentials()
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
playlists = sp.user_playlists('spotify')
while playlists:
for i, playlist in enumerate(playlists['items']):
print("%4d %s %s" % (i + 1 + playlists['offset'], playlist['uri'], playlist['name']))
if playlists['next']:
playlists = sp.next(playlists)
else:
playlists = None
Client credentials flow is appropriate for requests that do not require access to a
user's private data. Even if you are only making calls that do not require
authorization, using this flow yields the benefit of a higher rate limit
IDs URIs and URLs
=======================
*Spotipy* supports a number of different ID types:
@ -305,6 +351,7 @@ Spotipy authored by Paul Lamere (plamere) with contributions by:
- Steve Winton // swinton
- Tim Balzer // timbalzer
- corycorycory // corycorycory
- Nathan Coleman // nathancoleman
License
=======

View File

@ -0,0 +1,27 @@
# Add tracks to 'Your Collection' of saved tracks
import pprint
import sys
import spotipy
import spotipy.util as util
scope = 'user-library-modify'
if len(sys.argv) > 2:
username = sys.argv[1]
aids = sys.argv[2:]
else:
print("Usage: %s username album-id ..." % (sys.argv[0],))
sys.exit()
token = util.prompt_for_user_token(username, scope)
if token:
sp = spotipy.Spotify(auth=token)
sp.trace = False
results = sp.current_user_saved_albums_add(albums=aids)
pprint.pprint(results)
else:
print("Can't get token for", username)

View File

@ -34,7 +34,7 @@ def show_artist_albums(id):
print('Total albums:', len(albums))
unique = set() # skip duplicate albums
for album in albums:
name = album['name']
name = album['name'].lower()
if not name in unique:
print(name)
unique.add(name)

View File

@ -0,0 +1,23 @@
# shows audio analysis for the given track
from __future__ import print_function # (at top of module)
from spotipy.oauth2 import SpotifyClientCredentials
import json
import spotipy
import time
import sys
client_credentials_manager = SpotifyClientCredentials()
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
if len(sys.argv) > 1:
tid = sys.argv[1]
else:
tid = 'spotify:track:4TTV7EcfroSLWzXRY6gLv6'
start = time.time()
analysis = sp.audio_analysis(tid)
delta = time.time() - start
print(json.dumps(analysis, indent=4))
print ("analysis retrieved in %.2f seconds" % (delta,))

View File

@ -15,14 +15,22 @@ sp.trace=False
if len(sys.argv) > 1:
artist_name = ' '.join(sys.argv[1:])
results = sp.search(q=artist_name, limit=50)
tids = []
for i, t in enumerate(results['tracks']['items']):
print(' ', i, t['name'])
tids.append(t['uri'])
else:
artist_name = 'weezer'
start = time.time()
features = sp.audio_features(tids)
delta = time.time() - start
print(json.dumps(features, indent=4))
print ("features retrieved in %.2f seconds" % (delta,))
results = sp.search(q=artist_name, limit=50)
tids = []
for i, t in enumerate(results['tracks']['items']):
print(' ', i, t['name'])
tids.append(t['uri'])
start = time.time()
features = sp.audio_features(tids)
delta = time.time() - start
for feature in features:
print(json.dumps(feature, indent=4))
print()
analysis = sp._get(feature['analysis_url'])
print(json.dumps(analysis, indent=4))
print()
print ("features retrieved in %.2f seconds" % (delta,))

View File

@ -0,0 +1,38 @@
# Modify the details of a playlist (name, public, collaborative)
import sys
import spotipy
import spotipy.util as util
if len(sys.argv) > 3:
username = sys.argv[1]
playlist_id = sys.argv[2]
name = sys.argv[3]
public = None
if len(sys.argv) > 4:
public = sys.argv[4].lower() == 'true'
collaborative = None
if len(sys.argv) > 5:
collaborative = sys.argv[5].lower() == 'true'
else:
print ("Usage: %s username playlist_id name [public collaborative]" %
(sys.argv[0]))
sys.exit()
scope = 'playlist-modify-public playlist-modify-private'
token = util.prompt_for_user_token(username, scope)
if token:
sp = spotipy.Spotify(auth=token)
sp.trace = False
results = sp.user_playlist_change_details(
username, playlist_id, name=name, public=public,
collaborative=collaborative)
print results
else:
print "Can't get token for", username

26
examples/my_playlists.py Normal file
View File

@ -0,0 +1,26 @@
# Shows the top artists for a user
import pprint
import sys
import spotipy
import spotipy.util as util
import simplejson as json
if len(sys.argv) > 1:
username = sys.argv[1]
else:
print("Usage: %s username" % (sys.argv[0],))
sys.exit()
scope = ''
token = util.prompt_for_user_token(username, scope)
if token:
sp = spotipy.Spotify(auth=token)
sp.trace = False
results = sp.current_user_playlists(limit=50)
for i, item in enumerate(results['items']):
print("%d %s" %(i, item['name']))
else:
print("Can't get token for", username)

View File

@ -1,5 +1,5 @@
# shows artist info for a URN or URL
# shows album info for a URN or URL
import spotipy
import sys
@ -8,10 +8,9 @@ import pprint
if len(sys.argv) > 1:
urn = sys.argv[1]
else:
urn = 'spotify:artist:3jOstUTkEu2JkjvRdBA5Gu'
urn = 'spotify:album:5yTx83u3qerZF7GRJu7eFk'
sp = spotipy.Spotify()
artist = sp.artist(urn)
pprint.pprint(artist)
album = sp.album(urn)
pprint.pprint(album)

View File

@ -1,5 +1,4 @@
# Adds tracks to a playlist
# shows a user's saved tracks (need to be authenticated via oauth)
import sys
import spotipy

View File

@ -0,0 +1,14 @@
# shows artist info for a URN or URL
import spotipy
import sys
import pprint
if len(sys.argv) > 1:
search_str = sys.argv[1]
else:
search_str = 'Radiohead'
sp = spotipy.Spotify(requests_timeout=.1)
result = sp.search(search_str)
pprint.pprint(result)

View File

@ -16,7 +16,7 @@ if __name__ == '__main__':
username = sys.argv[1]
else:
print("Whoops, need your username!")
print("usage: python user_playlists.py [username]")
print("usage: python user_playlists_contents.py [username]")
sys.exit()
token = util.prompt_for_user_token(username)

View File

@ -0,0 +1,25 @@
# Gets all the public playlists for the given
# user. Uses Client Credentials flow
#
import sys
import spotipy
from spotipy.oauth2 import SpotifyClientCredentials
client_credentials_manager = SpotifyClientCredentials()
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
user = 'spotify'
if len(sys.argv) > 1:
user = sys.argv[1]
playlists = sp.user_playlists(user)
while playlists:
for i, playlist in enumerate(playlists['items']):
print("%4d %s %s" % (i + 1 + playlists['offset'], playlist['uri'], playlist['name']))
if playlists['next']:
playlists = sp.next(playlists)
else:
playlists = None

View File

@ -2,7 +2,7 @@ from setuptools import setup
setup(
name='spotipy',
version='2.3.8',
version='2.4.0',
description='simple client for the Spotify Web API',
author="@plamere",
author_email="paul@echonest.com",

View File

@ -3,7 +3,6 @@
from __future__ import print_function
import sys
import base64
import requests
import json
import time
@ -11,16 +10,23 @@ import time
''' A simple and thin Python library for the Spotify Web API
'''
class SpotifyException(Exception):
def __init__(self, http_status, code, msg):
def __init__(self, http_status, code, msg, headers=None):
self.http_status = http_status
self.code = code
self.msg = msg
# `headers` is used to support `Retry-After` in the event of a
# 429 status code.
if headers is None:
headers = {}
self.headers = headers
def __str__(self):
return 'http status: {0}, code:{1} - {2}'.format(
self.http_status, self.code, self.msg)
class Spotify(object):
'''
Example usage::
@ -45,7 +51,7 @@ class Spotify(object):
max_get_retries = 10
def __init__(self, auth=None, requests_session=True,
client_credentials_manager=None, proxies=None):
client_credentials_manager=None, proxies=None, requests_timeout=None):
'''
Create a Spotify API object.
@ -59,12 +65,14 @@ class Spotify(object):
SpotifyClientCredentials object
:param proxies:
Definition of proxies (optional)
:param requests_timeout:
Tell Requests to stop waiting for a response after a given number of seconds
'''
self.prefix = 'https://api.spotify.com/v1/'
self._auth = auth
self.client_credentials_manager = client_credentials_manager
self.proxies = proxies
self.requests_timeout = requests_timeout
if isinstance(requests_session, requests.Session):
self._session = requests_session
@ -86,6 +94,7 @@ class Spotify(object):
def _internal_call(self, method, url, payload, params):
args = dict(params=params)
args["timeout"] = self.requests_timeout
if not url.startswith('http'):
url = self.prefix + url
headers = self._auth_headers()
@ -111,10 +120,11 @@ class Spotify(object):
except:
if r.text and len(r.text) > 0 and r.text != 'null':
raise SpotifyException(r.status_code,
-1, '%s:\n %s' % (r.url, r.json()['error']['message']))
-1, '%s:\n %s' % (r.url, r.json()['error']['message']),
headers=r.headers)
else:
raise SpotifyException(r.status_code,
-1, '%s:\n %s' % (r.url, 'error'))
-1, '%s:\n %s' % (r.url, 'error'), headers=r.headers)
finally:
r.connection.close()
if r.text and len(r.text) > 0 and r.text != 'null':
@ -142,25 +152,26 @@ class Spotify(object):
if retries < 0:
raise
else:
print ('retrying ...' + str(delay) + 'secs')
time.sleep(delay)
sleep_seconds = int(e.headers.get('Retry-After', delay))
print ('retrying ...' + str(sleep_seconds) + 'secs')
time.sleep(sleep_seconds)
delay += 1
else:
raise
except Exception as e:
except Exception as e:
raise
print ('exception', str(e))
# some other exception. Requests have
# been know to throw a BadStatusLine exception
retries -= 1
if retries >= 0:
sleep_seconds = int(e.headers.get('Retry-After', delay))
print ('retrying ...' + str(delay) + 'secs')
time.sleep(delay)
time.sleep(sleep_seconds)
delay += 1
else:
raise
def _post(self, url, args=None, payload=None, **kwargs):
if args:
kwargs.update(args)
@ -211,15 +222,16 @@ class Spotify(object):
trid = self._get_id('track', track_id)
return self._get('tracks/' + trid)
def tracks(self, tracks):
def tracks(self, tracks, market = None):
''' returns a list of tracks given a list of track IDs, URIs, or URLs
Parameters:
- tracks - a list of spotify URIs, URLs or IDs
- market - an ISO 3166-1 alpha-2 country code.
'''
tlist = [self._get_id('track', t) for t in tracks]
return self._get('tracks/?ids=' + ','.join(tlist))
return self._get('tracks/?ids=' + ','.join(tlist), market = market)
def artist(self, artist_id):
''' returns a single artist given the artist's ID, URI or URL
@ -231,7 +243,6 @@ class Spotify(object):
trid = self._get_id('artist', artist_id)
return self._get('artists/' + trid)
def artists(self, artists):
''' returns a list of artists given the artist IDs, URIs, or URLs
@ -242,8 +253,8 @@ class Spotify(object):
tlist = [self._get_id('artist', a) for a in artists]
return self._get('artists/?ids=' + ','.join(tlist))
def artist_albums(self, artist_id, album_type=None, country=None,
limit=20, offset=0):
def artist_albums(self, artist_id, album_type=None, country=None, limit=20,
offset=0):
''' Get Spotify catalog information about an artist's albums
Parameters:
@ -256,7 +267,7 @@ class Spotify(object):
trid = self._get_id('artist', artist_id)
return self._get('artists/' + trid + '/albums', album_type=album_type,
country=country, limit=limit, offset=offset)
country=country, limit=limit, offset=offset)
def artist_top_tracks(self, artist_id, country='US'):
''' Get Spotify catalog information about an artist's top 10 tracks
@ -301,7 +312,8 @@ class Spotify(object):
'''
trid = self._get_id('album', album_id)
return self._get('albums/' + trid + '/tracks/', limit=limit, offset=offset)
return self._get('albums/' + trid + '/tracks/', limit=limit,
offset=offset)
def albums(self, albums):
''' returns a list of albums given the album IDs, URIs, or URLs
@ -313,7 +325,7 @@ class Spotify(object):
tlist = [self._get_id('album', a) for a in albums]
return self._get('albums/?ids=' + ','.join(tlist))
def search(self, q, limit=10, offset=0, type='track'):
def search(self, q, limit=10, offset=0, type='track', market=None):
''' searches for an item
Parameters:
@ -322,8 +334,9 @@ class Spotify(object):
- offset - the index of the first item to return
- type - the type of item to return. One of 'artist', 'album',
'track' or 'playlist'
- market - An ISO 3166-1 alpha-2 country code or the string from_token.
'''
return self._get('search', q=q, limit=limit, offset=offset, type=type)
return self._get('search', q=q, limit=limit, offset=offset, type=type, market=market)
def user(self, user):
''' Gets basic profile information about a Spotify User
@ -333,6 +346,14 @@ class Spotify(object):
'''
return self._get('users/' + user)
def current_user_playlists(self, limit=50, offset=0):
""" Get current user playlists without required getting his profile
Parameters:
- limit - the number of items to return
- offset - the index of the first item to return
"""
return self._get("me/playlists", limit=limit, offset=offset)
def user_playlists(self, user, limit=50, offset=0):
''' Gets playlists of a user
@ -341,22 +362,23 @@ class Spotify(object):
- limit - the number of items to return
- offset - the index of the first item to return
'''
return self._get("users/%s/playlists" % user, limit=limit, offset=offset)
return self._get("users/%s/playlists" % user, limit=limit,
offset=offset)
def user_playlist(self, user, playlist_id = None, fields=None):
def user_playlist(self, user, playlist_id=None, fields=None):
''' Gets playlist of a user
Parameters:
- user - the id of the user
- playlist_id - the id of the playlist
- fields - which fields to return
'''
if playlist_id == None:
if playlist_id is None:
return self._get("users/%s/starred" % (user), fields=fields)
plid = self._get_id('playlist', playlist_id)
return self._get("users/%s/playlists/%s" % (user, plid), fields=fields)
def user_playlist_tracks(self, user, playlist_id = None, fields=None,
limit=100, offset=0, market=None):
def user_playlist_tracks(self, user, playlist_id=None, fields=None,
limit=100, offset=0, market=None):
''' Get full details of the tracks of a playlist owned by a user.
Parameters:
@ -369,7 +391,8 @@ class Spotify(object):
'''
plid = self._get_id('playlist', playlist_id)
return self._get("users/%s/playlists/%s/tracks" % (user, plid),
limit=limit, offset=offset, fields=fields, market=market)
limit=limit, offset=offset, fields=fields,
market=market)
def user_playlist_create(self, user, name, public=True):
''' Creates a playlist for a user
@ -379,11 +402,42 @@ class Spotify(object):
- name - the name of the playlist
- public - is the created playlist public
'''
data = {'name':name, 'public':public }
return self._post("users/%s/playlists" % (user,), payload = data)
data = {'name': name, 'public': public}
return self._post("users/%s/playlists" % (user,), payload=data)
def user_playlist_change_details(
self, user, playlist_id, name=None, public=None,
collaborative=None):
''' Changes a playlist's name and/or public/private state
Parameters:
- user - the id of the user
- playlist_id - the id of the playlist
- name - optional name of the playlist
- public - optional is the playlist public
- collaborative - optional is the playlist collaborative
'''
data = {}
if isinstance(name, basestring):
data['name'] = name
if isinstance(public, bool):
data['public'] = public
if isinstance(collaborative, bool):
data['collaborative'] = collaborative
return self._put("users/%s/playlists/%s" % (user, playlist_id),
payload=data)
def user_playlist_unfollow(self, user, playlist_id):
''' Unfollows (deletes) a playlist for a user
Parameters:
- user - the id of the user
- name - the name of the playlist
'''
return self._delete("users/%s/playlists/%s/followers" % (user, playlist_id))
def user_playlist_add_tracks(self, user, playlist_id, tracks,
position=None):
position=None):
''' Adds tracks to a playlist
Parameters:
@ -393,9 +447,9 @@ class Spotify(object):
- position - the position to add the tracks
'''
plid = self._get_id('playlist', playlist_id)
ftracks = [ self._get_uri('track', tid) for tid in tracks]
return self._post("users/%s/playlists/%s/tracks" % (user,plid),
payload = ftracks, position=position)
ftracks = [self._get_uri('track', tid) for tid in tracks]
return self._post("users/%s/playlists/%s/tracks" % (user, plid),
payload=ftracks, position=position)
def user_playlist_replace_tracks(self, user, playlist_id, tracks):
''' Replace all tracks in a playlist
@ -406,13 +460,14 @@ class Spotify(object):
- tracks - the list of track ids to add to the playlist
'''
plid = self._get_id('playlist', playlist_id)
ftracks = [ self._get_uri('track', tid) for tid in tracks]
payload = { "uris": ftracks }
return self._put("users/%s/playlists/%s/tracks" % (user,plid),
payload = payload)
ftracks = [self._get_uri('track', tid) for tid in tracks]
payload = {"uris": ftracks}
return self._put("users/%s/playlists/%s/tracks" % (user, plid),
payload=payload)
def user_playlist_reorder_tracks(self, user, playlist_id, range_start, insert_before,
range_length=1, snapshot_id=None):
def user_playlist_reorder_tracks(
self, user, playlist_id, range_start, insert_before,
range_length=1, snapshot_id=None):
''' Reorder tracks in a playlist
Parameters:
@ -424,16 +479,16 @@ class Spotify(object):
- snapshot_id - optional playlist's snapshot ID
'''
plid = self._get_id('playlist', playlist_id)
payload = { "range_start": range_start,
"range_length": range_length,
"insert_before": insert_before }
payload = {"range_start": range_start,
"range_length": range_length,
"insert_before": insert_before}
if snapshot_id:
payload["snapshot_id"] = snapshot_id
return self._put("users/%s/playlists/%s/tracks" % (user,plid),
payload = payload)
return self._put("users/%s/playlists/%s/tracks" % (user, plid),
payload=payload)
def user_playlist_remove_all_occurrences_of_tracks(self, user, playlist_id,
tracks, snapshot_id=None):
def user_playlist_remove_all_occurrences_of_tracks(
self, user, playlist_id, tracks, snapshot_id=None):
''' Removes all occurrences of the given tracks from the given playlist
Parameters:
@ -445,15 +500,15 @@ class Spotify(object):
'''
plid = self._get_id('playlist', playlist_id)
ftracks = [ self._get_uri('track', tid) for tid in tracks]
payload = { "tracks": [ {"uri": track} for track in ftracks] }
ftracks = [self._get_uri('track', tid) for tid in tracks]
payload = {"tracks": [{"uri": track} for track in ftracks]}
if snapshot_id:
payload["snapshot_id"] = snapshot_id
return self._delete("users/%s/playlists/%s/tracks" % (user, plid),
payload = payload)
payload=payload)
def user_playlist_remove_specific_occurrences_of_tracks(self, user,
playlist_id, tracks, snapshot_id=None):
def user_playlist_remove_specific_occurrences_of_tracks(
self, user, playlist_id, tracks, snapshot_id=None):
''' Removes all occurrences of the given tracks from the given playlist
Parameters:
@ -472,11 +527,34 @@ class Spotify(object):
"uri": self._get_uri("track", tr["uri"]),
"positions": tr["positions"],
})
payload = { "tracks": ftracks }
payload = {"tracks": ftracks}
if snapshot_id:
payload["snapshot_id"] = snapshot_id
return self._delete("users/%s/playlists/%s/tracks" % (user, plid),
payload = payload)
payload=payload)
def user_playlist_follow_playlist(self, playlist_owner_id, playlist_id):
'''
Add the current authenticated user as a follower of a playlist.
Parameters:
- playlist_owner_id - the user id of the playlist owner
- playlist_id - the id of the playlist
'''
return self._put("users/{}/playlists/{}/followers".format(playlist_owner_id, playlist_id))
def user_playlist_is_following(self, playlist_owner_id, playlist_id, user_ids):
'''
Check to see if the given users are following the given playlist
Parameters:
- playlist_owner_id - the user id of the playlist owner
- playlist_id - the id of the playlist
- user_ids - the ids of the users that you want to check to see if they follow the playlist. Maximum: 5 ids.
'''
return self._get("users/{}/playlists/{}/followers/contains?ids={}".format(playlist_owner_id, playlist_id, ','.join(user_ids)))
def me(self):
''' Get detailed profile information about the current user.
@ -511,7 +589,7 @@ class Spotify(object):
'''
return self._get('me/tracks', limit=limit, offset=offset)
def current_user_followed_artists(self, limit=20, after=None):
''' Gets a list of the artists followed by the current authorized user
@ -520,64 +598,83 @@ class Spotify(object):
- after - ghe last artist ID retrieved from the previous request
'''
return self._get('me/following', type='artist', limit=limit, after=after)
return self._get('me/following', type='artist', limit=limit,
after=after)
def current_user_saved_tracks_delete(self, tracks=[]):
def current_user_saved_tracks_delete(self, tracks=None):
''' Remove one or more tracks from the current user's
"Your Music" library.
Parameters:
- tracks - a list of track URIs, URLs or IDs
'''
tlist = [self._get_id('track', t) for t in tracks]
tlist = []
if tracks is not None:
tlist = [self._get_id('track', t) for t in tracks]
return self._delete('me/tracks/?ids=' + ','.join(tlist))
def current_user_saved_tracks_contains(self, tracks=[]):
def current_user_saved_tracks_contains(self, tracks=None):
''' Check if one or more tracks is already saved in
the current Spotify users Your Music library.
Parameters:
- tracks - a list of track URIs, URLs or IDs
'''
tlist = [self._get_id('track', t) for t in tracks]
tlist = []
if tracks is not None:
tlist = [self._get_id('track', t) for t in tracks]
return self._get('me/tracks/contains?ids=' + ','.join(tlist))
def current_user_saved_tracks_add(self, tracks=[]):
def current_user_saved_tracks_add(self, tracks=None):
''' Add one or more tracks to the current user's
"Your Music" library.
Parameters:
- tracks - a list of track URIs, URLs or IDs
'''
tlist = [self._get_id('track', t) for t in tracks]
tlist = []
if tracks is not None:
tlist = [self._get_id('track', t) for t in tracks]
return self._put('me/tracks/?ids=' + ','.join(tlist))
def current_user_top_artists(self, limit=20, offset=0, time_range='medium_term'):
def current_user_top_artists(self, limit=20, offset=0,
time_range='medium_term'):
''' Get the current user's top artists
Parameters:
- limit - the number of entities to return
- offset - the index of the first entity to return
- time_range - Over what time frame are the affinities computed.
- time_range - Over what time frame are the affinities computed
Valid-values: short_term, medium_term, long_term
'''
return self._get('me/top/artists', time_range=time_range, limit=limit,offset=offset)
return self._get('me/top/artists', time_range=time_range, limit=limit,
offset=offset)
def current_user_top_tracks(self, limit=20, offset=0, time_range='medium_term'):
def current_user_top_tracks(self, limit=20, offset=0,
time_range='medium_term'):
''' Get the current user's top tracks
Parameters:
- limit - the number of entities to return
- offset - the index of the first entity to return
- time_range - Over what time frame are the affinities computed.
- time_range - Over what time frame are the affinities computed
Valid-values: short_term, medium_term, long_term
'''
return self._get('me/top/tracks', time_range=time_range, limit=limit,offset=offset)
return self._get('me/top/tracks', time_range=time_range, limit=limit,
offset=offset)
def current_user_saved_albums_add(self, albums=[]):
''' Add one or more albums to the current user's
"Your Music" library.
Parameters:
- albums - a list of album URIs, URLs or IDs
'''
alist = [self._get_id('album', a) for a in albums]
r = self._put('me/albums?ids=' + ','.join(alist))
return r
def featured_playlists(self, locale=None, country=None,
timestamp=None, limit=20, offset = 0):
def featured_playlists(self, locale=None, country=None, timestamp=None,
limit=20, offset=0):
''' Get a list of Spotify featured playlists
Parameters:
@ -600,9 +697,10 @@ class Spotify(object):
items.
'''
return self._get('browse/featured-playlists', locale=locale,
country=country, timestamp=timestamp, limit=limit, offset=offset)
country=country, timestamp=timestamp, limit=limit,
offset=offset)
def new_releases(self, country=None, limit=20, offset = 0):
def new_releases(self, country=None, limit=20, offset=0):
''' Get a list of new album releases featured in Spotify
Parameters:
@ -615,10 +713,10 @@ class Spotify(object):
(the first object). Use with limit to get the next set of
items.
'''
return self._get('browse/new-releases', country=country,
limit=limit, offset=offset)
return self._get('browse/new-releases', country=country, limit=limit,
offset=offset)
def categories(self, country=None, locale=None, limit=20, offset = 0):
def categories(self, country=None, locale=None, limit=20, offset=0):
''' Get a list of new album releases featured in Spotify
Parameters:
@ -635,9 +733,10 @@ class Spotify(object):
items.
'''
return self._get('browse/categories', country=country, locale=locale,
limit=limit, offset=offset)
limit=limit, offset=offset)
def category_playlists(self, category_id=None, country=None, limit=20, offset = 0):
def category_playlists(self, category_id=None, country=None, limit=20,
offset=0):
''' Get a list of new album releases featured in Spotify
Parameters:
@ -652,11 +751,11 @@ class Spotify(object):
(the first object). Use with limit to get the next set of
items.
'''
return self._get('browse/categories/' + category_id + '/playlists', country=country,
limit=limit, offset=offset)
return self._get('browse/categories/' + category_id + '/playlists',
country=country, limit=limit, offset=offset)
def recommendations(self, seed_artists=[], seed_genres=[], seed_tracks=[],
limit = 20, country=None, **kwargs):
def recommendations(self, seed_artists=None, seed_genres=None,
seed_tracks=None, limit=20, country=None, **kwargs):
''' Get a list of recommended tracks for one to five seeds.
Parameters:
@ -664,50 +763,61 @@ class Spotify(object):
- seed_tracks - a list of artist IDs, URIs or URLs
- seed_genres - a list of genre names. Available genres for
- seed_genres - a list of genre names. Available genres for
recommendations can be found by calling recommendation_genre_seeds
- country - An ISO 3166-1 alpha-2 country code. If provided, all
- country - An ISO 3166-1 alpha-2 country code. If provided, all
results will be playable in this country.
- limit - The maximum number of items to return. Default: 20.
Minimum: 1. Maximum: 100
- min/max/target_<attribute> - For the tuneable track attributes listed
- min/max/target_<attribute> - For the tuneable track attributes listed
in the documentation, these values provide filters and targeting on
results.
'''
params = dict(limit=limit)
if seed_artists:
params['seed_artists'] = [self._get_id('artist', a) for a in seed_artists]
params['seed_artists'] = ','.join(
[self._get_id('artist', a) for a in seed_artists])
if seed_genres:
params['seed_genres'] = seed_genres
params['seed_genres'] = ','.join(seed_genres)
if seed_tracks:
params['seed_tracks'] = [self._get_id('track', t) for t in seed_tracks]
params['seed_tracks'] = ','.join(
[self._get_id('track', t) for t in seed_tracks])
if country:
params['market'] = country
for attribute in ["acousticness", "danceability", "duration_ms", "energy",
"instrumentalness", "key", "liveness", "loudness", "mode", "popularity",
"speechiness", "tempo", "time_signature", "valence"]:
for attribute in ["acousticness", "danceability", "duration_ms",
"energy", "instrumentalness", "key", "liveness",
"loudness", "mode", "popularity", "speechiness",
"tempo", "time_signature", "valence"]:
for prefix in ["min_", "max_", "target_"]:
param = prefix + attribute
if param in kwargs:
params[param] = kwargs[param]
return self._get('recommendations', **params)
return self._get('recommendations', **params)
def recommendation_genre_seeds(self):
''' Get a list of genres available for the recommendations function.
''' Get a list of genres available for the recommendations function.
'''
return self._get('recommendations/available-genre-seeds')
def audio_analysis(self, track_id):
''' Get audio analysis for a track based upon its Spotify ID
Parameters:
- track_id - a track URI, URL or ID
'''
trid = self._get_id('track', track_id)
return self._get('audio-analysis/' + trid)
def audio_features(self, tracks=[]):
''' Get audio features for multiple tracks based upon their Spotify IDs
Parameters:
- tracks - a list of track URIs, URLs or IDs, maximum: 50 ids
'''
tlist = [self._get_id('track', t) for t in tracks]
results = self._get('audio-features?ids=' + ','.join(tlist))
results = self._get('audio-features?ids=' + ','.join(tlist))
# the response has changed, look for the new style first, and if
# its not there, fallback on the old style
if 'audio_features' in results:
@ -715,19 +825,27 @@ class Spotify(object):
else:
return results
def audio_analysis(self, id):
''' Get audio analysis for a track based upon its Spotify ID
Parameters:
- id - a track URIs, URLs or IDs
'''
id = self._get_id('track', id)
return self._get('audio-analysis/'+id)
def _get_id(self, type, id):
fields = id.split(':')
if len(fields) >= 3:
if type != fields[-2]:
self._warn('expected id of type ' + type + ' but found type ' \
+ fields[2] + " " + id)
self._warn('expected id of type %s but found type %s %s',
type, fields[-2], id)
return fields[-1]
fields = id.split('/')
if len(fields) >= 3:
itype = fields[-2]
if type != itype:
self._warn('expected id of type ' + type + ' but found type ' \
+ itype + " " + id)
self._warn('expected id of type %s but found type %s %s',
type, itype, id)
return fields[-1]
return id

View File

@ -79,7 +79,7 @@ class SpotifyClientCredentials(object):
def _is_token_expired(self, token_info):
now = int(time.time())
return token_info['expires_at'] < now
return token_info['expires_at'] - now < 60
def _add_custom_values_to_token_info(self, token_info):
"""
@ -131,11 +131,11 @@ class SpotifyOAuth(object):
token_info = json.loads(token_info_string)
# if scopes don't match, then bail
if 'scope' not in token_info or self.scope != token_info['scope']:
if 'scope' not in token_info or not self._is_scope_subset(self.scope, token_info['scope']):
return None
if self._is_token_expired(token_info):
token_info = self._refresh_access_token(token_info['refresh_token'])
token_info = self.refresh_access_token(token_info['refresh_token'])
except IOError:
pass
@ -151,6 +151,11 @@ class SpotifyOAuth(object):
self._warn("couldn't write token cache to " + self.cache_path)
pass
def _is_scope_subset(self, needle_scope, haystack_scope):
needle_scope = set(needle_scope.split())
haystack_scope = set(haystack_scope.split())
return needle_scope <= haystack_scope
def _is_token_expired(self, token_info):
now = int(time.time())
@ -222,7 +227,7 @@ class SpotifyOAuth(object):
else:
return None
def _refresh_access_token(self, refresh_token):
def refresh_access_token(self, refresh_token):
payload = { 'refresh_token': refresh_token,
'grant_type': 'refresh_token'}

View File

@ -3,9 +3,9 @@
from __future__ import print_function
import os
import subprocess
from . import oauth2
import spotipy
import webbrowser
def prompt_for_user_token(username, scope=None, client_id = None,
client_secret = None, redirect_uri = None):
@ -67,8 +67,8 @@ def prompt_for_user_token(username, scope=None, client_id = None,
''')
auth_url = sp_oauth.get_authorize_url()
try:
subprocess.call(["open", auth_url])
print("Opening %s in your browser" % auth_url)
webbrowser.open(auth_url)
print("Opened %s in your browser" % auth_url)
except:
print("Please navigate here: %s" % auth_url)

View File

@ -86,6 +86,24 @@ class AuthTestSpotipy(unittest.TestCase):
albums = spotify.current_user_saved_albums()
self.assertTrue(len(albums['items']) > 0)
def test_current_user_playlists(self):
playlists = spotify.current_user_playlists(limit=10)
self.assertTrue('items' in playlists)
self.assertTrue(len(playlists['items']) == 10)
def test_user_playlist_follow(self):
spotify.user_playlist_follow_playlist('plamere', '4erXB04MxwRAVqcUEpu30O')
follows = spotify.user_playlist_is_following('plamere', '4erXB04MxwRAVqcUEpu30O', ['plamere'])
self.assertTrue(len(follows) == 1, 'proper follows length')
self.assertTrue(follows[0], 'is following')
spotify.user_playlist_unfollow('plamere', '4erXB04MxwRAVqcUEpu30O')
follows = spotify.user_playlist_is_following('plamere', '4erXB04MxwRAVqcUEpu30O', ['plamere'])
self.assertTrue(len(follows) == 1, 'proper follows length')
self.assertFalse(follows[0], 'is no longer following')
def test_current_user_save_and_unsave_tracks(self):
tracks = spotify.current_user_saved_tracks()
total = tracks['total']

View File

@ -37,6 +37,10 @@ class AuthTestSpotipy(unittest.TestCase):
bad_id = 'BAD_ID'
def test_audio_analysis(self):
result = spotify.audio_analysis(self.four_tracks[0])
assert('beats' in result)
def test_audio_features(self):
results = spotify.audio_features(self.four_tracks)
self.assertTrue(len(results) == len(self.four_tracks))

View File

@ -2,6 +2,7 @@
import spotipy
import unittest
import pprint
import requests
from spotipy.client import SpotifyException
@ -11,6 +12,7 @@ class TestSpotipy(unittest.TestCase):
creep_id = '3HfB5hBU0dmBt8T0iCmH42'
creep_url = 'http://open.spotify.com/track/3HfB5hBU0dmBt8T0iCmH42'
el_scorcho_urn = 'spotify:track:0Svkvt5I79wficMFgaqEQJ'
el_scorcho_bad_urn = 'spotify:track:0Svkvt5I79wficMFgaqEQK'
pinkerton_urn = 'spotify:album:04xe676vyiTeYNXw15o9jT'
weezer_urn = 'spotify:artist:3jOstUTkEu2JkjvRdBA5Gu'
pablo_honey_urn = 'spotify:album:6AZv3m27uyRxi8KyJSfUxL'
@ -39,7 +41,7 @@ class TestSpotipy(unittest.TestCase):
def test_album_tracks(self):
results = self.spotify.album_tracks(self.pinkerton_urn)
self.assertTrue(len(results['items']) == 10)
def test_album_tracks_many(self):
results = self.spotify.album_tracks(self.angeles_haydn_urn)
tracks = results['items']
@ -68,6 +70,13 @@ class TestSpotipy(unittest.TestCase):
track = self.spotify.track(self.creep_url)
self.assertTrue(track['name'] == 'Creep')
def test_track_bad_urn(self):
try:
track = self.spotify.track(self.el_scorcho_bad_urn)
self.assertTrue(False)
except spotipy.SpotifyException:
self.assertTrue(True)
def test_tracks(self):
results = self.spotify.tracks([self.creep_url, self.el_scorcho_urn])
self.assertTrue('tracks' in results)
@ -83,7 +92,7 @@ class TestSpotipy(unittest.TestCase):
self.assertTrue('artists' in results)
self.assertTrue(len(results['artists']) == 20)
for artist in results['artists']:
if artist['name'] == 'Rivers Cuomo':
if artist['name'] == 'Jimmy Eat World':
found = True
self.assertTrue(found)
@ -93,6 +102,12 @@ class TestSpotipy(unittest.TestCase):
self.assertTrue(len(results['artists']['items']) > 0)
self.assertTrue(results['artists']['items'][0]['name'] == 'Weezer')
def test_artist_search_with_market(self):
results = self.spotify.search(q='weezer', type='artist', market='GB')
self.assertTrue('artists' in results)
self.assertTrue(len(results['artists']['items']) > 0)
self.assertTrue(results['artists']['items'][0]['name'] == 'Weezer')
def test_artist_albums(self):
results = self.spotify.artist_albums(self.weezer_urn)
self.assertTrue('items' in results)
@ -105,6 +120,15 @@ class TestSpotipy(unittest.TestCase):
self.assertTrue(found)
def test_search_timeout(self):
sp = spotipy.Spotify(requests_timeout=.1)
try:
results = sp.search(q='my*', type='track')
self.assertTrue(False, 'unexpected search timeout')
except requests.ReadTimeout:
self.assertTrue(True, 'expected search timeout')
def test_album_search(self):
results = self.spotify.search(q='weezer pinkerton', type='album')
self.assertTrue('albums' in results)
@ -128,6 +152,13 @@ class TestSpotipy(unittest.TestCase):
except spotipy.SpotifyException:
self.assertTrue(True)
def test_track_bad_id(self):
try:
track = self.spotify.track(self.bad_id)
self.assertTrue(False)
except spotipy.SpotifyException:
self.assertTrue(True)
def test_unauthenticated_post_fails(self):
with self.assertRaises(SpotifyException) as cm:
self.spotify.user_playlist_create("spotify", "Best hits of the 90s")