Radio: Webradio im Sprachkanal, 44 Sender zum Durchschalten
/radio holt den Bot in den Sprachkanal und postet eine Tafel: Auswahlmenue mit allen Sendern, Stopp, und ein Embed mit dem laufenden Titel, das sich alle 30 Sekunden selbst nachfuehrt. Der Titel kommt aus dem Icecast-Strom selbst (Icy-MetaData). Bewusst so und nicht ueber eine Sender-API: das funktioniert bei jedem Strom, den jemand eintraegt, und nicht nur bei den drei, die ich kenne. Leere Titel-Bloecke heissen "unveraendert", deshalb werden bis zu drei gelesen. Die Tonkette braucht keine Opus-Bibliothek: ffmpeg holt das MP3 und gibt direkt Ogg/Opus aus, prism-media packt nur noch aus. Gemessen: 600 Pakete in fuenf Sekunden. Sonst muesste @discordjs/opus mit ins Image, und das will gebaut werden. Neu im Image ist nur ffmpeg. 44 RauteMusik-Sender sind ab Werk eingetragen — dieselben, die im LS25 und ETS als Bordradio laufen. Die Liste ist nicht abgeschrieben, sondern gemessen: jede Adresse einzeln angefragt, der Name kommt aus icy-name. Doppelgaenger sind raus (deutschrap-charts und wackenradio liefern denselben Strom wie deutschrap und metal). Eingetragen wird einmal, solange die Liste leer ist — wer loescht, behaelt es geloescht. Ein Auswahlmenue fasst nur 25 Eintraege. Bei 44 Sendern haette ein Deckel die letzten 19 stillschweigend verschluckt; stattdessen mehrere Menues, benannt nach ihrem ersten und letzten Sender. Der Bot geht raus, wenn der Kanal leer ist, und kommt nach einem Neustart in den Kanal zurueck — sonst beendet jeder Deploy die Musik endgueltig. Kein YouTube. Genau daran sind Groovy und Rythm gestorben, und was davon uebrig ist, ist ein Dauerlauf gegen kaputte Extraktoren. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,7 @@ import {
|
||||
createService, updateService, listServices, deleteService,
|
||||
savePage, getPage, listPages, listPublishedPages, deletePage,
|
||||
lsState, lsModSizes, setLsModSize,
|
||||
listStations, createStation, updateStation, deleteStation,
|
||||
} from '../db.js';
|
||||
import crypto from 'node:crypto';
|
||||
import { moduleStates, moduleDetail, setModuleEnabled, moduleEnabled, moduleFavorites, setModuleFavorites, moduleSettingKeys, MODULE_GROUPS, MODULES, MAX_FAVORITES } from '../modules.js';
|
||||
@@ -35,6 +36,7 @@ import { templateStates, setTemplate, TEMPLATE_GROUPS } from '../templates.js';
|
||||
import { tuningStates, setTuning, tuning, TUNING_GROUPS } from '../tuning.js';
|
||||
import { lastResults, GAME_ICONS, spielName, spielListe, zugangStand } from '../bot/server-monitor.js';
|
||||
import { istLsServer, modBasis } from '../bot/ls-farm.js';
|
||||
import { titelHolen } from '../bot/radio.js';
|
||||
import { watchdogState, watchedServices } from '../bot/watchdog.js';
|
||||
import { listRules, setRuleEnabled } from '../bot/automod.js';
|
||||
import {
|
||||
@@ -357,6 +359,65 @@ export function registerApiRoutes(app, client) {
|
||||
return { deleted: deleteMonitored(Number(request.params.id)), dienste: listMonitored() };
|
||||
});
|
||||
|
||||
// --- Radio-Sender ---
|
||||
//
|
||||
// Frei eintragbar statt fest verdrahtet: es gibt keinen Grund, warum
|
||||
// jemand nur die Sender hören dürfen sollte, die im Code stehen.
|
||||
|
||||
/** Eingaben prüfen; antwortet bei Fehlern selbst */
|
||||
function senderBody(request, reply) {
|
||||
const b = request.body ?? {};
|
||||
const name = String(b.name ?? '').trim().slice(0, 60);
|
||||
const url = String(b.url ?? '').trim();
|
||||
if (!name) { reply.code(400).send({ error: 'Name fehlt' }); return null; }
|
||||
// Kein http:// — der Strom liefe sonst unverschlüsselt durch den Server
|
||||
if (!/^https?:\/\/.+/.test(url)) { reply.code(400).send({ error: 'URL mit http(s) erwartet' }); return null; }
|
||||
return {
|
||||
name, url,
|
||||
emoji: String(b.emoji ?? '').trim().slice(0, 8) || null,
|
||||
sort: Number(b.sort) || 0,
|
||||
};
|
||||
}
|
||||
|
||||
app.get('/api/radio', async (request, reply) => {
|
||||
if (requireAnyScope(request, reply)) return;
|
||||
return { sender: listStations() };
|
||||
});
|
||||
|
||||
app.post('/api/radio', async (request, reply) => {
|
||||
if (requireScope(request, reply, 'settings')) return;
|
||||
const s = senderBody(request, reply);
|
||||
if (!s) return;
|
||||
createStation(s);
|
||||
logAudit(getSessionUser(request), 'radiosender angelegt', `${s.name} (${s.url})`);
|
||||
return { ok: true, sender: listStations() };
|
||||
});
|
||||
|
||||
app.put('/api/radio/:id', async (request, reply) => {
|
||||
if (requireScope(request, reply, 'settings')) return;
|
||||
const s = senderBody(request, reply);
|
||||
if (!s) return;
|
||||
updateStation({ ...s, id: Number(request.params.id) });
|
||||
logAudit(getSessionUser(request), 'radiosender geändert', s.name);
|
||||
return { ok: true, sender: listStations() };
|
||||
});
|
||||
|
||||
app.delete('/api/radio/:id', async (request, reply) => {
|
||||
if (requireScope(request, reply, 'settings')) return;
|
||||
logAudit(getSessionUser(request), 'radiosender entfernt', String(request.params.id));
|
||||
return { deleted: deleteStation(Number(request.params.id)), sender: listStations() };
|
||||
});
|
||||
|
||||
// Was der Sender gerade spielt — zum Prüfen beim Eintragen. Der Titel
|
||||
// kommt aus dem Datenstrom selbst, es braucht also keine Sender-API.
|
||||
app.post('/api/radio/pruefen', async (request, reply) => {
|
||||
if (requireScope(request, reply, 'settings')) return;
|
||||
const url = String(request.body?.url ?? '').trim();
|
||||
if (!/^https?:\/\/.+/.test(url)) return reply.code(400).send({ error: 'URL mit http(s) erwartet' });
|
||||
const { titel, sender } = await titelHolen(url);
|
||||
return { erreichbar: sender != null || titel != null, sender, titel };
|
||||
});
|
||||
|
||||
// Ticket-Anliegen. Ohne Eintrag bleibt es beim einen Knopf — deshalb ist
|
||||
// eine leere Liste kein Fehler, sondern der Normalfall fuer kleine Server.
|
||||
app.get('/api/ticket-categories', async (request, reply) => {
|
||||
|
||||
Reference in New Issue
Block a user