fix(lavalink): Update join, play, and voice state handling for Shoukaku integration

This commit is contained in:
Jose Daniel G. Percy 2025-04-24 01:01:33 +08:00
parent 854cf12d64
commit 0d0125bf55
3 changed files with 82 additions and 58 deletions

View File

@ -1,4 +1,4 @@
const { SlashCommandBuilder, PermissionFlagsBits, ChannelType } = require('discord.js');
const { SlashCommandBuilder, PermissionFlagsBits, ChannelType, MessageFlags } = require('discord.js'); // Import MessageFlags
const logger = require('../utils/logger');
module.exports = {
@ -6,7 +6,8 @@ module.exports = {
.setName('join')
.setDescription('Joins your current voice channel'),
async execute(interaction, client) { // Added client parameter
await interaction.deferReply({ ephemeral: true }); // Defer reply as joining might take time
// Use flags for ephemeral deferral
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
const member = interaction.member;
const voiceChannel = member?.voice?.channel;
@ -29,44 +30,57 @@ module.exports = {
return interaction.editReply('I can only join standard voice channels.');
}
// 3. Create or get the player and connect
let player = client.manager.get(interaction.guildId);
// Get the initialized Shoukaku player manager from the client object
const musicPlayer = interaction.client.player;
if (!musicPlayer) {
logger.error('Music player not initialized on client object!');
return interaction.editReply('The music player is not ready yet. Please try again shortly.');
}
// 3. Get or create the player and connect using Shoukaku
let player = musicPlayer.getPlayer(interaction.guildId);
if (!player) {
try {
player = client.manager.create({
guild: interaction.guildId,
voiceChannel: voiceChannel.id,
textChannel: interaction.channelId, // Store the channel where command was used
selfDeafen: true, // Automatically deafen the bot
// selfMute: false, // Bot starts unmuted
// Create player using the Shoukaku manager
player = await musicPlayer.createPlayer({
guildId: interaction.guildId,
textChannel: interaction.channelId,
voiceChannel: voiceChannel.id
});
player.connect();
// Connection is handled within createPlayer
logger.info(`Created player and connected to voice channel ${voiceChannel.name} (${voiceChannel.id}) in guild ${interaction.guild.name} (${interaction.guildId})`);
await interaction.editReply(`Joined ${voiceChannel.name}! Ready to play music.`);
} catch (error) {
logger.error(`Failed to create/connect player for guild ${interaction.guildId}: ${error.message}`, error);
// Try to destroy player if partially created
if (player) player.destroy();
// Player destruction is handled internally if creation fails or via destroy method
return interaction.editReply('An error occurred while trying to join the voice channel.');
}
} else {
// If player exists but is not connected or in a different channel
// If player exists but is in a different channel
if (player.voiceChannel !== voiceChannel.id) {
player.setVoiceChannel(voiceChannel.id);
if (!player.playing && !player.paused && !player.queue.size) {
player.connect(); // Connect if not already playing/paused/queued
}
// Destroy the old player and create a new one in the correct channel
player.destroy();
try {
player = await musicPlayer.createPlayer({
guildId: interaction.guildId,
textChannel: interaction.channelId,
voiceChannel: voiceChannel.id
});
logger.info(`Moved player to voice channel ${voiceChannel.name} (${voiceChannel.id}) in guild ${interaction.guildId}`);
await interaction.editReply(`Moved to ${voiceChannel.name}!`);
} catch (error) {
logger.error(`Failed to move player for guild ${interaction.guildId}: ${error.message}`, error);
return interaction.editReply('An error occurred while trying to move to the voice channel.');
}
} else {
// Already in the correct channel
await interaction.editReply(`I'm already in ${voiceChannel.name}!`);
}
// Update text channel if needed
// Update text channel if needed (Shoukaku player object stores textChannel)
if (player.textChannel !== interaction.channelId) {
player.setTextChannel(interaction.channelId);
player.textChannel = interaction.channelId; // Directly update the property
logger.debug(`Updated player text channel to ${interaction.channel.name} (${interaction.channelId}) in guild ${interaction.guildId}`);
}
}

View File

@ -1,6 +1,6 @@
const { SlashCommandBuilder, PermissionFlagsBits, ChannelType, EmbedBuilder } = require('discord.js');
const logger = require('../utils/logger');
const { musicPlayer } = require('../structures/ShoukakuEvents');
// Removed direct import of musicPlayer
module.exports = {
data: new SlashCommandBuilder()
@ -35,14 +35,21 @@ module.exports = {
}
try {
// Get the initialized player from the client object
const musicPlayer = interaction.client.player;
if (!musicPlayer) {
logger.error('Music player not initialized on client object!');
return interaction.editReply('The music player is not ready yet. Please try again shortly.');
}
// 3. Get or create player
let player = musicPlayer.getPlayer(interaction.guildId);
if (!player) {
try {
player = await musicPlayer.createPlayer({
guildId: interaction.guildId,
textChannel: interaction.channelId,
voiceChannel: voiceChannel.id
textChannel: interaction.channelId, // Use interaction.channelId directly
voiceChannel: voiceChannel.id // Use voiceChannel.id directly
});
logger.info(`Created player and connected to voice channel ${voiceChannel.name} (${voiceChannel.id}) for play command.`);
} catch (error) {
@ -61,7 +68,7 @@ module.exports = {
}
// 4. Search for tracks
const searchResults = await musicPlayer.search({
const searchResults = await musicPlayer.search({ // Use the player instance from the client
query: query,
requester: interaction.user
});

View File

@ -4,46 +4,49 @@ const logger = require('../utils/logger');
module.exports = {
name: Events.VoiceStateUpdate,
execute(oldState, newState, client) { // Added client parameter
// Pass the event data to the Erela.js manager
// It handles the logic for joining/leaving channels, server muting/deafening, etc.
if (client.manager) {
try {
// Use newState primarily, as erela.js handles the diff internally
client.manager.voiceStateUpdate(newState);
// Optional: Add more specific logging if needed
// logger.debug(`Voice state update processed for user ${newState.member?.user?.tag || 'Unknown'} in guild ${newState.guild.id}`);
} catch (error) {
logger.error(`Error processing voice state update: ${error.message}`, error);
}
} else {
logger.warn('Voice state update received, but Erela.js manager is not initialized yet.');
// Shoukaku handles voice state updates internally via its connector.
// We don't need to manually pass the update like with Erela.js.
// The warning about Erela.js manager not being initialized can be ignored/removed.
// Custom logic for player cleanup based on voice state changes.
const musicPlayer = client.player;
if (!musicPlayer) {
// Player manager might not be ready yet, especially during startup.
// logger.debug('Voice state update received, but Shoukaku player manager is not ready yet.');
return;
}
// You can add custom logic here if needed, for example:
// - Check if the bot itself was disconnected and clean up the player.
// - Check if the channel the bot was in becomes empty.
const player = client.manager?.players.get(newState.guild.id);
if (!player) return;
const player = musicPlayer.getPlayer(newState.guild.id);
if (!player) return; // No active player for this guild
// Check if the bot was disconnected
if (newState.id === client.user.id && !newState.channelId && player) {
logger.info(`Bot was disconnected from voice channel in guild ${newState.guild.id}. Destroying player.`);
player.destroy();
// Check if the bot was disconnected (newState has no channelId for the bot)
if (newState.id === client.user.id && !newState.channelId && oldState.channelId === player.voiceChannel) {
logger.info(`Bot was disconnected from voice channel ${oldState.channel?.name || oldState.channelId} in guild ${newState.guild.id}. Destroying player.`);
player.destroy(); // Use Shoukaku player's destroy method
return; // Exit early as the player is destroyed
}
// Check if the bot's channel is now empty (excluding the bot itself)
const channel = client.channels.cache.get(player.voiceChannel);
if (channel && channel.members.size === 1 && channel.members.has(client.user.id)) {
// Ensure the channel exists and the update is relevant to the bot's channel
if (channel && (newState.channelId === player.voiceChannel || oldState.channelId === player.voiceChannel)) {
// Fetch members again to ensure freshness after the update
const members = channel.members;
if (members.size === 1 && members.has(client.user.id)) {
logger.info(`Voice channel ${channel.name} (${player.voiceChannel}) in guild ${newState.guild.id} is now empty (only bot left). Destroying player.`);
// Optional: Add a timeout before destroying
// setTimeout(() => {
// const currentChannel = client.channels.cache.get(player.voiceChannel);
// if (currentChannel && currentChannel.members.size === 1) {
// const currentMembers = currentChannel?.members;
// if (currentMembers && currentMembers.size === 1 && currentMembers.has(client.user.id)) {
// logger.info(`Timeout finished: Destroying player in empty channel ${channel.name}.`);
// player.destroy();
// } else {
// logger.info(`Timeout finished: Channel ${channel.name} is no longer empty. Player not destroyed.`);
// }
// }, 60000); // e.g., 1 minute timeout
player.destroy();
player.destroy(); // Destroy immediately for now
}
}
},
};