my website https://bigspeed.me
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(nowplaying): initial commit

brwr.dev e7c48100 f9a64219

verified
+601
+4
spotify_proxy/.gitignore
··· 1 + *.beam 2 + *.ez 3 + /build 4 + erl_crash.dump
+24
spotify_proxy/README.md
··· 1 + # spotify_proxy 2 + 3 + [![Package Version](https://img.shields.io/hexpm/v/spotify_proxy)](https://hex.pm/packages/spotify_proxy) 4 + [![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/spotify_proxy/) 5 + 6 + ```sh 7 + gleam add spotify_proxy@1 8 + ``` 9 + ```gleam 10 + import spotify_proxy 11 + 12 + pub fn main() -> Nil { 13 + // TODO: An example of the project in use 14 + } 15 + ``` 16 + 17 + Further documentation can be found at <https://hexdocs.pm/spotify_proxy>. 18 + 19 + ## Development 20 + 21 + ```sh 22 + gleam run # Run the project 23 + gleam test # Run the tests 24 + ```
+20
spotify_proxy/gleam.toml
··· 1 + name = "spotify_proxy" 2 + version = "1.0.0" 3 + 4 + [erlang] 5 + application_start_module = "spotify_proxy" 6 + 7 + [dependencies] 8 + gleam_stdlib = ">= 0.44.0 and < 2.0.0" 9 + gleam_erlang = ">= 1.3.0 and < 2.0.0" 10 + gleam_otp = ">= 1.1.0 and < 2.0.0" 11 + wisp = ">= 2.0.0 and < 3.0.0" 12 + mist = ">= 5.0.3 and < 6.0.0" 13 + envoy = ">= 1.0.2 and < 2.0.0" 14 + gleam_http = ">= 4.2.0 and < 5.0.0" 15 + gleam_httpc = ">= 5.0.0 and < 6.0.0" 16 + gleam_json = ">= 3.0.2 and < 4.0.0" 17 + logging = ">= 1.3.0 and < 2.0.0" 18 + 19 + [dev-dependencies] 20 + gleeunit = ">= 1.0.0 and < 2.0.0"
+42
spotify_proxy/manifest.toml
··· 1 + # This file was generated by Gleam 2 + # You typically do not need to edit this file 3 + 4 + packages = [ 5 + { name = "directories", version = "1.2.0", build_tools = ["gleam"], requirements = ["envoy", "gleam_stdlib", "platform", "simplifile"], otp_app = "directories", source = "hex", outer_checksum = "D13090CFCDF6759B87217E8DDD73A75903A700148A82C1D33799F333E249BF9E" }, 6 + { name = "envoy", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "95FD059345AA982E89A0B6E2A3BF1CF43E17A7048DCD85B5B65D3B9E4E39D359" }, 7 + { name = "exception", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "329D269D5C2A314F7364BD2711372B6F2C58FA6F39981572E5CA68624D291F8C" }, 8 + { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" }, 9 + { name = "gleam_crypto", version = "1.5.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "50774BAFFF1144E7872814C566C5D653D83A3EBF23ACC3156B757A1B6819086E" }, 10 + { name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" }, 11 + { name = "gleam_http", version = "4.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "FFE29C3832698AC3EF6202922EC534EE19540152D01A7C2D22CB97482E4AF211" }, 12 + { name = "gleam_httpc", version = "5.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "C545172618D07811494E97AAA4A0FB34DA6F6D0061FDC8041C2F8E3BE2B2E48F" }, 13 + { name = "gleam_json", version = "3.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "874FA3C3BB6E22DD2BB111966BD40B3759E9094E05257899A7C08F5DE77EC049" }, 14 + { name = "gleam_otp", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "7987CBEBC8060B88F14575DEF546253F3116EBE2A5DA6FD82F38243FCE97C54B" }, 15 + { name = "gleam_stdlib", version = "0.63.2", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "962B25C667DA07F4CAB32001F44D3C41C1A89E58E3BBA54F183B482CF6122150" }, 16 + { name = "gleam_yielder", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_yielder", source = "hex", outer_checksum = "8E4E4ECFA7982859F430C57F549200C7749823C106759F4A19A78AEA6687717A" }, 17 + { name = "gleeunit", version = "1.6.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "FDC68A8C492B1E9B429249062CD9BAC9B5538C6FBF584817205D0998C42E1DAC" }, 18 + { name = "glisten", version = "8.0.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "logging", "telemetry"], otp_app = "glisten", source = "hex", outer_checksum = "534BB27C71FB9E506345A767C0D76B17A9E9199934340C975DC003C710E3692D" }, 19 + { name = "gramps", version = "6.0.0", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "8B7195978FBFD30B43DF791A8A272041B81E45D245314D7A41FC57237AA882A0" }, 20 + { name = "houdini", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "houdini", source = "hex", outer_checksum = "5DB1053F1AF828049C2B206D4403C18970ABEF5C18671CA3C2D2ED0DD64F6385" }, 21 + { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, 22 + { name = "logging", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "1098FBF10B54B44C2C7FDF0B01C1253CAFACDACABEFB4B0D027803246753E06D" }, 23 + { name = "marceau", version = "1.3.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "2D1C27504BEF45005F5DFB18591F8610FB4BFA91744878210BDC464412EC44E9" }, 24 + { name = "mist", version = "5.0.3", build_tools = ["gleam"], requirements = ["exception", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "gleam_yielder", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "7C4BE717A81305323C47C8A591E6B9BA4AC7F56354BF70B4D3DF08CC01192668" }, 25 + { name = "platform", version = "1.0.0", build_tools = ["gleam"], requirements = [], otp_app = "platform", source = "hex", outer_checksum = "8339420A95AD89AAC0F82F4C3DB8DD401041742D6C3F46132A8739F6AEB75391" }, 26 + { name = "simplifile", version = "2.3.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0A868DAC6063D9E983477981839810DC2E553285AB4588B87E3E9C96A7FB4CB4" }, 27 + { name = "telemetry", version = "1.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "telemetry", source = "hex", outer_checksum = "7015FC8919DBE63764F4B4B87A95B7C0996BD539E0D499BE6EC9D7F3875B79E6" }, 28 + { name = "wisp", version = "2.0.0", build_tools = ["gleam"], requirements = ["directories", "exception", "filepath", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "houdini", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "E9E4CEE4A5ACB41E3D9CFEF7AB07BF7DF670AB32E1CB434A61C6DA6859C66168" }, 29 + ] 30 + 31 + [requirements] 32 + envoy = { version = ">= 1.0.2 and < 2.0.0" } 33 + gleam_erlang = { version = ">= 1.3.0 and < 2.0.0" } 34 + gleam_http = { version = ">= 4.2.0 and < 5.0.0" } 35 + gleam_httpc = { version = ">= 5.0.0 and < 6.0.0" } 36 + gleam_json = { version = ">= 3.0.2 and < 4.0.0" } 37 + gleam_otp = { version = ">= 1.1.0 and < 2.0.0" } 38 + gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } 39 + gleeunit = { version = ">= 1.0.0 and < 2.0.0" } 40 + logging = { version = ">= 1.3.0 and < 2.0.0" } 41 + mist = { version = ">= 5.0.3 and < 6.0.0" } 42 + wisp = { version = ">= 2.0.0 and < 3.0.0" }
+81
spotify_proxy/src/spotify_proxy.gleam
··· 1 + import envoy 2 + import gleam/erlang/process 3 + import gleam/otp/actor 4 + import gleam/otp/static_supervisor as sup 5 + import gleam/otp/supervision 6 + import gleam/result 7 + import logging 8 + import spotify_proxy/api 9 + import spotify_proxy/spotify 10 + import spotify_proxy/status 11 + import spotify_proxy/util 12 + import wisp 13 + 14 + pub fn start(_app, _type) -> Result(process.Pid, actor.StartError) { 15 + logging.configure() 16 + logging.set_level(logging.Debug) 17 + 18 + let spotify_id = get_env("SPOTIFY_ID") 19 + let spotify_secret = get_env("SPOTIFY_SECRET") 20 + let spotify_refresh = get_env("SPOTIFY_REFRESH") 21 + 22 + // There are three actors here - one to query the Spotify API, one to hold the current status, 23 + // and one just to handle API requests. 24 + // I'm not entirely sure this is the best way to break these out, but the Spotify actor is *very* 25 + // fallible and I didn't really want to lose statuses if for whatever reason someone's request 26 + // errors or whatever 27 + // Flow is Spotify actor requests status -> Saves to status actor 28 + // Web actor requests status -> Status actor sends to web actor -> Web actor serializes and replies 29 + // 30 + // gg? 31 + 32 + let status_name = process.new_name("spotify_status") 33 + let status_subject = process.named_subject(status_name) 34 + 35 + let spotify_child = 36 + supervision.worker(fn() { 37 + spotify.spawn_link( 38 + spotify_id, 39 + spotify_secret, 40 + spotify_refresh, 41 + status_subject, 42 + ) 43 + }) 44 + 45 + let status_child = 46 + supervision.worker(fn() { 47 + status.spawn_link(status_name) |> result.map(actor.Started(_, Nil)) 48 + }) 49 + 50 + let api_child = api.supervised(status_subject) 51 + 52 + let res = 53 + sup.new(sup.OneForOne) 54 + |> sup.add(spotify_child) 55 + |> sup.add(status_child) 56 + |> sup.add(api_child) 57 + |> sup.start 58 + 59 + case res { 60 + Ok(actor.Started(pid, _data)) -> { 61 + let sup_name = process.new_name("main_supervisor") 62 + let _ = process.register(pid, sup_name) 63 + Ok(pid) 64 + } 65 + Error(why) -> Error(why) 66 + } 67 + } 68 + 69 + fn get_env(key: String) -> String { 70 + case envoy.get(key) { 71 + Ok(v) -> v 72 + Error(Nil) -> { 73 + wisp.log_critical("Missing env var " <> key <> ". Bye :(") 74 + util.stop() 75 + } 76 + } 77 + } 78 + 79 + pub fn main() { 80 + process.sleep_forever() 81 + }
+79
spotify_proxy/src/spotify_proxy/api.gleam
··· 1 + import gleam/erlang/process 2 + import gleam/int 3 + import gleam/json 4 + import gleam/option.{type Option, None, Some} 5 + import gleam/otp/actor 6 + import mist 7 + import spotify_proxy/status 8 + import spotify_proxy/util 9 + import wisp 10 + import wisp/wisp_mist 11 + 12 + type Context { 13 + Context(status_subject: process.Subject(status.Msg)) 14 + } 15 + 16 + pub fn supervised(status_subject: process.Subject(status.Msg)) { 17 + let secret_key_base = wisp.random_string(64) 18 + 19 + let ctx = Context(status_subject:) 20 + 21 + wisp_mist.handler(fn(req) { handle_request(req, ctx) }, secret_key_base) 22 + |> mist.new 23 + |> mist.port(8000) 24 + |> mist.supervised 25 + } 26 + 27 + fn handle_request(req: wisp.Request, ctx: Context) -> wisp.Response { 28 + case wisp.path_segments(req) { 29 + ["now-playing"] -> now_playing(req, ctx) 30 + _ -> wisp.not_found() 31 + } 32 + } 33 + 34 + fn now_playing(_req: wisp.Request, ctx: Context) -> wisp.Response { 35 + let now_playing = actor.call(ctx.status_subject, 250, status.Get) 36 + let resp = status_to_json(now_playing) 37 + wisp.json_response(json.to_string(resp), 200) 38 + } 39 + 40 + fn status_to_json( 41 + currently_playing: Option(status.CurrentlyPlaying), 42 + ) -> json.Json { 43 + case currently_playing { 44 + None -> { 45 + json.object([#("playing", json.bool(False))]) 46 + } 47 + Some(currently_playing) -> { 48 + let status.CurrentlyPlaying(song:, set_at:) = currently_playing 49 + let status.Song(artists:, duration_ms:, progress_ms:, url:, name:) = song 50 + 51 + // add the time since progress was recorded to the progress bar :3 52 + // this doesn't make a huge difference actually unless i increase the refresh timeout thing 53 + // 0 <= progress <= duration_ms 54 + // NOTE: set_at is in SECONDS! 55 + let interpolated_progress = 56 + int.min( 57 + duration_ms, 58 + progress_ms + { int.max(0, util.now() - set_at) * 1000 }, 59 + ) 60 + 61 + json.object([ 62 + #("playing", json.bool(True)), 63 + #("artists", json.array(artists, artist_to_json)), 64 + #("duration_ms", json.int(duration_ms)), 65 + #("progress_ms", json.int(interpolated_progress)), 66 + #("url", json.string(url)), 67 + #("name", json.string(name)), 68 + ]) 69 + } 70 + } 71 + } 72 + 73 + fn artist_to_json(artist: status.Artist) -> json.Json { 74 + let status.Artist(url:, name:) = artist 75 + json.object([ 76 + #("url", json.string(url)), 77 + #("name", json.string(name)), 78 + ]) 79 + }
+246
spotify_proxy/src/spotify_proxy/spotify.gleam
··· 1 + import gleam/bit_array 2 + import gleam/dynamic/decode 3 + import gleam/erlang/process 4 + import gleam/http 5 + import gleam/http/request 6 + import gleam/http/response 7 + import gleam/httpc 8 + import gleam/int 9 + import gleam/json 10 + import gleam/option.{type Option, None, Some} 11 + import gleam/otp/actor 12 + import gleam/result 13 + import gleam/string 14 + import gleam/uri 15 + import logging 16 + import spotify_proxy/status 17 + import spotify_proxy/util 18 + 19 + type State { 20 + State( 21 + self_subject: process.Subject(Nil), 22 + status_subject: process.Subject(status.Msg), 23 + id: String, 24 + secret: String, 25 + refresh: String, 26 + access: Option(Token), 27 + ) 28 + } 29 + 30 + type Token { 31 + Token(key: String, expires: Int) 32 + } 33 + 34 + pub fn spawn_link( 35 + id: String, 36 + secret: String, 37 + refresh: String, 38 + status_subject: process.Subject(status.Msg), 39 + ) { 40 + let init = fn(subject: process.Subject(Nil)) { 41 + let selector = process.new_selector() |> process.select(subject) 42 + let state = 43 + State( 44 + self_subject: subject, 45 + status_subject:, 46 + id:, 47 + secret:, 48 + refresh:, 49 + access: None, 50 + ) 51 + process.send(subject, Nil) 52 + actor.initialised(state) 53 + |> actor.selecting(selector) 54 + |> actor.returning(subject) 55 + } 56 + 57 + actor.new_with_initialiser(1000, fn(sub) { Ok(init(sub)) }) 58 + |> actor.on_message(handle_message) 59 + |> actor.start() 60 + } 61 + 62 + // only supports one message - refresh 63 + fn handle_message(state: State, _msg) -> actor.Next(State, Nil) { 64 + let assert Ok(req) = 65 + request.to("https://api.spotify.com/v1/me/player/currently-playing") 66 + use #(state, resp) <- try_or_abnormal(make_request(state, req, 0)) 67 + case resp { 68 + None -> { 69 + logging.log(logging.Info, "No status, not playing") 70 + actor.send(state.status_subject, status.Set(None)) 71 + queue_next(state) 72 + } 73 + Some(resp) -> { 74 + use song <- try_or_abnormal(json.parse( 75 + resp.body, 76 + currently_playing_resp_decoder(), 77 + )) 78 + actor.send(state.status_subject, status.Set(song)) 79 + queue_next(state) 80 + } 81 + } 82 + } 83 + 84 + fn queue_next(state: State) -> actor.Next(State, Nil) { 85 + logging.log( 86 + logging.Debug, 87 + "successfully refreshed status, queueing refresh for 10 secs from now...", 88 + ) 89 + process.send_after(state.self_subject, 10_000, Nil) 90 + actor.continue(state) 91 + } 92 + 93 + fn try_or_abnormal( 94 + res: Result(value, error), 95 + cb: fn(value) -> actor.Next(State, Nil), 96 + ) -> actor.Next(State, Nil) { 97 + case res { 98 + Ok(v) -> cb(v) 99 + Error(why) -> { 100 + logging.log( 101 + logging.Error, 102 + "Error, shutting down: " <> string.inspect(why), 103 + ) 104 + actor.stop_abnormal(string.inspect(why)) 105 + } 106 + } 107 + } 108 + 109 + fn make_request( 110 + state: State, 111 + req: request.Request(String), 112 + try: Int, 113 + ) -> Result(#(State, Option(response.Response(String))), FetchError) { 114 + case try { 115 + 2 -> { 116 + logging.log(logging.Error, "hit 2 tries for making request, bai...") 117 + Error(RepeatedFailure) 118 + } 119 + try -> { 120 + logging.log(logging.Debug, "try #" <> int.to_string(try)) 121 + use token <- result.try(access_token(state)) 122 + let state = State(..state, access: Some(token)) 123 + let req = request.set_header(req, "Authorization", "Bearer " <> token.key) 124 + use resp <- result.try(httpc.send(req) |> result.map_error(Http)) 125 + case resp.status { 126 + 200 -> Ok(#(state, Some(resp))) 127 + 204 -> Ok(#(state, None)) 128 + 401 -> { 129 + logging.log( 130 + logging.Warning, 131 + "Got 401 fetching status, force refreshing token", 132 + ) 133 + use token <- result.try(refresh_token(state)) 134 + make_request(State(..state, access: Some(token)), req, try + 1) 135 + } 136 + c -> { 137 + logging.log( 138 + logging.Error, 139 + "Got unhandled status code " <> int.to_string(c), 140 + ) 141 + Error(BadStatus(c)) 142 + } 143 + } 144 + } 145 + } 146 + } 147 + 148 + fn access_token(state: State) -> Result(Token, FetchError) { 149 + let now = util.now() 150 + case state.access { 151 + Some(access) if now < access.expires -> Ok(access) 152 + _ -> { 153 + logging.log(logging.Debug, "Token not loaded or expired, refreshing") 154 + let token = refresh_token(state) 155 + logging.log(logging.Debug, "Done!") 156 + token 157 + } 158 + } 159 + } 160 + 161 + fn refresh_token(state: State) -> Result(Token, FetchError) { 162 + let assert Ok(req) = request.to("https://accounts.spotify.com/api/token") 163 + let req = 164 + req 165 + |> request.set_header("Content-Type", "application/x-www-form-urlencoded") 166 + |> request.set_header("Authorization", basic_auth(state.id, state.secret)) 167 + |> request.set_method(http.Post) 168 + |> request.set_body( 169 + uri.query_to_string([ 170 + #("grant_type", "refresh_token"), 171 + #("refresh_token", state.refresh), 172 + ]), 173 + ) 174 + use resp <- result.try(httpc.send(req) |> result.map_error(Http)) 175 + case resp.status { 176 + 200 -> { 177 + resp.body 178 + |> json.parse(refresh_token_resp_decoder()) 179 + |> result.map_error(Json) 180 + } 181 + c -> { 182 + logging.log( 183 + logging.Error, 184 + "Got bad response code " <> int.to_string(c) <> " when refreshing token", 185 + ) 186 + Error(BadStatus(c)) 187 + } 188 + } 189 + } 190 + 191 + fn basic_auth(user: String, pass: String) -> String { 192 + "Basic " 193 + <> { 194 + { user <> ":" <> pass } 195 + |> bit_array.from_string 196 + |> bit_array.base64_encode(True) 197 + } 198 + } 199 + 200 + fn refresh_token_resp_decoder() -> decode.Decoder(Token) { 201 + use access_token <- decode.field("access_token", decode.string) 202 + use expires_in <- decode.field("expires_in", decode.int) 203 + decode.success(Token(key: access_token, expires: util.now() + expires_in)) 204 + } 205 + 206 + type FetchError { 207 + Http(httpc.HttpError) 208 + Json(json.DecodeError) 209 + BadStatus(Int) 210 + RepeatedFailure 211 + } 212 + 213 + fn currently_playing_resp_decoder() -> decode.Decoder(Option(status.Song)) { 214 + use currently_playing <- decode.field("is_playing", decode.bool) 215 + case currently_playing { 216 + False -> decode.success(None) 217 + True -> { 218 + use currently_playing_type <- decode.field( 219 + "currently_playing_type", 220 + decode.string, 221 + ) 222 + case currently_playing_type { 223 + "track" -> { 224 + use progress_ms <- decode.field("progress_ms", decode.int) 225 + use song <- decode.field("item", song_decoder(progress_ms)) 226 + decode.success(Some(song)) 227 + } 228 + _ -> decode.success(None) 229 + } 230 + } 231 + } 232 + } 233 + 234 + fn song_decoder(progress_ms: Int) -> decode.Decoder(status.Song) { 235 + use artists <- decode.field("artists", decode.list(artist_decoder())) 236 + use duration_ms <- decode.field("duration_ms", decode.int) 237 + use url <- decode.subfield(["external_urls", "spotify"], decode.string) 238 + use name <- decode.field("name", decode.string) 239 + decode.success(status.Song(artists:, duration_ms:, progress_ms:, url:, name:)) 240 + } 241 + 242 + fn artist_decoder() -> decode.Decoder(status.Artist) { 243 + use url <- decode.subfield(["external_urls", "spotify"], decode.string) 244 + use name <- decode.field("name", decode.string) 245 + decode.success(status.Artist(url:, name:)) 246 + }
+6
spotify_proxy/src/spotify_proxy/spotify_proxy_ffi.erl
··· 1 + -module(spotify_proxy_ffi). 2 + -export([now/0]). 3 + 4 + now() -> 5 + {MegaSecs, Secs, _} = os:timestamp(), 6 + 1000000 * MegaSecs + Secs.
+73
spotify_proxy/src/spotify_proxy/status.gleam
··· 1 + import gleam/erlang/process 2 + import gleam/option.{type Option, None} 3 + import gleam/otp/actor 4 + import gleam/result 5 + import gleam/string 6 + import logging 7 + import spotify_proxy/util 8 + 9 + type State = 10 + Option(CurrentlyPlaying) 11 + 12 + pub type Msg { 13 + Set(Option(Song)) 14 + Get(reply_to: process.Subject(Option(CurrentlyPlaying))) 15 + } 16 + 17 + pub fn spawn_link(name: process.Name(Msg)) { 18 + let subject = process.named_subject(name) 19 + 20 + let init = fn() { 21 + let selector = process.new_selector() |> process.select(subject) 22 + let state = None 23 + actor.initialised(state) 24 + |> actor.selecting(selector) 25 + |> actor.returning(subject) 26 + } 27 + 28 + use actor.Started(pid, _data) <- result.try( 29 + actor.new_with_initialiser(1000, fn(_) { Ok(init()) }) 30 + |> actor.on_message(handle_message) 31 + |> actor.start(), 32 + ) 33 + 34 + let _ = process.register(pid, name) 35 + Ok(pid) 36 + } 37 + 38 + fn handle_message(state: State, msg: Msg) -> actor.Next(State, Msg) { 39 + case msg { 40 + Set(currently_playing) -> { 41 + logging.log( 42 + logging.Debug, 43 + "setting new status: " <> string.inspect(currently_playing), 44 + ) 45 + 46 + currently_playing 47 + |> option.map(CurrentlyPlaying(song: _, set_at: util.now())) 48 + |> actor.continue() 49 + } 50 + Get(reply_subject) -> { 51 + process.send(reply_subject, state) 52 + actor.continue(state) 53 + } 54 + } 55 + } 56 + 57 + pub type CurrentlyPlaying { 58 + CurrentlyPlaying(song: Song, set_at: Int) 59 + } 60 + 61 + pub type Song { 62 + Song( 63 + artists: List(Artist), 64 + duration_ms: Int, 65 + progress_ms: Int, 66 + url: String, 67 + name: String, 68 + ) 69 + } 70 + 71 + pub type Artist { 72 + Artist(url: String, name: String) 73 + }
+13
spotify_proxy/src/spotify_proxy/util.gleam
··· 1 + import gleam/erlang/process 2 + 3 + @external(erlang, "init", "stop") 4 + fn init_stop() -> a 5 + 6 + pub fn stop() -> a { 7 + let v = init_stop() 8 + process.sleep_forever() 9 + v 10 + } 11 + 12 + @external(erlang, "spotify_proxy_ffi", "now") 13 + pub fn now() -> Int
+13
spotify_proxy/test/spotify_proxy_test.gleam
··· 1 + import gleeunit 2 + 3 + pub fn main() -> Nil { 4 + gleeunit.main() 5 + } 6 + 7 + // gleeunit test functions end in `_test` 8 + pub fn hello_world_test() { 9 + let name = "Joe" 10 + let greeting = "Hello, " <> name <> "!" 11 + 12 + assert greeting == "Hello, Joe!" 13 + }