Update music system to use Lavalink with Kazagumo and Shoukaku

Replace discord-player with Kazagumo and Shoukaku for Lavalink integration in the music system, updating bot.js and music.js, and package.json dependencies.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Event-Id: f646d3fd-490d-47d9-8776-1bedc07bc744
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/3DIN02r
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
sirpiglr 2025-12-13 08:32:33 +00:00
parent 958ebfcaeb
commit 6fd04beaaf
5 changed files with 209 additions and 97 deletions

View file

@ -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)

View file

@ -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)}\``;
}

27
package-lock.json generated
View file

@ -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",

View file

@ -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"
}

View file

@ -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 |
|---------|-------------|