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