♻️ Start rewrite

This commit is contained in:
Androz2091 2020-08-13 15:21:42 +02:00
parent cb7d251daa
commit 50158cfe12
5 changed files with 272 additions and 550 deletions

View file

@ -1,4 +1,4 @@
module.exports = {
version: require('./package.json').version,
Player: require('./src/Player')
};
}

View file

@ -32,7 +32,7 @@
"merge-options": "^2.0.0",
"node-fetch": "^2.6.0",
"spotify-url-info": "^1.3.1",
"ytpl": "^0.2.0",
"ytpl": "^0.2.4",
"ytsr": "^0.1.19"
},
"devDependencies": {

View file

@ -5,6 +5,8 @@ const ytpl = require('ytpl')
const spotify = require('spotify-url-info')
const Queue = require('./Queue')
const Track = require('./Track')
const Util = require('./Util')
const { EventEmitter } = require('events')
/**
* @typedef Filters
@ -54,6 +56,7 @@ const filters = {
* @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.
* @property {number} [leaveOnEmptyCooldown=0] Used when leaveOnEmpty is enabled, to let the time to users to come back in the voice channel.
*/
/**
@ -67,14 +70,20 @@ const defaultPlayerOptions = {
leaveOnEmpty: true
}
class Player {
class Player extends EventEmitter {
/**
* @param {Discord.Client} client Discord.js client
* @param {PlayerOptions} options Player options
*/
constructor (client, options = {}) {
if (!client) throw new SyntaxError('Invalid Discord client')
super()
/**
* Utilities
* @type {Util}
*/
this.util = Util
/**
* Discord.js client instance
* @type {Discord.Client}
@ -82,9 +91,9 @@ class Player {
this.client = client
/**
* Player queues
* @type {Queue[]}
* @type {Discord.Collection<Discord.Snowflake, Queue>}
*/
this.queues = []
this.queues = new Discord.Collection()
/**
* Player options
* @type {PlayerOptions}
@ -103,38 +112,90 @@ class Player {
client.on('voiceStateUpdate', (oldState, newState) => this._handleVoiceStateUpdate(oldState, newState))
}
/**
* Set the filters enabled for the guild. [Full list of the filters](https://discord-player.js.org/global.html#Filters)
* @param {Discord.Snowflake} guildID
* @param {Filters} newFilters
*
* @example
* client.on('message', async (message) => {
*
* const args = message.content.slice(settings.prefix.length).trim().split(/ +/g);
* const command = args.shift().toLowerCase();
*
* if(command === 'bassboost'){
* const bassboostEnabled = client.player.getQueue(message.guild.id).filters.bassboost;
* if(!bassboostEnabled){
* client.player.setFilters(message.guild.id, {
* bassboost: true
* });
* message.channel.send("Bassboost effect has been enabled!");
* } else {
* client.player.setFilters(message.guild.id, {
* bassboost: false
* });
* message.channel.send("Bassboost effect has been disabled!");
* }
* }
*
* });
*/
setFilters (guildID, newFilters) {
resolveQueryType (query) {
if (this.util.isSpotifyLink(query)) {
return 'spotify-song'
} else if (this.util.isYTPlaylistLink(query)) {
return 'youtube-playlist'
} else if (this.util.isYTVideoLink(query)) {
return 'youtube-video'
} else {
return 'youtube-video-keywords'
}
}
async _searchTracks (message, query) {
const tracks = []
const queryType = this.resolveQueryType(query)
if (queryType === 'spotify-song') {
const matchSpotifyURL = query.match(/https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:track\/|\?uri=spotify:track:)((\w|-){22})/)
if (matchSpotifyURL) {
const spotifyData = await spotify.getPreview(query).catch(() => {})
if (spotifyData) {
const YTQuery = `${spotifyData.artist} - ${spotifyData.track}`
const results = await ytsr(query)
if (results.items.length !== 0) {
const resultsVideo = results.items.filter((i) => i.type === 'video')
tracks.push(...resultsVideo.map((r) => new Track(r, message.author, null)))
}
}
}
} else if (queryType === 'youtube-playlist') {
const playlistID = await ytpl.getPlaylistID(query).catch(() => {})
if (playlistID) {
const playlist = await ytpl(playlistID).catch(() => {})
if (playlist) {
tracks.push(...playlist.items.map((i) => new Track({
title: i.title,
duration: i.duration,
thumbnail: i.thumbnail,
author: i.author,
link: i.url,
fromPlaylist: true
}, message.author, null)))
}
}
} else if (queryType === 'youtube-video-keywords') {
await ytsr(query).then((results) => {
if (results.items.length !== 0) {
const resultsVideo = results.items.filter((i) => i.type === 'video')
tracks.push(...resultsVideo.map((r) => new Track(r, null, null)))
}
}).catch(() => {})
}
if (tracks.length === 0) throw new Error('No tracks found for the specified query.')
let track = tracks[0]
try {
this.emit('searchResults', message, query, tracks)
const answers = await message.channel.awaitMessages(m => m.author.id === message.author.id, {
max: 1,
time: 60000,
errors: ['time']
})
const index = parseInt(answers.first().content, 10)
if (isNaN(index) || index > tracks.length || index < 1) {
this.emit('searchCancel', message)
return
}
track = tracks[index - 1]
} catch {
this.emit('searchCancel', message)
return
}
return track
}
setFilters (message, newFilters) {
return new Promise((resolve, reject) => {
// Get guild queue
const queue = this.queues.find((g) => g.guildID === guildID)
const queue = this.queues.find((g) => g.guildID === message.guild.id)
if (!queue) return reject(new Error('Not playing'))
Object.keys(newFilters).forEach((filterName) => {
queue.filters[filterName] = newFilters[filterName]
@ -143,183 +204,85 @@ class Player {
})
}
/**
* Resolve an array of tracks objects from a query string
* @param {string} query The query
* @param {boolean} allResults Whether all the results should be returned, or only the first one
* @returns {Promise<Track[]>}
*
* @example
* client.on('message', async (message) => {
*
* const args = message.content.slice(settings.prefix.length).trim().split(/ +/g);
* const command = args.shift().toLowerCase();
*
* if(command === 'play'){
* // Search for tracks
* let tracks = await client.player.searchTracks(args[0]);
* // Sends an embed with the 10 first songs
* if(tracks.length > 10) tracks = tracks.substr(0, 10);
* const embed = new Discord.MessageEmbed()
* .setDescription(tracks.map((t, i) => `**${i+1} -** ${t.name}`).join("\n"))
* .setFooter("Send the number of the track you want to play!");
* message.channel.send(embed);
* // Wait for user answer
* await message.channel.awaitMessages((m) => m.content > 0 && m.content < 10, { max: 1, time: 20000, errors: ["time"] }).then(async (answers) => {
* let index = parseInt(answers.first().content, 10);
* track = track[index-1];
* // Then play the song
* client.player.play(message.member.voice.channel, track);
* });
* }
*
* });
*/
searchTracks (query, allResults = false) {
return new Promise(async (resolve, reject) => {
if (ytpl.validateURL(query)) {
const playlistID = await ytpl.getPlaylistID(query).catch(() => {})
if (playlistID) {
const playlist = await ytpl(playlistID).catch(() => {})
if (playlist) {
return resolve(playlist.items.map((i) => new Track({
title: i.title,
duration: i.duration,
thumbnail: i.thumbnail,
author: i.author,
link: i.url,
fromPlaylist: true
}, null, null)))
}
}
}
const matchSpotifyURL = query.match(/https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:track\/|\?uri=spotify:track:)((\w|-){22})/)
if (matchSpotifyURL) {
const spotifyData = await spotify.getPreview(query).catch(e => resolve([]))
query = `${spotifyData.artist} - ${spotifyData.track}`
}
// eslint-disable-next-line no-useless-escape
const matchYoutubeURL = query.match(/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/)
if (matchYoutubeURL) {
query = matchYoutubeURL[1]
}
ytsr(query).then((results) => {
if (results.items.length < 1) return resolve([])
const resultsVideo = results.items.filter((i) => i.type === 'video')
resolve(allResults ? resultsVideo.map((r) => new Track(r, null, null)) : [new Track(resultsVideo[0], null, null)])
}).catch(() => {
return resolve([])
})
})
isPlaying (message) {
return this.queues.some((g) => g.guildID === message.guild.id)
}
/**
* 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)
_addTracksToQueue (message, tracks) {
const queue = this.getQueue(message)
if (!queue) throw new Error('Cannot add tracks to queue because no song is currently played on the server.')
queue.tracks = queue.tracks.concat(tracks)
return queue
}
/**
* 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 {any} The played content
*
* @example
* client.on('message', async (message) => {
*
* const args = message.content.slice(settings.prefix.length).trim().split(/ +/g);
* const command = args.shift().toLowerCase();
*
* // !play Despacito
* // will play "Despacito" in the member voice channel
*
* if(command === 'play'){
* const result = await client.player.play(message.member.voice.channel, args.join(" "));
* if(result.type === 'playlist'){
* message.channel.send(`${result.tracks.length} songs added to the queue!\nCurrently playing **${result.tracks[0].name}**...`);
* } else {
* message.channel.send(`Currently playing ${result.name}...`);
* }
* }
*
* });
*/
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(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()
// Create a new guild with data
const queue = new Queue(voiceChannel.guild.id)
queue.voiceConnection = connection
queue.filters = {}
Object.keys(this.filters).forEach((f) => {
queue.filters[f] = false
})
let result = null
if (typeof track === 'object') {
track.requestedBy = user
result = track
// Add the track to the queue
_createQueue (message, track) {
return new Promise((resolve, reject) => {
const channel = message.member.voice ? message.member.voice.channel : null
if (!channel) reject('NotConnected')
const queue = new Queue(message.guild.id, message, this.filters)
this.queues.set(message.guild.id, queue)
channel.join().then((connection) => {
queue.voiceConnection = connection
queue.tracks.push(track)
} else if (typeof track === 'string') {
const results = await this.searchTracks(track).catch(() => {
return reject(new Error('Not found'))
})
if (!results) return
if (results.length > 1) {
result = {
type: 'playlist',
tracks: results
}
} else if (results[0]) {
result = results[0]
} else {
return reject(new Error('Not found'))
}
results.forEach((i) => {
i.requestedBy = user
queue.tracks.push(i)
})
}
// Add the queue to the list
this.queues.push(queue)
// Play the track
this._playTrack(queue.guildID, true)
// Resolve the track
resolve(result)
this.emit('queueCreate', message, queue)
this._playTrack(message.guild.id, true)
}).catch((err) => {
console.error(err)
this.queues.delete(message.guild.id)
reject('UnableToJoin')
})
})
}
/**
* 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
*
* @example
* client.on('message', async (message) => {
*
* const args = message.content.slice(settings.prefix.length).trim().split(/ +/g);
* const command = args.shift().toLowerCase();
*
* if(command === 'pause'){
* const track = await client.player.pause(message.guild.id);
* message.channel.send(`${track.name} paused!`);
* }
*
* });
*/
pause (guildID) {
async _handlePlaylist (message, query) {
const playlist = await ytpl(query).catch(() => {})
if (!playlist) return this.emit('noResults', message, query)
playlist.tracks = playlist.items.map((item) => new Track(item, message.author))
playlist.duration = playlist.tracks.reduce((prev, next) => prev + next.duration, 0)
playlist.thumbnail = playlist.tracks[0].thumbnail
playlist.requestedBy = message.author
if (this.isPlaying(message)) {
const queue = this._addTracksToQueue(message, playlist.tracks)
this.emit('addList', message, queue, playlist)
} else {
const track = new Track(playlist.tracks.shift(), message.author)
const queue = await this._createQueue(message, track).catch((e) => this.emit('error', message, e))
this._addTracksToQueue(message, playlist.tracks)
this.emit('playlistStart', message, queue, playlist, queue.tracks[0])
}
}
async play (message, query) {
const isPlaying = this.isPlaying(message)
if (!isPlaying) {
if (this.util.isYTPlaylistLink(query)) {
this._handlePlaylist(message, query)
}
let trackToPlay
if (query instanceof Track) {
trackToPlay = query
} else if (this.util.isYTVideoLink(query)) {
const videoData = await ytdl.getBasicInfo(query)
trackToPlay = new Track(videoData, message.author)
} else {
trackToPlay = await this._searchTracks(message, query)
}
if (trackToPlay) {
if (this.isPlaying(message)) {
const queue = this._addToQueue(message, trackToPlay)
this.emit('addSong', message, queue, queue.tracks[queue.tracks.length - 1])
} else {
const queue = await this._createQueue(message, trackToPlay)
this.emit('playSong', message, queue, queue.tracks[0])
}
}
}
}
pause (message) {
return new Promise((resolve, reject) => {
// Get guild queue
const queue = this.queues.find((g) => g.guildID === guildID)
const queue = this.queues.find((g) => g.guildID === message.guild.id)
if (!queue) return reject(new Error('Not playing'))
// Pause the dispatcher
queue.voiceConnection.dispatcher.pause()
@ -329,28 +292,10 @@ class Player {
})
}
/**
* 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
*
* @example
* client.on('message', async (message) => {
*
* const args = message.content.slice(settings.prefix.length).trim().split(/ +/g);
* const command = args.shift().toLowerCase();
*
* if(command === 'resume'){
* const track = await client.player.resume(message.guild.id);
* message.channel.send(`${track.name} resumed!`);
* }
*
* });
*/
resume (guildID) {
resume (message) {
return new Promise((resolve, reject) => {
// Get guild queue
const queue = this.queues.find((g) => g.guildID === guildID)
const queue = this.queues.find((g) => g.guildID === message.guild.id)
if (!queue) return reject(new Error('Not playing'))
// Pause the dispatcher
queue.voiceConnection.dispatcher.resume()
@ -360,58 +305,23 @@ class Player {
})
}
/**
* Stop the music in the guild
* @param {Discord.Snowflake} guildID The ID of the guild where the music should be stopped
* @returns {Promise<void>}
*
* @example
* client.on('message', (message) => {
*
* const args = message.content.slice(settings.prefix.length).trim().split(/ +/g);
* const command = args.shift().toLowerCase();
*
* if(command === 'stop'){
* client.player.stop(message.guild.id);
* message.channel.send('Music stopped!');
* }
*
* });
*/
stop (guildID) {
stop (message) {
return new Promise((resolve, reject) => {
// Get guild queue
const queue = this.queues.find((g) => g.guildID === guildID)
const queue = this.queues.find((g) => g.guildID === message.guild.id)
if (!queue) return reject(new Error('Not playing'))
// Stop the dispatcher
queue.stopped = true
queue.tracks = []
if (queue.stream) queue.stream.destroy()
queue.voiceConnection.dispatcher.end()
if (this.options.leaveOnStop) queue.voiceConnection.channel.leave()
this.queues.delete(message.guild.id)
// Resolve
resolve()
})
}
/**
* Update the volume
* @param {Discord.Snowflake} guildID The ID of the guild where the music should be modified
* @param {number} percent The new volume (0-100)
* @returns {Promise<void>}
*
* @example
* client.on('message', (message) => {
*
* const args = message.content.slice(settings.prefix.length).trim().split(/ +/g);
* const command = args.shift().toLowerCase();
*
* if(command === 'set-volume'){
* client.player.setVolume(message.guild.id, parseInt(args[0]));
* message.channel.send(`Volume set to ${args[0]} !`);
* }
*
* });
*/
setVolume (guildID, percent) {
return new Promise((resolve, reject) => {
// Get guild queue
@ -425,130 +335,12 @@ class Player {
})
}
/**
* Get a guild queue
* @param {Discord.Snowflake} guildID
* @returns {?Queue}
*
* @example
* client.on('message', (message) => {
*
* const args = message.content.slice(settings.prefix.length).trim().split(/ +/g);
* const command = args.shift().toLowerCase();
*
* if(command === 'queue'){
* const queue = await client.player.getQueue(message.guild.id);
* message.channel.send('Server queue:\n'+(queue.tracks.map((track, i) => {
* return `${i === 0 ? 'Current' : `#${i+1}`} - ${track.name} | ${track.author}`;
* }).join('\n')));
* }
*
* // Output:
*
* // Server queue:
* // Current - Despacito | Luis Fonsi
* // #2 - Memories | Maroon 5
* // #3 - Dance Monkey | Tones And I
* // #4 - Circles | Post Malone
* });
*/
getQueue (guildID) {
getQueue (message) {
// Gets guild queue
const queue = this.queues.find((g) => g.guildID === guildID)
const queue = this.queues.find((g) => g.guildID === message.guild.id)
return queue
}
/**
* Add a track to the guild queue
* @param {Discord.Snowflake} guildID The ID of the guild where the track should be added
* @param {Track|string} trackName The name of the track to add to the queue
* @param {Discord.User?} user The user who requested the track
* @returns {any} The content added to the queue
*
* @example
* client.on('message', async (message) => {
*
* const args = message.content.slice(settings.prefix.length).trim().split(/ +/g);
* const command = args.shift().toLowerCase();
*
* if(command === 'play'){
* let trackPlaying = client.player.isPlaying(message.guild.id);
* // If there's already a track being played
* if(trackPlaying){
* const result = await client.player.addToQueue(message.guild.id, args.join(" "));
* if(result.type === 'playlist'){
* message.channel.send(`${result.tracks.length} songs added to the queue!`);
* } else {
* message.channel.send(`${result.name} added to the queue!`);
* }
* } else {
* // Else, play the track
* const result = await client.player.addToQueue(message.member.voice.channel, args[0]);
* if(result.type === 'playlist'){
* message.channel.send(`${result.tracks.length} songs added to the queue\nCurrently playing **${result.tracks[0].name}**!`);
* } else {
* message.channel.send(`Currently playing ${result.name}`);
* }
* }
* }
*
* });
*/
addToQueue (guildID, track, user) {
return new Promise(async (resolve, reject) => {
// Get guild queue
const queue = this.queues.find((g) => g.guildID === guildID)
if (!queue) return reject(new Error('Not playing'))
// Search the track
let result = null
if (typeof track === 'object') {
track.requestedBy = user
result = track
// Add the track to the queue
queue.tracks.push(track)
} else if (typeof track === 'string') {
const results = await this.searchTracks(track).catch(() => {
return reject(new Error('Not found'))
})
if (!results) return
if (results.length > 1) {
result = {
type: 'playlist',
tracks: results
}
} else if (results[0]) {
result = results[0]
} else {
return reject(new Error('Not found'))
}
results.forEach((i) => {
i.requestedBy = user
queue.tracks.push(i)
})
}
// Resolve the result
resolve(result)
})
}
/**
* Clear the guild queue, except the current track
* @param {Discord.Snowflake} guildID The ID of the guild where the queue should be cleared
* @returns {Promise<Queue>} The updated queue
*
* @example
* client.on('message', (message) => {
*
* const args = message.content.slice(settings.prefix.length).trim().split(/ +/g);
* const command = args.shift().toLowerCase();
*
* if(command === 'clear-queue'){
* client.player.clearQueue(message.guild.id);
* message.channel.send('Queue cleared!');
* }
*
* });
*/
clearQueue (guildID) {
return new Promise((resolve, reject) => {
// Get guild queue
@ -561,24 +353,6 @@ class Player {
})
}
/**
* Skip a track
* @param {Discord.Snowflake} guildID The ID of the guild where the track should be skipped
* @returns {Promise<Track>}
*
* @example
* client.on('message', async (message) => {
*
* const args = message.content.slice(settings.prefix.length).trim().split(/ +/g);
* const command = args.shift().toLowerCase();
*
* if(command === 'skip'){
* const track = await client.player.skip(message.guild.id);
* message.channel.send(`${track.name} skipped!`);
* }
*
* });
*/
skip (guildID) {
return new Promise((resolve, reject) => {
// Get guild queue
@ -593,62 +367,17 @@ class Player {
})
}
/**
* Get the currently playing track
* @param {Discord.Snowflake} guildID
* @returns {Promise<Track>} The track which is currently played
*
* @example
* client.on('message', async (message) => {
*
* const args = message.content.slice(settings.prefix.length).trim().split(/ +/g);
* const command = args.shift().toLowerCase();
*
* if(command === 'now-playing'){
* let track = await client.player.nowPlaying(message.guild.id);
* message.channel.send(`Currently playing ${track.name}...`);
* }
*
* });
*/
nowPlaying (guildID) {
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'))
const currentTrack = queue.playing
const currentTrack = queue.tracks[0]
// Resolve the current track
resolve(currentTrack)
})
}
/**
* Enable or disable the repeat mode
* @param {Discord.Snowflake} guildID
* @param {Boolean} enabled Whether the repeat mode should be enabled
* @returns {Promise<Void>}
*
* @example
* client.on('message', async (message) => {
*
* const args = message.content.slice(settings.prefix.length).trim().split(/ +/g);
* const command = args.shift().toLowerCase();
*
* if(command === 'repeat-mode'){
* const repeatModeEnabled = client.player.getQueue(message.guild.id).repeatMode;
* if(repeatModeEnabled){
* // if the repeat mode is currently enabled, disable it
* client.player.setRepeatMode(message.guild.id, false);
* message.channel.send("Repeat mode disabled! The current song will no longer be played again and again...");
* } else {
* // if the repeat mode is currently disabled, enable it
* client.player.setRepeatMode(message.guild.id, true);
* message.channel.send("Repeat mode enabled! The current song will be played again and again until you run the command again!");
* }
* }
*
* });
*/
setRepeatMode (guildID, enabled) {
return new Promise((resolve, reject) => {
// Get guild queue
@ -661,26 +390,6 @@ class Player {
})
}
/**
* 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
*
* @example
* client.on('message', async (message) => {
*
* const args = message.content.slice(settings.prefix.length).trim().split(/ +/g);
* const command = args.shift().toLowerCase();
*
* if(command === 'shuffle'){
* // Shuffle the server queue
* client.player.shuffle(message.guild.id).then(() => {
* message.channel.send('Queue shuffled!');
* });
* }
*
* });
*/
shuffle (guildID) {
return new Promise((resolve, reject) => {
// Get guild queue
@ -695,27 +404,6 @@ class Player {
})
}
/**
* 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>}
*
* @example
* client.on('message', async (message) => {
*
* const args = message.content.slice(settings.prefix.length).trim().split(/ +/g);
* const command = args.shift().toLowerCase();
*
* if(command === 'remove'){
* // Remove a track from the queue
* client.player.remove(message.guild.id, args[0]).then(() => {
* message.channel.send('Removed track!');
* });
* }
*
* });
*/
remove (guildID, track) {
return new Promise((resolve, reject) => {
// Gets guild queue
@ -739,25 +427,6 @@ class Player {
})
}
/**
* Creates progress bar of the current song
* @param {Discord.Snowflake} guildID
* @returns {String}
*
* @example
* client.on('message', async (message) => {
*
* const args = message.content.slice(settings.prefix.length).trim().split(/ +/g);
* const command = args.shift().toLowerCase();
*
* if(command === 'now-playing'){
* client.player.nowPlaying(message.guild.id).then((song) => {
* message.channel.send('Currently playing ' + song.name + '\n\n'+ client.player.createProgressBar(message.guild.id));
* });
* }
*
* });
*/
createProgressBar (guildID) {
// Gets guild queue
const queue = this.queues.find((g) => g.guildID === guildID)
@ -767,7 +436,7 @@ class Player {
? queue.voiceConnection.dispatcher.streamTime + queue.additionalStreamTime
: 0
// Total stream time
const totalTime = queue.playing.durationMS
const totalTime = queue.tracks[0].durationMS
// Stream progress
const index = Math.round((currentStreamTime / totalTime) * 15)
// conditions
@ -780,14 +449,8 @@ class Player {
}
}
/**
* Handle the voice state update event
* @ignore
* @private
* @param {Discord.VoiceState} oldState
* @param {Discord.VoiceState} newState
*/
_handleVoiceStateUpdate (oldState, newState) {
const isEmpty = (channel) => (channel.members.filter((member) => !member.user.bot)).size === 0
if (!this.options.leaveOnEmpty) return
// If the member leaves a voice channel
if (!oldState.channelID || newState.channelID) return
@ -795,25 +458,19 @@ class Player {
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
// 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')
if (!isEmpty(queue.voiceConnection.channel)) return
setTimeout(() => {
// Disconnect from the voice channel
queue.voiceConnection.channel.leave()
// Delete the queue
this.queues.delete(queue.guildID)
// Emit end event
queue.emit('channelEmpty', queue.firstMessage, queue)
}, this.options.leaveOnEmptyCooldown ?? 0)
}
}
/**
* Play a stream in a channel
* @ignore
* @private
* @param {Queue} queue The queue to play
* @param {Boolean} updateFilter Whether this method is called to update some ffmpeg filters
* @returns {Promise<void>}
*/
_playYTDLStream (queue, updateFilter) {
_playYTDLStream (track, queue, updateFilter) {
return new Promise((resolve) => {
const seekTime = updateFilter ? queue.voiceConnection.dispatcher.streamTime + queue.additionalStreamTime : undefined
const encoderArgsFilters = []
@ -828,7 +485,7 @@ class Player {
} else {
encoderArgs = ['-af', encoderArgsFilters.join(',')]
}
const newStream = ytdl(queue.playing.url, {
const newStream = ytdl(track.url, {
filter: 'audioonly',
opusEncoded: true,
encoderArgs,
@ -861,38 +518,32 @@ class Player {
})
}
/**
* 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 _playTrack (guildID, firstPlay) {
// Get guild queue
const queue = this.queues.find((g) => g.guildID === guildID)
if (queue.stopped) return
// If there isn't any music in the queue
if (queue.tracks.length < 1 && !firstPlay && !queue.repeatMode) {
if (queue.tracks.length === 0) {
// 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)
this.queues.delete(guildID)
// Emit stop event
if (queue.stopped) {
if (this.options.leaveOnStop) queue.voiceConnection.channel.leave()
return queue.emit('stop')
}
// Emit end event
return queue.emit('end')
}
const wasPlaying = queue.playing
const nowPlaying = queue.playing = queue.repeatMode ? wasPlaying : queue.tracks.shift()
// if the track needs to be the next one
if (!queue.repeatMode && !firstPlay) queue.tracks.shift()
const track = queue.playing
// Reset lastSkipped state
queue.lastSkipped = false
this._playYTDLStream(queue, false).then(() => {
this._playYTDLStream(track, queue, false).then(() => {
// Emit trackChanged event
if (!firstPlay) {
queue.emit('trackChanged', wasPlaying, nowPlaying, queue.lastSkipped, queue.repeatMode)
queue.emit('trackStart', queue.firstMessage, track, queue)
}
})
}

View file

@ -1,6 +1,7 @@
const Discord = require('discord.js')
const { EventEmitter } = require('events')
const Track = require('./Track')
const Player = require('./Player')
/**
* Represents a guild queue.
@ -8,8 +9,10 @@ const Track = require('./Track')
class Queue extends EventEmitter {
/**
* @param {Discord.Snowflake} guildID ID of the guild this queue is for.
* @param {Discord.Message} message Message that initialized the queue
* @param {import('./Player').Filters[]} filters Filters the queue should be initialized with.
*/
constructor (guildID) {
constructor (guildID, message, filters) {
super()
/**
* ID of the guild this queue is for.
@ -21,11 +24,6 @@ class Queue extends EventEmitter {
* @type {Discord.VoiceConnection}
*/
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[]}
@ -61,11 +59,23 @@ class Queue extends EventEmitter {
* @type {Filters}
*/
this.filters = {}
Object.keys(filters).forEach((f) => {
this.filters[f] = false
})
/**
* Additional stream time
* @type {Number}
*/
this.additionalStreamTime = 0
/**
* Message that initialized the queue
* @type {Discord.Message}
*/
this.firstMessage = message
}
get playing () {
return this.tracks[0]
}
get calculatedVolume () {

61
src/Util.js Normal file
View file

@ -0,0 +1,61 @@
const ytpl = require('ytpl')
const Discord = require('discord.js')
const youtubeVideoRegex = (/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/)
const spotifySongRegex = (/https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:track\/|\?uri=spotify:track:)((\w|-){22})/)
module.exports = class Util {
constructor () {
throw new Error(`The ${this.constructor.name} class may not be instantiated.`)
}
static isSpotifyLink (query) {
return spotifySongRegex.test(query)
}
static isYTPlaylistLink (query) {
return ytpl.validateURL(query)
}
static isYTVideoLink (query) {
return youtubeVideoRegex.test(query)
}
/**
* Sends a selection embed in a channel and await for the member's answer
* @param {Discord.User} user The user able to choose the song
* @param {Discord.TextChannel} channel The channel in which the selection embed will be sent
* @param {Tracks[]} tracks The tracks the selection embed should contain
* @param {Object} options
* @param {number} options.trackCount The number of tracks to show on the embed
* @param {Function} options.formatTrack Function use to map songs in the selection embed
* @param {String} options.embedColor Color of the selection embed
* @param {String} options.embedFooter Text of the footer of the selection embed
* @param {number} options.collectorTimeout Number of time before the bot cancels the selection and send the collectorTimeoutMessage message
* @param {number} options.collectorTimeoutMessage Message sent when the selection time has expired
* @param {Function} options.embedCallback Function called to allow users to edit the selection embed
* @returns {Promise<Track>}
*/
static async awaitSelection (user, channel, tracks, { trackCount, formatTrack, embedColor, embedFooter, collectorTimeout, collectorTimeoutMessage, embedCallback }) {
if (trackCount) tracks.splice(trackCount)
formatTrack = formatTrack || ((track, index) => `${++index} - ${track.name}`)
const embed = new Discord.MessageEmbed()
.setDescription(tracks.map(formatTrack))
.setColor(embedColor || 'RED')
.setFooter(embedFooter || 'Please send the number of the track you would like to listen.')
await channel.send(embed)
const collected = await channel.awaitMessages((message) => message.author.id === user.id && !isNaN(message.content) && parseInt(message.content) > 0 && parseInt(message.content) < tracks, {
time: collectorTimeout,
errors: ['time']
}).catch((reason) => {
channel.send(collectorTimeoutMessage)
embedCallback(embed, null)
return null
})
if (!collected) return null
const index = parseInt(collected.first().content) - 1
const track = tracks[index]
embedCallback(embed, track)
return track
}
}