From 1c8d45c96939c921e5ebfd26ff3c3158e07060c6 Mon Sep 17 00:00:00 2001 From: Romain Beauxis Date: Fri, 4 Oct 2024 13:46:51 -0500 Subject: [PATCH 1/3] Add spiniton API functions. --- src/libs/extra/spinitron.liq | 259 +++++++++++++++++++++++++++++++++++ src/libs/stdlib.liq | 1 + 2 files changed, 260 insertions(+) create mode 100644 src/libs/extra/spinitron.liq diff --git a/src/libs/extra/spinitron.liq b/src/libs/extra/spinitron.liq new file mode 100644 index 0000000000..3dd0a8539f --- /dev/null +++ b/src/libs/extra/spinitron.liq @@ -0,0 +1,259 @@ +let spinitron = {submit=()} + +# Submit a track to the spinitron track system +# and return the raw response. +# @category Interaction +# @flag extra +# @param ~api_key API key +def spinitron.submit.raw( + ~host="https://spinitron.com/api", + ~api_key, + ~live=true, + ~start=null(), + ~duration=null(), + ~artist, + ~release=null(), + ~label=null(), + ~genre=null(), + ~song, + ~composer=null(), + ~isrc=null() +) = + params = [("song", song), ("artist", artist)] + + def fold_optional_string_params(params, param) = + let (label, param) = param + if + null.defined(param) + then + [(label, null.get(param)), ...params] + else + params + end + end + + params = + list.fold( + fold_optional_string_params, + params, + [ + ("live", null.map(string, (live : bool?))), + ("start", start), + ("duration", null.map(string, (duration : int?))), + ("release", release), + ("label", label), + ("genre", genre), + ("composer", composer), + ("isrc", isrc) + ] + ) + + def encode_param(param) = + let (label, param) = param + "#{label}=#{url.encode(param)}" + end + + params = string.concat(separator="&", list.map(encode_param, params)) + + http.post( + data=params, + headers= + [ + ("Accept", "application/json"), + ("Content-Type", "application/x-www-form-urlencoded"), + ( + "Authorization", + "Bearer #{(api_key : string)}" + ) + ], + "#{host}/spins" + ) +end + +# Submit a track to the spinitron track system +# and return the parsed response +# @category Interaction +# @flag extra +# @param ~api_key API key +def replaces spinitron.submit(%argsof(spinitron.submit.raw)) = + resp = spinitron.submit.raw(%argsof(spinitron.submit.raw)) + + if + resp.status_code == 201 + then + let json.parse (resp : + { + id: int, + playlist_id: int, + "start" as spin_start: string, + "end" as spin_end: string?, + duration: int?, + timezone: string?, + image: string?, + classical: bool, + artist: string, + "artist-custom" as artist_custom: string?, + composer: string?, + release: string?, + "release-custom" as release_custom: string?, + va: bool, + label: string?, + "label-custom" as label_custom: string?, + released: int?, + medium: string?, + genre: string?, + song: string, + note: string?, + request: bool, + local: bool, + new: bool, + work: string?, + conductor: string?, + performers: string?, + ensemble: string?, + "catalog-number" as catalog_number: string?, + isrc: string?, + upc: string?, + iswc: string?, + "_links" as links: {self: {href: string}?, playlist: {href: string}?}? + } + ) = resp + + resp + elsif + resp.status_code == 422 + then + let json.parse (errors : [{field: string, message: string}]) = resp + + errors = + list.map( + fun (p) -> + begin + let {field, message} = p + "#{field}: #{message}" + end, + errors + ) + + errors = + string.concat( + separator= + ", ", + errors + ) + + error.raise( + error.raise( + error.http, + "Invalid fields: #{errors}" + ) + ) + else + let json.parse ({name, message, code, status, type} : + {name: string, message: string, code: int, status: int, type: string?} + ) = resp + + type = type ?? "undefined" + + error.raise( + error.raise( + error.http, + "#{name}: #{message} (code: #{code}, status: #{status}, type: #{type})" + ) + ) + end +end + +# Submit a spin using the given metadata to the spinitron track system +# and return the parsed response. `artist` and `song` (or `title`) must +# be present either as metadata or as optional argument. +# @category Interaction +# @flag extra +# @param m Metadata to submit. Overrides optional arguments when present. +# @param ~api_key API key +def spinitron.submit.metadata( + %argsof(spinitron.submit[!artist,!song]), + ~artist=null(), + ~song=null(), + m +) = + def conv_opt_arg(convert, label, default) = + list.assoc.mem(label, m) ? convert(m[label]) : default + end + + opt_arg = + fun (label, default) -> conv_opt_arg(fun (x) -> null(x), label, default) + + live = conv_opt_arg(bool_of_string, "live", live) + start = opt_arg("start", start) + duration = conv_opt_arg(int_of_string, "duration", duration) + artist = opt_arg("artist", artist) + release = opt_arg("release", release) + label = opt_arg("label", label) + genre = opt_arg("genre", genre) + song = opt_arg("title", song) + song = opt_arg("song", song) + composer = opt_arg("composer", composer) + isrc = opt_arg("isrc", isrc) + + if + artist == null() or song == null() + then + error.raise( + error.invalid, + "Both \"artist\" and \"song\" (or \"title\" metadata) must be provided!" + ) + end + + artist = null.get(artist) + song = null.get(song) + + spinitron.submit(%argsof(spinitron.submit)) +end + +# Specialized version of `source.on_metadata` that submits spins using +# the source's metadata to the spinitron track system. `artist` and `song` +# (or `title`) must be present either as metadata or as optional argument. +# @category Interaction +# @flag extra +# @param m Metadata to submit. Overrides optional arguments when present. +# @param ~api_key API key +def spinitron.submit.on_metadata( + ~id=null(), + %argsof(spinitron.submit.metadata), + s +) = + def on_metadata(m) = + if + m["title"] == "" and m["song"] == "" + then + log.important( + label=source.id(s), + "Field \"song\" or \"title\" missing, skipping metadata spinitron \ + submission." + ) + elsif + m["artist"] == "" + then + log.important( + label=source.id(s), + "Field \"artist\" missing, skipping metadata spinitron submission." + ) + else + try + ignore(spinitron.submit.metadata(%argsof(spinitron.submit.metadata), m)) + log.info( + label=source.id(s), + "Successfully submitted spin from metadata" + ) + catch err do + log.important( + label=source.id(s), + "Error while submitting spin from metadata: #{err}" + ) + end + end + end + + source.on_metadata(id=id, s, on_metadata) +end diff --git a/src/libs/stdlib.liq b/src/libs/stdlib.liq index c58ae30c42..dacb255f90 100644 --- a/src/libs/stdlib.liq +++ b/src/libs/stdlib.liq @@ -54,6 +54,7 @@ %include_extra "extra/interactive.liq" %include_extra "extra/visualization.liq" %include_extra "extra/openai.liq" +%include_extra "extra/spinitron.liq" %include_extra "extra/metadata.liq" %include_extra "extra/fades.liq" %include_extra "extra/video.liq" From f83e3777e20e3d59cc6fb9831ae9944af53ce0fe Mon Sep 17 00:00:00 2001 From: Romain Beauxis Date: Mon, 7 Oct 2024 11:09:05 -0400 Subject: [PATCH 2/3] Cleanup --- src/libs/extra/spinitron.liq | 44 +++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/src/libs/extra/spinitron.liq b/src/libs/extra/spinitron.liq index 3dd0a8539f..3dc050ce7f 100644 --- a/src/libs/extra/spinitron.liq +++ b/src/libs/extra/spinitron.liq @@ -8,7 +8,7 @@ let spinitron = {submit=()} def spinitron.submit.raw( ~host="https://spinitron.com/api", ~api_key, - ~live=true, + ~live=false, ~start=null(), ~duration=null(), ~artist, @@ -37,7 +37,7 @@ def spinitron.submit.raw( fold_optional_string_params, params, [ - ("live", null.map(string, (live : bool?))), + ("live", null.map(fun (b) -> b ? "1" : "0" , (live : bool?))), ("start", start), ("duration", null.map(string, (duration : int?))), ("release", release), @@ -90,13 +90,13 @@ def replaces spinitron.submit(%argsof(spinitron.submit.raw)) = duration: int?, timezone: string?, image: string?, - classical: bool, + classical: bool?, artist: string, "artist-custom" as artist_custom: string?, composer: string?, release: string?, "release-custom" as release_custom: string?, - va: bool, + va: bool?, label: string?, "label-custom" as label_custom: string?, released: int?, @@ -104,9 +104,9 @@ def replaces spinitron.submit(%argsof(spinitron.submit.raw)) = genre: string?, song: string, note: string?, - request: bool, - local: bool, - new: bool, + request: bool?, + local: bool?, + new: bool?, work: string?, conductor: string?, performers: string?, @@ -170,13 +170,28 @@ end # @category Interaction # @flag extra # @param m Metadata to submit. Overrides optional arguments when present. +# @param ~mapper Metadata mapper that can be used to map metadata fields to spinitron's expected. \ +# Returned metadata are added to the submitted metadata. By default, `title` is \ +# mapped to `song` and `album` to `release` if neither of those passed otherwise. # @param ~api_key API key def spinitron.submit.metadata( %argsof(spinitron.submit[!artist,!song]), + ~mapper=( + fun (m) -> + [ + ...(m["song"] != "" or m["title"] == "" ? [] : [("song", m["title"])] ), + ...( + m["release"] != "" or m["album"] == "" + ? [] : [("release", m["album"])] + ) + ] + ), ~artist=null(), ~song=null(), m ) = + m = [...m, ...mapper(m)] + def conv_opt_arg(convert, label, default) = list.assoc.mem(label, m) ? convert(m[label]) : default end @@ -191,7 +206,6 @@ def spinitron.submit.metadata( release = opt_arg("release", release) label = opt_arg("label", label) genre = opt_arg("genre", genre) - song = opt_arg("title", song) song = opt_arg("song", song) composer = opt_arg("composer", composer) isrc = opt_arg("isrc", isrc) @@ -208,7 +222,11 @@ def spinitron.submit.metadata( artist = null.get(artist) song = null.get(song) - spinitron.submit(%argsof(spinitron.submit)) + res = spinitron.submit(%argsof(spinitron.submit)) + + print(res) + + res end # Specialized version of `source.on_metadata` that submits spins using @@ -227,7 +245,7 @@ def spinitron.submit.on_metadata( if m["title"] == "" and m["song"] == "" then - log.important( + log.severe( label=source.id(s), "Field \"song\" or \"title\" missing, skipping metadata spinitron \ submission." @@ -235,19 +253,19 @@ def spinitron.submit.on_metadata( elsif m["artist"] == "" then - log.important( + log.severe( label=source.id(s), "Field \"artist\" missing, skipping metadata spinitron submission." ) else try ignore(spinitron.submit.metadata(%argsof(spinitron.submit.metadata), m)) - log.info( + log.important( label=source.id(s), "Successfully submitted spin from metadata" ) catch err do - log.important( + log.severe( label=source.id(s), "Error while submitting spin from metadata: #{err}" ) From 0d207c649475f045622329b5379a8a197fa82eef Mon Sep 17 00:00:00 2001 From: Romain Beauxis Date: Mon, 7 Oct 2024 11:10:06 -0400 Subject: [PATCH 3/3] Changes entry. --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 3fe09d0f5c..0a67b1ccd6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -23,6 +23,7 @@ New: - Optimized runtime (#3927, #3928, #3919) - Added `finally` to execute code regardless of whether or not an exception is raised (see: #3895 for more details). +- Added support for Spinitron submission API (#4158) - Removed gstreamer support. Gstreamer's architecture was never a good fit for us and created a huge maintenance and debugging burden and it had been marked as deprecated for a while. Most, if not all of its features should be available using