Writing a music command for your discord.js bot (Updated March 2020)
Tags: musicbot, discordjs, node, bot
Published: 2019-05-25 12:19:43

---
title: Writing a music command for your discord.js bot (Updated March 2020)
published: true
description: In this guide you will learn how to write a 'play' command which will let you play songs from youtube on your Discord bot.
tags: musicbot, discordjs, nodejs, bot
---

**Edit Nov 20: This guide is outdated because the play command code changes all the time. I recommend cloning my bot which is on [GitHub](https://github.com/galnir/Master-Bot) if you're looking for a music bot with features like saved playlists**

If you need a guide on how to set up a bot, please read _[this](https://dev.to/galnir/how-to-make-a-discord-music-bot-3c0p)_

Discord is a popular voice and chat app for gamers and non-gamers who use it to communicate with their friends without paying for a voice server like before.

Almost every big server has a Discord bot, which can be used for managing it, administrative commands like ban and kick, gifs and **playing music**. So why pay for a music bot when you can create one for **free**?

No time or energy to read this guide? Just clone my music bot from [Github](https://github.com/galnir/Master-Bot), follow the Prerequisites section, hit `npm install` and you'll have a working music bot!

This guide assumes you have a basic Discord bot that uses Commando already set up. If the bot does not use discord.js-commando, I _highly_ recommend you to read [this guide](https://discordjs.guide/commando/) as Commando makes your life _so_ much easier, and it's required for this guide.

The code for the music command is available [here](https://github.com/galnir/Master-Bot/blob/master/commands/music/play.js).

## Prerequisites
Make sure [ffmpeg](https://www.ffmpeg.org/download.html), [python 2.7](https://www.python.org/downloads/), and Node(**v12 atleast!**) are installed.
Obtain a Youtube API key [here](https://developers.google.com/youtube/v3/getting-started).

## Installing packages
Let's install the packages we're going to work with:
npm:``` npm install discordjs/discord.js discordjs/Commando ffmpeg-static node-opus simple-youtube-api ytdl-core ```

yarn: ``` yarn add discordjs/discord.js discordjs/Commando ffmpeg-static node-opus simple-youtube-api ytdl-core@latest```

## Index.js (your main file)

Before we get to play.js, we need to extend the 'Guild' class so we could add a property that will hold our song queue. That will allow the bot to play music at multiple servers at a time.
To do that we'll import 'Structures' at the top of index.js, and use it to extend the 'Guild' class:
```javascript
// your index.js should look similar to this:
const { CommandoClient } = require('discord.js-commando');
const { Structures } = require('discord.js');
const path = require('path');
const { prefix, token } = require('./config.json');
// It's vital this is before the initiation of the client
Structures.extend('Guild', Guild => {
  class MusicGuild extends Guild {
    constructor(client, data) {
      super(client, data);
      this.musicData = {
        queue: [],
        isPlaying: false,
        volume: 1,
        songDispatcher: null
      };
    }
  }
  return MusicGuild;
});
const client = new CommandoClient({
  commandPrefix: prefix,
  owner: 'your-discord-user-id',
  unknownCommandResponse: false
});

client.registry
  .registerDefaultTypes()
  .registerGroups([
    ['music', 'Music Command Group']
  ])
  .registerDefaultGroups()
  .registerDefaultCommands()
  .registerCommandsIn(path.join(__dirname, 'commands'));

client.once('ready', () => {
  console.log('Ready!');
});

client.login(token);

```
## play.js
In your 'commands' folder, create a folder named music and inside it create **play.js**.
We'll start with importing packages and our Youtube API key:
```javascript
const { Command } = require('discord.js-commando');
const { MessageEmbed } = require('discord.js');
const Youtube = require('simple-youtube-api');
const ytdl = require('ytdl-core');
const { youtubeAPI } = require('../../config.json');
const youtube = new Youtube(youtubeAPI);
```

Next we'll declare the 'PlayCommand' class which extends 'Command': 
```javascript
module.exports = class PlayCommand extends Command {
  constructor(client) {
    super(client, {
      name: 'play', 
      memberName: 'play',
      group: 'music', // this means the folder the file is inside
      description: 'Play any song or playlist from youtube',
      guildOnly: true, // make this command available only in servers not dm's
      clientPermissions: ['SPEAK', 'CONNECT'],
      args: [
        {
          key: 'query', // here we name the variable that will hold the input
          prompt: 'What song would you like to listen to?', // send this msg if
          // the user hasn't provided any arg or if the arg was not a string
          type: 'string',
          validate: query => query.length > 0 && query.length < 200 
        }
      ]
    });
  }
```
Every command starts with the run method(the code you want the bot to run when the command is used):
```javascript
  async run(message, { query }) {
    // don't let users run this command if they are not in a voice channel
    var voiceChannel = message.member.voice.channel;
    if (!voiceChannel) return message.say('Join a channel and try again');
```

Users have 3 options when running this command:
1. Run it with a song name
2. Run it with a Youtube URL(any kind of URL)
3. Run it with a Youtube playlist URL

For example:
```
!play Darude Sandstorm
!play https://www.youtube.com/watch?v=y6120QOlsfU (and other url kinds)
!play https://www.youtube.com/playlist?list=PLuUrokoVSxlfUJuJB_D8j_wsFR4exaEmy
```
In order to do that we will write an if statement that checks against [regex](https://regexone.com/) for any type of a Youtube URL. If the input matches the regex, we will apply different logic than the one we will apply on queries by song names.

First of all check if _query_ is a playlist URL:
```javascript
    if (
      query.match(
        /^(?!.*\?.*\bv=)https:\/\/www\.youtube\.com\/.*\?.*\blist=.*$/
      )
    ) {
      try {
        const playlist = await youtube.getPlaylist(query); // get playlist data 
        const videosObj = await playlist.getVideos(); // songs data object
        //const videos = Object.entries(videosObj); // turn the object to array
        // iterate through the videos array and make a song object out of each vid
        for (let i = 0; i < videosObj.length; i++) { 
          const video = await videosObj[i].fetch();

          const url = `https://www.youtube.com/watch?v=${video.raw.id}`;
          const title = video.raw.snippet.title;
          let duration = this.formatDuration(video.duration);
          const thumbnail = video.thumbnails.high.url;
          if (duration == '00:00') duration = 'Live Stream';
          const song = {
            url,
            title,
            duration,
            thumbnail,
            voiceChannel
          };
          
          message.guild.musicData.queue.push(song); // if you remember, the queue lives in the guild object so each server has its own queue
          
        }
        if (message.guild.musicData.isPlaying == false) { // if nothing is playing
          message.guild.musicData.isPlaying = true;
          return this.playSong(message.guild.musicData.queue, message); // play the playlist
        } else if (message.guild.musicData.isPlaying == true) { // if something is already playing
          return message.say(
            `Playlist - :musical_note:  ${playlist.title} :musical_note: has been added to queue`
          );
        }
      } catch (err) {
        console.error(err);
        return message.say('Playlist is either private or it does not exist');
      }
    }
```

Youtube URL regex:
```javascript
    if (query.match(/^(http(s)?:\/\/)?((w){3}.)?youtu(be|.be)?(\.com)?\/.+/)) {
      const url = query; // temp variable
      try {
        query = query
          .replace(/(>|<)/gi, '')
          .split(/(vi\/|v=|\/v\/|youtu\.be\/|\/embed\/)/);
        const id = query[2].split(/[^0-9a-z_\-]/i)[0];
        const video = await youtube.getVideoByID(id);
        const title = video.title;
        let duration = this.formatDuration(video.duration);
        const thumbnail = video.thumbnails.high.url;
        if (duration == '00:00') duration = 'Live Stream';
        const song = {
          url,
          title,
          duration,
          thumbnail,
          voiceChannel
        };
        message.guild.musicData.queue.push(song);
        if (
          message.guild.musicData.isPlaying == false ||
          typeof message.guild.musicData.isPlaying == 'undefined'
        ) {
          message.guild.musicData.isPlaying = true;
          return this.playSong(message.guild.musicData.queue, message);
        } else if (message.guild.musicData.isPlaying == true) {
          return message.say(`${song.title} added to queue`);
        }
      } catch (err) {
        console.error(err);
        return message.say('Something went wrong, please try again later');
      }
    }

```
If the user has entered a song name as an argument:
```javascript 
    try {
      // search for the song and get 5 results back
      const videos = await youtube.searchVideos(query, 5);
      if (videos.length < 5) {
        return message.say(
          `I had some trouble finding what you were looking for, please try again or be more specific`
        );
      }
      const vidNameArr = [];
      // create an array that contains the result titles
      for (let i = 0; i < videos.length; i++) {
        vidNameArr.push(`${i + 1}: ${videos[i].title}`);
      }
      vidNameArr.push('exit'); // push 'exit' string as it will be an option
      // create and display an embed which will present the user the 5 results
      // so he can choose his desired result
      const embed = new MessageEmbed()
        .setColor('#e9f931')
        .setTitle('Choose a song by commenting a number between 1 and 5')
        .addField('Song 1', vidNameArr[0])
        .addField('Song 2', vidNameArr[1])
        .addField('Song 3', vidNameArr[2])
        .addField('Song 4', vidNameArr[3])
        .addField('Song 5', vidNameArr[4])
        .addField('Exit', 'exit'); // user can reply with 'exit' if none matches
      var songEmbed = await message.say({ embed });
      try {
        // wait 1 minute for the user's response
        var response = await message.channel.awaitMessages(
          msg => (msg.content > 0 && msg.content < 6) || msg.content === 'exit',
          {
            max: 1,
            maxProcessed: 1,
            time: 60000,
            errors: ['time']
          }
        );
        // assign videoIndex to user's response
        var videoIndex = parseInt(response.first().content);
      } catch (err) {
        console.error(err);
        songEmbed.delete();
        return message.say(
          'Please try again and enter a number between 1 and 5 or exit'
        );
      }
      // if the user responded with 'exit', cancel the command
      if (response.first().content === 'exit') return songEmbed.delete();
      try {
        // get video data from the API
        var video = await youtube.getVideoByID(videos[videoIndex - 1].id);
      } catch (err) {
        console.error(err);
        songEmbed.delete();
        return message.say(
          'An error has occured when trying to get the video ID from youtube'
        );
      }
      const url = `https://www.youtube.com/watch?v=${video.raw.id}`;
      const title = video.title;
      let duration = this.formatDuration(video.duration);
      const thumbnail = video.thumbnails.high.url;
        if (duration == '00:00') duration = 'Live Stream';
        const song = {
          url,
          title,
          duration,
          thumbnail,
          voiceChannel
        };

        message.guild.musicData.queue.push(song);

        if (message.guild.musicData.isPlaying == false) {
          message.guild.musicData.isPlaying = true;
          songEmbed.delete(); // delete the selection embed
          this.playSong(message.guild.musicData.queue, message);
        } else if (message.guild.musicData.isPlaying == true) {
          songEmbed.delete();
          // add the song to queue if one is already playing
          return message.say(`${song.title} added to queue`);
        }
    } catch (err) {
      // if something went wrong when calling the api:
      console.error(err);
      if (songEmbed) {
        songEmbed.delete();
      }
      return message.say(
        'Something went wrong with searching the video you requested :('
      );
    }
  }
```

So what's that *playSong* function we called multiple times above? This function takes the queue and the message object as arguments. When called, it tells the bot to join the user's channel and start playing music!

```javascript
// this is inside the PlayCommand class
  playSong(queue, message) {
    let voiceChannel;
    queue[0].voiceChannel
      .join() // join the user's voice channel
      .then(connection => {
        const dispatcher = connection
          .play(
            ytdl(queue[0].url, { // pass the url to .ytdl()
              quality: 'highestaudio',
              // download part of the song before playing it
              // helps reduces stuttering
              highWaterMark: 1024 * 1024 * 10
            })
          )
          .on('start', () => {
            // the following line is essential to other commands like skip
            message.guild.musicData.songDispatcher = dispatcher;
            dispatcher.setVolume(message.guild.musicData.volume);
            voiceChannel = queue[0].voiceChannel;
            // display the current playing song as a nice little embed
            const videoEmbed = new MessageEmbed()
              .setThumbnail(queue[0].thumbnail) // song thumbnail
              .setColor('#e9f931')
              .addField('Now Playing:', queue[0].title)
              .addField('Duration:', queue[0].duration);
            // also display next song title, if there is one in queue
            if (queue[1]) videoEmbed.addField('Next Song:', queue[1].title);
            message.say(videoEmbed); // send the embed to chat
            return queue.shift(); //  dequeue the song
          })
          .on('finish', () => { // this event fires when the song has ended
            if (queue.length >= 1) { // if there are more songs in queue
              return this.playSong(queue, message); // continue playing
            } else { // else if there are no more songs in queue
              message.guild.musicData.isPlaying = false;
              return voiceChannel.leave(); // leave the voice channel
            }
          })
          .on('error', e => {
            message.say('Cannot play song');
            message.guild.musicData.queue.length = 0;
            message.guild.musicData.isPlaying = false;
            message.guild.musicData.nowPlaying = null;
            console.error(e);
            return voiceChannel.leave();
          });
      })
      .catch(e => {
        console.error(e);
        return voiceChannel.leave();
      });
  }
```
formatDuration function:
```javascript
  formatDuration(durationObj) {
    const duration = `${durationObj.hours ? durationObj.hours + ':' : ''}${
      durationObj.minutes ? durationObj.minutes : '00'
    }:${
      durationObj.seconds < 10
        ? '0' + durationObj.seconds
        : durationObj.seconds
        ? durationObj.seconds
        : '00'
    }`;
    return duration;
  }
```

## That's it!
You can check out other music commands on the bot's [repo](https://github.com/galnir/Master-Bot/tree/master/commands/music)

If you're running into issues, either comment down below or open an issue in the bot's GitHub repository.

I've also written a guide on writing a music quiz(trivia) command, you can check it out [here](https://dev.to/galnir/writing-a-music-trivia-command-for-your-discord-js-bot-3d52)