Name, Seitentitel, Dienste (playwish-web, playwish-controller), Dienst-Benutzer (playwish), Pfade (/opt/playwish, /usr/local/src/playwish), Vorlagen und Repository-Adresse. „Audiofy“ war bereits von mehreren Produkten belegt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
238 lines
7.9 KiB
JavaScript
238 lines
7.9 KiB
JavaScript
// Playwish – Gastseite. Daten nur per textContent einsetzen (kein innerHTML mit fremden Inhalten).
|
||
"use strict";
|
||
|
||
const STATUS_ALLE_MS = 5000;
|
||
const SUCHE_VERZOEGERUNG_MS = 400;
|
||
const STATUS_TEXT = {
|
||
queued: "auf der Liste",
|
||
pending: "wartet auf Freigabe",
|
||
played: "gespielt",
|
||
rejected: "abgelehnt",
|
||
deleted: "entfernt",
|
||
};
|
||
|
||
const $ = (id) => document.getElementById(id);
|
||
let letzterStatus = null;
|
||
let schutz = { token: async () => null, reset() {}, bereit: () => true }; // Turnstile (falls aktiv)
|
||
let fortschrittBasis = null; // {progress, dauer, zeit, spielt}
|
||
|
||
function el(tag, klasse, text) {
|
||
const e = document.createElement(tag);
|
||
if (klasse) e.className = klasse;
|
||
if (text !== undefined && text !== null) e.textContent = text;
|
||
return e;
|
||
}
|
||
|
||
function cover(url, gross) {
|
||
const img = el("img", "cover");
|
||
img.alt = "";
|
||
img.loading = "lazy";
|
||
img.decoding = "async";
|
||
if (url && url.startsWith("https://i.scdn.co/")) img.src = url;
|
||
if (gross) img.width = img.height = 72;
|
||
return img;
|
||
}
|
||
|
||
function textBlock(titel, interpret, extra) {
|
||
const t = el("div", "text");
|
||
t.append(el("span", "t", titel), el("span", "a", interpret));
|
||
if (extra) t.append(extra);
|
||
return t;
|
||
}
|
||
|
||
let toastTimer = null;
|
||
function toast(text, fehler) {
|
||
const t = $("toast");
|
||
t.textContent = text;
|
||
t.className = "toast" + (fehler ? " fehler" : "");
|
||
t.hidden = false;
|
||
clearTimeout(toastTimer);
|
||
toastTimer = setTimeout(() => { t.hidden = true; }, 4000);
|
||
}
|
||
|
||
async function api(pfad, optionen) {
|
||
const antwort = await fetch(pfad, Object.assign({ credentials: "same-origin" }, optionen));
|
||
let daten = null;
|
||
try { daten = await antwort.json(); } catch (_) { /* leer */ }
|
||
if (antwort.status === 401) { // Zugang abgelaufen/deaktiviert → Hinweisseite zeigen
|
||
location.reload();
|
||
throw new Error("Kein Zugang.");
|
||
}
|
||
if (!antwort.ok) {
|
||
const meldung = daten && typeof daten.detail === "string" ? daten.detail : "Da ist etwas schiefgelaufen.";
|
||
throw new Error(meldung);
|
||
}
|
||
return daten;
|
||
}
|
||
|
||
// --- Läuft gerade ---------------------------------------------------------
|
||
|
||
function zeigeJetzt(jetzt) {
|
||
const box = $("jetzt-inhalt");
|
||
box.replaceChildren();
|
||
if (!jetzt) {
|
||
box.className = "titel-zeile leer";
|
||
box.append(el("span", "hinweis", "Gerade läuft nichts."));
|
||
$("fortschritt").hidden = true;
|
||
fortschrittBasis = null;
|
||
return;
|
||
}
|
||
box.className = "titel-zeile";
|
||
const extra = jetzt.is_wish ? el("span", "marke", "Wunsch") : null;
|
||
const text = textBlock(jetzt.title, jetzt.artist);
|
||
if (extra) text.querySelector(".t").append(extra);
|
||
if (!jetzt.is_playing) text.append(el("span", "m", "pausiert"));
|
||
box.append(cover(jetzt.cover_url, true), text);
|
||
if (jetzt.duration_ms) {
|
||
fortschrittBasis = {
|
||
progress: jetzt.progress_ms + (jetzt.is_playing ? jetzt.age_ms : 0),
|
||
dauer: jetzt.duration_ms,
|
||
zeit: performance.now(),
|
||
spielt: jetzt.is_playing,
|
||
};
|
||
$("fortschritt").hidden = false;
|
||
zeichneFortschritt();
|
||
}
|
||
}
|
||
|
||
function zeichneFortschritt() {
|
||
if (!fortschrittBasis) return;
|
||
const b = fortschrittBasis;
|
||
const pos = b.progress + (b.spielt ? performance.now() - b.zeit : 0);
|
||
$("fortschritt-balken").style.width = Math.min(100, (pos / b.dauer) * 100) + "%";
|
||
}
|
||
|
||
// --- Status (Warteschlange, eigene Wünsche, Hinweise) -----------------------
|
||
|
||
function zeigeStatus(s) {
|
||
letzterStatus = s;
|
||
zeigeJetzt(s.now_playing);
|
||
$("hinweis-pausiert").hidden = !s.wishes_paused;
|
||
$("hinweis-moderation").hidden = !s.moderation || s.wishes_paused;
|
||
|
||
const l = s.limit;
|
||
$("limit-info").textContent = s.wishes_paused ? "" :
|
||
l.remaining > 0
|
||
? `Du kannst noch ${l.remaining} von ${l.count} Wünschen in ${l.minutes} Minuten abgeben.`
|
||
: `Limit erreicht – nächster Wunsch in ca. ${l.next_in_minutes} Min.`;
|
||
|
||
const ws = $("warteschlange");
|
||
ws.replaceChildren();
|
||
s.queue.forEach((w, i) => {
|
||
const li = el("li");
|
||
const wer = w.mine ? "dein Wunsch" : (w.name ? `gewünscht von ${w.name}` : null);
|
||
li.append(el("span", "status", String(i + 1) + "."), cover(w.cover_url),
|
||
textBlock(w.title, w.artist, wer ? el("span", "m", wer) : null));
|
||
ws.append(li);
|
||
});
|
||
$("warteschlange-leer").hidden = s.queue.length > 0;
|
||
const fi = $("freigabe-info");
|
||
fi.hidden = !s.pending_count;
|
||
fi.textContent = s.pending_count === 1 ? "1 Wunsch wartet auf Freigabe." : `${s.pending_count} Wünsche warten auf Freigabe.`;
|
||
|
||
const meine = $("meine");
|
||
meine.replaceChildren();
|
||
s.mine.forEach((w) => {
|
||
const li = el("li");
|
||
li.append(textBlock(w.title, w.artist), el("span", "status " + w.status, STATUS_TEXT[w.status] || w.status));
|
||
meine.append(li);
|
||
});
|
||
$("meine-karte").hidden = s.mine.length === 0;
|
||
aktualisiereKnoepfe();
|
||
}
|
||
|
||
async function ladeStatus() {
|
||
try {
|
||
zeigeStatus(await api("/api/status"));
|
||
} catch (_) { /* nächster Versuch beim nächsten Intervall */ }
|
||
}
|
||
|
||
// --- Suche & Wünschen --------------------------------------------------------
|
||
|
||
let sucheTimer = null;
|
||
let sucheNr = 0;
|
||
|
||
function aktualisiereKnoepfe() {
|
||
const aus = letzterStatus && (letzterStatus.wishes_paused || letzterStatus.limit.remaining === 0);
|
||
document.querySelectorAll("button.wuenschen").forEach((b) => {
|
||
if (!b.dataset.gesperrt && !b.dataset.erledigt) b.disabled = !!aus;
|
||
});
|
||
}
|
||
|
||
function zeigeTreffer(treffer) {
|
||
const liste = $("treffer");
|
||
liste.replaceChildren();
|
||
if (!treffer.length) {
|
||
liste.append(el("li", "hinweis", "Nichts gefunden."));
|
||
return;
|
||
}
|
||
treffer.forEach((t) => {
|
||
const li = el("li");
|
||
const text = textBlock(t.title, t.artist, t.blocked ? el("span", "gesperrt", t.blocked) : null);
|
||
if (t.explicit) text.querySelector(".t").append(el("span", "marke e", "E"));
|
||
const knopf = el("button", "wuenschen", "Wünschen");
|
||
knopf.type = "button";
|
||
if (t.blocked) { knopf.disabled = true; knopf.dataset.gesperrt = "1"; }
|
||
knopf.addEventListener("click", () => wuenschen(t, knopf));
|
||
li.append(cover(t.cover_url), text, knopf);
|
||
liste.append(li);
|
||
});
|
||
aktualisiereKnoepfe();
|
||
}
|
||
|
||
async function suchen(q) {
|
||
const nr = ++sucheNr;
|
||
try {
|
||
const daten = await api("/api/search?q=" + encodeURIComponent(q));
|
||
if (nr === sucheNr) zeigeTreffer(daten.results);
|
||
} catch (fehler) {
|
||
if (nr === sucheNr) toast(fehler.message, true);
|
||
}
|
||
}
|
||
|
||
async function wuenschen(titel, knopf) {
|
||
knopf.disabled = true;
|
||
const name = $("name").value.trim();
|
||
try { localStorage.setItem("playwish-name", name); } catch (_) { /* privat */ }
|
||
try {
|
||
if (!schutz.bereit()) knopf.textContent = "Prüfung …";
|
||
const turnstile = await schutz.token();
|
||
const daten = await api("/api/wish", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ track_uri: titel.track_uri, name: name || null, turnstile }),
|
||
}).finally(() => schutz.reset());
|
||
knopf.textContent = "✓ Gewünscht";
|
||
knopf.dataset.erledigt = "1";
|
||
toast(daten.message);
|
||
ladeStatus();
|
||
} catch (fehler) {
|
||
toast(fehler.message, true);
|
||
knopf.textContent = "Wünschen";
|
||
knopf.disabled = false;
|
||
}
|
||
}
|
||
|
||
function start() {
|
||
try { $("name").value = localStorage.getItem("playwish-name") || ""; } catch (_) { /* privat */ }
|
||
|
||
$("suchfeld").addEventListener("input", (ereignis) => {
|
||
clearTimeout(sucheTimer);
|
||
const q = ereignis.target.value.trim();
|
||
if (q.length < 2) { sucheNr++; $("treffer").replaceChildren(); return; }
|
||
sucheTimer = setTimeout(() => suchen(q), SUCHE_VERZOEGERUNG_MS);
|
||
});
|
||
$("suchfeld").addEventListener("keydown", (ereignis) => {
|
||
if (ereignis.key === "Enter") { ereignis.target.blur(); } // Tastatur auf dem Handy schließen
|
||
});
|
||
|
||
window.PlaywishTurnstile.einrichten($("turnstile"), "wish").then((s) => { schutz = s; });
|
||
|
||
ladeStatus();
|
||
setInterval(() => { if (!document.hidden) ladeStatus(); }, STATUS_ALLE_MS);
|
||
setInterval(zeichneFortschritt, 1000);
|
||
document.addEventListener("visibilitychange", () => { if (!document.hidden) ladeStatus(); });
|
||
}
|
||
|
||
start();
|