feat(lavalink): Migrate from Erela.js to Shoukaku for music playback management
This commit is contained in:
parent
5a29fe3d9d
commit
e54c23cc63
@ -13,7 +13,7 @@
|
||||
"dependencies": {
|
||||
"discord.js": "^14.18.0",
|
||||
"dotenv": "^16.5.0",
|
||||
"erela.js": "^2.4.0",
|
||||
"shoukaku": "^4.1.1",
|
||||
"winston": "^3.17.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
const { SlashCommandBuilder, PermissionFlagsBits, ChannelType, EmbedBuilder } = require('discord.js');
|
||||
const logger = require('../utils/logger');
|
||||
const { musicPlayer } = require('../structures/ShoukakuEvents');
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
@ -9,7 +10,7 @@ module.exports = {
|
||||
option.setName('query')
|
||||
.setDescription('The URL or search term for the song/playlist')
|
||||
.setRequired(true)),
|
||||
async execute(interaction, client) { // Added client parameter
|
||||
async execute(interaction, client) {
|
||||
await interaction.deferReply(); // Defer reply immediately
|
||||
|
||||
const member = interaction.member;
|
||||
@ -30,128 +31,100 @@ module.exports = {
|
||||
return interaction.editReply('I need permission to **speak** in your voice channel!');
|
||||
}
|
||||
if (voiceChannel.type !== ChannelType.GuildVoice) {
|
||||
return interaction.editReply('I can only join standard voice channels.');
|
||||
return interaction.editReply('I can only join standard voice channels.');
|
||||
}
|
||||
|
||||
// 3. Get or create player, connect if necessary
|
||||
let player = client.manager.get(interaction.guildId);
|
||||
if (!player) {
|
||||
try {
|
||||
player = client.manager.create({
|
||||
guild: interaction.guildId,
|
||||
voiceChannel: voiceChannel.id,
|
||||
textChannel: interaction.channelId,
|
||||
selfDeafen: true,
|
||||
});
|
||||
player.connect();
|
||||
logger.info(`Created player and connected to voice channel ${voiceChannel.name} (${voiceChannel.id}) for play command.`);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to create/connect player for guild ${interaction.guildId} during play command: ${error.message}`, error);
|
||||
return interaction.editReply('An error occurred while trying to join the voice channel.');
|
||||
}
|
||||
} else {
|
||||
// Ensure bot is in the user's channel if already connected elsewhere
|
||||
if (player.voiceChannel !== voiceChannel.id) {
|
||||
// Optional: Add check if user has permission to move bot or if bot is already playing
|
||||
// if (player.playing) {
|
||||
// return interaction.editReply("I'm currently playing in another channel!");
|
||||
// }
|
||||
player.setVoiceChannel(voiceChannel.id);
|
||||
if (!player.playing && !player.paused && !player.queue.size) {
|
||||
player.connect();
|
||||
}
|
||||
logger.info(`Moved player to voice channel ${voiceChannel.name} (${voiceChannel.id}) for play command.`);
|
||||
}
|
||||
// Update text channel if needed
|
||||
if (player.textChannel !== interaction.channelId) {
|
||||
player.setTextChannel(interaction.channelId);
|
||||
logger.debug(`Updated player text channel to ${interaction.channel.name} (${interaction.channelId}) in guild ${interaction.guildId}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 4. Search for tracks
|
||||
try {
|
||||
const searchResult = await player.search(query, interaction.user); // Pass interaction.user as requester
|
||||
|
||||
// --- Handle Search Results ---
|
||||
let responseEmbed = new EmbedBuilder().setColor('#0099ff');
|
||||
|
||||
switch (searchResult.loadType) {
|
||||
case 'LOAD_FAILED':
|
||||
logger.error(`Search failed for query "${query}" in guild ${interaction.guildId}. Error: ${searchResult.exception?.message}`);
|
||||
await interaction.editReply(`Oops! I couldn't load tracks for your query. Error: ${searchResult.exception?.message || 'Unknown error'}`);
|
||||
// Optionally destroy player if nothing is playing/queued
|
||||
if (!player.playing && !player.paused && player.queue.isEmpty) {
|
||||
player.destroy();
|
||||
}
|
||||
return;
|
||||
|
||||
case 'NO_MATCHES':
|
||||
await interaction.editReply(`No results found for "${query}".`);
|
||||
if (!player.playing && !player.paused && player.queue.isEmpty) {
|
||||
player.destroy();
|
||||
}
|
||||
return;
|
||||
|
||||
case 'TRACK_LOADED':
|
||||
const track = searchResult.tracks[0];
|
||||
player.queue.add(track);
|
||||
logger.info(`Added track to queue: ${track.title} (Guild: ${interaction.guildId})`);
|
||||
|
||||
responseEmbed
|
||||
.setTitle('Track Added to Queue')
|
||||
.setDescription(`[${track.title}](${track.uri})`)
|
||||
.addFields({ name: 'Position in queue', value: `${player.queue.size}`, inline: true });
|
||||
if (track.thumbnail) {
|
||||
responseEmbed.setThumbnail(track.thumbnail);
|
||||
}
|
||||
|
||||
await interaction.editReply({ embeds: [responseEmbed] });
|
||||
break; // Proceed to play check
|
||||
|
||||
case 'PLAYLIST_LOADED':
|
||||
player.queue.add(searchResult.tracks);
|
||||
logger.info(`Added playlist: ${searchResult.playlist?.name} (${searchResult.tracks.length} tracks) (Guild: ${interaction.guildId})`);
|
||||
|
||||
responseEmbed
|
||||
.setTitle('Playlist Added to Queue')
|
||||
.setDescription(`**${searchResult.playlist?.name || 'Unnamed Playlist'}** (${searchResult.tracks.length} tracks)`)
|
||||
.addFields({ name: 'Starting track', value: `[${searchResult.tracks[0].title}](${searchResult.tracks[0].uri})` });
|
||||
|
||||
await interaction.editReply({ embeds: [responseEmbed] });
|
||||
break; // Proceed to play check
|
||||
|
||||
case 'SEARCH_RESULT':
|
||||
const firstTrack = searchResult.tracks[0];
|
||||
player.queue.add(firstTrack);
|
||||
logger.info(`Added search result to queue: ${firstTrack.title} (Guild: ${interaction.guildId})`);
|
||||
|
||||
responseEmbed
|
||||
.setTitle('Track Added to Queue')
|
||||
.setDescription(`[${firstTrack.title}](${firstTrack.uri})`)
|
||||
.addFields({ name: 'Position in queue', value: `${player.queue.size}`, inline: true });
|
||||
if (firstTrack.thumbnail) {
|
||||
responseEmbed.setThumbnail(firstTrack.thumbnail);
|
||||
}
|
||||
|
||||
await interaction.editReply({ embeds: [responseEmbed] });
|
||||
break; // Proceed to play check
|
||||
// 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
|
||||
});
|
||||
logger.info(`Created player and connected to voice channel ${voiceChannel.name} (${voiceChannel.id}) for play command.`);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to create/connect player for guild ${interaction.guildId} during play command: ${error.message}`);
|
||||
return interaction.editReply('An error occurred while trying to join the voice channel.');
|
||||
}
|
||||
} else if (player.voiceChannel !== voiceChannel.id) {
|
||||
// If player exists but in a different voice channel, destroy it and create a new one
|
||||
player.destroy();
|
||||
player = await musicPlayer.createPlayer({
|
||||
guildId: interaction.guildId,
|
||||
textChannel: interaction.channelId,
|
||||
voiceChannel: voiceChannel.id
|
||||
});
|
||||
logger.info(`Moved player to voice channel ${voiceChannel.name} (${voiceChannel.id}) for play command.`);
|
||||
}
|
||||
|
||||
// 5. Start playing if not already playing
|
||||
if (!player.playing && !player.paused && player.queue.totalSize > 0) {
|
||||
player.play();
|
||||
logger.info(`Started playing in guild ${interaction.guildId}`);
|
||||
// 4. Search for tracks
|
||||
const searchResults = await musicPlayer.search({
|
||||
query: query,
|
||||
requester: interaction.user
|
||||
});
|
||||
|
||||
if (!searchResults || searchResults.length === 0) {
|
||||
await interaction.editReply(`No results found for "${query}".`);
|
||||
if (!player.playing && player.queue.length === 0) {
|
||||
player.destroy();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 5. Add track(s) to queue and create response embed
|
||||
const responseEmbed = new EmbedBuilder().setColor('#0099ff');
|
||||
|
||||
// Add first track (or all tracks if it's a playlist)
|
||||
const firstTrack = searchResults[0];
|
||||
|
||||
// Detect if it's a playlist based on number of tracks
|
||||
const isPlaylist = searchResults.length > 1 &&
|
||||
searchResults[0].info.uri.includes('playlist');
|
||||
|
||||
if (isPlaylist) {
|
||||
// Add all tracks to the queue
|
||||
for (const track of searchResults) {
|
||||
await player.enqueue(track);
|
||||
}
|
||||
|
||||
// Set up playlist embed
|
||||
responseEmbed
|
||||
.setTitle('Playlist Added to Queue')
|
||||
.setDescription(`**Playlist** (${searchResults.length} tracks)`)
|
||||
.addFields({ name: 'Starting track', value: `[${firstTrack.info.title}](${firstTrack.info.uri})` });
|
||||
|
||||
logger.info(`Added playlist with ${searchResults.length} tracks to queue (Guild: ${interaction.guildId})`);
|
||||
} else {
|
||||
// Add single track to queue
|
||||
await player.enqueue(firstTrack);
|
||||
|
||||
// Set up track embed
|
||||
responseEmbed
|
||||
.setTitle('Track Added to Queue')
|
||||
.setDescription(`[${firstTrack.info.title}](${firstTrack.info.uri})`)
|
||||
.addFields({ name: 'Position in queue', value: `${player.queue.length}`, inline: true });
|
||||
|
||||
// Add thumbnail if available
|
||||
if (firstTrack.info.thumbnail) {
|
||||
responseEmbed.setThumbnail(firstTrack.info.thumbnail);
|
||||
}
|
||||
|
||||
logger.info(`Added track to queue: ${firstTrack.info.title} (Guild: ${interaction.guildId})`);
|
||||
}
|
||||
|
||||
// Send response
|
||||
await interaction.editReply({ embeds: [responseEmbed] });
|
||||
|
||||
} catch (error) {
|
||||
logger.error(`Error during search/play for query "${query}" in guild ${interaction.guildId}: ${error.message}`, error);
|
||||
await interaction.editReply('An unexpected error occurred while trying to play the music.').catch(e => logger.error(`Failed to send error reply for play command: ${e.message}`));
|
||||
// Optionally destroy player on critical error
|
||||
// if (!player.playing && !player.paused && player.queue.isEmpty) {
|
||||
// player.destroy();
|
||||
// }
|
||||
logger.error(`Error during search/play for query "${query}" in guild ${interaction.guildId}: ${error.message}`);
|
||||
await interaction.editReply('An unexpected error occurred while trying to play the music.').catch(e =>
|
||||
logger.error(`Failed to send error reply for play command: ${e.message}`)
|
||||
);
|
||||
}
|
||||
|
||||
logger.info(`Executed command 'play' for user ${interaction.user.tag}`);
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
const { Events } = require('discord.js');
|
||||
const { Events, ActivityType } = require('discord.js');
|
||||
const logger = require('../utils/logger');
|
||||
const loadErelaEvents = require('../structures/ErelaEvents'); // Import the Erela event loader
|
||||
const { setupPlayer } = require('../structures/ShoukakuEvents'); // Import the Shoukaku player
|
||||
|
||||
module.exports = {
|
||||
name: Events.ClientReady,
|
||||
@ -8,37 +8,16 @@ module.exports = {
|
||||
async execute(client) {
|
||||
logger.info(`Ready! Logged in as ${client.user.tag}`);
|
||||
|
||||
// Wait a moment before initializing Erela.js to give Lavalink time to start up
|
||||
// This is especially important in Docker environments
|
||||
await new Promise(resolve => setTimeout(resolve, 5000)); // 5 second delay
|
||||
|
||||
// Initialize the Erela Manager once the client is ready
|
||||
// Initialize the Shoukaku music player
|
||||
try {
|
||||
// Log the actual connection details being used (without exposing the password)
|
||||
const node = client.manager.nodes[0];
|
||||
logger.info(`Attempting to connect to Lavalink at ${node.options.host}:${node.options.port} with identifier "${node.options.identifier}"`);
|
||||
|
||||
client.manager.init(client.user.id);
|
||||
logger.info(`Erela.js Manager initialized for user ID: ${client.user.id}`);
|
||||
|
||||
// Load Erela.js event handlers
|
||||
loadErelaEvents(client);
|
||||
|
||||
// Set up the music player with the client
|
||||
client.player = setupPlayer(client);
|
||||
logger.info('Shoukaku music player initialized successfully');
|
||||
} catch (error) {
|
||||
logger.error(`Failed to initialize Erela.js Manager: ${error.message}`);
|
||||
// Try to reconnect after a delay if initialization fails
|
||||
setTimeout(() => {
|
||||
try {
|
||||
client.manager.init(client.user.id);
|
||||
logger.info(`Erela.js Manager initialization retry successful`);
|
||||
loadErelaEvents(client);
|
||||
} catch (retryError) {
|
||||
logger.error(`Retry initialization also failed: ${retryError.message}`);
|
||||
}
|
||||
}, 10000); // 10 second delay before retry
|
||||
logger.error(`Failed to initialize Shoukaku music player: ${error.message}`);
|
||||
}
|
||||
|
||||
// Set activity status
|
||||
client.user.setActivity('Music | /play', { type: 'LISTENING' });
|
||||
client.user.setActivity('Music | /play', { type: ActivityType.Listening });
|
||||
},
|
||||
};
|
||||
|
||||
47
src/index.js
47
src/index.js
@ -1,7 +1,7 @@
|
||||
// Load environment variables from .env file
|
||||
require('dotenv').config();
|
||||
const { Client, GatewayIntentBits, Collection } = require('discord.js');
|
||||
const { Manager } = require('erela.js');
|
||||
const { Shoukaku, Connectors } = require('shoukaku');
|
||||
const logger = require('./utils/logger');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
@ -27,28 +27,26 @@ const client = new Client({
|
||||
],
|
||||
});
|
||||
|
||||
// Initialize Erela.js Manager
|
||||
client.manager = new Manager({
|
||||
nodes: [
|
||||
{
|
||||
host: process.env.LAVALINK_HOST || 'localhost',
|
||||
port: parseInt(process.env.LAVALINK_PORT || '2333'),
|
||||
password: process.env.LAVALINK_PASSWORD || 'youshallnotpass',
|
||||
secure: process.env.LAVALINK_SECURE === 'true',
|
||||
retryAmount: 10, // Number of connection attempts
|
||||
retryDelay: 5000, // 5 seconds between each retry
|
||||
identifier: "lavalink", // Identifier for logs
|
||||
},
|
||||
],
|
||||
// Function to send raw voice data to Discord
|
||||
send(id, payload) {
|
||||
const guild = client.guilds.cache.get(id);
|
||||
if (guild) guild.shard.send(payload);
|
||||
},
|
||||
// Define Shoukaku nodes
|
||||
const Nodes = [
|
||||
{
|
||||
name: 'lavalink',
|
||||
url: `${process.env.LAVALINK_HOST || 'localhost'}:${process.env.LAVALINK_PORT || '2333'}/v4/websocket`,
|
||||
auth: process.env.LAVALINK_PASSWORD || 'youshallnotpass',
|
||||
secure: process.env.LAVALINK_SECURE === 'true'
|
||||
}
|
||||
];
|
||||
|
||||
// Initialize Shoukaku
|
||||
client.shoukaku = new Shoukaku(new Connectors.DiscordJS(client), Nodes, {
|
||||
moveOnDisconnect: false,
|
||||
resume: true,
|
||||
reconnectTries: 10,
|
||||
reconnectInterval: 5000,
|
||||
});
|
||||
|
||||
// Show the actual Lavalink connection details (without exposing the actual password)
|
||||
logger.info(`Lavalink connection configured to: ${process.env.LAVALINK_HOST}:${process.env.LAVALINK_PORT} (Password: ${process.env.LAVALINK_PASSWORD ? '[SET]' : '[NOT SET]'})`);
|
||||
logger.info(`Lavalink connection configured to: ${process.env.LAVALINK_HOST}:${process.env.LAVALINK_PORT}/v4/websocket (Password: ${process.env.LAVALINK_PASSWORD ? '[SET]' : '[NOT SET]'})`);
|
||||
|
||||
// Collections for commands
|
||||
client.commands = new Collection();
|
||||
@ -89,9 +87,12 @@ for (const file of eventFiles) {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Erela.js Event Handling ---
|
||||
// (This is handled within the 'ready' event after manager initialization)
|
||||
logger.info('Erela.js event handling will be attached in the ready event.');
|
||||
// --- Shoukaku Event Handling ---
|
||||
// Set up Shoukaku event handlers
|
||||
client.shoukaku.on('ready', (name) => logger.info(`Lavalink Node: ${name} is now connected`));
|
||||
client.shoukaku.on('error', (name, error) => logger.error(`Lavalink Node: ${name} emitted an error: ${error.message}`));
|
||||
client.shoukaku.on('close', (name, code, reason) => logger.warn(`Lavalink Node: ${name} closed with code ${code}. Reason: ${reason || 'No reason'}`));
|
||||
client.shoukaku.on('disconnect', (name, reason) => logger.warn(`Lavalink Node: ${name} disconnected. Reason: ${reason || 'No reason'}`));
|
||||
|
||||
// Log in to Discord with your client's token
|
||||
client.login(process.env.DISCORD_TOKEN)
|
||||
|
||||
280
src/structures/ShoukakuEvents.js
Normal file
280
src/structures/ShoukakuEvents.js
Normal file
@ -0,0 +1,280 @@
|
||||
const logger = require('../utils/logger');
|
||||
const { EmbedBuilder } = require('discord.js');
|
||||
|
||||
/**
|
||||
* Manages player instances and track playback using Shoukaku
|
||||
* @param {Client} client Discord.js client
|
||||
*/
|
||||
class MusicPlayer {
|
||||
constructor(client) {
|
||||
this.client = client;
|
||||
this.players = new Map(); // Store active players
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a player for a guild or returns existing one
|
||||
* @param {Object} options Options for creating the player
|
||||
* @param {string} options.guildId The guild ID
|
||||
* @param {string} options.textChannel The text channel ID
|
||||
* @param {string} options.voiceChannel The voice channel ID
|
||||
* @returns {Object} The player object
|
||||
*/
|
||||
async createPlayer({ guildId, textChannel, voiceChannel }) {
|
||||
// Check if player already exists
|
||||
if (this.players.has(guildId)) {
|
||||
return this.players.get(guildId);
|
||||
}
|
||||
|
||||
// Get Shoukaku node
|
||||
const node = this.client.shoukaku.getNode();
|
||||
if (!node) {
|
||||
throw new Error('No available Lavalink nodes!');
|
||||
}
|
||||
|
||||
try {
|
||||
// Create a new connection to the voice channel
|
||||
const connection = await node.joinChannel({
|
||||
guildId: guildId,
|
||||
channelId: voiceChannel,
|
||||
shardId: 0,
|
||||
deaf: true
|
||||
});
|
||||
|
||||
// Create a player object to track state and add methods
|
||||
const player = {
|
||||
guild: guildId,
|
||||
textChannel: textChannel,
|
||||
voiceChannel: voiceChannel,
|
||||
connection: connection,
|
||||
queue: [],
|
||||
current: null,
|
||||
playing: false,
|
||||
volume: 100,
|
||||
|
||||
// Play a track
|
||||
async play(track) {
|
||||
this.current = track;
|
||||
|
||||
// Start playback
|
||||
await this.connection.playTrack({ track: track.track });
|
||||
this.playing = true;
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
// Stop the current track
|
||||
stop() {
|
||||
this.connection.stopTrack();
|
||||
return this;
|
||||
},
|
||||
|
||||
// Skip to the next track
|
||||
skip() {
|
||||
this.stop();
|
||||
if (this.queue.length > 0) {
|
||||
const nextTrack = this.queue.shift();
|
||||
this.play(nextTrack);
|
||||
} else {
|
||||
this.current = null;
|
||||
this.playing = false;
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
// Set player volume
|
||||
setVolume(volume) {
|
||||
this.volume = volume;
|
||||
this.connection.setGlobalVolume(volume);
|
||||
return this;
|
||||
},
|
||||
|
||||
// Pause playback
|
||||
pause() {
|
||||
this.connection.setPaused(true);
|
||||
return this;
|
||||
},
|
||||
|
||||
// Resume playback
|
||||
resume() {
|
||||
this.connection.setPaused(false);
|
||||
return this;
|
||||
},
|
||||
|
||||
// Destroy the player and disconnect
|
||||
destroy() {
|
||||
this.connection.disconnect();
|
||||
musicPlayer.players.delete(this.guild);
|
||||
return this;
|
||||
},
|
||||
|
||||
// Add a track to the queue or play it if nothing is playing
|
||||
async enqueue(track, immediate = false) {
|
||||
if (immediate || (!this.playing && !this.current)) {
|
||||
await this.play(track);
|
||||
} else {
|
||||
this.queue.push(track);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
// Set up event listeners for this player
|
||||
connection.on('start', () => {
|
||||
logger.info(`Track started in guild ${player.guild}: ${player.current?.info?.title || 'Unknown'}`);
|
||||
|
||||
// Send now playing message
|
||||
if (player.current) {
|
||||
const channel = this.client.channels.cache.get(player.textChannel);
|
||||
if (channel) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor('#0099ff')
|
||||
.setTitle('Now Playing')
|
||||
.setDescription(`[${player.current.info.title}](${player.current.info.uri})`)
|
||||
.addFields({ name: 'Requested by', value: `${player.current.requester?.tag || 'Unknown'}`, inline: true })
|
||||
.setTimestamp();
|
||||
|
||||
if (player.current.info.thumbnail) {
|
||||
embed.setThumbnail(player.current.info.thumbnail);
|
||||
}
|
||||
|
||||
channel.send({ embeds: [embed] }).catch(e =>
|
||||
logger.error(`Failed to send trackStart message: ${e.message}`)
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
connection.on('end', () => {
|
||||
logger.info(`Track ended in guild ${player.guild}: ${player.current?.info?.title || 'Unknown'}`);
|
||||
player.playing = false;
|
||||
player.current = null;
|
||||
|
||||
// Play next track in queue if available
|
||||
if (player.queue.length > 0) {
|
||||
const nextTrack = player.queue.shift();
|
||||
player.play(nextTrack);
|
||||
} else {
|
||||
// Send queue end message
|
||||
const channel = this.client.channels.cache.get(player.textChannel);
|
||||
if (channel) {
|
||||
channel.send('Queue finished. Add more songs!').catch(e =>
|
||||
logger.error(`Failed to send queueEnd message: ${e.message}`)
|
||||
);
|
||||
}
|
||||
// Optional: Add timeout before disconnecting
|
||||
// setTimeout(() => {
|
||||
// if (!player.playing) player.destroy();
|
||||
// }, 300000); // 5 minutes
|
||||
player.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
connection.on('exception', (error) => {
|
||||
logger.error(`Track error in guild ${player.guild}: ${error.message}`);
|
||||
const channel = this.client.channels.cache.get(player.textChannel);
|
||||
if (channel) {
|
||||
channel.send(`An error occurred while trying to play: ${player.current?.info?.title || 'the track'}.
|
||||
Details: ${error.message || 'Unknown error'}`).catch(e =>
|
||||
logger.error(`Failed to send trackError message: ${e.message}`)
|
||||
);
|
||||
}
|
||||
player.skip();
|
||||
});
|
||||
|
||||
// Store the player and return it
|
||||
this.players.set(guildId, player);
|
||||
return player;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to create player for guild ${guildId}: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an existing player
|
||||
* @param {string} guildId The guild ID
|
||||
* @returns {Object|null} The player object or null
|
||||
*/
|
||||
getPlayer(guildId) {
|
||||
return this.players.get(guildId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for tracks using Shoukaku
|
||||
* @param {Object} options Options for the search
|
||||
* @param {string} options.query The search query
|
||||
* @param {string} options.requester The user who requested the track
|
||||
* @returns {Promise<Array>} Array of track objects
|
||||
*/
|
||||
async search({ query, requester }) {
|
||||
const node = this.client.shoukaku.getNode();
|
||||
if (!node) throw new Error('No available Lavalink nodes!');
|
||||
|
||||
try {
|
||||
// Determine search type
|
||||
let searchOptions = {};
|
||||
if (query.startsWith('http')) {
|
||||
// Direct URL
|
||||
searchOptions = { query };
|
||||
} else {
|
||||
// Search with prefix
|
||||
searchOptions = { query: `ytsearch:${query}` };
|
||||
}
|
||||
|
||||
// Perform the search
|
||||
const result = await node.rest.resolve(searchOptions);
|
||||
if (!result || result.loadType === 'error' || result.loadType === 'empty') {
|
||||
throw new Error(result?.exception?.message || 'No results found');
|
||||
}
|
||||
|
||||
// Process results
|
||||
let tracks = [];
|
||||
if (result.loadType === 'playlist') {
|
||||
// Playlist processing
|
||||
tracks = result.data.tracks.map(track => ({
|
||||
track: track.encoded,
|
||||
info: track.info,
|
||||
requester: requester
|
||||
}));
|
||||
} else if (result.loadType === 'track') {
|
||||
// Single track
|
||||
const track = result.data;
|
||||
tracks = [{
|
||||
track: track.encoded,
|
||||
info: track.info,
|
||||
requester: requester
|
||||
}];
|
||||
} else if (result.loadType === 'search') {
|
||||
// Search results
|
||||
tracks = result.data.slice(0, 10).map(track => ({
|
||||
track: track.encoded,
|
||||
info: track.info,
|
||||
requester: requester
|
||||
}));
|
||||
}
|
||||
|
||||
return tracks;
|
||||
} catch (error) {
|
||||
logger.error(`Search error: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create and export the player manager
|
||||
const musicPlayer = new MusicPlayer(null);
|
||||
module.exports = {
|
||||
setupPlayer: (client) => {
|
||||
if (!client || !client.shoukaku) {
|
||||
logger.error("ShoukakuEvents requires a client with an initialized shoukaku instance.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize the player with the client
|
||||
musicPlayer.client = client;
|
||||
|
||||
logger.info("Shoukaku music player initialized and ready.");
|
||||
return musicPlayer;
|
||||
},
|
||||
musicPlayer
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user