♻️ Rewrite all JSDoc documentation

This commit is contained in:
Androz2091 2020-06-02 13:11:12 +02:00
parent 5669581708
commit e1e51ddf03
5 changed files with 356 additions and 309 deletions

View file

@ -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);
}); });
} }

View file

@ -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;

View file

@ -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
View 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;

View file

@ -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);
} }
} }
}
});
}); });
} }