The discord API provides you with an easy tool to create and use your own bots and tools. Today we are going to take a look at how we can create a basic music bot and add it to our server. The bot will be able to play, skip and stop the music and will also support queuing functionality.
Table of contents
Open Table of contents
Prerequisites
Before we get started creating the bot make sure that you have installed all the needed tools.
After the installation, we can continue by setting up our discord bot.
Setting up a discord bot
First, we need to create a new application on the discord development portal.
We can do so by visiting the portal and clicking on new application.
After that, we need to give our application a name and click the create button.
After that, we need to select the bot tab and click on add bot.
Now our bot is created and we can continue with inviting it to our server.
Adding the bot to your server
After creating our bot we can invite it using the OAuth2 URL Generator.
For that, we need to navigate to the OAuth2 page and select bot in the scope tap.
After that, we need to select the needed permissions to play music and read messages.
Then we can copy our generated URL and paste it into our browser.
After pasting it, we add it to our server by selecting the server and clicking the authorize button.
Creating our project
Now we can start creating our project using our terminal.
First, we create a directory and move into it. We can do so by using these two commands.
mkdir musicbot && cd musicbot
After that, we can create our project modules using the npm init command. After entering the command you will be asked some questions just answer them and continue.
Then we just need to create the two files we will work in.
touch index.js && touch config.json
Now we just need to open our project in our text editor. I personally use VS Code and can open it with the following command.
code .
Discord js basics
Now we just need to install some dependencies before we can get started.
npm install discord.js@^12.5.3 ffmpeg fluent-ffmpeg @discordjs/opus ytdl-core --save
After the installation finished we can continue with writing our config.json file. Here we save the token of our bot and the prefix he should listen for.
{
"prefix": "!",
"token": "your-token"
}
To get your token you need to visit the discord developer portal again and copy it from the bot section.
That are the only things we need to do in our config.json
file. So let’s start writing our Javascript code. The article includes two versions, one for the new discord.js v13, which uses slash commands combined with the discord-player library to implement the music functionality and one for discord.js v12.5.3, which implements the functionality without a library. The older version is better for learning purposes and the newer version works with the current discord.js and is a lot easier to implement, so choose which you prefer.
Discord bot version 0.13
Now we just need to install some dependencies before we can get started.
npm install discord.js discord-player @discordjs/opus
After installing the dependencies, we can import them in our dependencies.
const { Client, GuildMember, Intents } = require("discord.js");
const { Player, QueryType } = require("discord-player");
const config = require("./config.json");
After that, we can create our client and log in using our token.
const client = new Client({
intents: [Intents.FLAGS.GUILD_VOICE_STATES, Intents.FLAGS.GUILD_MESSAGES, Intents.FLAGS.GUILDS]
});
client.login(config.token);
Now let’s add some basic listeners that console.log when they get executed.
client.once('ready', () => {
console.log('Ready!');
});
client.on("error", console.error);
client.on("warn", console.warn);
After that, we can start our bot using the node command and he should be online on Discord and print “Ready!” in the console.
node index.js
Creating discord player
Now that the client for the discord bot is created, we can continue by initializing our player, which will allow us to play and manage music in our Discord channel.
const player = new Player(client);
We can also add some error handlers that will be called if an error occurs.
player.on("error", (queue, error) => {
console.log(`[${queue.guild.name}] Error emitted from the queue: ${error.message}`);
});
player.on("connectionError", (queue, error) => {
console.log(`[${queue.guild.name}] Error emitted from the connection: ${error.message}`);
});
The last thing we need to do is add listeners for the different player events like a song starting or being added.
player.on("trackStart", (queue, track) => {
queue.metadata.send(`🎶 | Started playing: **${track.title}** in **${queue.connection.channel.name}**!`);
});
player.on("trackAdd", (queue, track) => {
queue.metadata.send(`🎶 | Track **${track.title}** queued!`);
});
player.on("botDisconnect", (queue) => {
queue.metadata.send("❌ | I was manually disconnected from the voice channel, clearing queue!");
});
player.on("channelEmpty", (queue) => {
queue.metadata.send("❌ | Nobody is in the voice channel, leaving...");
});
player.on("queueEnd", (queue) => {
queue.metadata.send("✅ | Queue finished!");
});
In most cases, we just send a message into the Discord text channel using the send()
function.
Adding slash commands
After the player has been set up successfully we can continue by adding our Slash commands to our client. This step is needed so Discord knows which commands the bot can execute.
client.on("messageCreate", async (message) => {
if (message.author.bot || !message.guild) return;
if (!client.application?.owner) await client.application?.fetch();
});
We can do this by implementing a simple !deploy
command that saves our commands in the guild.commands
variable of a message. A slash command has a name, a description, and an optional options field that contains the command’s parameters. For example, the play command takes a song query as an argument.
client.on("messageCreate", async (message) => {
...
if (message.content === "!deploy" && message.author.id === client.application?.owner?.id) {
await message.guild.commands.set([
{
name: "play",
description: "Plays a song from youtube",
options: [
{
name: "query",
type: "STRING",
description: "The song you want to play",
required: true
}
]
},
{
name: "skip",
description: "Skip to the current song"
},
{
name: "queue",
description: "See the queue"
},
{
name: "stop",
description: "Stop the player"
},
]);
await message.reply("Deployed!");
}
});
After entering !deploy
in your Discord text chat the slash commands will be added to your application. When typing /
into the chat you should see something similar to this:
Implementing interactions
Once the interactions (slash commands) are defined, we can continue by implementing them. All slash commands trigger the interactionCreate
event and can be implemented inside the async function below. Before executing any functionality, we run a few conditionals to check if the user is allowed to perform the given functionality.
client.on("interactionCreate", async (interaction) => {
if (!interaction.isCommand() || !interaction.guildId) return;
if (!(interaction.member instanceof GuildMember) || !interaction.member.voice.channel) {
return void interaction.reply({ content: "You are not in a voice channel!", ephemeral: true });
}
if (interaction.guild.me.voice.channelId && interaction.member.voice.channelId !== interaction.guild.me.voice.channelId) {
return void interaction.reply({ content: "You are not in my voice channel!", ephemeral: true });
}
});
After that we check which command is being executed by matching the commandName
with the name of the commands we defined above.
client.on("interactionCreate", async (interaction) => {
...
if (interaction.commandName === "play") {
// TODO: Implement play command
}
});
The implementation can then be added inside of the if
statement.
Playing songs
The play command requires us to search for the provided song and add the found result to the current queue of songs. Let’s start by retrieving the user-provided query using the options.get()
function. After that we can use the player.search()
function to search for the desired song.
if (interaction.commandName === "play") {
await interaction.deferReply();
const query = interaction.options.get("query").value;
const searchResult = await player
.search(query, {
requestedBy: interaction.user,
searchEngine: QueryType.AUTO
})
.catch(() => {});
if (!searchResult || !searchResult.tracks.length) return void interaction.followUp({ content: "No results were found!" });
}
Now that we have the song, we can create a queue for the songs (if there is already a queue the createQueue
function will return the existing one). Once the queue is created, we can try joining the user’s voice channel. If that is successful, we add the song to the current queue using the addTracks
function.
if (interaction.commandName === "play") {
...
const queue = await player.createQueue(interaction.guild, {
metadata: interaction.channel
});
try {
if (!queue.connection) await queue.connect(interaction.member.voice.channel);
} catch {
void player.deleteQueue(interaction.guildId);
return void interaction.followUp({ content: "Could not join your voice channel!" });
}
await interaction.followUp({ content: `⏱ | Loading your ${searchResult.playlist ? "playlist" : "track"}...` });
searchResult.playlist ? queue.addTracks(searchResult.tracks) : queue.addTrack(searchResult.tracks[0]);
if (!queue.playing) await queue.play();
}
Lastly, if the queue isn’t already playing let’s start it using the play()
function.
Skipping songs
Skipping is quite easy and can be done by calling the skip()
function on the queue.
if (interaction.commandName === "skip") {
await interaction.deferReply();
const queue = player.getQueue(interaction.guildId);
if (!queue || !queue.playing) return void interaction.followUp({ content: "❌ | No music is being played!" });
const currentTrack = queue.current;
const success = queue.skip();
return void interaction.followUp({
content: success ? `✅ | Skipped **${currentTrack}**!` : "❌ | Something went wrong!"
});
}
If the action is successful we can write a message to the Discord text channel using interaction.followUp()
.
Stopping songs
The stop functionality will remove all the songs from the queue and the bot will leave the voice channel. This can be achieved by destroying the current queue which automatically makes the bot leave the voice channel (unless configured otherwise in the player configuration).
else if (interaction.commandName === "stop") {
await interaction.deferReply();
const queue = player.getQueue(interaction.guildId);
if (!queue || !queue.playing) return void interaction.followUp({ content: "❌ | No music is being played!" });
queue.destroy();
return void interaction.followUp({ content: "🛑 | Stopped the player!" });
}
Complete source code for the index.js:
Here you can get the complete source code for our music bot:
const { Client, GuildMember, Intents } = require("discord.js");
const { Player, QueryType } = require("discord-player");
const config = require("./config.json");
const client = new Client({
intents: [Intents.FLAGS.GUILD_VOICE_STATES, Intents.FLAGS.GUILD_MESSAGES, Intents.FLAGS.GUILDS]
});
client.on("ready", () => {
console.log("Bot is online!");
client.user.setActivity({
name: "🎶 | Music Time",
type: "LISTENING"
});
});
client.on("error", console.error);
client.on("warn", console.warn);
const player = new Player(client);
player.on("error", (queue, error) => {
console.log(`[${queue.guild.name}] Error emitted from the queue: ${error.message}`);
});
player.on("connectionError", (queue, error) => {
console.log(`[${queue.guild.name}] Error emitted from the connection: ${error.message}`);
});
player.on("trackStart", (queue, track) => {
queue.metadata.send(`🎶 | Started playing: **${track.title}** in **${queue.connection.channel.name}**!`);
});
player.on("trackAdd", (queue, track) => {
queue.metadata.send(`🎶 | Track **${track.title}** queued!`);
});
player.on("botDisconnect", (queue) => {
queue.metadata.send("❌ | I was manually disconnected from the voice channel, clearing queue!");
});
player.on("channelEmpty", (queue) => {
queue.metadata.send("❌ | Nobody is in the voice channel, leaving...");
});
player.on("queueEnd", (queue) => {
queue.metadata.send("✅ | Queue finished!");
});
client.on("messageCreate", async (message) => {
if (message.author.bot || !message.guild) return;
if (!client.application?.owner) await client.application?.fetch();
if (message.content === "!deploy" && message.author.id === client.application?.owner?.id) {
await message.guild.commands.set([
{
name: "play",
description: "Plays a song from youtube",
options: [
{
name: "query",
type: "STRING",
description: "The song you want to play",
required: true
}
]
},
{
name: "skip",
description: "Skip to the current song"
},
{
name: "stop",
description: "Stop the player"
},
]);
await message.reply("Deployed!");
}
});
client.on("interactionCreate", async (interaction) => {
if (!interaction.isCommand() || !interaction.guildId) return;
if (!(interaction.member instanceof GuildMember) || !interaction.member.voice.channel) {
return void interaction.reply({ content: "You are not in a voice channel!", ephemeral: true });
}
if (interaction.guild.me.voice.channelId && interaction.member.voice.channelId !== interaction.guild.me.voice.channelId) {
return void interaction.reply({ content: "You are not in my voice channel!", ephemeral: true });
}
if (interaction.commandName === "play") {
await interaction.deferReply();
const query = interaction.options.get("query").value;
const searchResult = await player
.search(query, {
requestedBy: interaction.user,
searchEngine: QueryType.AUTO
})
.catch(() => {});
if (!searchResult || !searchResult.tracks.length) return void interaction.followUp({ content: "No results were found!" });
const queue = await player.createQueue(interaction.guild, {
metadata: interaction.channel
});
try {
if (!queue.connection) await queue.connect(interaction.member.voice.channel);
} catch {
void player.deleteQueue(interaction.guildId);
return void interaction.followUp({ content: "Could not join your voice channel!" });
}
await interaction.followUp({ content: `⏱ | Loading your ${searchResult.playlist ? "playlist" : "track"}...` });
searchResult.playlist ? queue.addTracks(searchResult.tracks) : queue.addTrack(searchResult.tracks[0]);
if (!queue.playing) await queue.play();
} else if (interaction.commandName === "skip") {
await interaction.deferReply();
const queue = player.getQueue(interaction.guildId);
if (!queue || !queue.playing) return void interaction.followUp({ content: "❌ | No music is being played!" });
const currentTrack = queue.current;
const success = queue.skip();
return void interaction.followUp({
content: success ? `✅ | Skipped **${currentTrack}**!` : "❌ | Something went wrong!"
});
} else if (interaction.commandName === "stop") {
await interaction.deferReply();
const queue = player.getQueue(interaction.guildId);
if (!queue || !queue.playing) return void interaction.followUp({ content: "❌ | No music is being played!" });
queue.destroy();
return void interaction.followUp({ content: "🛑 | Stopped the player!" });
} else {
interaction.reply({
content: "Unknown command!",
ephemeral: true
});
}
});
client.login(config.token);
Discord bot version 0.12
Now we just need to install some dependencies before we can get started.
npm install discord.js ffmpeg fluent-ffmpeg @discordjs/opus ytdl-core --save
After installing the dependencies, we can import them in our dependencies.
const Discord = require('discord.js');
const {
prefix,
token,
} = require('./config.json');
const ytdl = require('ytdl-core');
After that, we can create our client and login using our token.
const client = new Discord.Client();
client.login(token);
Now let’s add some basic listeners that console.log when they get executed.
client.once('ready', () => {
console.log('Ready!');
});
client.once('reconnecting', () => {
console.log('Reconnecting!');
});
client.once('disconnect', () => {
console.log('Disconnect!');
});
After that, we can start our bot using the node command and he should be online on discord and print “Ready!” in the console.
node index.js
Reading messages
Now that our bot is on our server and able to go online, we can start reading chat messages and responding to them.
To read messages we only need to write one simple function.
client.on('message', async message => {
}
Here we create a listener for the message event and get the message and save it into a message object if it is triggered.
Now we need to check if the message is from our own bot and ignore it if it is.
if (message.author.bot) return;
In this line, we check if the author of the message is our bot and return if it is.
After that, we check if the message starts with the prefix we defined earlier and return if it doesn’t.
if (!message.content.startsWith(prefix)) return;
After that, we can check which command we need to execute. We can do so using some simple if statements.
const serverQueue = queue.get(message.guild.id);
if (message.content.startsWith(`${prefix}play`)) {
execute(message, serverQueue);
return;
} else if (message.content.startsWith(`${prefix}skip`)) {
skip(message, serverQueue);
return;
} else if (message.content.startsWith(`${prefix}stop`)) {
stop(message, serverQueue);
return;
} else {
message.channel.send("You need to enter a valid command!");
}
In this code block, we check which command to execute and call the command. If the input command isn’t valid we write an error message into the chat using the send() function.
Now that we know which command we need to execute we can start implementing these commands.
Adding songs
Let’s start by adding the play command. For that, we need a song and a guild (A guild represent an isolated collection of users and channels and is often referred to as a server). We also need the ytdl library we installed earlier.
First, we need to create a map with the name of the queue where we save all the songs we type in the chat.
const queue = new Map();
After that, we create an async function called execute and check if the user is in a voice chat and if the bot has the right permission. If not we write an error message and return.
async function execute(message, serverQueue) {
const args = message.content.split(" ");
const voiceChannel = message.member.voice.channel;
if (!voiceChannel)
return message.channel.send(
"You need to be in a voice channel to play music!"
);
const permissions = voiceChannel.permissionsFor(message.client.user);
if (!permissions.has("CONNECT") || !permissions.has("SPEAK")) {
return message.channel.send(
"I need the permissions to join and speak in your voice channel!"
);
}
}
Now we can continue with getting the song info and saving it into a song object. For that, we use our ytdl
library which gets the song information from the Youtube link.
const songInfo = await ytdl.getInfo(args[1]);
const song = {
title: songInfo.title,
url: songInfo.video_url,
};
This will get the information of the song using the ytdl
library we installed earlier. Then we save the information we need into a song object.
After saving the song info we just need to create a contract we can add to our queue. To do so we first need to check if our serverQueue is already defined which means that music is already playing. If so we just need to add the song to our existing serverQueue and send a success message. If not we need to create it and try to join the voice channel and start playing music.
if (!serverQueue) {
}else {
serverQueue.songs.push(song);
console.log(serverQueue.songs);
return message.channel.send(`${song.title} has been added to the queue!`);
}
Here we check if the serverQueue
is empty and add the song to it if it’s not. Now we just need to create our contract if the serverQueue
is null.
// Creating the contract for our queue
const queueContruct = {
textChannel: message.channel,
voiceChannel: voiceChannel,
connection: null,
songs: [],
volume: 5,
playing: true,
};
// Setting the queue using our contract
queue.set(message.guild.id, queueContruct);
// Pushing the song to our songs array
queueContruct.songs.push(song);
try {
// Here we try to join the voicechat and save our connection into our object.
var connection = await voiceChannel.join();
queueContruct.connection = connection;
// Calling the play function to start a song
play(message.guild, queueContruct.songs[0]);
} catch (err) {
// Printing the error message if the bot fails to join the voicechat
console.log(err);
queue.delete(message.guild.id);
return message.channel.send(err);
}
In this code block, we create a contract and add our song to the songs array. After that, we try to join the voice chat of the user and call our play()
function we will implement after that.
Playing songs
Now that we can add our songs to our queue and create a contract if there isn’t one yet we can start implementing our play functionality.
First, we will create a function called play which takes two parameters (the guild and the song we want to play) and checks if the song is empty. If so we will just leave the voice channel and delete the queue.
function play(guild, song) {
const serverQueue = queue.get(guild.id);
if (!song) {
serverQueue.voiceChannel.leave();
queue.delete(guild.id);
return;
}
}
After that, we will start playing our song using the play()
function of the connection and passing the URL of our song.
const dispatcher = serverQueue.connection
.play(ytdl(song.url))
.on("finish", () => {
serverQueue.songs.shift();
play(guild, serverQueue.songs[0]);
})
.on("error", error => console.error(error));
dispatcher.setVolumeLogarithmic(serverQueue.volume / 5);
serverQueue.textChannel.send(`Start playing: **${song.title}**`);
Here we create a stream and pass it the URL of our song. We also add two listeners that handle the end and error event.
Note: This is a recursive function which means that it calls itself over and over again. We use recursion so it plays the next song when the song is finished.
Now we are ready to play a song by just typing !play URL in the chat.
Skipping songs
Now we can start implementing the skipping functionality. For that, we just need to end the dispatcher we created in our play()
function so it starts the next song.
function skip(message, serverQueue) {
if (!message.member.voice.channel)
return message.channel.send(
"You have to be in a voice channel to stop the music!"
);
if (!serverQueue)
return message.channel.send("There is no song that I could skip!");
serverQueue.connection.dispatcher.end();
}
Here we check if the user that typed the command is in a voice channel and if there is a song to skip.
Stopping songs
The stop()
function is almost the same as the skip()
except that we clear the songs array which will make our bot delete the queue and leave the voice chat.
function stop(message, serverQueue) {
if (!message.member.voice.channel)
return message.channel.send(
"You have to be in a voice channel to stop the music!"
);
serverQueue.songs = [];
serverQueue.connection.dispatcher.end();
}
Complete source code for the index.js:
Here you can get the complete source code for our music bot:
const Discord = require("discord.js");
const { prefix, token } = require("./config.json");
const ytdl = require("ytdl-core");
const client = new Discord.Client();
const queue = new Map();
client.once("ready", () => {
console.log("Ready!");
});
client.once("reconnecting", () => {
console.log("Reconnecting!");
});
client.once("disconnect", () => {
console.log("Disconnect!");
});
client.on("message", async message => {
if (message.author.bot) return;
if (!message.content.startsWith(prefix)) return;
const serverQueue = queue.get(message.guild.id);
if (message.content.startsWith(`${prefix}play`)) {
execute(message, serverQueue);
return;
} else if (message.content.startsWith(`${prefix}skip`)) {
skip(message, serverQueue);
return;
} else if (message.content.startsWith(`${prefix}stop`)) {
stop(message, serverQueue);
return;
} else {
message.channel.send("You need to enter a valid command!");
}
});
async function execute(message, serverQueue) {
const args = message.content.split(" ");
const voiceChannel = message.member.voice.channel;
if (!voiceChannel)
return message.channel.send(
"You need to be in a voice channel to play music!"
);
const permissions = voiceChannel.permissionsFor(message.client.user);
if (!permissions.has("CONNECT") || !permissions.has("SPEAK")) {
return message.channel.send(
"I need the permissions to join and speak in your voice channel!"
);
}
const songInfo = await ytdl.getInfo(args[1]);
const song = {
title: songInfo.title,
url: songInfo.video_url
};
if (!serverQueue) {
const queueContruct = {
textChannel: message.channel,
voiceChannel: voiceChannel,
connection: null,
songs: [],
volume: 5,
playing: true
};
queue.set(message.guild.id, queueContruct);
queueContruct.songs.push(song);
try {
var connection = await voiceChannel.join();
queueContruct.connection = connection;
play(message.guild, queueContruct.songs[0]);
} catch (err) {
console.log(err);
queue.delete(message.guild.id);
return message.channel.send(err);
}
} else {
serverQueue.songs.push(song);
return message.channel.send(`${song.title} has been added to the queue!`);
}
}
function skip(message, serverQueue) {
if (!message.member.voice.channel)
return message.channel.send(
"You have to be in a voice channel to stop the music!"
);
if (!serverQueue)
return message.channel.send("There is no song that I could skip!");
serverQueue.connection.dispatcher.end();
}
function stop(message, serverQueue) {
if (!message.member.voice.channel)
return message.channel.send(
"You have to be in a voice channel to stop the music!"
);
serverQueue.songs = [];
serverQueue.connection.dispatcher.end();
}
function play(guild, song) {
const serverQueue = queue.get(guild.id);
if (!song) {
serverQueue.voiceChannel.leave();
queue.delete(guild.id);
return;
}
const dispatcher = serverQueue.connection
.play(ytdl(song.url))
.on("finish", () => {
serverQueue.songs.shift();
play(guild, serverQueue.songs[0]);
})
.on("error", error => console.error(error));
dispatcher.setVolumeLogarithmic(serverQueue.volume / 5);
serverQueue.textChannel.send(`Start playing: **${song.title}**`);
}
client.login(token);
Conclusion
You made it all the way until the end! Hope that this article helped you understand the Discord API and how you can use it to create a simple bot. If you want to see an example of a more advanced discord bot you can visit my Github repository.
If you have found this useful, please consider recommending and sharing it with other fellow developers.
If you have any questions or feedback, let me know in the comments down below.