From e4a5be7a216f2af3936e649dbed950f394582f0f Mon Sep 17 00:00:00 2001 From: Jacob Haip Date: Thu, 27 Apr 2023 22:28:37 -0400 Subject: copy spotify to haippi7 and add playing spotify from region demo --- user-programs/folk-haip.local/spotify.folk | 2 + user-programs/haippi7/region-play-spotify.folk | 11 + user-programs/haippi7/spotify.folk | 283 +++++++++++++++++++++++++ 3 files changed, 296 insertions(+) create mode 100644 user-programs/haippi7/region-play-spotify.folk create mode 100644 user-programs/haippi7/spotify.folk (limited to 'user-programs') diff --git a/user-programs/folk-haip.local/spotify.folk b/user-programs/folk-haip.local/spotify.folk index 360feb2b..e2c75421 100644 --- a/user-programs/folk-haip.local/spotify.folk +++ b/user-programs/folk-haip.local/spotify.folk @@ -3,6 +3,8 @@ package require tls 1.7 package require json # # Instructions for use: +# 0. Make sure additional dependencies are installed: +# sudo apt install tcl-tls tcllib # 1. edit Makefile or systemctl file to set SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET # 2. Run folk # 3. Open folk-whatever.local:8888 and login to spotify diff --git a/user-programs/haippi7/region-play-spotify.folk b/user-programs/haippi7/region-play-spotify.folk new file mode 100644 index 00000000..23deaaea --- /dev/null +++ b/user-programs/haippi7/region-play-spotify.folk @@ -0,0 +1,11 @@ +When laser blob /something/ inside region 0 { + Wish spotify:album:3l7JWewI3ZByxaT5BCgRx2 would be played on spotify +} + +When laser blob /something/ inside region 1 { + Wish spotify:track:0ZAdCDeysyoh5BOpuSGKy3 would be played on spotify +} + +When laser blob /something/ inside region 2 { + Wish spotify:artist:65dGLGjkw3UbddUg2GKQoZ would be played on spotify +} \ No newline at end of file diff --git a/user-programs/haippi7/spotify.folk b/user-programs/haippi7/spotify.folk new file mode 100644 index 00000000..e2c75421 --- /dev/null +++ b/user-programs/haippi7/spotify.folk @@ -0,0 +1,283 @@ +package require http +package require tls 1.7 +package require json + +# # Instructions for use: +# 0. Make sure additional dependencies are installed: +# sudo apt install tcl-tls tcllib +# 1. edit Makefile or systemctl file to set SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET +# 2. Run folk +# 3. Open folk-whatever.local:8888 and login to spotify +# 4. Now folk programs can control spotify +# Wish "spotify:track:232Y5lAjifWnc5NhUn34mz" would be played on spotify +# When spotify is currently playing /song/ { +# Wish $this is labelled "Playing: $song" +# } + +http::register https 443 [list ::tls::socket -autoservername true] + +proc ::handleConnectSpotify {chan addr port} { + fileevent $chan readable [list ::handleReadSpotify $chan $addr $port] +} +# TODO: Catch errors & return 501 +proc ::handlePageSpotify {path contentTypeVar} { + upvar $contentTypeVar contentType + + if {($path eq "/") || ([string match {/\?*} $path])} { + return { + + + + + Authorization Code flow with Spotify + + + + +
+
+

Authorization Code flow

+ Log in with Spotify +
+
+
+
+
+

oAuth info

+
+
Access token
+
Refresh token
+
+
+ +
+
+ + + + + + } + } + + subst { + + $path + + } +} +proc ::generateRandomString {length {chars "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"}} { + set range [expr {[string length $chars]-1}] + set txt "" + for {set i 0} {$i < $length} {incr i} { + set pos [expr {int(rand()*$range)}] + append txt [string range $chars $pos $pos] + } + return $txt +} +proc ::handleReadSpotify {chan addr port} { + chan configure $chan -translation crlf + gets $chan line; set firstline $line + puts "Http: $chan $addr $port: $line" + set headers [list] + while {[gets $chan line] >= 0 && $line ne ""} { + if {[regexp -expanded {^( [^\s:]+ ) \s* : \s* (.+)} $line -> k v]} { + lappend headers $k $v + } else { break } + } + set tclHostname [info hostname] + set hostnameWithoutDotLocal [string map {".local" ""} $tclHostname] + set redirectUrl "http://$hostnameWithoutDotLocal.local:8888/callback" + set redirectUrlEncoded "http%3A%2F%2F$hostnameWithoutDotLocal.local%3A8888%2Fcallback" + set clientId $::env(SPOTIFY_CLIENT_ID) + set clientSecret $::env(SPOTIFY_CLIENT_SECRET) + if {[regexp {GET ([^ ]*) HTTP/1.1} $firstline -> path]} { + set contentType "text/html; charset=utf-8" + puts "Path: $path" + if {($path eq "/") || ([string match {/\?*} $path])} { + set response [::handlePageSpotify $path contentType] + puts -nonewline $chan "HTTP/1.1 200 OK\nConnection: close\nContent-Type: $contentType\n\n" + chan configure $chan -encoding binary -translation binary + puts -nonewline $chan $response + } elseif {$path eq "/login"} { + set state [::generateRandomString 16] + set responseTypeParam "response_type=code" + set redirectUrlParam "redirect_uri=$redirectUrlEncoded" + set clientIdParam "client_id=$clientId" + set scopeParam "scope=user-read-private%20user-read-email%20user-read-currently-playing%20user-modify-playback-state" + set stateParam "state=$state" + puts -nonewline $chan "HTTP/1.1 302 Found\nSet-Cookie: spotify_auth_state=$state\nLocation: https://accounts.spotify.com/authorize?$responseTypeParam&$clientIdParam&$scopeParam&$redirectUrlParam&$stateParam" + } elseif {[string match "/callback*" $path]} { + regexp {/?code=(.+)&state=(.+)} $path qpFullMatch qpCode qpState + set encodedAuthorization [binary encode base64 "$clientId:$clientSecret"] + dict set hd "Content-Type" "application/x-www-form-urlencoded" + dict set hd Authorization "Basic $encodedAuthorization" + set token [::http::geturl "https://accounts.spotify.com/api/token" -headers $hd -query [::http::formatQuery grant_type authorization_code redirect_uri $redirectUrl code $qpCode]] + set responseBody [::http::data $token] + ::http::cleanup $token + puts $responseBody + set resultData [::json::json2dict $responseBody] + set accessToken [dict get $resultData access_token] + set refreshToken [dict get $resultData refresh_token] + set ::spotifyAccessToken $accessToken + puts -nonewline $chan "HTTP/1.1 302 Found\nSet-Cookie: spotify_auth_state=0; Max-Age=0\nLocation: /?access_token=$accessToken&refresh_token=$refreshToken" + } elseif {[string match "/refresh_token*" $path]} { + regexp {/?refresh_token=(.+)} $path qpFullMatch qpRefreshToken + set encodedAuthorization [binary encode base64 "$clientId:$clientSecret"] + dict set hd "Content-Type" "application/x-www-form-urlencoded" + dict set hd Authorization "Basic $encodedAuthorization" + set token [::http::geturl "https://accounts.spotify.com/api/token" -headers $hd -query [::http::formatQuery grant_type refresh_token refresh_token $qpRefreshToken]] + set responseBody [::http::data $token] + ::http::cleanup $token + puts $responseBody + set resultData [::json::json2dict $responseBody] + set accessToken [dict get $resultData access_token] + set ::spotifyAccessToken $accessToken + set response "{\"access_token\": \"$accessToken\"}" + puts -nonewline $chan "HTTP/1.1 200 OK\nConnection: close\nContent-Type: application/json; charset=utf-8\n\n" + chan configure $chan -encoding binary -translation binary + puts -nonewline $chan $response + } + close $chan + } else { puts "Closing: $chan $addr $port $headers"; close $chan } +} + +if {[catch {set ::serverSockSpotify [socket -server ::handleConnectSpotify 8888]}] == 1} { + error "There's already a Web-capable Folk node running on this machine." +} + +set ::spotifyAccessToken "" +When /someone/ wishes /uri/ would be played on spotify { + if {$::spotifyAccessToken eq ""} { + puts "spotifyAccessToken is empty" + } else { + dict set hd "Content-Type" "application/json" + dict set hd Authorization "Bearer $::spotifyAccessToken" + puts "Requesting play of $uri on Spotify" + # "spotify:album:04HMMwLmjkftjWy7xc6Bho" + set queryParam "{\"context_uri\": \"$uri\"}" + if {[string match "*track*" $uri]} { + # "spotify:track:232Y5lAjifWnc5NhUn34mz" + set queryParam "{\"uris\": \[\"$uri\"\]}" + } + set token [::http::geturl "https://api.spotify.com/v1/me/player/play" -method PUT -headers $hd -query $queryParam] + set responseBody [::http::data $token] + ::http::cleanup $token + puts $responseBody + } +} + +proc ::pollCurrentSong {} { + puts "Polling current song" + if {$::spotifyAccessToken eq ""} { + puts "spotifyAccessToken is empty" + } else { + dict set hd "Content-Type" "application/json" + dict set hd Authorization "Bearer $::spotifyAccessToken" + set token [::http::geturl "https://api.spotify.com/v1/me/player/currently-playing" -headers $hd] + set responseBody [::http::data $token] + puts $responseBody + set responseCode [::http::ncode $token] + ::http::cleanup $token + puts $responseCode + if {$responseCode == 204} { + Retract spotify claims spotify is currently playing /something/ + Assert spotify claims spotify is currently playing {} + puts "done doing the thing, for nothing" + } else { + set resultData [::json::json2dict $responseBody] + set item [dict get $resultData item] + set songTitle [dict get $item name] + set artists [dict get $item artists] + set artistsNames {} + foreach element $artists {lappend artistsNames [dict get $element name]} + set artistsString [join $artistsNames ", "] + Retract spotify claims spotify is currently playing /something/ + Assert spotify claims spotify is currently playing "$songTitle by $artistsString" + puts "done doing the thing" + } + } + after 20000 [list ::pollCurrentSong] +} +::pollCurrentSong -- cgit v1.2.3