Skip to content

Commit 83cd939

Browse files
committed
FEAT: added experimental Spotify module for access to Spotify's Web API using OAuth2 authorization mechanism
1 parent 64553ac commit 83cd939

File tree

3 files changed

+334
-1
lines changed

3 files changed

+334
-1
lines changed

src/boot/version.reb

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
3.1.1.3.1
1+
3.1.2.3.1

src/modules/spotify-test.r3

+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
Rebol [
2+
Title: "Test Spotify module"
3+
Date: 02-Jul-2020
4+
Author: "Oldes"
5+
File: %test-spotify.r3
6+
Version: 0.1.0
7+
Require: 3.1.2
8+
Note: {
9+
Spotify API is still in Beta, and some bugs still happens...
10+
like this one: https://github.com/spotify/web-api/issues/1594
11+
}
12+
]
13+
14+
do %spotify.reb
15+
16+
system/options/log/http: 1 ; for verbose output
17+
try/except [
18+
spotify: system/modules/spotify/authorize [
19+
client-id: {8a01eb3b22ee4ba3b310e5e4fe10fd68}
20+
; optional client-secret may be used for classic "Authorization Code Flow"
21+
; if not used, the authentication will use PKCE extension, which is recommended
22+
;client-secret: {00000000000000000000000000000000}
23+
scope: [
24+
;@@ In real app not all scopes may be required!
25+
;@@ https://developer.spotify.com/documentation/general/guides/scopes/
26+
;- Images
27+
;@ugc-image-upload ; Write access to user-provided images.
28+
;- Spotify Connect
29+
@user-read-playback-state ; Read access to a user’s player state.
30+
@user-modify-playback-state ; Write access to a user’s playback state
31+
@user-read-currently-playing ; Read access to a user’s currently playing content.
32+
;- Playback
33+
@streaming ; Control playback of a Spotify track. (Web Playback SDK and Premium only)
34+
@app-remote-control ; Remote control playback of Spotify. (iOS and Android SDK only)
35+
;- Users
36+
@user-read-email ; Read access to user’s email address.
37+
@user-read-private ; Read access to user’s subscription details (type of user account).
38+
;- Playlists
39+
@playlist-read-collaborative ; Include collaborative playlists when requesting a user's playlists.
40+
@playlist-modify-public ; Write access to a user's public playlists.
41+
@playlist-read-private ; Read access to user's private playlists.
42+
@playlist-modify-private ; Write access to a user's private playlists.
43+
;- Library
44+
@user-library-modify ; Write/delete access to a user's "Your Music" library.
45+
@user-library-read ; Read access to a user's "Your Music" library.
46+
;- Listening History
47+
@user-top-read ; Read access to a user's top artists and tracks.
48+
@user-read-playback-position ; Read access to a user’s playback position in a content.
49+
@user-read-recently-played ; Read access to a user’s recently played tracks.
50+
;- Follow
51+
@user-follow-read ; Read access to the list of artists and other users that the user follows.
52+
@user-follow-modify ; Write/delete access to the list of artists and other users that the user follows.
53+
]
54+
]
55+
][ probe system/state/last-error ]
56+
57+
unless spotify [
58+
print "*** Access to Spotify API failed!"
59+
halt
60+
]
61+
62+
63+
spot?: function [
64+
"Prints information what is currently playin thru Spotify"
65+
][
66+
log: system/options/log/http
67+
system/options/log/http: 0
68+
track: spotify/get %me/player/currently-playing
69+
unless track [
70+
print "No device!"
71+
exit
72+
]
73+
track: load-json track
74+
print ["Song: " track/item/name]
75+
print ["Album: " track/item/album/name]
76+
foreach artist track/item/artists [
77+
print ["Artist:" artist/name ]
78+
]
79+
print ["ID: " track/item/id]
80+
print ["Pop: " track/item/popularity]
81+
system/options/log/http: log
82+
()
83+
]
84+
85+
print {^/Get Current User's Profile:}
86+
print spotify/get %me
87+
88+
halt
89+
90+
print {^/Get Information About The User's Current Playback:}
91+
print spotify/get %me/player
92+
93+
print {^/Get a User's Available Devices:}
94+
print spotify/get %me/player/devices
95+
96+
print {^/Get the User's Currently Playing Track:}
97+
print spotify/get %me/player/currently-playing
98+
;@@ https://developer.spotify.com/documentation/web-api/reference/player/get-the-users-currently-playing-track/
99+
100+
print {^/Get a List of Current User's Playlists:}
101+
print spotify/get %me/playlists
102+
; optionaly for example with %me/playlists?limit=1&offset=2 to get just second playlist
103+
;@@ https://developer.spotify.com/documentation/web-api/reference/playlists/get-a-list-of-current-users-playlists/
104+
105+
print {^/Get a List of a User's Playlists:}
106+
print spotify/get %users/01vv226r3fsej509cp2yuozb8/playlists
107+
;@@ https://developer.spotify.com/documentation/web-api/reference/playlists/get-list-users-playlists/
108+
109+
print {^/Pause/Play/Seek a User's Playback:}
110+
print spotify/put %me/player/pause
111+
print spotify/put %me/player/play
112+
print spotify/put %me/player/seek?position_ms=0
113+
;@@ https://github.com/spotify/web-api/issues/1594
114+
115+
print {^/Search for an Item:}
116+
print spotify/get %search?type=artist&q=ixieindamix
117+
;@@ https://developer.spotify.com/documentation/web-api/reference/search/search/
118+
119+
print {^/Check if Current User Follows Artists or Users:}
120+
print spotify/get %me/following/contains?type=artist&ids=2IpvlHMrM6rBvDY7dAAg7D,74ASZWbe4lXaubB36ztrGX
121+
print {^/Get User's Followed Artists:}
122+
print spotify/get %me/following?type=artist&limit=2
123+
print {^/Start following Artists:}
124+
print spotify/put %me/following?type=artist&ids=74ASZWbe4lXaubB36ztrGX
125+
print {^/Stop following Artists:}
126+
print spotify/del %me/following?type=artist&ids=74ASZWbe4lXaubB36ztrGX
127+
;@@ https://developer.spotify.com/documentation/web-api/reference/follow/
128+
129+
130+
print {^/Get Audio Features for a Track:}
131+
print spotify/get %audio-features/0uq0ibSGT4AwiVLLZGs9Qm
132+
;@@ https://developer.spotify.com/documentation/web-api/reference/tracks/get-audio-features/
133+
134+
print {^/Save Tracks for Current User:}
135+
print spotify/put %me/tracks?ids=0uq0ibSGT4AwiVLLZGs9Qm
136+
;@@ https://developer.spotify.com/documentation/web-api/reference/library/save-tracks-user/
137+
138+
halt

src/modules/spotify.reb

+195
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
Rebol [
2+
Title: "Spotify"
3+
Purpose: "Spotify Web API access (experiment)"
4+
Date: 2-Jul-2020
5+
Author: "Oldes"
6+
File: %spotify.reb
7+
Name: 'spotify
8+
Type: 'module
9+
Version: 0.1.0
10+
Require: 'httpd
11+
Note: {
12+
Useful info:
13+
https://developer.spotify.com/documentation/general/guides/authorization-guide/
14+
https://aaronparecki.com/oauth-2-simplified/
15+
}
16+
]
17+
unless system/modules/httpd [
18+
print "Importing HTTPD module"
19+
do %httpd.reb
20+
]
21+
22+
; context template as provided to user as a main access point to API
23+
spotify: object [
24+
client-id: none
25+
client-secret: none
26+
scope: ""
27+
port-id: 8989
28+
token: none
29+
get: func [what [any-string!]][system/modules/spotify/request self 'GET what none]
30+
put: func [what [any-string!] /with data][system/modules/spotify/request self 'PUT what data]
31+
del: func [what [any-string!]][system/modules/spotify/request self 'DELETE what none]
32+
]
33+
34+
authorize: function [
35+
"OAuth2 Spotify authorization used to get the main context"
36+
ctx [block! object!] "Data used for initialization (at least client-id is needed)"
37+
][
38+
ctx: make spotify ctx
39+
40+
unless ctx/client-id [
41+
print ajoin ["*** `" value "` is needed to authorize with Spotify!"]
42+
return none
43+
]
44+
unless string? ctx/client-id [ ctx/client-id: form ctx/client-id ]
45+
unless integer? ctx/port-id [ ctx/port-id: 8989 ]
46+
unless string? ctx/scope [ ctx/scope: form ctx/scope ]
47+
48+
; url-encode spaces in scopes
49+
parse ctx/scope [any [change some #[bitset! #{0064000080}] #"+" | skip]]
50+
51+
redirect-uri: rejoin [
52+
"http%3A%2F%2Flocalhost:" ctx/port-id "%2Fspotify-callback%2F"
53+
]
54+
55+
unless ctx/client-secret [
56+
code-verifier: form random/secure checksum/method join ctx/client-id now/precise 'sha256
57+
code-challenge: enbase/url checksum/method code-verifier 'sha256 64
58+
trim/with code-challenge #"="
59+
]
60+
;-- 1. Have your application request authorization; the user logs in and authorizes access
61+
; build request url
62+
url: rejoin [
63+
https://accounts.spotify.com/authorize?
64+
"response_type=code&show_dialog=false"
65+
"&client_id=" ctx/client-id
66+
"&scope=" ctx/scope
67+
"&redirect_uri=" redirect-uri
68+
]
69+
; if client-secret was not specified, create challenge for PKCE extension
70+
if code-challenge [
71+
append append url "&state=" state: form random 99999999999
72+
append append url "&code_challenge_method=S256&code_challenge=" code-challenge
73+
]
74+
; and open the url in user's default browser
75+
browse url
76+
77+
; Result from the server is returned as a redirect, so let's start simple server
78+
; listening on specified port (limited to accept only local requests, as the redirect is
79+
; going from the browser actually.. it automaticaly close itself once data are received
80+
result: system/modules/httpd/http-server/config/actor ctx/port-id [
81+
root: #[false] ; we are not serving any content!
82+
keep-alive: #[false]
83+
] object [
84+
85+
;- Server's actor functions
86+
87+
On-Accept: func [info [object!]][
88+
; allow only connections from localhost
89+
; TRUE = accepted, FALSE = refuse
90+
find [ 127.0.0.1 ] info/remote-ip
91+
]
92+
On-Header: func [ctx [object!]][
93+
? ctx/inp/target/file
94+
switch/default ctx/inp/target/file [
95+
%spotify-callback/ [
96+
ctx/out/status: 200
97+
ctx/out/content: ajoin [
98+
"<h1>OAuth2 Callback</h1>"
99+
"<br/>Request header:<pre>" mold ctx/inp/header </pre>
100+
"<br/>Values:<pre>" mold ctx/inp/target/values </pre>
101+
;<pre> mold ctx </pre>
102+
]
103+
;wake-up ctx/parent make event! [type: 'CLOSE port: port]
104+
ctx/done?: ctx/inp/target/values
105+
]
106+
][
107+
ctx/out/status: 405
108+
]
109+
]
110+
]
111+
112+
?? result
113+
114+
; validate result from first step
115+
if any [
116+
not block? result
117+
none? result/code
118+
state <> result/state
119+
][
120+
print {*** Unexpected result from Spotify authorization!}
121+
return none
122+
]
123+
124+
;-- 2. Have your application request refresh and access tokens; Spotify returns access and refresh tokens
125+
126+
try/except [
127+
time: now
128+
ctx/token: load-json write https://accounts.spotify.com/api/token probe compose [
129+
POST [
130+
Content-Type: "application/x-www-form-urlencoded"
131+
] ( rejoin [
132+
"grant_type=authorization_code"
133+
"&code=" result/code
134+
"&scope=" any [result/scope ""]
135+
"&redirect_uri=" :redirect-uri
136+
"&client_id=" ctx/client-id
137+
either code-verifier [
138+
; PKCE version
139+
join "&code_verifier=" code-verifier
140+
][ ; original version
141+
join "&client_secret=" ctx/client-secret
142+
]
143+
])
144+
]
145+
ctx/token/expires_in: time + (to time! ctx/token/expires_in)
146+
][
147+
print "*** Failed to receive Spotify token!"
148+
;probe system/state/last-error
149+
return none
150+
]
151+
; return Spotify context
152+
ctx
153+
]
154+
155+
refresh: function[
156+
ctx [object!]
157+
][
158+
ctx/token: load-json write https://accounts.spotify.com/api/token compose [
159+
POST [
160+
Content-Type: "application/x-www-form-urlencoded"
161+
]( rejoin [
162+
"grant_type=refresh_token"
163+
"&refresh_token=" ctx/token/refresh_token
164+
"&client_id=" ctx/client-id
165+
either ctx/client-secret [
166+
join "&client_secret=" ctx/client-secret
167+
][]
168+
])
169+
]
170+
]
171+
172+
request: func [
173+
ctx [object!]
174+
method [word!]
175+
what [any-string!]
176+
data [any-type!]
177+
/local header
178+
][
179+
try/except [
180+
if now >= ctx/token/expires_in [ refresh ctx ]
181+
header: compose [
182+
Authorization: (join "Bearer " ctx/token/access_token)
183+
]
184+
if map? data [
185+
append header [Content-Type: "application/json"]
186+
data: to-json data
187+
]
188+
write join https://api.spotify.com/v1/ what reduce [
189+
method header any [data ""]
190+
]
191+
][
192+
print ["*** Spotify" method "of" mold what "failed!"]
193+
none
194+
]
195+
]

0 commit comments

Comments
 (0)