Skip to content

Commit c81eb96

Browse files
committed
Wrap Spotify API in helper class to handle token refreshes
1 parent 3f61a54 commit c81eb96

1 file changed

Lines changed: 82 additions & 78 deletions

File tree

plugins/spotify.py

Lines changed: 82 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,105 @@
11
import re
2-
from datetime import datetime
2+
from datetime import datetime, timedelta
3+
from threading import RLock
34

45
import requests
56
from requests import HTTPError
67
from requests.auth import HTTPBasicAuth
8+
from yarl import URL
79

810
from cloudbot import hook
911

10-
api_url = "https://api.spotify.com/v1/search?"
11-
token_url = "https://accounts.spotify.com/api/token"
12-
spuri = 'spotify:{}:{}'
13-
access_token = ""
14-
expires_at = datetime.min
15-
16-
spotify_re = re.compile(r'(spotify:(track|album|artist|user):([a-zA-Z0-9]+))',
17-
re.I)
18-
http_re = re.compile(r'(open\.spotify\.com/(track|album|artist|user)/'
19-
'([a-zA-Z0-9]+))', re.I)
20-
21-
22-
def sprequest(bot, params, alturl=None):
23-
global access_token, expires_at
24-
if alturl is None:
25-
alturl = api_url
26-
if datetime.now() >= expires_at:
27-
basic_auth = HTTPBasicAuth(
28-
bot.config.get("api_keys", {}).get("spotify_client_id"),
29-
bot.config.get("api_keys", {}).get("spotify_client_secret"))
30-
gtcc = {"grant_type": "client_credentials"}
31-
auth = requests.post(token_url, data=gtcc, auth=basic_auth).json()
32-
if 'access_token' in auth.keys():
33-
access_token = auth["access_token"]
34-
expires_at = datetime.fromtimestamp(datetime.now().timestamp() +
35-
auth["expires_in"])
36-
headers = {'Authorization': 'Bearer ' + access_token}
37-
return requests.get(alturl, params=params, headers=headers)
12+
api = None
3813

14+
spotify_re = re.compile(
15+
r'(spotify:(track|album|artist|user):([a-zA-Z0-9]+))', re.I
16+
)
17+
http_re = re.compile(
18+
r'(open\.spotify\.com/(track|album|artist|user)/([a-zA-Z0-9]+))', re.I
19+
)
3920

40-
@hook.command('spotify', 'sptrack')
41-
def spotify(bot, text, reply):
42-
"""<song> - Search Spotify for <song>"""
43-
params = {"q": text.strip(), "offset": 0, "limit": 1, "type": "track"}
21+
TYPE_MAP = {
22+
'artist': 'artists',
23+
'album': 'albums',
24+
'track': 'tracks',
25+
}
26+
27+
28+
class SpotifyAPI:
29+
api_url = URL("https://api.spotify.com/v1")
30+
token_refresh_url = URL("https://accounts.spotify.com/api/token")
31+
32+
def __init__(self, client_id=None, client_secret=None):
33+
self._client_id = client_id
34+
self._client_secret = client_secret
35+
self._access_token = None
36+
self._token_expires = datetime.min
37+
self._lock = RLock() # Make sure only one requests is parsed at a time
38+
39+
def request(self, endpoint, params=None):
40+
with self._lock:
41+
if datetime.now() >= self._token_expires:
42+
self._refresh_token()
43+
44+
r = requests.get(
45+
self.api_url / endpoint, params=params, headers={'Authorization': 'Bearer ' + self._access_token}
46+
)
47+
r.raise_for_status()
48+
49+
return r
50+
51+
def search(self, params):
52+
return self.request('search', params)
4453

45-
request = sprequest(bot, params)
54+
def _refresh_token(self):
55+
with self._lock:
56+
basic_auth = HTTPBasicAuth(self._client_id, self._client_secret)
57+
gtcc = {"grant_type": "client_credentials"}
58+
r = requests.post(self.token_refresh_url, data=gtcc, auth=basic_auth)
59+
r.raise_for_status()
60+
auth = r.json()
61+
self._access_token = auth["access_token"]
62+
self._token_expires = datetime.now() + timedelta(seconds=auth["expires_in"])
63+
64+
65+
def _search(text, _type, reply):
66+
params = {"q": text.strip(), "offset": 0, "limit": 1, "type": _type}
4667

4768
try:
48-
request.raise_for_status()
69+
request = api.search(params)
4970
except HTTPError as e:
5071
reply("Could not get track information: {}".format(e.request.status_code))
5172
raise
5273

53-
if request.status_code != requests.codes.ok:
54-
return "Could not get track information: {}".format(
55-
request.status_code)
74+
return request.json()[TYPE_MAP[_type]]["items"][0]
75+
76+
77+
@hook.onload
78+
def create_api(bot):
79+
keys = bot.config['api_keys']
80+
client_id = keys['spotify_client_id']
81+
client_secret = keys['spotify_client_secret']
82+
global api
83+
api = SpotifyAPI(client_id, client_secret)
5684

57-
data = request.json()["tracks"]["items"][0]
85+
86+
@hook.command('spotify', 'sptrack')
87+
def spotify(text, reply):
88+
"""<song> - Search Spotify for <song>"""
89+
data = _search(text, "track", reply)
5890

5991
try:
6092
return "\x02{}\x02 by \x02{}\x02 - {} / {}".format(
61-
data["artists"][0]["name"], data["external_urls"]["spotify"],
62-
data["name"], data["uri"])
93+
data["name"], data["artists"][0]["name"],
94+
data["external_urls"]["spotify"], data["uri"])
6395
except IndexError:
6496
return "Unable to find any tracks!"
6597

6698

6799
@hook.command("spalbum")
68-
def spalbum(bot, text, reply):
100+
def spalbum(text, reply):
69101
"""<album> - Search Spotify for <album>"""
70-
params = {"q": text.strip(), "offset": 0, "limit": 1, "type": "album"}
71-
72-
request = sprequest(bot, params)
73-
74-
try:
75-
request.raise_for_status()
76-
except HTTPError as e:
77-
reply("Could not get album information: {}".format(e.request.status_code))
78-
raise
79-
80-
if request.status_code != requests.codes.ok:
81-
return "Could not get album information: {}".format(
82-
request.status_code)
83-
84-
data = request.json()["albums"]["items"][0]
102+
data = _search(text, "album", reply)
85103

86104
try:
87105
return "\x02{}\x02 by \x02{}\x02 - {} / {}".format(
@@ -92,22 +110,9 @@ def spalbum(bot, text, reply):
92110

93111

94112
@hook.command("spartist", "artist")
95-
def spartist(bot, text, reply):
113+
def spartist(text, reply):
96114
"""<artist> - Search Spotify for <artist>"""
97-
params = {"q": text.strip(), "offset": 0, "limit": 1, "type": "artist"}
98-
99-
request = sprequest(bot, params)
100-
try:
101-
request.raise_for_status()
102-
except HTTPError as e:
103-
reply("Could not get artist information: {}".format(e.request.status_code))
104-
raise
105-
106-
if request.status_code != requests.codes.ok:
107-
return "Could not get artist information: {}".format(
108-
request.status_code)
109-
110-
data = request.json()["artists"]["items"][0]
115+
data = _search(text, "artist", reply)
111116

112117
try:
113118
return "\x02{}\x02 - {} / {}".format(
@@ -118,15 +123,14 @@ def spartist(bot, text, reply):
118123

119124
@hook.regex(http_re)
120125
@hook.regex(spotify_re)
121-
def spotify_url(bot, match):
122-
api_method = {'track': 'tracks', 'album': 'albums', 'artist': 'artists'}
126+
def spotify_url(match):
123127
_type = match.group(2)
124128
spotify_id = match.group(3)
125-
# no error catching here, if the API is down fail silently
126-
request = sprequest(bot, {}, 'http://api.spotify.com/v1/{}/{}'.format(
127-
api_method[_type], spotify_id))
128-
request.raise_for_status()
129+
130+
request = api.request("{}/{}".format(TYPE_MAP[_type], spotify_id))
131+
129132
data = request.json()
133+
130134
if _type == "track":
131135
name = data["name"]
132136
artist = data["artists"][0]["name"]

0 commit comments

Comments
 (0)