Open peey opened 4 years ago
I went through the code, and this is supported, but actually it isn't documented. Maybe we should add this to documentation as well.
A MWE:
import spotipy
from spotipy.oauth2 import SpotifyOAuth
# let's say an object "keys" contains all the secret config keys
auth = SpotifyOAuth(client_id=keys['clientId'], client_secret=keys['clientSecret'], redirect_uri=keys['redirectUri'])
tok = auth.refresh_access_token(keys['refreshToken'])
# TODO compare refresh token of the tok above with your saved refresh token and see if you need to update in case of mismatch
spotify = spotipy.Spotify()
spotify.set_auth(tok['access_token'])
After this, you can at least query for as long as the access token is valid. I don't know if that can also be automated (by passing SpotifyOAuth
object as one of the params maybe?)
Another way this can be done (with automated refreshing) is via a custom auth manager, I'd suggest adding this to the examples as well
class CustomAuthManager:
def __init__(self, keys):
self.auth = SpotifyOAuth(client_id=keys['clientId'], client_secret=keys['clientSecret'], redirect_uri=keys['redirectUri'])
self.refresh_token = keys['refreshToken']
self.current_token = None
def get_access_token(self):
import datetime
now = datetime.datetime.now()
# if no token or token about to expire soon
if not self.current_token or self.current_token['expires_at'] > now.timestamp() + 60:
self.current_token = self.auth.refresh_access_token(self.refresh_token)
return self.current_token['access_token']
spotify = spotipy.Spotify(auth_manager=CustomAuthManager(keys))
@peey did you have issues with refresh_access_token
? I just tried your code but eventually get_access_token
fails because of the unexpected asDict
argument
I ended up getting the token using Spotify's web api tutorial and reduced your get_access_token method to just return that token. This worked for my purposes.
My guess is that the overriding of get_access_token
is not working correctly due to recent changes of adding as_dict
as an argument. if i remember correctly, it fails at the client's _auth_headers
i'm fine with refreshing manually when I need it but fyi for anyone else that sees this request and want to use it
@andyd0 @peey I tried to implement your solution but then I keep getting an error http status: 403, code:-1 - https://api.spotify.com/v1/me/tracks?limit=40&offset=0: Insufficient client scope, reason: None
. My guess is that the scope
doesn't get passed to the OAuth object
. This is the code
oauth = SpotifyOAuth(
redirect_uri="http://127.0.0.1:8000/accounts/spotify/login/callback/",
scope='user-library-read',
cache_handler = cache_handler)
class CustomAuthManager():
def __init__(self, keys, scope):
self.auth = oauth
self.refresh_token = keys["refresh_token"]
self.access_token = None
self.scope = None
def get_access_token(self):
return SocialToken.objects.get(account__provider = "spotify")
spotify_auth = Spotify(auth_manager=CustomAuthManager(keys,
scope=keys["scope"]))
Traceback (most recent call last):
File "C:\Users\Kwaku Biney\Desktop\sparison-1\project\venv\lib\site-
packages\spotipy\client.py", line 244, in _internal_call
response.raise_for_status()
File "C:\Users\Kwaku Biney\Desktop\sparison-1\project\venv\lib\site-
packages\requests\models.py", line 943, in raise_for_status
raise HTTPError(http_error_msg, response=self)
During handling of the above exception (403 Client Error: Forbidden for url:
https://api.spotify.com/v1/me/tracks?limit=40&offset=0), another exception occurred:
File "C:\Users\Kwaku Biney\Desktop\sparison-1\project\venv\lib\site-
packages\django\core\handlers\exception.py", line 47, in inner
response = get_response(request)
File "C:\Users\Kwaku Biney\Desktop\sparison-1\project\venv\lib\site-
packages\django\core\handlers\base.py", line 179, in _get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "C:\Users\Kwaku Biney\Desktop\sparison-1\project\Sparison\views.py", line 44, in
liked
liked = spotify_auth.current_user_saved_tracks(limit=40)['items']
File "C:\Users\Kwaku Biney\Desktop\sparison-1\project\venv\lib\site-
packages\spotipy\client.py", line 1225, in current_user_saved_tracks
return self._get("me/tracks", limit=limit, offset=offset, market=market)
File "C:\Users\Kwaku Biney\Desktop\sparison-1\project\venv\lib\site-
packages\spotipy\client.py", line 290, in _get
return self._internal_call("GET", url, payload, kwargs)
File "C:\Users\Kwaku Biney\Desktop\sparison-1\project\venv\lib\site-
packages\spotipy\client.py", line 260, in _internal_call
raise SpotifyException(
Exception Type: SpotifyException at /liked/
Exception Value: http status: 403, code:-1 - https://api.spotify.com/v1/me/tracks?
limit=40&offset=0:
Insufficient client scope, reason: None
@Peter-Schorn
It's impossible for me to determine the issue if you don't post the code that implements the authorization process. Why don't you just use the built in auth managers?
@peey Your CustomAuthManager
is completely pointless and should never be used. Just use SpotifyOAuth
directly instead. It already automatically refreshes the access token when necessary for you.
Sorry guys, I only used this library for a weekend project and that's when I posted the issue and the workaround. I haven't used this library since (haven't even run the code much), so I won't be able to help more here.
With the above disclaimer in place, @Peter-Schorn the most I can say is that I wanted to use an access_token
that I obtained outside of spotipy (described more in first post of the issue), and I didn't find a way to do it at that time so I developed that code.
@Peter-Schorn The thing is I already have a refresh token and access token in my database from django-allauth
. I wanted a workaround where I would just use both the refresh token and access token for API calls using Spotipy
without going through the auth
process of Spotipy.
The SpotifyOauth
defines the auth manager to be passed to a Spotify
object which then makes the API calls. But how do I use the Spotify
object if I already have these in my DB? I wanted to avoid using two access tokens, one for authentication to my site and one for API calls. Wanted to use just one for both. SpotifyOAuth
seems to take care of refreshing the token perfectly but is there a way to pass my access token instead
This is the extra code I did not write earlier
spotify_auth = Spotify(auth_manager=CustomAuthManager(keys,
scope=keys["scope"]))
spotify_auth.current_user_saved_tracks(limit=40)['items']
seems to be failing at https://github.com/plamere/spotipy/blob/b80bfa5c52aaa1db6532fc00a3cfe11e38afd567/spotipy/client.py#L217 after https://github.com/plamere/spotipy/blob/b80bfa5c52aaa1db6532fc00a3cfe11e38afd567/spotipy/client.py#L293 is called from https://github.com/plamere/spotipy/blob/b80bfa5c52aaa1db6532fc00a3cfe11e38afd567/spotipy/client.py#L1232
The thing is I already have a refresh token and access token in my database from django-allauth. I wanted a workaround where I would just use both the refresh token and access token for API calls using spotipy without going through the auth process of spotipy.
The builtin authorization managers don't require you to go through the authorization process again if you've already done so and have the token info stored somewhere.
The solution to your problem is to create a custom class that inherits from CacheHandler
. Pass an instance of this class into the initializer for any of the auth managers (SpotifyOAuth
, SpotifyPKCE
, SpotifyClientCredentials
). The cache handler is responsible for retrieving the authorization information and storing it whenever it changes. Use the CacheFileHandler
and MemoryCacheHandler
as a guide.
Sorry @kweku-45 - This was a one and done thing for me so I no longer remember how I handled it then.
@Peter-Schorn does token_info
have to be a python dictionary? can it just be a string?
I tried using this script:
token_info = {}
token_info["access_token"] = SocialToken.objects.get(account__provider = "spotify").token
token_info["refresh_token"] = SocialToken.objects.get(account__provider = "spotify").token_secret
class SparisonCacheHandler(CacheHandler):
def __init__(self,
token_info=None,
):
if token_info:
self.token_info = token_info
else:
print("You have no token information")
def get_cached_token(self):
token_info = token_info
return token_info
def save_token_to_cache(self, token_info):
return token_info
oauth = SpotifyOAuth(
redirect_uri="http://127.0.0.1:8000/accounts/spotify/login/callback/",
scope='user-library-read',
cache_handler = SparisonCacheHandler(token_info = token_info) )
What happens is:
I get redirected to http://127.0.0.1:8000/accounts/spotify/login/callback/?code=xxxxx
when I try to make an API call then the app fails because I have already logged in the user. I was guessing I did not have to authorize the app all over again because I already have the access token??
Please use proper indentation when posting code. Otherwise, it becomes very hard to read.
The token info must be a dictionary with the following keys:
access_token
refresh_token
expires_at
(a unix timestamp that represents when the access token expires)scope
(must match the instance attribute of the same name of the auth manager; or, it could be None
if you don't request access to any scopes)The reason why you're getting redirected to the authorization URL is because you're missing that last two parameters, so the token info is considered invalid. How is spotipy supposed to know when to refresh the access token if it doesn't know when it expires?
Furthermore, there are major issues with your cache handler. First of all, the first line of get_cached_token
doesn't make sense:
token_info = token_info
token_info
is not defined in this scope.
Furthermore, in SparisonCacheHandler.__init__
, self.token_info
is not defined if token_info
is None
. Instead the initializer should look like this:
def __init__(self, token_info=None):
self.token_info = token_info
You also need to read the docstring for CacheHandler.get_cached_token
and CacheHandler.save_token_to_cache
. The latter should return None
; Your implementation returns the token info.
Furthermore, you need to come up with one centralized place where the token info will be stored. It doesn't make sense to store it in both the cache handler and in the SocialToken
object, whatever that is. Pick one or the other. For example if you want to store the token info in the SocialToken
object, then you could do something like this:
class SparisonCacheHandler(CacheHandler):
def __init__(self):
self.spotify_object = SocialToken.objects.get(account__provider="spotify")
def get_cached_token(self):
# retrieve the token info from the `SocialToken` object
token_info = {}
token_info["access_token"] = self.spotify_object.token
token_info["refresh_token"] = self.spotify_object.token_secret
token_info["expires_at"] = self.spotify_object.expires_at
token_info["scope"] = self.spotify_object.scope
return token_info
def save_token_to_cache(self, token_info):
# save the token info back to the `SocialToken` object
# notice that we're saving the token info back to the same place that we retrieved it from
# in `get_cached_token`; this is crucial
self.spotify_object.token = token_info["access_token"]
self.spotify_object.token_secret = token_info["refresh_token"]
self.spotify_object.expires_at = token_info["expires_at"]
self.spotify_object.scope = token_info["scope"]
@Peter-Schorn Thank you for your correction. I ended up implementing like this.
def convert_to_unix(DateTimeField):
return DateTimeField.timestamp()
#Implement authentication using Cache Handler
expires_at = SocialToken.objects.get(account__provider = "spotify").expires_at
expires_at_in_unix = convert_to_unix(expires_at)
class SparisonCacheHandler(CacheHandler):
def __init__(self):
self.spotify_object = SocialToken.objects.get(account__provider="spotify")
def get_cached_token(self):
token_info = {}
token_info["access_token"] = self.spotify_object.token
token_info["refresh_token"] = self.spotify_object.token_secret
token_info["expires_at"] = convert_to_unix(self.spotify_object.expires_at)
token_info["scope"] = "user-library-read"
return token_info
def save_token_to_cache(self, token_info):
self.spotify_object.token = token_info["access_token"]
self.spotify_object.token_secret = token_info["refresh_token"]
oauth = SpotifyOAuth(
redirect_uri="http://127.0.0.1:8000/accounts/spotify/login/callback/",
cache_handler = SparisonCacheHandler() ,
scope="user-library-read")
Spotify(auth_manager=oauth).current_user_saved_tracks(limit=40)['items']
I do still get the same error from the beginning: http status: 403, code:-1 - https://api.spotify.com/v1/me/tracks?limit=40&offset=0: Insufficient client scope, reason: None
Other calls like Spotify.me()
works though which then means the scope doesn't get passed to the SpotifyAuth
object for some reason.
Insufficient client scope
This means that you didn't request the correct scope(s) when going through the authorization process. Show me the code where you implement the authorization process. It's impossible for me to help you if you don't do this.
Another way you can determine if the correct scopes are being requested is to look at the URL of the page that asks you to sign in with your Spotify account. You should see "user-library-read" in the query string of the URL.
Other calls like Spotify.me() works though
These endpoints work because they don't require any scopes.
which then means the scope doesn't get passed to the SpotifyOAuth object for some reason.
No, it doesn't mean that because you're not using SpotifyOAuth
to implement the authorization process (from what I understand). It means the scope is not getting passed to whatever object in django-allauth
that you are using to implement the authorization process.
Also, there are still issues with your cache handler:
def save_token_to_cache(self, token_info):
self.spotify_object.token = token_info["access_token"]
self.spotify_object.token_secret = token_info["refresh_token"]
Why are you ignoring the expires_at
and scope
parameters? This won't work. You also should not be hardcoding the scope in SparisonCacheHandler.get_cached_token
. This should be stored in your spotify_object
.
I have set up a spotipy client using allauth token, see here https://github.com/sapristi/lalipo/blob/master/lalipo/spotipy_cache.py for how to implement the cache and override the client, and https://github.com/sapristi/lalipo/blob/master/lalipo/spotify_helpers.py#L9 for how to use it.
Note that allauth should be configured to store the token in db.
Is it possible for me to set
access_token
andrefresh_token
to spotipy (or even justaccess_token
) without spotipy having to worry about the authentication flow?I have taken care of the authentication flow in a different part of the app but still would like to be able to use spotipy as an API wrapper