This project was to create a discord music application that streams youtube audio directly into a voice call, given a link by any user. It also has functionality for music queues, skipping, fetching metadata about the current track, looping, and more. For some other projects, this application was used to integrate Discord with external servers.
|-commands/
| |-joinvc.js
| |-leavevc.js
| |-loop.js
| |-perms.js
| |-play.js
| |-playing.js
| |-queue.js
| |-skip.js
|-events/
| |-interactionCreate.js
| |-ready.js
|-modules/
| |-audioQueue.js
| |-audioTrack.js
|-index.js
|-config.js
|-util.js
// Modules
const config = require('./config.js');
const fs = require('fs');
const path = require('path');
const { Client, Intents, Collection } = require('discord.js');
// Define logging function
const log = console.log;
// Create discord bot client
const client = new Client({
intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_VOICE_STATES],
});
// Retrieve command data
client.commands = new Collection();
const commandsPath = path.join(__dirname, 'commands');
const commandFiles = fs
.readdirSync(commandsPath)
.filter((file) => file.endsWith('.js'));
// Initialize commands
for (const file of commandFiles) {
const filePath = path.join(commandsPath, file);
const command = require(filePath);
client.commands.set(command.meta.name, command);
}
// Retrieve event data
const eventsPath = path.join(__dirname, 'events');
const eventFiles = fs
.readdirSync(eventsPath)
.filter((file) => file.endsWith('.js'));
// Initialize events
for (const file of eventFiles) {
const filePath = path.join(eventsPath, file);
const event = require(filePath);
if (event.once) {
client.once(event.name, (...args) => event.execute(...args));
} else {
client.on(event.name, (...args) => event.execute(...args));
}
}
// Login to discord
client.login(config.meta.token);
// Modules
const config = require('../config.js');
const { SlashCommandBuilder } = require('@discordjs/builders');
const { joinVoiceChannel, getVoiceConnection } = require('@discordjs/voice');
const AudioQueue = require('../modules/audioQueue.js');
const { MessageEmbed } = require('discord.js');
// Define logging function
const log = console.log;
// Command
module.exports = {
disabled: false,
commandPermLevel: 0,
meta: new SlashCommandBuilder()
.setName('joinvc')
.setDescription('Make the bot join your voice channel'),
async execute(interaction, member) {
// Find member voice channel if it exists
const { channelId, guild } = member.voice;
if (!channelId) {
// Member is not in voice channel
await interaction.reply({
embeds: [
new MessageEmbed()
.setColor(config.colors.failure)
.setTitle('Command Failure')
.setAuthor({
name: member.displayName,
iconURL: member.displayAvatarURL({ dynamic: true }),
})
.setDescription('You are not currently in a voice channel')
.setTimestamp(),
],
});
return;
}
// Get voice channel connection if it exists
if (getVoiceConnection(guild.id)) {
// Bot is not in voice channel
await interaction.reply({
embeds: [
new MessageEmbed()
.setColor(config.colors.failure)
.setTitle('Command Failure')
.setAuthor({
name: member.displayName,
iconURL: member.displayAvatarURL({ dynamic: true }),
})
.setDescription('Bot is already in a voice channel')
.setTimestamp(),
],
});
return;
}
// Create voice channel connection
const connection = joinVoiceChannel({
channelId,
guildId: guild.id,
adapterCreator: guild.voiceAdapterCreator,
});
const queue = new AudioQueue(connection, interaction.channel);
queue.init();
connection.queue = queue;
// Notify user of success
await interaction.reply({
embeds: [
new MessageEmbed()
.setColor(config.colors.success)
.setTitle('Command Execution')
.setAuthor({
name: member.displayName,
iconURL: member.displayAvatarURL({ dynamic: true }),
})
.setDescription(
`Successfully joined \`${member.voice.channel.name}\``
)
.setTimestamp(),
],
});
},
};
// Modules
const config = require('../config.js');
const { SlashCommandBuilder } = require('@discordjs/builders');
const { getVoiceConnection } = require('@discordjs/voice');
const { MessageEmbed } = require('discord.js');
// Define logging function
const log = console.log;
// Command
module.exports = {
disabled: false,
commandPermLevel: 0,
meta: new SlashCommandBuilder()
.setName('leavevc')
.setDescription('Make the bot leave your voice channel'),
async execute(interaction, member) {
// Find member voice channel if it exists
const { channelId, guild } = member.voice;
if (!channelId) {
// Member is not in voice channel
await interaction.reply({
embeds: [
new MessageEmbed()
.setColor(config.colors.failure)
.setTitle('Command Failure')
.setAuthor({
name: member.displayName,
iconURL: member.displayAvatarURL({ dynamic: true }),
})
.setDescription('You are not currently in a voice channel')
.setTimestamp(),
],
});
return;
}
// Get voice channel connection if it exists
const connection = getVoiceConnection(guild.id);
if (!connection) {
// Bot is not in voice channel
await interaction.reply({
embeds: [
new MessageEmbed()
.setColor(config.colors.failure)
.setTitle('Command Failure')
.setAuthor({
name: member.displayName,
iconURL: member.displayAvatarURL({ dynamic: true }),
})
.setDescription('Bot is not currently in a voice channel')
.setTimestamp(),
],
});
return;
}
// Destroy connnection
connection.destroy();
// Notify user of success
await interaction.reply({
embeds: [
new MessageEmbed()
.setColor(config.colors.success)
.setTitle('Command Execution')
.setAuthor({
name: member.displayName,
iconURL: member.displayAvatarURL({ dynamic: true }),
})
.setDescription(`Successfully left \`${member.voice.channel.name}\``)
.setTimestamp(),
],
});
},
};
// Modules
const config = require('../config.js');
const { SlashCommandBuilder } = require('@discordjs/builders');
const { getVoiceConnection } = require('@discordjs/voice');
const { MessageEmbed } = require('discord.js');
// Define logging function
const log = console.log;
// Command
module.exports = {
disabled: false,
commandPermLevel: 0,
meta: new SlashCommandBuilder()
.setName('loop')
.setDescription('Loop the queue or the current track')
.addStringOption((option) => {
return option
.setName('input')
.setDescription('Whether to loop the queue or the current track')
.setRequired(true)
.addChoices(
{ name: 'queue', value: 'queue' },
{ name: 'track', value: 'track' }
);
}),
async execute(interaction, member) {
// Find member voice channel if it exists
const { channelId, guild } = member.voice;
if (!channelId) {
// Member is not in voice channel
await interaction.reply({
embeds: [
new MessageEmbed()
.setColor(config.colors.failure)
.setTitle('Command Failure')
.setAuthor({
name: member.displayName,
iconURL: member.displayAvatarURL({ dynamic: true }),
})
.setDescription('You are not currently in a voice channel')
.setTimestamp(),
],
});
return;
}
// Get voice channel connection if it exists
const connection = getVoiceConnection(guild.id);
if (!connection) {
// Bot is not in voice channel
await interaction.reply({
embeds: [
new MessageEmbed()
.setColor(config.colors.failure)
.setTitle('Command Failure')
.setAuthor({
name: member.displayName,
iconURL: member.displayAvatarURL({ dynamic: true }),
})
.setDescription('Bot is not currently in a voice channel')
.setTimestamp(),
],
});
return;
}
const loopTrack = interaction.options.getString('input') === 'track';
if (loopTrack) {
// Loop the current track if one is playing
const looped = connection.queue.loopCurrentTrack();
if (!looped) {
// No track is currently playing
await interaction.reply({
embeds: [
new MessageEmbed()
.setColor(config.colors.failure)
.setTitle('Command Failure')
.setAuthor({
name: member.displayName,
iconURL: member.displayAvatarURL({ dynamic: true }),
})
.setDescription('No track is currently playing')
.setTimestamp(),
],
});
return;
}
// Notify user of success
await interaction.reply({
embeds: [
new MessageEmbed()
.setColor(config.colors.success)
.setTitle('Command Execution')
.setAuthor({
name: member.displayName,
iconURL: member.displayAvatarURL({ dynamic: true }),
})
.setDescription('Looped current track'),
],
});
} else {
// Loop the entire queue
const looped = connection.queue.loopQueue();
// Notify user of success
await interaction.reply({
embeds: [
new MessageEmbed()
.setColor(config.colors.success)
.setTitle('Command Execution')
.setAuthor({
name: member.displayName,
iconURL: member.displayAvatarURL({ dynamic: true }),
})
.setDescription(`${looped ? 'Looped' : 'Un-looped'} the queue`),
],
});
}
},
};
// Modules
const config = require('../config.js');
const { SlashCommandBuilder } = require('@discordjs/builders');
const { MessageEmbed } = require('discord.js');
// Define logging function
const log = console.log;
// Command
module.exports = {
disabled: false,
commandPermLevel: 0,
meta: new SlashCommandBuilder()
.setName('perms')
.setDescription('Check your permissions level'),
async execute(interaction, member) {
// Check if member has perms
const { permLevel } = member;
const hasPerms = permLevel > 0;
await interaction.reply({
embeds: [
new MessageEmbed()
.setColor(hasPerms ? config.colors.success : config.colors.failure)
.setTitle('Permissions Check')
.setAuthor({
name: member.displayName,
iconURL: member.displayAvatarURL({ dynamic: true }),
})
.setDescription(
`You ${hasPerms ? '**do**' : '**do not**'} have permissions`
)
.addField(
'Permissions Level',
`[${permLevel}] ${config.authNames[permLevel]}`
)
.setTimestamp(),
],
});
},
};
// Modules
const config = require('../config.js');
const { SlashCommandBuilder } = require('@discordjs/builders');
const { getVoiceConnection } = require('@discordjs/voice');
const { MessageEmbed } = require('discord.js');
const ytdl = require('ytdl-core');
const validUrl = require('valid-url');
const luxon = require('luxon');
const AudioTrack = require('../modules/audioTrack.js');
// Define logging function
const log = console.log;
// Quality helper function
const opusCodes = [250, 249, 251]; // Priorities of which opus format to choose first
function isOpusAvailable(info) {
let availableCode;
opusCodes.some((code) => {
try {
ytdl.chooseFormat(info.formats, { quality: code });
availableCode = code;
return true;
} catch (err) {}
});
return availableCode;
}
// Duration helper function
function getDurationString(sec) {
return luxon.Duration.fromObject({ seconds: sec }).toFormat('hh:mm:ss');
}
// Command
module.exports = {
disabled: false,
commandPermLevel: 0,
meta: new SlashCommandBuilder()
.setName('play')
.setDescription('Play audio from a youtube link')
.addStringOption((option) => {
return option
.setName('link')
.setDescription('The youtube link from which to play audio')
.setRequired(true);
}),
async execute(interaction, member) {
// Find member voice channel if it exists
const { channelId, guild } = member.voice;
if (!channelId) {
// Member is not in voice channel
await interaction.reply({
embeds: [
new MessageEmbed()
.setColor(config.colors.failure)
.setTitle('Command Failure')
.setAuthor({
name: member.displayName,
iconURL: member.displayAvatarURL({ dynamic: true }),
})
.setDescription('You are not currently in a voice channel')
.setTimestamp(),
],
});
return;
}
// Get voice channel connection if it exists
const connection = getVoiceConnection(guild.id);
if (!connection) {
// Bot is not in voice channel
await interaction.reply({
embeds: [
new MessageEmbed()
.setColor(config.colors.failure)
.setTitle('Command Failure')
.setAuthor({
name: member.displayName,
iconURL: member.displayAvatarURL({ dynamic: true }),
})
.setDescription('Bot is not currently in a voice channel')
.setTimestamp(),
],
});
return;
}
// Validate url
const url = interaction.options.getString('link');
if (!validUrl.isWebUri(url)) {
// Link is not a valid url
await interaction.reply({
embeds: [
new MessageEmbed()
.setColor(config.colors.failure)
.setTitle('Command Failure')
.setAuthor({
name: member.displayName,
iconURL: member.displayAvatarURL({ dynamic: true }),
})
.setDescription('Link is not a valid URL')
.setTimestamp(),
],
});
return;
}
// Validate youtube url
let info;
try {
info = await ytdl.getInfo(url);
} catch (err) {
log(err);
// Link is not a valid youtube url
await interaction.reply({
embeds: [
new MessageEmbed()
.setColor(config.colors.failure)
.setTitle('Command Failure')
.setAuthor({
name: member.displayName,
iconURL: member.displayAvatarURL({ dynamic: true }),
})
.setDescription('Link is not a valid YouTube URL')
.setTimestamp(),
],
});
return;
}
// Check if opus quality is available
const opusCode = isOpusAvailable(info);
// Add metadata
info.url = url;
info.opusCode = opusCode;
const duration = getDurationString(
parseInt(info.videoDetails.lengthSeconds)
);
info.duration = duration;
//info.endTime = getEndTime(parseInt(info.videoDetails.lengthSeconds));
// Add to queue
connection.queue.addTrack(
new AudioTrack({
member,
metadata: info,
})
);
// Notify user of success
await interaction.reply({
embeds: [
new MessageEmbed()
.setColor(config.colors.success)
.setTitle('Command Execution')
.setAuthor({
name: member.displayName,
iconURL: member.displayAvatarURL({ dynamic: true }),
})
.setDescription(`Queued Track`)
.setFields(
{ name: 'URL', value: url },
{ name: 'Title', value: info.videoDetails.title },
{
name: 'Author',
value: info.videoDetails.author.name,
inline: true,
},
{
name: 'Duration',
value: duration,
inline: true,
},
{
name: 'High Quality',
value: opusCode ? config.emojis.success : config.emojis.failure,
inline: true,
}
)
.setTimestamp(),
],
});
},
};
// Modules
const config = require('../config.js');
const { SlashCommandBuilder } = require('@discordjs/builders');
const { getVoiceConnection } = require('@discordjs/voice');
const { MessageEmbed } = require('discord.js');
const luxon = require('luxon');
// Define logging function
const log = console.log;
// Time left helper function
function getTimeLeft(endTime) {
return endTime.diff(luxon.DateTime.now()).toFormat('hh:mm:ss');
}
// Command
module.exports = {
disabled: false,
commandPermLevel: 0,
meta: new SlashCommandBuilder()
.setName('playing')
.setDescription('Get metadata about the playing track'),
async execute(interaction, member) {
// Find member voice channel if it exists
const { channelId, guild } = member.voice;
if (!channelId) {
// Member is not in voice channel
await interaction.reply({
embeds: [
new MessageEmbed()
.setColor(config.colors.failure)
.setTitle('Command Failure')
.setAuthor({
name: member.displayName,
iconURL: member.displayAvatarURL({ dynamic: true }),
})
.setDescription('You are not currently in a voice channel')
.setTimestamp(),
],
});
return;
}
// Get voice channel connection if it exists
const connection = getVoiceConnection(guild.id);
if (!connection) {
// Bot is not in voice channel
await interaction.reply({
embeds: [
new MessageEmbed()
.setColor(config.colors.failure)
.setTitle('Command Failure')
.setAuthor({
name: member.displayName,
iconURL: member.displayAvatarURL({ dynamic: true }),
})
.setDescription('Bot is not currently in a voice channel')
.setTimestamp(),
],
});
return;
}
// Get playing track from queue
const firstTrack = connection.queue.getNextTrack();
if (!firstTrack) {
// No audio resource is playing
await interaction.reply({
embeds: [
new MessageEmbed()
.setColor(config.colors.failure)
.setTitle('Command Failure')
.setAuthor({
name: member.displayName,
iconURL: member.displayAvatarURL({ dynamic: true }),
})
.setDescription('No track is currently playing')
.setTimestamp(),
],
});
return;
}
const { metadata: data } = firstTrack;
// Notify user of success
await interaction.reply({
embeds: [
new MessageEmbed()
.setColor(config.colors.success)
.setTitle('Command Execution')
.setAuthor({
name: member.displayName,
iconURL: member.displayAvatarURL({ dynamic: true }),
})
.setDescription(`Track information`)
.setFields(
{ name: 'URL', value: data.url },
{ name: 'Title', value: data.videoDetails.title },
{
name: 'Author',
value: data.videoDetails.author.name,
inline: true,
},
{
name: 'Time Left',
value: getTimeLeft(data.endTime),
inline: true,
},
{
name: 'High Quality',
value: data.opusCode
? config.emojis.success
: config.emojis.failure,
inline: true,
}
)
.setTimestamp(),
],
});
},
};
// Modules
const config = require('../config.js');
const { SlashCommandBuilder } = require('@discordjs/builders');
const { getVoiceConnection } = require('@discordjs/voice');
const { MessageEmbed } = require('discord.js');
// Define logging function
const log = console.log;
// Command
module.exports = {
disabled: false,
commandPermLevel: 0,
meta: new SlashCommandBuilder()
.setName('queue')
.setDescription('Get the current queue'),
async execute(interaction, member) {
// Find member voice channel if it exists
const { channelId, guild } = member.voice;
if (!channelId) {
// Member is not in voice channel
await interaction.reply({
embeds: [
new MessageEmbed()
.setColor(config.colors.failure)
.setTitle('Command Failure')
.setAuthor({
name: member.displayName,
iconURL: member.displayAvatarURL({ dynamic: true }),
})
.setDescription('You are not currently in a voice channel')
.setTimestamp(),
],
});
return;
}
// Get voice channel connection if it exists
const connection = getVoiceConnection(guild.id);
if (!connection) {
// Bot is not in voice channel
await interaction.reply({
embeds: [
new MessageEmbed()
.setColor(config.colors.failure)
.setTitle('Command Failure')
.setAuthor({
name: member.displayName,
iconURL: member.displayAvatarURL({ dynamic: true }),
})
.setDescription('Bot is not currently in a voice channel')
.setTimestamp(),
],
});
return;
}
// Get queue and construct response
const fields = [];
const queuedTracks = connection.queue.getTracks();
queuedTracks.forEach((track, index) => {
const { metadata: data, member } = track;
fields.push({
name: `**${index + 1}**${track.playing ? ' - Now Playing' : ''}`,
value: `Title: \`${data.videoDetails.title}\` | Duration: \`${data.duration}\` | User: ${member}`,
});
});
// Notify user of success
const embed = new MessageEmbed()
.setColor(config.colors.success)
.setTitle('Command Execution')
.setAuthor({
name: member.displayName,
iconURL: member.displayAvatarURL({ dynamic: true }),
})
.setTimestamp();
if (fields.length > 0) {
embed.setDescription(`Current queue`).setFields(fields);
} else {
embed.setDescription('No queued tracks');
}
await interaction.reply({
embeds: [embed],
});
},
};
// Modules
const config = require('../config.js');
const { SlashCommandBuilder } = require('@discordjs/builders');
const { getVoiceConnection } = require('@discordjs/voice');
const { MessageEmbed } = require('discord.js');
// Define logging function
const log = console.log;
// Command
module.exports = {
disabled: false,
commandPermLevel: 0,
meta: new SlashCommandBuilder()
.setName('skip')
.setDescription('Skip the current track'),
async execute(interaction, member) {
// Find member voice channel if it exists
const { channelId, guild } = member.voice;
if (!channelId) {
// Member is not in voice channel
await interaction.reply({
embeds: [
new MessageEmbed()
.setColor(config.colors.failure)
.setTitle('Command Failure')
.setAuthor({
name: member.displayName,
iconURL: member.displayAvatarURL({ dynamic: true }),
})
.setDescription('You are not currently in a voice channel')
.setTimestamp(),
],
});
return;
}
// Get voice channel connection if it exists
const connection = getVoiceConnection(guild.id);
if (!connection) {
// Bot is not in voice channel
await interaction.reply({
embeds: [
new MessageEmbed()
.setColor(config.colors.failure)
.setTitle('Command Failure')
.setAuthor({
name: member.displayName,
iconURL: member.displayAvatarURL({ dynamic: true }),
})
.setDescription('Bot is not currently in a voice channel')
.setTimestamp(),
],
});
return;
}
// Skip the current track if one is playing
const skipped = connection.queue.skipCurrentTrack();
if (!skipped) {
// No track is currently playing
await interaction.reply({
embeds: [
new MessageEmbed()
.setColor(config.colors.failure)
.setTitle('Command Failure')
.setAuthor({
name: member.displayName,
iconURL: member.displayAvatarURL({ dynamic: true }),
})
.setDescription('No track is currently playing')
.setTimestamp(),
],
});
return;
}
// Notify user of success
await interaction.reply({
embeds: [
new MessageEmbed()
.setColor(config.colors.success)
.setTitle('Command Execution')
.setAuthor({
name: member.displayName,
iconURL: member.displayAvatarURL({ dynamic: true }),
})
.setDescription(`Skipped current track`),
],
});
},
};
// Modules
const config = require('../config.js');
const {
demuxProbe,
createAudioPlayer,
createAudioResource,
AudioPlayerStatus,
VoiceConnectionStatus,
} = require('@discordjs/voice');
const { MessageEmbed } = require('discord.js');
const ytdl = require('ytdl-core');
const luxon = require('luxon');
// End time helper function
function getEndTime(sec) {
return luxon.DateTime.now().plus({ seconds: sec });
}
// Audio queue
module.exports = class AudioQueue {
constructor(connection, channel) {
// Create internal structures
this.initialized = false;
this.connection = connection;
this.channel = channel;
this.queue = [];
this.looped = false;
}
init() {
if (this.initialized) {
log('Audio queue already initialized');
return;
}
// Create audio player
const player = createAudioPlayer();
this.connection.subscribe(player);
this.player = player;
// Attach error handler
player.on('error', (err) => {
log(`Error with audio playback: ${err}`);
// Stop audio playback
player.stop(true);
// Notify user of error
this.channel.send({
embeds: [
new MessageEmbed()
.setColor(config.colors.failure)
.setTitle('Audio Failure')
.setDescription('An error has occurred with audio playback')
.setTimestamp(),
],
});
});
this.connection.on('stateChange', (oldState, newState) => {
// console.log(
// `Voice connection transitioned from ${oldState.status} to ${newState.status}`
// );
if (
oldState.status === VoiceConnectionStatus.Ready &&
newState.status === VoiceConnectionStatus.Connecting
) {
this.connection.configureNetworking();
}
});
// player.on('stateChange', (oldState, newState) => {
// console.log(
// `Audio player transitioned from ${oldState.status} to ${newState.status}`
// );
// });
// Attach idle handler
player.on(AudioPlayerStatus.Idle, () => {
// Remove first track if not looped
if (!this.getNextTrack().looped) {
const lastTrack = this.removeFirstTrack();
// Add the track to the end of the queue if queue is looped
if (this.looped) {
lastTrack.playing = false;
this.addTrack(lastTrack);
}
}
// Play next track
this.playNextTrack();
});
}
async playNextTrack() {
// Get next track if it exists
const nextTrack = this.getNextTrack();
if (!nextTrack) {
// Notify user that queue has finished
this.channel.send({
embeds: [
new MessageEmbed()
.setColor(config.colors.info)
.setTitle('Queue Status')
.setDescription('All tracks in the queue have finished playing')
.setTimestamp(),
],
});
return;
}
nextTrack.metadata.endTime = getEndTime(
parseInt(nextTrack.metadata.videoDetails.lengthSeconds)
);
nextTrack.playing = true;
// Check for opus quality
const ytdlOptions = { highWaterMark: 1048576 * 32 };
if (nextTrack.metadata.opusCode) {
ytdlOptions.quality = nextTrack.metadata.opusCode.toString();
}
// Download video from youtube link
const { stream, type } = await demuxProbe(
ytdl.downloadFromInfo(nextTrack.metadata, ytdlOptions)
);
const resource = createAudioResource(stream, {
inputType: type,
metadata: nextTrack.metadata,
});
// Play resource
this.player.play(resource);
}
addTrack(info) {
this.queue.push(info);
// Play track if first track
if (this.queue.length === 1) {
this.playNextTrack();
}
}
getNextTrack() {
return this.queue[0];
}
getTracks() {
return this.queue;
}
skipCurrentTrack() {
if (this.queue.length <= 0) {
return false;
}
// Unloop the current track (just in case)
this.getNextTrack().looped = false;
// Stop the player (the idle handler will automatically move to the next track)
this.player.stop(true);
return true;
}
loopCurrentTrack() {
if (this.queue.length <= 0) {
return false;
}
// Stop the player (the idle handler will automatically move to the next track)
this.getNextTrack().looped = true;
return true;
}
loopQueue() {
this.looped = !this.looped;
return this.looped;
}
removeFirstTrack() {
return this.queue.shift();
}
removeTrackAt(pos) {
return this.queue.splice(pos, 1);
}
clear() {
this.queue = [];
}
};
// Audio track
module.exports = class AudioTrack {
constructor(options) {
// Create internal structures
const { member, metadata } = options;
this.member = member;
this.metadata = metadata;
this.playing = false;
this.looped = false;
}
};