stream: show a history of aired tracks (/history)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
nemunaire 2026-07-02 23:56:32 +08:00
commit 7fc372f18d
2 changed files with 62 additions and 4 deletions

View file

@ -51,6 +51,14 @@
}
.actions button:hover, .actions a:hover { background: rgba(155,140,255,.3); }
.actions button:disabled { opacity: .5; cursor: default; }
.history { margin-top: 1.75rem; text-align: left; }
.history h2 { font-size: .7rem; letter-spacing: .2em; text-transform: uppercase;
color: #7d768f; margin: 0 0 .4rem; font-weight: 600; }
.history ul { list-style: none; margin: 0; padding: 0; }
.history li { padding: .45rem 0; border-top: 1px solid rgba(255,255,255,.06); }
.history .h-title { color: #e8e4f2; font-size: .92rem; }
.history .h-artist { color: #8b849c; font-size: .8rem; margin-top: .1rem; }
.history .empty { color: #6b6480; font-size: .85rem; padding: .45rem 0; }
.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; }
@ -75,6 +83,10 @@
<input id="streamUrl" type="text" readonly>
<button id="copyBtn" type="button">Copier</button>
</div>
<section class="history">
<h2>Historique</h2>
<ul id="historyList"></ul>
</section>
</main>
<script>
const titleEl = document.getElementById("title");
@ -109,6 +121,30 @@
document.title = t ? (a ? `${t} — ${a} · radieo` : `${t} · radieo`) : "radieo";
} catch (e) { /* keep last known values */ }
}
// Historique des titres passés. Le premier élément renvoyé est le morceau
// en cours (déjà affiché plus haut), on ne montre donc que les précédents.
const historyList = document.getElementById("historyList");
const escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => (
{ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]
));
async function pollHistory() {
try {
const r = await fetch("/history", { cache: "no-store" });
const items = await r.json();
const past = Array.isArray(items) ? items.slice(1) : [];
if (past.length === 0) {
historyList.innerHTML = '<li class="empty"></li>';
return;
}
historyList.innerHTML = past.map((m) => {
const t = escapeHtml((m.title || "").trim() || "—");
const a = (m.artist || "").trim();
const artist = a ? `<div class="h-artist">${escapeHtml(a)}</div>` : "";
return `<li><div class="h-title">${t}</div>${artist}</li>`;
}).join("");
} catch (e) { /* keep last known values */ }
}
// Lien nu du flux, à ouvrir dans un lecteur externe (VLC…).
const shareUrl = location.origin + "/radio.mp3";
const urlEl = document.getElementById("streamUrl");
@ -132,11 +168,12 @@
skipBtn.disabled = true;
try { await fetch("/skip", { method: "POST" }); } catch (e) { /* ignore */ }
// Laisser le temps à la bascule, puis rafraîchir l'affichage.
setTimeout(() => { skipBtn.disabled = false; poll(); }, 900);
setTimeout(() => { skipBtn.disabled = false; poll(); pollHistory(); }, 900);
});
poll();
setInterval(poll, 5000);
pollHistory();
setInterval(() => { poll(); pollHistory(); }, 5000);
</script>
</body>
</html>

View file

@ -81,9 +81,24 @@ radio = crossfade(duration=3.0, fade_in=3.0, fade_out=3.0, radio)
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é.
# On mémorise les dernières métadonnées vues sur le flux réellement diffusé,
# ainsi qu'un historique borné des titres passés à l'antenne (le plus récent
# en tête). L'historique reflète ce qui a vraiment été diffusé.
now_playing = ref([])
radio.on_metadata(synchronous=false, fun(m) -> now_playing := m)
history = ref([])
history_max = 25
radio.on_metadata(
synchronous=false,
fun(m) -> begin
now_playing := m
entry = {title=m["title"], artist=m["artist"]}
head = list.hd(default={title="", artist=""}, 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()))
end
end
)
# --- Sortie : flux MP3 sur http://<hote>:8000/radio.mp3 ---
output.harbor(
@ -112,6 +127,12 @@ harbor.http.register(
end
)
# Historique des titres passés (le plus récent en tête, morceau courant inclus).
harbor.http.register(
port=8000, method="GET", "/history",
fun(_, resp) -> resp.json(history())
)
# Passer au morceau suivant : on saute le morceau en cours sur la source
# diffusée. request.dynamic a déjà préchargé le suivant, donc l'enchaînement
# est immédiat (le prochain /next est demandé au daemon dans la foulée).