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:
@@ -0,0 +1,88 @@
|
||||
// /radio — Steuertafel im Kanal öffnen.
|
||||
//
|
||||
// Der Befehl nimmt bewusst keinen Sendernamen entgegen: getippt werden müsste
|
||||
// er auf dem Handy erst recht, und die Auswahl steht ohnehin im Menü. Ein
|
||||
// Aufruf, eine Tafel, umschalten per Klick.
|
||||
import { SlashCommandBuilder, MessageFlags } from 'discord.js';
|
||||
import { moduleEnabled } from '../../modules.js';
|
||||
import { listStations, getStation, setRadioState } from '../../db.js';
|
||||
import { spielen, laeuft, tafelEmbed, tafelKomponenten, titelHolen } from '../radio.js';
|
||||
|
||||
export const data = new SlashCommandBuilder()
|
||||
.setName('radio')
|
||||
.setDescription('Radio im Sprachkanal — Sender wählen und umschalten');
|
||||
|
||||
export async function execute(interaction) {
|
||||
if (!moduleEnabled('radio')) {
|
||||
await interaction.reply({
|
||||
content: '❌ Radio ist ausgeschaltet — im Panel unter **Community** einschaltbar.',
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const stationen = listStations();
|
||||
if (stationen.length === 0) {
|
||||
await interaction.reply({
|
||||
content: '❌ Es sind noch keine Sender eingetragen — im Panel unter **Community → Radio**.',
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const kanal = interaction.member?.voice?.channel;
|
||||
const schonAn = laeuft(interaction.guild.id);
|
||||
if (!kanal && !schonAn) {
|
||||
await interaction.reply({
|
||||
content: '❌ Geh erst in einen Sprachkanal.',
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Darf der Bot da überhaupt rein? Sonst hängt der Verbindungsaufbau, bis
|
||||
// die Interaktion abgelaufen ist, und der Nutzer sieht nur „Fehler".
|
||||
if (kanal) {
|
||||
const rechte = kanal.permissionsFor(interaction.guild.members.me);
|
||||
if (!rechte?.has('Connect') || !rechte?.has('Speak')) {
|
||||
await interaction.reply({
|
||||
content: `❌ Ich darf in <#${kanal.id}> nicht rein oder nicht sprechen.`,
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Verbinden dauert länger als die drei Sekunden, die Discord gibt
|
||||
await interaction.deferReply();
|
||||
|
||||
let station = schonAn ? getStation(schonAn.stationId) : null;
|
||||
let titel = schonAn?.titel ?? null;
|
||||
if (!schonAn) {
|
||||
station = stationen[0];
|
||||
try {
|
||||
await spielen(kanal, station);
|
||||
} catch (error) {
|
||||
console.error('[radio]', error);
|
||||
await interaction.editReply('❌ Verbindung in den Sprachkanal fehlgeschlagen.');
|
||||
return;
|
||||
}
|
||||
({ titel } = await titelHolen(station.url));
|
||||
const s = laeuft(interaction.guild.id);
|
||||
if (s) s.titel = titel;
|
||||
}
|
||||
|
||||
const nachricht = await interaction.editReply({
|
||||
embeds: [tafelEmbed(interaction.client, station, titel, kanal?.id ?? schonAn?.channelId)],
|
||||
components: tafelKomponenten(stationen, station?.id),
|
||||
});
|
||||
|
||||
// Die Tafel merken, damit der Titel-Melder sie später bearbeiten kann
|
||||
setRadioState({
|
||||
guildId: interaction.guild.id,
|
||||
channelId: kanal?.id ?? schonAn.channelId,
|
||||
stationId: station?.id ?? null,
|
||||
panelChannelId: nachricht.channelId ?? interaction.channelId,
|
||||
panelMessageId: nachricht.id,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user