Star ✨ on GitHub

Music Commands

Learn how to create advanced music commands with Moonlink.js

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:

  1. Completed the "Creating a Music Bot" section
  2. A working music bot with basic commands
  3. Understanding of Moonlink.js and Discord.js basics
  4. 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!');
  },
};