stream: scrobble listened tracks to ListenBrainz
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
The web player decides when a track counts as listened (caught near its start and heard to ~90%, capped at 4 min) and triggers POST /scrobble. The token stays server-side (RADIEO_LISTENBRAINZ_TOKEN), submitting the listen with the canonical MusicBrainz MBID when available. Each airing is deduplicated so multiple tabs submit it once. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
b04e717f40
commit
d302cf1c88
7 changed files with 163 additions and 2 deletions
|
|
@ -91,7 +91,83 @@ harbor.http.register(
|
|||
port=8000, method="GET", "/nowplaying",
|
||||
fun(_, resp) -> begin
|
||||
m = now_playing()
|
||||
resp.json({title=m["title"], artist=m["artist"], url=m["url"], origin=m["origin"]})
|
||||
# `file` : jeton du morceau courant (nom de base), que le player renvoie à
|
||||
# /scrobble pour prouver qu'il parle bien du titre à l'antenne.
|
||||
# `duration`/`position` (secondes, arrondies) permettent au player de décider
|
||||
# d'un scrobble « écouté à 90 %, démarré au début ». duration=0 si inconnue.
|
||||
resp.json({
|
||||
title=m["title"], artist=m["artist"], url=m["url"], origin=m["origin"],
|
||||
file=path.basename(m["filename"]),
|
||||
duration=int_of_float(source.duration(radio)),
|
||||
position=int_of_float(source.elapsed(radio))
|
||||
})
|
||||
end
|
||||
)
|
||||
|
||||
# --- Scrobbling ListenBrainz (déclenché par le player) ----------------------
|
||||
# Le player décide QUAND un morceau est vraiment « écouté » (démarré au début et
|
||||
# entendu à ~90 %) et poste ici. Le token reste côté serveur (jamais exposé au
|
||||
# navigateur) : on lit RADIEO_LISTENBRAINZ_TOKEN dans l'environnement. Vide =
|
||||
# fonction désactivée. On soumet vers ListenBrainz avec le MBID canonique quand
|
||||
# l'ingest en a trouvé un (métadonnée musicbrainz_trackid), sinon artiste+titre.
|
||||
listenbrainz_submit_url = "https://api.listenbrainz.org/1/submit-listens"
|
||||
|
||||
# Envoie un « listen » (single) ou un « playing_now » pour les métadonnées `m`.
|
||||
# json.stringify(s) échappe proprement une chaîne (guillemets, unicode…).
|
||||
def submit_listen(playing_now, m) =
|
||||
token = environment.get(default="", "RADIEO_LISTENBRAINZ_TOKEN")
|
||||
title = m["title"]
|
||||
artist = m["artist"]
|
||||
mbid = m["musicbrainz_trackid"]
|
||||
if token != "" and title != "" and artist != "" then
|
||||
info = "\"submission_client\":\"radieo\""
|
||||
info = if mbid != "" then "#{info},\"recording_mbid\":#{json.stringify(mbid)}" else info end
|
||||
track_meta = "{\"artist_name\":#{json.stringify(artist)},\"track_name\":#{json.stringify(title)},\"additional_info\":{#{info}}}"
|
||||
body =
|
||||
if playing_now then
|
||||
"{\"listen_type\":\"playing_now\",\"payload\":[{\"track_metadata\":#{track_meta}}]}"
|
||||
else
|
||||
"{\"listen_type\":\"single\",\"payload\":[{\"listened_at\":#{int_of_float(time())},\"track_metadata\":#{track_meta}}]}"
|
||||
end
|
||||
ignore(http.post(
|
||||
data=body, timeout=8.0,
|
||||
headers=[("Authorization", "Token #{token}"), ("Content-Type", "application/json")],
|
||||
listenbrainz_submit_url
|
||||
))
|
||||
end
|
||||
end
|
||||
|
||||
# POST /scrobble?file=<jeton>[&type=playing_now]
|
||||
# - file doit correspondre au titre courant (sinon 409 : le titre a changé) ;
|
||||
# - un titre sans artiste/titre (jingle, inconnu) est ignoré silencieusement ;
|
||||
# - type=playing_now : « en cours d'écoute », renvoyé à chaque nouveau titre ;
|
||||
# - sinon (single) : scrobble définitif, dédupliqué par le flag `scrobbled`
|
||||
# pour ne compter le passage qu'une fois même si plusieurs auditeurs postent.
|
||||
harbor.http.register(
|
||||
port=8000, method="POST", "/scrobble",
|
||||
fun(req, resp) -> begin
|
||||
m = now_playing()
|
||||
cur = path.basename(m["filename"])
|
||||
want = list.assoc(default="", "file", req.query)
|
||||
kind = list.assoc(default="single", "type", req.query)
|
||||
playing_now = kind == "playing_now"
|
||||
if environment.get(default="", "RADIEO_LISTENBRAINZ_TOKEN") == "" then
|
||||
resp.json({scrobbled=false, reason="disabled"})
|
||||
elsif want == "" or want != cur then
|
||||
resp.status_code(409)
|
||||
resp.json({scrobbled=false, reason="stale"})
|
||||
elsif m["title"] == "" or m["artist"] == "" then
|
||||
resp.json({scrobbled=false, reason="ignored"})
|
||||
elsif playing_now then
|
||||
submit_listen(true, m)
|
||||
resp.json({scrobbled=true, playing_now=true})
|
||||
elsif scrobbled() then
|
||||
resp.json({scrobbled=false, reason="duplicate"})
|
||||
else
|
||||
scrobbled := true
|
||||
submit_listen(false, m)
|
||||
resp.json({scrobbled=true})
|
||||
end
|
||||
end
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue