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)
|
- 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
|
||||||
|
|
||||||
|
|
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 { 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);
|
||||||
|
});
|
||||||
|
|
|
@ -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,
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue