feat(lavalink): Migrate from Erela.js to Shoukaku for music playback management
This commit is contained in:
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
|
||||
};
|
||||
Reference in New Issue
Block a user