From 96a1ba89e6c18150ca8cc736fb88ba79bc1993ef Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sat, 4 Jul 2026 10:27:43 +0800 Subject: [PATCH] stream: add a whole-station restart of the current track 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 --- stream/index.html | 12 ++++++++++-- stream/radio.liq | 38 +++++++++++++++++++++++++++++++++++--- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/stream/index.html b/stream/index.html index a9426d2..fa9b90f 100644 --- a/stream/index.html +++ b/stream/index.html @@ -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)); diff --git a/stream/radio.liq b/stream/radio.liq index 058af6b..40ac29b 100644 --- a/stream/radio.liq +++ b/stream/radio.liq @@ -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=` : # 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