discord-player-play-dl/src/Player.js

430 lines
15 KiB
JavaScript
Raw Normal View History

2020-01-12 00:01:18 +05:00
const ytdl = require('ytdl-core');
const SimpleYouTubeAPI = require('simple-youtube-api');
const Discord = require('discord.js');
2020-01-12 00:01:18 +05:00
const Queue = require('./Queue');
const Track = require('./Track');
2020-01-12 00:01:18 +05:00
const Util = require('./Util');
/**
* @typedef PlayerOptions
* @property {boolean} [leaveOnEnd=true] Whether the bot should leave the current voice channel when the queue ends.
* @property {boolean} [leaveOnStop=true] Whether the bot should leave the current voice channel when the stop() function is used.
* @property {boolean} [leaveOnEmpty=true] Whether the bot should leave the voice channel if there is no more member in it.
*/
/**
* Default options for the player
* @ignore
* @type {PlayerOptions}
*/
const defaultPlayerOptions = {
2020-01-12 00:01:18 +05:00
leaveOnEnd: true,
leaveOnStop: true,
leaveOnEmpty: true
2020-01-12 00:01:18 +05:00
};
class Player {
/**
* @param {Discord.Client} client Discord.js client
* @param {string} youtubeToken Youtube Data v3 API Key
* @param {PlayerOptions} options Player options
2020-01-12 00:01:18 +05:00
*/
constructor(client, youtubeToken, options = {}){
if(!client) throw new SyntaxError('Invalid Discord client');
if(!youtubeToken) throw new SyntaxError('Invalid Token: Token must be a String');
2020-01-12 00:01:18 +05:00
/**
* Discord.js client instance
* @type {Discord.Client}
2020-01-12 00:01:18 +05:00
*/
this.client = client;
/**
* YouTube API Key
2020-01-12 00:01:18 +05:00
* @type {string}
*/
this.youtubeToken = youtubeToken;
/**
* Simple YouTube API client instance
* @type {SimpleYouTubeAPI.YouTube}
2020-01-12 00:01:18 +05:00
*/
this.youtube = new SimpleYouTubeAPI.YouTube(this.youtubeToken);
2020-01-12 00:01:18 +05:00
/**
* Player queues
2020-01-12 00:01:18 +05:00
* @type {Queue[]}
*/
this.queues = [];
/**
* Player options
2020-01-12 00:01:18 +05:00
* @type {PlayerOptions}
*/
this.options = defaultPlayerOptions;
for(const prop in options){
this.options[prop] = options[prop];
}
/**
* Utilities methods for the player
* @type {Util}
*/
this.util = new Util(this.youtube)
// Listener to check if the channel is empty
client.on('voiceStateUpdate', (oldState, newState) => this._handleVoiceStateUpdate.call(this, oldState, newState));
2020-01-12 00:01:18 +05:00
}
/**
* Whether a guild is currently playing something
* @param {Discord.Snowflake} guildID The guild ID to check
* @returns {boolean} Whether the guild is currently playing tracks
2020-01-12 00:01:18 +05:00
*/
isPlaying(guildID) {
return this.queues.some((g) => g.guildID === guildID);
}
/**
* Play a track in a voice channel
* @param {Discord.VoiceChannel} voiceChannel The voice channel in which the track will be played
* @param {Track|string} track The name of the track to play
* @param {Discord.User?} user The user who requested the track
* @returns {Promise<Track>} The played track
2020-01-12 00:01:18 +05:00
*/
play(voiceChannel, track, user) {
2020-01-12 00:01:18 +05:00
this.queues = this.queues.filter((g) => g.guildID !== voiceChannel.id);
return new Promise(async (resolve, reject) => {
if(!voiceChannel || typeof voiceChannel !== "object"){
return reject(`voiceChannel must be type of VoiceChannel. value=${voiceChannel}`);
}
const connection = voiceChannel.client.voice.connections.find((c) => c.channel.id === voiceChannel.id) || await voiceChannel.join();
if(typeof track !== "object"){
const results = await this.util.search(track, user);
track = results[0];
}
// Create a new guild with data
2020-01-12 00:01:18 +05:00
let queue = new Queue(voiceChannel.guild.id);
queue.voiceConnection = connection;
// Add the track to the queue
track.requestedBy = user;
queue.tracks.push(track);
2020-01-12 00:01:18 +05:00
// Add the queue to the list
this.queues.push(queue);
// Play the track
this._playTrack(queue.guildID, true);
// Resolve the track
resolve(track);
2020-01-12 00:01:18 +05:00
});
}
/**
* Pause the current track
* @param {Discord.Snowflake} guildID The ID of the guild where the current track should be paused
* @returns {Promise<Track>} The paused track
2020-01-12 00:01:18 +05:00
*/
pause (guildID) {
2020-01-12 00:01:18 +05:00
return new Promise(async(resolve, reject) => {
// Gets guild queue
let queue = this.queues.find((g) => g.guildID === guildID);
2020-02-21 15:32:16 +05:00
if(!queue) return reject('Not playing');
2020-01-12 00:01:18 +05:00
// Pauses the dispatcher
queue.voiceConnection.dispatcher.pause();
2020-01-12 00:01:18 +05:00
queue.playing = false;
// Resolves the guild queue
resolve(queue.tracks[0]);
2020-01-12 00:01:18 +05:00
});
}
/**
* Resume the current track
* @param {Discord.Snowflake} guildID The ID of the guild where the current track should be resumed
* @returns {Promise<Track>} The resumed track
2020-01-12 00:01:18 +05:00
*/
resume (guildID) {
2020-01-12 00:01:18 +05:00
return new Promise(async(resolve, reject) => {
// Get guild queue
2020-01-12 00:01:18 +05:00
let queue = this.queues.find((g) => g.guildID === guildID);
2020-02-21 15:32:16 +05:00
if(!queue) return reject('Not playing');
// Pause the dispatcher
queue.voiceConnection.dispatcher.resume();
2020-01-12 00:01:18 +05:00
queue.playing = true;
// Resolve the guild queue
resolve(queue.tracks[0]);
2020-01-12 00:01:18 +05:00
});
}
/**
* Stops playing music.
* @param {Discord.Snowflake} guildID The ID of the guild where the music should be stopped
2020-01-12 00:01:18 +05:00
* @returns {Promise<void>}
*/
stop(guildID){
return new Promise(async(resolve, reject) => {
// Get guild queue
2020-01-12 00:01:18 +05:00
let queue = this.queues.find((g) => g.guildID === guildID);
2020-02-21 15:32:16 +05:00
if(!queue) return reject('Not playing');
// Stop the dispatcher
2020-01-12 00:01:18 +05:00
queue.stopped = true;
queue.tracks = [];
queue.voiceConnection.dispatcher.end();
// Resolve
2020-01-12 00:01:18 +05:00
resolve();
});
}
/**
* Update the volume
* @param {Discord.Snowflake} guildID The ID of the guild where the music should be modified
* @param {number} percent The new volume (0-100)
2020-01-12 00:01:18 +05:00
* @returns {Promise<void>}
*/
setVolume(guildID, percent) {
return new Promise(async(resolve, reject) => {
// Gets guild queue
let queue = this.queues.find((g) => g.guildID === guildID);
2020-02-21 15:32:16 +05:00
if(!queue) return reject('Not playing');
2020-01-12 00:01:18 +05:00
// Updates volume
queue.voiceConnection.dispatcher.setVolumeLogarithmic(percent / 200);
queue.volume = percent;
2020-01-12 00:01:18 +05:00
// Resolves guild queue
resolve();
2020-01-12 00:01:18 +05:00
});
}
/**
* Get a guild queue
* @param {Discord.Snowflake} guildID
2020-01-18 22:29:53 +05:00
* @returns {?Queue}
2020-01-12 00:01:18 +05:00
*/
getQueue(guildID) {
2020-01-18 22:29:53 +05:00
// Gets guild queue
let queue = this.queues.find((g) => g.guildID === guildID);
return queue;
2020-01-12 00:01:18 +05:00
}
/**
* Add a track to the guild queue
* @param {Discord.Snowflake} guildID The ID of the guild where the track should be added
* @param {string} trackName The name of the track to add to the queue
* @param {Discord.User?} requestedBy The user who requested the track
* @returns {Promise<Track>} The added track
2020-01-12 00:01:18 +05:00
*/
addToQueue(guildID, trackName, requestedBy){
2020-01-12 00:01:18 +05:00
return new Promise(async(resolve, reject) => {
// Get guild queue
2020-01-12 00:01:18 +05:00
let queue = this.queues.find((g) => g.guildID === guildID);
2020-02-21 15:32:16 +05:00
if(!queue) return reject('Not playing');
// Search the track
let track = await this.util.search(trackName, requestedBy).catch(() => {});
if(!track[0]) return reject('Track not found');
// Update queue
queue.tracks.push(track[0]);
// Resolve the track
resolve(track[0]);
2020-01-12 00:01:18 +05:00
});
}
2020-01-18 17:04:05 +05:00
/**
* Set the queue for a guild.
* @param {Discord.Snowflake} guildID The ID of the guild where the queue should be set
* @param {Track[]} tracks The tracks list
* @returns {Promise<Queue>} The new queue
2020-01-18 17:04:05 +05:00
*/
setQueue(guildID, tracks){
2020-01-18 17:04:05 +05:00
return new Promise(async(resolve, reject) => {
// Get guild queue
2020-01-18 17:04:05 +05:00
let queue = this.queues.find((g) => g.guildID === guildID);
2020-02-21 15:32:16 +05:00
if(!queue) return reject('Not playing');
// Update queue
queue.tracks = tracks;
// Resolve the queue
2020-01-18 17:04:05 +05:00
resolve(queue);
});
}
2020-01-12 00:01:18 +05:00
/**
* Clear the guild queue, but not the current track
* @param {Discord.Snowflake} guildID The ID of the guild where the queue should be cleared
* @returns {Promise<Queue>} The updated queue
2020-01-12 00:01:18 +05:00
*/
clearQueue(guildID){
return new Promise(async(resolve, reject) => {
// Get guild queue
2020-01-12 00:01:18 +05:00
let queue = this.queues.find((g) => g.guildID === guildID);
2020-02-21 15:32:16 +05:00
if(!queue) return reject('Not playing');
// Clear queue
let currentlyPlaying = queue.tracks.shift();
queue.tracks = [ currentlyPlaying ];
// Resolve guild queue
2020-01-12 00:01:18 +05:00
resolve(queue);
});
}
/**
* Skip a track
* @param {Discord.Snowflake} guildID The ID of the guild where the track should be skipped
* @returns {Promise<Track>}
2020-01-12 00:01:18 +05:00
*/
skip(guildID){
return new Promise(async(resolve, reject) => {
// Get guild queue
2020-01-12 00:01:18 +05:00
let queue = this.queues.find((g) => g.guildID === guildID);
2020-02-21 15:32:16 +05:00
if(!queue) return reject('Not playing');
let currentTrack = queue.tracks[0];
// End the dispatcher
queue.voiceConnection.dispatcher.end();
queue.lastSkipped = true;
// Resolve the current track
resolve(currentTrack);
2020-01-12 00:01:18 +05:00
});
}
2020-01-18 15:15:10 +05:00
/**
* Get the currently playing track
* @param {Discord.Snowflake} guildID
* @returns {Promise<Track>} The track which is currently played
2020-01-18 15:15:10 +05:00
*/
nowPlaying(guildID){
return new Promise(async(resolve, reject) => {
// Get guild queue
2020-01-18 15:15:10 +05:00
let queue = this.queues.find((g) => g.guildID === guildID);
2020-02-21 15:32:16 +05:00
if(!queue) return reject('Not playing');
let currentTrack = queue.tracks[0];
// Resolve the current track
resolve(currentTrack);
2020-01-18 15:15:10 +05:00
});
}
2020-01-18 15:03:22 +05:00
/**
* Enable or disable the repeat mode
* @param {Discord.Snowflake} guildID
2020-01-18 15:03:22 +05:00
* @param {Boolean} enabled Whether the repeat mode should be enabled
* @returns {Promise<Void>}
*/
2020-02-02 15:20:46 +05:00
setRepeatMode(guildID, enabled) {
2020-01-18 15:03:22 +05:00
return new Promise(async(resolve, reject) => {
// Get guild queue
2020-01-18 15:03:22 +05:00
let queue = this.queues.find((g) => g.guildID === guildID);
2020-02-21 15:32:16 +05:00
if(!queue) return reject('Not playing');
2020-01-18 15:03:22 +05:00
// Enable/Disable repeat mode
queue.repeatMode = enabled;
// Resolve
resolve();
});
}
2020-04-24 20:14:34 +05:00
/**
* Shuffle the guild queue (except the first track)
* @param {Discord.Snowflake} guildID The ID of the guild where the queue should be shuffled
* @returns {Promise<Queue>} The updated queue
2020-04-24 20:14:34 +05:00
*/
shuffle(guildID){
return new Promise(async(resolve, reject) => {
// Get guild queue
2020-04-24 20:14:34 +05:00
let queue = this.queues.find((g) => g.guildID === guildID);
if(!queue) return reject('Not playing');
// Shuffle the queue (except the first track)
let currentTrack = queue.tracks.shift();
queue.tracks = queue.tracks.sort(() => Math.random() - 0.5);
queue.tracks.unshift(currentTrack);
2020-04-24 20:14:34 +05:00
// Resolve
resolve(queue);
2020-04-24 20:14:34 +05:00
});
}
2020-04-24 20:15:06 +05:00
2020-04-24 20:20:48 +05:00
/**
* Remove a track from the queue
* @param {Discord.Snowflake} guildID The ID of the guild where the track should be removed
* @param {number|Track} track The index of the track to remove or the track to remove object
* @returns {Promise<Track|null>}
2020-04-24 20:20:48 +05:00
*/
remove(guildID, track){
2020-04-24 20:20:48 +05:00
return new Promise(async(resolve, reject) => {
// Gets guild queue
let queue = this.queues.find((g) => g.guildID === guildID);
if(!queue) return reject('Not playing');
// Remove the track from the queue
let trackFound = null;
if(typeof track === "number"){
trackFound = queue.tracks[track];
if(trackFound){
queue.tracks = queue.tracks.filter((s) => s !== trackFound);
2020-04-24 20:20:48 +05:00
}
} else {
trackFound = queue.tracks.find((s) => s === track);
if(trackFound){
queue.tracks = queue.tracks.filter((s) => s !== trackFound);
2020-04-24 20:20:48 +05:00
}
}
// Resolve
resolve(trackFound);
2020-04-24 20:20:48 +05:00
});
}
2020-01-12 00:01:18 +05:00
/**
* Handle the voice state update event
* @ignore
* @private
* @param {Discord.VoiceState} oldState
* @param {Discord.VoiceState} newState
*/
_handleVoiceStateUpdate(oldState, newState) {
if(!this.options.leaveOnEmpty) return;
// If the member leaves a voice channel
if(!oldState.channelID || newState.channelID) return;
// Search for a queue for this channel
let queue = this.queues.find((g) => g.voiceConnection.channel.id === oldState.channelID);
if(queue){
// If the channel is not empty
if(queue.voiceConnection.channel.members.size > 1) return;
// Disconnect from the voice channel
queue.voiceConnection.channel.leave();
// Delete the queue
this.queues = this.queues.filter((g) => g.guildID !== queue.guildID);
// Emit end event
queue.emit('channelEmpty');
}
}
/**
* Start playing a track in a guild
2020-01-12 00:01:18 +05:00
* @ignore
* @private
* @param {Discord.Snowflake} guildID
2020-01-12 00:01:18 +05:00
* @param {Boolean} firstPlay Whether the function was called from the play() one
*/
async _playTrack(guildID, firstPlay) {
2020-01-12 00:01:18 +05:00
// Gets guild queue
let queue = this.queues.find((g) => g.guildID === guildID);
// If there isn't any music in the queue
if(queue.tracks.length < 2 && !firstPlay && !queue.repeatMode){
2020-01-12 00:01:18 +05:00
// Leaves the voice channel
if(this.options.leaveOnEnd && !queue.stopped) queue.voiceConnection.channel.leave();
2020-01-12 00:01:18 +05:00
// Remoces the guild from the guilds list
this.queues = this.queues.filter((g) => g.guildID !== guildID);
// Emits stop event
2020-01-12 22:56:11 +05:00
if(queue.stopped){
if(this.options.leaveOnStop) queue.voiceConnection.channel.leave();
2020-01-12 22:56:11 +05:00
return queue.emit('stop');
}
2020-01-12 00:01:18 +05:00
// Emits end event
return queue.emit('end');
}
// Emit trackChanged event
if(!firstPlay) queue.emit('trackChanged', (!queue.repeatMode ? queue.tracks.shift() : queue.tracks[0]), queue.tracks[0], queue.lastSkipped, queue.repeatMode);
queue.lastSkipped = false;
let track = queue.tracks[0];
// Download the track
queue.voiceConnection.play(ytdl(track.url, {
filter: "audioonly"
}));
2020-01-12 00:01:18 +05:00
// Set volume
queue.voiceConnection.dispatcher.setVolumeLogarithmic(queue.volume / 200);
// When the track ends
queue.voiceConnection.dispatcher.on('finish', () => {
// Play the next track
return this._playTrack(guildID, false);
2020-01-12 00:01:18 +05:00
});
}
};
module.exports = Player;