Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add spinitron API functions. #4158

Merged
merged 3 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
277 changes: 277 additions & 0 deletions src/libs/extra/spinitron.liq
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
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=false,
~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(fun (b) -> b ? "1" : "0" , (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 ~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

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("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)

res = spinitron.submit(%argsof(spinitron.submit))

print(res)

res
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.severe(
label=source.id(s),
"Field \"song\" or \"title\" missing, skipping metadata spinitron \
submission."
)
elsif
m["artist"] == ""
then
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.important(
label=source.id(s),
"Successfully submitted spin from metadata"
)
catch err do
log.severe(
label=source.id(s),
"Error while submitting spin from metadata: #{err}"
)
end
end
end

source.on_metadata(id=id, s, on_metadata)
end
1 change: 1 addition & 0 deletions src/libs/stdlib.liq
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading