stream: web player with now-playing
Serve a small web page at http://<host>:8000/ (stream/index.html) from the Liquidsoap harbor, alongside the /radio.mp3 stream. It shows the track currently on air and refreshes it from a /nowplaying JSON endpoint, fed by the broadcast source's live metadata — accurate even though the ingest daemon runs a track ahead (prefetch). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
3bd7edbb16
commit
f4eaf8e7d1
3 changed files with 92 additions and 0 deletions
|
|
@ -1,5 +1,6 @@
|
|||
FROM savonet/liquidsoap:v2.4.5
|
||||
|
||||
COPY radio.liq /etc/liquidsoap/radio.liq
|
||||
COPY index.html /etc/liquidsoap/index.html
|
||||
|
||||
CMD ["/etc/liquidsoap/radio.liq"]
|
||||
|
|
|
|||
66
stream/index.html
Normal file
66
stream/index.html
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>radieo</title>
|
||||
<style>
|
||||
:root { color-scheme: dark; }
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0; min-height: 100vh; display: flex; align-items: center;
|
||||
justify-content: center; font-family: system-ui, sans-serif;
|
||||
background: radial-gradient(circle at 30% 20%, #2a2140, #0d0b14 70%);
|
||||
color: #f2f0f7;
|
||||
}
|
||||
.card {
|
||||
width: min(90vw, 420px); padding: 2.5rem 2rem;
|
||||
background: rgba(255,255,255,.04); border: 1px solid rgba(255,255,255,.08);
|
||||
border-radius: 18px; box-shadow: 0 20px 60px rgba(0,0,0,.45);
|
||||
backdrop-filter: blur(8px); text-align: center;
|
||||
}
|
||||
.logo { font-size: .8rem; letter-spacing: .35em; text-transform: uppercase;
|
||||
color: #9b8cff; margin-bottom: 1.75rem; }
|
||||
.np-label { font-size: .7rem; letter-spacing: .2em; text-transform: uppercase;
|
||||
color: #7d768f; margin-bottom: .5rem; }
|
||||
.title { font-size: 1.55rem; font-weight: 650; line-height: 1.25;
|
||||
word-wrap: break-word; }
|
||||
.artist { margin-top: .35rem; font-size: 1.05rem; color: #b8b2c8; }
|
||||
audio { width: 100%; margin-top: 2rem; }
|
||||
.dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%;
|
||||
background: #4ade80; margin-right: .4rem; vertical-align: middle;
|
||||
box-shadow: 0 0 0 0 rgba(74,222,128,.6); animation: pulse 2s infinite; }
|
||||
@keyframes pulse {
|
||||
0% { box-shadow: 0 0 0 0 rgba(74,222,128,.5); }
|
||||
70% { box-shadow: 0 0 0 10px rgba(74,222,128,0); }
|
||||
100% { box-shadow: 0 0 0 0 rgba(74,222,128,0); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="card">
|
||||
<div class="logo">◈ radieo</div>
|
||||
<div class="np-label"><span class="dot"></span>en cours</div>
|
||||
<div class="title" id="title">—</div>
|
||||
<div class="artist" id="artist"></div>
|
||||
<audio id="player" controls autoplay preload="none" src="/radio.mp3"></audio>
|
||||
</main>
|
||||
<script>
|
||||
const titleEl = document.getElementById("title");
|
||||
const artistEl = document.getElementById("artist");
|
||||
async function poll() {
|
||||
try {
|
||||
const r = await fetch("/nowplaying", { cache: "no-store" });
|
||||
const m = await r.json();
|
||||
const t = (m.title || "").trim();
|
||||
const a = (m.artist || "").trim();
|
||||
titleEl.textContent = t || "—";
|
||||
artistEl.textContent = a;
|
||||
document.title = t ? (a ? `${t} — ${a} · radieo` : `${t} · radieo`) : "radieo";
|
||||
} catch (e) { /* keep last known values */ }
|
||||
}
|
||||
poll();
|
||||
setInterval(poll, 5000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
# La source principale est pilotée par le daemon d'ingestion via GET /next.
|
||||
# Le dossier /cache sert de secours quand le daemon n'a rien à proposer
|
||||
# (daemon indisponible, file momentanément vide…). Si tout est vide : silence.
|
||||
# Le harbor sert aussi une petite page web (/) et le titre courant (/nowplaying).
|
||||
|
||||
# --- Journalisation : tout sur la sortie standard (pratique en conteneur) ---
|
||||
settings.log.stdout := true
|
||||
|
|
@ -59,6 +60,11 @@ radio = crossfade(duration=3.0, fade_in=3.0, fade_out=3.0, radio)
|
|||
# mksafe garantit un flux continu : silence plutôt que plantage si tout est vide.
|
||||
radio = mksafe(radio)
|
||||
|
||||
# --- Métadonnées « en cours de lecture » -----------------------------------
|
||||
# On mémorise les dernières métadonnées vues sur le flux réellement diffusé.
|
||||
now_playing = ref([])
|
||||
radio.on_metadata(synchronous=false, fun(m) -> now_playing := m)
|
||||
|
||||
# --- Sortie : flux MP3 sur http://<hote>:8000/radio.mp3 ---
|
||||
output.harbor(
|
||||
%mp3(bitrate=192),
|
||||
|
|
@ -66,3 +72,22 @@ output.harbor(
|
|||
mount="radio.mp3",
|
||||
radio
|
||||
)
|
||||
|
||||
# --- Page web et API de lecture (mêmes port/harbor que le flux) ---
|
||||
home_html = file.contents("/etc/liquidsoap/index.html")
|
||||
|
||||
harbor.http.register(
|
||||
port=8000, method="GET", "/",
|
||||
fun(_, resp) -> begin
|
||||
resp.content_type("text/html; charset=utf-8")
|
||||
resp.html(home_html)
|
||||
end
|
||||
)
|
||||
|
||||
harbor.http.register(
|
||||
port=8000, method="GET", "/nowplaying",
|
||||
fun(_, resp) -> begin
|
||||
m = now_playing()
|
||||
resp.json({title=m["title"], artist=m["artist"]})
|
||||
end
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue