Update music command and bot configuration to include Shoukaku options, textId/voiceId, and shardId for more reliable voice connections. Replit-Commit-Author: Agent Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 3fdb9ba3-be2d-454d-a6cb-1448d76639c9 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/RxaoyQl Replit-Helium-Checkpoint-Created: true
363 lines
12 KiB
JavaScript
363 lines
12 KiB
JavaScript
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
|
|
|
|
module.exports = {
|
|
data: new SlashCommandBuilder()
|
|
.setName('music')
|
|
.setDescription('Music player controls')
|
|
.addSubcommand(sub =>
|
|
sub.setName('play')
|
|
.setDescription('Play a song or playlist')
|
|
.addStringOption(opt =>
|
|
opt.setName('query')
|
|
.setDescription('Song name, URL, or playlist link')
|
|
.setRequired(true)))
|
|
.addSubcommand(sub =>
|
|
sub.setName('skip')
|
|
.setDescription('Skip the current track'))
|
|
.addSubcommand(sub =>
|
|
sub.setName('stop')
|
|
.setDescription('Stop playback and clear the queue'))
|
|
.addSubcommand(sub =>
|
|
sub.setName('pause')
|
|
.setDescription('Pause the current track'))
|
|
.addSubcommand(sub =>
|
|
sub.setName('resume')
|
|
.setDescription('Resume playback'))
|
|
.addSubcommand(sub =>
|
|
sub.setName('queue')
|
|
.setDescription('View the current queue'))
|
|
.addSubcommand(sub =>
|
|
sub.setName('nowplaying')
|
|
.setDescription('Show the currently playing track'))
|
|
.addSubcommand(sub =>
|
|
sub.setName('volume')
|
|
.setDescription('Adjust the volume')
|
|
.addIntegerOption(opt =>
|
|
opt.setName('level')
|
|
.setDescription('Volume level (0-100)')
|
|
.setRequired(true)
|
|
.setMinValue(0)
|
|
.setMaxValue(100)))
|
|
.addSubcommand(sub =>
|
|
sub.setName('shuffle')
|
|
.setDescription('Shuffle the queue'))
|
|
.addSubcommand(sub =>
|
|
sub.setName('loop')
|
|
.setDescription('Set loop mode')
|
|
.addStringOption(opt =>
|
|
opt.setName('mode')
|
|
.setDescription('Loop mode')
|
|
.setRequired(true)
|
|
.addChoices(
|
|
{ name: 'Off', value: 'off' },
|
|
{ name: 'Track', value: 'track' },
|
|
{ name: 'Queue', value: 'queue' }
|
|
))),
|
|
|
|
async execute(interaction, supabase, client) {
|
|
const subcommand = interaction.options.getSubcommand();
|
|
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;
|
|
|
|
if (subcommand === 'play') {
|
|
if (!memberVoice) {
|
|
return interaction.reply({
|
|
embeds: [new EmbedBuilder()
|
|
.setColor(0xff0000)
|
|
.setDescription('You need to be in a voice channel to play music.')],
|
|
ephemeral: true
|
|
});
|
|
}
|
|
|
|
if (botVoice && botVoice.id !== memberVoice.id) {
|
|
return interaction.reply({
|
|
embeds: [new EmbedBuilder()
|
|
.setColor(0xff0000)
|
|
.setDescription(`I'm already playing music in <#${botVoice.id}>. Join that channel or wait until I'm done.`)],
|
|
ephemeral: true
|
|
});
|
|
}
|
|
|
|
const query = interaction.options.getString('query');
|
|
await interaction.deferReply();
|
|
|
|
try {
|
|
let player = kazagumo.players.get(interaction.guildId);
|
|
|
|
if (!player) {
|
|
player = await kazagumo.createPlayer({
|
|
guildId: interaction.guildId,
|
|
textId: interaction.channelId,
|
|
voiceId: memberVoice.id,
|
|
shardId: interaction.guild.shardId || 0,
|
|
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)
|
|
.setDescription(`No results found for: **${query}**`)]
|
|
});
|
|
}
|
|
|
|
const embed = new EmbedBuilder()
|
|
.setColor(0x5865f2)
|
|
.setAuthor({ name: 'Added to Queue', iconURL: interaction.user.displayAvatarURL() });
|
|
|
|
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.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] });
|
|
|
|
} catch (error) {
|
|
console.error('Music play error:', error);
|
|
|
|
let errorMessage = 'Something went wrong while trying to play music.';
|
|
if (error.message?.includes('Sign in')) {
|
|
errorMessage = 'This track requires age verification or is restricted.';
|
|
} else if (error.message?.includes('private')) {
|
|
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('No node')) {
|
|
errorMessage = 'Music servers are currently offline. Please try again later.';
|
|
}
|
|
|
|
return interaction.editReply({
|
|
embeds: [new EmbedBuilder()
|
|
.setColor(0xff0000)
|
|
.setDescription(errorMessage)]
|
|
});
|
|
}
|
|
}
|
|
|
|
const player = kazagumo.players.get(interaction.guildId);
|
|
|
|
if (subcommand === 'nowplaying') {
|
|
if (!player || !player.playing) {
|
|
return interaction.reply({
|
|
embeds: [new EmbedBuilder()
|
|
.setColor(0xff0000)
|
|
.setDescription('Nothing is currently playing.')],
|
|
ephemeral: true
|
|
});
|
|
}
|
|
|
|
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.uri})**\n\n${progress}`)
|
|
.setThumbnail(track.thumbnail || null)
|
|
.addFields(
|
|
{ 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: ${player.volume}%` });
|
|
|
|
return interaction.reply({ embeds: [embed] });
|
|
}
|
|
|
|
if (subcommand === 'queue') {
|
|
if (!player || !player.queue.current) {
|
|
return interaction.reply({
|
|
embeds: [new EmbedBuilder()
|
|
.setColor(0xff0000)
|
|
.setDescription('The queue is empty.')],
|
|
ephemeral: true
|
|
});
|
|
}
|
|
|
|
const current = player.queue.current;
|
|
const tracks = player.queue.slice(0, 10);
|
|
|
|
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.uri}) - \`${formatDuration(track.length)}\``
|
|
).join('\n');
|
|
|
|
if (player.queue.size > 10) {
|
|
description += `\n\n*...and ${player.queue.size - 10} more tracks*`;
|
|
}
|
|
} else {
|
|
description += '*No more tracks in queue*';
|
|
}
|
|
|
|
const embed = new EmbedBuilder()
|
|
.setColor(0x5865f2)
|
|
.setTitle(`Queue for ${interaction.guild.name}`)
|
|
.setDescription(description)
|
|
.setFooter({ text: `Total: ${player.queue.size + 1} tracks | Volume: ${player.volume}%` });
|
|
|
|
return interaction.reply({ embeds: [embed] });
|
|
}
|
|
|
|
if (!player || !player.playing) {
|
|
return interaction.reply({
|
|
embeds: [new EmbedBuilder()
|
|
.setColor(0xff0000)
|
|
.setDescription('No music is currently playing.')],
|
|
ephemeral: true
|
|
});
|
|
}
|
|
|
|
if (memberVoice?.id !== botVoice?.id) {
|
|
return interaction.reply({
|
|
embeds: [new EmbedBuilder()
|
|
.setColor(0xff0000)
|
|
.setDescription('You need to be in the same voice channel as the bot.')],
|
|
ephemeral: true
|
|
});
|
|
}
|
|
|
|
switch (subcommand) {
|
|
case 'skip': {
|
|
const current = player.queue.current;
|
|
player.skip();
|
|
return interaction.reply({
|
|
embeds: [new EmbedBuilder()
|
|
.setColor(0x5865f2)
|
|
.setDescription(`Skipped **${current?.title || 'current track'}**`)]
|
|
});
|
|
}
|
|
|
|
case 'stop': {
|
|
player.destroy();
|
|
return interaction.reply({
|
|
embeds: [new EmbedBuilder()
|
|
.setColor(0x5865f2)
|
|
.setDescription('Stopped playback and cleared the queue.')]
|
|
});
|
|
}
|
|
|
|
case 'pause': {
|
|
if (player.paused) {
|
|
return interaction.reply({
|
|
embeds: [new EmbedBuilder()
|
|
.setColor(0xff0000)
|
|
.setDescription('The player is already paused.')],
|
|
ephemeral: true
|
|
});
|
|
}
|
|
player.pause(true);
|
|
return interaction.reply({
|
|
embeds: [new EmbedBuilder()
|
|
.setColor(0x5865f2)
|
|
.setDescription('Paused the player.')]
|
|
});
|
|
}
|
|
|
|
case 'resume': {
|
|
if (!player.paused) {
|
|
return interaction.reply({
|
|
embeds: [new EmbedBuilder()
|
|
.setColor(0xff0000)
|
|
.setDescription('The player is not paused.')],
|
|
ephemeral: true
|
|
});
|
|
}
|
|
player.pause(false);
|
|
return interaction.reply({
|
|
embeds: [new EmbedBuilder()
|
|
.setColor(0x5865f2)
|
|
.setDescription('Resumed playback.')]
|
|
});
|
|
}
|
|
|
|
case 'volume': {
|
|
const level = interaction.options.getInteger('level');
|
|
player.setVolume(level);
|
|
return interaction.reply({
|
|
embeds: [new EmbedBuilder()
|
|
.setColor(0x5865f2)
|
|
.setDescription(`Volume set to **${level}%**`)]
|
|
});
|
|
}
|
|
|
|
case 'shuffle': {
|
|
player.queue.shuffle();
|
|
return interaction.reply({
|
|
embeds: [new EmbedBuilder()
|
|
.setColor(0x5865f2)
|
|
.setDescription('Shuffled the queue.')]
|
|
});
|
|
}
|
|
|
|
case 'loop': {
|
|
const mode = interaction.options.getString('mode');
|
|
const loopModes = {
|
|
off: 'none',
|
|
track: 'track',
|
|
queue: 'queue'
|
|
};
|
|
player.setLoop(loopModes[mode]);
|
|
return interaction.reply({
|
|
embeds: [new EmbedBuilder()
|
|
.setColor(0x5865f2)
|
|
.setDescription(`Loop mode set to **${mode}**`)]
|
|
});
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
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)}\``;
|
|
}
|