|
1 | 1 | import re |
2 | | -from datetime import datetime |
| 2 | +from datetime import datetime, timedelta |
| 3 | +from threading import RLock |
3 | 4 |
|
4 | 5 | import requests |
5 | 6 | from requests import HTTPError |
6 | 7 | from requests.auth import HTTPBasicAuth |
| 8 | +from yarl import URL |
7 | 9 |
|
8 | 10 | from cloudbot import hook |
9 | 11 |
|
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 |
38 | 13 |
|
| 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 | +) |
39 | 20 |
|
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() |
44 | 43 |
|
45 | | - request = sprequest(bot, params) |
| 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) |
| 53 | + |
| 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} |
46 | 67 |
|
47 | 68 | try: |
48 | | - request.raise_for_status() |
| 69 | + request = api.search(params) |
49 | 70 | except HTTPError as e: |
50 | 71 | reply("Could not get track information: {}".format(e.request.status_code)) |
51 | 72 | raise |
52 | 73 |
|
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] |
56 | 75 |
|
57 | | - data = request.json()["tracks"]["items"][0] |
58 | 76 |
|
59 | | - try: |
60 | | - return "\x02{}\x02 by \x02{}\x02 - {} / {}".format( |
61 | | - data["artists"][0]["name"], data["external_urls"]["spotify"], |
62 | | - data["name"], data["uri"]) |
63 | | - except IndexError: |
64 | | - return "Unable to find any tracks!" |
| 77 | +def _do_format(data, _type): |
| 78 | + name = data["name"] |
| 79 | + if _type == "track": |
| 80 | + artist = data["artists"][0]["name"] |
| 81 | + album = data["album"]["name"] |
65 | 82 |
|
| 83 | + return "Spotify Track", "\x02{}\x02 by \x02{}\x02 from the album \x02{}\x02".format(name, artist, album) |
| 84 | + elif _type == "artist": |
| 85 | + return "Spotify Artist", "\x02{}\x02, followers: \x02{}\x02, genres: \x02{}\x02".format( |
| 86 | + name, data["followers"]["total"], ', '.join(data["genres"]) |
| 87 | + ) |
| 88 | + elif _type == "album": |
| 89 | + return "Spotify Album", "\x02{}\x02 - \x02{}\x02".format(data["artists"][0]["name"], name) |
| 90 | + else: |
| 91 | + raise ValueError("Attempt to format unknown Spotify API type: " + _type) |
66 | 92 |
|
67 | | -@hook.command("spalbum") |
68 | | -def spalbum(bot, text, reply): |
69 | | - """<album> - Search Spotify for <album>""" |
70 | | - params = {"q": text.strip(), "offset": 0, "limit": 1, "type": "album"} |
71 | 93 |
|
72 | | - request = sprequest(bot, params) |
| 94 | +def _format_response(data, _type, show_pre=False, show_url=False, show_uri=False): |
| 95 | + pre, text = _do_format(data, _type) |
| 96 | + if show_pre: |
| 97 | + out = pre + ": " |
| 98 | + else: |
| 99 | + out = "" |
73 | 100 |
|
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 |
| 101 | + out += text |
79 | 102 |
|
80 | | - if request.status_code != requests.codes.ok: |
81 | | - return "Could not get album information: {}".format( |
82 | | - request.status_code) |
| 103 | + if show_uri or show_url: |
| 104 | + out += ' -' |
83 | 105 |
|
84 | | - data = request.json()["albums"]["items"][0] |
| 106 | + if show_url: |
| 107 | + out += ' ' + data["external_urls"]["spotify"] |
85 | 108 |
|
86 | | - try: |
87 | | - return "\x02{}\x02 by \x02{}\x02 - {} / {}".format( |
88 | | - data["artists"][0]["name"], data["name"], |
89 | | - data["external_urls"]["spotify"], data["uri"]) |
90 | | - except IndexError: |
91 | | - return "Unable to find any albums!" |
| 109 | + if show_uri: |
| 110 | + out += ' ' + "[{}]".format(data["uri"]) |
92 | 111 |
|
| 112 | + return out |
93 | 113 |
|
94 | | -@hook.command("spartist", "artist") |
95 | | -def spartist(bot, text, reply): |
96 | | - """<artist> - Search Spotify for <artist>""" |
97 | | - params = {"q": text.strip(), "offset": 0, "limit": 1, "type": "artist"} |
98 | 114 |
|
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 |
| 115 | +def _format_search(text, _type, reply): |
| 116 | + data = _search(text, _type, reply) |
| 117 | + return _format_response(data, _type, show_url=True, show_uri=True) |
105 | 118 |
|
106 | | - if request.status_code != requests.codes.ok: |
107 | | - return "Could not get artist information: {}".format( |
108 | | - request.status_code) |
109 | 119 |
|
110 | | - data = request.json()["artists"]["items"][0] |
| 120 | +@hook.onload |
| 121 | +def create_api(bot): |
| 122 | + keys = bot.config['api_keys'] |
| 123 | + client_id = keys['spotify_client_id'] |
| 124 | + client_secret = keys['spotify_client_secret'] |
| 125 | + global api |
| 126 | + api = SpotifyAPI(client_id, client_secret) |
111 | 127 |
|
112 | | - try: |
113 | | - return "\x02{}\x02 - {} / {}".format( |
114 | | - data["name"], data["external_urls"]["spotify"], data["uri"]) |
115 | | - except IndexError: |
116 | | - return "Unable to find any artists!" |
| 128 | + |
| 129 | +@hook.command('spotify', 'sptrack') |
| 130 | +def spotify(text, reply): |
| 131 | + """<song> - Search Spotify for <song>""" |
| 132 | + return _format_search(text, "track", reply) |
| 133 | + |
| 134 | + |
| 135 | +@hook.command("spalbum") |
| 136 | +def spalbum(text, reply): |
| 137 | + """<album> - Search Spotify for <album>""" |
| 138 | + return _format_search(text, "album", reply) |
| 139 | + |
| 140 | + |
| 141 | +@hook.command("spartist", "artist") |
| 142 | +def spartist(text, reply): |
| 143 | + """<artist> - Search Spotify for <artist>""" |
| 144 | + return _format_search(text, "artist", reply) |
117 | 145 |
|
118 | 146 |
|
119 | 147 | @hook.regex(http_re) |
120 | 148 | @hook.regex(spotify_re) |
121 | | -def spotify_url(bot, match): |
122 | | - api_method = {'track': 'tracks', 'album': 'albums', 'artist': 'artists'} |
| 149 | +def spotify_url(match): |
123 | 150 | _type = match.group(2) |
124 | 151 | 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() |
| 152 | + |
| 153 | + request = api.request("{}/{}".format(TYPE_MAP[_type], spotify_id)) |
| 154 | + |
129 | 155 | data = request.json() |
130 | | - if _type == "track": |
131 | | - name = data["name"] |
132 | | - artist = data["artists"][0]["name"] |
133 | | - album = data["album"]["name"] |
134 | | - url = data['external_urls']['spotify'] |
135 | | - uri = data['uri'] |
136 | 156 |
|
137 | | - return "Spotify Track: \x02{}\x02 by \x02{}\x02 from the album \x02{}\x02 {} [{}]".format( |
138 | | - name, artist, album, url, uri) |
139 | | - elif _type == "artist": |
140 | | - return "Spotify Artist: \x02{}\x02, followers: \x02{}\x02, genres: \x02{}\x02".format( |
141 | | - data["name"], data["followers"]["total"], |
142 | | - ', '.join(data["genres"])) |
143 | | - elif _type == "album": |
144 | | - return "Spotify Album: \x02{}\x02 - \x02{}\x02 {} [{}]".format( |
145 | | - data["artists"][0]["name"], data["name"], |
146 | | - data['external_urls']['spotify'], data['uri']) |
| 157 | + return _format_response(data, _type, show_pre=True) |
0 commit comments