stream: add a whole-station restart of the current track
All checks were successful
continuous-integration/drone/push Build is passing

Wire the Media Session previous-track control to a new POST /restart-track
route that requeues the current song from the start for every listener, and
keep next-track as skip. Exposing both handlers also makes Android (Chrome)
show the skip button, which it hides when only nexttrack is set.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
nemunaire 2026-07-04 10:27:43 +08:00
commit 96a1ba89e6
2 changed files with 45 additions and 5 deletions

View file

@ -245,9 +245,17 @@
safe(() => media.setActionHandler("play", () => player.play().catch(() => {})));
safe(() => media.setActionHandler("pause", () => player.pause()));
safe(() => media.setActionHandler("stop", () => player.pause()));
// Pas de reprise possible sur un direct : « précédent » comme « suivant »
// passent au morceau suivant côté serveur.
// « Suivant » saute le morceau courant. « Précédent » le rejoue depuis le
// début, pour toute l'antenne (flux partagé, pas de position par
// auditeur). Brancher les deux est aussi nécessaire sous Android (Chrome),
// qui traite précédent/suivant comme une paire et masque le bouton suivant
// si seul « nexttrack » est défini.
safe(() => media.setActionHandler("nexttrack", () => { skipBtn.click(); }));
safe(() => media.setActionHandler("previoustrack", () => {
fetch("/restart-track", { method: "POST" })
.then(() => setTimeout(() => { poll(); pollHistory(); }, 900))
.catch(() => {});
}));
// Le direct n'est pas déplaçable : on neutralise les actions de seek pour
// éviter que le système n'affiche des boutons d'avance/recul inopérants.
safe(() => media.setActionHandler("seekbackward", null));

View file

@ -72,9 +72,17 @@ backup = playlist(
check_next=audio_only, fallback_file
)
# fallback préfère la source principale et bascule sur le cache si elle n'a
# rien de prêt. track_sensitive=true : on ne coupe pas un morceau en cours.
music = fallback(track_sensitive=true, [main, 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
# début à l'antenne. Placée en tête du fallback, elle préempte la source
# principale dès qu'un morceau y est poussé.
requeue = request.queue(id="requeue")
# fallback préfère la file de rejeu, puis la source principale, et bascule sur le
# cache si rien n'est prêt. track_sensitive=true : on ne coupe pas un morceau en
# cours (le rejeu ne prend donc effet qu'à la prochaine frontière, provoquée
# explicitement par source.skip dans /restart-track).
music = fallback(track_sensitive=true, [requeue, main, backup])
# --- Jingles : intercalés toutes les 2 chansons -----------------------------
# Le dossier /jingles (monté depuis ./jingles) est parcouru et lu au hasard.
@ -270,6 +278,30 @@ harbor.http.register(
end
)
# Rejouer le morceau courant depuis le début, pour TOUTE l'antenne (le flux est
# partagé : il n'existe pas de position par auditeur). On repousse le fichier
# courant — pris dans le cache, donc forcément local et présent — en tête via la
# file de rejeu, puis on saute le morceau en cours pour l'y enchaîner aussitôt.
# On remet le compteur de chansons à zéro pour qu'un jingle ne vole pas la place
# du rejeu à cette frontière. Un morceau introuvable (jingle en cours, cache
# évincé) renvoie 409.
harbor.http.register(
port=8000, method="POST", "/restart-track",
fun(_, resp) -> begin
base = path.basename(list.assoc(default="", "filename", now_playing()))
full = "/cache/#{base}"
if base != "" and file.exists(full) then
song_count := 0
requeue.push.uri(full)
source.skip(radio)
resp.json({restarted=true})
else
resp.status_code(409)
resp.json({restarted=false})
end
end
)
# Télécharger un morceau. Sans paramètre : le titre en cours. Avec `?file=<nom>` :
# n'importe quel fichier encore présent dans le cache (les titres passés listés
# par /history exposent ce jeton). Le cache étant borné par le LRU, un morceau