♻️ Rewrite all JSDoc documentation
This commit is contained in:
parent
5669581708
commit
e1e51ddf03
5 changed files with 356 additions and 309 deletions
392
src/Player.js
392
src/Player.js
|
@ -1,22 +1,24 @@
|
||||||
const ytdl = require('ytdl-core');
|
const ytdl = require('ytdl-core');
|
||||||
const SYA = require('simple-youtube-api');
|
const SimpleYouTubeAPI = require('simple-youtube-api');
|
||||||
const mergeOptions = require('merge-options');
|
const Discord = require('discord.js');
|
||||||
|
|
||||||
const { VoiceChannel, version } = require("discord.js");
|
|
||||||
if(version.split('.')[0] !== '12') throw new Error("Only the master branch of discord.js library is supported for now. Install it using 'npm install discordjs/discord.js'.");
|
|
||||||
const Queue = require('./Queue');
|
const Queue = require('./Queue');
|
||||||
|
const Track = require('./Track');
|
||||||
const Util = require('./Util');
|
const Util = require('./Util');
|
||||||
const Song = require('./Song');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Player options.
|
* @typedef PlayerOptions
|
||||||
* @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} leaveOnEnd Whether the bot should leave the current voice channel when the queue ends.
|
* @property {boolean} [leaveOnEmpty=true] Whether the bot should leave the voice channel if there is no more member in it.
|
||||||
* @property {Boolean} leaveOnStop Whether the bot should leave the current voice channel when the stop() function is used.
|
|
||||||
* @property {Boolean} leaveOnEmpty Whether the bot should leave the voice channel if there is no more member in it.
|
|
||||||
*/
|
*/
|
||||||
const PlayerOptions = {
|
|
||||||
|
/**
|
||||||
|
* Default options for the player
|
||||||
|
* @ignore
|
||||||
|
* @type {PlayerOptions}
|
||||||
|
*/
|
||||||
|
const defaultPlayerOptions = {
|
||||||
leaveOnEnd: true,
|
leaveOnEnd: true,
|
||||||
leaveOnStop: true,
|
leaveOnStop: true,
|
||||||
leaveOnEmpty: true
|
leaveOnEmpty: true
|
||||||
|
@ -25,103 +27,98 @@ const PlayerOptions = {
|
||||||
class Player {
|
class Player {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Client} client Your Discord Client instance.
|
* @param {Discord.Client} client Discord.js client
|
||||||
* @param {string} youtubeToken Your Youtube Data v3 API key.
|
* @param {string} youtubeToken Youtube Data v3 API Key
|
||||||
* @param {PlayerOptions} options The PlayerOptions object.
|
* @param {PlayerOptions} options Player options
|
||||||
*/
|
*/
|
||||||
constructor(client, youtubeToken, options = {}){
|
constructor(client, youtubeToken, options = {}){
|
||||||
if(!client) throw new SyntaxError('Invalid Discord client');
|
if(!client) throw new SyntaxError('Invalid Discord client');
|
||||||
if(!youtubeToken) throw new SyntaxError('Invalid Token: Token must be a String');
|
if(!youtubeToken) throw new SyntaxError('Invalid Token: Token must be a String');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Your Discord Client instance.
|
* Discord.js client instance
|
||||||
* @type {Client}
|
* @type {Discord.Client}
|
||||||
*/
|
*/
|
||||||
this.client = client;
|
this.client = client;
|
||||||
/**
|
/**
|
||||||
* Your Youtube Data v3 API key.
|
* YouTube API Key
|
||||||
* @type {string}
|
* @type {string}
|
||||||
*/
|
*/
|
||||||
this.youtubeToken = youtubeToken;
|
this.youtubeToken = youtubeToken;
|
||||||
/**
|
/**
|
||||||
* The Simple Youtube API Client.
|
* Simple YouTube API client instance
|
||||||
* @type {Youtube}
|
* @type {SimpleYouTubeAPI.YouTube}
|
||||||
*/
|
*/
|
||||||
this.SYA = new SYA(this.youtubeToken);
|
this.youtube = new SimpleYouTubeAPI.YouTube(this.youtubeToken);
|
||||||
/**
|
/**
|
||||||
* The guilds data.
|
* Player queues
|
||||||
* @type {Queue[]}
|
* @type {Queue[]}
|
||||||
*/
|
*/
|
||||||
this.queues = [];
|
this.queues = [];
|
||||||
/**
|
/**
|
||||||
* Player options.
|
* Player options
|
||||||
* @type {PlayerOptions}
|
* @type {PlayerOptions}
|
||||||
*/
|
*/
|
||||||
this.options = mergeOptions(PlayerOptions, options);
|
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
|
// Listener to check if the channel is empty
|
||||||
client.on('voiceStateUpdate', (oldState, newState) => {
|
client.on('voiceStateUpdate', (oldState, newState) => this._handleVoiceStateUpdate.call(this, 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.connection.channel.id === oldState.channelID);
|
|
||||||
if(queue){
|
|
||||||
// If the channel is not empty
|
|
||||||
if(queue.connection.channel.members.size > 1) return;
|
|
||||||
// Disconnect from the voice channel
|
|
||||||
queue.connection.channel.leave();
|
|
||||||
// Delete the queue
|
|
||||||
this.queues = this.queues.filter((g) => g.guildID !== queue.guildID);
|
|
||||||
// Emit end event
|
|
||||||
queue.emit('channelEmpty');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether a guild is currently playing songs
|
* Whether a guild is currently playing something
|
||||||
* @param {string} guildID The guild ID to check
|
* @param {Discord.Snowflake} guildID The guild ID to check
|
||||||
* @returns {Boolean} Whether the guild is currently playing songs
|
* @returns {boolean} Whether the guild is currently playing tracks
|
||||||
*/
|
*/
|
||||||
isPlaying(guildID) {
|
isPlaying(guildID) {
|
||||||
return this.queues.some((g) => g.guildID === guildID);
|
return this.queues.some((g) => g.guildID === guildID);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plays a song in a voice channel.
|
* Play a track in a voice channel
|
||||||
* @param {voiceChannel} voiceChannel The voice channel in which the song will be played.
|
* @param {Discord.VoiceChannel} voiceChannel The voice channel in which the track will be played
|
||||||
* @param {string} songName The name of the song to play.
|
* @param {Track|string} track The name of the track to play
|
||||||
* @param {User} requestedBy The user who requested the song.
|
* @param {Discord.User?} user The user who requested the track
|
||||||
* @returns {Promise<Song>}
|
* @returns {Promise<Track>} The played track
|
||||||
*/
|
*/
|
||||||
play(voiceChannel, songName, requestedBy) {
|
play(voiceChannel, track, user) {
|
||||||
this.queues = this.queues.filter((g) => g.guildID !== voiceChannel.id);
|
this.queues = this.queues.filter((g) => g.guildID !== voiceChannel.id);
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
if(!voiceChannel || typeof voiceChannel !== "object") return reject("voiceChannel must be type of VoiceChannel. value="+voiceChannel);
|
if(!voiceChannel || typeof voiceChannel !== "object"){
|
||||||
if(typeof songName !== "string") return reject("songName must be type of string. value="+songName);
|
return reject(`voiceChannel must be type of VoiceChannel. value=${voiceChannel}`);
|
||||||
// Searches the song
|
}
|
||||||
let video = await Util.getFirstYoutubeResult(songName, this.SYA);
|
const connection = voiceChannel.client.voice.connections.find((c) => c.channel.id === voiceChannel.id) || await voiceChannel.join();
|
||||||
if(!video) return reject('Song not found');
|
if(typeof track !== "object"){
|
||||||
// Joins the voice channel
|
const results = await this.util.search(track, user);
|
||||||
let connection = await voiceChannel.join();
|
track = results[0];
|
||||||
// Creates a new guild with data
|
}
|
||||||
|
// Create a new guild with data
|
||||||
let queue = new Queue(voiceChannel.guild.id);
|
let queue = new Queue(voiceChannel.guild.id);
|
||||||
queue.connection = connection;
|
queue.voiceConnection = connection;
|
||||||
let song = new Song(video, queue, requestedBy);
|
// Add the track to the queue
|
||||||
queue.songs.push(song);
|
track.requestedBy = user;
|
||||||
|
queue.tracks.push(track);
|
||||||
// Add the queue to the list
|
// Add the queue to the list
|
||||||
this.queues.push(queue);
|
this.queues.push(queue);
|
||||||
// Plays the song
|
// Play the track
|
||||||
this._playSong(queue.guildID, true);
|
this._playTrack(queue.guildID, true);
|
||||||
// Resolves the song.
|
// Resolve the track
|
||||||
resolve(song);
|
resolve(track);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pauses the current song.
|
* Pause the current track
|
||||||
* @param {string} guildID
|
* @param {Discord.Snowflake} guildID The ID of the guild where the current track should be paused
|
||||||
* @returns {Promise<Song>}
|
* @returns {Promise<Track>} The paused track
|
||||||
*/
|
*/
|
||||||
pause (guildID) {
|
pause (guildID) {
|
||||||
return new Promise(async(resolve, reject) => {
|
return new Promise(async(resolve, reject) => {
|
||||||
|
@ -129,54 +126,54 @@ class Player {
|
||||||
let queue = this.queues.find((g) => g.guildID === guildID);
|
let queue = this.queues.find((g) => g.guildID === guildID);
|
||||||
if(!queue) return reject('Not playing');
|
if(!queue) return reject('Not playing');
|
||||||
// Pauses the dispatcher
|
// Pauses the dispatcher
|
||||||
queue.dispatcher.pause();
|
queue.voiceConnection.dispatcher.pause();
|
||||||
queue.playing = false;
|
queue.playing = false;
|
||||||
// Resolves the guild queue
|
// Resolves the guild queue
|
||||||
resolve(queue.songs[0]);
|
resolve(queue.tracks[0]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resumes the current song.
|
* Resume the current track
|
||||||
* @param {string} guildID
|
* @param {Discord.Snowflake} guildID The ID of the guild where the current track should be resumed
|
||||||
* @returns {Promise<Song>}
|
* @returns {Promise<Track>} The resumed track
|
||||||
*/
|
*/
|
||||||
resume (guildID) {
|
resume (guildID) {
|
||||||
return new Promise(async(resolve, reject) => {
|
return new Promise(async(resolve, reject) => {
|
||||||
// Gets guild queue
|
// Get guild queue
|
||||||
let queue = this.queues.find((g) => g.guildID === guildID);
|
let queue = this.queues.find((g) => g.guildID === guildID);
|
||||||
if(!queue) return reject('Not playing');
|
if(!queue) return reject('Not playing');
|
||||||
// Pauses the dispatcher
|
// Pause the dispatcher
|
||||||
queue.dispatcher.resume();
|
queue.voiceConnection.dispatcher.resume();
|
||||||
queue.playing = true;
|
queue.playing = true;
|
||||||
// Resolves the guild queue
|
// Resolve the guild queue
|
||||||
resolve(queue.songs[0]);
|
resolve(queue.tracks[0]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stops playing music.
|
* Stops playing music.
|
||||||
* @param {string} guildID
|
* @param {Discord.Snowflake} guildID The ID of the guild where the music should be stopped
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
stop(guildID){
|
stop(guildID){
|
||||||
return new Promise(async(resolve, reject) => {
|
return new Promise(async(resolve, reject) => {
|
||||||
// Gets guild queue
|
// Get guild queue
|
||||||
let queue = this.queues.find((g) => g.guildID === guildID);
|
let queue = this.queues.find((g) => g.guildID === guildID);
|
||||||
if(!queue) return reject('Not playing');
|
if(!queue) return reject('Not playing');
|
||||||
// Stops the dispatcher
|
// Stop the dispatcher
|
||||||
queue.stopped = true;
|
queue.stopped = true;
|
||||||
queue.songs = [];
|
queue.tracks = [];
|
||||||
queue.dispatcher.end();
|
queue.voiceConnection.dispatcher.end();
|
||||||
// Resolves
|
// Resolve
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the volume.
|
* Update the volume
|
||||||
* @param {string} guildID
|
* @param {Discord.Snowflake} guildID The ID of the guild where the music should be modified
|
||||||
* @param {number} percent
|
* @param {number} percent The new volume (0-100)
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
setVolume(guildID, percent) {
|
setVolume(guildID, percent) {
|
||||||
|
@ -185,15 +182,16 @@ class Player {
|
||||||
let queue = this.queues.find((g) => g.guildID === guildID);
|
let queue = this.queues.find((g) => g.guildID === guildID);
|
||||||
if(!queue) return reject('Not playing');
|
if(!queue) return reject('Not playing');
|
||||||
// Updates volume
|
// Updates volume
|
||||||
queue.dispatcher.setVolumeLogarithmic(percent / 200);
|
queue.voiceConnection.dispatcher.setVolumeLogarithmic(percent / 200);
|
||||||
|
queue.volume = percent;
|
||||||
// Resolves guild queue
|
// Resolves guild queue
|
||||||
resolve(queue);
|
resolve();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the guild queue.
|
* Get a guild queue
|
||||||
* @param {string} guildID
|
* @param {Discord.Snowflake} guildID
|
||||||
* @returns {?Queue}
|
* @returns {?Queue}
|
||||||
*/
|
*/
|
||||||
getQueue(guildID) {
|
getQueue(guildID) {
|
||||||
|
@ -203,108 +201,107 @@ class Player {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a song to the guild queue.
|
* Add a track to the guild queue
|
||||||
* @param {string} guildID
|
* @param {Discord.Snowflake} guildID The ID of the guild where the track should be added
|
||||||
* @param {string} songName The name of the song to add to the queue.
|
* @param {string} trackName The name of the track to add to the queue
|
||||||
* @param {User} requestedBy The user who requested the song.
|
* @param {Discord.User?} requestedBy The user who requested the track
|
||||||
* @returns {Promise<Song>}
|
* @returns {Promise<Track>} The added track
|
||||||
*/
|
*/
|
||||||
addToQueue(guildID, songName, requestedBy){
|
addToQueue(guildID, trackName, requestedBy){
|
||||||
return new Promise(async(resolve, reject) => {
|
return new Promise(async(resolve, reject) => {
|
||||||
// Gets guild queue
|
// Get guild queue
|
||||||
let queue = this.queues.find((g) => g.guildID === guildID);
|
let queue = this.queues.find((g) => g.guildID === guildID);
|
||||||
if(!queue) return reject('Not playing');
|
if(!queue) return reject('Not playing');
|
||||||
// Searches the song
|
// Search the track
|
||||||
let video = await Util.getFirstYoutubeResult(songName, this.SYA).catch(() => {});
|
let track = await this.util.search(trackName, requestedBy).catch(() => {});
|
||||||
if(!video) return reject('Song not found');
|
if(!track[0]) return reject('Track not found');
|
||||||
let song = new Song(video, queue, requestedBy);
|
// Update queue
|
||||||
// Updates queue
|
queue.tracks.push(track[0]);
|
||||||
queue.songs.push(song);
|
// Resolve the track
|
||||||
// Resolves the song
|
resolve(track[0]);
|
||||||
resolve(song);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the queue for a guild.
|
* Set the queue for a guild.
|
||||||
* @param {string} guildID
|
* @param {Discord.Snowflake} guildID The ID of the guild where the queue should be set
|
||||||
* @param {Array<Song>} songs The songs list
|
* @param {Track[]} tracks The tracks list
|
||||||
* @returns {Promise<Queue>}
|
* @returns {Promise<Queue>} The new queue
|
||||||
*/
|
*/
|
||||||
setQueue(guildID, songs){
|
setQueue(guildID, tracks){
|
||||||
return new Promise(async(resolve, reject) => {
|
return new Promise(async(resolve, reject) => {
|
||||||
// Gets guild queue
|
// Get guild queue
|
||||||
let queue = this.queues.find((g) => g.guildID === guildID);
|
let queue = this.queues.find((g) => g.guildID === guildID);
|
||||||
if(!queue) return reject('Not playing');
|
if(!queue) return reject('Not playing');
|
||||||
// Updates queue
|
// Update queue
|
||||||
queue.songs = songs;
|
queue.tracks = tracks;
|
||||||
// Resolves the queue
|
// Resolve the queue
|
||||||
resolve(queue);
|
resolve(queue);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears the guild queue, but not the current song.
|
* Clear the guild queue, but not the current track
|
||||||
* @param {string} guildID
|
* @param {Discord.Snowflake} guildID The ID of the guild where the queue should be cleared
|
||||||
* @returns {Promise<Queue>}
|
* @returns {Promise<Queue>} The updated queue
|
||||||
*/
|
*/
|
||||||
clearQueue(guildID){
|
clearQueue(guildID){
|
||||||
return new Promise(async(resolve, reject) => {
|
return new Promise(async(resolve, reject) => {
|
||||||
// Gets guild queue
|
// Get guild queue
|
||||||
let queue = this.queues.find((g) => g.guildID === guildID);
|
let queue = this.queues.find((g) => g.guildID === guildID);
|
||||||
if(!queue) return reject('Not playing');
|
if(!queue) return reject('Not playing');
|
||||||
// Clears queue
|
// Clear queue
|
||||||
let currentlyPlaying = queue.songs.shift();
|
let currentlyPlaying = queue.tracks.shift();
|
||||||
queue.songs = [ currentlyPlaying ];
|
queue.tracks = [ currentlyPlaying ];
|
||||||
// Resolves guild queue
|
// Resolve guild queue
|
||||||
resolve(queue);
|
resolve(queue);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Skips a song.
|
* Skip a track
|
||||||
* @param {string} guildID
|
* @param {Discord.Snowflake} guildID The ID of the guild where the track should be skipped
|
||||||
* @returns {Promise<Song>}
|
* @returns {Promise<Track>}
|
||||||
*/
|
*/
|
||||||
skip(guildID){
|
skip(guildID){
|
||||||
return new Promise(async(resolve, reject) => {
|
return new Promise(async(resolve, reject) => {
|
||||||
// Gets guild queue
|
// Get guild queue
|
||||||
let queue = this.queues.find((g) => g.guildID === guildID);
|
let queue = this.queues.find((g) => g.guildID === guildID);
|
||||||
if(!queue) return reject('Not playing');
|
if(!queue) return reject('Not playing');
|
||||||
let currentSong = queue.songs[0];
|
let currentTrack = queue.tracks[0];
|
||||||
// Ends the dispatcher
|
// End the dispatcher
|
||||||
queue.dispatcher.end();
|
queue.voiceConnection.dispatcher.end();
|
||||||
queue.skipped = true;
|
queue.lastSkipped = true;
|
||||||
// Resolves the current song
|
// Resolve the current track
|
||||||
resolve(currentSong);
|
resolve(currentTrack);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the currently playing song.
|
* Get the currently playing track
|
||||||
* @param {string} guildID
|
* @param {Discord.Snowflake} guildID
|
||||||
* @returns {Promise<Song>}
|
* @returns {Promise<Track>} The track which is currently played
|
||||||
*/
|
*/
|
||||||
nowPlaying(guildID){
|
nowPlaying(guildID){
|
||||||
return new Promise(async(resolve, reject) => {
|
return new Promise(async(resolve, reject) => {
|
||||||
// Gets guild queue
|
// Get guild queue
|
||||||
let queue = this.queues.find((g) => g.guildID === guildID);
|
let queue = this.queues.find((g) => g.guildID === guildID);
|
||||||
if(!queue) return reject('Not playing');
|
if(!queue) return reject('Not playing');
|
||||||
let currentSong = queue.songs[0];
|
let currentTrack = queue.tracks[0];
|
||||||
// Resolves the current song
|
// Resolve the current track
|
||||||
resolve(currentSong);
|
resolve(currentTrack);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enable or disable the repeat mode
|
* Enable or disable the repeat mode
|
||||||
* @param {string} guildID
|
* @param {Discord.Snowflake} guildID
|
||||||
* @param {Boolean} enabled Whether the repeat mode should be enabled
|
* @param {Boolean} enabled Whether the repeat mode should be enabled
|
||||||
* @returns {Promise<Void>}
|
* @returns {Promise<Void>}
|
||||||
*/
|
*/
|
||||||
setRepeatMode(guildID, enabled) {
|
setRepeatMode(guildID, enabled) {
|
||||||
return new Promise(async(resolve, reject) => {
|
return new Promise(async(resolve, reject) => {
|
||||||
// Gets guild queue
|
// Get guild queue
|
||||||
let queue = this.queues.find((g) => g.guildID === guildID);
|
let queue = this.queues.find((g) => g.guildID === guildID);
|
||||||
if(!queue) return reject('Not playing');
|
if(!queue) return reject('Not playing');
|
||||||
// Enable/Disable repeat mode
|
// Enable/Disable repeat mode
|
||||||
|
@ -315,89 +312,116 @@ class Player {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shuffles the guild queue.
|
* Shuffle the guild queue (except the first track)
|
||||||
* @param {string} guildID
|
* @param {Discord.Snowflake} guildID The ID of the guild where the queue should be shuffled
|
||||||
* @returns {Promise<Void>}
|
* @returns {Promise<Queue>} The updated queue
|
||||||
*/
|
*/
|
||||||
shuffle(guildID){
|
shuffle(guildID){
|
||||||
return new Promise(async(resolve, reject) => {
|
return new Promise(async(resolve, reject) => {
|
||||||
// Gets guild queue
|
// Get guild queue
|
||||||
let queue = this.queues.find((g) => g.guildID === guildID);
|
let queue = this.queues.find((g) => g.guildID === guildID);
|
||||||
if(!queue) return reject('Not playing');
|
if(!queue) return reject('Not playing');
|
||||||
// Shuffle the queue (except the first song)
|
// Shuffle the queue (except the first track)
|
||||||
let currentSong = queue.songs.shift();
|
let currentTrack = queue.tracks.shift();
|
||||||
queue.songs = queue.songs.sort(() => Math.random() - 0.5);
|
queue.tracks = queue.tracks.sort(() => Math.random() - 0.5);
|
||||||
queue.songs.unshift(currentSong);
|
queue.tracks.unshift(currentTrack);
|
||||||
// Resolve
|
// Resolve
|
||||||
resolve();
|
resolve(queue);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes a song from the queue
|
* Remove a track from the queue
|
||||||
* @param {string} guildID
|
* @param {Discord.Snowflake} guildID The ID of the guild where the track should be removed
|
||||||
* @param {number|Song} song The index of the song to remove or the song to remove object.
|
* @param {number|Track} track The index of the track to remove or the track to remove object
|
||||||
* @returns {Promise<Song|null>}
|
* @returns {Promise<Track|null>}
|
||||||
*/
|
*/
|
||||||
remove(guildID, song){
|
remove(guildID, track){
|
||||||
return new Promise(async(resolve, reject) => {
|
return new Promise(async(resolve, reject) => {
|
||||||
// Gets guild queue
|
// Gets guild queue
|
||||||
let queue = this.queues.find((g) => g.guildID === guildID);
|
let queue = this.queues.find((g) => g.guildID === guildID);
|
||||||
if(!queue) return reject('Not playing');
|
if(!queue) return reject('Not playing');
|
||||||
// Remove the song from the queue
|
// Remove the track from the queue
|
||||||
let songFound = null;
|
let trackFound = null;
|
||||||
if(typeof song === "number"){
|
if(typeof track === "number"){
|
||||||
songFound = queue.songs[song];
|
trackFound = queue.tracks[track];
|
||||||
if(songFound){
|
if(trackFound){
|
||||||
queue.songs = queue.songs.filter((s) => s !== songFound);
|
queue.tracks = queue.tracks.filter((s) => s !== trackFound);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
songFound = queue.songs.find((s) => s === song);
|
trackFound = queue.tracks.find((s) => s === track);
|
||||||
if(songFound){
|
if(trackFound){
|
||||||
queue.songs = queue.songs.filter((s) => s !== songFound);
|
queue.tracks = queue.tracks.filter((s) => s !== trackFound);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Resolve
|
// Resolve
|
||||||
resolve(songFound);
|
resolve(trackFound);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start playing songs in a guild.
|
* Handle the voice state update event
|
||||||
* @ignore
|
* @ignore
|
||||||
* @param {string} guildID
|
* @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
|
||||||
|
* @ignore
|
||||||
|
* @private
|
||||||
|
* @param {Discord.Snowflake} guildID
|
||||||
* @param {Boolean} firstPlay Whether the function was called from the play() one
|
* @param {Boolean} firstPlay Whether the function was called from the play() one
|
||||||
*/
|
*/
|
||||||
async _playSong(guildID, firstPlay) {
|
async _playTrack(guildID, firstPlay) {
|
||||||
// Gets guild queue
|
// Gets guild queue
|
||||||
let queue = this.queues.find((g) => g.guildID === guildID);
|
let queue = this.queues.find((g) => g.guildID === guildID);
|
||||||
// If there isn't any music in the queue
|
// If there isn't any music in the queue
|
||||||
if(queue.songs.length < 2 && !firstPlay && !queue.repeatMode){
|
if(queue.tracks.length < 2 && !firstPlay && !queue.repeatMode){
|
||||||
// Leaves the voice channel
|
// Leaves the voice channel
|
||||||
if(this.options.leaveOnEnd && !queue.stopped) queue.connection.channel.leave();
|
if(this.options.leaveOnEnd && !queue.stopped) queue.voiceConnection.channel.leave();
|
||||||
// Remoces the guild from the guilds list
|
// Remoces the guild from the guilds list
|
||||||
this.queues = this.queues.filter((g) => g.guildID !== guildID);
|
this.queues = this.queues.filter((g) => g.guildID !== guildID);
|
||||||
// Emits stop event
|
// Emits stop event
|
||||||
if(queue.stopped){
|
if(queue.stopped){
|
||||||
if(this.options.leaveOnStop) queue.connection.channel.leave();
|
if(this.options.leaveOnStop) queue.voiceConnection.channel.leave();
|
||||||
return queue.emit('stop');
|
return queue.emit('stop');
|
||||||
}
|
}
|
||||||
// Emits end event
|
// Emits end event
|
||||||
return queue.emit('end');
|
return queue.emit('end');
|
||||||
}
|
}
|
||||||
// Emit songChanged event
|
// Emit trackChanged event
|
||||||
if(!firstPlay) queue.emit('songChanged', (!queue.repeatMode ? queue.songs.shift() : queue.songs[0]), queue.songs[0], queue.skipped, queue.repeatMode);
|
if(!firstPlay) queue.emit('trackChanged', (!queue.repeatMode ? queue.tracks.shift() : queue.tracks[0]), queue.tracks[0], queue.lastSkipped, queue.repeatMode);
|
||||||
queue.skipped = false;
|
queue.lastSkipped = false;
|
||||||
let song = queue.songs[0];
|
let track = queue.tracks[0];
|
||||||
// Download the song
|
// Download the track
|
||||||
let dispatcher = queue.connection.play(ytdl(song.url, { filter: "audioonly" }));
|
queue.voiceConnection.play(ytdl(track.url, {
|
||||||
queue.dispatcher = dispatcher;
|
filter: "audioonly"
|
||||||
|
}));
|
||||||
// Set volume
|
// Set volume
|
||||||
dispatcher.setVolumeLogarithmic(queue.volume / 200);
|
queue.voiceConnection.dispatcher.setVolumeLogarithmic(queue.volume / 200);
|
||||||
// When the song ends
|
// When the track ends
|
||||||
dispatcher.on('finish', () => {
|
queue.voiceConnection.dispatcher.on('finish', () => {
|
||||||
// Play the next song
|
// Play the next track
|
||||||
return this._playSong(guildID, false);
|
return this._playTrack(guildID, false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
56
src/Queue.js
56
src/Queue.js
|
@ -1,4 +1,6 @@
|
||||||
const { EventEmitter } = require('events');
|
const Discord = require('discord.js')
|
||||||
|
const { EventEmitter } = require('events')
|
||||||
|
const Track = require('./Track')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a guild queue.
|
* Represents a guild queue.
|
||||||
|
@ -6,60 +8,56 @@ const { EventEmitter } = require('events');
|
||||||
class Queue extends EventEmitter {
|
class Queue extends EventEmitter {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a guild queue.
|
* @param {Discord.Snowflake} guildID ID of the guild this queue is for.
|
||||||
* @param {string} guildID
|
|
||||||
*/
|
*/
|
||||||
constructor(guildID){
|
constructor(guildID){
|
||||||
super();
|
super();
|
||||||
/**
|
/**
|
||||||
* The guild ID.
|
* ID of the guild this queue is for.
|
||||||
* @type {Snowflake}
|
* @type {Discord.Snowflake}
|
||||||
*/
|
*/
|
||||||
this.guildID = guildID;
|
this.guildID = guildID;
|
||||||
/**
|
/**
|
||||||
* The stream dispatcher.
|
* The voice connection of this queue.
|
||||||
* @type {StreamDispatcher}
|
* @type {Discord.VoiceConnection}
|
||||||
*/
|
*/
|
||||||
this.dispatcher = null;
|
this.voiceConnection = null;
|
||||||
/**
|
/**
|
||||||
* The voice connection.
|
* The tracks of this queue. The first one is currenlty playing and the others are going to be played.
|
||||||
* @type {VoiceConnection}
|
* @type {Track[]}
|
||||||
*/
|
*/
|
||||||
this.connection = null;
|
this.tracks = [];
|
||||||
/**
|
|
||||||
* Songs. The first one is currently playing and the rest is going to be played.
|
|
||||||
* @type {Song[]}
|
|
||||||
*/
|
|
||||||
this.songs = [];
|
|
||||||
/**
|
/**
|
||||||
* Whether the stream is currently stopped.
|
* Whether the stream is currently stopped.
|
||||||
* @type {Boolean}
|
* @type {boolean}
|
||||||
*/
|
*/
|
||||||
this.stopped = false;
|
this.stopped = false;
|
||||||
/**
|
/**
|
||||||
* Whether the last song was skipped.
|
* Whether the last track was skipped.
|
||||||
* @type {Boolean}
|
* @type {boolean}
|
||||||
*/
|
*/
|
||||||
this.skipped = false;
|
this.lastSkipped = false;
|
||||||
/**
|
/**
|
||||||
* The stream volume.
|
* The stream volume of this queue. (0-100)
|
||||||
* @type {Number}
|
* @type {number}
|
||||||
*/
|
*/
|
||||||
this.volume = 100;
|
this.volume = 100;
|
||||||
/**
|
/**
|
||||||
* Whether the stream is currently playing.
|
* Whether the stream is currently playing.
|
||||||
* @type {Boolean}
|
* @type {boolean}
|
||||||
*/
|
*/
|
||||||
this.playing = true;
|
this.playing = true;
|
||||||
/**
|
/**
|
||||||
* Whether the repeat mode is enabled.
|
* Whether the repeat mode is enabled.
|
||||||
* @type {Boolean}
|
* @type {boolean}
|
||||||
*/
|
*/
|
||||||
this.repeatMode = false;
|
this.repeatMode = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
module.exports = Queue;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emitted when the queue is empty.
|
* Emitted when the queue is empty.
|
||||||
* @event Queue#end
|
* @event Queue#end
|
||||||
|
@ -71,11 +69,9 @@ class Queue extends EventEmitter {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emitted when the song changes.
|
* Emitted when the track changes.
|
||||||
* @event Queue#songChanged
|
* @event Queue#trackChanged
|
||||||
* @param {Song} oldSong The old song (playing before)
|
* @param {Track} oldTrack The old track (playing before)
|
||||||
* @param {Song} newSong The new song (currently playing)
|
* @param {Track} newTrack The new track (currently playing)
|
||||||
* @param {Boolean} skipped Whether the change is due to the skip() function
|
* @param {Boolean} skipped Whether the change is due to the skip() function
|
||||||
*/
|
*/
|
||||||
|
|
||||||
module.exports = Queue;
|
|
58
src/Song.js
58
src/Song.js
|
@ -1,58 +0,0 @@
|
||||||
/**
|
|
||||||
* Represents a song.
|
|
||||||
*/
|
|
||||||
class Song {
|
|
||||||
/**
|
|
||||||
* @param {Video} video The Youtube video
|
|
||||||
* @param {Queue} queue The queue in which the song is
|
|
||||||
*/
|
|
||||||
constructor(video, queue, requestedBy) {
|
|
||||||
/**
|
|
||||||
* Song name.
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
this.name = video.title;
|
|
||||||
/**
|
|
||||||
* Song duration.
|
|
||||||
* @type {Number}
|
|
||||||
*/
|
|
||||||
this.duration = ((video.duration.hours*3600)+(video.duration.minutes*60)+(video.duration.seconds)) * 100;
|
|
||||||
/**
|
|
||||||
* Raw video object from Simple Youtube API
|
|
||||||
* @type {Video}
|
|
||||||
*/
|
|
||||||
this.rawVideo = video;
|
|
||||||
/**
|
|
||||||
* Raw informations about the song.
|
|
||||||
* @type {Object}
|
|
||||||
*/
|
|
||||||
this.raw = video.raw;
|
|
||||||
/**
|
|
||||||
* Author channel of the song.
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
this.author = video.raw.snippet.channelTitle;
|
|
||||||
/**
|
|
||||||
* Youtube video URL.
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
this.url = `https://www.youtube.com/watch?v=${video.id}`;
|
|
||||||
/**
|
|
||||||
* Youtube video thumbnail.
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
this.thumbnail = video.raw.snippet.thumbnails.default.url;
|
|
||||||
/**
|
|
||||||
* The queue in which the song is.
|
|
||||||
* @type {Queue}
|
|
||||||
*/
|
|
||||||
this.queue = queue;
|
|
||||||
/**
|
|
||||||
* The user who requested that song.
|
|
||||||
* @type {User}
|
|
||||||
*/
|
|
||||||
this.requestedBy = requestedBy;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = Song;
|
|
71
src/Track.js
Normal file
71
src/Track.js
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
const Discord = require('discord.js')
|
||||||
|
const Queue = require('./Queue')
|
||||||
|
const SimpleYouTubeAPI = require('simple-youtube-api')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a track.
|
||||||
|
*/
|
||||||
|
class Track {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {SimpleYouTubeAPI.Video} video The video for this track
|
||||||
|
* @param {Discord.User?} user The user who requested the track
|
||||||
|
* @param {Queue?} queue The queue in which is the track is
|
||||||
|
*/
|
||||||
|
constructor(video, user, queue) {
|
||||||
|
/**
|
||||||
|
* The track name
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this.name = video.title;
|
||||||
|
/**
|
||||||
|
* The full video object
|
||||||
|
* @type {SimpleYouTubeAPI.Video}
|
||||||
|
*/
|
||||||
|
this.data = video;
|
||||||
|
/**
|
||||||
|
* The Youtube URL of the track
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this.url = `https://www.youtube.com/watch?v=${video.id}`;
|
||||||
|
/**
|
||||||
|
* The user who requested the track
|
||||||
|
* @type {Discord.User?}
|
||||||
|
*/
|
||||||
|
this.requestedBy = user;
|
||||||
|
/**
|
||||||
|
* The queue in which the track is
|
||||||
|
* @type {Queue}
|
||||||
|
*/
|
||||||
|
this.queue = queue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the channel which is the author of the video on Youtube
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
get author() {
|
||||||
|
return this.data.raw.snippet.channelTitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Youtube video thumbnail
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
get thumbnail() {
|
||||||
|
return this.data.raw.snippet.thumbnails.default.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The track duration
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
get duration() {
|
||||||
|
return typeof this.data.duration === "object"
|
||||||
|
? ((this.data.duration.hours*3600)+(this.data.duration.minutes*60)+(this.data.duration.seconds)) * 1000
|
||||||
|
: parseInt(this.data.duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = Track;
|
74
src/Util.js
74
src/Util.js
|
@ -1,46 +1,60 @@
|
||||||
const fetch = require('node-fetch');
|
const fetch = require('node-fetch')
|
||||||
|
const Discord = require('discord.js')
|
||||||
|
const Track = require('./Track')
|
||||||
|
const SimpleYouTubeAPI = require('simple-youtube-api')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utilities.
|
* Utilities
|
||||||
* @ignore
|
|
||||||
*/
|
*/
|
||||||
class Util {
|
class Util {
|
||||||
|
|
||||||
constructor(){}
|
/**
|
||||||
|
* @param {SimpleYouTubeAPI.YouTube} youtube The SimpleYouTubeAPI client instance
|
||||||
|
*/
|
||||||
|
constructor(youtube){
|
||||||
|
/**
|
||||||
|
* The SimpleYouTubeAPI client instance
|
||||||
|
* @type {SimpleYouTubeAPI.YouTube}
|
||||||
|
*/
|
||||||
|
this.youtube = youtube;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the first youtube results for your search.
|
* Get the first youtube results for your search
|
||||||
* @param {string} search The name of the video or the video URL.
|
* @param {string} query The name of the video or the video URL
|
||||||
* @param {Youtube} SYA The Simple Youtube API Client.
|
* @param {Discord.User?} user The user who requested the track
|
||||||
* @returns {Promise<Video>}
|
* @returns {Promise<Track[]>}
|
||||||
*/
|
*/
|
||||||
static getFirstYoutubeResult(search, SYA){
|
search(query, user) {
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
search = search.replace(/<(.+)>/g, "$1");
|
query = query.replace(/<(.+)>/g, "$1");
|
||||||
// Try with URL
|
|
||||||
SYA.getVideo(search).then(async (video) => {
|
|
||||||
video = await video.fetch();
|
|
||||||
resolve(video);
|
|
||||||
}).catch(async (err) => {
|
|
||||||
if(err.message === "Bad Request"){
|
|
||||||
reject('Invalid Youtube Data v3 API key.');
|
|
||||||
} else {
|
|
||||||
try {
|
try {
|
||||||
// Try with song name
|
const videoData = SimpleYouTubeAPI.parseURL(query);
|
||||||
let results = await SYA.searchVideos(search, 1);
|
if(videoData.video){
|
||||||
if(results.length < 1) return reject('Not found');
|
const video = await this.youtube.getVideoById(videoData.video);
|
||||||
let fetched = await results.shift().fetch();
|
if(video){
|
||||||
results.push(fetched);
|
await video.fetch();
|
||||||
resolve(results.pop());
|
const track = new Track(video, user, null);
|
||||||
} catch(err){
|
return resolve([ track ]);
|
||||||
if(err.message === "Bad Request"){
|
}
|
||||||
reject('Invalid Youtube Data v3 API key.');
|
}
|
||||||
|
const results = await this.youtube.searchVideos(query, 1);
|
||||||
|
const tracks = [];
|
||||||
|
for(let result of (results.filter((r) => r.type === "video"))){
|
||||||
|
// @ts-ignore
|
||||||
|
await result.fetch();
|
||||||
|
// @ts-ignore
|
||||||
|
const track = new Track(result, user, null);
|
||||||
|
tracks.push(track);
|
||||||
|
}
|
||||||
|
return resolve(tracks);
|
||||||
|
} catch(e) {
|
||||||
|
if(e.message && e.message === "Bad Request"){
|
||||||
|
reject("Looks like your YouTube Data v3 API key is not valid...");
|
||||||
} else {
|
} else {
|
||||||
reject(err);
|
reject(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue