♻️ Start rewrite
This commit is contained in:
parent
cb7d251daa
commit
50158cfe12
5 changed files with 272 additions and 550 deletions
2
index.js
2
index.js
|
@ -1,4 +1,4 @@
|
|||
module.exports = {
|
||||
version: require('./package.json').version,
|
||||
Player: require('./src/Player')
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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": {
|
||||
|
|
735
src/Player.js
735
src/Player.js
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
22
src/Queue.js
22
src/Queue.js
|
@ -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
61
src/Util.js
Normal 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
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue