diff --git a/aethex-bot/bot.js b/aethex-bot/bot.js index a9544e4..4ebba2f 100644 --- a/aethex-bot/bot.js +++ b/aethex-bot/bot.js @@ -9,8 +9,8 @@ const { PermissionFlagsBits, } = require("discord.js"); const { createClient } = require("@supabase/supabase-js"); -const { Player } = require("discord-player"); -const { DefaultExtractors } = require("@discord-player/extractor"); +const { Kazagumo, Plugins } = require("kazagumo"); +const { Connectors } = require("shoukaku"); const http = require("http"); const fs = require("fs"); const path = require("path"); @@ -56,48 +56,85 @@ const client = new Client({ }); // ============================================================================= -// DISCORD PLAYER SETUP +// LAVALINK MUSIC SETUP (Kazagumo + Shoukaku) // ============================================================================= -const player = new Player(client, { - skipFFmpeg: false +const LavalinkNodes = [ + { + name: 'lavalink-v4', + url: 'lava-v4.ajieblogs.eu.org:443', + auth: 'https://dsc.gg/ajidevserver', + secure: true + }, + { + name: 'lavalink-serenetia', + url: 'lavalinkv4.serenetia.com:443', + auth: 'https://dsc.gg/ajidevserver', + secure: true + } +]; + +const kazagumo = new Kazagumo({ + defaultSearchEngine: 'youtube', + send: (guildId, payload) => { + const guild = client.guilds.cache.get(guildId); + if (guild) guild.shard.send(payload); + } +}, new Connectors.DiscordJS(client), LavalinkNodes); + +kazagumo.shoukaku.on('ready', (name) => { + console.log(`[Music] Lavalink node "${name}" connected`); }); -(async () => { - await player.extractors.loadMulti(DefaultExtractors); - console.log('[Music] Extractors loaded'); -})(); +kazagumo.shoukaku.on('error', (name, error) => { + console.error(`[Music] Lavalink node "${name}" error:`, error.message); +}); -player.events.on('playerStart', (queue, track) => { - const channel = queue.metadata?.channel; +kazagumo.shoukaku.on('close', (name, code, reason) => { + console.warn(`[Music] Lavalink node "${name}" closed: ${code} - ${reason}`); +}); + +kazagumo.shoukaku.on('disconnect', (name, players, moved) => { + console.warn(`[Music] Lavalink node "${name}" disconnected`); +}); + +kazagumo.on('playerStart', (player, track) => { + const channel = player.textChannel ? client.channels.cache.get(player.textChannel) : null; if (channel) { + const duration = track.length ? formatDuration(track.length) : 'Live'; const embed = new EmbedBuilder() .setColor(0x5865f2) .setTitle('Now Playing') - .setDescription(`**[${track.title}](${track.url})**`) - .setThumbnail(track.thumbnail) + .setDescription(`**[${track.title}](${track.uri})**`) + .setThumbnail(track.thumbnail || null) .addFields( - { name: 'Duration', value: track.duration, inline: true }, - { name: 'Author', value: track.author, inline: true } + { name: 'Duration', value: duration, inline: true }, + { name: 'Author', value: track.author || 'Unknown', inline: true } ); channel.send({ embeds: [embed] }).catch(() => {}); } }); -player.events.on('audioTrackAdd', (queue, track) => { - console.log(`[Music] Track added: ${track.title}`); +kazagumo.on('playerEnd', (player) => { + if (player.queue.size === 0 && !player.playing) { + const channel = player.textChannel ? client.channels.cache.get(player.textChannel) : null; + if (channel) { + channel.send({ + embeds: [new EmbedBuilder() + .setColor(0x5865f2) + .setDescription('Queue finished. Leaving voice channel...')] + }).catch(() => {}); + } + setTimeout(() => { + if (!player.playing && player.queue.size === 0) { + player.destroy(); + } + }, 30000); + } }); -player.events.on('playerError', (queue, error) => { - console.error(`[Music] Player error: ${error.message}`); -}); - -player.events.on('error', (queue, error) => { - console.error(`[Music] General error: ${error.message}`); -}); - -player.events.on('emptyQueue', (queue) => { - const channel = queue.metadata?.channel; +kazagumo.on('playerEmpty', (player) => { + const channel = player.textChannel ? client.channels.cache.get(player.textChannel) : null; if (channel) { channel.send({ embeds: [new EmbedBuilder() @@ -105,9 +142,25 @@ player.events.on('emptyQueue', (queue) => { .setDescription('Queue finished. Leaving voice channel...')] }).catch(() => {}); } + player.destroy(); }); -console.log('[Music] Discord Player initialized'); +kazagumo.on('playerError', (player, error) => { + console.error(`[Music] Player error:`, error); +}); + +function formatDuration(ms) { + const seconds = Math.floor((ms / 1000) % 60); + const minutes = Math.floor((ms / (1000 * 60)) % 60); + const hours = Math.floor(ms / (1000 * 60 * 60)); + if (hours > 0) { + return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; + } + return `${minutes}:${seconds.toString().padStart(2, '0')}`; +} + +client.kazagumo = kazagumo; +console.log('[Music] Kazagumo/Lavalink music system initialized'); // ============================================================================= // SUPABASE SETUP (Modified: Now optional) diff --git a/aethex-bot/commands/music.js b/aethex-bot/commands/music.js index 3a21346..bfe4cc1 100644 --- a/aethex-bot/commands/music.js +++ b/aethex-bot/commands/music.js @@ -1,5 +1,4 @@ const { SlashCommandBuilder, EmbedBuilder } = require('discord.js'); -const { useMainPlayer, useQueue } = require('discord-player'); module.exports = { data: new SlashCommandBuilder() @@ -57,7 +56,16 @@ module.exports = { async execute(interaction, client, supabase) { const subcommand = interaction.options.getSubcommand(); - const player = useMainPlayer(); + const kazagumo = client.kazagumo; + + if (!kazagumo) { + return interaction.reply({ + embeds: [new EmbedBuilder() + .setColor(0xff0000) + .setDescription('Music system is not available. Please try again later.')], + ephemeral: true + }); + } const memberVoice = interaction.member.voice.channel; const botVoice = interaction.guild.members.me.voice.channel; @@ -85,11 +93,21 @@ module.exports = { await interaction.deferReply(); try { - const result = await player.search(query, { - requestedBy: interaction.user - }); + let player = kazagumo.players.get(interaction.guildId); + + if (!player) { + player = await kazagumo.createPlayer({ + guildId: interaction.guildId, + textChannel: interaction.channelId, + voiceChannel: memberVoice.id, + volume: 50, + deaf: true + }); + } - if (!result.hasTracks()) { + const result = await kazagumo.search(query, { requester: interaction.user }); + + if (!result.tracks.length) { return interaction.editReply({ embeds: [new EmbedBuilder() .setColor(0xff0000) @@ -97,35 +115,28 @@ module.exports = { }); } - const { track, searchResult } = await player.play(memberVoice, result, { - nodeOptions: { - metadata: { - channel: interaction.channel, - client: interaction.guild.members.me, - requestedBy: interaction.user - }, - volume: 50, - leaveOnEmpty: true, - leaveOnEmptyCooldown: 300000, - leaveOnEnd: true, - leaveOnEndCooldown: 300000 - } - }); - const embed = new EmbedBuilder() .setColor(0x5865f2) .setAuthor({ name: 'Added to Queue', iconURL: interaction.user.displayAvatarURL() }); - if (searchResult.playlist) { - embed.setTitle(searchResult.playlist.title) - .setURL(searchResult.playlist.url) - .setDescription(`Added **${searchResult.tracks.length}** tracks from playlist`) - .setThumbnail(searchResult.playlist.thumbnail || track.thumbnail); + if (result.type === 'PLAYLIST') { + for (const track of result.tracks) { + player.queue.add(track); + } + embed.setTitle(result.playlistName || 'Playlist') + .setDescription(`Added **${result.tracks.length}** tracks from playlist`) + .setThumbnail(result.tracks[0]?.thumbnail || null); } else { + const track = result.tracks[0]; + player.queue.add(track); embed.setTitle(track.title) - .setURL(track.url) - .setDescription(`**Duration:** ${track.duration}\n**Author:** ${track.author}`) - .setThumbnail(track.thumbnail); + .setURL(track.uri) + .setDescription(`**Duration:** ${formatDuration(track.length)}\n**Author:** ${track.author || 'Unknown'}`) + .setThumbnail(track.thumbnail || null); + } + + if (!player.playing && !player.paused) { + player.play(); } return interaction.editReply({ embeds: [embed] }); @@ -140,8 +151,8 @@ module.exports = { errorMessage = 'This playlist or video is private.'; } else if (error.message?.includes('unavailable')) { errorMessage = 'This content is unavailable in the current region.'; - } else if (error.message?.includes('Could not extract')) { - errorMessage = 'Could not load this track. Try a different search term.'; + } else if (error.message?.includes('No node')) { + errorMessage = 'Music servers are currently offline. Please try again later.'; } return interaction.editReply({ @@ -152,10 +163,10 @@ module.exports = { } } - const queue = useQueue(interaction.guildId); + const player = kazagumo.players.get(interaction.guildId); if (subcommand === 'nowplaying') { - if (!queue || !queue.isPlaying()) { + if (!player || !player.playing) { return interaction.reply({ embeds: [new EmbedBuilder() .setColor(0xff0000) @@ -164,26 +175,28 @@ module.exports = { }); } - const track = queue.currentTrack; - const progress = queue.node.createProgressBar(); + const track = player.queue.current; + const position = player.shoukaku.position || 0; + const duration = track.length || 0; + const progress = createProgressBar(position, duration); const embed = new EmbedBuilder() .setColor(0x5865f2) .setTitle('Now Playing') - .setDescription(`**[${track.title}](${track.url})**\n\n${progress}`) - .setThumbnail(track.thumbnail) + .setDescription(`**[${track.title}](${track.uri})**\n\n${progress}`) + .setThumbnail(track.thumbnail || null) .addFields( - { name: 'Author', value: track.author, inline: true }, - { name: 'Duration', value: track.duration, inline: true }, - { name: 'Requested By', value: track.requestedBy?.username || 'Unknown', inline: true } + { name: 'Author', value: track.author || 'Unknown', inline: true }, + { name: 'Duration', value: formatDuration(duration), inline: true }, + { name: 'Requested By', value: track.requester?.username || 'Unknown', inline: true } ) - .setFooter({ text: `Volume: ${queue.node.volume}%` }); + .setFooter({ text: `Volume: ${player.volume}%` }); return interaction.reply({ embeds: [embed] }); } if (subcommand === 'queue') { - if (!queue || !queue.isPlaying()) { + if (!player || !player.queue.current) { return interaction.reply({ embeds: [new EmbedBuilder() .setColor(0xff0000) @@ -192,19 +205,19 @@ module.exports = { }); } - const current = queue.currentTrack; - const tracks = queue.tracks.toArray().slice(0, 10); + const current = player.queue.current; + const tracks = player.queue.slice(0, 10); - let description = `**Now Playing:**\n[${current.title}](${current.url}) - \`${current.duration}\`\n\n`; + let description = `**Now Playing:**\n[${current.title}](${current.uri}) - \`${formatDuration(current.length)}\`\n\n`; if (tracks.length > 0) { description += '**Up Next:**\n'; description += tracks.map((track, i) => - `**${i + 1}.** [${track.title}](${track.url}) - \`${track.duration}\`` + `**${i + 1}.** [${track.title}](${track.uri}) - \`${formatDuration(track.length)}\`` ).join('\n'); - if (queue.tracks.size > 10) { - description += `\n\n*...and ${queue.tracks.size - 10} more tracks*`; + if (player.queue.size > 10) { + description += `\n\n*...and ${player.queue.size - 10} more tracks*`; } } else { description += '*No more tracks in queue*'; @@ -214,12 +227,12 @@ module.exports = { .setColor(0x5865f2) .setTitle(`Queue for ${interaction.guild.name}`) .setDescription(description) - .setFooter({ text: `Total: ${queue.tracks.size + 1} tracks | Volume: ${queue.node.volume}%` }); + .setFooter({ text: `Total: ${player.queue.size + 1} tracks | Volume: ${player.volume}%` }); return interaction.reply({ embeds: [embed] }); } - if (!queue || !queue.isPlaying()) { + if (!player || !player.playing) { return interaction.reply({ embeds: [new EmbedBuilder() .setColor(0xff0000) @@ -239,17 +252,17 @@ module.exports = { switch (subcommand) { case 'skip': { - const current = queue.currentTrack; - queue.node.skip(); + const current = player.queue.current; + player.skip(); return interaction.reply({ embeds: [new EmbedBuilder() .setColor(0x5865f2) - .setDescription(`Skipped **${current.title}**`)] + .setDescription(`Skipped **${current?.title || 'current track'}**`)] }); } case 'stop': { - queue.delete(); + player.destroy(); return interaction.reply({ embeds: [new EmbedBuilder() .setColor(0x5865f2) @@ -258,7 +271,7 @@ module.exports = { } case 'pause': { - if (queue.node.isPaused()) { + if (player.paused) { return interaction.reply({ embeds: [new EmbedBuilder() .setColor(0xff0000) @@ -266,7 +279,7 @@ module.exports = { ephemeral: true }); } - queue.node.pause(); + player.pause(true); return interaction.reply({ embeds: [new EmbedBuilder() .setColor(0x5865f2) @@ -275,7 +288,7 @@ module.exports = { } case 'resume': { - if (!queue.node.isPaused()) { + if (!player.paused) { return interaction.reply({ embeds: [new EmbedBuilder() .setColor(0xff0000) @@ -283,7 +296,7 @@ module.exports = { ephemeral: true }); } - queue.node.resume(); + player.pause(false); return interaction.reply({ embeds: [new EmbedBuilder() .setColor(0x5865f2) @@ -293,7 +306,7 @@ module.exports = { case 'volume': { const level = interaction.options.getInteger('level'); - queue.node.setVolume(level); + player.setVolume(level); return interaction.reply({ embeds: [new EmbedBuilder() .setColor(0x5865f2) @@ -302,7 +315,7 @@ module.exports = { } case 'shuffle': { - queue.tracks.shuffle(); + player.queue.shuffle(); return interaction.reply({ embeds: [new EmbedBuilder() .setColor(0x5865f2) @@ -312,15 +325,12 @@ module.exports = { case 'loop': { const mode = interaction.options.getString('mode'); - const { QueueRepeatMode } = require('discord-player'); - - const modes = { - off: QueueRepeatMode.OFF, - track: QueueRepeatMode.TRACK, - queue: QueueRepeatMode.QUEUE + const loopModes = { + off: 'none', + track: 'track', + queue: 'queue' }; - - queue.setRepeatMode(modes[mode]); + player.setLoop(loopModes[mode]); return interaction.reply({ embeds: [new EmbedBuilder() .setColor(0x5865f2) @@ -330,3 +340,23 @@ module.exports = { } } }; + +function formatDuration(ms) { + if (!ms) return '0:00'; + const seconds = Math.floor((ms / 1000) % 60); + const minutes = Math.floor((ms / (1000 * 60)) % 60); + const hours = Math.floor(ms / (1000 * 60 * 60)); + if (hours > 0) { + return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; + } + return `${minutes}:${seconds.toString().padStart(2, '0')}`; +} + +function createProgressBar(current, total, length = 15) { + if (!total) return '▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬'; + const percentage = current / total; + const progress = Math.round(length * percentage); + const empty = length - progress; + const progressBar = '▓'.repeat(progress) + '▒'.repeat(empty); + return `\`${formatDuration(current)}\` ${progressBar} \`${formatDuration(total)}\``; +} diff --git a/package-lock.json b/package-lock.json index c96549a..af956d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,9 @@ "express": "^5.2.1", "express-session": "^1.18.2", "ffmpeg-static": "^5.3.0", + "kazagumo": "^3.4.0", "pg": "^8.16.3", + "shoukaku": "^4.2.0", "stripe": "^20.0.0", "ws": "^8.18.3" } @@ -2078,6 +2080,18 @@ "node": ">=10" } }, + "node_modules/kazagumo": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/kazagumo/-/kazagumo-3.4.0.tgz", + "integrity": "sha512-xh/qDSMXJw+rLUzGG0tamXD3HZO8Ao6A5DGPEHF+oeVQxX1cB7k/EjmDE3KJu1GK6xMmdVh1GHtZaPMyTKDLNQ==", + "license": "ISC", + "dependencies": { + "shoukaku": "^4.2.0" + }, + "engines": { + "node": ">=16.5.0" + } + }, "node_modules/libsodium": { "version": "0.7.15", "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.15.tgz", @@ -3347,6 +3361,19 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/shoukaku": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/shoukaku/-/shoukaku-4.2.0.tgz", + "integrity": "sha512-3vPQLG484cZ1/2nd4ERRs6XESvGhvD8jZiB0STcpmTtnH6A/6ZcT3iYl00RoU1PZhC7TTrrvCYk1ca+KJjkoYw==", + "license": "MIT", + "dependencies": { + "ws": "^8.18.0" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=7.0.0" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", diff --git a/package.json b/package.json index 3e04747..b65ca42 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,9 @@ "express": "^5.2.1", "express-session": "^1.18.2", "ffmpeg-static": "^5.3.0", + "kazagumo": "^3.4.0", "pg": "^8.16.3", + "shoukaku": "^4.2.0", "stripe": "^20.0.0", "ws": "^8.18.3" } diff --git a/replit.md b/replit.md index 5f6d2e8..a0cc666 100644 --- a/replit.md +++ b/replit.md @@ -65,7 +65,7 @@ Users can spend their XP on items configured by server admins. | Custom | Server-specific rewards | ### 6. Music System -Full-featured music player using discord-player. +Full-featured music player using Lavalink (Kazagumo/Shoukaku). | Command | Description | |---------|-------------|