From 73001169cd3d6a2c918cbbf6dc58e9ad8628845a Mon Sep 17 00:00:00 2001 From: Androz2091 Date: Sun, 21 Jun 2020 15:36:16 +0200 Subject: [PATCH] :recycle: Rewrite everything with eslint and discord-ytdl-core --- .eslintrc.js | 22 +++ package.json | 12 +- src/Player.js | 470 +++++++++++++++++++++++++++++--------------------- src/Queue.js | 38 ++-- src/Track.js | 80 +++++---- src/Util.js | 63 ------- 6 files changed, 370 insertions(+), 315 deletions(-) create mode 100644 .eslintrc.js delete mode 100644 src/Util.js diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..43b6d49 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,22 @@ +module.exports = { + env: { + commonjs: true, + es6: true, + node: true + }, + extends: [ + 'standard' + ], + globals: { + Atomics: 'readonly', + SharedArrayBuffer: 'readonly' + }, + parserOptions: { + ecmaVersion: 11 + }, + rules: { + indent: ['error', 4], + 'no-async-promise-executor': 'off', + 'no-unused-vars': 'off' + } +} diff --git a/package.json b/package.json index 5f7085c..9d0bc7f 100644 --- a/package.json +++ b/package.json @@ -28,13 +28,21 @@ }, "homepage": "https://github.com/Androz2091/discord-player#readme", "dependencies": { - "ffmpeg-static": "^4.1.1", + "@discordjs/opus": "^0.3.2", + "discord-ytdl-core": "^4.0.0", "merge-options": "^2.0.0", + "node-fetch": "^2.6.0", "simple-youtube-api": "^5.2.1", - "ytdl-core": "^3.0.0" + "ytsr": "^0.1.15" }, "devDependencies": { "discord.js": "discordjs/discord.js", + "eslint": "^7.1.0", + "eslint-config-standard": "^14.1.1", + "eslint-plugin-import": "^2.20.2", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-promise": "^4.2.1", + "eslint-plugin-standard": "^4.0.1", "jsdoc": "^3.6.3", "minami": "Androz2091/minami" } diff --git a/src/Player.js b/src/Player.js index 8098a79..7e97a99 100644 --- a/src/Player.js +++ b/src/Player.js @@ -1,10 +1,24 @@ -const ytdl = require('ytdl-core'); -const SimpleYouTubeAPI = require('simple-youtube-api'); -const Discord = require('discord.js'); +const ytdl = require('discord-ytdl-core') +const Discord = require('discord.js') +const ytsr = require('ytsr') -const Queue = require('./Queue'); -const Track = require('./Track'); -const Util = require('./Util'); +const Queue = require('./Queue') +const Track = require('./Track') + +const filters = { + bassboost: 'bass=g=20,dynaudnorm=f=200', + '8D': 'apulsator=hz=0.128', + vaporwave: 'asetrate=441000*.8,aresample=44100,atempo=1.1', + nightcore: 'asetrate=441001*.25', + phaser: 'aphaser=in_gain=0.4', + tremolo: 'tremolo=f=6.5', + reverse: 'areverse', + treble: 'treble=g={GAIN}', + normalizer: 'dynaudnorm=f=150', + surrounding: 'surround', + pulsator: 'apulsator=hz=1', + subboost: 'asubboost' +} /** * @typedef PlayerOptions @@ -22,55 +36,111 @@ const defaultPlayerOptions = { leaveOnEnd: true, leaveOnStop: true, leaveOnEmpty: true -}; +} class Player { - /** * @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'); + constructor (client, options = {}) { + if (!client) throw new SyntaxError('Invalid Discord client') /** * Discord.js client instance * @type {Discord.Client} */ - this.client = client; - /** - * YouTube API Key - * @type {string} - */ - this.youtubeToken = youtubeToken; - /** - * Simple YouTube API client instance - * @type {SimpleYouTubeAPI.YouTube} - */ - this.youtube = new SimpleYouTubeAPI.YouTube(this.youtubeToken); + this.client = client /** * Player queues * @type {Queue[]} */ - this.queues = []; + this.queues = [] /** * Player options * @type {PlayerOptions} */ - this.options = defaultPlayerOptions; - for(const prop in options){ - this.options[prop] = options[prop]; + this.options = defaultPlayerOptions + for (const prop in options) { + this.options[prop] = options[prop] } - /** - * Utilities methods for the player - * @type {Util} - */ - this.util = new Util(this.youtube) // Listener to check if the channel is empty - client.on('voiceStateUpdate', (oldState, newState) => this._handleVoiceStateUpdate.call(this, oldState, newState)); + client.on('voiceStateUpdate', (oldState, newState) => this._handleVoiceStateUpdate(oldState, newState)) + } + + _playYTDLStream (queue, updateFilter) { + return new Promise((resolve) => { + const currentStreamTime = updateFilter ? queue.voiceConnection.dispatcher.streamTime / 1000 : undefined + const encoderArgsFilters = [] + Object.keys(queue.filters).forEach((filterName) => { + if (queue.filters[filterName]) { + encoderArgsFilters.push(filters[filterName]) + } + }) + let encoderArgs + if (encoderArgsFilters.length < 1) { + encoderArgs = [] + } else { + encoderArgs = ['-af', encoderArgsFilters.join(',')] + } + const newStream = ytdl(queue.playing.url, { + filter: 'audioonly', + opusEncoded: true, + encoderArgs, + seek: currentStreamTime + }) + setTimeout(() => { + queue.voiceConnection.play(newStream, { + type: 'opus' + }) + queue.voiceConnection.dispatcher.setVolumeLogarithmic(queue.volume / 200) + // When the track starts + queue.voiceConnection.dispatcher.on('start', () => { + resolve() + }) + // When the track ends + queue.voiceConnection.dispatcher.on('finish', () => { + // Play the next track + return this._playTrack(queue.guildID, false) + }) + }, 1000) + }) + } + + /** + * Update the filters for the guild + * @param {Discord.Snowflake} guildID + * @param {Object} newFilters + */ + updateFilters (guildID, newFilters) { + return new Promise((resolve, reject) => { + // Gets guild queue + const queue = this.queues.find((g) => g.guildID === guildID) + if (!queue) return reject(new Error('Not playing')) + Object.keys(newFilters).forEach((filterName) => { + queue.filters[filterName] = newFilters[filterName] + }) + this._playYTDLStream(queue, true, false) + }) + } + + /** + * Searchs tracks on YouTube + * @param {string} query The query + * @returns {Promise} + */ + searchTracks (query) { + return new Promise(async (resolve, reject) => { + if (query.match(/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/)) { + query = query.match(/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/)[1] + } + ytsr(query, (err, results) => { + if (err) return [] + const resultsVideo = results.items.filter((i) => i.type === 'video') + resolve(resultsVideo.map((r) => new Track(r, null, null))) + }) + }) } /** @@ -78,8 +148,8 @@ class Player { * @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); + isPlaying (guildID) { + return this.queues.some((g) => g.guildID === guildID) } /** @@ -89,30 +159,34 @@ class Player { * @param {Discord.User?} user The user who requested the track * @returns {Promise} The played track */ - play(voiceChannel, track, user) { - this.queues = this.queues.filter((g) => g.guildID !== voiceChannel.id); + 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 (!voiceChannel || typeof voiceChannel !== 'object') { + return reject(new Error(`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]; + const connection = voiceChannel.client.voice.connections.find((c) => c.channel.id === voiceChannel.id) || await voiceChannel.join() + if (typeof track !== 'object') { + const results = await this.searchTracks(track) + track = results[0] } // Create a new guild with data - let queue = new Queue(voiceChannel.guild.id); - queue.voiceConnection = connection; + const queue = new Queue(voiceChannel.guild.id) + queue.voiceConnection = connection + queue.filters = {} + Object.keys(filters).forEach((f) => { + queue.filters[f] = false + }) // Add the track to the queue - track.requestedBy = user; - queue.tracks.push(track); + track.requestedBy = user + queue.tracks.push(track) // Add the queue to the list - this.queues.push(queue); + this.queues.push(queue) // Play the track - this._playTrack(queue.guildID, true); + this._playTrack(queue.guildID, true) // Resolve the track - resolve(track); - }); + resolve(track) + }) } /** @@ -121,16 +195,16 @@ class Player { * @returns {Promise} The paused track */ pause (guildID) { - return new Promise(async(resolve, reject) => { + return new Promise((resolve, reject) => { // Gets guild queue - let queue = this.queues.find((g) => g.guildID === guildID); - if(!queue) return reject('Not playing'); + const queue = this.queues.find((g) => g.guildID === guildID) + if (!queue) return reject(new Error('Not playing')) // Pauses the dispatcher - queue.voiceConnection.dispatcher.pause(); - queue.playing = false; + queue.voiceConnection.dispatcher.pause() + queue.paused = true // Resolves the guild queue - resolve(queue.tracks[0]); - }); + resolve(queue.tracks[0]) + }) } /** @@ -139,16 +213,16 @@ class Player { * @returns {Promise} The resumed track */ resume (guildID) { - return new Promise(async(resolve, reject) => { + return new Promise((resolve, reject) => { // Get guild queue - let queue = this.queues.find((g) => g.guildID === guildID); - if(!queue) return reject('Not playing'); + const queue = this.queues.find((g) => g.guildID === guildID) + if (!queue) return reject(new Error('Not playing')) // Pause the dispatcher - queue.voiceConnection.dispatcher.resume(); - queue.playing = true; + queue.voiceConnection.dispatcher.resume() + queue.paused = false // Resolve the guild queue - resolve(queue.tracks[0]); - }); + resolve(queue.tracks[0]) + }) } /** @@ -156,37 +230,37 @@ class Player { * @param {Discord.Snowflake} guildID The ID of the guild where the music should be stopped * @returns {Promise} */ - stop(guildID){ - return new Promise(async(resolve, reject) => { + stop (guildID) { + return new Promise((resolve, reject) => { // Get guild queue - let queue = this.queues.find((g) => g.guildID === guildID); - if(!queue) return reject('Not playing'); + const queue = this.queues.find((g) => g.guildID === guildID) + if (!queue) return reject(new Error('Not playing')) // Stop the dispatcher - queue.stopped = true; - queue.tracks = []; - queue.voiceConnection.dispatcher.end(); + queue.stopped = true + queue.tracks = [] + queue.voiceConnection.dispatcher.end() // Resolve - resolve(); - }); + resolve() + }) } /** * Update the volume - * @param {Discord.Snowflake} guildID The ID of the guild where the music should be modified + * @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} */ - setVolume(guildID, percent) { - return new Promise(async(resolve, reject) => { - // Gets guild queue - let queue = this.queues.find((g) => g.guildID === guildID); - if(!queue) return reject('Not playing'); + setVolume (guildID, percent) { + return new Promise((resolve, reject) => { + // Get guild queue + const queue = this.queues.find((g) => g.guildID === guildID) + if (!queue) return reject(new Error('Not playing')) // Updates volume - queue.voiceConnection.dispatcher.setVolumeLogarithmic(percent / 200); - queue.volume = percent; + queue.voiceConnection.dispatcher.setVolumeLogarithmic(percent / 200) + queue.volume = percent // Resolves guild queue - resolve(); - }); + resolve() + }) } /** @@ -194,10 +268,10 @@ class Player { * @param {Discord.Snowflake} guildID * @returns {?Queue} */ - getQueue(guildID) { + getQueue (guildID) { // Gets guild queue - let queue = this.queues.find((g) => g.guildID === guildID); - return queue; + const queue = this.queues.find((g) => g.guildID === guildID) + return queue } /** @@ -207,19 +281,23 @@ class Player { * @param {Discord.User?} requestedBy The user who requested the track * @returns {Promise} The added track */ - addToQueue(guildID, trackName, requestedBy){ - return new Promise(async(resolve, reject) => { + addToQueue (guildID, trackName, requestedBy) { + return new Promise((resolve, reject) => { // Get guild queue - let queue = this.queues.find((g) => g.guildID === guildID); - if(!queue) return reject('Not playing'); + const queue = this.queues.find((g) => g.guildID === guildID) + if (!queue) return reject(new Error('Not playing')) // Search the track - let track = await this.util.search(trackName, requestedBy).catch(() => {}); - if(!track[0]) return reject('Track not found'); - // Update queue - queue.tracks.push(track[0]); - // Resolve the track - resolve(track[0]); - }); + this.search(trackName).then((track) => { + if (!track[0]) return reject(new Error('Track not found')) + track[0].requestedBy = requestedBy + // Update queue + queue.tracks.push(track[0]) + // Resolve the track + resolve(track[0]) + }).catch(() => { + return reject(new Error('Track not found')) + }) + }) } /** @@ -228,16 +306,16 @@ class Player { * @param {Track[]} tracks The tracks list * @returns {Promise} The new queue */ - setQueue(guildID, tracks){ - return new Promise(async(resolve, reject) => { + setQueue (guildID, tracks) { + return new Promise((resolve, reject) => { // Get guild queue - let queue = this.queues.find((g) => g.guildID === guildID); - if(!queue) return reject('Not playing'); + const queue = this.queues.find((g) => g.guildID === guildID) + if (!queue) return reject(new Error('Not playing')) // Update queue - queue.tracks = tracks; + queue.tracks = tracks // Resolve the queue - resolve(queue); - }); + resolve(queue) + }) } /** @@ -245,17 +323,17 @@ class Player { * @param {Discord.Snowflake} guildID The ID of the guild where the queue should be cleared * @returns {Promise} The updated queue */ - clearQueue(guildID){ - return new Promise(async(resolve, reject) => { + clearQueue (guildID) { + return new Promise((resolve, reject) => { // Get guild queue - let queue = this.queues.find((g) => g.guildID === guildID); - if(!queue) return reject('Not playing'); + const queue = this.queues.find((g) => g.guildID === guildID) + if (!queue) return reject(new Error('Not playing')) // Clear queue - let currentlyPlaying = queue.tracks.shift(); - queue.tracks = [ currentlyPlaying ]; + const currentlyPlaying = queue.tracks.shift() + queue.tracks = [currentlyPlaying] // Resolve guild queue - resolve(queue); - }); + resolve(queue) + }) } /** @@ -263,18 +341,18 @@ class Player { * @param {Discord.Snowflake} guildID The ID of the guild where the track should be skipped * @returns {Promise} */ - skip(guildID){ - return new Promise(async(resolve, reject) => { + skip (guildID) { + return new Promise((resolve, reject) => { // Get guild queue - let queue = this.queues.find((g) => g.guildID === guildID); - if(!queue) return reject('Not playing'); - let currentTrack = queue.tracks[0]; + const queue = this.queues.find((g) => g.guildID === guildID) + if (!queue) return reject(new Error('Not playing')) + const currentTrack = queue.tracks[0] // End the dispatcher - queue.voiceConnection.dispatcher.end(); - queue.lastSkipped = true; + queue.voiceConnection.dispatcher.end() + queue.lastSkipped = true // Resolve the current track - resolve(currentTrack); - }); + resolve(currentTrack) + }) } /** @@ -282,15 +360,15 @@ class Player { * @param {Discord.Snowflake} guildID * @returns {Promise} The track which is currently played */ - nowPlaying(guildID){ - return new Promise(async(resolve, reject) => { + nowPlaying (guildID) { + return new Promise((resolve, reject) => { // Get guild queue - let queue = this.queues.find((g) => g.guildID === guildID); - if(!queue) return reject('Not playing'); - let currentTrack = queue.tracks[0]; + const queue = this.queues.find((g) => g.guildID === guildID) + if (!queue) return reject(new Error('Not playing')) + const currentTrack = queue.tracks[0] // Resolve the current track - resolve(currentTrack); - }); + resolve(currentTrack) + }) } /** @@ -299,16 +377,16 @@ class Player { * @param {Boolean} enabled Whether the repeat mode should be enabled * @returns {Promise} */ - setRepeatMode(guildID, enabled) { - return new Promise(async(resolve, reject) => { + setRepeatMode (guildID, enabled) { + return new Promise((resolve, reject) => { // Get guild queue - let queue = this.queues.find((g) => g.guildID === guildID); - if(!queue) return reject('Not playing'); + const queue = this.queues.find((g) => g.guildID === guildID) + if (!queue) return reject(new Error('Not playing')) // Enable/Disable repeat mode - queue.repeatMode = enabled; + queue.repeatMode = enabled // Resolve - resolve(); - }); + resolve() + }) } /** @@ -316,18 +394,18 @@ class Player { * @param {Discord.Snowflake} guildID The ID of the guild where the queue should be shuffled * @returns {Promise} The updated queue */ - shuffle(guildID){ - return new Promise(async(resolve, reject) => { + shuffle (guildID) { + return new Promise((resolve, reject) => { // Get guild queue - let queue = this.queues.find((g) => g.guildID === guildID); - if(!queue) return reject('Not playing'); + const queue = this.queues.find((g) => g.guildID === guildID) + if (!queue) return reject(new Error('Not playing')) // Shuffle the queue (except the first track) - let currentTrack = queue.tracks.shift(); - queue.tracks = queue.tracks.sort(() => Math.random() - 0.5); - queue.tracks.unshift(currentTrack); + const currentTrack = queue.tracks.shift() + queue.tracks = queue.tracks.sort(() => Math.random() - 0.5) + queue.tracks.unshift(currentTrack) // Resolve - resolve(queue); - }); + resolve(queue) + }) } /** @@ -336,27 +414,27 @@ class Player { * @param {number|Track} track The index of the track to remove or the track to remove object * @returns {Promise} */ - remove(guildID, track){ - return new Promise(async(resolve, reject) => { + remove (guildID, track) { + return new Promise((resolve, reject) => { // Gets guild queue - let queue = this.queues.find((g) => g.guildID === guildID); - if(!queue) return reject('Not playing'); + const queue = this.queues.find((g) => g.guildID === guildID) + if (!queue) return reject(new Error('Not playing')) // Remove the track from the queue - let trackFound = null; - if(typeof track === "number"){ - trackFound = queue.tracks[track]; - if(trackFound){ - queue.tracks = queue.tracks.filter((s) => s !== trackFound); + let trackFound = null + if (typeof track === 'number') { + trackFound = queue.tracks[track] + if (trackFound) { + queue.tracks = queue.tracks.filter((t) => t !== trackFound) } } else { - trackFound = queue.tracks.find((s) => s === track); - if(trackFound){ - queue.tracks = queue.tracks.filter((s) => s !== trackFound); + trackFound = queue.tracks.find((s) => s === track) + if (trackFound) { + queue.tracks = queue.tracks.filter((s) => s !== trackFound) } } // Resolve - resolve(trackFound); - }); + resolve(trackFound) + }) } /** @@ -366,21 +444,21 @@ class Player { * @param {Discord.VoiceState} oldState * @param {Discord.VoiceState} newState */ - _handleVoiceStateUpdate(oldState, newState) { - if(!this.options.leaveOnEmpty) return; + _handleVoiceStateUpdate (oldState, newState) { + if (!this.options.leaveOnEmpty) return // If the member leaves a voice channel - if(!oldState.channelID || newState.channelID) return; + 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){ + const 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; + if (queue.voiceConnection.channel.members.size > 1) return // Disconnect from the voice channel - queue.voiceConnection.channel.leave(); + queue.voiceConnection.channel.leave() // Delete the queue - this.queues = this.queues.filter((g) => g.guildID !== queue.guildID); + this.queues = this.queues.filter((g) => g.guildID !== queue.guildID) // Emit end event - queue.emit('channelEmpty'); + queue.emit('channelEmpty') } } @@ -391,40 +469,34 @@ class Player { * @param {Discord.Snowflake} guildID * @param {Boolean} firstPlay Whether the function was called from the play() one */ - async _playTrack(guildID, firstPlay) { - // Gets guild queue - let queue = this.queues.find((g) => g.guildID === guildID); + async _playTrack (guildID, firstPlay) { + // Get guild queue + const queue = this.queues.find((g) => g.guildID === guildID) // If there isn't any music in the queue - if(queue.tracks.length < 2 && !firstPlay && !queue.repeatMode){ - // Leaves the voice channel - 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.voiceConnection.channel.leave(); - return queue.emit('stop'); + if (queue.tracks.length < 2 && !firstPlay && !queue.repeatMode) { + // Leave the voice channel + if (this.options.leaveOnEnd && !queue.stopped) queue.voiceConnection.channel.leave() + // Remove the guild from the guilds list + this.queues = this.queues.filter((g) => g.guildID !== guildID) + // Emit stop event + if (queue.stopped) { + if (this.options.leaveOnStop) queue.voiceConnection.channel.leave() + return queue.emit('stop') } - // Emits end event - return queue.emit('end'); + // Emit end event + return queue.emit('end') } - // Emit trackChanged event - if(!firstPlay) queue.emit('trackChanged', (!queue.repeatMode ? queue.tracks.shift() : queue.tracks[0]), queue.tracks[0], queue.lastSkipped, queue.repeatMode); - queue.lastSkipped = false; - let track = queue.tracks[0]; - // Download the track - queue.voiceConnection.play(ytdl(track.url, { - filter: "audioonly" - })); - // Set volume - queue.voiceConnection.dispatcher.setVolumeLogarithmic(queue.volume / 200); - // When the track ends - queue.voiceConnection.dispatcher.on('finish', () => { - // Play the next track - return this._playTrack(guildID, false); - }); + const wasPlaying = queue.playing + const nowPlaying = queue.playing = queue.repeatMode ? wasPlaying : queue.tracks.shift() + // Reset lastSkipped state + queue.lastSkipped = false + this._playYTDLStream(queue, false).then(() => { + // Emit trackChanged event + if (!firstPlay) { + queue.emit('trackChanged', nowPlaying, wasPlaying, queue.lastSkipped, queue.repeatMode) + } + }) } - }; -module.exports = Player; \ No newline at end of file +module.exports = Player diff --git a/src/Queue.js b/src/Queue.js index e42c727..0637842 100644 --- a/src/Queue.js +++ b/src/Queue.js @@ -6,57 +6,65 @@ const Track = require('./Track') * Represents a guild queue. */ class Queue extends EventEmitter { - /** * @param {Discord.Snowflake} guildID ID of the guild this queue is for. */ - constructor(guildID){ - super(); + constructor (guildID) { + super() /** * ID of the guild this queue is for. * @type {Discord.Snowflake} */ - this.guildID = guildID; + this.guildID = guildID /** * The voice connection of this queue. * @type {Discord.VoiceConnection} */ - this.voiceConnection = null; + this.voiceConnection = null + /** + * The song currently played. + * @type {Track} + */ + this.playing = null /** * The tracks of this queue. The first one is currenlty playing and the others are going to be played. * @type {Track[]} */ - this.tracks = []; + this.tracks = [] /** * Whether the stream is currently stopped. * @type {boolean} */ - this.stopped = false; + this.stopped = false /** * Whether the last track was skipped. * @type {boolean} */ - this.lastSkipped = false; + this.lastSkipped = false /** * The stream volume of this queue. (0-100) * @type {number} */ - this.volume = 100; + this.volume = 100 /** - * Whether the stream is currently playing. + * Whether the stream is currently paused. * @type {boolean} */ - this.playing = true; + this.paused = true /** * Whether the repeat mode is enabled. * @type {boolean} */ - this.repeatMode = false; + this.repeatMode = false + /** + * Filters status + * @type {Object} + */ + this.filters = {} } +} -}; - -module.exports = Queue; +module.exports = Queue /** * Emitted when the queue is empty. diff --git a/src/Track.js b/src/Track.js index b3f0133..3dd6efa 100644 --- a/src/Track.js +++ b/src/Track.js @@ -1,71 +1,79 @@ 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 {Object} videoData The video data 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) { + constructor (videoData, user, queue) { /** * The track name * @type {string} */ - this.name = video.title; - /** - * The full video object - * @type {SimpleYouTubeAPI.Video} - */ - this.data = video; + this.name = videoData.title /** * The Youtube URL of the track * @type {string} */ - this.url = `https://www.youtube.com/watch?v=${video.id}`; + this.url = videoData.link + /** + * The video duration (formatted). + * @type {string} + */ + this.duration = videoData.duration + /** + * The video description + * @type {string} + */ + this.description = videoData.description + /** + * The video thumbnail + * @type {string} + */ + this.thumbnail = videoData.thumbnail + /** + * The video views + */ + this.views = videoData.views + /** + * The video channel + * @type {string} + */ + this.author = videoData.author.name /** * The user who requested the track * @type {Discord.User?} */ - this.requestedBy = 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; + this.queue = queue } /** * 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) + get durationMS () { + const args = this.duration.split(':') + if (args.length === 3) { + return parseInt(args[0]) * 60 * 60 * 1000 + + parseInt(args[1]) * 60 * 1000 + + parseInt(args[2]) * 1000 + } else if(args.length === 2) { + return parseInt(args[0]) * 60 * 1000 + + parseInt(args[1]) * 1000 + } else { + return parseInt(args[0]) * 1000 + } } +} -}; - -module.exports = Track; +module.exports = Track diff --git a/src/Util.js b/src/Util.js deleted file mode 100644 index 0a92906..0000000 --- a/src/Util.js +++ /dev/null @@ -1,63 +0,0 @@ -const fetch = require('node-fetch') -const Discord = require('discord.js') -const Track = require('./Track') -const SimpleYouTubeAPI = require('simple-youtube-api') - -/** - * Utilities - */ -class Util { - - /** - * @param {SimpleYouTubeAPI.YouTube} youtube The SimpleYouTubeAPI client instance - */ - 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} - */ - search(query, user) { - return new Promise(async (resolve, reject) => { - 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;