stream: show the source provider of the current track
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
96a1ba89e6
commit
bfa7cc1046
3 changed files with 40 additions and 5 deletions
|
|
@ -29,7 +29,13 @@ def annotate_uri(path: Path, track: Track) -> str:
|
||||||
def esc(value: str) -> str:
|
def esc(value: str) -> str:
|
||||||
return value.replace("\\", "\\\\").replace('"', '\\"')
|
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
|
# 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
|
# source. Only http(s) locators qualify (yt-dlp tracks); a Subsonic song id
|
||||||
# is opaque and points at no public page.
|
# is opaque and points at no public page.
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,10 @@
|
||||||
color: #9b8cff; margin-bottom: 1.75rem; }
|
color: #9b8cff; margin-bottom: 1.75rem; }
|
||||||
.np-label { font-size: .7rem; letter-spacing: .2em; text-transform: uppercase;
|
.np-label { font-size: .7rem; letter-spacing: .2em; text-transform: uppercase;
|
||||||
color: #7d768f; margin-bottom: .5rem; }
|
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;
|
.title { font-size: 1.55rem; font-weight: 650; line-height: 1.25;
|
||||||
word-wrap: break-word; }
|
word-wrap: break-word; }
|
||||||
.title a { color: inherit; text-decoration: none; transition: color .15s; }
|
.title a { color: inherit; text-decoration: none; transition: color .15s; }
|
||||||
|
|
@ -98,7 +102,7 @@
|
||||||
<div class="bg" id="bg"></div>
|
<div class="bg" id="bg"></div>
|
||||||
<main class="card">
|
<main class="card">
|
||||||
<div class="logo" id="stationName">◈</div>
|
<div class="logo" id="stationName">◈</div>
|
||||||
<div class="np-label"><span class="dot"></span><span id="npLabel">Préchargement</span></div>
|
<div class="np-label"><span class="dot"></span><span id="npLabel">Préchargement</span><span id="provider" class="provider" hidden></span></div>
|
||||||
<div class="title" id="title">—</div>
|
<div class="title" id="title">—</div>
|
||||||
<div class="artist" id="artist"></div>
|
<div class="artist" id="artist"></div>
|
||||||
<audio id="player" controls autoplay preload="none"></audio>
|
<audio id="player" controls autoplay preload="none"></audio>
|
||||||
|
|
@ -151,8 +155,20 @@
|
||||||
const titleEl = document.getElementById("title");
|
const titleEl = document.getElementById("title");
|
||||||
const artistEl = document.getElementById("artist");
|
const artistEl = document.getElementById("artist");
|
||||||
const npLabel = document.getElementById("npLabel");
|
const npLabel = document.getElementById("npLabel");
|
||||||
|
const providerEl = document.getElementById("provider");
|
||||||
const player = document.getElementById("player");
|
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
|
// 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
|
// 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.
|
// vient du daemon d'ingestion, relayée par le stream via /ingest/status.
|
||||||
|
|
@ -279,6 +295,14 @@
|
||||||
? `<a href="${escapeHtml(u)}" target="_blank" rel="noopener">${escapeHtml(label)}</a>`
|
? `<a href="${escapeHtml(u)}" target="_blank" rel="noopener">${escapeHtml(label)}</a>`
|
||||||
: escapeHtml(label);
|
: escapeHtml(label);
|
||||||
artistEl.textContent = a;
|
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;
|
document.title = t ? (a ? `${t} — ${a} · ${STATION_NAME}` : `${t} · ${STATION_NAME}`) : STATION_NAME;
|
||||||
updateMediaSession(t, a);
|
updateMediaSession(t, a);
|
||||||
} catch (e) { /* keep last known values */ }
|
} catch (e) { /* keep last known values */ }
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,11 @@ backup = playlist(
|
||||||
mode="randomize", reload_mode="watch", mime_type="audio/x-mpegurl",
|
mode="randomize", reload_mode="watch", mime_type="audio/x-mpegurl",
|
||||||
check_next=audio_only, fallback_file
|
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
|
# 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
|
# endpoint /restart-track y pousse le morceau courant pour le rejouer depuis le
|
||||||
|
|
@ -198,8 +203,8 @@ radio.on_metadata(
|
||||||
now_playing := m
|
now_playing := m
|
||||||
# `file` : nom de base du fichier à l'antenne, servant de jeton de
|
# `file` : nom de base du fichier à l'antenne, servant de jeton de
|
||||||
# téléchargement (/download?file=…). Vide si la métadonnée manque.
|
# 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"])}
|
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="", file=""}, history())
|
head = list.hd(default={title="", artist="", url="", origin="", file=""}, history())
|
||||||
is_dup = head.title == entry.title and head.artist == entry.artist
|
is_dup = head.title == entry.title and head.artist == entry.artist
|
||||||
if not is_dup and (entry.title != "" or entry.artist != "") then
|
if not is_dup and (entry.title != "" or entry.artist != "") then
|
||||||
history := list.prefix(history_max, list.add(entry, history()))
|
history := list.prefix(history_max, list.add(entry, history()))
|
||||||
|
|
@ -239,7 +244,7 @@ harbor.http.register(
|
||||||
port=8000, method="GET", "/nowplaying",
|
port=8000, method="GET", "/nowplaying",
|
||||||
fun(_, resp) -> begin
|
fun(_, resp) -> begin
|
||||||
m = now_playing()
|
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
|
end
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue