♻️ Rewrite all JSDoc documentation
This commit is contained in:
parent
5669581708
commit
e1e51ddf03
5 changed files with 356 additions and 309 deletions
398
src/Player.js
398
src/Player.js
|
@ -1,22 +1,24 @@
|
|||
const ytdl = require('ytdl-core');
|
||||
const SYA = require('simple-youtube-api');
|
||||
const mergeOptions = require('merge-options');
|
||||
const SimpleYouTubeAPI = require('simple-youtube-api');
|
||||
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 Track = require('./Track');
|
||||
const Util = require('./Util');
|
||||
const Song = require('./Song');
|
||||
|
||||
/**
|
||||
* Player options.
|
||||
* @typedef {PlayerOptions}
|
||||
*
|
||||
* @property {Boolean} leaveOnEnd Whether the bot should leave the current voice channel when the queue ends.
|
||||
* @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 = {
|
||||
* @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 = {
|
||||
leaveOnEnd: true,
|
||||
leaveOnStop: true,
|
||||
leaveOnEmpty: true
|
||||
|
@ -25,158 +27,153 @@ const PlayerOptions = {
|
|||
class Player {
|
||||
|
||||
/**
|
||||
* @param {Client} client Your Discord Client instance.
|
||||
* @param {string} youtubeToken Your Youtube Data v3 API key.
|
||||
* @param {PlayerOptions} options The PlayerOptions object.
|
||||
* @param {Discord.Client} client Discord.js client
|
||||
* @param {string} youtubeToken Youtube Data v3 API Key
|
||||
* @param {PlayerOptions} options Player options
|
||||
*/
|
||||
constructor(client, youtubeToken, options = {}){
|
||||
if(!client) throw new SyntaxError('Invalid Discord client');
|
||||
if(!youtubeToken) throw new SyntaxError('Invalid Token: Token must be a String');
|
||||
|
||||
/**
|
||||
* Your Discord Client instance.
|
||||
* @type {Client}
|
||||
* Discord.js client instance
|
||||
* @type {Discord.Client}
|
||||
*/
|
||||
this.client = client;
|
||||
/**
|
||||
* Your Youtube Data v3 API key.
|
||||
* YouTube API Key
|
||||
* @type {string}
|
||||
*/
|
||||
this.youtubeToken = youtubeToken;
|
||||
/**
|
||||
* The Simple Youtube API Client.
|
||||
* @type {Youtube}
|
||||
* Simple YouTube API client instance
|
||||
* @type {SimpleYouTubeAPI.YouTube}
|
||||
*/
|
||||
this.SYA = new SYA(this.youtubeToken);
|
||||
this.youtube = new SimpleYouTubeAPI.YouTube(this.youtubeToken);
|
||||
/**
|
||||
* The guilds data.
|
||||
* Player queues
|
||||
* @type {Queue[]}
|
||||
*/
|
||||
this.queues = [];
|
||||
/**
|
||||
* Player options.
|
||||
* Player options
|
||||
* @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
|
||||
client.on('voiceStateUpdate', (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');
|
||||
}
|
||||
});
|
||||
client.on('voiceStateUpdate', (oldState, newState) => this._handleVoiceStateUpdate.call(this, oldState, newState));
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a guild is currently playing songs
|
||||
* @param {string} guildID The guild ID to check
|
||||
* @returns {Boolean} Whether the guild is currently playing songs
|
||||
* 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
|
||||
*/
|
||||
isPlaying(guildID) {
|
||||
return this.queues.some((g) => g.guildID === guildID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays a song in a voice channel.
|
||||
* @param {voiceChannel} voiceChannel The voice channel in which the song will be played.
|
||||
* @param {string} songName The name of the song to play.
|
||||
* @param {User} requestedBy The user who requested the song.
|
||||
* @returns {Promise<Song>}
|
||||
* 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
|
||||
*/
|
||||
play(voiceChannel, songName, requestedBy) {
|
||||
play(voiceChannel, track, user) {
|
||||
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);
|
||||
if(typeof songName !== "string") return reject("songName must be type of string. value="+songName);
|
||||
// Searches the song
|
||||
let video = await Util.getFirstYoutubeResult(songName, this.SYA);
|
||||
if(!video) return reject('Song not found');
|
||||
// Joins the voice channel
|
||||
let connection = await voiceChannel.join();
|
||||
// Creates a new guild with data
|
||||
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
|
||||
let queue = new Queue(voiceChannel.guild.id);
|
||||
queue.connection = connection;
|
||||
let song = new Song(video, queue, requestedBy);
|
||||
queue.songs.push(song);
|
||||
queue.voiceConnection = connection;
|
||||
// Add the track to the queue
|
||||
track.requestedBy = user;
|
||||
queue.tracks.push(track);
|
||||
// Add the queue to the list
|
||||
this.queues.push(queue);
|
||||
// Plays the song
|
||||
this._playSong(queue.guildID, true);
|
||||
// Resolves the song.
|
||||
resolve(song);
|
||||
// Play the track
|
||||
this._playTrack(queue.guildID, true);
|
||||
// Resolve the track
|
||||
resolve(track);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses the current song.
|
||||
* @param {string} guildID
|
||||
* @returns {Promise<Song>}
|
||||
* 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
|
||||
*/
|
||||
pause(guildID){
|
||||
pause (guildID) {
|
||||
return new Promise(async(resolve, reject) => {
|
||||
// Gets guild queue
|
||||
let queue = this.queues.find((g) => g.guildID === guildID);
|
||||
if(!queue) return reject('Not playing');
|
||||
// Pauses the dispatcher
|
||||
queue.dispatcher.pause();
|
||||
queue.voiceConnection.dispatcher.pause();
|
||||
queue.playing = false;
|
||||
// Resolves the guild queue
|
||||
resolve(queue.songs[0]);
|
||||
resolve(queue.tracks[0]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resumes the current song.
|
||||
* @param {string} guildID
|
||||
* @returns {Promise<Song>}
|
||||
* 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
|
||||
*/
|
||||
resume(guildID){
|
||||
resume (guildID) {
|
||||
return new Promise(async(resolve, reject) => {
|
||||
// Gets guild queue
|
||||
// Get guild queue
|
||||
let queue = this.queues.find((g) => g.guildID === guildID);
|
||||
if(!queue) return reject('Not playing');
|
||||
// Pauses the dispatcher
|
||||
queue.dispatcher.resume();
|
||||
// Pause the dispatcher
|
||||
queue.voiceConnection.dispatcher.resume();
|
||||
queue.playing = true;
|
||||
// Resolves the guild queue
|
||||
resolve(queue.songs[0]);
|
||||
// Resolve the guild queue
|
||||
resolve(queue.tracks[0]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops playing music.
|
||||
* @param {string} guildID
|
||||
* @param {Discord.Snowflake} guildID The ID of the guild where the music should be stopped
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
stop(guildID){
|
||||
return new Promise(async(resolve, reject) => {
|
||||
// Gets guild queue
|
||||
// Get guild queue
|
||||
let queue = this.queues.find((g) => g.guildID === guildID);
|
||||
if(!queue) return reject('Not playing');
|
||||
// Stops the dispatcher
|
||||
// Stop the dispatcher
|
||||
queue.stopped = true;
|
||||
queue.songs = [];
|
||||
queue.dispatcher.end();
|
||||
// Resolves
|
||||
queue.tracks = [];
|
||||
queue.voiceConnection.dispatcher.end();
|
||||
// Resolve
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the volume.
|
||||
* @param {string} guildID
|
||||
* @param {number} percent
|
||||
* 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)
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
setVolume(guildID, percent) {
|
||||
|
@ -185,15 +182,16 @@ class Player {
|
|||
let queue = this.queues.find((g) => g.guildID === guildID);
|
||||
if(!queue) return reject('Not playing');
|
||||
// Updates volume
|
||||
queue.dispatcher.setVolumeLogarithmic(percent / 200);
|
||||
queue.voiceConnection.dispatcher.setVolumeLogarithmic(percent / 200);
|
||||
queue.volume = percent;
|
||||
// Resolves guild queue
|
||||
resolve(queue);
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the guild queue.
|
||||
* @param {string} guildID
|
||||
* Get a guild queue
|
||||
* @param {Discord.Snowflake} guildID
|
||||
* @returns {?Queue}
|
||||
*/
|
||||
getQueue(guildID) {
|
||||
|
@ -203,108 +201,107 @@ class Player {
|
|||
}
|
||||
|
||||
/**
|
||||
* Adds a song to the guild queue.
|
||||
* @param {string} guildID
|
||||
* @param {string} songName The name of the song to add to the queue.
|
||||
* @param {User} requestedBy The user who requested the song.
|
||||
* @returns {Promise<Song>}
|
||||
* 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
|
||||
*/
|
||||
addToQueue(guildID, songName, requestedBy){
|
||||
addToQueue(guildID, trackName, requestedBy){
|
||||
return new Promise(async(resolve, reject) => {
|
||||
// Gets guild queue
|
||||
// Get guild queue
|
||||
let queue = this.queues.find((g) => g.guildID === guildID);
|
||||
if(!queue) return reject('Not playing');
|
||||
// Searches the song
|
||||
let video = await Util.getFirstYoutubeResult(songName, this.SYA).catch(() => {});
|
||||
if(!video) return reject('Song not found');
|
||||
let song = new Song(video, queue, requestedBy);
|
||||
// Updates queue
|
||||
queue.songs.push(song);
|
||||
// Resolves the song
|
||||
resolve(song);
|
||||
// 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]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the queue for a guild.
|
||||
* @param {string} guildID
|
||||
* @param {Array<Song>} songs The songs list
|
||||
* @returns {Promise<Queue>}
|
||||
* 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
|
||||
*/
|
||||
setQueue(guildID, songs){
|
||||
setQueue(guildID, tracks){
|
||||
return new Promise(async(resolve, reject) => {
|
||||
// Gets guild queue
|
||||
// Get guild queue
|
||||
let queue = this.queues.find((g) => g.guildID === guildID);
|
||||
if(!queue) return reject('Not playing');
|
||||
// Updates queue
|
||||
queue.songs = songs;
|
||||
// Resolves the queue
|
||||
// Update queue
|
||||
queue.tracks = tracks;
|
||||
// Resolve the queue
|
||||
resolve(queue);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the guild queue, but not the current song.
|
||||
* @param {string} guildID
|
||||
* @returns {Promise<Queue>}
|
||||
* 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
|
||||
*/
|
||||
clearQueue(guildID){
|
||||
return new Promise(async(resolve, reject) => {
|
||||
// Gets guild queue
|
||||
// Get guild queue
|
||||
let queue = this.queues.find((g) => g.guildID === guildID);
|
||||
if(!queue) return reject('Not playing');
|
||||
// Clears queue
|
||||
let currentlyPlaying = queue.songs.shift();
|
||||
queue.songs = [ currentlyPlaying ];
|
||||
// Resolves guild queue
|
||||
// Clear queue
|
||||
let currentlyPlaying = queue.tracks.shift();
|
||||
queue.tracks = [ currentlyPlaying ];
|
||||
// Resolve guild queue
|
||||
resolve(queue);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Skips a song.
|
||||
* @param {string} guildID
|
||||
* @returns {Promise<Song>}
|
||||
* Skip a track
|
||||
* @param {Discord.Snowflake} guildID The ID of the guild where the track should be skipped
|
||||
* @returns {Promise<Track>}
|
||||
*/
|
||||
skip(guildID){
|
||||
return new Promise(async(resolve, reject) => {
|
||||
// Gets guild queue
|
||||
// Get guild queue
|
||||
let queue = this.queues.find((g) => g.guildID === guildID);
|
||||
if(!queue) return reject('Not playing');
|
||||
let currentSong = queue.songs[0];
|
||||
// Ends the dispatcher
|
||||
queue.dispatcher.end();
|
||||
queue.skipped = true;
|
||||
// Resolves the current song
|
||||
resolve(currentSong);
|
||||
let currentTrack = queue.tracks[0];
|
||||
// End the dispatcher
|
||||
queue.voiceConnection.dispatcher.end();
|
||||
queue.lastSkipped = true;
|
||||
// Resolve the current track
|
||||
resolve(currentTrack);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the currently playing song.
|
||||
* @param {string} guildID
|
||||
* @returns {Promise<Song>}
|
||||
* Get the currently playing track
|
||||
* @param {Discord.Snowflake} guildID
|
||||
* @returns {Promise<Track>} The track which is currently played
|
||||
*/
|
||||
nowPlaying(guildID){
|
||||
return new Promise(async(resolve, reject) => {
|
||||
// Gets guild queue
|
||||
// Get guild queue
|
||||
let queue = this.queues.find((g) => g.guildID === guildID);
|
||||
if(!queue) return reject('Not playing');
|
||||
let currentSong = queue.songs[0];
|
||||
// Resolves the current song
|
||||
resolve(currentSong);
|
||||
let currentTrack = queue.tracks[0];
|
||||
// Resolve the current track
|
||||
resolve(currentTrack);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable or disable the repeat mode
|
||||
* @param {string} guildID
|
||||
* @param {Discord.Snowflake} guildID
|
||||
* @param {Boolean} enabled Whether the repeat mode should be enabled
|
||||
* @returns {Promise<Void>}
|
||||
*/
|
||||
setRepeatMode(guildID, enabled) {
|
||||
return new Promise(async(resolve, reject) => {
|
||||
// Gets guild queue
|
||||
// Get guild queue
|
||||
let queue = this.queues.find((g) => g.guildID === guildID);
|
||||
if(!queue) return reject('Not playing');
|
||||
// Enable/Disable repeat mode
|
||||
|
@ -315,89 +312,116 @@ class Player {
|
|||
}
|
||||
|
||||
/**
|
||||
* Shuffles the guild queue.
|
||||
* @param {string} guildID
|
||||
* @returns {Promise<Void>}
|
||||
* 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
|
||||
*/
|
||||
shuffle(guildID){
|
||||
return new Promise(async(resolve, reject) => {
|
||||
// Gets guild queue
|
||||
// Get guild queue
|
||||
let queue = this.queues.find((g) => g.guildID === guildID);
|
||||
if(!queue) return reject('Not playing');
|
||||
// Shuffle the queue (except the first song)
|
||||
let currentSong = queue.songs.shift();
|
||||
queue.songs = queue.songs.sort(() => Math.random() - 0.5);
|
||||
queue.songs.unshift(currentSong);
|
||||
// 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);
|
||||
// Resolve
|
||||
resolve();
|
||||
resolve(queue);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a song from the queue
|
||||
* @param {string} guildID
|
||||
* @param {number|Song} song The index of the song to remove or the song to remove object.
|
||||
* @returns {Promise<Song|null>}
|
||||
* 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>}
|
||||
*/
|
||||
remove(guildID, song){
|
||||
remove(guildID, track){
|
||||
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 song from the queue
|
||||
let songFound = null;
|
||||
if(typeof song === "number"){
|
||||
songFound = queue.songs[song];
|
||||
if(songFound){
|
||||
queue.songs = queue.songs.filter((s) => s !== songFound);
|
||||
// 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);
|
||||
}
|
||||
} else {
|
||||
songFound = queue.songs.find((s) => s === song);
|
||||
if(songFound){
|
||||
queue.songs = queue.songs.filter((s) => s !== songFound);
|
||||
trackFound = queue.tracks.find((s) => s === track);
|
||||
if(trackFound){
|
||||
queue.tracks = queue.tracks.filter((s) => s !== trackFound);
|
||||
}
|
||||
}
|
||||
// Resolve
|
||||
resolve(songFound);
|
||||
resolve(trackFound);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start playing songs in a guild.
|
||||
* Handle the voice state update event
|
||||
* @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
|
||||
*/
|
||||
async _playSong(guildID, firstPlay) {
|
||||
async _playTrack(guildID, firstPlay) {
|
||||
// Gets guild queue
|
||||
let queue = this.queues.find((g) => g.guildID === guildID);
|
||||
// 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
|
||||
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
|
||||
this.queues = this.queues.filter((g) => g.guildID !== guildID);
|
||||
// Emits stop event
|
||||
if(queue.stopped){
|
||||
if(this.options.leaveOnStop) queue.connection.channel.leave();
|
||||
if(this.options.leaveOnStop) queue.voiceConnection.channel.leave();
|
||||
return queue.emit('stop');
|
||||
}
|
||||
// Emits end event
|
||||
return queue.emit('end');
|
||||
}
|
||||
// Emit songChanged event
|
||||
if(!firstPlay) queue.emit('songChanged', (!queue.repeatMode ? queue.songs.shift() : queue.songs[0]), queue.songs[0], queue.skipped, queue.repeatMode);
|
||||
queue.skipped = false;
|
||||
let song = queue.songs[0];
|
||||
// Download the song
|
||||
let dispatcher = queue.connection.play(ytdl(song.url, { filter: "audioonly" }));
|
||||
queue.dispatcher = dispatcher;
|
||||
// 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"
|
||||
}));
|
||||
// Set volume
|
||||
dispatcher.setVolumeLogarithmic(queue.volume / 200);
|
||||
// When the song ends
|
||||
dispatcher.on('finish', () => {
|
||||
// Play the next song
|
||||
return this._playSong(guildID, false);
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
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.
|
||||
|
@ -6,60 +8,56 @@ const { EventEmitter } = require('events');
|
|||
class Queue extends EventEmitter {
|
||||
|
||||
/**
|
||||
* Represents a guild queue.
|
||||
* @param {string} guildID
|
||||
* @param {Discord.Snowflake} guildID ID of the guild this queue is for.
|
||||
*/
|
||||
constructor(guildID){
|
||||
super();
|
||||
/**
|
||||
* The guild ID.
|
||||
* @type {Snowflake}
|
||||
* ID of the guild this queue is for.
|
||||
* @type {Discord.Snowflake}
|
||||
*/
|
||||
this.guildID = guildID;
|
||||
/**
|
||||
* The stream dispatcher.
|
||||
* @type {StreamDispatcher}
|
||||
* The voice connection of this queue.
|
||||
* @type {Discord.VoiceConnection}
|
||||
*/
|
||||
this.dispatcher = null;
|
||||
this.voiceConnection = null;
|
||||
/**
|
||||
* The voice connection.
|
||||
* @type {VoiceConnection}
|
||||
* The tracks of this queue. The first one is currenlty playing and the others are going to be played.
|
||||
* @type {Track[]}
|
||||
*/
|
||||
this.connection = null;
|
||||
/**
|
||||
* Songs. The first one is currently playing and the rest is going to be played.
|
||||
* @type {Song[]}
|
||||
*/
|
||||
this.songs = [];
|
||||
this.tracks = [];
|
||||
/**
|
||||
* Whether the stream is currently stopped.
|
||||
* @type {Boolean}
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.stopped = false;
|
||||
/**
|
||||
* Whether the last song was skipped.
|
||||
* @type {Boolean}
|
||||
* Whether the last track was skipped.
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.skipped = false;
|
||||
this.lastSkipped = false;
|
||||
/**
|
||||
* The stream volume.
|
||||
* @type {Number}
|
||||
* The stream volume of this queue. (0-100)
|
||||
* @type {number}
|
||||
*/
|
||||
this.volume = 100;
|
||||
/**
|
||||
* Whether the stream is currently playing.
|
||||
* @type {Boolean}
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.playing = true;
|
||||
/**
|
||||
* Whether the repeat mode is enabled.
|
||||
* @type {Boolean}
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.repeatMode = false;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
module.exports = Queue;
|
||||
|
||||
/**
|
||||
* Emitted when the queue is empty.
|
||||
* @event Queue#end
|
||||
|
@ -71,11 +69,9 @@ class Queue extends EventEmitter {
|
|||
*/
|
||||
|
||||
/**
|
||||
* Emitted when the song changes.
|
||||
* @event Queue#songChanged
|
||||
* @param {Song} oldSong The old song (playing before)
|
||||
* @param {Song} newSong The new song (currently playing)
|
||||
* Emitted when the track changes.
|
||||
* @event Queue#trackChanged
|
||||
* @param {Track} oldTrack The old track (playing before)
|
||||
* @param {Track} newTrack The new track (currently playing)
|
||||
* @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;
|
82
src/Util.js
82
src/Util.js
|
@ -1,49 +1,63 @@
|
|||
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.
|
||||
* @ignore
|
||||
* Utilities
|
||||
*/
|
||||
class Util {
|
||||
|
||||
constructor(){}
|
||||
|
||||
/**
|
||||
* Gets the first youtube results for your search.
|
||||
* @param {string} search The name of the video or the video URL.
|
||||
* @param {Youtube} SYA The Simple Youtube API Client.
|
||||
* @returns {Promise<Video>}
|
||||
* @param {SimpleYouTubeAPI.YouTube} youtube The SimpleYouTubeAPI client instance
|
||||
*/
|
||||
static getFirstYoutubeResult(search, SYA){
|
||||
constructor(youtube){
|
||||
/**
|
||||
* The SimpleYouTubeAPI client instance
|
||||
* @type {SimpleYouTubeAPI.YouTube}
|
||||
*/
|
||||
this.youtube = youtube;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first youtube results for your search
|
||||
* @param {string} query The name of the video or the video URL
|
||||
* @param {Discord.User?} user The user who requested the track
|
||||
* @returns {Promise<Track[]>}
|
||||
*/
|
||||
search(query, user) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
search = search.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 with song name
|
||||
let results = await SYA.searchVideos(search, 1);
|
||||
if(results.length < 1) return reject('Not found');
|
||||
let fetched = await results.shift().fetch();
|
||||
results.push(fetched);
|
||||
resolve(results.pop());
|
||||
} catch(err){
|
||||
if(err.message === "Bad Request"){
|
||||
reject('Invalid Youtube Data v3 API key.');
|
||||
} else {
|
||||
reject(err);
|
||||
}
|
||||
query = query.replace(/<(.+)>/g, "$1");
|
||||
try {
|
||||
const videoData = SimpleYouTubeAPI.parseURL(query);
|
||||
if(videoData.video){
|
||||
const video = await this.youtube.getVideoById(videoData.video);
|
||||
if(video){
|
||||
await video.fetch();
|
||||
const track = new Track(video, user, null);
|
||||
return resolve([ track ]);
|
||||
}
|
||||
}
|
||||
});
|
||||
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 {
|
||||
reject(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
module.exports = Util;
|
||||
module.exports = Util;
|
||||
|
|
Loading…
Reference in a new issue