2020-06-21 18:36:16 +05:00
|
|
|
const ytdl = require('discord-ytdl-core')
|
|
|
|
const Discord = require('discord.js')
|
|
|
|
const ytsr = require('ytsr')
|
2020-01-12 00:01:18 +05:00
|
|
|
|
2020-06-21 18:36:16 +05:00
|
|
|
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'
|
|
|
|
}
|
2020-01-12 00:01:18 +05:00
|
|
|
|
|
|
|
/**
|
2020-06-02 16:11:12 +05:00
|
|
|
* @typedef PlayerOptions
|
|
|
|
* @property {boolean} [leaveOnEnd=true] Whether the bot should leave the current voice channel when the queue ends.
|
|
|
|
* @property {boolean} [leaveOnStop=true] Whether the bot should leave the current voice channel when the stop() function is used.
|
|
|
|
* @property {boolean} [leaveOnEmpty=true] Whether the bot should leave the voice channel if there is no more member in it.
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Default options for the player
|
|
|
|
* @ignore
|
|
|
|
* @type {PlayerOptions}
|
|
|
|
*/
|
|
|
|
const defaultPlayerOptions = {
|
2020-01-12 00:01:18 +05:00
|
|
|
leaveOnEnd: true,
|
2020-01-12 22:55:37 +05:00
|
|
|
leaveOnStop: true,
|
|
|
|
leaveOnEmpty: true
|
2020-06-21 18:36:16 +05:00
|
|
|
}
|
2020-01-12 00:01:18 +05:00
|
|
|
|
|
|
|
class Player {
|
|
|
|
/**
|
2020-06-02 16:11:12 +05:00
|
|
|
* @param {Discord.Client} client Discord.js client
|
|
|
|
* @param {PlayerOptions} options Player options
|
2020-01-12 00:01:18 +05:00
|
|
|
*/
|
2020-06-21 18:36:16 +05:00
|
|
|
constructor (client, options = {}) {
|
|
|
|
if (!client) throw new SyntaxError('Invalid Discord client')
|
2020-06-02 16:11:12 +05:00
|
|
|
|
2020-01-12 00:01:18 +05:00
|
|
|
/**
|
2020-06-02 16:11:12 +05:00
|
|
|
* Discord.js client instance
|
|
|
|
* @type {Discord.Client}
|
2020-01-12 00:01:18 +05:00
|
|
|
*/
|
2020-06-21 18:36:16 +05:00
|
|
|
this.client = client
|
2020-01-12 00:01:18 +05:00
|
|
|
/**
|
2020-06-02 16:11:12 +05:00
|
|
|
* Player queues
|
2020-01-12 00:01:18 +05:00
|
|
|
* @type {Queue[]}
|
|
|
|
*/
|
2020-06-21 18:36:16 +05:00
|
|
|
this.queues = []
|
2020-01-12 00:01:18 +05:00
|
|
|
/**
|
2020-06-02 16:11:12 +05:00
|
|
|
* Player options
|
2020-01-12 00:01:18 +05:00
|
|
|
* @type {PlayerOptions}
|
|
|
|
*/
|
2020-06-21 18:36:16 +05:00
|
|
|
this.options = defaultPlayerOptions
|
|
|
|
for (const prop in options) {
|
|
|
|
this.options[prop] = options[prop]
|
2020-06-02 16:11:12 +05:00
|
|
|
}
|
2020-01-12 22:55:37 +05:00
|
|
|
|
|
|
|
// Listener to check if the channel is empty
|
2020-06-21 18:36:16 +05:00
|
|
|
client.on('voiceStateUpdate', (oldState, newState) => this._handleVoiceStateUpdate(oldState, newState))
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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<Track[]>}
|
|
|
|
*/
|
|
|
|
searchTracks (query) {
|
|
|
|
return new Promise(async (resolve, reject) => {
|
2020-06-21 20:17:24 +05:00
|
|
|
// eslint-disable-next-line no-useless-escape
|
|
|
|
const matchURL = query.match(/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/)
|
|
|
|
if (matchURL) {
|
|
|
|
query = matchURL[1]
|
2020-06-21 18:36:16 +05:00
|
|
|
}
|
|
|
|
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)))
|
|
|
|
})
|
|
|
|
})
|
2020-01-12 00:01:18 +05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-06-02 16:11:12 +05:00
|
|
|
* 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
|
2020-01-12 00:01:18 +05:00
|
|
|
*/
|
2020-06-21 18:36:16 +05:00
|
|
|
isPlaying (guildID) {
|
|
|
|
return this.queues.some((g) => g.guildID === guildID)
|
2020-01-12 00:01:18 +05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-06-02 16:11:12 +05:00
|
|
|
* Play a track in a voice channel
|
|
|
|
* @param {Discord.VoiceChannel} voiceChannel The voice channel in which the track will be played
|
|
|
|
* @param {Track|string} track The name of the track to play
|
|
|
|
* @param {Discord.User?} user The user who requested the track
|
|
|
|
* @returns {Promise<Track>} The played track
|
2020-01-12 00:01:18 +05:00
|
|
|
*/
|
2020-06-21 18:36:16 +05:00
|
|
|
play (voiceChannel, track, user) {
|
|
|
|
this.queues = this.queues.filter((g) => g.guildID !== voiceChannel.id)
|
2020-01-12 00:01:18 +05:00
|
|
|
return new Promise(async (resolve, reject) => {
|
2020-06-21 18:36:16 +05:00
|
|
|
if (!voiceChannel || typeof voiceChannel !== 'object') {
|
|
|
|
return reject(new Error(`voiceChannel must be type of VoiceChannel. value=${voiceChannel}`))
|
2020-06-02 16:11:12 +05:00
|
|
|
}
|
2020-06-21 18:36:16 +05:00
|
|
|
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]
|
2020-06-02 16:11:12 +05:00
|
|
|
}
|
|
|
|
// Create a new guild with data
|
2020-06-21 18:36:16 +05:00
|
|
|
const queue = new Queue(voiceChannel.guild.id)
|
|
|
|
queue.voiceConnection = connection
|
|
|
|
queue.filters = {}
|
|
|
|
Object.keys(filters).forEach((f) => {
|
|
|
|
queue.filters[f] = false
|
|
|
|
})
|
2020-06-02 16:11:12 +05:00
|
|
|
// Add the track to the queue
|
2020-06-21 18:36:16 +05:00
|
|
|
track.requestedBy = user
|
|
|
|
queue.tracks.push(track)
|
2020-01-12 00:01:18 +05:00
|
|
|
// Add the queue to the list
|
2020-06-21 18:36:16 +05:00
|
|
|
this.queues.push(queue)
|
2020-06-02 16:11:12 +05:00
|
|
|
// Play the track
|
2020-06-21 18:36:16 +05:00
|
|
|
this._playTrack(queue.guildID, true)
|
2020-06-02 16:11:12 +05:00
|
|
|
// Resolve the track
|
2020-06-21 18:36:16 +05:00
|
|
|
resolve(track)
|
|
|
|
})
|
2020-01-12 00:01:18 +05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-06-02 16:11:12 +05:00
|
|
|
* 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
|
2020-01-12 00:01:18 +05:00
|
|
|
*/
|
2020-06-02 16:11:12 +05:00
|
|
|
pause (guildID) {
|
2020-06-21 18:36:16 +05:00
|
|
|
return new Promise((resolve, reject) => {
|
2020-01-12 00:01:18 +05:00
|
|
|
// Gets guild queue
|
2020-06-21 18:36:16 +05:00
|
|
|
const queue = this.queues.find((g) => g.guildID === guildID)
|
|
|
|
if (!queue) return reject(new Error('Not playing'))
|
2020-01-12 00:01:18 +05:00
|
|
|
// Pauses the dispatcher
|
2020-06-21 18:36:16 +05:00
|
|
|
queue.voiceConnection.dispatcher.pause()
|
|
|
|
queue.paused = true
|
2020-01-12 00:01:18 +05:00
|
|
|
// Resolves the guild queue
|
2020-06-21 18:36:16 +05:00
|
|
|
resolve(queue.tracks[0])
|
|
|
|
})
|
2020-01-12 00:01:18 +05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-06-02 16:11:12 +05:00
|
|
|
* 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
|
2020-01-12 00:01:18 +05:00
|
|
|
*/
|
2020-06-02 16:11:12 +05:00
|
|
|
resume (guildID) {
|
2020-06-21 18:36:16 +05:00
|
|
|
return new Promise((resolve, reject) => {
|
2020-06-02 16:11:12 +05:00
|
|
|
// Get guild queue
|
2020-06-21 18:36:16 +05:00
|
|
|
const queue = this.queues.find((g) => g.guildID === guildID)
|
|
|
|
if (!queue) return reject(new Error('Not playing'))
|
2020-06-02 16:11:12 +05:00
|
|
|
// Pause the dispatcher
|
2020-06-21 18:36:16 +05:00
|
|
|
queue.voiceConnection.dispatcher.resume()
|
|
|
|
queue.paused = false
|
2020-06-02 16:11:12 +05:00
|
|
|
// Resolve the guild queue
|
2020-06-21 18:36:16 +05:00
|
|
|
resolve(queue.tracks[0])
|
|
|
|
})
|
2020-01-12 00:01:18 +05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Stops playing music.
|
2020-06-02 16:11:12 +05:00
|
|
|
* @param {Discord.Snowflake} guildID The ID of the guild where the music should be stopped
|
2020-01-12 00:01:18 +05:00
|
|
|
* @returns {Promise<void>}
|
|
|
|
*/
|
2020-06-21 18:36:16 +05:00
|
|
|
stop (guildID) {
|
|
|
|
return new Promise((resolve, reject) => {
|
2020-06-02 16:11:12 +05:00
|
|
|
// Get guild queue
|
2020-06-21 18:36:16 +05:00
|
|
|
const queue = this.queues.find((g) => g.guildID === guildID)
|
|
|
|
if (!queue) return reject(new Error('Not playing'))
|
2020-06-02 16:11:12 +05:00
|
|
|
// Stop the dispatcher
|
2020-06-21 18:36:16 +05:00
|
|
|
queue.stopped = true
|
|
|
|
queue.tracks = []
|
|
|
|
queue.voiceConnection.dispatcher.end()
|
2020-06-02 16:11:12 +05:00
|
|
|
// Resolve
|
2020-06-21 18:36:16 +05:00
|
|
|
resolve()
|
|
|
|
})
|
2020-01-12 00:01:18 +05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-06-02 16:11:12 +05:00
|
|
|
* Update the volume
|
2020-06-21 18:36:16 +05:00
|
|
|
* @param {Discord.Snowflake} guildID The ID of the guild where the music should be modified
|
2020-06-02 16:11:12 +05:00
|
|
|
* @param {number} percent The new volume (0-100)
|
2020-01-12 00:01:18 +05:00
|
|
|
* @returns {Promise<void>}
|
|
|
|
*/
|
2020-06-21 18:36:16 +05:00
|
|
|
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'))
|
2020-01-12 00:01:18 +05:00
|
|
|
// Updates volume
|
2020-06-21 18:36:16 +05:00
|
|
|
queue.voiceConnection.dispatcher.setVolumeLogarithmic(percent / 200)
|
|
|
|
queue.volume = percent
|
2020-01-12 00:01:18 +05:00
|
|
|
// Resolves guild queue
|
2020-06-21 18:36:16 +05:00
|
|
|
resolve()
|
|
|
|
})
|
2020-01-12 00:01:18 +05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-06-02 16:11:12 +05:00
|
|
|
* Get a guild queue
|
|
|
|
* @param {Discord.Snowflake} guildID
|
2020-01-18 22:29:53 +05:00
|
|
|
* @returns {?Queue}
|
2020-01-12 00:01:18 +05:00
|
|
|
*/
|
2020-06-21 18:36:16 +05:00
|
|
|
getQueue (guildID) {
|
2020-01-18 22:29:53 +05:00
|
|
|
// Gets guild queue
|
2020-06-21 18:36:16 +05:00
|
|
|
const queue = this.queues.find((g) => g.guildID === guildID)
|
|
|
|
return queue
|
2020-01-12 00:01:18 +05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-06-02 16:11:12 +05:00
|
|
|
* Add a track to the guild queue
|
|
|
|
* @param {Discord.Snowflake} guildID The ID of the guild where the track should be added
|
|
|
|
* @param {string} trackName The name of the track to add to the queue
|
|
|
|
* @param {Discord.User?} requestedBy The user who requested the track
|
|
|
|
* @returns {Promise<Track>} The added track
|
2020-01-12 00:01:18 +05:00
|
|
|
*/
|
2020-06-21 18:36:16 +05:00
|
|
|
addToQueue (guildID, trackName, requestedBy) {
|
|
|
|
return new Promise((resolve, reject) => {
|
2020-06-02 16:11:12 +05:00
|
|
|
// Get guild queue
|
2020-06-21 18:36:16 +05:00
|
|
|
const queue = this.queues.find((g) => g.guildID === guildID)
|
|
|
|
if (!queue) return reject(new Error('Not playing'))
|
2020-06-02 16:11:12 +05:00
|
|
|
// Search the track
|
2020-06-21 18:36:16 +05:00
|
|
|
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'))
|
|
|
|
})
|
|
|
|
})
|
2020-01-12 00:01:18 +05:00
|
|
|
}
|
|
|
|
|
2020-01-18 17:04:05 +05:00
|
|
|
/**
|
2020-06-02 16:11:12 +05:00
|
|
|
* Set the queue for a guild.
|
|
|
|
* @param {Discord.Snowflake} guildID The ID of the guild where the queue should be set
|
|
|
|
* @param {Track[]} tracks The tracks list
|
|
|
|
* @returns {Promise<Queue>} The new queue
|
2020-01-18 17:04:05 +05:00
|
|
|
*/
|
2020-06-21 18:36:16 +05:00
|
|
|
setQueue (guildID, tracks) {
|
|
|
|
return new Promise((resolve, reject) => {
|
2020-06-02 16:11:12 +05:00
|
|
|
// Get guild queue
|
2020-06-21 18:36:16 +05:00
|
|
|
const queue = this.queues.find((g) => g.guildID === guildID)
|
|
|
|
if (!queue) return reject(new Error('Not playing'))
|
2020-06-02 16:11:12 +05:00
|
|
|
// Update queue
|
2020-06-21 18:36:16 +05:00
|
|
|
queue.tracks = tracks
|
2020-06-02 16:11:12 +05:00
|
|
|
// Resolve the queue
|
2020-06-21 18:36:16 +05:00
|
|
|
resolve(queue)
|
|
|
|
})
|
2020-01-18 17:04:05 +05:00
|
|
|
}
|
|
|
|
|
2020-01-12 00:01:18 +05:00
|
|
|
/**
|
2020-06-02 16:11:12 +05:00
|
|
|
* Clear the guild queue, but not the current track
|
|
|
|
* @param {Discord.Snowflake} guildID The ID of the guild where the queue should be cleared
|
|
|
|
* @returns {Promise<Queue>} The updated queue
|
2020-01-12 00:01:18 +05:00
|
|
|
*/
|
2020-06-21 18:36:16 +05:00
|
|
|
clearQueue (guildID) {
|
|
|
|
return new Promise((resolve, reject) => {
|
2020-06-02 16:11:12 +05:00
|
|
|
// Get guild queue
|
2020-06-21 18:36:16 +05:00
|
|
|
const queue = this.queues.find((g) => g.guildID === guildID)
|
|
|
|
if (!queue) return reject(new Error('Not playing'))
|
2020-06-02 16:11:12 +05:00
|
|
|
// Clear queue
|
2020-06-21 18:36:16 +05:00
|
|
|
const currentlyPlaying = queue.tracks.shift()
|
|
|
|
queue.tracks = [currentlyPlaying]
|
2020-06-02 16:11:12 +05:00
|
|
|
// Resolve guild queue
|
2020-06-21 18:36:16 +05:00
|
|
|
resolve(queue)
|
|
|
|
})
|
2020-01-12 00:01:18 +05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-06-02 16:11:12 +05:00
|
|
|
* Skip a track
|
|
|
|
* @param {Discord.Snowflake} guildID The ID of the guild where the track should be skipped
|
|
|
|
* @returns {Promise<Track>}
|
2020-01-12 00:01:18 +05:00
|
|
|
*/
|
2020-06-21 18:36:16 +05:00
|
|
|
skip (guildID) {
|
|
|
|
return new Promise((resolve, reject) => {
|
2020-06-02 16:11:12 +05:00
|
|
|
// Get guild queue
|
2020-06-21 18:36:16 +05:00
|
|
|
const queue = this.queues.find((g) => g.guildID === guildID)
|
|
|
|
if (!queue) return reject(new Error('Not playing'))
|
|
|
|
const currentTrack = queue.tracks[0]
|
2020-06-02 16:11:12 +05:00
|
|
|
// End the dispatcher
|
2020-06-21 18:36:16 +05:00
|
|
|
queue.voiceConnection.dispatcher.end()
|
|
|
|
queue.lastSkipped = true
|
2020-06-02 16:11:12 +05:00
|
|
|
// Resolve the current track
|
2020-06-21 18:36:16 +05:00
|
|
|
resolve(currentTrack)
|
|
|
|
})
|
2020-01-12 00:01:18 +05:00
|
|
|
}
|
|
|
|
|
2020-01-18 15:15:10 +05:00
|
|
|
/**
|
2020-06-02 16:11:12 +05:00
|
|
|
* Get the currently playing track
|
|
|
|
* @param {Discord.Snowflake} guildID
|
|
|
|
* @returns {Promise<Track>} The track which is currently played
|
2020-01-18 15:15:10 +05:00
|
|
|
*/
|
2020-06-21 18:36:16 +05:00
|
|
|
nowPlaying (guildID) {
|
|
|
|
return new Promise((resolve, reject) => {
|
2020-06-02 16:11:12 +05:00
|
|
|
// Get guild queue
|
2020-06-21 18:36:16 +05:00
|
|
|
const queue = this.queues.find((g) => g.guildID === guildID)
|
|
|
|
if (!queue) return reject(new Error('Not playing'))
|
|
|
|
const currentTrack = queue.tracks[0]
|
2020-06-02 16:11:12 +05:00
|
|
|
// Resolve the current track
|
2020-06-21 18:36:16 +05:00
|
|
|
resolve(currentTrack)
|
|
|
|
})
|
2020-01-18 15:15:10 +05:00
|
|
|
}
|
|
|
|
|
2020-01-18 15:03:22 +05:00
|
|
|
/**
|
|
|
|
* Enable or disable the repeat mode
|
2020-06-02 16:11:12 +05:00
|
|
|
* @param {Discord.Snowflake} guildID
|
2020-01-18 15:03:22 +05:00
|
|
|
* @param {Boolean} enabled Whether the repeat mode should be enabled
|
|
|
|
* @returns {Promise<Void>}
|
|
|
|
*/
|
2020-06-21 18:36:16 +05:00
|
|
|
setRepeatMode (guildID, enabled) {
|
|
|
|
return new Promise((resolve, reject) => {
|
2020-06-02 16:11:12 +05:00
|
|
|
// Get guild queue
|
2020-06-21 18:36:16 +05:00
|
|
|
const queue = this.queues.find((g) => g.guildID === guildID)
|
|
|
|
if (!queue) return reject(new Error('Not playing'))
|
2020-01-18 15:03:22 +05:00
|
|
|
// Enable/Disable repeat mode
|
2020-06-21 18:36:16 +05:00
|
|
|
queue.repeatMode = enabled
|
2020-01-18 15:03:22 +05:00
|
|
|
// Resolve
|
2020-06-21 18:36:16 +05:00
|
|
|
resolve()
|
|
|
|
})
|
2020-01-18 15:03:22 +05:00
|
|
|
}
|
|
|
|
|
2020-04-24 20:14:34 +05:00
|
|
|
/**
|
2020-06-02 16:11:12 +05:00
|
|
|
* 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
|
2020-04-24 20:14:34 +05:00
|
|
|
*/
|
2020-06-21 18:36:16 +05:00
|
|
|
shuffle (guildID) {
|
|
|
|
return new Promise((resolve, reject) => {
|
2020-06-02 16:11:12 +05:00
|
|
|
// Get guild queue
|
2020-06-21 18:36:16 +05:00
|
|
|
const queue = this.queues.find((g) => g.guildID === guildID)
|
|
|
|
if (!queue) return reject(new Error('Not playing'))
|
2020-06-02 16:11:12 +05:00
|
|
|
// Shuffle the queue (except the first track)
|
2020-06-21 18:36:16 +05:00
|
|
|
const currentTrack = queue.tracks.shift()
|
|
|
|
queue.tracks = queue.tracks.sort(() => Math.random() - 0.5)
|
|
|
|
queue.tracks.unshift(currentTrack)
|
2020-04-24 20:14:34 +05:00
|
|
|
// Resolve
|
2020-06-21 18:36:16 +05:00
|
|
|
resolve(queue)
|
|
|
|
})
|
2020-04-24 20:14:34 +05:00
|
|
|
}
|
2020-04-24 20:15:06 +05:00
|
|
|
|
2020-04-24 20:20:48 +05:00
|
|
|
/**
|
2020-06-02 16:11:12 +05:00
|
|
|
* 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>}
|
2020-04-24 20:20:48 +05:00
|
|
|
*/
|
2020-06-21 18:36:16 +05:00
|
|
|
remove (guildID, track) {
|
|
|
|
return new Promise((resolve, reject) => {
|
2020-04-24 20:20:48 +05:00
|
|
|
// Gets guild queue
|
2020-06-21 18:36:16 +05:00
|
|
|
const queue = this.queues.find((g) => g.guildID === guildID)
|
|
|
|
if (!queue) return reject(new Error('Not playing'))
|
2020-06-02 16:11:12 +05:00
|
|
|
// Remove the track from the queue
|
2020-06-21 18:36:16 +05:00
|
|
|
let trackFound = null
|
|
|
|
if (typeof track === 'number') {
|
|
|
|
trackFound = queue.tracks[track]
|
|
|
|
if (trackFound) {
|
|
|
|
queue.tracks = queue.tracks.filter((t) => t !== trackFound)
|
2020-04-24 20:20:48 +05:00
|
|
|
}
|
|
|
|
} else {
|
2020-06-21 18:36:16 +05:00
|
|
|
trackFound = queue.tracks.find((s) => s === track)
|
|
|
|
if (trackFound) {
|
|
|
|
queue.tracks = queue.tracks.filter((s) => s !== trackFound)
|
2020-04-24 20:20:48 +05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
// Resolve
|
2020-06-21 18:36:16 +05:00
|
|
|
resolve(trackFound)
|
|
|
|
})
|
2020-04-24 20:20:48 +05:00
|
|
|
}
|
|
|
|
|
2020-01-12 00:01:18 +05:00
|
|
|
/**
|
2020-06-02 16:11:12 +05:00
|
|
|
* Handle the voice state update event
|
|
|
|
* @ignore
|
|
|
|
* @private
|
|
|
|
* @param {Discord.VoiceState} oldState
|
|
|
|
* @param {Discord.VoiceState} newState
|
|
|
|
*/
|
2020-06-21 18:36:16 +05:00
|
|
|
_handleVoiceStateUpdate (oldState, newState) {
|
|
|
|
if (!this.options.leaveOnEmpty) return
|
2020-06-02 16:11:12 +05:00
|
|
|
// If the member leaves a voice channel
|
2020-06-21 18:36:16 +05:00
|
|
|
if (!oldState.channelID || newState.channelID) return
|
2020-06-02 16:11:12 +05:00
|
|
|
// Search for a queue for this channel
|
2020-06-21 18:36:16 +05:00
|
|
|
const queue = this.queues.find((g) => g.voiceConnection.channel.id === oldState.channelID)
|
|
|
|
if (queue) {
|
2020-06-02 16:11:12 +05:00
|
|
|
// If the channel is not empty
|
2020-06-21 18:36:16 +05:00
|
|
|
if (queue.voiceConnection.channel.members.size > 1) return
|
2020-06-02 16:11:12 +05:00
|
|
|
// Disconnect from the voice channel
|
2020-06-21 18:36:16 +05:00
|
|
|
queue.voiceConnection.channel.leave()
|
2020-06-02 16:11:12 +05:00
|
|
|
// Delete the queue
|
2020-06-21 18:36:16 +05:00
|
|
|
this.queues = this.queues.filter((g) => g.guildID !== queue.guildID)
|
2020-06-02 16:11:12 +05:00
|
|
|
// Emit end event
|
2020-06-21 18:36:16 +05:00
|
|
|
queue.emit('channelEmpty')
|
2020-06-02 16:11:12 +05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-21 20:17:00 +05:00
|
|
|
/**
|
|
|
|
* Play a stream in a channel
|
|
|
|
* @ignore
|
|
|
|
* @private
|
|
|
|
* @param {Queue} queue The queue to play
|
|
|
|
* @param {*} updateFilter Whether this method is called to update some ffmpeg filters
|
|
|
|
* @returns {Promise<void>}
|
|
|
|
*/
|
|
|
|
_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)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-06-02 16:11:12 +05:00
|
|
|
/**
|
|
|
|
* Start playing a track in a guild
|
2020-01-12 00:01:18 +05:00
|
|
|
* @ignore
|
2020-06-02 16:11:12 +05:00
|
|
|
* @private
|
|
|
|
* @param {Discord.Snowflake} guildID
|
2020-01-12 00:01:18 +05:00
|
|
|
* @param {Boolean} firstPlay Whether the function was called from the play() one
|
|
|
|
*/
|
2020-06-21 18:36:16 +05:00
|
|
|
async _playTrack (guildID, firstPlay) {
|
|
|
|
// Get guild queue
|
|
|
|
const queue = this.queues.find((g) => g.guildID === guildID)
|
2020-01-12 00:01:18 +05:00
|
|
|
// If there isn't any music in the queue
|
2020-06-21 18:36:16 +05:00
|
|
|
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')
|
2020-01-12 22:56:11 +05:00
|
|
|
}
|
2020-06-21 18:36:16 +05:00
|
|
|
// Emit end event
|
|
|
|
return queue.emit('end')
|
2020-01-12 00:01:18 +05:00
|
|
|
}
|
2020-06-21 18:36:16 +05:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
})
|
2020-01-12 00:01:18 +05:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2020-06-21 18:36:16 +05:00
|
|
|
module.exports = Player
|