My personal dotfiles
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

spotify-api.el 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. ;; spotify-api.el --- Spotify.el API integration layer
  2. ;; Copyright (C) 2014-2016 Daniel Fernandes Martins
  3. ;; Code:
  4. (defvar *spotify-oauth2-token*)
  5. (defvar *spotify-user*)
  6. (defconst spotify-api-endpoint "https://api.spotify.com/v1")
  7. (defconst spotify-oauth2-auth-url "https://accounts.spotify.com/authorize")
  8. (defconst spotify-oauth2-token-url "https://accounts.spotify.com/api/token")
  9. (defconst spotify-oauth2-scopes "playlist-read-private playlist-modify-public playlist-modify-private user-read-private")
  10. (defconst spotify-oauth2-callback "http://localhost:8591/")
  11. (defcustom spotify-oauth2-client-id ""
  12. "The unique identifier for your application. More info at
  13. https://developer.spotify.com/web-api/tutorial/."
  14. :type 'string)
  15. (defcustom spotify-oauth2-client-secret ""
  16. "The key that you will need to pass in secure calls to the Spotify Accounts and
  17. Web API services. More info at
  18. https://developer.spotify.com/web-api/tutorial/."
  19. :type 'string)
  20. (defcustom spotify-api-search-limit 50
  21. "Number of items returned when searching for something using the Spotify API."
  22. :type 'integer)
  23. (defcustom spotify-api-locale "en_US"
  24. "Optional. The desired language, consisting of an ISO 639 language code and
  25. an ISO 3166-1 alpha-2 country code, joined by an underscore.
  26. For example: es_MX, meaning Spanish (Mexico). Provide this parameter if you
  27. want the category metadata returned in a particular language."
  28. :type 'string)
  29. (defcustom spotify-api-country "US"
  30. "Optional. A country: an ISO 3166-1 alpha-2 country code. Provide this
  31. parameter if you want to narrow the list of returned categories to those
  32. relevant to a particular country. If omitted, the returned items will be
  33. globally relevant."
  34. :type 'string)
  35. (defun spotify-api-auth ()
  36. "Starts the Spotify Oauth2 authentication and authorization workflow."
  37. (oauth2-auth-and-store spotify-oauth2-auth-url
  38. spotify-oauth2-token-url
  39. spotify-oauth2-scopes
  40. spotify-oauth2-client-id
  41. spotify-oauth2-client-secret
  42. spotify-oauth2-callback))
  43. (defun spotify-api-call (method uri &optional data is-retry)
  44. "Makes a request to the given Spotify service endpoint and returns the parsed
  45. JSON response."
  46. (let ((url (concat spotify-api-endpoint uri))
  47. (headers '(("Content-Type" . "application/json"))))
  48. (with-current-buffer (oauth2-url-retrieve-synchronously *spotify-oauth2-token*
  49. url method data headers)
  50. (toggle-enable-multibyte-characters t)
  51. (goto-char (point-min))
  52. ;; If (json-read) signals 'end-of-file, we still kill the temp buffer
  53. ;; and re-signal the error
  54. (condition-case err
  55. (when (search-forward-regexp "^$" nil t)
  56. (let* ((json-object-type 'hash-table)
  57. (json-array-type 'list)
  58. (json-key-type 'symbol)
  59. (json (json-read))
  60. (error-json (gethash 'error json)))
  61. (kill-buffer)
  62. ;; Retries the request when the token expires and gets refreshed
  63. (if (and (hash-table-p error-json)
  64. (eq 401 (gethash 'status error-json))
  65. (not is-retry))
  66. (spotify-api-call method uri data t)
  67. json)))
  68. (end-of-file
  69. (kill-buffer)
  70. (signal (car err) (cdr err)))))))
  71. (defun spotify-disconnect ()
  72. "Clears the Spotify session currently in use."
  73. (interactive)
  74. (makunbound '*spotify-oauth2-token*)
  75. (makunbound '*spotify-user*)
  76. (stop-mode-line-timer)
  77. (message "Spotify session closed"))
  78. ;;;###autoload
  79. (defun spotify-connect ()
  80. "Starts a new Spotify session."
  81. (interactive)
  82. (spotify-disconnect)
  83. (defvar *spotify-oauth2-token* (spotify-api-auth))
  84. (defvar *spotify-user* (spotify-api-call "GET" "/me"))
  85. (when *spotify-user*
  86. (message "Welcome, %s!" (spotify-current-user-name))
  87. (start-mode-line-timer)))
  88. (defun spotify-connected-p ()
  89. "Returns whether there's an established session with Spotify API."
  90. (and (boundp '*spotify-user*) (not (null *spotify-user*))))
  91. (defun spotify-current-user-name ()
  92. "Returns the user's display name of the current Spotify session."
  93. (gethash 'display_name *spotify-user*))
  94. (defun spotify-current-user-id ()
  95. "Returns the user's id of the current Spotify session."
  96. (spotify-get-item-id *spotify-user*))
  97. (defun spotify-get-items (json)
  98. "Returns the list of items from the given json object."
  99. (gethash 'items json))
  100. (defun spotify-get-search-track-items (json)
  101. "Returns track items from the given search results json."
  102. (spotify-get-items (gethash 'tracks json)))
  103. (defun spotify-get-search-playlist-items (json)
  104. "Returns playlist items from the given search results json."
  105. (spotify-get-items (gethash 'playlists json)))
  106. (defun spotify-get-message (json)
  107. "Returns the message from the featured playlists response."
  108. (gethash 'message json))
  109. (defun spotify-get-playlist-tracks (json)
  110. (mapcar #'(lambda (item)
  111. (gethash 'track item))
  112. (spotify-get-items json)))
  113. (defun spotify-get-search-playlist-items (json)
  114. "Returns the playlist items from the given search results json."
  115. (spotify-get-items (gethash 'playlists json)))
  116. (defun spotify-get-track-album (json)
  117. "Returns the simplified album object from the given track object."
  118. (gethash 'album json))
  119. (defun spotify-get-track-number (json)
  120. "Returns the track number from the given track object."
  121. (gethash 'track_number json))
  122. (defun spotify-get-track-duration (json)
  123. "Returns the track duration, in milliseconds, from the given track object."
  124. (gethash 'duration_ms json))
  125. (defun spotify-get-track-duration-formatted (json)
  126. "Returns the formatted track duration from the given track object."
  127. (format-seconds "%m:%02s" (/ (spotify-get-track-duration json) 1000)))
  128. (defun spotify-get-track-album-name (json)
  129. "Returns the album name from the given track object."
  130. (spotify-get-item-name (spotify-get-track-album json)))
  131. (defun spotify-get-track-artist (json)
  132. "Returns the first artist from the given track object."
  133. (spotify-get-item-name (first (gethash 'artists json))))
  134. (defun spotify-get-track-popularity (json)
  135. "Returns the popularity from the given track/album/artist object."
  136. (gethash 'popularity json))
  137. (defun spotify-is-track-playable (json)
  138. "Returns whether the given track is playable by the current user."
  139. (not (eq :json-false (gethash 'is_playable json))))
  140. (defun spotify-get-item-name (json)
  141. "Returns the name from the given track/album/artist object."
  142. (gethash 'name json))
  143. (defun spotify-get-item-id (json)
  144. "Returns the id from the givem object."
  145. (gethash 'id json))
  146. (defun spotify-get-item-uri (json)
  147. "Returns the uri from the given track/album/artist object."
  148. (gethash 'uri json))
  149. (defun spotify-get-playlist-track-count (json)
  150. "Returns the number of tracks of the given playlist object."
  151. (gethash 'total (gethash 'tracks json)))
  152. (defun spotify-get-playlist-owner-id (json)
  153. "Returns the owner id of the given playlist object."
  154. (spotify-get-item-id (gethash 'owner json)))
  155. (defun spotify-api-search (type query page)
  156. "Searches artists, albums, tracks or playlists that match a keyword string,
  157. depending on the `type' argument."
  158. (let ((offset (* spotify-api-search-limit (1- page))))
  159. (spotify-api-call "GET"
  160. (concat "/search?"
  161. (url-build-query-string `((q ,query)
  162. (type ,type)
  163. (limit ,spotify-api-search-limit)
  164. (offset ,offset)
  165. (market from_token))
  166. nil t)))))
  167. (defun spotify-api-featured-playlists (page)
  168. "Returns the given page of Spotify's featured playlists."
  169. (let ((offset (* spotify-api-search-limit (1- page))))
  170. (spotify-api-call
  171. "GET"
  172. (concat "/browse/featured-playlists?"
  173. (url-build-query-string `((locale ,spotify-api-locale)
  174. (country ,spotify-api-country)
  175. (limit ,spotify-api-search-limit)
  176. (offset ,offset))
  177. nil t)))))
  178. (defun spotify-api-user-playlists (user-id page)
  179. "Returns the playlists for the given user."
  180. (let ((offset (* spotify-api-search-limit (1- page))))
  181. (spotify-api-call
  182. "GET"
  183. (concat (format "/users/%s/playlists?" (url-hexify-string user-id))
  184. (url-build-query-string `((limit ,spotify-api-search-limit)
  185. (offset ,offset))
  186. nil t)))))
  187. (defun spotify-api-playlist-create (user-id name is-public)
  188. "Creates a new playlist with the given name for the given user."
  189. (spotify-api-call
  190. "POST"
  191. (format "/users/%s/playlists"
  192. (url-hexify-string user-id))
  193. (format "{\"name\":\"%s\",\"public\":\"%s\"}"
  194. name
  195. (if is-public "true" "false"))))
  196. (defun spotify-api-playlist-follow (playlist)
  197. "Adds the current user as a follower of a playlist."
  198. (condition-case err
  199. (let ((owner (spotify-get-playlist-owner-id playlist))
  200. (id (spotify-get-item-id playlist)))
  201. (spotify-api-call
  202. "PUT"
  203. (format "/users/%s/playlists/%s/followers"
  204. (url-hexify-string owner)
  205. (url-hexify-string id))))
  206. (end-of-file t)))
  207. (defun spotify-api-playlist-unfollow (playlist)
  208. "Removes the current user as a follower of a playlist."
  209. (condition-case err
  210. (let ((owner (spotify-get-playlist-owner-id playlist))
  211. (id (spotify-get-item-id playlist)))
  212. (spotify-api-call
  213. "DELETE"
  214. (format "/users/%s/playlists/%s/followers"
  215. (url-hexify-string owner)
  216. (url-hexify-string id))))
  217. (end-of-file t)))
  218. (defun spotify-api-playlist-tracks (playlist page)
  219. "Returns the tracks of the given user's playlist."
  220. (let ((owner (spotify-get-playlist-owner-id playlist))
  221. (id (spotify-get-item-id playlist))
  222. (offset (* spotify-api-search-limit (1- page))))
  223. (spotify-api-call
  224. "GET"
  225. (concat (format "/users/%s/playlists/%s/tracks?"
  226. (url-hexify-string owner)
  227. (url-hexify-string id) offset)
  228. (url-build-query-string `((limit ,spotify-api-search-limit)
  229. (offset ,offset)
  230. (market from_token))
  231. nil t)))))
  232. (defun spotify-popularity-bar (popularity)
  233. "Returns the popularity indicator bar proportional to the given parameter,
  234. which must be a number between 0 and 100."
  235. (let ((num-bars (truncate (/ popularity 10))))
  236. (concat (make-string num-bars ?\u25cf)
  237. (make-string (- 10 num-bars) ?\u25cb))))
  238. (provide 'spotify-api)