11import re
2- from datetime import datetime
2+ from datetime import datetime , timedelta
3+ from threading import RLock
34
45import requests
56from requests import HTTPError
67from requests .auth import HTTPBasicAuth
8+ from yarl import URL
79
810from 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