Files
99admandClaude Opus 5.5 fc15f2b933 Umbenennung: Audiofy heißt jetzt Playwish
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>
2026-10-17 20:27:59 +01:00

238 lines
7.9 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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();