From bfa7cc10467d03c484a93e60dc51c18ca4ac8d15 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sat, 4 Jul 2026 11:04:45 +0800 Subject: [PATCH] stream: show the source provider of the current track Co-Authored-By: Claude Opus 4.8 --- ingest/radieo/api.py | 8 +++++++- stream/index.html | 26 +++++++++++++++++++++++++- stream/radio.liq | 11 ++++++++--- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/ingest/radieo/api.py b/ingest/radieo/api.py index cacc9d6..f55b0c9 100644 --- a/ingest/radieo/api.py +++ b/ingest/radieo/api.py @@ -29,7 +29,13 @@ def annotate_uri(path: Path, track: Track) -> str: def esc(value: str) -> str: return value.replace("\\", "\\\\").replace('"', '\\"') - fields = [f'title="{esc(track.title)}"', f'artist="{esc(track.artist)}"'] + fields = [ + f'title="{esc(track.title)}"', + f'artist="{esc(track.artist)}"', + # Provider that produced the track (subsonic, ytdlp…), surfaced by the + # stream so the player can show a discreet source indicator. + f'origin="{esc(track.origin)}"', + ] # Web page the track was pulled from, so the player can link back to the # source. Only http(s) locators qualify (yt-dlp tracks); a Subsonic song id # is opaque and points at no public page. diff --git a/stream/index.html b/stream/index.html index fa9b90f..d6cbf68 100644 --- a/stream/index.html +++ b/stream/index.html @@ -37,6 +37,10 @@ color: #9b8cff; margin-bottom: 1.75rem; } .np-label { font-size: .7rem; letter-spacing: .2em; text-transform: uppercase; color: #7d768f; margin-bottom: .5rem; } + /* Provenance discrète du morceau : d'où vient la piste (OpenSubsonic, + YouTube…). Ton effacé, glissée à la suite de l'état « en cours ». */ + .provider { color: #6b6480; } + .provider::before { content: "·"; margin: 0 .35em; color: #4a4560; } .title { font-size: 1.55rem; font-weight: 650; line-height: 1.25; word-wrap: break-word; } .title a { color: inherit; text-decoration: none; transition: color .15s; } @@ -98,7 +102,7 @@
-
Préchargement
+
Préchargement
@@ -151,8 +155,20 @@ const titleEl = document.getElementById("title"); const artistEl = document.getElementById("artist"); const npLabel = document.getElementById("npLabel"); + const providerEl = document.getElementById("provider"); const player = document.getElementById("player"); + // Noms d'affichage des providers d'ingestion (champ `origin` du morceau). + // Inconnu → on réutilise tel quel, faute de mieux. + const PROVIDER_NAMES = { + subsonic: "OpenSubsonic", + ytdlp: "YouTube", + listenbrainz: "ListenBrainz", + // Filet de secours : morceau rejoué depuis le cache local (déjà diffusé), + // quand l'ingest n'a rien à proposer (démarrage, panne…). + cache: "le cache local", + }; + // Tant que le buffer de préchargement (PREFETCH côté ingest) n'est pas // rempli, on affiche « Préchargement N/M » plutôt que « en cours ». L'info // vient du daemon d'ingestion, relayée par le stream via /ingest/status. @@ -279,6 +295,14 @@ ? `${escapeHtml(label)}` : escapeHtml(label); artistEl.textContent = a; + // Provenance discrète : nom lisible du provider, masqué s'il est absent. + const origin = (m.origin || "").trim(); + if (origin) { + providerEl.textContent = "via " + (PROVIDER_NAMES[origin] || origin); + providerEl.hidden = false; + } else { + providerEl.hidden = true; + } document.title = t ? (a ? `${t} — ${a} · ${STATION_NAME}` : `${t} · ${STATION_NAME}`) : STATION_NAME; updateMediaSession(t, a); } catch (e) { /* keep last known values */ } diff --git a/stream/radio.liq b/stream/radio.liq index 40ac29b..da7cb3e 100644 --- a/stream/radio.liq +++ b/stream/radio.liq @@ -71,6 +71,11 @@ backup = playlist( mode="randomize", reload_mode="watch", mime_type="audio/x-mpegurl", check_next=audio_only, fallback_file ) +# Les morceaux du secours sont rejoués depuis le cache local (déjà diffusés) et +# n'ont pas d'annotation d'origine. On les étiquette explicitement pour que le +# player affiche « via le cache local » plutôt que rien quand on retombe sur ce +# filet (ingest injoignable, file vide pendant le préchargement…). +backup = metadata.map(fun(_) -> [("origin", "cache")], backup) # File de rejeu ponctuel : normalement vide (donc non prête, transparente). Le # endpoint /restart-track y pousse le morceau courant pour le rejouer depuis le @@ -198,8 +203,8 @@ radio.on_metadata( now_playing := m # `file` : nom de base du fichier à l'antenne, servant de jeton de # téléchargement (/download?file=…). Vide si la métadonnée manque. - entry = {title=m["title"], artist=m["artist"], url=m["url"], file=path.basename(m["filename"])} - head = list.hd(default={title="", artist="", url="", file=""}, history()) + entry = {title=m["title"], artist=m["artist"], url=m["url"], origin=m["origin"], file=path.basename(m["filename"])} + head = list.hd(default={title="", artist="", url="", origin="", file=""}, history()) is_dup = head.title == entry.title and head.artist == entry.artist if not is_dup and (entry.title != "" or entry.artist != "") then history := list.prefix(history_max, list.add(entry, history())) @@ -239,7 +244,7 @@ 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"]}) + resp.json({title=m["title"], artist=m["artist"], url=m["url"], origin=m["origin"]}) end )