-
Notifications
You must be signed in to change notification settings - Fork 0
/
web_api.py
205 lines (161 loc) · 7.8 KB
/
web_api.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# Handles the authentication and communication with the Spotify Web API.
import logging
import os
import shelve
import sys
import uuid
import requests
import time
import webbrowser
import json
from notif_handler import send_notif
info_file = os.path.join(os.path.dirname(__file__), '.info')
class WebApi:
# This follows the 'Authorization Code Flow' path set out by
# https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow.
def __init__(self, scope_list, client_id, redirect_uri):
self.api_url = 'https://api.spotify.com/v1/'
self.authorize_access_url = 'https://accounts.spotify.com/authorize/'
self.register_user_url = 'https://platelminto.eu.pythonanywhere.com/users/complete'
self.refresh_token_url = 'https://platelminto.eu.pythonanywhere.com/users/refresh'
self.scope_list = scope_list
self.client_id = client_id
self.redirect_uri = redirect_uri
# Load authentication tokens, and if they do checks
# whether they need to be refreshed.
self.load_auth_values()
self.check_for_refresh_token(self.expiry_time)
# Called when registering as a new user
def get_auth_info(self):
# If we are new, re-do entire auth process.
with shelve.open(info_file) as shelf:
shelf.clear()
new_uuid = uuid.uuid4()
shelf['uuid'] = new_uuid
self.uuid = shelf['uuid']
current_time = time.time()
self.generate_auth_code()
info = self.get_access_info()
self.save_auth_values(info.get('access_token'), info.get('refresh_token'),
current_time + info.get('expires_in'))
# The authorization keys need to be refreshed every once in a while
def check_for_refresh_token(self, expiry_time):
if time.time() > expiry_time:
self.refresh_tokens()
# This is how the user can authenticate themselves and must follow the instructions
# in the README.
def generate_auth_code(self):
params = {'client_id': self.client_id, 'response_type': 'code', 'state': self.uuid,
'redirect_uri': self.redirect_uri,
'scope': ' '.join(self.scope_list)}
r = requests.get(self.authorize_access_url, params=params)
webbrowser.open_new(r.url)
def get_access_info(self):
timeout = time.time()
while time.time() < timeout + 120: # We spend 2 minutes waiting for auth confirmation
response = requests.post(self.register_user_url,
json={'uuid': str(self.uuid)})
if response.status_code == 200:
logging.info('initial authentication done.')
send_notif('Success', 'You are now authenticated.')
return response.json()
time.sleep(3)
send_notif('Could not authenticate', 'Spotify Helper closed.')
sys.exit('Could not authenticate')
# Gets the new tokens after previous ones expire, following the format described by
# the Spotify authorization guide.
def refresh_tokens(self):
payload = {'grant_type': 'refresh_token', 'refresh_token': self.refresh_token,
'uuid': str(self.uuid)}
obtained_time = time.time()
try:
r = requests.post(self.refresh_token_url, json=payload, timeout=4)
except (requests.exceptions.ConnectionError, requests.exceptions.ReadTimeout):
raise ConnectionError
# 403 indicates the user UUID is not registered on the server,
if r.status_code == 403:
send_notif('Authentication error', 'Please re-authenticate, or try restarting the app.')
self.get_auth_info()
logging.warning('Authentication refresh failed, info: {}'.format(r.content))
return # Values get saved in get_auth_info()
# If we need additional permissions and have added them to the scope, the
# old keys will not work, and we need new authentication info.
# 400-499 means there is a general user error, so we start anew with
# the authentication process.
if (400 <= r.status_code < 500) or \
(not set(r.json().get('scope').split(' ')).issuperset(set(self.scope_list))):
self.get_auth_info()
return
info = r.json()
if 'refresh_token' in info:
self.refresh_token = info.get('refresh_token')
self.save_auth_values(info.get('access_token'), self.refresh_token,
obtained_time + info.get('expires_in'))
self.load_auth_values()
def save_auth_values(self, access_token, refresh_token, expiry_time):
with shelve.open(info_file) as shelf:
shelf['access_token'] = access_token
shelf['refresh_token'] = refresh_token
shelf['expiry_time'] = expiry_time
self.load_auth_values()
def load_auth_values(self):
try:
with shelve.open(info_file) as shelf:
self.uuid = shelf['uuid']
self.access_token = shelf['access_token']
self.refresh_token = shelf['refresh_token']
self.expiry_time = shelf['expiry_time']
except KeyError:
self.get_auth_info()
# The authorization values need to be in a specified header.
def get_access_header(self):
self.check_for_refresh_token(self.expiry_time)
return {'Authorization': 'Bearer ' + self.access_token}
# The following functions are wrappers around requests' basic rest functions.
def get(self, endpoint, params=None, timeout=4, retry=1):
try:
return requests.get(self.api_url + endpoint,
params=params, headers=self.get_access_header(),
timeout=timeout)
except requests.exceptions.ConnectionError:
if retry != 0:
return self.get(endpoint, params, timeout, retry - 1)
except requests.exceptions.ReadTimeout:
pass
raise ConnectionError
def post(self, endpoint, params=None, payload=None, timeout=4, retry=1):
try:
return requests.post(self.api_url + endpoint,
data=json.dumps(payload), params=params,
headers=self.get_access_header(),
timeout=timeout)
except requests.exceptions.ConnectionError:
if retry != 0:
return self.post(endpoint, params, payload, timeout, retry - 1)
except requests.exceptions.ReadTimeout:
pass
raise ConnectionError
def put(self, endpoint, params=None, payload=None, timeout=4, retry=1):
try:
return requests.put(self.api_url + endpoint,
data=json.dumps(payload), params=params,
headers=self.get_access_header(),
timeout=timeout)
except requests.exceptions.ConnectionError:
if retry != 0:
return self.put(endpoint, params, payload, timeout, retry - 1)
except requests.exceptions.ReadTimeout:
pass
raise ConnectionError
def delete(self, endpoint, params=None, payload=None, timeout=4, retry=1):
try:
return requests.delete(self.api_url + endpoint,
data=json.dumps(payload), params=params,
headers=self.get_access_header(),
timeout=timeout)
except requests.exceptions.ConnectionError:
if retry != 0:
return self.delete(endpoint, params, payload, timeout, retry - 1)
except requests.exceptions.ReadTimeout:
pass
raise ConnectionError