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:
parent
958ebfcaeb
commit
6fd04beaaf
5 changed files with 209 additions and 97 deletions
|
|
@ -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,68 @@ 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}`);
|
||||
});
|
||||
|
||||
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('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()
|
||||
|
|
@ -105,9 +125,42 @@ player.events.on('emptyQueue', (queue) => {
|
|||
.setDescription('Queue finished. Leaving voice channel...')]
|
||||
}).catch(() => {});
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (!player.playing && player.queue.size === 0) {
|
||||
player.destroy();
|
||||
}
|
||||
}, 30000);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[Music] Discord Player initialized');
|
||||
kazagumo.on('playerEmpty', (player) => {
|
||||
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(() => {});
|
||||
}
|
||||
player.destroy();
|
||||
});
|
||||
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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 (!result.hasTracks()) {
|
||||
if (!player) {
|
||||
player = await kazagumo.createPlayer({
|
||||
guildId: interaction.guildId,
|
||||
textChannel: interaction.channelId,
|
||||
voiceChannel: memberVoice.id,
|
||||
volume: 50,
|
||||
deaf: true
|
||||
});
|
||||
}
|
||||
|
||||
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
27
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|---------|-------------|
|
||||
|
|
|
|||
Loading…
Reference in a new issue