const { EmbedBuilder } = require('discord.js'); const { createAudioPlayer, joinVoiceChannel, AudioPlayerStatus, VoiceConnectionStatus, entersState, } = require('@discordjs/voice'); const { getVideoInfo, playSong, preloadSong, formatDuration, safeCleanup, clearSongBuffer } = require('../utils/player'); const { requireVoiceChannel } = require('../utils/helpers'); function createSongsFromVideos(videos, requestedBy) { return videos.map(v => ({ title: v.title, url: v.url, duration: v.duration, thumbnail: v.thumbnail, requestedBy: requestedBy, })); } function onPlayerIdle(interaction, queue) { console.log(`[PLAYER IDLE] Guild: ${interaction.guild.id}, Loop mode: ${queue.loopMode}, Is seeking: ${queue.isSeeking || false}`); safeCleanup(queue, 'Player Idle'); if (queue.isSeeking) { console.log(`[IDLE SKIP] Skipping queue shift because seek is in progress`); return; } if (queue.loopMode === 'song') { if (queue.songs[0]) { queue.songs[0].retryCount = 0; } playSong(interaction.guild.id, queue); } else if (queue.loopMode === 'queue') { const finishedSong = queue.songs.shift(); queue.songs.push(finishedSong); if (queue.songs.length > 0) { playSong(interaction.guild.id, queue); } } else { const finishedSong = queue.songs.shift(); clearSongBuffer(finishedSong, 'finished playing'); if (queue.songs.length > 0) { playSong(interaction.guild.id, queue); } else { console.log(`[QUEUE EMPTY] Guild: ${interaction.guild.id}`); } } } function onPlayerError(error, interaction, queue) { console.error('Audio player error:', error); safeCleanup(queue, 'Player Error'); if (queue.isSeeking) { console.log(`[ERROR DURING SEEK] Not shifting queue, seek in progress`); queue.isSeeking = false; return; } queue.songs.shift(); if (queue.songs.length > 0) { playSong(interaction.guild.id, queue); } } function onPlayerAutoPaused(player) { console.warn('Player auto-paused, attempting to resume...'); try { player.unpause(); } catch (err) { console.error('Failed to unpause:', err); } } function onConnectionError(error, queue) { console.error('Voice connection error:', error); safeCleanup(queue, 'Connection Error'); } async function onConnectionDisconnected(connection, queues, guildId) { try { await Promise.race([ entersState(connection, VoiceConnectionStatus.Signalling, 5000), entersState(connection, VoiceConnectionStatus.Connecting, 5000), ]); } catch (error) { connection.destroy(); queues.delete(guildId); } } function onConnectionDestroyed(queues, guildId) { queues.delete(guildId); } function setupPlayerEventListeners(player, interaction, queue) { player.on(AudioPlayerStatus.Idle, () => onPlayerIdle(interaction, queue)); player.on('error', (error) => onPlayerError(error, interaction, queue)); player.on(AudioPlayerStatus.AutoPaused, () => onPlayerAutoPaused(player)); } function setupConnectionEventListeners(connection, queues, guildId, queue) { connection.on('error', (error) => onConnectionError(error, queue)); connection.on(VoiceConnectionStatus.Disconnected, () => onConnectionDisconnected(connection, queues, guildId) ); connection.on(VoiceConnectionStatus.Destroyed, () => onConnectionDestroyed(queues, guildId) ); } async function createVoiceConnection(voiceChannel, interaction) { const connection = joinVoiceChannel({ channelId: voiceChannel.id, guildId: interaction.guild.id, adapterCreator: voiceChannel.guild.voiceAdapterCreator, }); try { await entersState(connection, VoiceConnectionStatus.Ready, 30000); return connection; } catch (error) { console.error('Failed to join voice channel:', error); connection.destroy(); throw error; } } async function initializeQueue(voiceChannel, interaction, queues, songs) { const player = createAudioPlayer(); let connection; try { connection = await createVoiceConnection(voiceChannel, interaction); } catch (error) { return null; } connection.subscribe(player); const queue = { voiceChannel, connection, player, songs: songs, volume: 50, isPlaying: false, loopMode: 'off', }; queues.set(interaction.guild.id, queue); setupPlayerEventListeners(player, interaction, queue); setupConnectionEventListeners(connection, queues, interaction.guild.id, queue); return queue; } function addSongsToQueue(queue, songs, interaction) { const oldLength = queue.songs.length; const wasIdle = queue.player.state.status === AudioPlayerStatus.Idle; songs.forEach(song => queue.songs.push(song)); const newLength = queue.songs.length; console.log(`[QUEUE ADD] Guild: ${interaction.guild.id}, Added ${songs.length} songs, Queue: ${oldLength} -> ${newLength}, Player state: ${queue.player.state.status}, Was idle: ${wasIdle}`); if (wasIdle && oldLength === 0 && queue.songs.length > 0) { if (queue.isSeeking) { console.log(`[AUTO-PLAY BLOCKED] Seek in progress, not starting playback`); } else { console.log(`[AUTO-PLAY] Starting playback, songs in queue: ${queue.songs.length}`); playSong(interaction.guild.id, queue); } } if (queue.songs.length >= 2) { const nextSong = queue.songs[1]; if (!nextSong.audioBuffer && !nextSong.isPreloading) { console.log(`[PRELOAD TRIGGER] Preloading next song: ${nextSong.title}`); preloadSong(nextSong).catch(() => {}); } } return wasIdle; } function createNewPlaybackEmbed(firstSong, isPlaylist, songs) { const embed = new EmbedBuilder() .setColor(0x00ff00) .setTitle(isPlaylist ? 'Playlist Added' : 'Now Playing') .setDescription(`**${firstSong.title}**`) .addFields( { name: 'Duration', value: formatDuration(firstSong.duration), inline: true }, { name: 'Requested by', value: firstSong.requestedBy, inline: true } ) .setThumbnail(firstSong.thumbnail); if (isPlaylist) { embed.addFields({ name: 'Playlist', value: `${songs.length} songs added to queue` }); } return embed; } function createAddToQueueEmbed(firstSong, isPlaylist, songs, queuePosition) { const embed = new EmbedBuilder() .setColor(0x00ff00) .setTitle(isPlaylist ? 'Playlist Added to Queue' : 'Added to Queue') .setDescription(`**${firstSong.title}**`) .addFields( { name: 'Position', value: `${queuePosition}`, inline: true }, { name: 'Requested by', value: firstSong.requestedBy, inline: true } ) .setThumbnail(firstSong.thumbnail); if (isPlaylist) { embed.addFields({ name: 'Playlist', value: `${songs.length} songs added to queue` }); } return embed; } async function handlePlay(interaction, queues) { await interaction.deferReply(); const query = interaction.options.getString('query'); const voiceChannel = requireVoiceChannel(interaction); if (!voiceChannel) return; const videoInfo = await getVideoInfo(query); if (!videoInfo) { return interaction.editReply('Could not find that video!'); } const isPlaylist = videoInfo.isPlaylist; const videos = videoInfo.videos; if (!videos || videos.length === 0) { return interaction.editReply('Could not find any videos!'); } const songs = createSongsFromVideos(videos, interaction.user.tag); const firstSong = songs[0]; let queue = queues.get(interaction.guild.id); if (!queue) { queue = await initializeQueue(voiceChannel, interaction, queues, songs); if (!queue) { return interaction.editReply('Failed to join voice channel! Try again.'); } const embed = createNewPlaybackEmbed(firstSong, isPlaylist, songs); interaction.editReply({ embeds: [embed] }); playSong(interaction.guild.id, queue); } else { const queuePosition = queue.songs.length - songs.length + 1; addSongsToQueue(queue, songs, interaction); const embed = createAddToQueueEmbed(firstSong, isPlaylist, songs, queuePosition); interaction.editReply({ embeds: [embed] }); } } module.exports = { handlePlay };