Fix multiple server audio player handling
parent
581c6a074b
commit
9920e71aa8
|
@ -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)
|
||||
- Each theme has unique nicknames that the bot randomly assigns itself
|
||||
- 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
|
||||
|
||||
|
|
22
src/index.ts
22
src/index.ts
|
@ -16,6 +16,7 @@ import { getRandomInt, randomChance } from './utils/random';
|
|||
import { getRandomBehavior, getAudioFilePrefix } from './utils/bot-behaviors';
|
||||
import { getRandomNickname, getTheme } from './utils/themes';
|
||||
import { CatAudioPlayer } from './utils/audio-player';
|
||||
import { AudioPlayerManager } from './utils/audio-player-manager';
|
||||
import { guildSettings } from './utils/guild-settings';
|
||||
import { deployCommands } from './utils/command-deployer';
|
||||
import { commandsMap } from './commands';
|
||||
|
@ -35,11 +36,11 @@ const client = new Client({
|
|||
]
|
||||
});
|
||||
|
||||
// Initialize audio player
|
||||
const audioPlayer = new CatAudioPlayer();
|
||||
// Initialize audio player manager
|
||||
const audioPlayerManager = new AudioPlayerManager();
|
||||
|
||||
// Create status manager
|
||||
const statusManager = new StatusManager(client);
|
||||
const statusManager = new StatusManager(client, audioPlayerManager);
|
||||
|
||||
// Map to track voice channel join timers
|
||||
const voiceJoinTimers = new Map<string, NodeJS.Timeout>();
|
||||
|
@ -343,7 +344,7 @@ function scheduleNextVoiceChannelJoin(guildId: string, initialChannel?: VoiceCha
|
|||
const audioFilePrefix = getAudioFilePrefix(guildId);
|
||||
|
||||
// Join and play sounds
|
||||
await audioPlayer.joinChannel(
|
||||
await audioPlayerManager.joinChannel(
|
||||
chosenChannel,
|
||||
config.voiceChannelConfig.minStayDuration,
|
||||
config.voiceChannelConfig.maxStayDuration,
|
||||
|
@ -377,3 +378,16 @@ function scheduleNextVoiceChannelJoin(guildId: string, initialChannel?: VoiceCha
|
|||
|
||||
// Log in to Discord with your 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);
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -6,7 +6,8 @@ import {
|
|||
getVoiceConnection,
|
||||
joinVoiceChannel,
|
||||
VoiceConnectionStatus,
|
||||
NoSubscriberBehavior
|
||||
NoSubscriberBehavior,
|
||||
VoiceConnection
|
||||
} from '@discordjs/voice';
|
||||
import { VoiceChannel } from 'discord.js';
|
||||
import path from 'path';
|
||||
|
@ -19,6 +20,8 @@ export class CatAudioPlayer {
|
|||
private audioPlayer: AudioPlayer;
|
||||
private isPlaying: boolean = false;
|
||||
private shouldStop: boolean = false;
|
||||
private currentVoiceChannel: VoiceChannel | null = null;
|
||||
private currentConnection: VoiceConnection | null = null;
|
||||
|
||||
constructor() {
|
||||
this.audioPlayer = createAudioPlayer({
|
||||
|
@ -31,6 +34,29 @@ export class CatAudioPlayer {
|
|||
this.isPlaying = false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
@ -44,6 +70,15 @@ export class CatAudioPlayer {
|
|||
audioFilePrefix: string = ''
|
||||
): Promise<void> {
|
||||
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
|
||||
// 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
|
||||
|
@ -53,6 +88,9 @@ export class CatAudioPlayer {
|
|||
// @ts-ignore: Type compatibility issue between Discord.js and @discordjs/voice
|
||||
adapterCreator: voiceChannel.guild.voiceAdapterCreator,
|
||||
});
|
||||
|
||||
// Store the connection
|
||||
this.currentConnection = connection;
|
||||
|
||||
// Handle connection events
|
||||
connection.on(VoiceConnectionStatus.Disconnected, async () => {
|
||||
|
@ -65,6 +103,8 @@ export class CatAudioPlayer {
|
|||
} catch (error) {
|
||||
// If we can't reconnect within 5 seconds, destroy the connection
|
||||
connection.destroy();
|
||||
this.currentVoiceChannel = null;
|
||||
this.currentConnection = null;
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -77,7 +117,7 @@ export class CatAudioPlayer {
|
|||
// Determine how long the cat will stay
|
||||
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
|
||||
this.playRandomCatSounds(minMeowInterval, maxMeowInterval, stayDuration, audioFilePrefix);
|
||||
|
@ -88,16 +128,19 @@ export class CatAudioPlayer {
|
|||
|
||||
// Wait for any current sound to finish
|
||||
setTimeout(() => {
|
||||
const connection = getVoiceConnection(voiceChannel.guild.id);
|
||||
if (connection) {
|
||||
connection.destroy();
|
||||
console.log(`Cat left ${voiceChannel.name}`);
|
||||
if (this.currentConnection) {
|
||||
this.currentConnection.destroy();
|
||||
console.log(`Bot left ${voiceChannel.name} in ${voiceChannel.guild.name}`);
|
||||
this.currentVoiceChannel = null;
|
||||
this.currentConnection = null;
|
||||
}
|
||||
}, 1000);
|
||||
}, stayDuration);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error joining voice channel:', error);
|
||||
this.currentVoiceChannel = null;
|
||||
this.currentConnection = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,13 +2,16 @@ import { ActivityType, Client } from 'discord.js';
|
|||
import { config } from '../config/config';
|
||||
import { getRandomElement } from './random';
|
||||
import { getAvailableThemeIds, getTheme } from './themes';
|
||||
import { AudioPlayerManager } from './audio-player-manager';
|
||||
|
||||
export class StatusManager {
|
||||
private client: Client;
|
||||
private audioPlayerManager: AudioPlayerManager;
|
||||
private statusUpdateInterval: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor(client: Client) {
|
||||
constructor(client: Client, audioPlayerManager: AudioPlayerManager) {
|
||||
this.client = client;
|
||||
this.audioPlayerManager = audioPlayerManager;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -46,17 +49,7 @@ export class StatusManager {
|
|||
* Check if the bot is currently in any voice channel
|
||||
*/
|
||||
private isInVoiceChannel(): boolean {
|
||||
if (!this.client.guilds) return false;
|
||||
|
||||
// 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;
|
||||
return this.audioPlayerManager.isInAnyVoiceChannel();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue