Files
discord-music-bot/src/structures/ShoukakuEvents.js
2025-04-24 01:33:42 +08:00

285 lines
11 KiB
JavaScript

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.options.nodeResolver(this.client.shoukaku.nodes);
if (!node) {
throw new Error('No available Lavalink nodes!');
}
try {
// Create a new connection to the voice channel
const connection = await this.client.shoukaku.joinVoiceChannel({
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.encoded });
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 }) {
// Get the first available node
const node = this.client.shoukaku.options.nodeResolver(this.client.shoukaku.nodes);
if (!node) throw new Error('No available Lavalink nodes!');
try {
// Determine search type and prepare the identifier string
let identifier;
if (query.startsWith('http')) {
// Direct URL
identifier = query;
} else {
// Search with prefix (Lavalink handles ytsearch/ytmsearch automatically with the plugin)
// identifier = `ytsearch:${query}`; // Prefix might not be needed with the plugin, let Lavalink decide
identifier = `ytsearch:${query}`; // Pass the raw query for non-URLs
}
// Perform the search using the identifier string
const result = await node.rest.resolve(identifier);
if (!result || result.loadType === 'error' || result.loadType === 'empty') {
// Log the identifier for debugging if search fails
logger.debug(`Search failed for identifier: ${identifier}`);
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
};