From def97776071f9e502a1e45537af08769bb5f9e24 Mon Sep 17 00:00:00 2001 From: Lorenzo Mele Date: Tue, 14 Apr 2015 01:03:29 +0200 Subject: [PATCH 01/46] Update __init__.py fixing issue #41: importing client is made using relative path --- spotipy/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spotipy/__init__.py b/spotipy/__init__.py index 9339d2a..9be1dce 100644 --- a/spotipy/__init__.py +++ b/spotipy/__init__.py @@ -1,2 +1,2 @@ VERSION='2.0.1' -from client import Spotify, SpotifyException +from .client import Spotify, SpotifyException From ec638870ca6f47df57c9fa0b4eaab346e5fc551d Mon Sep 17 00:00:00 2001 From: Mattia Maestrini Date: Wed, 5 Aug 2015 18:52:38 +0100 Subject: [PATCH 02/46] Add 'market' parameter to the search endpoint --- spotipy/client.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spotipy/client.py b/spotipy/client.py index c926814..1a2a244 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -288,7 +288,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: @@ -297,8 +297,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 From f832bd54dd293c9b76d46d48e00ba1430eab5566 Mon Sep 17 00:00:00 2001 From: Gerhard Poul Date: Tue, 13 Oct 2015 21:20:53 +0200 Subject: [PATCH 03/46] Fix usage message --- examples/user_playlists_contents.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/user_playlists_contents.py b/examples/user_playlists_contents.py index b0bac03..e03ceec 100644 --- a/examples/user_playlists_contents.py +++ b/examples/user_playlists_contents.py @@ -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) From ffe64828b53c868b6b79066c66ecd4f7f9f70b74 Mon Sep 17 00:00:00 2001 From: Aarno Aukia Date: Sat, 31 Oct 2015 16:59:04 +0100 Subject: [PATCH 04/46] clarify SPOTIPY_REDIRECT_URI registration/authentication process --- docs/index.rst | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 5889bc4..855e87e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -98,6 +98,14 @@ 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 `_. @@ -117,8 +125,9 @@ Call ``util.prompt_for_user_token`` method with the username and the desired scope (see `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:: From 8de2591c18013e6065bbc8b58f7af08acae59073 Mon Sep 17 00:00:00 2001 From: myselfhimself Date: Wed, 2 Dec 2015 20:43:13 +0100 Subject: [PATCH 05/46] Avoid unneeded token renewal when cached token's scope contains currently required scope Before that there was a strict string comparision "cached scope" == "currently required scope". Added a scope subset detection method. --- spotipy/oauth2.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/spotipy/oauth2.py b/spotipy/oauth2.py index bb10e32..e6b300c 100644 --- a/spotipy/oauth2.py +++ b/spotipy/oauth2.py @@ -130,7 +130,7 @@ 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): @@ -150,6 +150,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()) From 83e42efa88be2008cd8e8a358a076f93ddae3a7a Mon Sep 17 00:00:00 2001 From: eugenio412 Date: Fri, 8 Apr 2016 17:12:31 +0200 Subject: [PATCH 06/46] include refresh_access_token method to the library in this way it's possible to refresh token even if it's not cached but saved somewhere else --- spotipy/oauth2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spotipy/oauth2.py b/spotipy/oauth2.py index bb10e32..4ed635c 100644 --- a/spotipy/oauth2.py +++ b/spotipy/oauth2.py @@ -134,7 +134,7 @@ class SpotifyOAuth(object): 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 @@ -221,7 +221,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'} From e8e82c3c496d824151a9946c3ecf54c2701a744d Mon Sep 17 00:00:00 2001 From: felipeumanzor Date: Sat, 16 Apr 2016 20:20:35 -0300 Subject: [PATCH 07/46] Duplicated album detection improved not case sensitive. --- examples/artist_discography.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/artist_discography.py b/examples/artist_discography.py index 8d30d15..34114bb 100644 --- a/examples/artist_discography.py +++ b/examples/artist_discography.py @@ -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) From 2096d32a4e735123bcb5183d18cf92324fe733e0 Mon Sep 17 00:00:00 2001 From: Christopher Li Date: Sun, 22 May 2016 18:57:16 -0400 Subject: [PATCH 08/46] Added user_playlist_unfollow() method --- spotipy/client.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/spotipy/client.py b/spotipy/client.py index be9162b..6bcc65c 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -157,7 +157,6 @@ class Spotify(object): else: raise - def _post(self, url, args=None, payload=None, **kwargs): if args: kwargs.update(args) @@ -378,6 +377,16 @@ class Spotify(object): data = {'name':name, 'public':public } return self._post("users/%s/playlists" % (user,), 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 + - public - is the created playlist public + ''' + return self._delete("users/%s/playlists/%s/followers" % (user, playlist_id)) + def user_playlist_add_tracks(self, user, playlist_id, tracks, position=None): ''' Adds tracks to a playlist From 70701d83872c6d190ca5ba086999693ff5589905 Mon Sep 17 00:00:00 2001 From: Christopher Li Date: Sun, 22 May 2016 19:05:22 -0400 Subject: [PATCH 09/46] Adjusted docstring for user_playlist_unfollow method --- spotipy/client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/spotipy/client.py b/spotipy/client.py index 6bcc65c..a773526 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -383,7 +383,6 @@ class Spotify(object): Parameters: - user - the id of the user - name - the name of the playlist - - public - is the created playlist public ''' return self._delete("users/%s/playlists/%s/followers" % (user, playlist_id)) From 1fae780adbbfb0809c79f7147898426849cc7dac Mon Sep 17 00:00:00 2001 From: Nathan Coleman Date: Tue, 31 May 2016 20:02:06 -0400 Subject: [PATCH 10/46] Support changing details for a user playlist --- spotipy/client.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/spotipy/client.py b/spotipy/client.py index be9162b..685e145 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -378,6 +378,24 @@ class Spotify(object): 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): + ''' 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 + ''' + data = {} + if name is not None: + data['name'] = name + if public is not None: + data['public'] = public + return self._put("users/%s/playlists/%s" % (user, playlist_id), + payload=data) + def user_playlist_add_tracks(self, user, playlist_id, tracks, position=None): ''' Adds tracks to a playlist From 80bcbb18934fe4e97be7c5fd37636c9c80f853de Mon Sep 17 00:00:00 2001 From: Nathan Coleman Date: Tue, 31 May 2016 20:02:25 -0400 Subject: [PATCH 11/46] Make a pass for cleanup and PEP8 compliance --- spotipy/client.py | 195 +++++++++++++++++++++++++--------------------- 1 file changed, 106 insertions(+), 89 deletions(-) diff --git a/spotipy/client.py b/spotipy/client.py index 685e145..ec3476d 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -3,7 +3,6 @@ from __future__ import print_function import sys -import base64 import requests import json import time @@ -11,6 +10,7 @@ import time ''' A simple and thin Python library for the Spotify Web API ''' + class SpotifyException(Exception): def __init__(self, http_status, code, msg): self.http_status = http_status @@ -21,6 +21,7 @@ class SpotifyException(Exception): return 'http status: {0}, code:{1} - {2}'.format( self.http_status, self.code, self.msg) + class Spotify(object): ''' Example usage:: @@ -45,7 +46,7 @@ class Spotify(object): max_get_retries = 10 def __init__(self, auth=None, requests_session=True, - client_credentials_manager=None): + client_credentials_manager=None): ''' Create a Spotify API object. @@ -107,11 +108,12 @@ class Spotify(object): r.raise_for_status() 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'])) + raise SpotifyException( + r.status_code, -1, + '%s:\n %s' % (r.url, r.json()['error']['message'])) else: - raise SpotifyException(r.status_code, - -1, '%s:\n %s' % (r.url, 'error')) + raise SpotifyException( + r.status_code, -1, '%s:\n %s' % (r.url, 'error')) finally: r.connection.close() if r.text and len(r.text) > 0 and r.text != 'null': @@ -144,7 +146,7 @@ class Spotify(object): delay += 1 else: raise - except Exception as e: + except Exception as e: raise print ('exception', str(e)) # some other exception. Requests have @@ -157,7 +159,6 @@ class Spotify(object): else: raise - def _post(self, url, args=None, payload=None, **kwargs): if args: kwargs.update(args) @@ -228,7 +229,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 @@ -239,8 +239,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: @@ -253,7 +253,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 @@ -298,7 +298,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 @@ -338,22 +339,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): + def user_playlist_tracks(self, user, playlist_id=None, fields=None, + limit=100, offset=0): ''' Get full details of the tracks of a playlist owned by a user. Parameters: @@ -365,7 +367,7 @@ 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) + limit=limit, offset=offset, fields=fields) def user_playlist_create(self, user, name, public=True): ''' Creates a playlist for a user @@ -375,8 +377,8 @@ 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): @@ -397,7 +399,7 @@ class Spotify(object): payload=data) def user_playlist_add_tracks(self, user, playlist_id, tracks, - position=None): + position=None): ''' Adds tracks to a playlist Parameters: @@ -407,9 +409,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 @@ -420,13 +422,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: @@ -438,16 +441,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: @@ -459,15 +462,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: @@ -486,11 +489,11 @@ 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 me(self): ''' Get detailed profile information about the current user. @@ -525,7 +528,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 @@ -534,64 +537,73 @@ 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 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._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 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: @@ -614,9 +626,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: @@ -629,10 +642,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: @@ -649,9 +662,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: @@ -666,11 +680,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: @@ -678,40 +692,43 @@ 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_ - For the tuneable track attributes listed + - min/max/target_ - 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'] = [self._get_id('artist', a) + for a in seed_artists] if seed_genres: params['seed_genres'] = seed_genres if seed_tracks: - params['seed_tracks'] = [self._get_id('track', t) for t in seed_tracks] + params['seed_tracks'] = [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') @@ -721,7 +738,7 @@ class Spotify(object): - 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: @@ -733,15 +750,15 @@ class Spotify(object): 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 From 59a0565867dc86128dbdd4c83eae9c2eccabffad Mon Sep 17 00:00:00 2001 From: Nathan Coleman Date: Tue, 31 May 2016 20:10:57 -0400 Subject: [PATCH 12/46] Add to the contributor list --- docs/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/index.rst b/docs/index.rst index 34e7187..aab633d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -302,6 +302,7 @@ Spotipy authored by Paul Lamere (plamere) with contributions by: - Steve Winton // swinton - Tim Balzer // timbalzer - corycorycory // corycorycory + - Nathan Coleman // nathancoleman License ======= From 0b9a0276c1edd3c9de478021b8b8daba4dbba4b0 Mon Sep 17 00:00:00 2001 From: eugenio412 Date: Fri, 10 Jun 2016 13:33:45 +0200 Subject: [PATCH 13/46] Update client.py added available market on search and search now is also for artist by default --- spotipy/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spotipy/client.py b/spotipy/client.py index be9162b..966fb94 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -310,7 +310,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,artist',market='US'): ''' searches for an item Parameters: @@ -320,7 +320,7 @@ class Spotify(object): - type - the type of item to return. One of 'artist', 'album', 'track' or 'playlist' ''' - 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 From 891731c78e472e3cd30cc7167c691d9876783e73 Mon Sep 17 00:00:00 2001 From: eugenio412 Date: Fri, 10 Jun 2016 13:36:14 +0200 Subject: [PATCH 14/46] Update oauth2.py refresh access token is now available also for developers, not only for the library --- spotipy/oauth2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spotipy/oauth2.py b/spotipy/oauth2.py index bb10e32..4ed635c 100644 --- a/spotipy/oauth2.py +++ b/spotipy/oauth2.py @@ -134,7 +134,7 @@ class SpotifyOAuth(object): 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 @@ -221,7 +221,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'} From c9888f16bc91dc2aeddd175b25a5237b8f31eba1 Mon Sep 17 00:00:00 2001 From: eugenio412 Date: Sat, 11 Jun 2016 13:32:15 +0200 Subject: [PATCH 15/46] Update client.py it wasnt working before, now it is --- spotipy/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spotipy/client.py b/spotipy/client.py index 966fb94..be9162b 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -310,7 +310,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,artist',market='US'): + def search(self, q, limit=10, offset=0, type='track'): ''' searches for an item Parameters: @@ -320,7 +320,7 @@ class Spotify(object): - type - the type of item to return. One of 'artist', 'album', 'track' or 'playlist' ''' - return self._get('search', q=q, limit=limit, offset=offset, type=type,market=market) + return self._get('search', q=q, limit=limit, offset=offset, type=type) def user(self, user): ''' Gets basic profile information about a Spotify User From e0daaefab6fc143a78e8b28500f08a783ccf486e Mon Sep 17 00:00:00 2001 From: Jesse Ward Date: Mon, 13 Jun 2016 01:10:45 -0400 Subject: [PATCH 16/46] Fixed show_album example to retrieve album data. This example was mistakenly fetching and displaying artist information. --- examples/show_album.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/examples/show_album.py b/examples/show_album.py index 2ec7b8f..e492b3c 100644 --- a/examples/show_album.py +++ b/examples/show_album.py @@ -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) From ff7b0f3e2a42fb7c3db050d0461d567aae5e2222 Mon Sep 17 00:00:00 2001 From: Luke Woloszyn Date: Wed, 27 Jul 2016 08:10:45 -0700 Subject: [PATCH 17/46] get new access token ~60 seconds before old one times out --- spotipy/oauth2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spotipy/oauth2.py b/spotipy/oauth2.py index bb10e32..abab767 100644 --- a/spotipy/oauth2.py +++ b/spotipy/oauth2.py @@ -78,7 +78,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): """ From 9cd153d78a03140e9b140b285637c9b16948278e Mon Sep 17 00:00:00 2001 From: Jota Sprout Date: Fri, 14 Oct 2016 18:31:58 -0400 Subject: [PATCH 18/46] Humble Spelling Correction Contributing sooner than expected. Yay. --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 559aafd..6dbb944 100644 --- a/README.md +++ b/README.md @@ -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,7 +64,7 @@ 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 From 5e3cc1f546712a93f5ce2ed4742fb06584c69627 Mon Sep 17 00:00:00 2001 From: Hugh Rawlinson Date: Sat, 15 Oct 2016 14:41:48 +0200 Subject: [PATCH 19/46] Add Audio-Analysis Endpoint Method --- spotipy/client.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/spotipy/client.py b/spotipy/client.py index c3b4b69..ff55b12 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -712,6 +712,14 @@ 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: From 1f4189b1be192f9ed0080804805154a812164d6e Mon Sep 17 00:00:00 2001 From: Bang Dao Date: Tue, 18 Oct 2016 10:14:54 +0700 Subject: [PATCH 20/46] add me_playlists method --- spotipy/client.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/spotipy/client.py b/spotipy/client.py index c3b4b69..357b68c 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -21,6 +21,7 @@ class SpotifyException(Exception): return 'http status: {0}, code:{1} - {2}'.format( self.http_status, self.code, self.msg) + class Spotify(object): ''' Example usage:: @@ -330,6 +331,14 @@ class Spotify(object): ''' return self._get('users/' + user) + def me_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 From 976b0d07e415303dabb06a55bd439c37aefdf60f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josef=20Lindman=20H=C3=B6rnlund?= Date: Tue, 18 Oct 2016 17:51:08 +0200 Subject: [PATCH 21/46] Add market parameter to tracks() call --- spotipy/client.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spotipy/client.py b/spotipy/client.py index c3b4b69..a2f1880 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -208,15 +208,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 From 1304ac33c06eb401f36fd25bda449f0f881b7bd3 Mon Sep 17 00:00:00 2001 From: steinitzu Date: Mon, 24 Oct 2016 12:19:08 -0400 Subject: [PATCH 22/46] Recommendations seeds parameters formatted as a comma seperated list to comply with Spotify API. Fixes problem of only first seed_artist/track/genre being used to generate recommendations. Mltiple params in the form of "seed_artist=xxxx&seed_artists=yyyy" results in Spotify API ignoring everything but the first parameter. --- spotipy/client.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/spotipy/client.py b/spotipy/client.py index c3b4b69..8b15d9e 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -144,7 +144,7 @@ class Spotify(object): delay += 1 else: raise - except Exception as e: + except Exception as e: raise print ('exception', str(e)) # some other exception. Requests have @@ -508,7 +508,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 @@ -649,7 +649,7 @@ 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, + return self._get('browse/categories/' + category_id + '/playlists', country=country, limit=limit, offset=offset) def recommendations(self, seed_artists=[], seed_genres=[], seed_tracks=[], @@ -661,40 +661,42 @@ 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_ - For the tuneable track attributes listed + - min/max/target_ - 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", + 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') From aae5f0ee80f0e36116b0f42e5f1f4a0aeed3f8dd Mon Sep 17 00:00:00 2001 From: Alexey V Paramonov Date: Sat, 29 Oct 2016 17:02:25 +0300 Subject: [PATCH 23/46] Added 'requests_timeout' option to tell Requests to stop waiting for a response after a given number of seconds --- spotipy/client.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/spotipy/client.py b/spotipy/client.py index 8b15d9e..66071d2 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -45,7 +45,7 @@ class Spotify(object): max_get_retries = 10 def __init__(self, auth=None, requests_session=True, - client_credentials_manager=None): + client_credentials_manager=None, requests_timeout=None): ''' Create a Spotify API object. @@ -57,11 +57,13 @@ class Spotify(object): for performance reasons (connection pooling). :param client_credentials_manager: SpotifyClientCredentials object - + :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.requests_timeout = requests_timeout if isinstance(requests_session, requests.Session): self._session = requests_session @@ -83,6 +85,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() From 1e937dd29f6f55d41188c12b733a143bf51e3282 Mon Sep 17 00:00:00 2001 From: jairogcontreras Date: Wed, 2 Nov 2016 13:51:32 -0700 Subject: [PATCH 24/46] fixed incorrect comment about purpose of script --- examples/show_my_saved_tracks.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/show_my_saved_tracks.py b/examples/show_my_saved_tracks.py index 4e74562..ac3fc42 100644 --- a/examples/show_my_saved_tracks.py +++ b/examples/show_my_saved_tracks.py @@ -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 From 7f0f7efe49b4561ab377710588b1299e929b4986 Mon Sep 17 00:00:00 2001 From: Jason Hamilton Date: Fri, 11 Nov 2016 11:28:40 -0800 Subject: [PATCH 25/46] fixes test for related artists. Fixes issue #128. --- tests/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tests.py b/tests/tests.py index c78233d..8f57e24 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -83,7 +83,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) From a97959c54e35972fa56e722811e25ecceff91348 Mon Sep 17 00:00:00 2001 From: Stephen Date: Thu, 8 Dec 2016 14:27:43 -0500 Subject: [PATCH 26/46] add follow playlist api call --- spotipy/client.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/spotipy/client.py b/spotipy/client.py index 8b15d9e..0d32172 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -475,6 +475,17 @@ class Spotify(object): return self._delete("users/%s/playlists/%s/tracks" % (user, plid), 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 me(self): ''' Get detailed profile information about the current user. An alias for the 'current_user' method. From aecd392c4a941e0445b1c044ad4035313ffac4a7 Mon Sep 17 00:00:00 2001 From: Ben Tappin Date: Thu, 24 Sep 2015 18:30:07 +0100 Subject: [PATCH 27/46] Use 'Retry-After' header if present. --- spotipy/client.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/spotipy/client.py b/spotipy/client.py index 0d32172..ba24148 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -12,10 +12,15 @@ import time ''' 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( @@ -108,10 +113,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': @@ -139,8 +145,9 @@ 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 @@ -151,8 +158,9 @@ class Spotify(object): # 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 From 6d67858e8cbca6617bb803f7ce2d979f2b619bb7 Mon Sep 17 00:00:00 2001 From: Willie Zhu Date: Thu, 15 Dec 2016 16:21:05 -0500 Subject: [PATCH 28/46] Add audio analysis API --- spotipy/client.py | 8 ++++++++ tests/authtests2.py | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/spotipy/client.py b/spotipy/client.py index 0d32172..9d64ce6 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -711,6 +711,14 @@ class Spotify(object): ''' 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: diff --git a/tests/authtests2.py b/tests/authtests2.py index f0ebcd7..670e1c8 100644 --- a/tests/authtests2.py +++ b/tests/authtests2.py @@ -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)) From 3587e107c0b576167fe2b2401630a215853e25f9 Mon Sep 17 00:00:00 2001 From: delucks Date: Tue, 20 Dec 2016 22:44:03 -0800 Subject: [PATCH 29/46] Swap use of subprocess.call for webbrowser.open in util.py --- spotipy/util.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spotipy/util.py b/spotipy/util.py index e3422f4..102c462 100644 --- a/spotipy/util.py +++ b/spotipy/util.py @@ -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) From 5ecfad1d3abb905198334e6eecd6467766868776 Mon Sep 17 00:00:00 2001 From: Nathan Coleman Date: Mon, 26 Dec 2016 11:32:28 -0600 Subject: [PATCH 30/46] Support optional collaborative kwarg --- spotipy/client.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/spotipy/client.py b/spotipy/client.py index 4b70b57..6d0a86a 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -383,7 +383,8 @@ class Spotify(object): return self._post("users/%s/playlists" % (user,), payload=data) def user_playlist_change_details( - self, user, playlist_id, name=None, public=None): + self, user, playlist_id, name=None, public=None, + collaborative=None): ''' Changes a playlist's name and/or public/private state Parameters: @@ -391,12 +392,15 @@ class Spotify(object): - 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 name is not None: data['name'] = name - if public is not None: + 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) From b8ae99a6e606568147a14006ba2a3aaba2569749 Mon Sep 17 00:00:00 2001 From: Nathan Coleman Date: Mon, 26 Dec 2016 11:32:51 -0600 Subject: [PATCH 31/46] Add example for user_playlist_change_details() --- examples/change_playlist_details.py | 38 +++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 examples/change_playlist_details.py diff --git a/examples/change_playlist_details.py b/examples/change_playlist_details.py new file mode 100644 index 0000000..ac2a7bb --- /dev/null +++ b/examples/change_playlist_details.py @@ -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 From 655e1bf7e3f730a86cbfdb5ac5bef19850614260 Mon Sep 17 00:00:00 2001 From: Nathan Coleman Date: Mon, 26 Dec 2016 11:46:45 -0600 Subject: [PATCH 32/46] Add type check for name kwarg --- spotipy/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spotipy/client.py b/spotipy/client.py index 6d0a86a..f990c80 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -395,7 +395,7 @@ class Spotify(object): - collaborative - optional is the playlist collaborative ''' data = {} - if name is not None: + if isinstance(name, basestring): data['name'] = name if isinstance(public, bool): data['public'] = public From 75883944515cc1b2943db038d9d885179a68a96e Mon Sep 17 00:00:00 2001 From: Alex Ausch Date: Tue, 27 Dec 2016 12:49:59 -0500 Subject: [PATCH 33/46] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 559aafd..05a3b8f 100644 --- a/README.md +++ b/README.md @@ -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() From 5be0ba4d94d18bf586474de350eaa9527d98251e Mon Sep 17 00:00:00 2001 From: Paul Lamere Date: Sat, 31 Dec 2016 07:49:19 -0500 Subject: [PATCH 34/46] better audio example --- examples/audio_features.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/examples/audio_features.py b/examples/audio_features.py index d956c37..da366c4 100644 --- a/examples/audio_features.py +++ b/examples/audio_features.py @@ -24,5 +24,10 @@ if len(sys.argv) > 1: start = time.time() features = sp.audio_features(tids) delta = time.time() - start - print(json.dumps(features, indent=4)) + 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,)) From 45232a24895912fc49a3b09b076f67401b82ccb2 Mon Sep 17 00:00:00 2001 From: Paul Lamere Date: Sat, 31 Dec 2016 09:33:09 -0500 Subject: [PATCH 35/46] improved example --- docs/index.rst | 39 ++++++++++++++++++++++++++++++- examples/user_public_playlists.py | 25 ++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 examples/user_public_playlists.py diff --git a/docs/index.rst b/docs/index.rst index 415b706..6c74f1f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -103,7 +103,19 @@ Register your app at `_. -*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, @@ -145,6 +157,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: diff --git a/examples/user_public_playlists.py b/examples/user_public_playlists.py new file mode 100644 index 0000000..3557fc3 --- /dev/null +++ b/examples/user_public_playlists.py @@ -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 From fdff788ff2cc06dfd02d97f3f15a1d45ec6e9c7b Mon Sep 17 00:00:00 2001 From: Paul Lamere Date: Sat, 31 Dec 2016 09:33:51 -0500 Subject: [PATCH 36/46] Improved example --- examples/audio_features.py | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/examples/audio_features.py b/examples/audio_features.py index da366c4..079f4f9 100644 --- a/examples/audio_features.py +++ b/examples/audio_features.py @@ -15,19 +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 - 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,)) +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,)) From b9af5ad7199f0a636ce9c6da652e210f4be94453 Mon Sep 17 00:00:00 2001 From: Paul Lamere Date: Sat, 31 Dec 2016 09:38:25 -0500 Subject: [PATCH 37/46] Added audio_analysis example --- examples/audio_analysis_for_track.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 examples/audio_analysis_for_track.py diff --git a/examples/audio_analysis_for_track.py b/examples/audio_analysis_for_track.py new file mode 100644 index 0000000..39e8347 --- /dev/null +++ b/examples/audio_analysis_for_track.py @@ -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,)) From d18892a99e0efebf195d40d9067df64e0d929eac Mon Sep 17 00:00:00 2001 From: Paul Lamere Date: Sat, 31 Dec 2016 09:52:03 -0500 Subject: [PATCH 38/46] Added timeout test --- examples/test_request_timeout.py | 14 ++++++++++++++ tests/tests.py | 10 ++++++++++ 2 files changed, 24 insertions(+) create mode 100644 examples/test_request_timeout.py diff --git a/examples/test_request_timeout.py b/examples/test_request_timeout.py new file mode 100644 index 0000000..167e4f1 --- /dev/null +++ b/examples/test_request_timeout.py @@ -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) diff --git a/tests/tests.py b/tests/tests.py index 8f57e24..7fc8c39 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -2,6 +2,7 @@ import spotipy import unittest import pprint +import requests from spotipy.client import SpotifyException @@ -105,6 +106,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) From b48990f02a35a1a48d3f5f4aafcf1a1713550361 Mon Sep 17 00:00:00 2001 From: Paul Lamere Date: Sat, 31 Dec 2016 10:10:22 -0500 Subject: [PATCH 39/46] renamed me_playlists t renamed me_playlists to current_user_playlists to match style of other 'me' endpoints --- spotipy/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spotipy/client.py b/spotipy/client.py index 357b68c..e36e8c8 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -331,7 +331,7 @@ class Spotify(object): ''' return self._get('users/' + user) - def me_playlists(self, limit=50, offset=0): + 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 From 76df337079fda3be6efdef332ab3b0e6018cbfd7 Mon Sep 17 00:00:00 2001 From: Paul Lamere Date: Sat, 31 Dec 2016 10:18:46 -0500 Subject: [PATCH 40/46] Added test for current_user_playlists --- tests/authtests.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/authtests.py b/tests/authtests.py index 540db5f..a0e7e3b 100644 --- a/tests/authtests.py +++ b/tests/authtests.py @@ -86,6 +86,11 @@ 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_current_user_save_and_unsave_tracks(self): tracks = spotify.current_user_saved_tracks() total = tracks['total'] From 18e5811539b4ea35667934e5c9d9476419abf98b Mon Sep 17 00:00:00 2001 From: Paul Lamere Date: Sat, 31 Dec 2016 10:19:43 -0500 Subject: [PATCH 41/46] added current_user_playlists example --- examples/my_playlists.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 examples/my_playlists.py diff --git a/examples/my_playlists.py b/examples/my_playlists.py new file mode 100644 index 0000000..58e02df --- /dev/null +++ b/examples/my_playlists.py @@ -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) From 36c830021fa28fd0498eaf659d0672f7fbc31798 Mon Sep 17 00:00:00 2001 From: Paul Lamere Date: Sat, 31 Dec 2016 10:58:01 -0500 Subject: [PATCH 42/46] added unfollow endpoint + tests --- spotipy/client.py | 12 ++++++++++++ tests/authtests.py | 13 +++++++++++++ 2 files changed, 25 insertions(+) diff --git a/spotipy/client.py b/spotipy/client.py index d54740d..1b9387d 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -507,6 +507,18 @@ class Spotify(object): ''' 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. An alias for the 'current_user' method. diff --git a/tests/authtests.py b/tests/authtests.py index a0e7e3b..13504a6 100644 --- a/tests/authtests.py +++ b/tests/authtests.py @@ -91,6 +91,19 @@ class AuthTestSpotipy(unittest.TestCase): 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'] From 325bf99a2960a94db3d701445668dbbb2892f9d1 Mon Sep 17 00:00:00 2001 From: Paul Lamere Date: Sat, 31 Dec 2016 11:47:41 -0500 Subject: [PATCH 43/46] Added market search tests --- tests/tests.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/tests.py b/tests/tests.py index 7fc8c39..e21135a 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -94,6 +94,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) From eecff1fce7ff9954ded75d27c9576496ff7f1302 Mon Sep 17 00:00:00 2001 From: Paul Lamere Date: Sat, 31 Dec 2016 17:38:33 -0500 Subject: [PATCH 44/46] Expanded 404 test --- tests/tests.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/tests/tests.py b/tests/tests.py index e21135a..760b91c 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -12,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' @@ -40,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'] @@ -69,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) @@ -144,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") From f683535165c8bb0d43db0a4bdf779200bf6236fd Mon Sep 17 00:00:00 2001 From: Paul Lamere Date: Sat, 31 Dec 2016 18:51:37 -0500 Subject: [PATCH 45/46] added current_user_saved_albums_add() --- examples/add_a_saved_album.py | 27 +++++++++++++++++++++++++++ spotipy/client.py | 10 ++++++++++ 2 files changed, 37 insertions(+) create mode 100644 examples/add_a_saved_album.py diff --git a/examples/add_a_saved_album.py b/examples/add_a_saved_album.py new file mode 100644 index 0000000..b98abcd --- /dev/null +++ b/examples/add_a_saved_album.py @@ -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) diff --git a/spotipy/client.py b/spotipy/client.py index de22432..0d13fa0 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -660,6 +660,16 @@ class Spotify(object): 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): ''' Get a list of Spotify featured playlists From a7840c8cecdc1ccca3e05bef0c9004b2679475f7 Mon Sep 17 00:00:00 2001 From: Paul Lamere Date: Sat, 31 Dec 2016 19:06:17 -0500 Subject: [PATCH 46/46] Updated version numbers --- README.md | 1 + setup.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6dbb944..9038bf6 100644 --- a/README.md +++ b/README.md @@ -71,3 +71,4 @@ If you have suggestions, bugs or other issues specific to this library, file the - 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 diff --git a/setup.py b/setup.py index efcd94a..48562fd 100644 --- a/setup.py +++ b/setup.py @@ -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",