···11+# spotify_proxy
22+33+[](https://hex.pm/packages/spotify_proxy)
44+[](https://hexdocs.pm/spotify_proxy/)
55+66+```sh
77+gleam add spotify_proxy@1
88+```
99+```gleam
1010+import spotify_proxy
1111+1212+pub fn main() -> Nil {
1313+ // TODO: An example of the project in use
1414+}
1515+```
1616+1717+Further documentation can be found at <https://hexdocs.pm/spotify_proxy>.
1818+1919+## Development
2020+2121+```sh
2222+gleam run # Run the project
2323+gleam test # Run the tests
2424+```
+20
spotify_proxy/gleam.toml
···11+name = "spotify_proxy"
22+version = "1.0.0"
33+44+[erlang]
55+application_start_module = "spotify_proxy"
66+77+[dependencies]
88+gleam_stdlib = ">= 0.44.0 and < 2.0.0"
99+gleam_erlang = ">= 1.3.0 and < 2.0.0"
1010+gleam_otp = ">= 1.1.0 and < 2.0.0"
1111+wisp = ">= 2.0.0 and < 3.0.0"
1212+mist = ">= 5.0.3 and < 6.0.0"
1313+envoy = ">= 1.0.2 and < 2.0.0"
1414+gleam_http = ">= 4.2.0 and < 5.0.0"
1515+gleam_httpc = ">= 5.0.0 and < 6.0.0"
1616+gleam_json = ">= 3.0.2 and < 4.0.0"
1717+logging = ">= 1.3.0 and < 2.0.0"
1818+1919+[dev-dependencies]
2020+gleeunit = ">= 1.0.0 and < 2.0.0"
+42
spotify_proxy/manifest.toml
···11+# This file was generated by Gleam
22+# You typically do not need to edit this file
33+44+packages = [
55+ { name = "directories", version = "1.2.0", build_tools = ["gleam"], requirements = ["envoy", "gleam_stdlib", "platform", "simplifile"], otp_app = "directories", source = "hex", outer_checksum = "D13090CFCDF6759B87217E8DDD73A75903A700148A82C1D33799F333E249BF9E" },
66+ { name = "envoy", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "95FD059345AA982E89A0B6E2A3BF1CF43E17A7048DCD85B5B65D3B9E4E39D359" },
77+ { name = "exception", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "329D269D5C2A314F7364BD2711372B6F2C58FA6F39981572E5CA68624D291F8C" },
88+ { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" },
99+ { name = "gleam_crypto", version = "1.5.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "50774BAFFF1144E7872814C566C5D653D83A3EBF23ACC3156B757A1B6819086E" },
1010+ { name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" },
1111+ { name = "gleam_http", version = "4.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "FFE29C3832698AC3EF6202922EC534EE19540152D01A7C2D22CB97482E4AF211" },
1212+ { 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" },
1313+ { name = "gleam_json", version = "3.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "874FA3C3BB6E22DD2BB111966BD40B3759E9094E05257899A7C08F5DE77EC049" },
1414+ { name = "gleam_otp", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "7987CBEBC8060B88F14575DEF546253F3116EBE2A5DA6FD82F38243FCE97C54B" },
1515+ { name = "gleam_stdlib", version = "0.63.2", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "962B25C667DA07F4CAB32001F44D3C41C1A89E58E3BBA54F183B482CF6122150" },
1616+ { name = "gleam_yielder", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_yielder", source = "hex", outer_checksum = "8E4E4ECFA7982859F430C57F549200C7749823C106759F4A19A78AEA6687717A" },
1717+ { name = "gleeunit", version = "1.6.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "FDC68A8C492B1E9B429249062CD9BAC9B5538C6FBF584817205D0998C42E1DAC" },
1818+ { 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" },
1919+ { 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" },
2020+ { name = "houdini", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "houdini", source = "hex", outer_checksum = "5DB1053F1AF828049C2B206D4403C18970ABEF5C18671CA3C2D2ED0DD64F6385" },
2121+ { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" },
2222+ { name = "logging", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "1098FBF10B54B44C2C7FDF0B01C1253CAFACDACABEFB4B0D027803246753E06D" },
2323+ { name = "marceau", version = "1.3.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "2D1C27504BEF45005F5DFB18591F8610FB4BFA91744878210BDC464412EC44E9" },
2424+ { 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" },
2525+ { name = "platform", version = "1.0.0", build_tools = ["gleam"], requirements = [], otp_app = "platform", source = "hex", outer_checksum = "8339420A95AD89AAC0F82F4C3DB8DD401041742D6C3F46132A8739F6AEB75391" },
2626+ { name = "simplifile", version = "2.3.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0A868DAC6063D9E983477981839810DC2E553285AB4588B87E3E9C96A7FB4CB4" },
2727+ { name = "telemetry", version = "1.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "telemetry", source = "hex", outer_checksum = "7015FC8919DBE63764F4B4B87A95B7C0996BD539E0D499BE6EC9D7F3875B79E6" },
2828+ { 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" },
2929+]
3030+3131+[requirements]
3232+envoy = { version = ">= 1.0.2 and < 2.0.0" }
3333+gleam_erlang = { version = ">= 1.3.0 and < 2.0.0" }
3434+gleam_http = { version = ">= 4.2.0 and < 5.0.0" }
3535+gleam_httpc = { version = ">= 5.0.0 and < 6.0.0" }
3636+gleam_json = { version = ">= 3.0.2 and < 4.0.0" }
3737+gleam_otp = { version = ">= 1.1.0 and < 2.0.0" }
3838+gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" }
3939+gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
4040+logging = { version = ">= 1.3.0 and < 2.0.0" }
4141+mist = { version = ">= 5.0.3 and < 6.0.0" }
4242+wisp = { version = ">= 2.0.0 and < 3.0.0" }
+81
spotify_proxy/src/spotify_proxy.gleam
···11+import envoy
22+import gleam/erlang/process
33+import gleam/otp/actor
44+import gleam/otp/static_supervisor as sup
55+import gleam/otp/supervision
66+import gleam/result
77+import logging
88+import spotify_proxy/api
99+import spotify_proxy/spotify
1010+import spotify_proxy/status
1111+import spotify_proxy/util
1212+import wisp
1313+1414+pub fn start(_app, _type) -> Result(process.Pid, actor.StartError) {
1515+ logging.configure()
1616+ logging.set_level(logging.Debug)
1717+1818+ let spotify_id = get_env("SPOTIFY_ID")
1919+ let spotify_secret = get_env("SPOTIFY_SECRET")
2020+ let spotify_refresh = get_env("SPOTIFY_REFRESH")
2121+2222+ // There are three actors here - one to query the Spotify API, one to hold the current status,
2323+ // and one just to handle API requests.
2424+ // I'm not entirely sure this is the best way to break these out, but the Spotify actor is *very*
2525+ // fallible and I didn't really want to lose statuses if for whatever reason someone's request
2626+ // errors or whatever
2727+ // Flow is Spotify actor requests status -> Saves to status actor
2828+ // Web actor requests status -> Status actor sends to web actor -> Web actor serializes and replies
2929+ //
3030+ // gg?
3131+3232+ let status_name = process.new_name("spotify_status")
3333+ let status_subject = process.named_subject(status_name)
3434+3535+ let spotify_child =
3636+ supervision.worker(fn() {
3737+ spotify.spawn_link(
3838+ spotify_id,
3939+ spotify_secret,
4040+ spotify_refresh,
4141+ status_subject,
4242+ )
4343+ })
4444+4545+ let status_child =
4646+ supervision.worker(fn() {
4747+ status.spawn_link(status_name) |> result.map(actor.Started(_, Nil))
4848+ })
4949+5050+ let api_child = api.supervised(status_subject)
5151+5252+ let res =
5353+ sup.new(sup.OneForOne)
5454+ |> sup.add(spotify_child)
5555+ |> sup.add(status_child)
5656+ |> sup.add(api_child)
5757+ |> sup.start
5858+5959+ case res {
6060+ Ok(actor.Started(pid, _data)) -> {
6161+ let sup_name = process.new_name("main_supervisor")
6262+ let _ = process.register(pid, sup_name)
6363+ Ok(pid)
6464+ }
6565+ Error(why) -> Error(why)
6666+ }
6767+}
6868+6969+fn get_env(key: String) -> String {
7070+ case envoy.get(key) {
7171+ Ok(v) -> v
7272+ Error(Nil) -> {
7373+ wisp.log_critical("Missing env var " <> key <> ". Bye :(")
7474+ util.stop()
7575+ }
7676+ }
7777+}
7878+7979+pub fn main() {
8080+ process.sleep_forever()
8181+}
+79
spotify_proxy/src/spotify_proxy/api.gleam
···11+import gleam/erlang/process
22+import gleam/int
33+import gleam/json
44+import gleam/option.{type Option, None, Some}
55+import gleam/otp/actor
66+import mist
77+import spotify_proxy/status
88+import spotify_proxy/util
99+import wisp
1010+import wisp/wisp_mist
1111+1212+type Context {
1313+ Context(status_subject: process.Subject(status.Msg))
1414+}
1515+1616+pub fn supervised(status_subject: process.Subject(status.Msg)) {
1717+ let secret_key_base = wisp.random_string(64)
1818+1919+ let ctx = Context(status_subject:)
2020+2121+ wisp_mist.handler(fn(req) { handle_request(req, ctx) }, secret_key_base)
2222+ |> mist.new
2323+ |> mist.port(8000)
2424+ |> mist.supervised
2525+}
2626+2727+fn handle_request(req: wisp.Request, ctx: Context) -> wisp.Response {
2828+ case wisp.path_segments(req) {
2929+ ["now-playing"] -> now_playing(req, ctx)
3030+ _ -> wisp.not_found()
3131+ }
3232+}
3333+3434+fn now_playing(_req: wisp.Request, ctx: Context) -> wisp.Response {
3535+ let now_playing = actor.call(ctx.status_subject, 250, status.Get)
3636+ let resp = status_to_json(now_playing)
3737+ wisp.json_response(json.to_string(resp), 200)
3838+}
3939+4040+fn status_to_json(
4141+ currently_playing: Option(status.CurrentlyPlaying),
4242+) -> json.Json {
4343+ case currently_playing {
4444+ None -> {
4545+ json.object([#("playing", json.bool(False))])
4646+ }
4747+ Some(currently_playing) -> {
4848+ let status.CurrentlyPlaying(song:, set_at:) = currently_playing
4949+ let status.Song(artists:, duration_ms:, progress_ms:, url:, name:) = song
5050+5151+ // add the time since progress was recorded to the progress bar :3
5252+ // this doesn't make a huge difference actually unless i increase the refresh timeout thing
5353+ // 0 <= progress <= duration_ms
5454+ // NOTE: set_at is in SECONDS!
5555+ let interpolated_progress =
5656+ int.min(
5757+ duration_ms,
5858+ progress_ms + { int.max(0, util.now() - set_at) * 1000 },
5959+ )
6060+6161+ json.object([
6262+ #("playing", json.bool(True)),
6363+ #("artists", json.array(artists, artist_to_json)),
6464+ #("duration_ms", json.int(duration_ms)),
6565+ #("progress_ms", json.int(interpolated_progress)),
6666+ #("url", json.string(url)),
6767+ #("name", json.string(name)),
6868+ ])
6969+ }
7070+ }
7171+}
7272+7373+fn artist_to_json(artist: status.Artist) -> json.Json {
7474+ let status.Artist(url:, name:) = artist
7575+ json.object([
7676+ #("url", json.string(url)),
7777+ #("name", json.string(name)),
7878+ ])
7979+}