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:
@@ -1882,3 +1882,78 @@ export function archiveStats() {
|
||||
lastCommit: lastCommitStmt.get().ts ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Radio ─────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Sender sind frei eintragbar, nicht fest verdrahtet: es gibt keinen Grund,
|
||||
// warum jemand nur die Sender hören dürfen sollte, die ich kenne. Die URL ist
|
||||
// eindeutig — denselben Sender zweimal in der Liste hilft niemandem.
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS radio_stations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
url TEXT NOT NULL UNIQUE,
|
||||
emoji TEXT,
|
||||
sort INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
`);
|
||||
const listStationsStmt = db.prepare('SELECT * FROM radio_stations ORDER BY sort, name');
|
||||
const getStationStmt = db.prepare('SELECT * FROM radio_stations WHERE id = ?');
|
||||
const insertStationStmt = db.prepare(`
|
||||
INSERT INTO radio_stations (name, url, emoji, sort) VALUES (@name, @url, @emoji, @sort)
|
||||
ON CONFLICT(url) DO NOTHING
|
||||
`);
|
||||
const updateStationStmt = db.prepare(`
|
||||
UPDATE radio_stations SET name = @name, url = @url, emoji = @emoji, sort = @sort WHERE id = @id
|
||||
`);
|
||||
const deleteStationStmt = db.prepare('DELETE FROM radio_stations WHERE id = ?');
|
||||
export const listStations = () => listStationsStmt.all();
|
||||
export const getStation = (id) => getStationStmt.get(id) ?? null;
|
||||
export const createStation = (s) => insertStationStmt.run({
|
||||
name: String(s.name ?? '').slice(0, 60),
|
||||
url: String(s.url ?? ''),
|
||||
emoji: s.emoji ? String(s.emoji).slice(0, 8) : null,
|
||||
sort: Number(s.sort) || 0,
|
||||
}).lastInsertRowid;
|
||||
export const updateStation = (s) => updateStationStmt.run({
|
||||
id: Number(s.id),
|
||||
name: String(s.name ?? '').slice(0, 60),
|
||||
url: String(s.url ?? ''),
|
||||
emoji: s.emoji ? String(s.emoji).slice(0, 8) : null,
|
||||
sort: Number(s.sort) || 0,
|
||||
});
|
||||
export const deleteStation = (id) => deleteStationStmt.run(id).changes > 0;
|
||||
|
||||
// Was gerade wo läuft. Steht in der Datenbank und nicht nur im Speicher, damit
|
||||
// der Bot nach einem Neustart von selbst zurückkommt — ein Deploy wirft ihn
|
||||
// sonst aus dem Sprachkanal und die Musik ist einfach weg.
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS radio_state (
|
||||
guild_id TEXT PRIMARY KEY,
|
||||
channel_id TEXT NOT NULL,
|
||||
station_id INTEGER,
|
||||
panel_channel_id TEXT,
|
||||
panel_message_id TEXT,
|
||||
seit TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
`);
|
||||
const radioStateStmt = db.prepare('SELECT * FROM radio_state WHERE guild_id = ?');
|
||||
const radioStatesStmt = db.prepare('SELECT * FROM radio_state');
|
||||
const setRadioStateStmt = db.prepare(`
|
||||
INSERT INTO radio_state (guild_id, channel_id, station_id, panel_channel_id, panel_message_id, seit)
|
||||
VALUES (@guild_id, @channel_id, @station_id, @panel_channel_id, @panel_message_id, datetime('now'))
|
||||
ON CONFLICT(guild_id) DO UPDATE SET
|
||||
channel_id = excluded.channel_id, station_id = excluded.station_id,
|
||||
panel_channel_id = excluded.panel_channel_id, panel_message_id = excluded.panel_message_id
|
||||
`);
|
||||
const clearRadioStateStmt = db.prepare('DELETE FROM radio_state WHERE guild_id = ?');
|
||||
export const radioState = (guildId) => radioStateStmt.get(guildId) ?? null;
|
||||
export const radioStates = () => radioStatesStmt.all();
|
||||
export const setRadioState = (s) => setRadioStateStmt.run({
|
||||
guild_id: String(s.guildId),
|
||||
channel_id: String(s.channelId),
|
||||
station_id: s.stationId != null ? Number(s.stationId) : null,
|
||||
panel_channel_id: s.panelChannelId ? String(s.panelChannelId) : null,
|
||||
panel_message_id: s.panelMessageId ? String(s.panelMessageId) : null,
|
||||
});
|
||||
export const clearRadioState = (guildId) => clearRadioStateStmt.run(guildId).changes > 0;
|
||||
|
||||
Reference in New Issue
Block a user