Music Commands
Learn how to create advanced music commands with Moonlink.js
This comprehensive guide will show you how to create advanced music commands for your Discord bot using Moonlink.js. You'll learn how to implement features like volume control, track seeking, audio filters, and much more, with detailed breakdowns of each code component.
Introduction
In the previous section, we created a basic music bot with essential commands. Now, let's expand our bot's functionality by adding more advanced music commands that will significantly enhance the user experience:
- Volume Control: Fine-tune playback volume
- Track Seeking: Jump to specific timestamps in a track
- Audio Filters: Apply effects like bassboost, nightcore, and more
- Now Playing Information: View detailed track information
- Loop Modes: Repeat tracks or entire queue
- Queue Shuffle: Randomize the playback order
Prerequisites
Before we begin, ensure you have:
- Completed the "Creating a Music Bot" section
- A working music bot with basic commands
- Understanding of Moonlink.js and Discord.js basics
- Node.js and required dependencies installed
Command Structure Overview
Each command we create follows a consistent and robust structure:
// Basic command structure
module.exports = {
name: 'commandname',
aliases: ['cmd', 'cm'], // Optional alternative names
description: 'Command description',
execute(message, args, client) {
// Safety checks
// Command logic
// User feedback
}
};
Let's break down the essential safety checks that every command should include:
// Player existence check
const player = client.manager.players.get(message.guild.id);
if (!player) {
return message.reply('No active player!');
}
// Voice channel check
if (!message.member.voice.channel) {
return message.reply('Join a voice channel first!');
}
// Same channel check
if (message.member.voice.channel?.id !== player.voiceChannelId) {
return message.reply('You must be in the same voice channel!');
}
Volume Command
The volume command allows users to adjust playback volume. Let's break it down:
// 1. Command structure and initial validation
module.exports = {
name: 'volume',
aliases: ['vol', 'v'],
description: 'Adjust the player volume',
execute(message, args, client) {
// Player validation
const player = client.manager.players.get(message.guild.id);
if (!player) {
return message.reply('No active player!');
}
// Voice channel validation
if (message.member.voice.channel?.id !== player.voiceChannelId) {
return message.reply('You must be in the same voice channel!');
}
This first part sets up the command structure and performs the initial validations.
// 2. Current volume check
if (!args.length) {
return message.reply(`Current volume is: **${player.volume}%**`);
}
// 3. Parse and validate volume input
const volume = parseInt(args[0]);
if (isNaN(volume)) {
return message.reply('Please provide a valid number!');
}
This section handles checking the current volume when no arguments are provided and parses the volume input.
// 4. Range validation and volume application
if (volume < 0 || volume > 1000) {
return message.reply('Volume must be between 0 and 1000!');
}
player.setVolume(volume);
message.reply(`Volume set to: **${volume}%**`);
},
};
The final part validates the volume range and applies the change.
Complete Volume Command:
module.exports = {
name: 'volume',
aliases: ['vol', 'v'],
description: 'Adjust the player volume',
execute(message, args, client) {
const player = client.manager.players.get(message.guild.id);
if (!player) {
return message.reply('No active player!');
}
if (message.member.voice.channel?.id !== player.voiceChannelId) {
return message.reply('You must be in the same voice channel!');
}
if (!args.length) {
return message.reply(`Current volume is: **${player.volume}%**`);
}
const volume = parseInt(args[0]);
if (isNaN(volume)) {
return message.reply('Please provide a valid number!');
}
if (volume < 0 || volume > 1000) {
return message.reply('Volume must be between 0 and 1000!');
}
player.setVolume(volume);
message.reply(`Volume set to: **${volume}%**`);
},
};
Seek Command
The seek command enables precise navigation within tracks. Let's examine each part:
// 1. Command setup and initial validations
module.exports = {
name: 'seek',
aliases: ['jump', 'goto'],
description: 'Seek to a specific position in the current track',
execute(message, args, client) {
// Player and track validation
const player = client.manager.players.get(message.guild.id);
if (!player) {
return message.reply('No active player!');
}
if (message.member.voice.channel?.id !== player.voiceChannelId) {
return message.reply('You must be in the same voice channel!');
}
if (!player.current) {
return message.reply('Nothing is playing!');
}
// Seekable check
if (!player.current.isSeekable) {
return message.reply('This track cannot be seeked!');
}
This section establishes the command and performs initial validations.
// 2. Time parsing logic
if (!args.length) {
return message.reply('Provide a position (e.g., 1:30 or 90)');
}
const position = args[0];
let milliseconds = 0;
// Handle different time formats
if (position.includes(':')) {
// Format: minutes:seconds
const [minutes, seconds] = position.split(':');
milliseconds = (parseInt(minutes) * 60 + parseInt(seconds)) * 1000;
} else {
// Format: seconds only
milliseconds = parseInt(position) * 1000;
}
This part handles parsing different time formats.
// 3. Final validation and seeking
if (isNaN(milliseconds)) {
return message.reply('Invalid time format!');
}
if (milliseconds > player.current.duration) {
return message.reply(`Track is only ${formatDuration(player.current.duration)} long!`);
}
player.seek(milliseconds);
message.reply(`Seeked to: **${formatDuration(milliseconds)}**`);
},
};
// Helper function for time formatting
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));
return `${hours ? `${hours}:` : ''}${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
}
The final part validates the parsed time and performs the seek operation.
Complete Seek Command:
module.exports = {
name: 'seek',
aliases: ['jump', 'goto'],
description: 'Seek to a specific position in the current track',
execute(message, args, client) {
const player = client.manager.players.get(message.guild.id);
if (!player) {
return message.reply('No active player!');
}
if (message.member.voice.channel?.id !== player.voiceChannelId) {
return message.reply('You must be in the same voice channel!');
}
if (!player.current) {
return message.reply('Nothing is playing!');
}
if (!player.current.isSeekable) {
return message.reply('This track cannot be seeked!');
}
if (!args.length) {
return message.reply('Provide a position (e.g., 1:30 or 90)');
}
const position = args[0];
let milliseconds = 0;
if (position.includes(':')) {
const [minutes, seconds] = position.split(':');
milliseconds = (parseInt(minutes) * 60 + parseInt(seconds)) * 1000;
} else {
milliseconds = parseInt(position) * 1000;
}
if (isNaN(milliseconds)) {
return message.reply('Invalid time format!');
}
if (milliseconds > player.current.duration) {
return message.reply(`Track is only ${formatDuration(player.current.duration)} long!`);
}
player.seek(milliseconds);
message.reply(`Seeked to: **${formatDuration(milliseconds)}**`);
},
};
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));
return `${hours ? `${hours}:` : ''}${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
}
Now Playing Command
The Now Playing command provides detailed information about the currently playing track. Let's break it down:
// 1. Initial setup and imports
const { EmbedBuilder } = require('discord.js');
module.exports = {
name: 'nowplaying',
aliases: ['np', 'current'],
description: 'Show information about the current track',
execute(message, args, client) {
// Player and track validation
const player = client.manager.players.get(message.guild.id);
if (!player) {
return message.reply('No active player!');
}
if (!player.current) {
return message.reply('Nothing is playing!');
}
This part sets up the command and performs initial validations.
// 2. Helper functions for formatting
const track = player.current;
const 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));
return `${hours ? `${hours}:` : ''}${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
};
const createProgressBar = (current, total, length = 15) => {
const progress = Math.round((current / total) * length);
return '▬'.repeat(progress) + '🔘' + '▬'.repeat(length - progress);
};
This section defines helper functions for time formatting and progress bar creation.
// 3. Create and send rich embed
const embed = new EmbedBuilder()
.setTitle('Now Playing')
.setColor('#0099ff')
.setDescription(`[${track.title}](${track.uri})`)
.addFields(
{ name: 'Author', value: track.author, inline: true },
{ name: 'Requested By', value: `<@${track.requester}>`, inline: true },
{ name: 'Duration', value: `\`${formatDuration(player.position)} / ${formatDuration(track.duration)}\`\n${createProgressBar(player.position, track.duration)}`, inline: false }
);
if (track.thumbnail) {
embed.setThumbnail(track.thumbnail);
}
message.reply({ embeds: [embed] });
},
};
The final part creates and sends a rich embed with track information.
Complete Now Playing Command:
const { EmbedBuilder } = require('discord.js');
module.exports = {
name: 'nowplaying',
aliases: ['np', 'current'],
description: 'Show information about the current track',
execute(message, args, client) {
const player = client.manager.players.get(message.guild.id);
if (!player) {
return message.reply('No active player!');
}
if (!player.current) {
return message.reply('Nothing is playing!');
}
const track = player.current;
const 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));
return `${hours ? `${hours}:` : ''}${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
};
const createProgressBar = (current, total, length = 15) => {
const progress = Math.round((current / total) * length);
return '▬'.repeat(progress) + '🔘' + '▬'.repeat(length - progress);
};
const embed = new EmbedBuilder()
.setTitle('Now Playing')
.setColor('#0099ff')
.setDescription(`[${track.title}](${track.uri})`)
.addFields(
{ name: 'Author', value: track.author, inline: true },
{ name: 'Requested By', value: `<@${track.requester}>`, inline: true },
{ name: 'Duration', value: `\`${formatDuration(player.position)} / ${formatDuration(track.duration)}\`\n${createProgressBar(player.position, track.duration)}`, inline: false }
);
if (track.thumbnail) {
embed.setThumbnail(track.thumbnail);
}
message.reply({ embeds: [embed] });
},
};
Loop Command
The Loop command enables different repeat modes for playback. Let's examine each part:
// 1. Command setup and initial validations
module.exports = {
name: 'loop',
aliases: ['repeat', 'l'],
description: 'Set the loop mode',
execute(message, args, client) {
// Player validation
const player = client.manager.players.get(message.guild.id);
if (!player) {
return message.reply('No active player!');
}
// Voice channel validation
if (message.member.voice.channel?.id !== player.voiceChannelId) {
return message.reply('You must be in the same voice channel!');
}
This part sets up the command and performs initial validations.
// 2. Toggle mode if no arguments
if (!args.length) {
if (player.loop === 'none') {
player.setLoop('track');
return message.reply('Track loop enabled.');
} else {
player.setLoop('none');
return message.reply('Loop disabled.');
}
}
This section handles toggling the loop mode when no arguments are provided.
// 3. Parse and apply requested loop mode
const mode = args[0].toLowerCase();
switch (mode) {
case 'none':
case 'off':
case 'disable':
player.setLoop('none');
message.reply('Loop disabled.');
break;
case 'track':
case 'song':
case 'current':
player.setLoop('track');
message.reply('Track loop enabled.');
break;
case 'queue':
case 'all':
player.setLoop('queue');
message.reply('Queue loop enabled.');
break;
default:
message.reply('Invalid mode! Use: `none`, `track`, or `queue`.');
break;
}
},
};
The final part parses and applies the requested loop mode.
Complete Loop Command:
module.exports = {
name: 'loop',
aliases: ['repeat', 'l'],
description: 'Set the loop mode',
execute(message, args, client) {
const player = client.manager.players.get(message.guild.id);
if (!player) {
return message.reply('No active player!');
}
if (message.member.voice.channel?.id !== player.voiceChannelId) {
return message.reply('You must be in the same voice channel!');
}
if (!args.length) {
if (player.loop === 'none') {
player.setLoop('track');
return message.reply('Track loop enabled.');
} else {
player.setLoop('none');
return message.reply('Loop disabled.');
}
}
const mode = args[0].toLowerCase();
switch (mode) {
case 'none':
case 'off':
case 'disable':
player.setLoop('none');
message.reply('Loop disabled.');
break;
case 'track':
case 'song':
case 'current':
player.setLoop('track');
message.reply('Track loop enabled.');
break;
case 'queue':
case 'all':
player.setLoop('queue');
message.reply('Queue loop enabled.');
break;
default:
message.reply('Invalid mode! Use: `none`, `track`, or `queue`.');
break;
}
},
};
Filter Command
The Filter command applies audio effects to the playback. Let's break it down:
// 1. Command setup and initial validations
module.exports = {
name: 'filter',
aliases: ['effect', 'fx'],
description: 'Apply an audio filter',
execute(message, args, client) {
// Player validation
const player = client.manager.players.get(message.guild.id);
if (!player) {
return message.reply('No active player!');
}
// Voice channel validation
if (message.member.voice.channel?.id !== player.voiceChannelId) {
return message.reply('You must be in the same voice channel!');
}
This part sets up the command and performs initial validations.
// 2. Show available filters if no arguments
if (!args.length) {
return message.reply('Available filters: `reset`, `bassboost`, `nightcore`, `vaporwave`, `8d`, `tremolo`, `vibrato`, `karaoke`');
}
// 3. Get and apply requested filter
const filter = args[0].toLowerCase();
This section handles showing available filters and getting the requested filter.
// 4. Apply filter effects
switch (filter) {
case 'reset':
// Remove all filters
player.filters.reset();
message.reply('All filters reset.');
break;
case 'bassboost':
// Enhance low frequencies
player.filters.setEqualizer([
{ band: 0, gain: 0.6 }, // 25 Hz
{ band: 1, gain: 0.7 }, // 40 Hz
{ band: 2, gain: 0.8 }, // 63 Hz
{ band: 3, gain: 0.55 }, // 100 Hz
{ band: 4, gain: 0.25 }, // 160 Hz
]);
message.reply('Bassboost filter applied.');
break;
case 'nightcore':
// Increase speed and pitch
player.filters.setTimescale({
speed: 1.2, // 20% faster
pitch: 1.2, // 20% higher pitch
rate: 1.0 // Normal rate
});
message.reply('Nightcore filter applied.');
break;
case 'vaporwave':
// Decrease speed and pitch
player.filters.setTimescale({
speed: 0.8, // 20% slower
pitch: 0.8, // 20% lower pitch
rate: 1.0 // Normal rate
});
message.reply('Vaporwave filter applied.');
break;
case '8d':
// Rotating audio effect
player.filters.setRotation({
rotationHz: 0.2 // Rotation speed
});
message.reply('8D filter applied.');
break;
case 'tremolo':
// Amplitude variation
player.filters.setTremolo({
frequency: 4.0, // Variation speed
depth: 0.75 // Effect intensity
});
message.reply('Tremolo filter applied.');
break;
case 'vibrato':
// Frequency variation
player.filters.setVibrato({
frequency: 4.0, // Variation speed
depth: 0.75 // Effect intensity
});
message.reply('Vibrato filter applied.');
break;
case 'karaoke':
// Vocal removal attempt
player.filters.setKaraoke({
level: 1.0, // Effect level
monoLevel: 1.0, // Mono channel level
filterBand: 220.0, // Frequency band
filterWidth: 100.0 // Width of effect
});
message.reply('Karaoke filter applied.');
break;
default:
message.reply('Invalid filter! Available filters: `reset`, `bassboost`, `nightcore`, `vaporwave`, `8d`, `tremolo`, `vibrato`, `karaoke`');
break;
}
},
};
The final part applies the selected filter effect.
Complete Filter Command:
module.exports = {
name: 'filter',
aliases: ['effect', 'fx'],
description: 'Apply an audio filter',
execute(message, args, client) {
const player = client.manager.players.get(message.guild.id);
if (!player) {
return message.reply('No active player!');
}
if (message.member.voice.channel?.id !== player.voiceChannelId) {
return message.reply('You must be in the same voice channel!');
}
if (!args.length) {
return message.reply('Available filters: `reset`, `bassboost`, `nightcore`, `vaporwave`, `8d`, `tremolo`, `vibrato`, `karaoke`');
}
const filter = args[0].toLowerCase();
switch (filter) {
case 'reset':
player.filters.reset();
message.reply('All filters reset.');
break;
case 'bassboost':
player.filters.setEqualizer([
{ band: 0, gain: 0.6 }, // 25 Hz
{ band: 1, gain: 0.7 }, // 40 Hz
{ band: 2, gain: 0.8 }, // 63 Hz
{ band: 3, gain: 0.55 }, // 100 Hz
{ band: 4, gain: 0.25 }, // 160 Hz
]);
message.reply('Bassboost filter applied.');
break;
case 'nightcore':
player.filters.setTimescale({
speed: 1.2, // 20% faster
pitch: 1.2, // 20% higher pitch
rate: 1.0 // Normal rate
});
message.reply('Nightcore filter applied.');
break;
case 'vaporwave':
player.filters.setTimescale({
speed: 0.8, // 20% slower
pitch: 0.8, // 20% lower pitch
rate: 1.0 // Normal rate
});
message.reply('Vaporwave filter applied.');
break;
case '8d':
player.filters.setRotation({
rotationHz: 0.2 // Rotation speed
});
message.reply('8D filter applied.');
break;
case 'tremolo':
player.filters.setTremolo({
frequency: 4.0, // Variation speed
depth: 0.75 // Effect intensity
});
message.reply('Tremolo filter applied.');
break;
case 'vibrato':
player.filters.setVibrato({
frequency: 4.0, // Variation speed
depth: 0.75 // Effect intensity
});
message.reply('Vibrato filter applied.');
break;
case 'karaoke':
player.filters.setKaraoke({
level: 1.0, // Effect level
monoLevel: 1.0, // Mono channel level
filterBand: 220.0, // Frequency band
filterWidth: 100.0 // Width of effect
});
message.reply('Karaoke filter applied.');
break;
default:
message.reply('Invalid filter! Available filters: `reset`, `bassboost`, `nightcore`, `vaporwave`, `8d`, `tremolo`, `vibrato`, `karaoke`');
break;
}
},
};
Shuffle Command
The Shuffle command randomizes the order of tracks in the queue. Let's break it down:
// 1. Command setup and initial validations
module.exports = {
name: 'shuffle',
aliases: ['mix', 'randomize'],
description: 'Shuffle the queue',
execute(message, args, client) {
// Player validation
const player = client.manager.players.get(message.guild.id);
if (!player) {
return message.reply('No active player!');
}
// Voice channel validation
if (message.member.voice.channel?.id !== player.voiceChannelId) {
return message.reply('You must be in the same voice channel!');
}
This part sets up the command and performs initial validations.
// 2. Queue validation and shuffling
if (!player.queue.length) {
return message.reply('The queue is empty!');
}
player.queue.shuffle();
// 3. User feedback
message.reply('🔀 Queue shuffled!');
},
};
The final part validates the queue and performs the shuffle operation.
Complete Shuffle Command:
module.exports = {
name: 'shuffle',
aliases: ['mix', 'randomize'],
description: 'Shuffle the queue',
execute(message, args, client) {
const player = client.manager.players.get(message.guild.id);
if (!player) {
return message.reply('No active player!');
}
if (message.member.voice.channel?.id !== player.voiceChannelId) {
return message.reply('You must be in the same voice channel!');
}
if (!player.queue.length) {
return message.reply('The queue is empty!');
}
player.queue.shuffle();
message.reply('🔀 Queue shuffled!');
},
};