Fix multiple server audio player handling

main
Talon 2025-03-31 23:17:57 +02:00
parent 581c6a074b
commit 9920e71aa8
5 changed files with 144 additions and 22 deletions

View File

@ -33,6 +33,8 @@ A Discord bot that simulates various animal behaviors in your server! Choose fro
- Per-server configuration (each server can have its own theme) - Per-server configuration (each server can have its own theme)
- Each theme has unique nicknames that the bot randomly assigns itself - Each theme has unique nicknames that the bot randomly assigns itself
- Bot automatically changes its Discord nickname to match the theme - Bot automatically changes its Discord nickname to match the theme
- Support for multiple servers simultaneously with independent configurations
- Can join voice channels in multiple servers at the same time
## Setup ## Setup

View File

@ -16,6 +16,7 @@ import { getRandomInt, randomChance } from './utils/random';
import { getRandomBehavior, getAudioFilePrefix } from './utils/bot-behaviors'; import { getRandomBehavior, getAudioFilePrefix } from './utils/bot-behaviors';
import { getRandomNickname, getTheme } from './utils/themes'; import { getRandomNickname, getTheme } from './utils/themes';
import { CatAudioPlayer } from './utils/audio-player'; import { CatAudioPlayer } from './utils/audio-player';
import { AudioPlayerManager } from './utils/audio-player-manager';
import { guildSettings } from './utils/guild-settings'; import { guildSettings } from './utils/guild-settings';
import { deployCommands } from './utils/command-deployer'; import { deployCommands } from './utils/command-deployer';
import { commandsMap } from './commands'; import { commandsMap } from './commands';
@ -35,11 +36,11 @@ const client = new Client({
] ]
}); });
// Initialize audio player // Initialize audio player manager
const audioPlayer = new CatAudioPlayer(); const audioPlayerManager = new AudioPlayerManager();
// Create status manager // Create status manager
const statusManager = new StatusManager(client); const statusManager = new StatusManager(client, audioPlayerManager);
// Map to track voice channel join timers // Map to track voice channel join timers
const voiceJoinTimers = new Map<string, NodeJS.Timeout>(); const voiceJoinTimers = new Map<string, NodeJS.Timeout>();
@ -343,7 +344,7 @@ function scheduleNextVoiceChannelJoin(guildId: string, initialChannel?: VoiceCha
const audioFilePrefix = getAudioFilePrefix(guildId); const audioFilePrefix = getAudioFilePrefix(guildId);
// Join and play sounds // Join and play sounds
await audioPlayer.joinChannel( await audioPlayerManager.joinChannel(
chosenChannel, chosenChannel,
config.voiceChannelConfig.minStayDuration, config.voiceChannelConfig.minStayDuration,
config.voiceChannelConfig.maxStayDuration, config.voiceChannelConfig.maxStayDuration,
@ -377,3 +378,16 @@ function scheduleNextVoiceChannelJoin(guildId: string, initialChannel?: VoiceCha
// Log in to Discord with your token // Log in to Discord with your token
client.login(config.token); client.login(config.token);
// Handle graceful shutdown
process.on('SIGINT', () => {
console.log('Received SIGINT signal. Cleaning up...');
audioPlayerManager.cleanup();
process.exit(0);
});
process.on('SIGTERM', () => {
console.log('Received SIGTERM signal. Cleaning up...');
audioPlayerManager.cleanup();
process.exit(0);
});

View File

@ -0,0 +1,70 @@
import { CatAudioPlayer } from './audio-player';
import { VoiceChannel } from 'discord.js';
/**
* Manages audio players for multiple guilds
*/
export class AudioPlayerManager {
private audioPlayers: Map<string, CatAudioPlayer> = new Map();
/**
* Get an audio player for a specific guild
* Creates a new one if it doesn't exist
*/
public getAudioPlayer(guildId: string): CatAudioPlayer {
if (!this.audioPlayers.has(guildId)) {
this.audioPlayers.set(guildId, new CatAudioPlayer());
}
return this.audioPlayers.get(guildId)!;
}
/**
* Join a voice channel in a specific guild
*/
public async joinChannel(
voiceChannel: VoiceChannel,
minStayDuration: number,
maxStayDuration: number,
minMeowInterval: number,
maxMeowInterval: number,
audioFilePrefix: string = ''
): Promise<void> {
const guildId = voiceChannel.guild.id;
const audioPlayer = this.getAudioPlayer(guildId);
await audioPlayer.joinChannel(
voiceChannel,
minStayDuration,
maxStayDuration,
minMeowInterval,
maxMeowInterval,
audioFilePrefix
);
}
/**
* Check if the bot is in a voice channel in any guild
*/
public isInAnyVoiceChannel(): boolean {
// Check all managed audio players
for (const audioPlayer of this.audioPlayers.values()) {
if (audioPlayer.isInVoiceChannel()) {
return true;
}
}
return false;
}
/**
* Clean up any resources when the bot is shutting down
*/
public cleanup(): void {
for (const audioPlayer of this.audioPlayers.values()) {
audioPlayer.cleanup();
}
this.audioPlayers.clear();
}
}

View File

@ -6,7 +6,8 @@ import {
getVoiceConnection, getVoiceConnection,
joinVoiceChannel, joinVoiceChannel,
VoiceConnectionStatus, VoiceConnectionStatus,
NoSubscriberBehavior NoSubscriberBehavior,
VoiceConnection
} from '@discordjs/voice'; } from '@discordjs/voice';
import { VoiceChannel } from 'discord.js'; import { VoiceChannel } from 'discord.js';
import path from 'path'; import path from 'path';
@ -19,6 +20,8 @@ export class CatAudioPlayer {
private audioPlayer: AudioPlayer; private audioPlayer: AudioPlayer;
private isPlaying: boolean = false; private isPlaying: boolean = false;
private shouldStop: boolean = false; private shouldStop: boolean = false;
private currentVoiceChannel: VoiceChannel | null = null;
private currentConnection: VoiceConnection | null = null;
constructor() { constructor() {
this.audioPlayer = createAudioPlayer({ this.audioPlayer = createAudioPlayer({
@ -32,6 +35,29 @@ export class CatAudioPlayer {
}); });
} }
/**
* Check if this player is currently in a voice channel
*/
public isInVoiceChannel(): boolean {
return this.currentVoiceChannel !== null;
}
/**
* Clean up resources
*/
public cleanup(): void {
if (this.currentConnection) {
try {
this.currentConnection.destroy();
} catch (error) {
console.error('Error cleaning up voice connection:', error);
}
this.currentVoiceChannel = null;
this.currentConnection = null;
}
}
/** /**
* Join a voice channel and start playing cat sounds * Join a voice channel and start playing cat sounds
*/ */
@ -44,6 +70,15 @@ export class CatAudioPlayer {
audioFilePrefix: string = '' audioFilePrefix: string = ''
): Promise<void> { ): Promise<void> {
try { try {
// If already in a voice channel in this guild, leave it first
const existingConnection = getVoiceConnection(voiceChannel.guild.id);
if (existingConnection) {
existingConnection.destroy();
}
// Store the current voice channel
this.currentVoiceChannel = voiceChannel;
// Create connection - using Discord.js's built-in adapter // Create connection - using Discord.js's built-in adapter
// The @ts-ignore is required because of type compatibility issues between Discord.js and @discordjs/voice // The @ts-ignore is required because of type compatibility issues between Discord.js and @discordjs/voice
// This is a common issue and is safe to ignore in this case // This is a common issue and is safe to ignore in this case
@ -54,6 +89,9 @@ export class CatAudioPlayer {
adapterCreator: voiceChannel.guild.voiceAdapterCreator, adapterCreator: voiceChannel.guild.voiceAdapterCreator,
}); });
// Store the connection
this.currentConnection = connection;
// Handle connection events // Handle connection events
connection.on(VoiceConnectionStatus.Disconnected, async () => { connection.on(VoiceConnectionStatus.Disconnected, async () => {
try { try {
@ -65,6 +103,8 @@ export class CatAudioPlayer {
} catch (error) { } catch (error) {
// If we can't reconnect within 5 seconds, destroy the connection // If we can't reconnect within 5 seconds, destroy the connection
connection.destroy(); connection.destroy();
this.currentVoiceChannel = null;
this.currentConnection = null;
} }
}); });
@ -77,7 +117,7 @@ export class CatAudioPlayer {
// Determine how long the cat will stay // Determine how long the cat will stay
const stayDuration = getRandomInt(minStayDuration, maxStayDuration) * 1000; const stayDuration = getRandomInt(minStayDuration, maxStayDuration) * 1000;
console.log(`Cat joined ${voiceChannel.name} for ${stayDuration / 1000} seconds`); console.log(`Bot joined ${voiceChannel.name} in ${voiceChannel.guild.name} for ${stayDuration / 1000} seconds`);
// Start playing sounds // Start playing sounds
this.playRandomCatSounds(minMeowInterval, maxMeowInterval, stayDuration, audioFilePrefix); this.playRandomCatSounds(minMeowInterval, maxMeowInterval, stayDuration, audioFilePrefix);
@ -88,16 +128,19 @@ export class CatAudioPlayer {
// Wait for any current sound to finish // Wait for any current sound to finish
setTimeout(() => { setTimeout(() => {
const connection = getVoiceConnection(voiceChannel.guild.id); if (this.currentConnection) {
if (connection) { this.currentConnection.destroy();
connection.destroy(); console.log(`Bot left ${voiceChannel.name} in ${voiceChannel.guild.name}`);
console.log(`Cat left ${voiceChannel.name}`); this.currentVoiceChannel = null;
this.currentConnection = null;
} }
}, 1000); }, 1000);
}, stayDuration); }, stayDuration);
} catch (error) { } catch (error) {
console.error('Error joining voice channel:', error); console.error('Error joining voice channel:', error);
this.currentVoiceChannel = null;
this.currentConnection = null;
} }
} }

View File

@ -2,13 +2,16 @@ import { ActivityType, Client } from 'discord.js';
import { config } from '../config/config'; import { config } from '../config/config';
import { getRandomElement } from './random'; import { getRandomElement } from './random';
import { getAvailableThemeIds, getTheme } from './themes'; import { getAvailableThemeIds, getTheme } from './themes';
import { AudioPlayerManager } from './audio-player-manager';
export class StatusManager { export class StatusManager {
private client: Client; private client: Client;
private audioPlayerManager: AudioPlayerManager;
private statusUpdateInterval: NodeJS.Timeout | null = null; private statusUpdateInterval: NodeJS.Timeout | null = null;
constructor(client: Client) { constructor(client: Client, audioPlayerManager: AudioPlayerManager) {
this.client = client; this.client = client;
this.audioPlayerManager = audioPlayerManager;
} }
/** /**
@ -46,17 +49,7 @@ export class StatusManager {
* Check if the bot is currently in any voice channel * Check if the bot is currently in any voice channel
*/ */
private isInVoiceChannel(): boolean { private isInVoiceChannel(): boolean {
if (!this.client.guilds) return false; return this.audioPlayerManager.isInAnyVoiceChannel();
// Check if the bot is in a voice channel in any guild
for (const guild of this.client.guilds.cache.values()) {
const member = guild.members.cache.get(this.client.user?.id || '');
if (member?.voice.channel) {
return true;
}
}
return false;
} }
/** /**