import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelType, MessageFlags, ComponentType } from "discord.js" import type { ButtonInteraction, Client, Guild, GuildChannel, Locale } from "discord.js" import { useMainPlayer, useQueue } from "discord-player" import type { GuildPlayer, Disco } from "@/types/schemas" import type { PlayerMetadata } from "@/types/player" import uptime from "./uptime" import dbGuild from "@/schemas/guild" import { t, getGuildLocale } from "./i18n" import { logConsole, logConsoleError } from "./console" const progressIntervals = new Map() export function startProgressSaving(guildId: string, botId: string) { if (!guildId || !botId) { logConsole('discord_player', 'progress_saving.missing_ids'); return } logConsole('discord_player', 'progress_saving.start', { guildId, botId }) const key = `${guildId}-${botId}` if (progressIntervals.has(key)) { clearInterval(progressIntervals.get(key)) progressIntervals.delete(key) } // eslint-disable-next-line @typescript-eslint/no-misused-promises const interval = setInterval(async () => { try { const queue = useQueue(guildId) if (!queue || !queue.isPlaying() || !queue.currentTrack) { await stopProgressSaving(guildId, botId); return } const guildProfile = await dbGuild.findOne({ guildId }) if (!guildProfile) { await stopProgressSaving(guildId, botId); return } const dbData = guildProfile.get("guildPlayer") as GuildPlayer dbData.instances ??= [] const instanceIndex = dbData.instances.findIndex(instance => instance.botId === botId) if (instanceIndex === -1) { dbData.instances.push({ botId, replay: { textChannelId: (queue.metadata as PlayerMetadata).channel?.id ?? "", voiceChannelId: queue.connection?.joinConfig.channelId ?? "", trackUrl: queue.currentTrack.url, progress: queue.node.playbackTime } }) } else { dbData.instances[instanceIndex].replay.trackUrl = queue.currentTrack.url dbData.instances[instanceIndex].replay.progress = queue.node.playbackTime } guildProfile.set("guildPlayer", dbData) guildProfile.markModified("guildPlayer") await guildProfile.save().catch(console.error) } catch (error) { logConsoleError('discord_player', 'progress_saving.error', { guildId, botId }, error as Error) await stopProgressSaving(guildId, botId) } }, 3000) progressIntervals.set(key, interval) } export async function stopProgressSaving(guildId: string, botId: string) { if (!guildId || !botId) { logConsole('discord_player', 'progress_saving.missing_ids'); return } logConsole('discord_player', 'progress_saving.stop', { guildId, botId }) const key = `${guildId}-${botId}` if (progressIntervals.has(key)) { clearInterval(progressIntervals.get(key)) progressIntervals.delete(key) } const guildProfile = await dbGuild.findOne({ guildId: guildId }) if (!guildProfile) { logConsole('discord_player', 'progress_saving.database_not_exist'); return } const dbData = guildProfile.get("guildPlayer") as GuildPlayer if (dbData.instances) { const instanceIndex = dbData.instances.findIndex(instance => instance.botId === botId) if (instanceIndex === -1) return dbData.instances[instanceIndex].replay = { textChannelId: "", voiceChannelId: "", trackUrl: "", progress: 0 } guildProfile.set("guildPlayer", dbData) guildProfile.markModified("guildPlayer") await guildProfile.save().catch(console.error) } } export async function playerReplay(client: Client, dbData: GuildPlayer) { const botId = client.user?.id ?? "" const instance = dbData.instances?.find(instance => instance.botId === botId) if (!instance) { logConsole('discordjs', 'replay.no_data', { botId }); return } if (!instance.replay.textChannelId) { logConsole('discordjs', 'replay.no_text_channel_id', { botId }); return } if (!instance.replay.voiceChannelId) { logConsole('discordjs', 'replay.no_voice_channel_id', { botId }); return } const textChannel = await client.channels.fetch(instance.replay.textChannelId) if (!textChannel || (textChannel.type !== ChannelType.GuildText && textChannel.type !== ChannelType.GuildAnnouncement)) { logConsole('discordjs', 'replay.text_channel_not_found', { channelId: instance.replay.textChannelId, botId }) return } const voiceChannel = await client.channels.fetch(instance.replay.voiceChannelId) if (!voiceChannel || (voiceChannel.type !== ChannelType.GuildVoice && voiceChannel.type !== ChannelType.GuildStageVoice)) { logConsole('discordjs', 'replay.voice_channel_not_found', { channelId: instance.replay.textChannelId, botId }) return } const player = useMainPlayer() const queue = player.nodes.create((textChannel as GuildChannel).guild, { metadata: { channel: textChannel, client: textChannel.guild.members.me, requestedBy: client.user }, selfDeaf: true, volume: 20, leaveOnEmpty: true, leaveOnEmptyCooldown: 30000, leaveOnEnd: true, leaveOnEndCooldown: 300000 }) try { if (!queue.connection) await queue.connect(voiceChannel) } catch (error) { logConsoleError('discord_player', 'replay.connect_error', {}, error as Error) } if (!instance.replay.trackUrl) return const guildLocale = await getGuildLocale(queue.guild.id) const result = await player.search(instance.replay.trackUrl, { requestedBy: client.user ?? undefined }) if (!result.hasTracks()) await textChannel.send(t(guildLocale, "player.no_track_found", { url: instance.replay.trackUrl })) const track = result.tracks[0] const entry = queue.tasksQueue.acquire() await entry.getTask() queue.addTrack(track) try { if (!queue.isPlaying()) await queue.node.play() if (instance.replay.progress) await queue.node.seek(instance.replay.progress) startProgressSaving(queue.guild.id, botId) await textChannel.send(t(guildLocale, "player.music_restarted")) } catch (error) { logConsoleError('discord_player', 'replay.play_error', {}, error as Error) } finally { queue.tasksQueue.release() } } export async function playerDisco(client: Client, guild: Guild, dbData: Disco) { try { if (!dbData.channelId) { logConsole('discord_player', 'disco.channel_not_configured', { guild: guild.name }) clearInterval(client.disco.interval) return "clear" } const channel = await client.channels.fetch(dbData.channelId) if (!channel || (channel.type !== ChannelType.GuildText && channel.type !== ChannelType.GuildAnnouncement)) { logConsole('discord_player', 'disco.channel_not_found', { guild: guild.name, channelId: dbData.channelId }) clearInterval(client.disco.interval) return "clear" } const guildLocale = await getGuildLocale(guild.id) const { embed, components } = generatePlayerEmbed(guild, guildLocale) if (components && embed.data.footer) embed.setFooter({ text: `Uptime: ${uptime(client.uptime)} \n ${embed.data.footer.text}` }) else embed.setFooter({ text: `Uptime: ${uptime(client.uptime)}` }) const messages = await channel.messages.fetch() const bots = ["1065047326860783636", "1119343522059927684", "1119344050412204032", "1210714000321548329", "660961595006124052"] await Promise.all(messages.map(async msg => { if (!bots.includes(msg.author.id)) await msg.delete() })) const botMessage = messages.find(msg => client.user && msg.author.id === client.user.id) if (botMessage) { if (!components && botMessage.components.length > 0) { await botMessage.delete() return await channel.send({ embeds: [embed] }) } else if (components) return await botMessage.edit({ embeds: [embed], components }) else return await botMessage.edit({ embeds: [embed] }) } else return await channel.send({ embeds: [embed] }) } catch (error) { logConsoleError('discord_player', 'disco.general_error', {}, error as Error) return "clear" } } export async function playerEdit(interaction: ButtonInteraction) { const guild = interaction.guild if (!guild) return interaction.reply({ content: t(interaction.locale, "common.no_dm"), flags: MessageFlags.Ephemeral }) const { components } = generatePlayerEmbed(guild, interaction.locale) if (!components) return components.forEach(actionRow => { actionRow.components.forEach((button) => button.setDisabled(true)) }) await interaction.update({ components }) } export function generatePlayerEmbed(guild: Guild, locale: Locale | string) { const embed = new EmbedBuilder().setColor("#ffc370") const queue = useQueue(guild.id) if (!queue) { embed.setTitle(t(locale, "player.no_session")) return { embed, components: null } } const track = queue.currentTrack if (!track) { embed.setTitle(t(locale, "player.no_track")) return { embed, components: null } } const sourceValue = track.source === "spotify" ? t(locale, "player.sources.spotify") : track.source === "youtube" ? t(locale, "player.sources.youtube") : t(locale, "player.sources.unknown") const loopValue = queue.repeatMode === 3 ? t(locale, "player.loop_modes.autoplay") : queue.repeatMode === 2 ? t(locale, "player.loop_modes.queue") : queue.repeatMode === 1 ? t(locale, "player.loop_modes.track") : t(locale, "player.loop_modes.off") const progressionName = queue.node.isPaused() ? t(locale, "player.progression_paused") : t(locale, "player.progression") embed .setTitle(track.title) .setAuthor({ name: track.author }) .setURL(track.url) .setImage(track.thumbnail) .addFields( { name: t(locale, "player.duration"), value: track.duration, inline: true }, { name: t(locale, "player.source"), value: sourceValue, inline: true }, { name: t(locale, "player.volume"), value: `${queue.node.volume}%`, inline: true }, { name: progressionName, value: queue.node.createProgressBar() ?? t(locale, "common.none") }, { name: t(locale, "player.loop"), value: loopValue, inline: true } ) .setDescription(`**${t(locale, "player.next_track")} :** ${queue.tracks.data[0] ? queue.tracks.data[0].title : t(locale, "player.no_next_track")}`) .setFooter({ text: t(locale, "player.requested_by", { user: track.requestedBy ? track.requestedBy.tag : t(locale, "common.unknown") }) }) const components = [ new ActionRowBuilder().addComponents( new ButtonBuilder().setLabel(queue.node.isPaused() ? "▶️" : "⏸️").setStyle(2).setCustomId(queue.node.isPaused() ? "player_resume" : "player_pause"), new ButtonBuilder().setLabel("⏹️").setStyle(2).setCustomId("player_stop"), new ButtonBuilder().setLabel("⏭️").setStyle(2).setCustomId("player_skip").setDisabled(queue.tracks.data.length !== 0), new ButtonBuilder().setLabel("🔉").setStyle(2).setCustomId("player_volume_down").setDisabled(queue.node.volume === 0), new ButtonBuilder().setLabel("🔊").setStyle(2).setCustomId("player_volume_up").setDisabled(queue.node.volume === 100) ), new ActionRowBuilder().addComponents( new ButtonBuilder().setLabel("🔀").setStyle(2).setCustomId("player_shuffle"), new ButtonBuilder().setLabel("🔁").setStyle(2).setCustomId("player_loop"), new ButtonBuilder().setLabel("⏮️").setStyle(2).setCustomId("player_previous").setDisabled(queue.history.previousTrack ? false : true) ) ] return { embed, components } } /** * Génère l'embed et les composants pour la configuration du module Disco * @param dbData - Données du module Disco depuis la base de données * @param client - Client Discord * @param guildId - ID de la guilde * @param locale - Locale pour la traduction * @returns Objet contenant l'embed et les composants */ export function generateDiscoEmbed(dbData: Disco, client: Client, guildId: string, locale: Locale) { // Récupérer les informations du canal let channelInfo = t(locale, "player.disco.channel_not_configured") if (dbData.channelId) { const guild = client.guilds.cache.get(guildId) const channel = guild?.channels.cache.get(dbData.channelId) channelInfo = channel ? `<#${channel.id}>` : t(locale, "player.common.channel_not_found") } // Créer l'embed principal const embed = new EmbedBuilder() .setTitle(t(locale, "player.disco.title")) .setColor(dbData.enabled ? 0xFFC370 : 0x808080) .setDescription(dbData.enabled ? t(locale, "player.disco.description_enabled") : t(locale, "player.disco.description_disabled") ) .addFields( { name: t(locale, "common.status"), value: dbData.enabled ? t(locale, "player.common.enabled") : t(locale, "player.common.disabled"), inline: true }, { name: t(locale, "common.channel"), value: channelInfo, inline: true } ) .setTimestamp() // Bouton toggle - désactivé si pas de canal configuré pour l'activation const toggleButton = new ButtonBuilder() .setCustomId(dbData.enabled ? "player_disco_disable" : "player_disco_enable") .setLabel(dbData.enabled ? t(locale, "common.disable") : t(locale, "common.enable")) .setStyle(dbData.enabled ? ButtonStyle.Danger : ButtonStyle.Success) .setEmoji(dbData.enabled ? "❌" : "✅") // Désactiver le bouton d'activation si aucun canal n'est configuré if (!dbData.enabled && !dbData.channelId) { toggleButton.setDisabled(true) } // Bouton de configuration du canal const channelButton = new ButtonBuilder() .setCustomId("player_disco_channel") .setLabel(t(locale, "player.common.configure_channel")) .setStyle(ButtonStyle.Secondary) .setEmoji("📺") const components = [ { type: ComponentType.ActionRow, components: [toggleButton, channelButton] } ] return { embed, components } }