2015-06-05 09:07:28 +00:00
|
|
|
|
2015-06-05 09:14:13 +00:00
|
|
|
from __future__ import print_function
|
2014-05-16 15:32:55 +00:00
|
|
|
import base64
|
|
|
|
import requests
|
2014-05-20 12:30:48 +00:00
|
|
|
import os
|
2014-06-19 18:41:52 +00:00
|
|
|
import json
|
2014-05-20 12:30:48 +00:00
|
|
|
import time
|
2014-08-21 11:35:01 +00:00
|
|
|
import sys
|
2014-05-16 15:32:55 +00:00
|
|
|
|
2015-06-05 09:16:30 +00:00
|
|
|
# Workaround to support both python 2 & 3
|
|
|
|
try:
|
|
|
|
import urllib.request, urllib.error
|
|
|
|
import urllib.parse as urllibparse
|
|
|
|
except ImportError:
|
|
|
|
import urllib as urllibparse
|
|
|
|
|
|
|
|
|
2015-04-01 14:03:29 +00:00
|
|
|
|
2014-05-16 15:32:55 +00:00
|
|
|
class SpotifyOauthError(Exception):
|
|
|
|
pass
|
|
|
|
|
2015-04-01 14:03:29 +00:00
|
|
|
|
|
|
|
class SpotifyClientCredentials(object):
|
|
|
|
OAUTH_TOKEN_URL = 'https://accounts.spotify.com/api/token'
|
|
|
|
|
|
|
|
def __init__(self, client_id=None, client_secret=None):
|
|
|
|
"""
|
|
|
|
You can either provid a client_id and client_secret to the
|
|
|
|
constructor or set SPOTIPY_CLIENT_ID and SPOTIPY_CLIENT_SECRET
|
|
|
|
environment variables
|
|
|
|
"""
|
|
|
|
if not client_id:
|
|
|
|
client_id = os.getenv('SPOTIPY_CLIENT_ID')
|
|
|
|
|
|
|
|
if not client_secret:
|
|
|
|
client_secret = os.getenv('SPOTIPY_CLIENT_SECRET')
|
|
|
|
|
|
|
|
if not client_id:
|
|
|
|
raise SpotifyOauthError('No client id')
|
|
|
|
|
|
|
|
if not client_secret:
|
|
|
|
raise SpotifyOauthError('No client secret')
|
|
|
|
|
|
|
|
self.client_id = client_id
|
|
|
|
self.client_secret = client_secret
|
|
|
|
self.token_info = None
|
|
|
|
|
|
|
|
def get_access_token(self):
|
|
|
|
"""
|
|
|
|
If a valid access token is in memory, returns it
|
|
|
|
Else feches a new token and returns it
|
|
|
|
"""
|
|
|
|
if self.token_info and not self._is_token_expired(self.token_info):
|
|
|
|
return self.token_info['access_token']
|
|
|
|
|
|
|
|
token_info = self._request_access_token()
|
|
|
|
token_info = self._add_custom_values_to_token_info(token_info)
|
|
|
|
self.token_info = token_info
|
|
|
|
return self.token_info['access_token']
|
|
|
|
|
|
|
|
def _request_access_token(self):
|
|
|
|
"""Gets client credentials access token """
|
|
|
|
payload = { 'grant_type': 'client_credentials'}
|
|
|
|
|
2015-06-05 10:12:36 +00:00
|
|
|
if sys.version_info[0] >= 3: # Python 3
|
|
|
|
auth_header = base64.b64encode(str(self.client_id + ':' + self.client_secret).encode())
|
|
|
|
headers = {'Authorization': 'Basic %s' % auth_header.decode()}
|
|
|
|
else: # Python 2
|
|
|
|
auth_header = base64.b64encode(self.client_id + ':' + self.client_secret)
|
|
|
|
headers = {'Authorization': 'Basic %s' % auth_header}
|
2015-04-01 14:03:29 +00:00
|
|
|
|
|
|
|
response = requests.post(self.OAUTH_TOKEN_URL, data=payload,
|
|
|
|
headers=headers, verify=True)
|
|
|
|
if response.status_code is not 200:
|
|
|
|
raise SpotifyOauthError(response.reason)
|
|
|
|
token_info = response.json()
|
|
|
|
return token_info
|
|
|
|
|
|
|
|
def _is_token_expired(self, token_info):
|
|
|
|
now = int(time.time())
|
|
|
|
return token_info['expires_at'] < now
|
|
|
|
|
|
|
|
def _add_custom_values_to_token_info(self, token_info):
|
|
|
|
"""
|
|
|
|
Store some values that aren't directly provided by a Web API
|
|
|
|
response.
|
|
|
|
"""
|
|
|
|
token_info['expires_at'] = int(time.time()) + token_info['expires_in']
|
|
|
|
return token_info
|
|
|
|
|
|
|
|
|
2014-05-16 15:32:55 +00:00
|
|
|
class SpotifyOAuth(object):
|
|
|
|
'''
|
|
|
|
Implements Authorization Code Flow for Spotify's OAuth implementation.
|
|
|
|
'''
|
|
|
|
|
|
|
|
OAUTH_AUTHORIZE_URL = 'https://accounts.spotify.com/authorize'
|
|
|
|
OAUTH_TOKEN_URL = 'https://accounts.spotify.com/api/token'
|
2014-05-20 12:30:48 +00:00
|
|
|
|
2015-04-01 14:03:29 +00:00
|
|
|
def __init__(self, client_id, client_secret, redirect_uri,
|
2014-08-20 18:04:29 +00:00
|
|
|
state=None, scope=None, cache_path=None):
|
2014-08-22 13:23:06 +00:00
|
|
|
'''
|
|
|
|
Creates a SpotifyOAuth object
|
|
|
|
|
|
|
|
Parameters:
|
|
|
|
- client_id - the client id of your app
|
|
|
|
- client_secret - the client secret of your app
|
|
|
|
- redirect_uri - the redirect URI of your app
|
|
|
|
- state - security state
|
|
|
|
- scope - the desired scope of the request
|
|
|
|
- cache_path - path to location to save tokens
|
|
|
|
'''
|
2014-08-22 15:48:12 +00:00
|
|
|
|
2014-05-16 15:32:55 +00:00
|
|
|
self.client_id = client_id
|
|
|
|
self.client_secret = client_secret
|
|
|
|
self.redirect_uri = redirect_uri
|
|
|
|
self.state=state
|
2014-05-20 12:30:48 +00:00
|
|
|
self.cache_path = cache_path
|
2014-08-22 15:00:29 +00:00
|
|
|
self.scope=self._normalize_scope(scope)
|
2015-04-01 14:03:29 +00:00
|
|
|
|
2014-05-20 12:30:48 +00:00
|
|
|
def get_cached_token(self):
|
2014-08-22 15:48:12 +00:00
|
|
|
''' Gets a cached auth token
|
|
|
|
'''
|
2014-05-20 12:30:48 +00:00
|
|
|
token_info = None
|
|
|
|
if self.cache_path:
|
|
|
|
try:
|
|
|
|
f = open(self.cache_path)
|
|
|
|
token_info_string = f.read()
|
|
|
|
f.close()
|
|
|
|
token_info = json.loads(token_info_string)
|
2014-07-07 15:03:27 +00:00
|
|
|
|
|
|
|
# if scopes don't match, then bail
|
|
|
|
if 'scope' not in token_info or self.scope != token_info['scope']:
|
|
|
|
return None
|
|
|
|
|
2014-08-22 15:00:29 +00:00
|
|
|
if self._is_token_expired(token_info):
|
|
|
|
token_info = self._refresh_access_token(token_info['refresh_token'])
|
2014-07-07 15:03:27 +00:00
|
|
|
|
2014-05-20 12:30:48 +00:00
|
|
|
except IOError:
|
|
|
|
pass
|
|
|
|
return token_info
|
|
|
|
|
2014-08-22 15:00:29 +00:00
|
|
|
def _save_token_info(self, token_info):
|
2014-05-20 12:30:48 +00:00
|
|
|
if self.cache_path:
|
2014-08-22 13:23:06 +00:00
|
|
|
try:
|
|
|
|
f = open(self.cache_path, 'w')
|
|
|
|
f.write(json.dumps(token_info))
|
|
|
|
f.close()
|
|
|
|
except IOError:
|
|
|
|
self._warn("couldn't write token cache to " + self.cache_path)
|
|
|
|
pass
|
|
|
|
|
2014-05-16 15:32:55 +00:00
|
|
|
|
2014-08-22 15:00:29 +00:00
|
|
|
def _is_token_expired(self, token_info):
|
2014-05-20 12:30:48 +00:00
|
|
|
now = int(time.time())
|
|
|
|
return token_info['expires_at'] < now
|
2015-04-01 14:03:29 +00:00
|
|
|
|
2014-05-16 15:32:55 +00:00
|
|
|
def get_authorize_url(self):
|
2014-08-22 15:48:12 +00:00
|
|
|
""" Gets the URL to use to authorize this app
|
|
|
|
"""
|
2014-05-16 15:32:55 +00:00
|
|
|
payload = {'client_id': self.client_id,
|
|
|
|
'response_type': 'code',
|
|
|
|
'redirect_uri': self.redirect_uri}
|
|
|
|
if self.scope:
|
|
|
|
payload['scope'] = self.scope
|
|
|
|
if self.state:
|
|
|
|
payload['state'] = self.state
|
|
|
|
|
2015-06-05 09:16:30 +00:00
|
|
|
urlparams = urllibparse.urlencode(payload)
|
2014-05-16 15:32:55 +00:00
|
|
|
|
|
|
|
return "%s?%s" % (self.OAUTH_AUTHORIZE_URL, urlparams)
|
|
|
|
|
2014-08-22 15:48:12 +00:00
|
|
|
def parse_response_code(self, url):
|
|
|
|
""" Parse the response code in the given response url
|
|
|
|
|
|
|
|
Parameters:
|
|
|
|
- url - the response url
|
|
|
|
"""
|
|
|
|
|
2014-05-23 11:19:16 +00:00
|
|
|
try:
|
2014-08-22 15:48:12 +00:00
|
|
|
return url.split("?code=")[1].split("&")[0]
|
2014-05-23 11:19:16 +00:00
|
|
|
except IndexError:
|
|
|
|
return None
|
2014-05-16 15:32:55 +00:00
|
|
|
|
|
|
|
def get_access_token(self, code):
|
2014-08-22 15:48:12 +00:00
|
|
|
""" Gets the access token for the app given the code
|
|
|
|
|
|
|
|
Parameters:
|
|
|
|
- code - the response code
|
|
|
|
"""
|
|
|
|
|
2014-05-16 15:32:55 +00:00
|
|
|
payload = {'redirect_uri': self.redirect_uri,
|
|
|
|
'code': code,
|
|
|
|
'grant_type': 'authorization_code'}
|
|
|
|
if self.scope:
|
|
|
|
payload['scope'] = self.scope
|
|
|
|
if self.state:
|
|
|
|
payload['state'] = self.state
|
|
|
|
|
2015-06-05 09:11:06 +00:00
|
|
|
auth_header = base64.b64encode(str(self.client_id + ':' + self.client_secret).encode())
|
|
|
|
headers = {'Authorization': 'Basic %s' % auth_header.decode()}
|
2014-05-16 15:32:55 +00:00
|
|
|
|
|
|
|
|
2015-04-01 14:03:29 +00:00
|
|
|
response = requests.post(self.OAUTH_TOKEN_URL, data=payload,
|
2014-08-20 18:04:29 +00:00
|
|
|
headers=headers, verify=True)
|
2014-05-16 15:32:55 +00:00
|
|
|
if response.status_code is not 200:
|
|
|
|
raise SpotifyOauthError(response.reason)
|
2014-05-20 12:30:48 +00:00
|
|
|
token_info = response.json()
|
2014-08-04 14:42:15 +00:00
|
|
|
token_info = self._add_custom_values_to_token_info(token_info)
|
2014-08-22 15:00:29 +00:00
|
|
|
self._save_token_info(token_info)
|
2014-05-20 12:30:48 +00:00
|
|
|
return token_info
|
|
|
|
|
2014-08-22 15:00:29 +00:00
|
|
|
def _normalize_scope(self, scope):
|
2014-07-07 15:03:27 +00:00
|
|
|
if scope:
|
|
|
|
scopes = scope.split()
|
|
|
|
scopes.sort()
|
|
|
|
return ' '.join(scopes)
|
|
|
|
else:
|
|
|
|
return None
|
|
|
|
|
2014-08-22 15:00:29 +00:00
|
|
|
def _refresh_access_token(self, refresh_token):
|
2014-05-20 12:30:48 +00:00
|
|
|
payload = { 'refresh_token': refresh_token,
|
|
|
|
'grant_type': 'refresh_token'}
|
|
|
|
|
2014-12-22 20:20:47 +00:00
|
|
|
auth_header = base64.b64encode(bytes(self.client_id + ':' + self.client_secret, encoding='utf-8'))
|
|
|
|
headers = {'Authorization': 'Basic %s' % auth_header.decode('ascii')}
|
2014-08-21 11:35:01 +00:00
|
|
|
|
2015-04-01 14:03:29 +00:00
|
|
|
response = requests.post(self.OAUTH_TOKEN_URL, data=payload,
|
2014-08-21 11:35:01 +00:00
|
|
|
headers=headers)
|
|
|
|
if response.status_code != 200:
|
|
|
|
if False: # debugging code
|
|
|
|
print('headers', headers)
|
|
|
|
print('request', response.url)
|
|
|
|
self._warn("couldn't refresh token: code:%d reason:%s" \
|
|
|
|
% (response.status_code, response.reason))
|
|
|
|
return None
|
2014-05-20 12:30:48 +00:00
|
|
|
token_info = response.json()
|
2014-08-04 14:42:15 +00:00
|
|
|
token_info = self._add_custom_values_to_token_info(token_info)
|
2014-05-23 11:19:16 +00:00
|
|
|
if not 'refresh_token' in token_info:
|
|
|
|
token_info['refresh_token'] = refresh_token
|
2014-08-22 15:00:29 +00:00
|
|
|
self._save_token_info(token_info)
|
2014-05-20 12:30:48 +00:00
|
|
|
return token_info
|
|
|
|
|
2014-08-04 14:42:15 +00:00
|
|
|
def _add_custom_values_to_token_info(self, token_info):
|
2015-04-01 14:03:29 +00:00
|
|
|
'''
|
2014-08-04 14:42:15 +00:00
|
|
|
Store some values that aren't directly provided by a Web API
|
|
|
|
response.
|
|
|
|
'''
|
|
|
|
token_info['expires_at'] = int(time.time()) + token_info['expires_in']
|
|
|
|
token_info['scope'] = self.scope
|
|
|
|
return token_info
|
|
|
|
|
2014-08-21 11:35:01 +00:00
|
|
|
def _warn(self, msg):
|
|
|
|
print('warning:' + msg, file=sys.stderr)
|
|
|
|
|