UNPKG

discord.js-music-v11

Version:
411 lines (358 loc) 12.7 kB
const YoutubeDL = require('youtube-dl'); const ytdl = require('ytdl-core'); /** * Takes a discord.js client and turns it into a music bot. * Thanks to 'derekmartinez18' for helping. * * @param {Client} client - The discord.js client. * @param {object} options - (Optional) Options to configure the music bot. Acceptable options are: * prefix: The prefix to use for the commands (default '!'). * global: Whether to use a global queue instead of a server-specific queue (default false). * maxQueueSize: The maximum queue size (default 20). * anyoneCanSkip: Allow anybody to skip the song. * clearInvoker: Clear the command message. * volume: The default volume of the player. */ module.exports = function (client, options) { // Get all options. let PREFIX = (options && options.prefix) || '!'; let GLOBAL = (options && options.global) || false; let MAX_QUEUE_SIZE = (options && options.maxQueueSize) || 20; let DEFAULT_VOLUME = (options && options.volume) || 50; let ALLOW_ALL_SKIP = (options && options.anyoneCanSkip) || false; let CLEAR_INVOKER = (options && options.clearInvoker) || false; // Create an object of queues. let queues = {}; // Catch message events. client.on('message', msg => { const message = msg.content.trim(); // Check if the message is a command. if (message.toLowerCase().startsWith(PREFIX.toLowerCase())) { // Get the command and suffix. const command = message.substring(PREFIX.length).split(/[ \n]/)[0].toLowerCase().trim(); const suffix = message.substring(PREFIX.length + command.length).trim(); // Process the commands. switch (command) { case 'play': return play(msg, suffix); case 'skip': return skip(msg, suffix); case 'queue': return queue(msg, suffix); case 'pause': return pause(msg, suffix); case 'resume': return resume(msg, suffix); case 'volume': return volume(msg, suffix); case 'leave': return leave(msg, suffix); case 'clearqueue': return clearqueue(msg, suffix); } if (CLEAR_INVOKER) { msg.delete(); } } }); /** * Checks if a user is an admin. * * @param {GuildMember} member - The guild member * @returns {boolean} - */ function isAdmin(member) { return member.hasPermission("ADMINISTRATOR"); } /** * Checks if the user can skip the song. * * @param {GuildMember} member - The guild member * @param {array} queue - The current queue * @returns {boolean} - If the user can skip */ function canSkip(member, queue) { if (ALLOW_ALL_SKIP) return true; else if (queue[0].requester === member.id) return true; else if (isAdmin(member)) return true; else return false; } /** * Gets the song queue of the server. * * @param {integer} server - The server id. * @returns {object} - The song queue. */ function getQueue(server) { // Check if global queues are enabled. if (GLOBAL) server = '_'; // Change to global queue. // Return the queue. if (!queues[server]) queues[server] = []; return queues[server]; } /** * The command for adding a song to the queue. * * @param {Message} msg - Original message. * @param {string} suffix - Command suffix. * @returns {<promise>} - The response edit. */ function play(msg, suffix) { // Make sure the user is in a voice channel. if (msg.member.voiceChannel === undefined) return msg.channel.sendMessage(wrap('You\'re not in a voice channel.')); // Make sure the suffix exists. if (!suffix) return msg.channel.sendMessage(wrap('No video specified!')); // Get the queue. const queue = getQueue(msg.guild.id); // Check if the queue has reached its maximum size. if (queue.length >= MAX_QUEUE_SIZE) { return msg.channel.sendMessage(wrap('Maximum queue size reached!')); } // Get the video information. msg.channel.sendMessage(wrap('Searching...')).then(response => { var searchstring = suffix if (!suffix.toLowerCase().startsWith('http')) { searchstring = 'gvsearch1:' + suffix; } YoutubeDL.getInfo(searchstring, ['-q', '--no-warnings', '--force-ipv4'], (err, info) => { // Verify the info. if (err || info.format_id === undefined || info.format_id.startsWith('0')) { return response.edit(wrap('Invalid video!')); } info.requester = msg.author.id; // Queue the video. response.edit(wrap('Queued: ' + info.title)).then(() => { queue.push(info); // Play if only one element in the queue. if (queue.length === 1) executeQueue(msg, queue); }).catch(console.log); }); }).catch(console.log); } /** * The command for skipping a song. * * @param {Message} msg - Original message. * @param {string} suffix - Command suffix. * @returns {<promise>} - The response message. */ function skip(msg, suffix) { // Get the voice connection. const voiceConnection = client.voiceConnections.find(val => val.channel.guild.id == msg.guild.id); if (voiceConnection === null) return msg.channel.sendMessage(wrap('No music being played.')); // Get the queue. const queue = getQueue(msg.guild.id); if (!canSkip(msg.member, queue)) return msg.channel.sendMessage(wrap('You cannot skip this as you didn\'t queue it.')).then((response) => { response.delete(5000); }); // Get the number to skip. let toSkip = 1; // Default 1. if (!isNaN(suffix) && parseInt(suffix) > 0) { toSkip = parseInt(suffix); } toSkip = Math.min(toSkip, queue.length); // Skip. queue.splice(0, toSkip - 1); // Resume and stop playing. const dispatcher = voiceConnection.player.dispatcher; if (voiceConnection.paused) dispatcher.resume(); dispatcher.end(); msg.channel.sendMessage(wrap('Skipped ' + toSkip + '!')); } /** * The command for listing the queue. * * @param {Message} msg - Original message. * @param {string} suffix - Command suffix. */ function queue(msg, suffix) { // Get the queue. const queue = getQueue(msg.guild.id); // Get the queue text. const text = queue.map((video, index) => ( (index + 1) + ': ' + video.title )).join('\n'); // Get the status of the queue. let queueStatus = 'Stopped'; const voiceConnection = client.voiceConnections.find(val => val.channel.guild.id == msg.guild.id); if (voiceConnection !== null) { const dispatcher = voiceConnection.player.dispatcher; queueStatus = dispatcher.paused ? 'Paused' : 'Playing'; } // Send the queue and status. msg.channel.sendMessage(wrap('Queue (' + queueStatus + '):\n' + text)); } /** * The command for pausing the current song. * * @param {Message} msg - Original message. * @param {string} suffix - Command suffix. * @returns {<promise>} - The response message. */ function pause(msg, suffix) { // Get the voice connection. const voiceConnection = client.voiceConnections.find(val => val.channel.guild.id == msg.guild.id); if (voiceConnection === null) return msg.channel.sendMessage(wrap('No music being played.')); if (!isAdmin(msg.member)) return msg.channel.sendMessage(wrap('You are not authorized to use this.')); // Pause. msg.channel.sendMessage(wrap('Playback paused.')); const dispatcher = voiceConnection.player.dispatcher; if (!dispatcher.paused) dispatcher.pause(); } /** * The command for leaving the channel and clearing the queue. * * @param {Message} msg - Original message. * @param {string} suffix - Command suffix. * @returns {<promise>} - The response message. */ function leave(msg, suffix) { if (isAdmin(msg.member)) { const voiceConnection = client.voiceConnections.find(val => val.channel.guild.id == msg.guild.id); if (voiceConnection === null) return msg.channel.sendMessage(wrap('I\'m not in any channel!.')); // Clear the queue. const queue = getQueue(msg.guild.id); queue.splice(0, queue.length); // End the stream and disconnect. voiceConnection.player.dispatcher.end(); voiceConnection.disconnect(); } else { msg.channel.sendMessage(wrap('You don\'t have permission to use that command!')); } } /** * The command for clearing the song queue. * * @param {Message} msg - Original message. * @param {string} suffix - Command suffix. */ function clearqueue(msg, suffix) { if (isAdmin(msg.member)) { const queue = getQueue(msg.guild.id); queue.splice(0, queue.length); msg.channel.sendMessage(wrap('Queue cleared!')); } else { msg.channel.sendMessage(wrap('You don\'t have permission to use that command!')); } } /** * The command for resuming the current song. * * @param {Message} msg - Original message. * @param {string} suffix - Command suffix. * @returns {<promise>} - The response message. */ function resume(msg, suffix) { // Get the voice connection. const voiceConnection = client.voiceConnections.find(val => val.channel.guild.id == msg.guild.id); if (voiceConnection === null) return msg.channel.sendMessage(wrap('No music being played.')); if (!isAdmin(msg.member)) return msg.channel.sendMessage(wrap('You are not authorized to use this.')); // Resume. msg.channel.sendMessage(wrap('Playback resumed.')); const dispatcher = voiceConnection.player.dispatcher; if (dispatcher.paused) dispatcher.resume(); } /** * The command for changing the song volume. * * @param {Message} msg - Original message. * @param {string} suffix - Command suffix. * @returns {<promise>} - The response message. */ function volume(msg, suffix) { // Get the voice connection. const voiceConnection = client.voiceConnections.find(val => val.channel.guild.id == msg.guild.id); if (voiceConnection === null) return msg.channel.sendMessage(wrap('No music being played.')); if (!isAdmin(msg.member)) return msg.channel.sendMessage(wrap('You are not authorized to use this.')); // Get the dispatcher const dispatcher = voiceConnection.player.dispatcher; if (suffix > 200 || suffix < 0) return msg.channel.sendMessage(wrap('Volume out of range!')).then((response) => { response.delete(5000); }); msg.channel.sendMessage(wrap("Volume set to " + suffix)); dispatcher.setVolume((suffix/100)); } /** * Executes the next song in the queue. * * @param {Message} msg - Original message. * @param {object} queue - The song queue for this server. * @returns {<promise>} - The voice channel. */ function executeQueue(msg, queue) { // If the queue is empty, finish. if (queue.length === 0) { msg.channel.sendMessage(wrap('Playback finished.')); // Leave the voice channel. const voiceConnection = client.voiceConnections.find(val => val.channel.guild.id == msg.guild.id); if (voiceConnection !== null) return voiceConnection.disconnect(); } new Promise((resolve, reject) => { // Join the voice channel if not already in one. const voiceConnection = client.voiceConnections.find(val => val.channel.guild.id == msg.guild.id); if (voiceConnection === null) { // Check if the user is in a voice channel. if (msg.member.voiceChannel) { msg.member.voiceChannel.join().then(connection => { resolve(connection); }).catch((error) => { console.log(error); }); } else { // Otherwise, clear the queue and do nothing. queue.splice(0, queue.length); reject(); } } else { resolve(voiceConnection); } }).then(connection => { // Get the first item in the queue. const video = queue[0]; console.log(video.webpage_url); // Play the video. msg.channel.sendMessage(wrap('Now Playing: ' + video.title)).then(() => { let dispatcher = connection.playStream(ytdl(video.webpage_url, {filter: 'audioonly'}), {seek: 0, volume: (DEFAULT_VOLUME/100)}); connection.on('error', (error) => { // Skip to the next song. console.log(error); queue.shift(); executeQueue(msg, queue); }); dispatcher.on('error', (error) => { // Skip to the next song. console.log(error); queue.shift(); executeQueue(msg, queue); }); dispatcher.on('end', () => { // Wait a second. setTimeout(() => { if (queue.length > 0) { // Remove the song from the queue. queue.shift(); // Play the next song in the queue. executeQueue(msg, queue); } }, 1000); }); }).catch((error) => { console.log(error); }); }).catch((error) => { console.log(error); }); } } /** * Wrap text in a code block and escape grave characters. * * @param {string} text - The input text. * @returns {string} - The wrapped text. */ function wrap(text) { return '```\n' + text.replace(/`/g, '`' + String.fromCharCode(8203)) + '\n```'; }