From dbec4e9775bf96bc3212e232b7706ed4e02fceed Mon Sep 17 00:00:00 2001 From: D4rkst3r Date: Fri, 28 Aug 2026 13:50:06 +0200 Subject: [PATCH] Panel: die volle Fernbedienung ueber der Senderliste Was laeuft, wo, aus welcher Quelle, mit Fortschrittsbalken. Sprachkanal waehlen, Sender abspielen, Ueberspringen, Zum Radio, Stopp, Lautstaerke-Schieber. Einreihen per Link oder Suchbegriff, Warteschlange mit Hoch/Runter/Weg und Leeren. Darunter ein aufklappbarer yt-dlp-Block: Zustand, Fassung, cookies.txt, eigene Argumente, Aktualisieren -- und die letzte Fehlerzeile im Klartext. Steht ueber der Senderliste, weil man oefter etwas abspielt als die Liste pflegt. Die Abfrage laeuft nur, solange der Community-Reiter offen ist. Sonst ginge alle fuenf Sekunden eine Anfrage gegen eine Ansicht raus, die niemand ansieht. Der Balken erscheint nur bei bekannter Dauer. Bei einem Livestream gaebe er einen Stand vor, den niemand kennt -- dort steht stattdessen "live". Die Kanalauswahl graut aus, wo der Bot nicht rein darf. Ein Kanal, den das Panel anbietet und in den der Bot dann nicht kommt, sieht aus wie ein kaputter Knopf. Geprueft: der Vite-Build laeuft durch, und ein Abgleich stellt alle 30 Feldpfade, die die Oberflaeche vom Stand liest, gegen die echte API-Antwort -- 0 fehlen. Der Abgleich hat eine Gegenprobe mit bekanntem Ausgang (ein Feld, das es nicht gibt, muss als fehlend erkannt werden) und prueft vorher, ob die sonst leeren Zweige (Warteschlange, Cookies, letzter Fehler) ueberhaupt besetzt sind -- sonst meldet er "ungueltig" statt "in Ordnung". Seine erste Fassung hatte Fehlalarm geschlagen (?. und .map als fehlende Felder gelesen); das ist behoben. NICHT geprueft: das tatsaechliche Rendern im Browser -- hier ist weder Browser noch DOM-Werkzeug vorhanden. --- frontend/src/pages/Settings.jsx | 467 ++++++++++++++++++++++++++++++++ frontend/src/style.css | 55 ++++ src/modules.js | 17 +- 3 files changed, 536 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx index fefbf26..ca9fd88 100644 --- a/frontend/src/pages/Settings.jsx +++ b/frontend/src/pages/Settings.jsx @@ -95,6 +95,21 @@ const WUNSCH_STATUS = [ // Alles mit Editor, Vorschau oder Liste bleibt einspaltig. const SENDER_LEER = { id: null, name: '', url: '', emoji: '', sort: 0 }; +/** + * 3:07 statt 187. Ohne bekannte Dauer steht „live" da und nicht „0:00" — + * ein Livestream hat keine Laenge, und eine Null waere eine Behauptung. + */ +function dauerText(sekunden) { + if (sekunden === null || sekunden === undefined) return 'live'; + const t = Math.max(0, Math.round(sekunden)); + const h = Math.floor(t / 3600); + const m = Math.floor((t % 3600) / 60); + const r = t % 60; + return h > 0 + ? `${h}:${String(m).padStart(2, '0')}:${String(r).padStart(2, '0')}` + : `${m}:${String(r).padStart(2, '0')}`; +} + const DENSE_TABS = new Set(['feeds', 'community', 'support', 'system']); // Welcher Team-Scope schaltet welchen Tab frei (null = nur Owner) @@ -432,6 +447,14 @@ export default function Settings({ me }) { const [senderDraft, setSenderDraft] = useState({ id: null, name: '', url: '', emoji: '', sort: 0 }); // Ergebnis der Sender-Probe: { erreichbar, sender, titel } oder 'laeuft' const [senderProbe, setSenderProbe] = useState(null); + // Fernbedienung: der Live-Stand vom Bot (null = noch nichts geholt) + const [radio, setRadio] = useState(null); + const [radioKanal, setRadioKanal] = useState(''); + const [radioEingabe, setRadioEingabe] = useState(''); + const [radioTreffer, setRadioTreffer] = useState(null); // Auswahl nach einer Suche + const [radioLaedt, setRadioLaedt] = useState(false); + const [radioFehler, setRadioFehler] = useState(''); + const [ytCookies, setYtCookies] = useState(''); const [owner, setOwner] = useState(null); const [auditAlle, setAuditAlle] = useState(false); // Module (An/Aus für alle Funktionen) @@ -472,6 +495,23 @@ export default function Settings({ me }) { apiGet('/api/radio').then((d) => setSender(d.sender ?? [])).catch(() => {}); }, []); + // Der Live-Stand der Fernbedienung. + // + // Nur solange der Community-Reiter offen ist: sonst liefe im Hintergrund + // alle fuenf Sekunden eine Anfrage gegen eine Ansicht, die niemand sieht. + // Fuenf Sekunden, weil der Fortschrittsbalken sonst ruckelt und ein Titel + // selten kuerzer als zwei Minuten ist. + useEffect(() => { + if (tab !== 'community') return undefined; + let lebt = true; + const holen = () => apiGet('/api/radio/status') + .then((d) => { if (lebt) setRadio(d); }) + .catch(() => {}); + holen(); + const timer = setInterval(holen, 5000); + return () => { lebt = false; clearInterval(timer); }; + }, [tab]); + // Portal-Dienste (Kacheln auf der Startseite) const emptyService = { id: null, name: '', url: '', icon: '', description: '', sort: 0 }; const [services, setServices] = useState([]); @@ -2151,6 +2191,323 @@ export default function Settings({ me }) { ohne Haken antwortet /radio nur mit einem Hinweis +
+ +
+ + {/* ── Fernbedienung ────────────────────────────────────── + Steht ueber der Senderliste, weil man oefter etwas + abspielt als die Liste pflegt. */} + {radio?.mehrdeutig && ( +

+ Der Bot steht in mehreren Servern und es ist keiner eingestellt — + unter System die Server-ID eintragen, sonst weiss + das Panel nicht, welchen es steuern soll. +

+ )} + + {radio && !radio.mehrdeutig && ( +
+
+ {radio.laeuft ? ( + <> +
+ + {radio.laeuft.art === 'youtube' ? '▶️' : '📻'} + +
+ + {radio.laeuft.art === 'youtube' + ? radio.laeuft.titel + : radio.laeuft.sender} + + + {radio.laeuft.art === 'youtube' + ? [radio.laeuft.kanal, radio.laeuft.heimStation + && `danach zurück auf ${radio.laeuft.heimStation.name}`] + .filter(Boolean).join(' · ') + : (radio.laeuft.titel ?? 'Titel wird abgefragt …')} + + + 🔊 {radio.laeuft.kanalName ?? radio.laeuft.kanalId} + +
+
+ {/* Der Balken nur, wo er etwas aussagen kann: ohne + bekannte Dauer gaebe er einen Stand vor, den + niemand kennt. */} + {radio.laeuft.dauer ? ( +
+
+ +
+ + {dauerText(radio.laeuft.verstrichen)} / {dauerText(radio.laeuft.dauer)} + +
+ ) : radio.laeuft.art === 'youtube' && ( +

🔴 Livestream

+ )} + {radio.laeuft.tonLaeuft === false && ( +

+ ⚠️ Es kommt kein Ton an. + {radio.werkzeuge.ffmpeg === false + && ' Auf dem Server fehlt ffmpeg.'} +

+ )} + + ) : ( +

+ Nichts läuft gerade. + {radio.stand.n > 0 + && ` ${radio.stand.n} Titel warten — Sprachkanal wählen und abspielen.`} +

+ )} +
+ +
+
+ + +
+
+ + {/* onMouseUp/onChange statt live bei jedem Pixel: sonst + geht je Mausbewegung eine Anfrage raus. */} + setRadio({ ...radio, lautstaerke: Number(e.target.value) })} + onMouseUp={(e) => radioSteuern('laut', { wert: Number(e.target.value) })} + onTouchEnd={(e) => radioSteuern('laut', { wert: Number(e.target.value) })} /> +
+
+ +
+ + + + +
+ + {/* ── Einreihen ── */} +
+ +
+ { setRadioEingabe(e.target.value); setRadioTreffer(null); }} + onKeyDown={(e) => e.key === 'Enter' && radioEinreihen()} /> + +
+ {!radio.werkzeuge.youtubeAn && ( + + YouTube ist ausgeschaltet — Haken oben setzen und speichern. + + )} +
+ + {/* Bei einer Suche wird nicht geraten, welcher gemeint war. */} + {radioTreffer && ( +
+ {radioTreffer.map((t) => ( +
+ ▶️ +
+ {t.titel} + + {[t.kanal, dauerText(t.dauer)].filter(Boolean).join(' · ')} + +
+
+ +
+
+ ))} +
+ )} + + {/* ── Warteschlange ── */} + {radio.warteschlange.length > 0 && ( + <> + +
+ {radio.warteschlange.map((e, i) => ( +
+ {i + 1}. +
+ {e.titel} + + {[e.kanal, dauerText(e.dauer), + e.wunsch_von && `von ${e.wunsch_von}`] + .filter(Boolean).join(' · ')} + +
+
+ + + +
+
+ ))} +
+
+ +
+ + )} + + {radioFehler &&

❌ {radioFehler}

} + + {/* ── yt-dlp ────────────────────────────────────── + Steht sichtbar hier und nicht in einer Fussnote: + wenn YouTube klemmt, ist das fast immer die + Antwort, und Stille erklaert sich nicht selbst. */} +
+ + yt-dlp — {radio.werkzeuge.ytdlp === false + ? '❌ fehlt' + : radio.werkzeuge.ytdlp === null + ? '… noch nicht geprüft' + : `✅ ${radio.werkzeuge.ytdlpFassung ?? 'da'}`} + {radio.werkzeuge.cookies ? ' · Cookies gesetzt' : ''} + + + {radio.werkzeuge.letzterFehler && ( + <> + +
{radio.werkzeuge.letzterFehler.text}
+ + )} + +

+ YouTube sperrt Server-Adressen inzwischen häufig + („Sign in to confirm you're not a bot"). Zwei Griffe helfen, + beide ohne neues Image: eine cookies.txt aus dem + Browser eines angemeldeten Kontos, und eigene Argumente + (Feld weiter unten in dieser Sektion). +

+ +
+ +