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 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 };