stream: let listeners queue a yt-dlp URL on request
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Add an input in the queue tab to enqueue a yt-dlp URL: a single track, or a whole playlist/album. Requests are a priority lane in the ingest queue — pop_next serves them before the auto radio, so the next /next plays the request without cutting the current track. They download lazily (a few ahead), so a large playlist queues instantly and bypasses anti-repeat. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
ef1a19504e
commit
493e55ed18
5 changed files with 225 additions and 22 deletions
|
|
@ -157,6 +157,25 @@
|
|||
.queue .q-num { color: #6b6480; font-size: .8rem; font-variant-numeric: tabular-nums;
|
||||
min-width: 1.2em; text-align: right; }
|
||||
.queue .q-meta { flex: 1; min-width: 0; }
|
||||
/* Ajout à la file : un input d'URL + bouton, calqués sur le bloc « share ».
|
||||
Le message de retour s'affiche discrètement sous le formulaire, en rouge
|
||||
tamisé quand c'est une erreur. */
|
||||
.enqueue { display: flex; gap: .5rem; margin-bottom: .75rem; }
|
||||
.enqueue input {
|
||||
flex: 1; min-width: 0; padding: .55rem .7rem; font-size: .85rem;
|
||||
color: #cfc9de; background: rgba(255,255,255,.05);
|
||||
border: 1px solid rgba(255,255,255,.1); border-radius: 10px;
|
||||
}
|
||||
.enqueue button {
|
||||
padding: .55rem .9rem; font-size: .8rem; font-weight: 600; cursor: pointer;
|
||||
color: #f2f0f7; background: rgba(155,140,255,.18);
|
||||
border: 1px solid rgba(155,140,255,.35); border-radius: 10px;
|
||||
transition: background .15s;
|
||||
}
|
||||
.enqueue button:hover { background: rgba(155,140,255,.3); }
|
||||
.enqueue button:disabled { opacity: .5; cursor: default; }
|
||||
.enqueue-msg { font-size: .8rem; color: #8ad9a0; margin-bottom: .5rem; }
|
||||
.enqueue-msg.err { color: #e08b9b; }
|
||||
.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; }
|
||||
|
|
@ -193,6 +212,12 @@
|
|||
<ul id="historyList"></ul>
|
||||
</section>
|
||||
<section class="tab-panel queue" id="panelQueue" role="tabpanel" aria-labelledby="tabQueue" hidden>
|
||||
<form class="enqueue" id="enqueueForm">
|
||||
<input id="enqueueUrl" type="url" inputmode="url"
|
||||
placeholder="URL yt-dlp (piste, playlist ou album)…" required>
|
||||
<button type="submit" id="enqueueBtn">Ajouter</button>
|
||||
</form>
|
||||
<div class="enqueue-msg" id="enqueueMsg" hidden></div>
|
||||
<ul id="queueList"></ul>
|
||||
</section>
|
||||
</div>
|
||||
|
|
@ -245,6 +270,8 @@
|
|||
// Filet de secours : morceau rejoué depuis le cache local (déjà diffusé),
|
||||
// quand l'ingest n'a rien à proposer (démarrage, panne…).
|
||||
cache: "le cache local",
|
||||
// Morceau mis en file par un auditeur via l'input « Ajouter à la file ».
|
||||
request: "une demande",
|
||||
};
|
||||
|
||||
// Tant que le buffer de préchargement (PREFETCH côté ingest) n'est pas
|
||||
|
|
@ -450,6 +477,47 @@
|
|||
}).join("");
|
||||
} catch (e) { /* keep last known values */ }
|
||||
}
|
||||
|
||||
// Ajout à la file : on POST l'URL au stream, qui la relaie à l'ingest. Ce
|
||||
// dernier résout l'URL (piste seule, ou playlist/album entier) et la place
|
||||
// en file prioritaire — le prochain morceau diffusé sera la demande. On
|
||||
// affiche un retour bref puis on rafraîchit la file. La résolution d'une
|
||||
// grosse playlist peut prendre quelques secondes.
|
||||
const enqueueForm = document.getElementById("enqueueForm");
|
||||
const enqueueUrl = document.getElementById("enqueueUrl");
|
||||
const enqueueBtn = document.getElementById("enqueueBtn");
|
||||
const enqueueMsg = document.getElementById("enqueueMsg");
|
||||
function showEnqueueMsg(text, ok) {
|
||||
enqueueMsg.textContent = text;
|
||||
enqueueMsg.classList.toggle("err", !ok);
|
||||
enqueueMsg.hidden = false;
|
||||
}
|
||||
enqueueForm.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const link = enqueueUrl.value.trim();
|
||||
if (!link) return;
|
||||
const prev = enqueueBtn.textContent;
|
||||
enqueueBtn.disabled = true;
|
||||
enqueueBtn.textContent = "…";
|
||||
try {
|
||||
const r = await fetch("/enqueue?url=" + encodeURIComponent(link), { method: "POST" });
|
||||
if (r.ok) {
|
||||
const d = await r.json().catch(() => ({}));
|
||||
const n = Number(d.queued) || 0;
|
||||
showEnqueueMsg(n > 1 ? `${n} titres ajoutés à la file` : "1 titre ajouté à la file", true);
|
||||
enqueueUrl.value = "";
|
||||
pollQueue();
|
||||
} else {
|
||||
showEnqueueMsg("Impossible de résoudre cette URL", false);
|
||||
}
|
||||
} catch (err) {
|
||||
showEnqueueMsg("Erreur réseau", false);
|
||||
} finally {
|
||||
enqueueBtn.disabled = false;
|
||||
enqueueBtn.textContent = prev;
|
||||
}
|
||||
});
|
||||
|
||||
// Lien nu du flux, à ouvrir dans un lecteur externe (VLC…).
|
||||
const shareUrl = location.origin + "/radio.mp3";
|
||||
const urlEl = document.getElementById("streamUrl");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue