summaryrefslogtreecommitdiff
path: root/src/handlers/playCommand.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/handlers/playCommand.js')
-rw-r--r--src/handlers/playCommand.js271
1 files changed, 271 insertions, 0 deletions
diff --git a/src/handlers/playCommand.js b/src/handlers/playCommand.js
new file mode 100644
index 0000000..5551d28
--- /dev/null
+++ b/src/handlers/playCommand.js
@@ -0,0 +1,271 @@
+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 };