-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathuser.py
141 lines (118 loc) · 6.32 KB
/
user.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
import logging
import time
from datetime import datetime, timezone
import requests
import cfg
import models
from playlist import Playlist
class AuthorizationException(Exception):
pass
class User:
spotify_id: str
display_name: str
access_token: str
refresh_token: str
expires_at: float
app_token: str
def __init__(self, spotify_id: str, token: str) -> None:
search = cfg.db.execute(f"SELECT * FROM users where spotify_id = '{spotify_id}' and app_password = '{token}'")
user = search.fetchone()
# fetchone() returns None if there are no more results to grab, so we can check user for None to see if the
# login info was correct
if not user:
logging.info(f"login failed with '{token}' as {spotify_id}")
raise AuthorizationException(f"Could not find user '{spotify_id}' with token '{token}' in database")
logging.info(f"login succeeded with '{token}' as {spotify_id}")
self.spotify_id = user["spotify_id"]
self.display_name = user["display_name"]
self.access_token = user["access_token"]
self.refresh_token = user["refresh_token"]
self.expires_at = user["expires_at"]
self.app_token = user["app_password"]
def refresh(self):
logging.info(f"refreshing token for {self.refresh_token}")
headers = {'Authorization': cfg.auth_header, 'Content-Type': 'application/x-www-form-urlencoded'}
body = {'grant_type': 'refresh_token', 'refresh_token': self.refresh_token}
response = requests.post(f'{cfg.auth_url}/api/token', headers=headers, data=body)
logging.info(f"got {response.status_code} from POST {response.url}")
response.raise_for_status()
response = response.json()
self.access_token = response['access_token']
self.expires_at = response['expires_in'] + time.time()
cfg.db.execute(
f"""
UPDATE users SET
access_token = '{self.access_token}',
expires_at = '{self.expires_at}'
WHERE spotify_id = '{self.spotify_id}'
"""
)
cfg.db.commit()
def get_playlists(self) -> list[models.Playlist]:
query = cfg.db.execute(f"SELECT * FROM playlists WHERE owner = '{self.spotify_id}'")
return [Playlist(**i) for i in query.fetchall()]
def call_api(self, method: str, endpoint: str, params: dict = None, body: dict | bytes = None, raw_url: bool = False) -> dict:
"""
Uses the `requests` library to send requests to Spotify. Refreshes user token if needed. Should not be called
directly - use the below wrappers (get, delete, etc.) instead.
:param method: Type of request (get, delete, etc.) to preform
:param endpoint: The path to use for the request
:param params: Any parameters to pass to Spotify with the request
:param body: Data to send as the body of the request
:param raw_url: If false, `endpoint` will be appended to the api url. If true, `endpoint` will be used directly.
:return: The JSON response from Spotify, deserialized to a dict
"""
# If the access token has expired, refresh the token.
if self.expires_at <= datetime.now(timezone.utc).timestamp():
self.refresh()
headers = {'Authorization': f'Bearer {self.access_token}', 'Content-Type': 'application/json'}
response = requests.request(
method,
f'{cfg.api_url}/v1{endpoint}' if not raw_url else endpoint,
headers=headers,
params=params,
json=body
)
logging.info(f"got {response.status_code} from {method} {response.url} {' with body ' + str(response.request.body) if body else ''}")
response.raise_for_status()
return response.json()
def get(self, endpoint: str, params: dict = None, body: dict | bytes = None, raw_url: bool = False) -> dict:
"""
Send a GET request to the Spotify API
:param endpoint: The path to use for the request
:param params: Any parameters to pass to Spotify with the request
:param body: Data to send as the body of the request
:param raw_url: If false, `endpoint` will be appended to the api url. If true, `endpoint` will be used directly.
:return: The JSON response from Spotify, deserialized to a dict
"""
return self.call_api("GET", endpoint, params, body, raw_url)
def delete(self, endpoint: str, params: dict = None, body: dict | bytes = None, raw_url: bool = False) -> dict:
"""
Send a DELETE request to the Spotify API
:param endpoint: The path to use for the request
:param params: Any parameters to pass to Spotify with the request
:param body: Data to send as the body of the request
:param raw_url: If false, `endpoint` will be appended to the api url. If true, `endpoint` will be used directly.
:return: The JSON response from Spotify, deserialized to a dict
"""
return self.call_api("DELETE", endpoint, params, body, raw_url)
def post(self, endpoint: str, params: dict = None, body: dict | bytes = None, raw_url: bool = False) -> dict:
"""
Send a POST request to the Spotify API
:param endpoint: The path to use for the request
:param params: Any parameters to pass to Spotify with the request
:param body: Data to send as the body of the request
:param raw_url: If false, `endpoint` will be appended to the api url. If true, `endpoint` will be used directly.
:return: The JSON response from Spotify, deserialized to a dict
"""
return self.call_api("POST", endpoint, params, body, raw_url)
def put(self, endpoint: str, params: dict = None, body: dict | bytes = None, raw_url: bool = False) -> dict:
"""
Send a PUT request to the Spotify API
:param endpoint: The path to use for the request
:param params: Any parameters to pass to Spotify with the request
:param body: Data to send as the body of the request
:param raw_url: If false, `endpoint` will be appended to the api url. If true, `endpoint` will be used directly.
:return: The JSON response from Spotify, deserialized to a dict
"""
return self.call_api("PUT", endpoint, params, body, raw_url)