Die zweite Tafel: /musik, Warteschlange, Eingabefenster
Eine eigene Nachricht statt ein paar Knoepfe mehr an der Radio-Tafel. Discord laesst fuenf Reihen je Nachricht zu, vier davon gehoeren dort den Sender-Menues -- fuer Ueberspringen, Einreihen, Warteschlange und Leeren ist kein Platz, und ein Sender-Menue zu opfern hiesse, Sender aus der Auswahl zu werfen. Der Vorzug ist groesser als die Notloesung: /radio bleibt genau das, was es war. Wer nie ein Video einreiht, merkt vom ganzen Umbau nichts. Bei einem Suchbegriff wird bewusst nicht der erste Treffer genommen, sondern die Auswahl gezeigt: "Sleeping Sun" hat drei Fassungen, und welche gemeint war, weiss nur wer es getippt hat. Eine Adresse geht direkt rein, eine Playlist mit allen Eintraegen auf einmal. Die Abhaengigkeit zeigt nur in eine Richtung: radio-musik.js kennt das Radio, das Radio kennt es nicht -- es meldet ueber beiAenderung() nur, dass sich etwas geaendert hat. Ein gegenseitiger Import zwischen zwei Dateien beisst beim naechsten Umbau. Gemessen gegen echtes YouTube am 28.08.2026: Adresse landet in der Liste, Suchbegriff liefert fuenf Treffer und reiht NICHTS ein (Liste nachweislich unveraendert), kaputte Adresse meldet "Video unavailable" und laesst die Liste in Ruhe. Die Tafel zeigt Dauer je Titel, Gesamtdauer, Wunschgeber und die letzte Fehlerzeile im Klartext. Nicht geprueft, weil es dafuer eine echte Discord-Sprachverbindung braucht: die Darstellung waehrend etwas laeuft, und das Weiterschalten.
This commit is contained in:
@@ -0,0 +1,463 @@
|
||||
// Die zweite Tafel: YouTube und die Warteschlange.
|
||||
//
|
||||
// Warum eine eigene Nachricht und nicht ein paar Knöpfe mehr an der
|
||||
// Radio-Tafel: Discord lässt fünf Reihen je Nachricht zu, und vier davon
|
||||
// gehören dort den Sender-Menüs. Für „Überspringen", „Einreihen",
|
||||
// „Warteschlange" und „Leeren" ist da schlicht kein Platz — und ein
|
||||
// Sender-Menü zu opfern hiesse, Sender aus der Auswahl zu werfen.
|
||||
//
|
||||
// Zwei Tafeln haben ausserdem einen Vorzug, der nicht bloss Notlösung ist:
|
||||
// `/radio` bleibt genau das, was es war. Wer nie ein Video einreiht, merkt
|
||||
// von diesem ganzen Umbau nichts.
|
||||
//
|
||||
// Die Abhängigkeit zeigt bewusst nur in eine Richtung: diese Datei kennt das
|
||||
// Radio, das Radio kennt sie nicht. Es meldet über `beiAenderung()` nur, dass
|
||||
// sich etwas geändert hat. Ein gegenseitiger Import zwischen zwei Dateien ist
|
||||
// etwas, das beim Lesen niemand erwartet und beim Umbauen jeden beisst.
|
||||
import {
|
||||
ActionRowBuilder, ButtonBuilder, ButtonStyle, MessageFlags,
|
||||
ModalBuilder, TextInputBuilder, TextInputStyle,
|
||||
StringSelectMenuBuilder, StringSelectMenuOptionBuilder,
|
||||
} from 'discord.js';
|
||||
import { brandEmbed } from '../embeds.js';
|
||||
import { brandFooter, youtubeAn } from '../runtime-settings.js';
|
||||
import { moduleEnabled } from '../modules.js';
|
||||
import {
|
||||
queueList, queueAdd, queueRemove, queueClear, queueVor, queueStand,
|
||||
ytPanel, setYtPanel, clearYtPanel, getStation,
|
||||
} from '../db.js';
|
||||
import {
|
||||
laeuft, weiter, spielen, beiAenderung, tafelAktualisieren, tafelnMelden, radioQuelle,
|
||||
} from './radio.js';
|
||||
import {
|
||||
aufloesen, ytdlpVorhanden, letzterFehler, TREFFER,
|
||||
} from './youtube.js';
|
||||
|
||||
// Discord lässt 25 Einträge je Auswahlmenü zu. Die Warteschlange kann länger
|
||||
// sein — dann zeigen die Menüs die ersten 25, und der Rest wird über das
|
||||
// Web-Panel verwaltet. Das steht auch so in der Tafel, statt stillschweigend
|
||||
// zu unterschlagen, dass da noch etwas ist.
|
||||
const JE_MENU = 25;
|
||||
|
||||
// ── Darstellung ───────────────────────────────────────────────────────────
|
||||
|
||||
/** 3:07 statt 187 */
|
||||
export function dauerText(sekunden) {
|
||||
if (sekunden === null || sekunden === undefined) return 'live';
|
||||
const s = Math.max(0, Math.round(sekunden));
|
||||
const h = Math.floor(s / 3600);
|
||||
const m = Math.floor((s % 3600) / 60);
|
||||
const rest = s % 60;
|
||||
return h > 0
|
||||
? `${h}:${String(m).padStart(2, '0')}:${String(rest).padStart(2, '0')}`
|
||||
: `${m}:${String(rest).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ein Balken für den Fortschritt.
|
||||
*
|
||||
* Nur wo er etwas aussagen kann: ohne bekannte Dauer (Livestream) gäbe er
|
||||
* einen Stand vor, den niemand kennt. Dann steht dort lieber nichts.
|
||||
*/
|
||||
function fortschritt(verstrichen, dauer) {
|
||||
if (!dauer || dauer <= 0) return null;
|
||||
const anteil = Math.min(1, Math.max(0, verstrichen / dauer));
|
||||
const voll = Math.round(anteil * 12);
|
||||
return `\`${'━'.repeat(voll)}${'─'.repeat(12 - voll)}\` ${dauerText(verstrichen)} / ${dauerText(dauer)}`;
|
||||
}
|
||||
|
||||
/** Titel kürzen, damit Discord die Nachricht nicht ablehnt */
|
||||
const kurz = (text, n) => (String(text ?? '').length > n
|
||||
? `${String(text).slice(0, n - 1)}…`
|
||||
: String(text ?? ''));
|
||||
|
||||
export function musikEmbed(client, guildId) {
|
||||
const embed = brandEmbed(client, 'RADIO')
|
||||
.setTitle('▶️ Musik')
|
||||
.setFooter({ text: brandFooter('RADIO') })
|
||||
.setTimestamp();
|
||||
|
||||
// Ein fehlendes yt-dlp ist die häufigste Ursache und die am schlechtesten
|
||||
// zu erratende: alles sieht normal aus, es passiert nur nichts. Deshalb
|
||||
// steht sie ganz oben und nicht in einer Fussnote.
|
||||
if (ytdlpVorhanden() === false) {
|
||||
return embed.setDescription('⚠️ Auf dem Server fehlt **yt-dlp** — ohne das kann der Bot '
|
||||
+ 'kein YouTube abspielen. Die Radiosender laufen davon unberührt weiter.');
|
||||
}
|
||||
if (!youtubeAn()) {
|
||||
return embed.setDescription('YouTube ist ausgeschaltet — im Panel unter '
|
||||
+ '**Community → Radio** einschalten.');
|
||||
}
|
||||
|
||||
const jetzt = laeuft(guildId);
|
||||
const liste = queueList(guildId);
|
||||
const stand = queueStand(guildId);
|
||||
|
||||
if (jetzt?.quelle?.art === 'youtube') {
|
||||
const t = jetzt.quelle.titel;
|
||||
const verstrichen = Math.floor((Date.now() - jetzt.seit) / 1000);
|
||||
const balken = fortschritt(verstrichen, t.dauer);
|
||||
embed.setDescription(`**${kurz(t.titel, 200)}**\n`
|
||||
+ `${t.kanal ? `_${kurz(t.kanal, 80)}_\n` : ''}`
|
||||
+ (balken ? `${balken}\n` : '🔴 _Livestream_\n')
|
||||
+ `in <#${jetzt.channelId}>`);
|
||||
if (t.thumb) embed.setThumbnail(t.thumb);
|
||||
} else if (jetzt) {
|
||||
// Es läuft etwas, nur eben ein Sender. Das ist kein Fehler und wird
|
||||
// auch nicht als einer dargestellt.
|
||||
const heim = jetzt.heimStationId ? getStation(jetzt.heimStationId) : null;
|
||||
embed.setDescription(`📻 Gerade läuft das Radio${heim ? ` (**${heim.name}**)` : ''}.\n`
|
||||
+ 'Was hier eingereiht wird, kommt als Nächstes dran.');
|
||||
} else {
|
||||
embed.setDescription(liste.length > 0
|
||||
? 'Nichts läuft. **Überspringen** startet die Warteschlange — '
|
||||
+ 'geh vorher in einen Sprachkanal.'
|
||||
: 'Nichts läuft und nichts eingereiht. **Einreihen…** nimmt einen '
|
||||
+ 'YouTube-Link oder einen Suchbegriff.');
|
||||
}
|
||||
|
||||
if (liste.length > 0) {
|
||||
const zeilen = liste.slice(0, 10).map((e, i) =>
|
||||
`\`${String(i + 1).padStart(2)}.\` ${kurz(e.titel, 60)} \`${dauerText(e.dauer)}\``
|
||||
+ (e.wunsch_von ? ` — _${kurz(e.wunsch_von, 24)}_` : ''));
|
||||
if (liste.length > 10) zeilen.push(`_… und ${liste.length - 10} weitere_`);
|
||||
embed.addFields({
|
||||
name: `Warteschlange — ${stand.n} Titel, ${dauerText(stand.dauer)} gesamt`,
|
||||
value: zeilen.join('\n').slice(0, 1024),
|
||||
});
|
||||
}
|
||||
|
||||
// Der letzte Fehlschlag, im Klartext. Genau das ist der Unterschied
|
||||
// zwischen „YouTube geht nicht" und „YouTube verlangt eine Anmeldung".
|
||||
const fehler = letzterFehler();
|
||||
if (fehler && !jetzt) {
|
||||
embed.addFields({
|
||||
name: '⚠️ Zuletzt schiefgegangen',
|
||||
value: `\`\`\`${kurz(fehler.text, 300)}\`\`\``,
|
||||
});
|
||||
}
|
||||
return embed;
|
||||
}
|
||||
|
||||
const knopf = (id, label, emoji, stil = ButtonStyle.Secondary, aus = false) =>
|
||||
new ButtonBuilder().setCustomId(id).setLabel(label).setEmoji(emoji)
|
||||
.setStyle(stil).setDisabled(aus);
|
||||
|
||||
export function musikKomponenten(guildId) {
|
||||
const liste = queueList(guildId).slice(0, JE_MENU);
|
||||
const jetzt = laeuft(guildId);
|
||||
const reihen = [];
|
||||
|
||||
if (liste.length > 0) {
|
||||
const optionen = (praefix) => liste.map((e, i) => {
|
||||
const o = new StringSelectMenuOptionBuilder()
|
||||
.setLabel(kurz(`${i + 1}. ${e.titel}`, 100))
|
||||
.setValue(String(e.id));
|
||||
const unten = [dauerText(e.dauer), e.kanal].filter(Boolean).join(' · ');
|
||||
if (unten) o.setDescription(kurz(unten, 100));
|
||||
return o;
|
||||
});
|
||||
reihen.push(new ActionRowBuilder().addComponents(
|
||||
new StringSelectMenuBuilder().setCustomId('musik_vor')
|
||||
.setPlaceholder('⏭️ Als Nächstes spielen …').addOptions(optionen())
|
||||
));
|
||||
reihen.push(new ActionRowBuilder().addComponents(
|
||||
new StringSelectMenuBuilder().setCustomId('musik_weg')
|
||||
.setPlaceholder('🗑️ Aus der Liste nehmen …').addOptions(optionen())
|
||||
));
|
||||
}
|
||||
|
||||
// Am Anschlag ausgrauen statt stumm nichts zu tun — dieselbe Regel wie
|
||||
// an der Radio-Tafel: ein Knopf, der sich drücken lässt und nichts
|
||||
// bewirkt, sieht aus wie ein kaputter Knopf.
|
||||
const nichtsDa = liste.length === 0;
|
||||
reihen.push(new ActionRowBuilder().addComponents(
|
||||
knopf('musik_add', 'Einreihen…', '▶️', ButtonStyle.Success),
|
||||
knopf('musik_skip', 'Überspringen', '⏭️', ButtonStyle.Primary,
|
||||
nichtsDa && jetzt?.quelle?.art !== 'youtube'),
|
||||
knopf('musik_leeren', 'Leeren', '🗑️', ButtonStyle.Danger, nichtsDa),
|
||||
knopf('musik_heim', 'Zum Radio', '📻', ButtonStyle.Secondary,
|
||||
!jetzt || jetzt.quelle?.art !== 'youtube' || !jetzt.heimStationId),
|
||||
knopf('musik_refresh', 'Aktualisieren', '🔄'),
|
||||
));
|
||||
return reihen;
|
||||
}
|
||||
|
||||
/** Die gespeicherte Tafel neu zeichnen — für den Melder aus `radio.js` */
|
||||
export async function musikTafelAktualisieren(client, guildId) {
|
||||
const p = ytPanel(guildId);
|
||||
if (!p) return;
|
||||
const kanal = await client.channels.fetch(p.channel_id).catch(() => null);
|
||||
const nachricht = await kanal?.messages?.fetch(p.message_id).catch(() => null);
|
||||
if (!nachricht) {
|
||||
// Die Nachricht ist weg (gelöscht, Kanal aufgeräumt). Den Verweis
|
||||
// stehenzulassen hiesse, es bei jeder Änderung erneut zu versuchen.
|
||||
clearYtPanel(guildId);
|
||||
return;
|
||||
}
|
||||
await nachricht.edit({
|
||||
embeds: [musikEmbed(client, guildId)],
|
||||
components: musikKomponenten(guildId),
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
// ── Einreihen ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Eingabe auflösen und einreihen — die eine Stelle für Discord *und* Panel.
|
||||
*
|
||||
* Bei einem Suchbegriff wird bewusst nicht der erste Treffer genommen,
|
||||
* sondern die Auswahl zurückgegeben: „Sleeping Sun" hat drei Fassungen, und
|
||||
* welche gemeint war, weiss nur der, der es getippt hat. Bei einer Adresse
|
||||
* gibt es nichts zu wählen — die geht direkt rein, eine Playlist mit allen
|
||||
* Einträgen auf einmal.
|
||||
*
|
||||
* @returns {Promise<{eingereiht?: object[], treffer?: object[], fehler?: string}>}
|
||||
*/
|
||||
export async function einreihen(guildId, eingabe, { wunschVon = null } = {}) {
|
||||
if (!youtubeAn()) return { fehler: 'YouTube ist ausgeschaltet.' };
|
||||
const r = await aufloesen(eingabe, { max: TREFFER });
|
||||
if (r.fehler) return { fehler: r.fehler };
|
||||
if (r.suche) return { treffer: r.treffer };
|
||||
return { eingereiht: eintraegeAufnehmen(guildId, r.treffer, wunschVon) };
|
||||
}
|
||||
|
||||
/** Fertige Einträge in die Liste schreiben */
|
||||
export function eintraegeAufnehmen(guildId, eintraege, wunschVon = null) {
|
||||
for (const e of eintraege) queueAdd(guildId, { ...e, wunsch_von: wunschVon });
|
||||
return eintraege;
|
||||
}
|
||||
|
||||
/**
|
||||
* Läuft nichts, soll das Einreihen sofort Musik machen.
|
||||
*
|
||||
* Sonst legt jemand drei Titel in eine Liste, es passiert nichts, und der
|
||||
* naheliegende Schluss wäre „kaputt". Läuft dagegen schon etwas — Sender oder
|
||||
* Titel —, wird nicht dazwischengefunkt: die Liste kommt dran, wenn sie dran
|
||||
* ist.
|
||||
*/
|
||||
export async function ggfAnwerfen(client, guildId, kanalId) {
|
||||
if (laeuft(guildId) || !kanalId) return false;
|
||||
const q = await weiter(client, guildId, { erzwungen: true, kanalId });
|
||||
return Boolean(q);
|
||||
}
|
||||
|
||||
// ── Suchtreffer zwischenlagern ────────────────────────────────────────────
|
||||
//
|
||||
// Ein Discord-Auswahlmenü gibt nur die `value` zurück, und da passt keine
|
||||
// vollständige Auskunft hinein. Also bleiben die Treffer kurz hier liegen und
|
||||
// die Kennung wandert in die customId.
|
||||
//
|
||||
// Mit Verfallszeit, weil sonst jede Suche für immer Speicher belegt — bei
|
||||
// einem Bot, der monatelang läuft, ist das kein theoretischer Einwand.
|
||||
const treffermerker = new Map(); // token → { treffer, zeit }
|
||||
const MERK_DAUER = 5 * 60_000;
|
||||
|
||||
function trefferMerken(token, treffer) {
|
||||
const grenze = Date.now() - MERK_DAUER;
|
||||
for (const [k, v] of treffermerker) if (v.zeit < grenze) treffermerker.delete(k);
|
||||
treffermerker.set(token, { treffer, zeit: Date.now() });
|
||||
}
|
||||
|
||||
function trefferHolen(token) {
|
||||
const e = treffermerker.get(token);
|
||||
if (!e || e.zeit < Date.now() - MERK_DAUER) return null;
|
||||
return e.treffer;
|
||||
}
|
||||
|
||||
/** Die flüchtige Auswahl nach einer Suche */
|
||||
function trefferAuswahl(token, treffer) {
|
||||
return new ActionRowBuilder().addComponents(
|
||||
new StringSelectMenuBuilder()
|
||||
.setCustomId(`musik_treffer:${token}`)
|
||||
.setPlaceholder('Welchen davon?')
|
||||
.addOptions(treffer.map((t, i) => {
|
||||
const o = new StringSelectMenuOptionBuilder()
|
||||
.setLabel(kurz(t.titel, 100))
|
||||
.setValue(String(i));
|
||||
const unten = [dauerText(t.dauer), t.kanal].filter(Boolean).join(' · ');
|
||||
if (unten) o.setDescription(kurz(unten, 100));
|
||||
return o;
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
// ── Anbindung ─────────────────────────────────────────────────────────────
|
||||
|
||||
export function startMusik() {
|
||||
// Damit ein Titelwechsel, ein Stopp oder ein Überspringen auch hier
|
||||
// ankommt — ohne dass `radio.js` diese Datei kennen muss.
|
||||
beiAenderung(musikTafelAktualisieren);
|
||||
}
|
||||
|
||||
/** Wo darf gespielt werden: der eigene Kanal, sonst der laufende */
|
||||
async function zielKanal(interaction) {
|
||||
const eigener = interaction.member?.voice?.channel;
|
||||
if (eigener) return eigener;
|
||||
const jetzt = laeuft(interaction.guild?.id);
|
||||
if (!jetzt) return null;
|
||||
return interaction.client.channels.fetch(jetzt.channelId).catch(() => null);
|
||||
}
|
||||
|
||||
const IDS = ['musik_add', 'musik_skip', 'musik_leeren', 'musik_heim', 'musik_refresh',
|
||||
'musik_vor', 'musik_weg'];
|
||||
|
||||
/** Knöpfe, Menüs und das Eingabefenster der Musik-Tafel */
|
||||
export async function handleMusikInteraction(interaction) {
|
||||
const { customId } = interaction;
|
||||
const istTreffer = customId.startsWith('musik_treffer:');
|
||||
if (customId !== 'musik_modal' && !istTreffer && !IDS.includes(customId)) return false;
|
||||
|
||||
const guildId = interaction.guild?.id;
|
||||
|
||||
// Das Eingabefenster muss *vor* jedem Aufschub kommen: Discord lässt ein
|
||||
// Modal nur als erste Antwort auf eine Interaktion zu.
|
||||
if (customId === 'musik_add') {
|
||||
await interaction.showModal(new ModalBuilder()
|
||||
.setCustomId('musik_modal')
|
||||
.setTitle('Einreihen')
|
||||
.addComponents(new ActionRowBuilder().addComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId('eingabe')
|
||||
.setLabel('YouTube-Link oder Suchbegriff')
|
||||
.setPlaceholder('https://youtu.be/… oder: nightwish sleeping sun')
|
||||
.setStyle(TextInputStyle.Short)
|
||||
.setMaxLength(300)
|
||||
.setRequired(true)
|
||||
)));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Auflösen dauert länger als die drei Sekunden, die Discord gibt.
|
||||
if (customId === 'musik_modal' || istTreffer) {
|
||||
// Beim Eingabefenster gibt es noch keine Nachricht, also eine neue
|
||||
// (flüchtige). Bei der Trefferauswahl gibt es sie — dann wird sie
|
||||
// ersetzt, statt eine zweite daneben zu stellen und die Auswahl
|
||||
// stehen zu lassen, als könnte man noch einmal wählen.
|
||||
await (istTreffer
|
||||
? interaction.deferUpdate()
|
||||
: interaction.deferReply({ flags: MessageFlags.Ephemeral }));
|
||||
const kanal = await zielKanal(interaction);
|
||||
|
||||
let eintraege = null;
|
||||
if (istTreffer) {
|
||||
const treffer = trefferHolen(customId.split(':')[1]);
|
||||
if (!treffer) {
|
||||
await interaction.editReply('❌ Die Auswahl ist abgelaufen — bitte neu suchen.');
|
||||
return true;
|
||||
}
|
||||
const gewaehlt = treffer[Number(interaction.values[0])];
|
||||
if (!gewaehlt) {
|
||||
await interaction.editReply('❌ Diesen Treffer gibt es nicht mehr.');
|
||||
return true;
|
||||
}
|
||||
eintraege = eintraegeAufnehmen(guildId, [gewaehlt],
|
||||
interaction.member?.displayName ?? interaction.user?.username);
|
||||
} else {
|
||||
const eingabe = interaction.fields.getTextInputValue('eingabe');
|
||||
const r = await einreihen(guildId, eingabe, {
|
||||
wunschVon: interaction.member?.displayName ?? interaction.user?.username,
|
||||
});
|
||||
if (r.fehler) {
|
||||
await interaction.editReply(`❌ ${r.fehler}`);
|
||||
return true;
|
||||
}
|
||||
if (r.treffer) {
|
||||
const token = interaction.id;
|
||||
trefferMerken(token, r.treffer);
|
||||
await interaction.editReply({
|
||||
content: `🔎 ${r.treffer.length} Treffer:`,
|
||||
components: [trefferAuswahl(token, r.treffer)],
|
||||
});
|
||||
return true;
|
||||
}
|
||||
eintraege = r.eingereiht;
|
||||
}
|
||||
|
||||
const angeworfen = await ggfAnwerfen(interaction.client, guildId, kanal?.id);
|
||||
await interaction.editReply({
|
||||
content: eintraege.length === 1
|
||||
? `✅ **${kurz(eintraege[0].titel, 120)}** ${angeworfen ? 'läuft jetzt' : 'eingereiht'}.`
|
||||
: `✅ ${eintraege.length} Titel eingereiht${angeworfen ? ' — der erste läuft' : ''}.`
|
||||
+ (kanal ? '' : '\n_Geh in einen Sprachkanal, damit es losgeht._'),
|
||||
components: [],
|
||||
});
|
||||
// Hier zeichnet `tafelnMelden` beide Tafeln: die Antwort oben war
|
||||
// flüchtig und ist keine von ihnen.
|
||||
await tafelnMelden(interaction.client, guildId);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Ab hier: alles, was die Tafel an Ort und Stelle neu zeichnet.
|
||||
if (customId === 'musik_skip') {
|
||||
const kanal = await zielKanal(interaction);
|
||||
if (!kanal) {
|
||||
await interaction.reply({
|
||||
content: '❌ Geh erst in einen Sprachkanal.',
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
// Aufgeschoben, weil ein Quellenwechsel eine Verbindung aufbauen kann
|
||||
// und das über den drei Sekunden liegt.
|
||||
await interaction.deferUpdate();
|
||||
await weiter(interaction.client, guildId, { erzwungen: true, kanalId: kanal.id });
|
||||
} else if (customId === 'musik_leeren') {
|
||||
queueClear(guildId);
|
||||
await interaction.deferUpdate();
|
||||
} else if (customId === 'musik_heim') {
|
||||
const jetzt = laeuft(guildId);
|
||||
const heim = jetzt?.heimStationId ? getStation(jetzt.heimStationId) : null;
|
||||
await interaction.deferUpdate();
|
||||
if (heim) {
|
||||
const kanal = await interaction.client.channels.fetch(jetzt.channelId).catch(() => null);
|
||||
// Die Warteschlange bleibt stehen: „zum Radio" heisst „jetzt
|
||||
// wieder Sender", nicht „alles wegwerfen".
|
||||
if (kanal) await spielen(kanal, radioQuelle(heim));
|
||||
}
|
||||
} else if (customId === 'musik_vor') {
|
||||
queueVor(guildId, Number(interaction.values[0]));
|
||||
await interaction.deferUpdate();
|
||||
} else if (customId === 'musik_weg') {
|
||||
queueRemove(Number(interaction.values[0]));
|
||||
await interaction.deferUpdate();
|
||||
} else {
|
||||
await interaction.deferUpdate(); // musik_refresh
|
||||
}
|
||||
|
||||
await interaction.editReply({
|
||||
embeds: [musikEmbed(interaction.client, guildId)],
|
||||
components: musikKomponenten(guildId),
|
||||
}).catch(() => {});
|
||||
// Die Radio-Tafel zeigt dieselbe Wirklichkeit und muss mitgezogen werden.
|
||||
// Nur die Radio-Tafel: die Musik-Tafel ist die Nachricht, auf der gerade
|
||||
// geklickt wurde, und die ist eine Zeile weiter oben schon neu gezeichnet.
|
||||
// `tafelnMelden` würde sie ein zweites Mal anfassen — eine Anfrage an
|
||||
// Discord für nichts.
|
||||
await tafelAktualisieren(interaction.client, guildId).catch(() => {});
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Die Tafel im Kanal öffnen — von `/musik` aufgerufen */
|
||||
export async function musikTafelOeffnen(interaction) {
|
||||
if (!moduleEnabled('radio')) {
|
||||
await interaction.reply({
|
||||
content: '❌ Radio ist ausgeschaltet — im Panel unter **Community → Radio** einschalten.',
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
return;
|
||||
}
|
||||
await interaction.deferReply();
|
||||
const guildId = interaction.guild.id;
|
||||
const nachricht = await interaction.editReply({
|
||||
embeds: [musikEmbed(interaction.client, guildId)],
|
||||
components: musikKomponenten(guildId),
|
||||
});
|
||||
// Die alte Tafel verliert ihre Knöpfe nicht, aber ihren Anspruch: von hier
|
||||
// an wird die neue nachgeführt. Zwei Tafeln, von denen eine einfriert,
|
||||
// wären schlechter als eine, die umzieht.
|
||||
setYtPanel(guildId, nachricht.channelId ?? interaction.channelId, nachricht.id);
|
||||
}
|
||||
|
||||
// Nur zum Prüfen von aussen — die Merker-Verfallszeit ohne Wartezeit testbar.
|
||||
export const _treffermerker = treffermerker;
|
||||
Reference in New Issue
Block a user