// Audiofy – 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("audiofy-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("audiofy-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.AudiofyTurnstile.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();