diff --git a/server/src/plugins/radio/index.ts b/server/src/plugins/radio/index.ts index bad1fa0..54482a6 100644 --- a/server/src/plugins/radio/index.ts +++ b/server/src/plugins/radio/index.ts @@ -7,7 +7,6 @@ import { import type { VoiceBasedChannel } from 'discord.js'; import { ChannelType } from 'discord.js'; import type { Plugin, PluginContext } from '../../core/plugin.js'; -import { claimVoice, releaseVoice } from '../../core/plugin.js'; import { sseBroadcast } from '../../core/sse.js'; import { getState, setState } from '../../core/persistence.js'; import { @@ -67,9 +66,8 @@ function stopStream(guildId: string): void { if (!state) return; try { state.ffmpeg.kill('SIGKILL'); } catch {} try { state.player.stop(true); } catch {} - try { getVoiceConnection(guildId)?.destroy(); } catch {} + try { getVoiceConnection(guildId, 'radio')?.destroy(); } catch {} guildRadioState.delete(guildId); - releaseVoice(guildId, 'radio'); broadcastState(guildId); console.log(`[Radio] Stopped stream in guild ${guildId}`); } @@ -97,6 +95,7 @@ async function startStream( guildId, adapterCreator: guild.voiceAdapterCreator, selfDeaf: true, + group: 'radio', }); try { @@ -128,7 +127,7 @@ async function startStream( if (guildRadioState.has(guildId)) { console.log(`[Radio] ffmpeg exited (code ${code}), cleaning up`); guildRadioState.delete(guildId); - try { connection.destroy(); } catch {} + try { getVoiceConnection(guildId, 'radio')?.destroy(); } catch {} broadcastState(guildId); } }); @@ -150,9 +149,6 @@ async function startStream( stopStream(guildId); }); - // Claim voice for this guild (stops other plugins like soundboard) - claimVoice(guildId, 'radio', () => stopStream(guildId)); - // State tracken const channelName = 'name' in channel ? (channel as any).name : voiceChannelId; guildRadioState.set(guildId, { diff --git a/server/src/plugins/soundboard/index.ts b/server/src/plugins/soundboard/index.ts index 75bb3c3..70020c4 100644 --- a/server/src/plugins/soundboard/index.ts +++ b/server/src/plugins/soundboard/index.ts @@ -16,7 +16,6 @@ import sodium from 'libsodium-wrappers'; import nacl from 'tweetnacl'; import { ChannelType, Events, type VoiceState, type Message } from 'discord.js'; import type { Plugin, PluginContext } from '../../core/plugin.js'; -import { claimVoice } from '../../core/plugin.js'; import { sseBroadcast } from '../../core/sse.js'; // ── Config (env) ── @@ -239,7 +238,7 @@ async function ensureConnectionReady(connection: VoiceConnection, channelId: str try { connection.destroy(); } catch {} guildAudioState.delete(guildId); console.log(`${SB} Creating fresh connection (attempt 3)...`); - const newConn = joinVoiceChannel({ channelId, guildId, adapterCreator: debugAdapterCreator(guild), selfMute: false, selfDeaf: false }); + const newConn = joinVoiceChannel({ channelId, guildId, adapterCreator: debugAdapterCreator(guild), selfMute: false, selfDeaf: false, group: 'soundboard' }); newConn.on('stateChange', (o: any, n: any) => console.log(`${SB} [fresh-conn] ${o.status} → ${n.status}`)); newConn.on('error', (err: any) => console.error(`${SB} [fresh-conn] ERROR: ${err?.message ?? err}`)); try { await entersState(newConn, VoiceConnectionStatus.Ready, 15_000); console.log(`${SB} Connection ready (fresh)`); return newConn; } @@ -270,13 +269,13 @@ function attachVoiceLifecycle(state: GuildAudioState, guild: any) { catch { if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { reconnectAttempts++; console.log(`${SB} Rejoin attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS}`); connection.rejoin({ channelId: state.channelId, selfDeaf: false, selfMute: false }); } else { reconnectAttempts = 0; console.log(`${SB} Max reconnect attempts reached, creating fresh connection`); try { connection.destroy(); } catch {} - const nc = joinVoiceChannel({ channelId: state.channelId, guildId: state.guildId, adapterCreator: debugAdapterCreator(guild), selfMute: false, selfDeaf: false }); + const nc = joinVoiceChannel({ channelId: state.channelId, guildId: state.guildId, adapterCreator: debugAdapterCreator(guild), selfMute: false, selfDeaf: false, group: 'soundboard' }); state.connection = nc; nc.subscribe(state.player); attachVoiceLifecycle(state, guild); } } } else if (newS.status === VoiceConnectionStatus.Destroyed) { console.warn(`${SB} Connection destroyed, recreating...`); connectedSince.delete(state.guildId); - const nc = joinVoiceChannel({ channelId: state.channelId, guildId: state.guildId, adapterCreator: debugAdapterCreator(guild), selfMute: false, selfDeaf: false }); + const nc = joinVoiceChannel({ channelId: state.channelId, guildId: state.guildId, adapterCreator: debugAdapterCreator(guild), selfMute: false, selfDeaf: false, group: 'soundboard' }); state.connection = nc; nc.subscribe(state.player); attachVoiceLifecycle(state, guild); } else if (newS.status === VoiceConnectionStatus.Connecting || newS.status === VoiceConnectionStatus.Signalling) { isReconnecting = true; @@ -287,7 +286,7 @@ function attachVoiceLifecycle(state: GuildAudioState, guild: any) { console.warn(`${SB} Timeout waiting for Ready from ${newS.status} (attempt ${reconnectAttempts}): ${(e as Error)?.message ?? e}`); if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { await new Promise(r => setTimeout(r, reconnectAttempts * 2000)); isReconnecting = false; connection.rejoin({ channelId: state.channelId, selfDeaf: false, selfMute: false }); } else { reconnectAttempts = 0; isReconnecting = false; console.error(`${SB} Max attempts from ${newS.status}, fresh connection`); try { connection.destroy(); } catch {} - const nc = joinVoiceChannel({ channelId: state.channelId, guildId: state.guildId, adapterCreator: debugAdapterCreator(guild), selfMute: false, selfDeaf: false }); + const nc = joinVoiceChannel({ channelId: state.channelId, guildId: state.guildId, adapterCreator: debugAdapterCreator(guild), selfMute: false, selfDeaf: false, group: 'soundboard' }); state.connection = nc; nc.subscribe(state.player); attachVoiceLifecycle(state, guild); } } } @@ -333,8 +332,6 @@ let _pluginCtx: PluginContext | null = null; async function playFilePath(guildId: string, channelId: string, filePath: string, volume?: number, relativeKey?: string): Promise { console.log(`${SB} playFilePath: guild=${guildId} channel=${channelId} file=${path.basename(filePath)} vol=${volume ?? 'default'}`); - // Claim voice for this guild (stops radio if playing) - claimVoice(guildId, 'soundboard', () => { /* soundboard cleanup handled by lifecycle */ }); const ctx = _pluginCtx!; const guild = ctx.client.guilds.cache.get(guildId); if (!guild) { console.error(`${SB} Guild ${guildId} not found in cache (cached: ${ctx.client.guilds.cache.map(g => g.id).join(', ')})`); throw new Error('Guild nicht gefunden'); } @@ -342,7 +339,7 @@ async function playFilePath(guildId: string, channelId: string, filePath: string let state = guildAudioState.get(guildId); if (!state) { console.log(`${SB} No existing audio state, creating new connection...`); - const connection = joinVoiceChannel({ channelId, guildId, adapterCreator: debugAdapterCreator(guild), selfMute: false, selfDeaf: false }); + const connection = joinVoiceChannel({ channelId, guildId, adapterCreator: debugAdapterCreator(guild), selfMute: false, selfDeaf: false, group: 'soundboard' }); // Debug: catch ALL state transitions and errors from the start connection.on('stateChange', (o: any, n: any) => { console.log(`${SB} [conn] ${o.status} → ${n.status}`); @@ -364,10 +361,10 @@ async function playFilePath(guildId: string, channelId: string, filePath: string // Channel-Wechsel try { - const current = getVoiceConnection(guildId); + const current = getVoiceConnection(guildId, 'soundboard'); if (current && current.joinConfig?.channelId !== channelId) { current.destroy(); - const connection = joinVoiceChannel({ channelId, guildId, adapterCreator: debugAdapterCreator(guild), selfMute: false, selfDeaf: false }); + const connection = joinVoiceChannel({ channelId, guildId, adapterCreator: debugAdapterCreator(guild), selfMute: false, selfDeaf: false, group: 'soundboard' }); const player = state.player ?? createAudioPlayer({ behaviors: { noSubscriber: NoSubscriberBehavior.Play } }); connection.subscribe(player); state = { connection, player, guildId, channelId, currentVolume: state.currentVolume ?? getPersistedVolume(guildId) }; @@ -377,8 +374,8 @@ async function playFilePath(guildId: string, channelId: string, filePath: string } } catch {} - if (!getVoiceConnection(guildId)) { - const connection = joinVoiceChannel({ channelId, guildId, adapterCreator: debugAdapterCreator(guild), selfMute: false, selfDeaf: false }); + if (!getVoiceConnection(guildId, 'soundboard')) { + const connection = joinVoiceChannel({ channelId, guildId, adapterCreator: debugAdapterCreator(guild), selfMute: false, selfDeaf: false, group: 'soundboard' }); const player = state?.player ?? createAudioPlayer({ behaviors: { noSubscriber: NoSubscriberBehavior.Play } }); connection.subscribe(player); state = { connection, player, guildId, channelId, currentVolume: state?.currentVolume ?? getPersistedVolume(guildId) };