293 lines
12 KiB
JavaScript
293 lines
12 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 instance and node
|
|
const shoukaku = this.client.shoukaku; // Get the main shoukaku instance
|
|
const node = shoukaku.options.nodeResolver(shoukaku.nodes);
|
|
if (!node) {
|
|
throw new Error('No available Lavalink nodes!');
|
|
}
|
|
|
|
try {
|
|
// Create a new connection to the voice channel using the shoukaku instance
|
|
const connection = await shoukaku.joinVoiceChannel({
|
|
guildId: guildId,
|
|
channelId: voiceChannel,
|
|
shardId: 0, // Assuming shardId 0, adjust if sharding
|
|
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;
|
|
logger.debug(`Attempting to play track: ${track.info.title} (${track.info.uri}) in guild ${this.guild}`);
|
|
logger.debug(`Track encoded data: ${track.encoded}`); // Log encoded data
|
|
try {
|
|
// Start playback
|
|
await this.connection.playTrack({ track: track.encoded });
|
|
this.playing = true;
|
|
logger.debug(`playTrack called successfully for: ${track.info.title}`);
|
|
} catch (playError) {
|
|
logger.error(`Error calling playTrack for ${track.info.title}: ${playError.message}`);
|
|
console.error(playError); // Log full error object
|
|
this.playing = false;
|
|
this.current = null;
|
|
// Maybe try skipping? Or just log and let the 'end' event handle it if it fires.
|
|
}
|
|
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;
|
|
},
|
|
|
|
shoukaku: shoukaku, // Store shoukaku instance on the player object
|
|
|
|
// Destroy the player and disconnect
|
|
destroy() {
|
|
// Use the stored Shoukaku instance to leave the channel
|
|
this.shoukaku.leaveVoiceChannel(this.guild);
|
|
// Remove the player instance from the manager's map
|
|
musicPlayer.players.delete(this.guild);
|
|
logger.debug(`Destroyed player for guild ${this.guild}`);
|
|
return this; // Return this for potential chaining, though unlikely needed here
|
|
},
|
|
|
|
// Add a track to the queue or play it if nothing is playing
|
|
async enqueue(track, immediate = false) {
|
|
if (immediate || (!this.playing && !this.current)) {
|
|
logger.debug(`Enqueue: Playing immediately - ${track.info.title}`);
|
|
await this.play(track);
|
|
} else {
|
|
logger.debug(`Enqueue: Adding to queue - ${track.info.title}`);
|
|
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 exception in guild ${player.guild}: ${error.message || 'Unknown error'}`);
|
|
console.error("Full track exception details:", error); // Log the full error object
|
|
const channel = this.client.channels.cache.get(player.textChannel);
|
|
if (channel) {
|
|
channel.send(`An error occurred during playback: ${error.message || 'Unknown error'}`).catch(e =>
|
|
logger.error(`Failed to send trackException message: ${e.message}`)
|
|
);
|
|
}
|
|
// Attempt to skip to the next track on exception
|
|
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.identifier The pre-constructed search identifier (e.g., 'ytsearch:query', 'scsearch:query', or a URL)
|
|
* @param {string} options.requester The user who requested the track
|
|
* @returns {Promise<Array>} Array of track objects
|
|
*/
|
|
async search({ identifier, requester }) { // Accept identifier directly
|
|
// 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 {
|
|
// Perform the search using the provided identifier string
|
|
logger.debug(`Performing search with identifier: ${identifier}`);
|
|
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 => ({
|
|
encoded: track.encoded, // Correct property name
|
|
info: track.info,
|
|
requester: requester
|
|
}));
|
|
} else if (result.loadType === 'track') {
|
|
// Single track
|
|
const track = result.data;
|
|
tracks = [{
|
|
encoded: track.encoded, // Correct property name
|
|
info: track.info,
|
|
requester: requester
|
|
}];
|
|
} else if (result.loadType === 'search') {
|
|
// Search results
|
|
tracks = result.data.slice(0, 10).map(track => ({
|
|
encoded: track.encoded, // Correct property name
|
|
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
|
|
};
|