Audiofy – Musikwünsche für Partys

Server (Website, Admin-Bereich, Player-API) und Player (raspotify +
Controller) mit Install-Skripten für Debian-Systeme.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-17 19:16:39 +01:00
co-authored by Claude Opus 5.5
commit 3610de7bae
45 changed files with 5666 additions and 0 deletions
+176
View File
@@ -0,0 +1,176 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="theme-color" content="#121212">
<meta name="robots" content="noindex">
<title>Audiofy – Admin</title>
<link rel="stylesheet" href="/static/app.css">
<link rel="stylesheet" href="/static/admin.css">
<script src="/static/admin.js" defer></script>
</head>
<body>
<main>
<div class="kopf">
<h1>Audiofy Admin</h1>
<button id="abmelden" class="klein" type="button">Abmelden</button>
</div>
<div class="pillen" id="pillen"></div>
<nav class="reiter" role="tablist">
<button type="button" role="tab" data-reiter="wiedergabe" aria-selected="true">Wiedergabe</button>
<button type="button" role="tab" data-reiter="wuensche">Wünsche<span id="zahl-freigabe" class="zahl" hidden></span></button>
<button type="button" role="tab" data-reiter="gaeste">Gäste</button>
<button type="button" role="tab" data-reiter="einstellungen">Einstellungen</button>
<button type="button" role="tab" data-reiter="verlauf">Verlauf</button>
</nav>
<div id="notaus-hinweis" class="notaus-aktiv" hidden>Not-Aus aktiv – der Controller ist angehalten.</div>
<!-- ===== Wiedergabe ===== -->
<section data-seite="wiedergabe">
<div class="karte">
<h2>Läuft gerade</h2>
<div id="jetzt-inhalt" class="titel-zeile"></div>
<div class="fortschritt"><div id="fortschritt-balken"></div></div>
<p id="naechster" class="hinweis klein"></p>
<p id="pi-fehler" class="hinweis klein fehlertext" hidden></p>
<div class="knopfreihe">
<button class="gross mit-symbol" type="button" data-befehl="play"><span class="symbol" aria-hidden="true">▶</span><span>Play</span></button>
<button class="gross mit-symbol" type="button" data-befehl="pause"><span class="symbol" aria-hidden="true">⏸</span><span>Pause</span></button>
<button class="gross mit-symbol" type="button" data-befehl="skip"><span class="symbol" aria-hidden="true">⏭</span><span>Weiter</span></button>
</div>
<div class="lautstaerke">
<span aria-hidden="true">🔊</span>
<input id="lautstaerke" type="range" min="0" max="100" step="5" aria-label="Lautstärke">
<output id="lautstaerke-wert">–</output>
</div>
</div>
<div class="karte">
<h2>Als Nächstes spielen</h2>
<input id="naechstes-suche" type="search" placeholder="Titel suchen …" autocomplete="off" maxlength="100">
<ul id="naechstes-treffer" class="liste"></ul>
</div>
<div class="karte">
<h2>Not-Aus</h2>
<p class="hinweis klein">Stoppt die Wiedergabe und hält den Controller an, bis du wieder freigibst.</p>
<button id="notaus" class="gross mit-symbol gefahr voll" type="button"><span class="symbol" aria-hidden="true">⛔</span><span>Not-Aus</span></button>
</div>
<div class="karte">
<h2>Letzte Befehle</h2>
<ul id="befehle" class="liste kompakt"></ul>
</div>
</section>
<!-- ===== Wünsche ===== -->
<section data-seite="wuensche" hidden>
<div class="karte">
<div class="schalter-zeile">
<div class="text"><span class="t">Wünsche annehmen</span><span class="m">Aus = Gäste sehen „pausiert“</span></div>
<label class="schalter"><input type="checkbox" id="s-annahme"><span></span></label>
</div>
<div class="schalter-zeile">
<div class="text"><span class="t">Moderation</span><span class="m">Wünsche erst nach deiner Freigabe spielen</span></div>
<label class="schalter"><input type="checkbox" id="s-moderation"><span></span></label>
</div>
</div>
<div class="karte" id="freigabe-karte">
<h2>Zur Freigabe</h2>
<ul id="freigabe" class="liste"></ul>
<p id="freigabe-leer" class="hinweis">Nichts zu prüfen.</p>
<p class="legende"><span>✓ freigeben</span><span>✕ ablehnen</span><span>🚫 Gast sperren</span></p>
</div>
<div class="karte">
<h2>Warteschlange</h2>
<ul id="warteschlange" class="liste"></ul>
<p id="warteschlange-leer" class="hinweis">Keine Wünsche in der Warteschlange.</p>
<p class="legende"><span>↑ früher</span><span>↓ später</span><span>✕ löschen</span><span>🚫 Gast sperren</span></p>
</div>
</section>
<!-- ===== Gäste ===== -->
<section data-seite="gaeste" hidden>
<div class="karte">
<h2>Gastzugang</h2>
<div class="schalter-zeile">
<div class="text"><span class="t">Zugang offen</span><span class="m">Aus = niemand kommt auf die Wunschseite</span></div>
<label class="schalter"><input type="checkbox" id="s-zugang"><span></span></label>
</div>
<img id="qr" class="qr" alt="QR-Code zum Einladungslink">
<input id="einladungslink" class="link-feld" type="text" readonly aria-label="Einladungslink">
<div class="knopfreihe zwei">
<button id="link-kopieren" class="gross" type="button">Link kopieren</button>
<a id="qr-png" class="gross" href="/admin/api/access/qr?format=png" download>QR als Bild</a>
</div>
<button id="link-erneuern" class="gross voll" type="button">Neuen Link erzeugen</button>
<p class="hinweis klein">Ein neuer Link macht den alten und alle damit erteilten Zugänge ungültig.</p>
</div>
<div class="karte">
<h2>Limit pro Gast</h2>
<div class="feld-zeile"><span>Wünsche</span><input id="e-limit-anzahl" type="number" min="1" max="50"></div>
<div class="feld-zeile"><span>pro Minuten</span><input id="e-limit-minuten" type="number" min="1" max="1440"></div>
<button class="gross akzent voll" type="button" data-speichern="e-limit-anzahl:wish_limit_count,e-limit-minuten:wish_limit_minutes">Speichern</button>
</div>
<div class="karte">
<h2>Sperrliste</h2>
<div class="schalter-zeile">
<div class="text"><span class="t">Explizite Titel sperren</span></div>
<label class="schalter"><input type="checkbox" id="s-explizit"><span></span></label>
</div>
<input id="sperr-suche" type="search" placeholder="Titel oder Interpret zum Sperren suchen …" autocomplete="off" maxlength="100">
<ul id="sperr-treffer" class="liste"></ul>
<h2 class="abstand">Gesperrt</h2>
<ul id="sperrliste" class="liste"></ul>
<p id="sperrliste-leer" class="hinweis">Nichts gesperrt.</p>
</div>
</section>
<!-- ===== Einstellungen ===== -->
<section data-seite="einstellungen" hidden>
<div class="karte">
<h2>Ablauf</h2>
<div class="feld-zeile"><span>Jeder wievielte Titel ist ein Wunsch (N)</span><input id="e-n" type="number" min="1" max="20"></div>
<div class="feld-zeile"><span>Sperrzeit für Wiederholungen (Min.)</span><input id="e-sperrzeit" type="number" min="0" max="1440"></div>
<button class="gross akzent voll" type="button" data-speichern="e-n:wish_every_n,e-sperrzeit:repeat_block_minutes">Speichern</button>
</div>
<div class="karte">
<h2>Lautsprecher</h2>
<div id="bt-aktuell" class="titel-zeile"></div>
<p class="hinweis klein">Neuen Bluetooth-Lautsprecher vorher in den Kopplungsmodus versetzen (meist Bluetooth-Taste gedrückt halten, bis die LED schnell blinkt). Während der Suche kann die Musik kurz stocken – WLAN und Bluetooth teilen sich oft einen Funkchip.</p>
<button id="bt-suchen" class="gross voll" type="button">Nach Lautsprechern suchen</button>
<p id="bt-status" class="hinweis klein"></p>
<ul id="bt-geraete" class="liste"></ul>
</div>
<div class="karte">
<h2>Grund-Playlist</h2>
<p class="hinweis klein">Aktuell: <a id="playlist-link" target="_blank" rel="noopener noreferrer"></a></p>
<input id="playlist-neu" type="text" placeholder="Spotify-Playlist-Link einfügen" autocomplete="off" maxlength="300">
<div class="knopfreihe zwei">
<button id="playlist-wechseln" class="gross akzent" type="button">Wechseln</button>
<button id="neu-mischen" class="gross" type="button">Neu mischen</button>
</div>
</div>
</section>
<!-- ===== Verlauf ===== -->
<section data-seite="verlauf" hidden>
<div class="karte">
<h2>Verlauf</h2>
<ul id="verlauf" class="liste"></ul>
<p id="verlauf-leer" class="hinweis">Noch nichts gespielt.</p>
</div>
</section>
</main>
<div id="toast" class="toast" role="status" aria-live="assertive" hidden></div>
</body>
</html>