forked from danielfm/smudge
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathspotify-api.el
More file actions
312 lines (262 loc) · 12.2 KB
/
spotify-api.el
File metadata and controls
312 lines (262 loc) · 12.2 KB
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
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
;; spotify-api.el --- Spotify.el API integration layer
;; Copyright (C) 2014-2018 Daniel Fernandes Martins
;; Code:
(defvar *spotify-oauth2-token* nil "Cached OAuth2 token")
(defvar *spotify-user* nil "Cached user object")
(defconst spotify-api-endpoint "https://api.spotify.com/v1")
(defconst spotify-oauth2-auth-url "https://accounts.spotify.com/authorize")
(defconst spotify-oauth2-token-url "https://accounts.spotify.com/api/token")
(defconst spotify-oauth2-scopes "playlist-read-private playlist-modify-public playlist-modify-private user-read-private")
(defconst spotify-oauth2-callback "http://localhost:8591/")
(defcustom spotify-oauth2-client-id ""
"The unique identifier for your application. More info at
https://developer.spotify.com/web-api/tutorial/."
:type 'string)
(defcustom spotify-oauth2-client-secret ""
"The key that you will need to pass in secure calls to the Spotify Accounts and
Web API services. More info at
https://developer.spotify.com/web-api/tutorial/."
:type 'string)
(defcustom spotify-api-search-limit 50
"Number of items returned when searching for something using the Spotify API."
:type 'integer)
(defcustom spotify-api-locale "en_US"
"Optional. The desired language, consisting of an ISO 639 language code and
an ISO 3166-1 alpha-2 country code, joined by an underscore.
For example: es_MX, meaning Spanish (Mexico). Provide this parameter if you
want the category metadata returned in a particular language."
:type 'string)
(defcustom spotify-api-country "US"
"Optional. A country: an ISO 3166-1 alpha-2 country code. Provide this
parameter if you want to narrow the list of returned categories to those
relevant to a particular country. If omitted, the returned items will be
globally relevant."
:type 'string)
(defun spotify-retrieve-oauth2-token ()
"Retrieves the Oauth2 access token that must be used to interact with the
Spotify API."
(if *spotify-oauth2-token*
*spotify-oauth2-token*
(let ((token (oauth2-auth spotify-oauth2-auth-url
spotify-oauth2-token-url
spotify-oauth2-client-id
spotify-oauth2-client-secret
spotify-oauth2-scopes
nil
spotify-oauth2-callback)))
(setq *spotify-oauth2-token* token)
(when (null token)
(user-error "OAuth2 authentication failed"))
token)))
(defun spotify-current-user ()
"Retrieves the object that represents the authenticated user."
(if *spotify-user*
*spotify-user*
(let ((user (spotify-api-call "GET" "/me")))
(setq *spotify-user* user)
user)))
(defun spotify-api-call (method uri &optional data is-retry)
"Makes a request to the given Spotify service endpoint and returns the parsed
JSON response."
(let ((url (concat spotify-api-endpoint uri))
(headers '(("Content-Type" . "application/json"))))
(with-current-buffer (oauth2-url-retrieve-synchronously (spotify-retrieve-oauth2-token)
url method data headers)
(toggle-enable-multibyte-characters t)
(goto-char (point-min))
;; If (json-read) signals 'end-of-file, we still kill the temp buffer
;; and re-signal the error
(condition-case err
(when (search-forward-regexp "^$" nil t)
(let* ((json-object-type 'hash-table)
(json-array-type 'list)
(json-key-type 'symbol)
(json (json-read))
(error-json (gethash 'error json)))
(kill-buffer)
;; Retries the request when the token expires and gets refreshed
(if (and (hash-table-p error-json)
(eq 401 (gethash 'status error-json))
(not is-retry))
(spotify-api-call method uri data t)
json)))
(end-of-file
(kill-buffer)
(signal (car err) (cdr err)))))))
(defun spotify-current-user-name ()
"Returns the user's display name of the current Spotify session."
(gethash 'display_name (spotify-current-user)))
(defun spotify-current-user-id ()
"Returns the user's id of the current Spotify session."
(spotify-get-item-id (spotify-current-user)))
(defun spotify-get-items (json)
"Returns the list of items from the given json object."
(gethash 'items json))
(defun spotify-get-search-track-items (json)
"Returns track items from the given search results json."
(spotify-get-items (gethash 'tracks json)))
(defun spotify-get-search-playlist-items (json)
"Returns playlist items from the given search results json."
(spotify-get-items (gethash 'playlists json)))
(defun spotify-get-message (json)
"Returns the message from the featured playlists response."
(gethash 'message json))
(defun spotify-get-playlist-tracks (json)
(mapcar #'(lambda (item)
(gethash 'track item))
(spotify-get-items json)))
(defun spotify-get-search-playlist-items (json)
"Returns the playlist items from the given search results json."
(spotify-get-items (gethash 'playlists json)))
(defun spotify-get-track-album (json)
"Returns the simplified album object from the given track object."
(gethash 'album json))
(defun spotify-get-track-number (json)
"Returns the track number from the given track object."
(gethash 'track_number json))
(defun spotify-get-track-duration (json)
"Returns the track duration, in milliseconds, from the given track object."
(gethash 'duration_ms json))
(defun spotify-get-track-duration-formatted (json)
"Returns the formatted track duration from the given track object."
(format-seconds "%m:%02s" (/ (spotify-get-track-duration json) 1000)))
(defun spotify-get-track-album-name (json)
"Returns the album name from the given track object."
(spotify-get-item-name (spotify-get-track-album json)))
(defun spotify-get-track-artist (json)
"Returns the first simplified artist object from the given track object."
(first (gethash 'artists json)))
(defun spotify-get-track-artist-name (json)
"Returns the first artist name from the given track object."
(spotify-get-item-name (spotify-get-track-artist json)))
(defun spotify-get-track-popularity (json)
"Returns the popularity from the given track/album/artist object."
(gethash 'popularity json))
(defun spotify-is-track-playable (json)
"Returns whether the given track is playable by the current user."
(not (eq :json-false (gethash 'is_playable json))))
(defun spotify-get-item-name (json)
"Returns the name from the given track/album/artist object."
(gethash 'name json))
(defun spotify-get-item-id (json)
"Returns the id from the given object."
(gethash 'id json))
(defun spotify-get-item-uri (json)
"Returns the uri from the given track/album/artist object."
(gethash 'uri json))
(defun spotify-get-playlist-track-count (json)
"Returns the number of tracks of the given playlist object."
(gethash 'total (gethash 'tracks json)))
(defun spotify-get-playlist-owner-id (json)
"Returns the owner id of the given playlist object."
(spotify-get-item-id (gethash 'owner json)))
(defun spotify-api-search (type query page)
"Searches artists, albums, tracks or playlists that match a keyword string,
depending on the `type' argument."
(let ((offset (* spotify-api-search-limit (1- page))))
(spotify-api-call "GET"
(concat "/search?"
(url-build-query-string `((q ,query)
(type ,type)
(limit ,spotify-api-search-limit)
(offset ,offset)
(market from_token))
nil t)))))
(defun spotify-api-featured-playlists (page)
"Returns the given page of Spotify's featured playlists."
(let ((offset (* spotify-api-search-limit (1- page))))
(spotify-api-call
"GET"
(concat "/browse/featured-playlists?"
(url-build-query-string `((locale ,spotify-api-locale)
(country ,spotify-api-country)
(limit ,spotify-api-search-limit)
(offset ,offset))
nil t)))))
(defun spotify-api-user-playlists (user-id page)
"Returns the playlists for the given user."
(let ((offset (* spotify-api-search-limit (1- page))))
(spotify-api-call
"GET"
(concat (format "/users/%s/playlists?" (url-hexify-string user-id))
(url-build-query-string `((limit ,spotify-api-search-limit)
(offset ,offset))
nil t)))))
(defun spotify-api-playlist-create (user-id name is-public)
"Creates a new playlist with the given name for the given user."
(spotify-api-call
"POST"
(format "/users/%s/playlists"
(url-hexify-string user-id))
(format "{\"name\":\"%s\",\"public\":\"%s\"}"
name
(if is-public "true" "false"))))
(defun spotify-api-album-get-tracks (album-id)
"Get list of track ids for given album."
(mapcar 'spotify-get-item-id (spotify-get-items (spotify-api-call
"GET"
(format "/albums/%s/tracks"
(url-hexify-string album-id))))))
(defun spotify-api-playlist-add-track (user-id playlist-id track-id)
"Add single track to playlist"
(spotify-api-playlist-add-tracks user-id playlist-id (list track-id)))
(defun spotify-format-id (type id)
"Wrap raw id to type if necessary"
(if (string-match-p "spotify" id) (format "\"%s\"" id) (format "\"spotify:%s:%s\"" type id)))
(defun spotify-api-playlist-add-tracks (user-id playlist-id track-ids)
"Add tracks in list track-ids in playlist"
(let ((tracks (format "%s" (mapconcat (lambda (x) (spotify-format-id "track" x)) track-ids ","))))
(spotify-api-call
"POST"
(format "/users/%s/playlists/%s/tracks"
(url-hexify-string user-id) (url-hexify-string playlist-id))
(format "{\"uris\": [ %s ]}" tracks)
)))
(defun spotify-api-playlist-add-album (user-id playlist-id album-id)
"Add all tracks with album id to playlist"
(spotify-api-playlist-add-tracks user-id playlist-id (spotify-api-album-get-tracks album-id)))
(defun spotify-api-playlist-follow (playlist)
"Adds the current user as a follower of a playlist."
(condition-case err
(let ((owner (spotify-get-playlist-owner-id playlist))
(id (spotify-get-item-id playlist)))
(spotify-api-call
"PUT"
(format "/users/%s/playlists/%s/followers"
(url-hexify-string owner)
(url-hexify-string id))
""))
(end-of-file t)))
(defun spotify-api-playlist-unfollow (playlist)
"Removes the current user as a follower of a playlist."
(condition-case err
(let ((owner (spotify-get-playlist-owner-id playlist))
(id (spotify-get-item-id playlist)))
(spotify-api-call
"DELETE"
(format "/users/%s/playlists/%s/followers"
(url-hexify-string owner)
(url-hexify-string id))
""))
(end-of-file t)))
(defun spotify-api-playlist-tracks (playlist page)
"Returns the tracks of the given user's playlist."
(let ((owner (spotify-get-playlist-owner-id playlist))
(id (spotify-get-item-id playlist))
(offset (* spotify-api-search-limit (1- page))))
(spotify-api-call
"GET"
(concat (format "/users/%s/playlists/%s/tracks?"
(url-hexify-string owner)
(url-hexify-string id) offset)
(url-build-query-string `((limit ,spotify-api-search-limit)
(offset ,offset)
(market from_token))
nil t)))))
(defun spotify-popularity-bar (popularity)
"Returns the popularity indicator bar proportional to the given parameter,
which must be a number between 0 and 100."
(let ((num-bars (truncate (/ popularity 10))))
(concat (make-string num-bars ?X)
(make-string (- 10 num-bars) ?-))))
(provide 'spotify-api)