Star ✨ on GitHub

Creating a Music Bot

Learn how to create a simple music bot with Moonlink.js

Introduction

Now that you have set up Moonlink.js and configured it with your Discord bot, it's time to create a fully functional music bot. In this guide, we'll build a bot with the following capabilities:

  1. Connect to a voice channel when requested
  2. Play music from YouTube, SoundCloud, and other supported sources
  3. Handle basic playback controls (play, pause, resume, skip, stop)
  4. Manage a queue of songs for continuous playback

This step-by-step guide will help you understand how each component works together to create a seamless music experience for your Discord server.

Prerequisites

Before we begin building our music bot, make sure you have:

  1. Installed Moonlink.js and its dependencies (as covered in the Installation guide)
  2. Set up Lavalink and have it running (as explained in the Lavalink guide)
  3. Created a Discord bot and added it to your server with the proper permissions
  4. Set up the basic configuration for Moonlink.js (as shown in the Basic Setup guide)

If you haven't completed these steps, please go back to the previous sections before continuing.

Project Structure

A well-organized project structure makes your code easier to maintain and understand. Let's organize our music bot project with the following structure:

music-bot/
├── commands/           # Directory for all command files
│   ├── play.js         # Command to play music
│   ├── skip.js         # Command to skip the current track
│   ├── stop.js         # Command to stop playback
│   ├── pause.js        # Command to pause playback
│   ├── resume.js       # Command to resume playback
│   └── queue.js        # Command to display the current queue
├── events/             # Directory for event handler files
│   ├── ready.js        # Handles the bot's ready event
│   ├── interactionCreate.js  # Handles slash command interactions
│   └── messageCreate.js      # Handles message commands
├── config.json         # Configuration file for bot settings
└── index.js            # Main entry point for the bot

This structure separates commands and events into their own directories, making it easier to add, modify, or remove functionality as needed.

Step 1: Setting Up the Main File

First, let's create the main file (index.js) that will serve as the entry point for our bot. This file will:

  1. Initialize the Discord client
  2. Set up the Moonlink.js manager
  3. Load commands and event handlers
  4. Connect to Discord

Here's a step-by-step breakdown of the code:

// index.js
const { Client, GatewayIntentBits, Collection } = require('discord.js');
const { Manager } = require('moonlink.js');
const fs = require('fs');
const path = require('path');
const config = require('./config.json');

// Create a new Discord client with the necessary intents
// Intents determine what events your bot can receive
const client = new Client({
  intents: [
    GatewayIntentBits.Guilds,           // Needed for guild-related events
    GatewayIntentBits.GuildVoiceStates, // Required for voice functionality
    GatewayIntentBits.GuildMessages,    // Needed to receive messages
    GatewayIntentBits.MessageContent,   // Required to read message content
  ],
});

// Create a new Moonlink Manager instance
// This is the main interface for interacting with Lavalink
client.manager = new Manager({
  // Configure the Lavalink nodes to connect to
  nodes: [
    {
      host: config.lavalink.host,         // The hostname of your Lavalink server
      port: config.lavalink.port,         // The port your Lavalink server is running on
      password: config.lavalink.password, // The password for your Lavalink server
      secure: config.lavalink.secure,     // Whether to use SSL/TLS for the connection
    },
  ],
  // This function sends voice state updates to Discord
  // It's required for the bot to join voice channels
  sendPayload: (guildId, payload) => {
    const guild = client.guilds.cache.get(guildId);
    if (guild) guild.shard.send(JSON.parse(payload));
  },
  autoPlay: true, // Automatically play the next song in the queue
});

// Set up a collection to store commands
client.commands = new Collection();

// Load command files from the commands directory
const commandFiles = fs.readdirSync('./commands').filter(file => file.endsWith('.js'));

// Register each command in the collection
for (const file of commandFiles) {
  const command = require(`./commands/${file}`);
  client.commands.set(command.name, command);
}

// Load event handler files from the events directory
const eventFiles = fs.readdirSync('./events').filter(file => file.endsWith('.js'));

// Register each event handler
for (const file of eventFiles) {
  const event = require(`./events/${file}`);
  if (event.once) {
    // For events that should only trigger once
    client.once(event.name, (...args) => event.execute(...args, client));
  } else {
    // For events that can trigger multiple times
    client.on(event.name, (...args) => event.execute(...args, client));
  }
}

// Handle raw events for voice state updates
// This is crucial for Moonlink.js to work properly
client.on('raw', (packet) => {
  client.manager.packetUpdate(packet);
});

// Login to Discord with your bot token
client.login(config.token);

Configuration File

Create a config.json file to store your bot's configuration:

{
  "token": "YOUR_DISCORD_BOT_TOKEN",
  "prefix": "!",
  "lavalink": {
    "host": "localhost",
    "port": 2333,
    "password": "youshallnotpass",
    "secure": false
  }
}

Replace YOUR_DISCORD_BOT_TOKEN with your actual bot token from the Discord Developer Portal.

Step 2: Setting Up Event Handlers

Next, let's create the event handler files that will respond to various events from Discord and Moonlink.js.

Ready Event Handler

The ready event fires when your bot successfully connects to Discord. This is where we'll initialize the Moonlink.js manager:

// events/ready.js
module.exports = {
  name: 'ready',
  once: true,
  execute(client) {
    console.log(`Logged in as ${client.user.tag}`);
    
    // Initialize the Moonlink Manager with the bot's user ID
    // This is required for the manager to function correctly
    client.manager.init(client.user.id);
    console.log('Moonlink Manager initialized');
  },
};

Message Create Event Handler

This event handler processes commands from text messages:

// events/messageCreate.js
module.exports = {
  name: 'messageCreate',
  execute(message, client) {
    // Ignore messages from bots or messages without the prefix
    const prefix = client.config?.prefix || '!';
    if (message.author.bot || !message.content.startsWith(prefix)) return;
    
    // Extract the command name and arguments
    const args = message.content.slice(prefix.length).trim().split(/ +/);
    const commandName = args.shift().toLowerCase();
    
    // Check if the command exists
    const command = client.commands.get(commandName);
    if (!command) return;
    
    // Execute the command
    try {
      command.execute(message, args, client);
    } catch (error) {
      console.error(error);
      message.reply('There was an error executing that command.');
    }
  },
};

Interaction Create Event Handler

This handler processes slash commands (we'll implement these later):

// events/interactionCreate.js
module.exports = {
  name: 'interactionCreate',
  execute(interaction, client) {
    if (!interaction.isCommand()) return;
    
    const command = client.commands.get(interaction.commandName);
    if (!command) return;
    
    try {
      command.execute(interaction, client);
    } catch (error) {
      console.error(error);
      interaction.reply({
        content: 'There was an error executing this command!',
        ephemeral: true,
      });
    }
  },
};

Moonlink.js emits various events that we need to handle for our music bot to function properly. Add these event listeners to your index.js file:

// Add this to your index.js file after creating the manager

// Node connection events
client.manager.on('nodeConnect', (node) => {
  console.log(`Node ${node.identifier} connected`);
});

client.manager.on('nodeDisconnect', (node) => {
  console.log(`Node ${node.identifier} disconnected`);
});

client.manager.on('nodeError', (node, error) => {
  console.error(`Node ${node.identifier} encountered an error:`, error);
});

// Playback events
client.manager.on('trackStart', (player, track) => {
  // Send a message when a track starts playing
  const channel = client.channels.cache.get(player.textChannelId);
  if (channel) {
    channel.send(`Now playing: **${track.title}**`);
  }
});

client.manager.on('trackEnd', (player, track) => {
  console.log(`Track ended: ${track.title}`);
});

client.manager.on('queueEnd', (player) => {
  // Send a message when the queue ends
  const channel = client.channels.cache.get(player.textChannelId);
  if (channel) {
    channel.send('Queue ended. Disconnecting in 30 seconds if no new tracks are added.');
  }
  
  // Disconnect after a delay if no new tracks are added
  // This helps save resources when the bot is not in use
  setTimeout(() => {
    if (!player.playing && player.queue.size === 0) {
      player.destroy();
      if (channel) {
        channel.send('Disconnected due to inactivity.');
      }
    }
  }, 30000); // 30 seconds
});

Step 4: Creating Music Commands

Now, let's create the basic music commands for our bot. Each command will be in its own file in the commands directory.

Play Command

The play command is the most important command for a music bot. It allows users to search for and play music:

// commands/play.js
module.exports = {
  name: 'play',
  description: 'Play a song',
  async execute(message, args, client) {
    // Step 1: Check if the user is in a voice channel
    // Users must be in a voice channel to use music commands
    const { channel } = message.member.voice;
    if (!channel) {
      return message.reply('You need to join a voice channel first!');
    }
    
    // Step 2: Check if there's a search query
    // We need a song name or URL to search for
    if (!args.length) {
      return message.reply('Please provide a song to play!');
    }
    
    // Step 3: Create a player or get an existing one
    // The player manages the connection to the voice channel and playback
    const player = client.manager.createPlayer({
      guildId: message.guild.id,         // The ID of the Discord server
      voiceChannelId: channel.id,        // The ID of the voice channel to join
      textChannelId: message.channel.id, // The ID of the text channel for messages
      autoPlay: true,                    // Automatically play the next song
    });
    
    // Step 4: Connect to the voice channel
    // This establishes the connection to the voice channel
    player.connect();
    
    // Step 5: Search for the track
    // This uses Lavalink to search for the requested song
    const query = args.join(' ');
    const searchResult = await client.manager.search({ 
      query: query,
      requester: message.author.id  // Store who requested the song
    });
    
    // Step 6: Handle search results
    // Check if any tracks were found
    if (!searchResult.tracks.length) {
      return message.reply('No results found!');
    }
    
    // Step 7: Handle different result types
    // The loadType tells us what kind of result we got
    switch (searchResult.loadType) {
      case 'playlist':
        // For playlists, add all tracks to the queue
        player.queue.add(searchResult.tracks);
        
        message.reply({
          content: `Added playlist **${searchResult.playlistInfo.name}** with ${searchResult.tracks.length} tracks to the queue.`,
        });
        
        // Start playback if not already playing
        if (!player.playing) {
          player.play();
        }
        break;
        
      case 'search':
      case 'track':
        // For single tracks, add just that track to the queue
        player.queue.add(searchResult.tracks[0]);
        
        message.reply({
          content: `Added **${searchResult.tracks[0].title}** to the queue.`,
        });
        
        // Start playback if not already playing
        if (!player.playing) {
          player.play();
        }
        break;
        
      case 'empty':
        // No matches found
        message.reply('No matches found for your query!');
        break;
        
      case 'error':
        // Error loading track
        message.reply(`Error loading track: ${searchResult.error || 'Unknown error'}`);
        break;
    }
  },
};

Skip Command

The skip command allows users to skip the currently playing track:

// commands/skip.js
module.exports = {
  name: 'skip',
  description: 'Skip the current song',
  execute(message, args, client) {
    // Step 1: Get the player for this guild
    // Each guild has its own player instance
    const player = client.manager.players.get(message.guild.id);
    
    // Step 2: Check if there is a player
    // We can't skip if nothing is playing
    if (!player) {
      return message.reply('There is nothing playing in this server!');
    }
    
    // Step 3: Check if the user is in the same voice channel
    // This prevents users from controlling the bot from different channels
    if (message.member.voice.channel?.id !== player.voiceChannelId) {
      return message.reply('You need to be in the same voice channel as the bot to use this command!');
    }
    
    // Step 4: Check if there is a current track
    // We can't skip if nothing is playing
    if (!player.current) {
      return message.reply('There is nothing playing right now!');
    }
    
    // Step 5: Skip the current track
    // Store the current track before skipping for the message
    const currentTrack = player.current;
    player.skip();
    
    // Step 6: Inform the user
    message.reply(`Skipped: **${currentTrack.title}**`);
  },
};

Stop Command

The stop command stops playback and clears the queue:

// commands/stop.js
module.exports = {
  name: 'stop',
  description: 'Stop playback and clear the queue',
  execute(message, args, client) {
    // Step 1: Get the player for this guild
    const player = client.manager.players.get(message.guild.id);
    
    // Step 2: Check if there is a player
    if (!player) {
      return message.reply('There is nothing playing in this server!');
    }
    
    // Step 3: Check if the user is in the same voice channel
    if (message.member.voice.channel?.id !== player.voiceChannelId) {
      return message.reply('You need to be in the same voice channel as the bot to use this command!');
    }
    
    // Step 4: Stop playback and clear the queue
    player.stop();     // Stop the current track
    player.queue.clear(); // Clear the queue
    
    // Step 5: Inform the user
    message.reply('Stopped playback and cleared the queue.');
  },
};

Pause Command

The pause command pauses the currently playing track:

// commands/pause.js
module.exports = {
  name: 'pause',
  description: 'Pause the current song',
  execute(message, args, client) {
    // Step 1: Get the player for this guild
    const player = client.manager.players.get(message.guild.id);
    
    // Step 2: Check if there is a player
    if (!player) {
      return message.reply('There is nothing playing in this server!');
    }
    
    // Step 3: Check if the user is in the same voice channel
    if (message.member.voice.channel?.id !== player.voiceChannelId) {
      return message.reply('You need to be in the same voice channel as the bot to use this command!');
    }
    
    // Step 4: Check if the player is already paused
    if (player.paused) {
      return message.reply('The player is already paused!');
    }
    
    // Step 5: Pause the player
    player.pause();
    
    // Step 6: Inform the user
    message.reply('Paused the player.');
  },
};

Resume Command

The resume command resumes playback if it's paused:

// commands/resume.js
module.exports = {
  name: 'resume',
  description: 'Resume playback if paused',
  execute(message, args, client) {
    // Step 1: Get the player for this guild
    const player = client.manager.players.get(message.guild.id);
    
    // Step 2: Check if there is a player
    if (!player) {
      return message.reply('There is nothing playing in this server!');
    }
    
    // Step 3: Check if the user is in the same voice channel
    if (message.member.voice.channel?.id !== player.voiceChannelId) {
      return message.reply('You need to be in the same voice channel as the bot to use this command!');
    }
    
    // Step 4: Check if the player is paused
    if (!player.paused) {
      return message.reply('The player is not paused!');
    }
    
    // Step 5: Resume the player
    player.resume();
    
    // Step 6: Inform the user
    message.reply('Resumed playback.');
  },
};

Queue Command

The queue command displays the current queue of songs:

// commands/queue.js
const { EmbedBuilder } = require('discord.js');

module.exports = {
  name: 'queue',
  description: 'Show the current queue',
  execute(message, args, client) {
    // Step 1: Get the player for this guild
    const player = client.manager.players.get(message.guild.id);
    
    // Step 2: Check if there is a player
    if (!player) {
      return message.reply('There is nothing playing in this server!');
    }
    
    // Step 3: Check if there are tracks in the queue
    if (!player.current && player.queue.size === 0) {
      return message.reply('There are no tracks in the queue!');
    }
    
    // Step 4: Format duration for display
    // This helper function converts milliseconds to a readable format
    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')}`;
    };
    
    // Step 5: Create an embed for the queue
    // Embeds provide a nice, formatted way to display information
    const embed = new EmbedBuilder()
      .setTitle('Current Queue')
      .setColor('#0099ff');
    
    // Step 6: Add the current track to the embed
    if (player.current) {
      embed.setDescription(`**Now Playing:**\n[${player.current.title}](${player.current.uri}) | \`${formatDuration(player.current.duration)}\``);
    }
    
    // Step 7: Add the queue tracks to the embed
    if (player.queue.size > 0) {
      const tracks = player.queue.tracks.map((track, index) => {
        return `${index + 1}. [${track.title}](${track.uri}) | \`${formatDuration(track.duration)}\``;
      });
      
      embed.addFields({
        name: 'Up Next:',
        value: tracks.slice(0, 10).join('\n'),
      });
      
      // If there are more than 10 tracks, add a note
      if (player.queue.size > 10) {
        embed.addFields({
          name: 'And more...',
          value: `${player.queue.size - 10} more tracks in the queue`,
        });
      }
    }
    
    // Step 8: Send the embed to the channel
    message.reply({ embeds: [embed] });
  },
};

Step 5: Running the Bot

Now that we've created all the necessary files, it's time to run our bot. Follow these steps:

  1. Make sure Lavalink is running
    java -jar Lavalink.jar
    
  2. Start your bot:
    node index.js
    

You should see output indicating that your bot has logged in and the Moonlink Manager has been initialized.

Step 6: Testing the Bot

Once your bot is running, you can test it with the following commands:

  • !play <song name or URL> - Play a song or add it to the queue
  • !skip - Skip the current song
  • !stop - Stop playback and clear the queue
  • !pause - Pause the current song
  • !resume - Resume playback
  • !queue - Show the current queue

Try each command to ensure your bot is working correctly. Here are some examples:

  • !play never gonna give you up - Plays Rick Astley's classic
  • !play https://www.youtube.com/watch?v=dQw4w9WgXcQ - Plays the same song using a direct URL
  • !play https://open.spotify.com/playlist/37i9dQZF1DXcBWIGoYBM5M - Plays a Spotify playlist (if you have a Spotify plugin)

Next Steps

Congratulations! You've created a basic music bot with Moonlink.js. From here, you can expand its functionality by:

  1. Adding more commands like volume control, seeking within tracks, and audio filters
  2. Implementing slash commands for a better user experience
  3. Adding error handling and logging for better reliability
  4. Implementing a DJ role system to restrict certain commands to specific users
  5. Adding playlist support for saving and loading playlists

In the next section, we'll explore how to implement these advanced music commands to make your bot even more powerful.

Troubleshooting Common Issues

If you encounter issues with your bot, here are some common problems and solutions:

  • Bot doesn't join voice channels: Make sure you've set up the sendPayload function correctly and are handling raw events.
  • No sound is playing: Check that Lavalink is running and configured correctly.
  • Error connecting to Lavalink: Verify your Lavalink host, port, and password in the config file.
  • Commands not working: Ensure your command files are in the correct directory and properly exported.

Remember that building a music bot is a complex task, and it's normal to encounter issues along the way. Don't hesitate to check the Moonlink.js documentation or ask for help in the Discord community if you get stuck.