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