UNPKG

discord-player-music

Version:

A powerful music module for your Discord bot with lots of features

569 lines (568 loc) 29 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.QueueManager = void 0; const PlayerEnums_1 = require("../PlayerEnums"); const discord_js_1 = require("discord.js"); const voice_1 = require("@discordjs/voice"); const errors_json_1 = __importDefault(require("../data/errors.json")); const PlayerError_1 = require("../PlayerError"); const dpm_ytdl_1 = __importDefault(require("../modules/dpm-ytdl")); const parse = ms => ({ days: Math.floor(ms / 86400000), hours: Math.floor(ms / 3600000 % 24), minutes: Math.floor(ms / 60000 % 60), seconds: Math.floor(ms / 1000 % 60) }); /** * Class that controls Player Queue Manager * * @class * @classdesc Player Queue Manager Class */ class QueueManager { player; storage; /** * @constructor * * @param {Player} player Player Class */ constructor(player) { if (!player) throw new PlayerError_1.PlayerError(errors_json_1.default.default.requireParam.replace('{param}', 'player').replace('{data}', '<QueueManager>')); /** * Player Class * * @type {Player} * @private */ this.player = player; /** * Player Queue Storage * * @type {Collection<string, PlayerQueue>} * @private */ this.storage = new discord_js_1.Collection(); } /** * Allows you to get the number of server queues that are in storage * * @type {number} * * @returns {number} Player queue storage size */ get size() { return this.storage.size; } /** * Allows you to add tracks to the server queue * * @param {PlayerTrack} track Track info * * @fires Player#addedTrack * @fires Player#queueStarted * * @example * const searchData = client.player.search('Xtrullor - Samsara', message.member, message.channel); * if(searchData[0]?.title) client.player.queue.add(searchData[0]); * * @returns {Promise<boolean>} Status of adding a track to the server queue */ add(track) { return new Promise(async (res, rej) => { if (!track) throw new PlayerError_1.PlayerError(this.player.errors.default.requireParam.replace('{param}', 'track').replace('{data}', '<QueueManager>.add')); const guild = track.guild; let guildConnection = (0, voice_1.getVoiceConnection)(guild.id); if (!guildConnection) guildConnection = (0, voice_1.joinVoiceChannel)({ guildId: guild.id, channelId: track.channel.voice.id, adapterCreator: guild.voiceAdapterCreator }); const guildQueue = this.storage.get(guild.id); if (!guildQueue) { const queueobject = { channel: { text: track.channel.text, voice: track.channel.voice }, dispatcher: (0, voice_1.createAudioPlayer)(), tracks: [track], connection: guildConnection, loop: { track: false, queue: false }, volume: this.player.options.defaultVolume, startStream: Date.now(), state: PlayerEnums_1.GuildQueueState.PLAYING, filter: { name: null, value: null } }; this.storage.set(guild.id, queueobject); this.player.emit(PlayerEnums_1.Events.QUEUE_STARTED, this.storage.get(guild.id)); this.player.initGuildTrack(guild.id, track); return res(this.storage.get(guild.id) != null); } else { guildQueue.tracks.push(track); this.player.emit(PlayerEnums_1.Events.TRACK_ADD, track); return res(this.storage.get(guild.id)?.tracks.find(guildTrack => guildTrack.url === track.url) != null); } }); } /** * Allows you to set the state of the server queue (pause and resume) * * @param {string} guildID Discord Guild ID * @param {GuildQueueState} type State type * * @fires Player#queueStateChange * * @example * // Pause command * const setStateData = await client.player.queue.setState('GuildID', GuildQueueState.PAUSE); * * // Resume command * const setStateData = await client.player.queue.setState('GuildID', GuildQueueState.PLAYING); * * console.log(setStateData); * * @returns {Promise<DefaultData | ErrorData>} Installation status or object with an error */ setState(guildID, type) { return new Promise(async (res, rej) => { if (!guildID) throw new PlayerError_1.PlayerError(this.player.errors.default.requireParam.replace('{param}', 'guildID').replace('{data}', '<QueueManager>.setState')); const guildQueue = this.storage.get(guildID); if (!guildQueue) return res({ error: { code: 404, message: this.player.errors.default.missingQueue.replace('{id}', guildID) } }); const oldState = guildQueue.state; if (type && [PlayerEnums_1.GuildQueueState.PAUSED, PlayerEnums_1.GuildQueueState.PLAYING].includes(type)) { const newState = type; if (guildQueue.state.includes(newState)) return res({ error: { code: 404, message: this.player.errors.other.unknownState.replace('{state}', newState).replace('{id}', guildID) } }); if (newState.includes(PlayerEnums_1.GuildQueueState.PAUSED)) guildQueue.dispatcher.pause(); else guildQueue.dispatcher.unpause(); guildQueue.state = newState; this.player.emit(PlayerEnums_1.Events.QUEUE_STATE, guildQueue, oldState, newState); } else { if (guildQueue.state.includes(PlayerEnums_1.GuildQueueState.PAUSED)) { const newState = PlayerEnums_1.GuildQueueState.PLAYING; guildQueue.dispatcher.unpause(); guildQueue.state = newState; this.player.emit(PlayerEnums_1.Events.QUEUE_STATE, guildQueue, oldState, newState); } } return res({ status: !guildQueue.state.includes(oldState) }); }); } /** * Allows you to get the queue object for the server * * @param {string} guildID Discord Guild ID * * @example * const guildQueue = await client.player.queue.get('GuildID'); * if(guildQueue?.tracks) return message.channel.send({ content: `${guildQueue.tracks.length} tracks found in server queue!` }); * * @returns {Promise<PlayerQueue | ErrorData>} Queue object for server or `null` */ get(guildID) { return new Promise(async (res, rej) => { if (!guildID) throw new PlayerError_1.PlayerError(this.player.errors.default.requireParam.replace('{param}', 'guildID').replace('{data}', '<QueueManager>.get')); const guildQueue = this.storage.get(guildID); if (!guildQueue) return res({ error: { code: 404, message: this.player.errors.default.missingQueue.replace('{id}', guildID) } }); return res(guildQueue); }); } /** * Allows you to create a progress bar for playing the current track in the server queue * * @param {string} guildID Discord Guild ID * * @example * const progressData = await client.player.queue.pregress('GuildID'); * if(progressData?.bar) return message.channel.send({ content: `[${progressData.percents}] ${progressData.bar}` }); * * @returns {ProgressData<ProgressData | ErrorData>} Progress bar data or an object with an error */ progress(guildID) { return new Promise(async (res, rej) => { if (!guildID) throw new PlayerError_1.PlayerError(this.player.errors.default.requireParam.replace('{param}', 'guildID').replace('{data}', '<QueueManager>.progress')); const guildQueue = await this.get(guildID); if ('error' in guildQueue) return res({ error: { code: 404, message: this.player.errors.default.missingQueue.replace('{id}', guildID) } }); const emptyProgress = this.player.utils.createEmptyProgress(); if (!guildQueue.dispatcher) return res({ bar: emptyProgress, percents: '0%' }); const track = guildQueue.tracks[0]; if (!track) return res({ bar: emptyProgress, percents: '0%' }); const trackDuration = Math.floor((Number(track.duration.hours) * 3600) + (Number(track.duration.minutes) * 60) + Number(track.duration.seconds)); const totalSeconds = Math.floor(trackDuration * 1000); const alreadyPlayedSeconds = Math.floor(guildQueue.dispatcher.state.resource.playbackDuration || 0); if (isNaN(this.player.options.progressConfig.size)) return res({ bar: emptyProgress, percents: '0%' }); if (!totalSeconds) return res({ bar: emptyProgress, percents: '0%' }); if (isNaN(totalSeconds)) return res({ bar: emptyProgress, percents: '0%' }); if (!alreadyPlayedSeconds) return res({ bar: emptyProgress, percents: '0%' }); if (isNaN(alreadyPlayedSeconds)) return res({ bar: emptyProgress, percents: '0%' }); if (alreadyPlayedSeconds < totalSeconds) { const percentage = alreadyPlayedSeconds / totalSeconds; const progress = Math.round(this.player.options.progressConfig.size * percentage); const empty = this.player.options.progressConfig.size - progress; const progressBar = this.player.options.progressConfig.line.repeat(progress).replace(/.$/, this.player.options.progressConfig.slider); const emptyProgressBar = this.player.options.progressConfig.line.repeat(empty); const percents = Math.floor(percentage * 100); if (percents < 10) return res({ bar: this.player.utils.createEmptyProgress(), percents: `${percents}%` }); else return res({ bar: progressBar + emptyProgressBar, percents: `${percents}%` }); } else return res({ bar: this.player.options.progressConfig.line.repeat(this.player.options.progressConfig.size + 2), percents: `${(alreadyPlayedSeconds / totalSeconds) * 100}%` }); }); } /** * Allows you to find out information about the current stream * * @param {string} guildID Discord Guild ID * * @example * const streamData = await client.player.queue.streamInfo('GuildID'); * if(streamData?.state) return message.channel.send({ content: `Stream filter: ${streamData.filter.name}\nStream state: ${streamData.state.includes(GuildQueueState.PLAYING) ? '▶️' : '⏸️'}` }); * * @returns {Promise<StreamData | ErrorData>} General information about the current stream */ streamInfo(guildID) { return new Promise(async (res, rej) => { if (!guildID) throw new PlayerError_1.PlayerError(this.player.errors.default.requireParam.replace('{param}', 'guildID').replace('{data}', '<QueueManager>.dispatcherInfo')); const guildQueue = this.storage.get(guildID); if (!guildQueue) return res({ error: { code: 404, message: this.player.errors.default.missingQueue.replace('{id}', guildID) } }); const streamTime = parse(Date.now() - guildQueue.startStream); return res({ loop: { track: guildQueue.loop.track, queue: guildQueue.loop.queue }, filter: guildQueue.filter, state: guildQueue.state, volume: guildQueue.volume, streamTime: streamTime }); }); } /** * Allows you to get an object with information about the track * * @param {string} guildID Discord Guild ID * @param {number} index Track index * * @example * const trackData = await client.player.queue.trackInfo('GuildID', 0); * if(trackData?.title) return message.channel.send({ content: `Track title: ${trackData.title}\ntrack URL: ${trackData.url}` }); * * @returns {Promise<PlayerTrack | ErrorData>} object with track information or with an error */ trackInfo(guildID, index) { return new Promise(async (res, rej) => { if (!guildID) throw new PlayerError_1.PlayerError(this.player.errors.default.requireParam.replace('{param}', 'guildID').replace('{data}', '<QueueManager>.trackInfo')); if (!index) index = 0; const guildQueue = this.storage.get(guildID); if (!guildQueue) return res({ error: { code: 404, message: this.player.errors.default.missingQueue.replace('{id}', guildID) } }); const guildTracks = guildQueue.tracks; return res(guildTracks.indexOf(guildTracks[index]) == -1 ? guildTracks[0] : guildTracks[index]); }); } /** * Allows you to set a playback filter for the queue * * @param {string} guildID Discord Guild ID * @param {Filter} filter Filter type * * @example * const setFilterData = await client.player.queue.setFilter('GuildID', 'nightcore'); * if(setFilterData?.name) return message.channel.send({ content: `Filter '${setFilterData.name}' successfully installed!` }); * * @returns {Promise<PlayerFilter | ErrorData>} Set filter data or an object with an error */ setFilter(guildID, filter) { return new Promise(async (res, rej) => { if (!guildID) throw new PlayerError_1.PlayerError(this.player.errors.default.requireParam.replace('{param}', 'guildID').replace('{data}', '<QueueManager>.setFilter')); const guildQueue = this.storage.get(guildID); if (!guildQueue) return res({ error: { code: 404, message: this.player.errors.default.missingQueue.replace('{id}', guildID) } }); const playerFilter = await this.player.filters.get(filter || 'clear'); if ('error' in playerFilter) return res({ error: { code: 404, message: this.player.errors.filters.notFound.replace('{name}', filter || 'clear') } }); guildQueue.filter = playerFilter.name.includes('clear') ? { name: null, value: null } : playerFilter; this.player.initGuildTrack(guildID, guildQueue.tracks[0]); return res(playerFilter); }); } /** * Allows you to set the looping of a queue or a single track * * @param {string} guildID Discord Guild ID * @param {Loop} type Loop type * * @example * // Loop only first track * const setLoopData = await client.player.queue.setLoop('GuildID', Loop.TRACK); * * // Loop all queue tracks * const setLoopData = await client.player.queue.setLoop('GuildID', Loop.QUEUE); * * console.log(setLoopData); * * @returns {Promise<LoopData | ErrorData>} Loop setup status or object in error */ setLoop(guildID, type) { return new Promise(async (res, rej) => { if (!guildID) throw new PlayerError_1.PlayerError(this.player.errors.default.requireParam.replace('{param}', 'guildID').replace('{data}', '<QueueManager>.setLoop')); const guildQueue = this.storage.get(guildID); if (!guildQueue) return res({ error: { code: 404, message: this.player.errors.default.missingQueue.replace('{id}', guildID) } }); if (type) { switch (type) { case PlayerEnums_1.Loop.QUEUE: { if (this.player.options.synchronLoop) { guildQueue.loop.queue = !guildQueue.loop.queue; if (guildQueue.loop.track) guildQueue.loop.track = !guildQueue.loop.track; } else guildQueue.loop.queue = !guildQueue.loop.queue; return res({ track: guildQueue.loop.track, queue: guildQueue.loop.queue }); } case PlayerEnums_1.Loop.TRACK: { if (this.player.options.synchronLoop) { guildQueue.loop.track = !guildQueue.loop.track; if (guildQueue.loop.queue) guildQueue.loop.queue = !guildQueue.loop.queue; } else guildQueue.loop.track = !guildQueue.loop.track; return res({ track: guildQueue.loop.track, queue: guildQueue.loop.queue }); } } } else { if (this.player.options.synchronLoop) { guildQueue.loop.track = !guildQueue.loop.track; if (guildQueue.loop.queue) guildQueue.loop.queue = !guildQueue.loop.queue; } else guildQueue.loop.track = !guildQueue.loop.track; return res({ track: guildQueue.loop.track, queue: guildQueue.loop.queue }); } }); } /** * Allows you to set the playback volume of the server queue * * @param {string} guildID Discord Guild ID * @param {number} [value=5] Volume value * * @example * const setVolumeData = await client.player.queue.setVolume('GuildID', 10); * if(setVolumeData?.status) return message.channel.send({ content: `Volume playback successfully installed on 10!` }); * * @returns {Promise<DefaultData | ErrorData>} Server queue playback volume change status or object with an error */ setVolume(guildID, value) { return new Promise(async (res, rej) => { if (!guildID) throw new PlayerError_1.PlayerError(this.player.errors.default.requireParam.replace('{param}', 'guildID').replace('{data}', '<QueueManager>.setVolume')); if (value && typeof value != 'number') throw new PlayerError_1.PlayerError(this.player.errors.default.invalidType.replace('{value}', 'value').replace('{data}', 'number')); if (value && isNaN(value)) throw new PlayerError_1.PlayerError(this.player.errors.default.invalidValue.replace('{value}', 'value').replace('{data}', this.player.options.defaultVolume.toString())); const guildQueue = this.storage.get(guildID); if (!guildQueue) return res({ error: { code: 404, message: this.player.errors.default.missingQueue.replace('{id}', guildID) } }); guildQueue.volume = value || this.player.options.defaultVolume; guildQueue.dispatcher.state.resource?.volume.setVolume(value / this.player.options.defaultVolume); return res({ status: guildQueue.volume === (value || this.player.options.defaultVolume) }); }); } /** * Allows you to skip server tracks * * @param {string} guildID Discord Guild ID * * @example * const skipData = await client.player.queue.skipTrack('GuildID'); * if(skipData?.next) return message.channel.send({ content: `Skip track '${skipData.current}' and start playing track '${skipData.next}'!` }); * * @returns {Promise<SkipData | ErrorData>} Object with current and next track or error */ skipTrack(guildID) { return new Promise(async (res, rej) => { if (!guildID) throw new PlayerError_1.PlayerError(this.player.errors.default.requireParam.replace('{param}', 'guildID').replace('{data}', '<QueueManager>.skipTrack')); const guildQueue = this.storage.get(guildID); if (!guildQueue) return res({ error: { code: 404, message: this.player.errors.default.missingQueue.replace('{id}', guildID) } }); if (guildQueue.tracks.length === 1) { if (guildQueue.connection.state.status != voice_1.VoiceConnectionStatus.Destroyed) { guildQueue.connection.destroy(); this.storage.delete(guildID); } else this.storage.delete(guildID); return res({ current: guildQueue.tracks[0], next: null }); } if (guildQueue.loop.track) { const track = guildQueue.tracks.shift(); guildQueue.tracks.push(track); guildQueue.dispatcher.stop(); return res({ current: track, next: guildQueue.tracks[0] }); } if (guildQueue.loop.queue) guildQueue.dispatcher.stop(); guildQueue.dispatcher.stop(); return res({ current: guildQueue.tracks[0] || null, next: guildQueue.tracks[1] || null }); }); } /** * Allows you to delete tracks by index in the server queue * * @param {string} guildID Discord Guild ID * @param {number} index Track index * * @example * const removeTrackData = await client.player.queue.removetrack('GuildID', 0); * if(!removeTrackData?.error) return message.channel.send({ content: `Track with index 1 deleted from server queue!` }); * * @returns {Promise<RemoveTrackData | ErrorData>} Updated track list for the server or an object with an error */ removeTrack(guildID, index) { return new Promise(async (res, rej) => { if (!guildID) throw new PlayerError_1.PlayerError(this.player.errors.default.requireParam.replace('{param}', 'guildID').replace('{data}', '<QueueManager>.removeTrack')); if (!index) index = 0; const guildQueue = this.storage.get(guildID); if (!guildQueue) return res({ error: { code: 404, message: this.player.errors.default.missingQueue.replace('{id}', guildID) } }); const guildTracks = guildQueue.tracks; if (guildTracks.indexOf(guildTracks[index]) == -1) return res({ error: { code: 404, message: this.player.errors.other.unknownGuildTrack.replace('{index}', index.toString()).replace('{id}', guildID) } }); const guildTrack = guildTracks[index]; guildTracks.splice(index, 1); return res({ deleted: guildTrack, tracks: guildTracks }); }); } /** * Allows you to rewind the current track in the queue * * @param {string} guildID Discord Guild ID * @param {number} [value=5] Seek value * * @example * // Seek 10 seconds * const seekData = await client.player.queue.seek('GuildID', 10); * if(seekData?.status) return message.channel.send({ content: `The track has been successfully rewound by 10 seconds!` }); * * @returns {Promise<DefaultData | ErrorData>} Installation status or object with an error */ seek(guildID, value) { return new Promise(async (res, rej) => { if (!guildID) throw new PlayerError_1.PlayerError(this.player.errors.default.requireParam.replace('{param}', 'guildID').replace('{data}', '<QueueManager>.seek')); const guildQueue = this.storage.get(guildID); if (!guildQueue) return res({ error: { code: 404, message: this.player.errors.default.missingQueue.replace('{id}', guildID) } }); const stream = await (0, dpm_ytdl_1.default)(guildQueue.tracks[0].url, { seek: value, filter: guildQueue.filter.value }); const resource = (0, voice_1.createAudioResource)(stream, { inputType: voice_1.StreamType.OggOpus, inlineVolume: true }); guildQueue.dispatcher.play(resource); guildQueue.dispatcher.state.resource.playbackDuration = Math.floor(value * 1000); guildQueue.dispatcher.state.resource.volume.setVolume(guildQueue.volume / this.player.options.defaultVolume); return res({ status: guildQueue.dispatcher.state.resource.playbackDuration >= Math.floor(value * 1000) }); }); } /** * Allows you to shuffle tracks in the server queue * * @param {string} guildID Discord Guild ID * * @example * const shuffleData = await client.player.queue.shuffle('GuildID'); * if(!shuffleData?.error) return message.channel.send({ content: `10 tracks successfully shuffled!` }); * * @returns {Promise<Array<PlayerTrack> | ErrorData>} Shuffled list of tracks in the queue */ shuffle(guildID) { return new Promise(async (res, rej) => { if (!guildID) throw new PlayerError_1.PlayerError(this.player.errors.default.requireParam.replace('{param}', 'guildID').replace('{data}', '<QueueManager>.shuffle')); const guildQueue = this.storage.get(guildID); if (!guildQueue) return res({ error: { code: 404, message: this.player.errors.default.missingQueue.replace('{id}', this.player.client.user.id) } }); if (guildQueue.tracks.length <= 2) return res({ error: { code: 403, message: this.player.errors.other.shuffleInfo } }); const track = guildQueue.tracks.shift(); for (let i = guildQueue.tracks.length - 1; i > 0; i--) { const index = Math.floor(Math.random() * (i + 1)); [guildQueue.tracks[i], guildQueue.tracks[index]] = [guildQueue.tracks[index], guildQueue.tracks[i]]; } guildQueue.tracks.unshift(track); return res(guildQueue.tracks); }); } /** * Allows you to end playback of the server queue * * @param {string} guildID Discord Guild ID * * @example * const stopData = await client.player.queue.stop('GuildID'); * if(stopData?.status) return message.channel.send({ content: `Server queue playback successfully stopped!` }); * * @returns {Promise<DefaultData | ErrorData>} Presence of a queue for a server in storage or an object with an error */ stop(guildID) { return new Promise(async (res, rej) => { if (!guildID) throw new PlayerError_1.PlayerError(this.player.errors.default.requireParam.replace('{param}', 'guildID').replace('{data}', '<QueueManager>.stop')); const guildQueue = this.storage.get(guildID); if (!guildQueue) return res({ error: { code: 404, message: this.player.errors.default.missingQueue.replace('{id}', guildID) } }); if (guildQueue.connection.state.status != voice_1.VoiceConnectionStatus.Destroyed) guildQueue.connection.destroy(); return res({ status: this.storage.delete(guildID) }); }); } /** * Allows you to clear the server queue * * @param {string} guildID Discord Guild ID * * @example * const deleteData = client.player.queue.delete('GuildID'); * if(deleteData?.status) return message.channel.send({ content: `The server queue has been successfully removed from the storage!` }); * * @returns {Promise<DefaultData | ErrorData>} Server queue cleanup status or object with an error */ delete(guildID) { return new Promise(async (res, rej) => { if (!guildID) throw new PlayerError_1.PlayerError(this.player.errors.default.requireParam.replace('{param}', 'guildID').replace('{data}', '<QueueManager>.clear')); if (!this.storage.get(guildID)) return res({ error: { code: 404, message: this.player.errors.default.missingQueue.replace('{id}', guildID) } }); return res({ status: this.storage.delete(guildID) }); }); } } exports.QueueManager = QueueManager;