distube
Version:
A powerful Discord.js module for simplifying music commands and effortless playback of various sources with integrated audio filters.
1 lines • 157 kB
Source Map (JSON)
{"version":3,"sources":["../src/constant.ts","../src/core/DisTubeBase.ts","../src/core/DisTubeHandler.ts","../src/struct/DisTubeError.ts","../src/util.ts","../src/core/DisTubeVoice.ts","../src/core/manager/BaseManager.ts","../src/core/manager/FilterManager.ts","../src/type.ts","../src/struct/TaskQueue.ts","../src/struct/Queue.ts","../src/struct/Playlist.ts","../src/struct/Song.ts","../src/core/DisTubeOptions.ts","../src/core/DisTubeStream.ts","../src/core/manager/DisTubeVoiceManager.ts","../src/core/manager/GuildIdManager.ts","../src/core/manager/QueueManager.ts","../src/DisTube.ts","../src/struct/Plugin.ts","../src/struct/ExtractorPlugin.ts","../src/struct/InfoExtractorPlugin.ts","../src/struct/PlayableExtractorPlugin.ts"],"sourcesContent":["import type { DisTubeOptions, Filters } from \"./type\";\n\nexport const version: string = \"5.2.3\";\n\n/**\n * Audio configuration constants\n */\nexport const AUDIO_SAMPLE_RATE = 48000;\nexport const AUDIO_CHANNELS = 2;\n\n/**\n * Default volume percentage (0-100)\n */\nexport const DEFAULT_VOLUME = 50;\n\n/**\n * Timeout constants (in milliseconds)\n */\nexport const JOIN_TIMEOUT_MS = 30_000;\nexport const RECONNECT_TIMEOUT_MS = 5_000;\nexport const RECONNECT_MAX_ATTEMPTS = 5;\n\n/**\n * HTTP redirect status codes\n */\nexport const HTTP_REDIRECT_CODES = new Set([301, 302, 303, 307, 308]);\nexport const MAX_REDIRECT_DEPTH = 5;\n\n/**\n * Default DisTube audio filters.\n */\nexport const defaultFilters: Filters = {\n \"3d\": \"apulsator=hz=0.125\",\n bassboost: \"bass=g=10\",\n echo: \"aecho=0.8:0.9:1000:0.3\",\n flanger: \"flanger\",\n gate: \"agate\",\n haas: \"haas\",\n karaoke: \"stereotools=mlev=0.1\",\n nightcore: \"asetrate=48000*1.25,aresample=48000,bass=g=5\",\n reverse: \"areverse\",\n vaporwave: \"asetrate=48000*0.8,aresample=48000,atempo=1.1\",\n mcompand: \"mcompand\",\n phaser: \"aphaser\",\n tremolo: \"tremolo\",\n surround: \"surround\",\n earwax: \"earwax\",\n};\n\nexport const defaultOptions = {\n plugins: [],\n emitNewSongOnly: false,\n savePreviousSongs: true,\n nsfw: false,\n emitAddSongWhenCreatingQueue: true,\n emitAddListWhenCreatingQueue: true,\n joinNewVoiceChannel: true,\n} satisfies DisTubeOptions;\n","import type { Client } from \"discord.js\";\nimport type { DisTube } from \"../DisTube\";\nimport type { Queue } from \"../struct/Queue\";\nimport type { Song } from \"../struct/Song\";\nimport type { DisTubeEvents, DisTubePlugin } from \"../type\";\nimport type { DisTubeHandler } from \"./DisTubeHandler\";\nimport type { Options } from \"./DisTubeOptions\";\nimport type { DisTubeVoiceManager } from \"./manager/DisTubeVoiceManager\";\nimport type { QueueManager } from \"./manager/QueueManager\";\n\nexport abstract class DisTubeBase {\n distube: DisTube;\n constructor(distube: DisTube) {\n /**\n * DisTube\n */\n this.distube = distube;\n }\n /**\n * Emit the {@link DisTube} of this base\n * @param eventName - Event name\n * @param args - arguments\n */\n emit(eventName: keyof DisTubeEvents, ...args: any): boolean {\n return this.distube.emit(eventName, ...args);\n }\n /**\n * Emit error event\n * @param error - error\n * @param queue - The queue encountered the error\n * @param song - The playing song when encountered the error\n */\n emitError(error: Error, queue: Queue, song?: Song) {\n this.distube.emitError(error, queue, song);\n }\n /**\n * Emit debug event\n * @param message - debug message\n */\n debug(message: string) {\n this.distube.debug(message);\n }\n /**\n * The queue manager\n */\n get queues(): QueueManager {\n return this.distube.queues;\n }\n /**\n * The voice manager\n */\n get voices(): DisTubeVoiceManager {\n return this.distube.voices;\n }\n /**\n * Discord.js client\n */\n get client(): Client {\n return this.distube.client;\n }\n /**\n * DisTube options\n */\n get options(): Options {\n return this.distube.options;\n }\n /**\n * DisTube handler\n */\n get handler(): DisTubeHandler {\n return this.distube.handler;\n }\n /**\n * DisTube plugins\n */\n get plugins(): DisTubePlugin[] {\n return this.distube.plugins;\n }\n}\n","import { request } from \"undici\";\nimport { HTTP_REDIRECT_CODES, MAX_REDIRECT_DEPTH } from \"../constant\";\nimport { DisTubeError } from \"../struct/DisTubeError\";\nimport { Playlist } from \"../struct/Playlist\";\nimport { Song } from \"../struct/Song\";\nimport type { DisTubePlugin, ResolveOptions } from \"../type\";\nimport { PluginType } from \"../type\";\nimport { isURL } from \"../util\";\nimport { DisTubeBase } from \"./DisTubeBase\";\n\n/**\n * DisTube's Handler\n */\nexport class DisTubeHandler extends DisTubeBase {\n resolve<T = unknown>(song: Song<T>, options?: Omit<ResolveOptions, \"metadata\">): Promise<Song<T>>;\n resolve<T = unknown>(song: Playlist<T>, options?: Omit<ResolveOptions, \"metadata\">): Promise<Playlist<T>>;\n resolve<T = unknown>(song: string, options?: ResolveOptions<T>): Promise<Song<T> | Playlist<T>>;\n resolve<T = unknown>(song: Song, options: ResolveOptions<T>): Promise<Song<T>>;\n resolve<T = unknown>(song: Playlist, options: ResolveOptions<T>): Promise<Playlist<T>>;\n resolve(song: string | Song | Playlist, options?: ResolveOptions): Promise<Song | Playlist>;\n /**\n * Resolve a url or a supported object to a {@link Song} or {@link Playlist}\n * @throws {@link DisTubeError}\n * @param input - Resolvable input\n * @param options - Optional options\n * @returns Resolved\n */\n async resolve(input: string | Song | Playlist, options: ResolveOptions = {}): Promise<Song | Playlist> {\n if (input instanceof Song || input instanceof Playlist) {\n if (\"metadata\" in options) input.metadata = options.metadata;\n if (\"member\" in options) input.member = options.member;\n return input;\n }\n if (typeof input === \"string\") {\n if (isURL(input)) {\n const plugin =\n (await this._getPluginFromURL(input)) ||\n (await this._getPluginFromURL((input = await this.followRedirectLink(input))));\n if (!plugin) throw new DisTubeError(\"NOT_SUPPORTED_URL\");\n this.debug(`[${plugin.constructor.name}] Resolving from url: ${input}`);\n return plugin.resolve(input, options);\n }\n try {\n const song = await this.#searchSong(input, options);\n if (song) return song;\n } catch {\n throw new DisTubeError(\"NO_RESULT\", input);\n }\n }\n throw new DisTubeError(\"CANNOT_RESOLVE_SONG\", input);\n }\n\n async _getPluginFromURL(url: string): Promise<DisTubePlugin | null> {\n for (const plugin of this.plugins) if (await plugin.validate(url)) return plugin;\n return null;\n }\n\n _getPluginFromSong(song: Song): Promise<DisTubePlugin | null>;\n _getPluginFromSong<T extends PluginType>(\n song: Song,\n types: T[],\n validate?: boolean,\n ): Promise<(DisTubePlugin & { type: T }) | null>;\n async _getPluginFromSong<T extends PluginType>(\n song: Song,\n types?: T[],\n validate = true,\n ): Promise<(DisTubePlugin & { type: T }) | null> {\n if (!types || types.includes(<T>song.plugin?.type)) return song.plugin as DisTubePlugin & { type: T };\n if (!song.url) return null;\n for (const plugin of this.plugins) {\n if ((!types || types.includes(<T>plugin?.type)) && (!validate || (await plugin.validate(song.url)))) {\n return plugin as DisTubePlugin & { type: T };\n }\n }\n return null;\n }\n\n async #searchSong(query: string, options: ResolveOptions = {}, getStreamURL = false): Promise<Song | null> {\n const plugins = this.plugins.filter(p => p.type === PluginType.EXTRACTOR);\n if (!plugins.length) throw new DisTubeError(\"NO_EXTRACTOR_PLUGIN\");\n for (const plugin of plugins) {\n this.debug(`[${plugin.constructor.name}] Searching for song: ${query}`);\n const result = await plugin.searchSong(query, options);\n if (result) {\n if (getStreamURL && result.stream.playFromSource) result.stream.url = await plugin.getStreamURL(result);\n return result;\n }\n }\n return null;\n }\n\n /**\n * Get {@link Song}'s stream info and attach it to the song.\n * @param song - A Song\n */\n async attachStreamInfo(song: Song) {\n if (song.stream.playFromSource) {\n if (song.stream.url) return;\n this.debug(`[DisTubeHandler] Getting stream info: ${song}`);\n const plugin = await this._getPluginFromSong(song, [PluginType.EXTRACTOR, PluginType.PLAYABLE_EXTRACTOR]);\n if (!plugin) throw new DisTubeError(\"NOT_SUPPORTED_SONG\", song.toString());\n this.debug(`[${plugin.constructor.name}] Getting stream URL: ${song}`);\n song.stream.url = await plugin.getStreamURL(song);\n if (!song.stream.url) throw new DisTubeError(\"CANNOT_GET_STREAM_URL\", song.toString());\n } else {\n if (song.stream.song?.stream?.playFromSource && song.stream.song.stream.url) return;\n this.debug(`[DisTubeHandler] Getting stream info: ${song}`);\n const plugin = await this._getPluginFromSong(song, [PluginType.INFO_EXTRACTOR]);\n if (!plugin) throw new DisTubeError(\"NOT_SUPPORTED_SONG\", song.toString());\n this.debug(`[${plugin.constructor.name}] Creating search query for: ${song}`);\n const query = await plugin.createSearchQuery(song);\n if (!query) throw new DisTubeError(\"CANNOT_GET_SEARCH_QUERY\", song.toString());\n const altSong = await this.#searchSong(query, { metadata: song.metadata, member: song.member }, true);\n if (!altSong || !altSong.stream.playFromSource) throw new DisTubeError(\"NO_RESULT\", query || song.toString());\n song.stream.song = altSong;\n }\n }\n\n async followRedirectLink(url: string, maxRedirect = MAX_REDIRECT_DEPTH): Promise<string> {\n if (maxRedirect === 0) return url;\n\n const res = await request(url, {\n method: \"HEAD\",\n headers: {\n \"user-agent\":\n \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) \" +\n \"Chrome/129.0.0.0 Safari/537.3\",\n },\n });\n\n if (HTTP_REDIRECT_CODES.has(res.statusCode ?? 200)) {\n let location = res.headers.location;\n if (typeof location !== \"string\") location = location?.[0] ?? url;\n return this.followRedirectLink(location, --maxRedirect);\n }\n\n return url;\n }\n}\n","import { inspect } from \"node:util\";\n\nconst ERROR_MESSAGES = {\n INVALID_TYPE: (expected: (number | string) | readonly (number | string)[], got: any, name?: string) =>\n `Expected ${\n Array.isArray(expected) ? expected.map(e => (typeof e === \"number\" ? e : `'${e}'`)).join(\" or \") : `'${expected}'`\n }${name ? ` for '${name}'` : \"\"}, but got ${inspect(got)} (${typeof got})`,\n NUMBER_COMPARE: (name: string, expected: string, value: number) => `'${name}' must be ${expected} ${value}`,\n EMPTY_ARRAY: (name: string) => `'${name}' is an empty array`,\n EMPTY_FILTERED_ARRAY: (name: string, type: string) => `There is no valid '${type}' in the '${name}' array`,\n EMPTY_STRING: (name: string) => `'${name}' string must not be empty`,\n INVALID_KEY: (obj: string, key: string) => `'${key}' does not need to be provided in ${obj}`,\n MISSING_KEY: (obj: string, key: string) => `'${key}' needs to be provided in ${obj}`,\n MISSING_KEYS: (obj: string, key: string[], all: boolean) =>\n `${key.map(k => `'${k}'`).join(all ? \" and \" : \" or \")} need to be provided in ${obj}`,\n\n MISSING_INTENTS: (i: string) => `${i} intent must be provided for the Client`,\n DISABLED_OPTION: (o: string) => `DisTubeOptions.${o} is disabled`,\n ENABLED_OPTION: (o: string) => `DisTubeOptions.${o} is enabled`,\n\n NOT_IN_VOICE: \"User is not in any voice channel\",\n VOICE_FULL: \"The voice channel is full\",\n VOICE_ALREADY_CREATED: \"This guild already has a voice connection which is not managed by DisTube\",\n VOICE_CONNECT_FAILED: (s: number) => `Cannot connect to the voice channel after ${s} seconds`,\n VOICE_MISSING_PERMS: \"I do not have permission to join this voice channel\",\n VOICE_RECONNECT_FAILED: \"Cannot reconnect to the voice channel\",\n VOICE_DIFFERENT_GUILD: \"Cannot join a voice channel in a different guild\",\n VOICE_DIFFERENT_CLIENT: \"Cannot join a voice channel created by a different client\",\n\n FFMPEG_EXITED: (code: number) => `ffmpeg exited with code ${code}`,\n FFMPEG_NOT_INSTALLED: (path: string) => `ffmpeg is not installed at '${path}' path`,\n ENCRYPTION_LIBRARIES_MISSING:\n \"Cannot play audio as no valid encryption package is installed and your node doesn't support aes-256-gcm.\\n\" +\n \"Please install @noble/ciphers, @stablelib/xchacha20poly1305, sodium-native or libsodium-wrappers.\",\n\n NO_QUEUE: \"There is no playing queue in this guild\",\n QUEUE_EXIST: \"This guild has a Queue already\",\n QUEUE_STOPPED: \"The queue has been stopped already\",\n PAUSED: \"The queue has been paused already\",\n RESUMED: \"The queue has been playing already\",\n NO_PREVIOUS: \"There is no previous song in this queue\",\n NO_UP_NEXT: \"There is no up next song\",\n NO_SONG_POSITION: \"Does not have any song at this position\",\n NO_PLAYING_SONG: \"There is no playing song in the queue\",\n NO_EXTRACTOR_PLUGIN: \"There is no extractor plugin in the DisTubeOptions.plugins, please add one for searching songs\",\n NO_RELATED: \"Cannot find any related songs\",\n CANNOT_PLAY_RELATED: \"Cannot play the related song\",\n UNAVAILABLE_VIDEO: \"This video is unavailable\",\n UNPLAYABLE_FORMATS: \"No playable format found\",\n NON_NSFW: \"Cannot play age-restricted content in non-NSFW channel\",\n NOT_SUPPORTED_URL: \"This url is not supported\",\n NOT_SUPPORTED_SONG: (song: string) => `There is no plugin supporting this song (${song})`,\n NO_VALID_SONG: \"'songs' array does not have any valid Song or url\",\n CANNOT_RESOLVE_SONG: (t: any) => `Cannot resolve ${inspect(t)} to a Song`,\n CANNOT_GET_STREAM_URL: (song: string) => `Cannot get stream url with this song (${song})`,\n CANNOT_GET_SEARCH_QUERY: (song: string) => `Cannot get search query with this song (${song})`,\n NO_RESULT: (query: string) => `Cannot find any song with this query (${query})`,\n NO_STREAM_URL: (song: string) => `No stream url attached (${song})`,\n\n EMPTY_FILTERED_PLAYLIST:\n \"There is no valid video in the playlist\\n\" +\n \"Maybe age-restricted contents is filtered because you are in non-NSFW channel\",\n EMPTY_PLAYLIST: \"There is no valid video in the playlist\",\n};\n\ntype ErrorMessage = typeof ERROR_MESSAGES;\ntype ErrorCode = keyof ErrorMessage;\ntype StaticErrorCode = { [K in ErrorCode]-?: ErrorMessage[K] extends string ? K : never }[ErrorCode];\ntype TemplateErrorCode = Exclude<keyof typeof ERROR_MESSAGES, StaticErrorCode>;\n\nconst haveCode = (code: string): code is ErrorCode => Object.keys(ERROR_MESSAGES).includes(code);\nconst parseMessage = (m: string | ((...x: any) => string), ...args: any) => (typeof m === \"string\" ? m : m(...args));\nconst getErrorMessage = (code: string, ...args: any): string =>\n haveCode(code) ? parseMessage(ERROR_MESSAGES[code], ...args) : args[0];\nexport class DisTubeError<T extends string = any> extends Error {\n errorCode: string;\n constructor(code: T extends StaticErrorCode ? T : never);\n constructor(code: T extends TemplateErrorCode ? T : never, ...args: Parameters<ErrorMessage[typeof code]>);\n constructor(code: TemplateErrorCode, _: never);\n constructor(code: T extends ErrorCode ? never : T, message: string);\n constructor(code: string, ...args: any) {\n super(getErrorMessage(code, ...args));\n\n this.errorCode = code;\n if (Error.captureStackTrace) Error.captureStackTrace(this, DisTubeError);\n }\n\n override get name() {\n return `DisTubeError [${this.errorCode}]`;\n }\n\n get code() {\n return this.errorCode;\n }\n}\n","import { URL } from \"node:url\";\nimport type {\n Client,\n ClientOptions,\n Guild,\n GuildMember,\n GuildTextBasedChannel,\n Message,\n Snowflake,\n VoiceBasedChannel,\n VoiceState,\n} from \"discord.js\";\nimport { Constants, GatewayIntentBits, IntentsBitField, SnowflakeUtil } from \"discord.js\";\nimport { DisTubeVoice } from \"./core/DisTubeVoice\";\nimport { DisTubeError } from \"./struct/DisTubeError\";\nimport { Queue } from \"./struct/Queue\";\nimport type { GuildIdResolvable } from \"./type\";\n\nconst formatInt = (int: number) => (int < 10 ? `0${int}` : int);\n\n/**\n * Format duration to string\n * @param sec - Duration in seconds\n */\nexport function formatDuration(sec: number): string {\n if (!sec || !Number(sec)) return \"00:00\";\n const seconds = Math.floor(sec % 60);\n const minutes = Math.floor((sec % 3600) / 60);\n const hours = Math.floor(sec / 3600);\n if (hours > 0) return `${formatInt(hours)}:${formatInt(minutes)}:${formatInt(seconds)}`;\n if (minutes > 0) return `${formatInt(minutes)}:${formatInt(seconds)}`;\n return `00:${formatInt(seconds)}`;\n}\nconst SUPPORTED_PROTOCOL = [\"https:\", \"http:\", \"file:\"] as const;\n/**\n * Check if the string is an URL\n * @param input - input\n */\nexport function isURL(input: any): input is `${(typeof SUPPORTED_PROTOCOL)[number]}//${string}` {\n if (typeof input !== \"string\" || input.includes(\" \")) return false;\n try {\n const url = new URL(input);\n if (!SUPPORTED_PROTOCOL.some(p => p === url.protocol)) return false;\n } catch {\n return false;\n }\n return true;\n}\n/**\n * Check if the Client has enough intents to using DisTube\n * @param options - options\n */\nexport function checkIntents(options: ClientOptions): void {\n const intents = options.intents instanceof IntentsBitField ? options.intents : new IntentsBitField(options.intents);\n if (!intents.has(GatewayIntentBits.GuildVoiceStates)) throw new DisTubeError(\"MISSING_INTENTS\", \"GuildVoiceStates\");\n}\n\n/**\n * Check if the voice channel is empty\n * @param voiceState - voiceState\n */\nexport function isVoiceChannelEmpty(voiceState: VoiceState): boolean {\n const guild = voiceState.guild;\n const clientId = voiceState.client.user?.id;\n if (!guild || !clientId) return false;\n const voiceChannel = guild.members.me?.voice?.channel;\n if (!voiceChannel) return false;\n const members = voiceChannel.members.filter(m => !m.user.bot);\n return !members.size;\n}\n\nexport function isSnowflake(id: any): id is Snowflake {\n try {\n return SnowflakeUtil.deconstruct(id).timestamp > SnowflakeUtil.epoch;\n } catch {\n return false;\n }\n}\n\nexport function isMemberInstance(member: any): member is GuildMember {\n return (\n Boolean(member) &&\n isSnowflake(member.id) &&\n isSnowflake(member.guild?.id) &&\n isSnowflake(member.user?.id) &&\n member.id === member.user.id\n );\n}\n\nexport function isTextChannelInstance(channel: any): channel is GuildTextBasedChannel {\n return (\n Boolean(channel) &&\n isSnowflake(channel.id) &&\n isSnowflake(channel.guildId || channel.guild?.id) &&\n Constants.TextBasedChannelTypes.includes(channel.type) &&\n typeof channel.send === \"function\" &&\n (typeof channel.nsfw === \"boolean\" || typeof channel.parent?.nsfw === \"boolean\")\n );\n}\n\nexport function isMessageInstance(message: any): message is Message<true> {\n // Simple check for using distube normally\n return (\n Boolean(message) &&\n isSnowflake(message.id) &&\n isSnowflake(message.guildId || message.guild?.id) &&\n isMemberInstance(message.member) &&\n isTextChannelInstance(message.channel) &&\n Constants.NonSystemMessageTypes.includes(message.type) &&\n message.member.id === message.author?.id\n );\n}\n\nexport function isSupportedVoiceChannel(channel: any): channel is VoiceBasedChannel {\n return (\n Boolean(channel) &&\n isSnowflake(channel.id) &&\n isSnowflake(channel.guildId || channel.guild?.id) &&\n Constants.VoiceBasedChannelTypes.includes(channel.type)\n );\n}\n\nexport function isGuildInstance(guild: any): guild is Guild {\n return Boolean(guild) && isSnowflake(guild.id) && isSnowflake(guild.ownerId) && typeof guild.name === \"string\";\n}\n\nexport function resolveGuildId(resolvable: GuildIdResolvable): Snowflake {\n let guildId: string | undefined;\n if (typeof resolvable === \"string\") {\n guildId = resolvable;\n } else if (isObject(resolvable)) {\n if (\"guildId\" in resolvable && resolvable.guildId) {\n guildId = resolvable.guildId;\n } else if (resolvable instanceof Queue || resolvable instanceof DisTubeVoice || isGuildInstance(resolvable)) {\n guildId = resolvable.id;\n } else if (\"guild\" in resolvable && isGuildInstance(resolvable.guild)) {\n guildId = resolvable.guild.id;\n }\n }\n if (!isSnowflake(guildId)) throw new DisTubeError(\"INVALID_TYPE\", \"GuildIdResolvable\", resolvable);\n return guildId;\n}\n\nexport function isClientInstance(client: any): client is Client {\n return Boolean(client) && typeof client.login === \"function\";\n}\n\nexport function checkInvalidKey(\n target: Record<string, any>,\n source: Record<string, any> | string[],\n sourceName: string,\n) {\n if (!isObject(target)) throw new DisTubeError(\"INVALID_TYPE\", \"object\", target, sourceName);\n const sourceKeys = Array.isArray(source) ? source : objectKeys(source);\n const invalidKey = objectKeys(target).find(key => !sourceKeys.includes(key));\n if (invalidKey) throw new DisTubeError(\"INVALID_KEY\", sourceName, invalidKey);\n}\n\nexport function isObject(obj: any): obj is object {\n return typeof obj === \"object\" && obj !== null && !Array.isArray(obj);\n}\n\nexport type KeyOf<T> = T extends object ? (keyof T)[] : [];\nexport function objectKeys<T>(obj: T): KeyOf<T> {\n if (!isObject(obj)) return [] as KeyOf<T>;\n return Object.keys(obj) as KeyOf<T>;\n}\n\nexport function isNsfwChannel(channel?: GuildTextBasedChannel): boolean {\n if (!isTextChannelInstance(channel)) return false;\n if (channel.isThread()) return channel.parent?.nsfw ?? false;\n return channel.nsfw;\n}\n\nexport type Falsy = undefined | null | false | 0 | \"\";\nexport const isTruthy = <T>(x: T | Falsy): x is T => Boolean(x);\n\nexport const checkEncryptionLibraries = async () => {\n if (await import(\"node:crypto\").then(m => m.getCiphers().includes(\"aes-256-gcm\"))) return true;\n for (const lib of [\n \"@noble/ciphers\",\n \"@stablelib/xchacha20poly1305\",\n \"sodium-native\",\n \"sodium\",\n \"libsodium-wrappers\",\n \"tweetnacl\",\n ]) {\n try {\n await import(lib);\n return true;\n } catch {}\n }\n return false;\n};\n","import type { AudioPlayer, VoiceConnection } from \"@discordjs/voice\";\nimport {\n AudioPlayerStatus,\n createAudioPlayer,\n entersState,\n joinVoiceChannel,\n VoiceConnectionDisconnectReason,\n VoiceConnectionStatus,\n} from \"@discordjs/voice\";\nimport type { Snowflake, VoiceBasedChannel, VoiceState } from \"discord.js\";\nimport { Constants } from \"discord.js\";\nimport { TypedEmitter } from \"tiny-typed-emitter\";\nimport { JOIN_TIMEOUT_MS, RECONNECT_MAX_ATTEMPTS, RECONNECT_TIMEOUT_MS } from \"../constant\";\nimport { DisTubeError } from \"../struct/DisTubeError\";\nimport type { DisTubeVoiceEvents } from \"../type\";\nimport { checkEncryptionLibraries, isSupportedVoiceChannel } from \"../util\";\nimport type { DisTubeStream } from \"./DisTubeStream\";\nimport type { DisTubeVoiceManager } from \"./manager/DisTubeVoiceManager\";\n\n/**\n * Create a voice connection to the voice channel\n */\nexport class DisTubeVoice extends TypedEmitter<DisTubeVoiceEvents> {\n readonly id: Snowflake;\n readonly voices: DisTubeVoiceManager;\n readonly audioPlayer: AudioPlayer;\n connection!: VoiceConnection;\n emittedError!: boolean;\n isDisconnected = false;\n stream?: DisTubeStream;\n pausingStream?: DisTubeStream;\n #channel!: VoiceBasedChannel;\n #volume = 100;\n constructor(voiceManager: DisTubeVoiceManager, channel: VoiceBasedChannel) {\n super();\n /**\n * The voice manager that instantiated this connection\n */\n this.voices = voiceManager;\n this.id = channel.guildId;\n this.channel = channel;\n this.voices.add(this.id, this);\n this.audioPlayer = createAudioPlayer()\n .on(AudioPlayerStatus.Idle, oldState => {\n if (oldState.status !== AudioPlayerStatus.Idle) this.emit(\"finish\");\n })\n .on(\"error\", (error: NodeJS.ErrnoException) => {\n if (this.emittedError) return;\n this.emittedError = true;\n this.emit(\"error\", error);\n });\n this.connection\n .on(VoiceConnectionStatus.Disconnected, (_, newState) => {\n if (newState.reason === VoiceConnectionDisconnectReason.Manual) {\n // User disconnect\n this.leave();\n } else if (newState.reason === VoiceConnectionDisconnectReason.WebSocketClose && newState.closeCode === 4014) {\n // Move to other channel\n entersState(this.connection, VoiceConnectionStatus.Connecting, RECONNECT_TIMEOUT_MS).catch(() => {\n if (\n ![VoiceConnectionStatus.Ready, VoiceConnectionStatus.Connecting].includes(this.connection.state.status)\n ) {\n this.leave();\n }\n });\n } else if (this.connection.rejoinAttempts < RECONNECT_MAX_ATTEMPTS) {\n // Try to rejoin\n setTimeout(\n () => {\n this.connection.rejoin();\n },\n (this.connection.rejoinAttempts + 1) * RECONNECT_TIMEOUT_MS,\n ).unref();\n } else if (this.connection.state.status !== VoiceConnectionStatus.Destroyed) {\n // Leave after 5 attempts\n this.leave(new DisTubeError(\"VOICE_RECONNECT_FAILED\"));\n }\n })\n .on(VoiceConnectionStatus.Destroyed, () => {\n this.leave();\n })\n .on(\"error\", () => undefined);\n this.connection.subscribe(this.audioPlayer);\n }\n /**\n * The voice channel id the bot is in\n */\n get channelId() {\n return this.connection?.joinConfig?.channelId ?? undefined;\n }\n get channel() {\n if (!this.channelId) return this.#channel;\n if (this.#channel?.id === this.channelId) return this.#channel;\n const channel = this.voices.client.channels.cache.get(this.channelId);\n if (!channel) return this.#channel;\n for (const type of Constants.VoiceBasedChannelTypes) {\n if (channel.type === type) {\n this.#channel = channel;\n return channel;\n }\n }\n return this.#channel;\n }\n set channel(channel: VoiceBasedChannel) {\n if (!isSupportedVoiceChannel(channel)) {\n throw new DisTubeError(\"INVALID_TYPE\", \"BaseGuildVoiceChannel\", channel, \"DisTubeVoice#channel\");\n }\n if (channel.guildId !== this.id) throw new DisTubeError(\"VOICE_DIFFERENT_GUILD\");\n if (channel.client.user?.id !== this.voices.client.user?.id) throw new DisTubeError(\"VOICE_DIFFERENT_CLIENT\");\n if (channel.id === this.channelId) return;\n if (!channel.joinable) {\n if (channel.full) throw new DisTubeError(\"VOICE_FULL\");\n else throw new DisTubeError(\"VOICE_MISSING_PERMS\");\n }\n this.connection = this.#join(channel);\n this.#channel = channel;\n }\n #join(channel: VoiceBasedChannel) {\n return joinVoiceChannel({\n channelId: channel.id,\n guildId: this.id,\n adapterCreator: channel.guild.voiceAdapterCreator,\n group: channel.client.user?.id,\n });\n }\n /**\n * Join a voice channel with this connection\n * @param channel - A voice channel\n */\n async join(channel?: VoiceBasedChannel): Promise<DisTubeVoice> {\n if (channel) this.channel = channel;\n try {\n await entersState(this.connection, VoiceConnectionStatus.Ready, JOIN_TIMEOUT_MS);\n } catch {\n if (this.connection.state.status === VoiceConnectionStatus.Ready) return this;\n if (this.connection.state.status !== VoiceConnectionStatus.Destroyed) this.connection.destroy();\n this.voices.remove(this.id);\n throw new DisTubeError(\"VOICE_CONNECT_FAILED\", JOIN_TIMEOUT_MS / 1000);\n }\n return this;\n }\n /**\n * Leave the voice channel of this connection\n * @param error - Optional, an error to emit with 'error' event.\n */\n leave(error?: Error) {\n this.stop(true);\n if (!this.isDisconnected) {\n this.emit(\"disconnect\", error);\n this.isDisconnected = true;\n }\n if (this.connection.state.status !== VoiceConnectionStatus.Destroyed) this.connection.destroy();\n this.voices.remove(this.id);\n }\n /**\n * Stop the playing stream\n * @param force - If true, will force the {@link DisTubeVoice#audioPlayer} to enter the Idle state even\n * if the {@link DisTubeStream#audioResource} has silence padding frames.\n */\n stop(force = false) {\n this.audioPlayer.stop(force);\n }\n #streamErrorHandler?: (error: NodeJS.ErrnoException) => void;\n /**\n * Play a {@link DisTubeStream}\n * @param dtStream - DisTubeStream\n */\n async play(dtStream: DisTubeStream) {\n if (!(await checkEncryptionLibraries())) {\n dtStream.kill();\n throw new DisTubeError(\"ENCRYPTION_LIBRARIES_MISSING\");\n }\n this.emittedError = false;\n // Remove previous error listener to prevent memory leaks\n if (this.stream && this.#streamErrorHandler) {\n this.stream.off(\"error\", this.#streamErrorHandler);\n }\n this.#streamErrorHandler = (error: NodeJS.ErrnoException) => {\n if (this.emittedError || error.code === \"ERR_STREAM_PREMATURE_CLOSE\") return;\n this.emittedError = true;\n this.emit(\"error\", error);\n };\n dtStream.on(\"error\", this.#streamErrorHandler);\n if (this.audioPlayer.state.status !== AudioPlayerStatus.Paused) {\n this.audioPlayer.play(dtStream.audioResource);\n this.stream?.kill();\n dtStream.spawn();\n } else if (!this.pausingStream) {\n this.pausingStream = this.stream;\n }\n this.stream = dtStream;\n this.volume = this.#volume;\n }\n set volume(volume: number) {\n if (typeof volume !== \"number\" || Number.isNaN(volume)) {\n throw new DisTubeError(\"INVALID_TYPE\", \"number\", volume, \"volume\");\n }\n if (volume < 0) {\n throw new DisTubeError(\"NUMBER_COMPARE\", \"Volume\", \"bigger or equal to\", 0);\n }\n this.#volume = volume;\n this.stream?.setVolume((this.#volume / 100) ** (0.5 / Math.log10(2)));\n }\n /**\n * Get or set the volume percentage\n */\n get volume() {\n return this.#volume;\n }\n /**\n * Playback duration of the audio resource in seconds (time since playback started)\n */\n get playbackDuration() {\n return (this.stream?.audioResource?.playbackDuration ?? 0) / 1000;\n }\n /**\n * Current playback time in seconds, accounting for seek offset\n */\n get playbackTime() {\n return this.playbackDuration + (this.stream?.seekTime ?? 0);\n }\n pause() {\n this.audioPlayer.pause();\n }\n unpause() {\n const state = this.audioPlayer.state;\n if (state.status !== AudioPlayerStatus.Paused) return;\n if (this.stream?.audioResource && state.resource !== this.stream.audioResource) {\n this.audioPlayer.play(this.stream.audioResource);\n this.stream.spawn();\n this.pausingStream?.kill();\n delete this.pausingStream;\n } else {\n this.audioPlayer.unpause();\n }\n }\n /**\n * Whether the bot is self-deafened\n */\n get selfDeaf(): boolean {\n return this.connection.joinConfig.selfDeaf;\n }\n /**\n * Whether the bot is self-muted\n */\n get selfMute(): boolean {\n return this.connection.joinConfig.selfMute;\n }\n /**\n * Self-deafens/undeafens the bot.\n * @param selfDeaf - Whether or not the bot should be self-deafened\n * @returns true if the voice state was successfully updated, otherwise false\n */\n setSelfDeaf(selfDeaf: boolean): boolean {\n if (typeof selfDeaf !== \"boolean\") {\n throw new DisTubeError(\"INVALID_TYPE\", \"boolean\", selfDeaf, \"selfDeaf\");\n }\n return this.connection.rejoin({\n ...this.connection.joinConfig,\n selfDeaf,\n });\n }\n /**\n * Self-mutes/unmutes the bot.\n * @param selfMute - Whether or not the bot should be self-muted\n * @returns true if the voice state was successfully updated, otherwise false\n */\n setSelfMute(selfMute: boolean): boolean {\n if (typeof selfMute !== \"boolean\") {\n throw new DisTubeError(\"INVALID_TYPE\", \"boolean\", selfMute, \"selfMute\");\n }\n return this.connection.rejoin({\n ...this.connection.joinConfig,\n selfMute,\n });\n }\n /**\n * The voice state of this connection\n */\n get voiceState(): VoiceState | undefined {\n return this.channel?.guild?.members?.me?.voice;\n }\n}\n","import { Collection } from \"discord.js\";\nimport { DisTubeBase } from \"../DisTubeBase\";\n\n/**\n * Manages the collection of a data model.\n */\nexport abstract class BaseManager<V> extends DisTubeBase {\n /**\n * The collection of items for this manager.\n */\n collection = new Collection<string, V>();\n /**\n * The size of the collection.\n */\n get size() {\n return this.collection.size;\n }\n}\n","import type { FFmpegArg as FFmpegArgsValue, Filter, FilterResolvable, Queue } from \"../..\";\nimport { DisTubeError } from \"../..\";\nimport { BaseManager } from \"./BaseManager\";\n\n/**\n * Manage filters of a playing {@link Queue}\n */\nexport class FilterManager extends BaseManager<Filter> {\n /**\n * The queue to manage\n */\n queue: Queue;\n constructor(queue: Queue) {\n super(queue.distube);\n this.queue = queue;\n }\n\n #resolve(filter: FilterResolvable): Filter {\n if (typeof filter === \"object\" && typeof filter.name === \"string\" && typeof filter.value === \"string\") {\n return filter;\n }\n if (typeof filter === \"string\" && Object.hasOwn(this.distube.filters, filter)) {\n return {\n name: filter,\n value: this.distube.filters[filter],\n };\n }\n throw new DisTubeError(\"INVALID_TYPE\", \"FilterResolvable\", filter, \"filter\");\n }\n\n #apply() {\n this.queue._beginTime = this.queue.currentTime;\n this.queue.play(false);\n }\n\n /**\n * Enable a filter or multiple filters to the manager\n * @param filterOrFilters - The filter or filters to enable\n * @param override - Wether or not override the applied filter with new filter value\n */\n add(filterOrFilters: FilterResolvable | FilterResolvable[], override = false) {\n if (Array.isArray(filterOrFilters)) {\n for (const filter of filterOrFilters) {\n const ft = this.#resolve(filter);\n if (override || !this.has(ft)) this.collection.set(ft.name, ft);\n }\n } else {\n const ft = this.#resolve(filterOrFilters);\n if (override || !this.has(ft)) this.collection.set(ft.name, ft);\n }\n this.#apply();\n return this;\n }\n\n /**\n * Clear enabled filters of the manager\n */\n clear() {\n return this.set([]);\n }\n\n /**\n * Set the filters applied to the manager\n * @param filters - The filters to apply\n */\n set(filters: FilterResolvable[]) {\n if (!Array.isArray(filters)) throw new DisTubeError(\"INVALID_TYPE\", \"Array<FilterResolvable>\", filters, \"filters\");\n this.collection.clear();\n for (const f of filters) {\n const filter = this.#resolve(f);\n this.collection.set(filter.name, filter);\n }\n this.#apply();\n return this;\n }\n\n #removeFn(f: FilterResolvable) {\n return this.collection.delete(this.#resolve(f).name);\n }\n\n /**\n * Disable a filter or multiple filters\n * @param filterOrFilters - The filter or filters to disable\n */\n remove(filterOrFilters: FilterResolvable | FilterResolvable[]) {\n if (Array.isArray(filterOrFilters)) filterOrFilters.forEach(f => this.#removeFn(f));\n else this.#removeFn(filterOrFilters);\n this.#apply();\n return this;\n }\n\n /**\n * Check whether a filter enabled or not\n * @param filter - The filter to check\n */\n has(filter: FilterResolvable) {\n return this.collection.has(typeof filter === \"string\" ? filter : this.#resolve(filter).name);\n }\n\n /**\n * Array of enabled filter names\n */\n get names(): string[] {\n return [...this.collection.keys()];\n }\n\n /**\n * Array of enabled filters\n */\n get values(): Filter[] {\n return [...this.collection.values()];\n }\n\n get ffmpegArgs(): FFmpegArgsValue {\n return this.size ? { af: this.values.map(f => f.value).join(\",\") } : {};\n }\n\n override toString() {\n return this.names.toString();\n }\n}\n","import type {\n Guild,\n GuildMember,\n GuildTextBasedChannel,\n Interaction,\n Message,\n Snowflake,\n VoiceBasedChannel,\n VoiceState,\n} from \"discord.js\";\nimport type {\n DisTubeError,\n DisTubeVoice,\n ExtractorPlugin,\n InfoExtractorPlugin,\n PlayableExtractorPlugin,\n Playlist,\n Queue,\n Song,\n} from \".\";\n\nexport type Awaitable<T = any> = T | PromiseLike<T>;\n\nexport enum Events {\n ERROR = \"error\",\n ADD_LIST = \"addList\",\n ADD_SONG = \"addSong\",\n PLAY_SONG = \"playSong\",\n FINISH_SONG = \"finishSong\",\n EMPTY = \"empty\",\n FINISH = \"finish\",\n INIT_QUEUE = \"initQueue\",\n NO_RELATED = \"noRelated\",\n DISCONNECT = \"disconnect\",\n DELETE_QUEUE = \"deleteQueue\",\n FFMPEG_DEBUG = \"ffmpegDebug\",\n DEBUG = \"debug\",\n}\n\nexport type DisTubeEvents = {\n [Events.ADD_LIST]: [queue: Queue, playlist: Playlist];\n [Events.ADD_SONG]: [queue: Queue, song: Song];\n [Events.DELETE_QUEUE]: [queue: Queue];\n [Events.DISCONNECT]: [queue: Queue];\n [Events.ERROR]: [error: Error, queue: Queue, song: Song | undefined];\n [Events.FFMPEG_DEBUG]: [debug: string];\n [Events.DEBUG]: [debug: string];\n [Events.FINISH]: [queue: Queue];\n [Events.FINISH_SONG]: [queue: Queue, song: Song];\n [Events.INIT_QUEUE]: [queue: Queue];\n [Events.NO_RELATED]: [queue: Queue, error: DisTubeError];\n [Events.PLAY_SONG]: [queue: Queue, song: Song];\n};\n\nexport type TypedDisTubeEvents = {\n [K in keyof DisTubeEvents]: (...args: DisTubeEvents[K]) => Awaitable;\n};\n\nexport type DisTubeVoiceEvents = {\n disconnect: (error?: Error) => Awaitable;\n error: (error: Error) => Awaitable;\n finish: () => Awaitable;\n};\n\n/**\n * An FFmpeg audio filter object\n * ```ts\n * {\n * name: \"bassboost\",\n * value: \"bass=g=10\"\n * }\n * ```ts\n */\nexport interface Filter {\n /**\n * Name of the filter\n */\n name: string;\n /**\n * FFmpeg audio filter argument\n */\n value: string;\n}\n\n/**\n * Data that resolves to give an FFmpeg audio filter. This can be:\n * - A name of a default filters or custom filters (`string`)\n * - A {@link Filter} object\n * @see {@link defaultFilters}\n * @see {@link DisTubeOptions|DisTubeOptions.customFilters}\n */\nexport type FilterResolvable = string | Filter;\n\n/**\n * FFmpeg Filters\n * ```ts\n * {\n * \"Filter Name\": \"Filter Value\",\n * \"bassboost\": \"bass=g=10\"\n * }\n * ```\n * @see {@link defaultFilters}\n */\nexport type Filters = Record<string, string>;\n\n/**\n * DisTube options\n */\nexport type DisTubeOptions = {\n /**\n * DisTube plugins.\n * The order of this effects the priority of the plugins when verifying the input.\n */\n plugins?: DisTubePlugin[];\n /**\n * Whether or not emitting {@link Events.PLAY_SONG} event when looping a song\n * or next song is the same as the previous one\n */\n emitNewSongOnly?: boolean;\n /**\n * Whether or not saving the previous songs of the queue and enable {@link\n * DisTube#previous} method. Disable it may help to reduce the memory usage\n */\n savePreviousSongs?: boolean;\n /**\n * Override {@link defaultFilters} or add more ffmpeg filters\n */\n customFilters?: Filters;\n /**\n * Whether or not playing age-restricted content and disabling safe search in\n * non-NSFW channel\n */\n nsfw?: boolean;\n /**\n * Whether or not emitting `addSong` event when creating a new Queue\n */\n emitAddSongWhenCreatingQueue?: boolean;\n /**\n * Whether or not emitting `addList` event when creating a new Queue\n */\n emitAddListWhenCreatingQueue?: boolean;\n /**\n * Whether or not joining the new voice channel when using {@link DisTube#play}\n * method\n */\n joinNewVoiceChannel?: boolean;\n /**\n * FFmpeg options\n */\n ffmpeg?: {\n /**\n * FFmpeg path\n */\n path?: string;\n /**\n * FFmpeg default arguments\n */\n args?: Partial<FFmpegArgs>;\n };\n};\n\n/**\n * Data that can be resolved to give a guild id string. This can be:\n * - A guild id string | a guild {@link https://discord.js.org/#/docs/main/stable/class/Snowflake|Snowflake}\n * - A {@link https://discord.js.org/#/docs/main/stable/class/Guild | Guild}\n * - A {@link https://discord.js.org/#/docs/main/stable/class/Message | Message}\n * - A {@link https://discord.js.org/#/docs/main/stable/class/BaseGuildVoiceChannel\n * | BaseGuildVoiceChannel}\n * - A {@link https://discord.js.org/#/docs/main/stable/class/BaseGuildTextChannel\n * | BaseGuildTextChannel}\n * - A {@link https://discord.js.org/#/docs/main/stable/class/VoiceState |\n * VoiceState}\n * - A {@link https://discord.js.org/#/docs/main/stable/class/GuildMember |\n * GuildMember}\n * - A {@link https://discord.js.org/#/docs/main/stable/class/Interaction |\n * Interaction}\n * - A {@link DisTubeVoice}\n * - A {@link Queue}\n */\nexport type GuildIdResolvable =\n | Queue\n | DisTubeVoice\n | Snowflake\n | Message\n | GuildTextBasedChannel\n | VoiceBasedChannel\n | VoiceState\n | Guild\n | GuildMember\n | Interaction\n | string;\n\nexport interface SongInfo {\n plugin: DisTubePlugin | null;\n source: string;\n playFromSource: boolean;\n id: string;\n name?: string;\n isLive?: boolean;\n duration?: number;\n url?: string;\n thumbnail?: string;\n views?: number;\n likes?: number;\n dislikes?: number;\n reposts?: number;\n uploader?: {\n name?: string;\n url?: string;\n };\n ageRestricted?: boolean;\n}\n\nexport interface PlaylistInfo {\n source: string;\n songs: Song[];\n id?: string;\n name?: string;\n url?: string;\n thumbnail?: string;\n}\n\nexport type RelatedSong = Omit<Song, \"related\">;\n\nexport type PlayHandlerOptions = {\n /**\n * [Default: false] Skip the playing song (if exists) and play the added playlist\n * instantly\n */\n skip?: boolean;\n /**\n * [Default: 0] Position of the song/playlist to add to the queue, \\<= 0 to add to\n * the end of the queue\n */\n position?: number;\n /**\n * The default text channel of the queue\n */\n textChannel?: GuildTextBasedChannel;\n};\n\nexport interface JumpOptions {\n /**\n * [Default: false] Whether or not skipped song(s) will be added to the end of the\n * queue\n */\n requeue?: boolean;\n}\n\nexport interface PlayOptions<T = unknown> extends PlayHandlerOptions, ResolveOptions<T> {\n /**\n * Called message (For built-in search events. If this is a {@link\n * https://developer.mozilla.org/en-US/docs/Glossary/Falsy | falsy value}, it will\n * play the first result instead)\n */\n message?: Message;\n}\n\nexport interface ResolveOptions<T = unknown> {\n /**\n * Requested user\n */\n member?: GuildMember;\n /**\n * Metadata\n */\n metadata?: T;\n}\n\nexport interface ResolvePlaylistOptions<T = unknown> extends ResolveOptions<T> {\n /**\n * Source of the playlist\n */\n source?: string;\n}\n\nexport interface CustomPlaylistOptions {\n /**\n * A guild member creating the playlist\n */\n member?: GuildMember;\n /**\n * Whether or not fetch the songs in parallel\n */\n parallel?: boolean;\n /**\n * Metadata\n */\n metadata?: any;\n /**\n * Playlist name\n */\n name?: string;\n /**\n * Playlist source\n */\n source?: string;\n /**\n * Playlist url\n */\n url?: string;\n /**\n * Playlist thumbnail\n */\n thumbnail?: string;\n}\n\n/**\n * The repeat mode of a {@link Queue}\n * - `DISABLED` = 0\n * - `SONG` = 1\n * - `QUEUE` = 2\n */\nexport enum RepeatMode {\n DISABLED,\n SONG,\n QUEUE,\n}\n\n/**\n * All available plugin types:\n * - `EXTRACTOR` = `\"extractor\"`: {@link ExtractorPlugin}\n * - `INFO_EXTRACTOR` = `\"info-extractor\"`: {@link InfoExtractorPlugin}\n * - `PLAYABLE_EXTRACTOR` = `\"playable-extractor\"`: {@link PlayableExtractorPlugin}\n */\nexport enum PluginType {\n EXTRACTOR = \"extractor\",\n INFO_EXTRACTOR = \"info-extractor\",\n PLAYABLE_EXTRACTOR = \"playable-extractor\",\n}\n\nexport type DisTubePlugin = ExtractorPlugin | InfoExtractorPlugin | PlayableExtractorPlugin;\n\nexport type FFmpegArg = Record<string, string | number | boolean | Array<string | null | undefined> | null | undefined>;\n\n/**\n * FFmpeg arguments for different use cases\n */\nexport type FFmpegArgs = {\n global: FFmpegArg;\n input: FFmpegArg;\n output: FFmpegArg;\n};\n\n/**\n * FFmpeg options\n */\nexport type FFmpegOptions = {\n /**\n * Path to the ffmpeg executable\n */\n path: string;\n /**\n * Arguments\n */\n args: FFmpegArgs;\n};\n","class Task {\n resolve!: () => void;\n promise: Promise<void>;\n isPlay: boolean;\n constructor(isPlay: boolean) {\n this.isPlay = isPlay;\n this.promise = new Promise<void>(res => {\n this.resolve = res;\n });\n }\n}\n\n/**\n * Task queuing system\n */\nexport class TaskQueue {\n /**\n * The task array\n */\n #tasks: Task[] = [];\n\n /**\n * Waits for last task finished and queues a new task\n */\n queuing(isPlay = false): Promise<void> {\n const next = this.remaining ? this.#tasks[this.#tasks.length - 1].promise : Promise.resolve();\n this.#tasks.push(new Task(isPlay));\n return next;\n }\n\n /**\n * Removes the finished task and processes the next task\n */\n resolve(): void {\n this.#tasks.shift()?.resolve();\n }\n\n /**\n * The remaining number of tasks\n */\n get remaining(): number {\n return this.#tasks.length;\n }\n\n /**\n * Whether or not having a play task\n */\n get hasPlayTask(): boolean {\n return this.#tasks.some(t => t.isPlay);\n }\n}\n","import type { GuildTextBasedChannel, Snowflake } from \"discord.js\";\nimport { DEFAULT_VOLUME } from \"../constant\";\nimport { DisTubeBase } from \"../core/DisTubeBase\";\nimport type { DisTubeVoice } from \"../core/DisTubeVoice\";\nimport { FilterManager } from \"../core/manager/FilterManager\";\nimport type { DisTube } from \"../DisTube\";\nimport type { DisTubeVoiceEvents, FFmpegArgs, JumpOptions } from \"../type\";\nimport { Events, RepeatMode } from \"../type\";\nimport { formatDuration, objectKeys } from \"../util\";\nimport { DisTubeError } from \"./DisTubeError\";\nimport type { Song } from \"./Song\";\nimport { TaskQueue } from \"./TaskQueue\";\n\n/**\n * Represents a queue.\n */\nexport class Queue extends DisTubeBase {\n /**\n * Queue id (Guild id)\n */\n readonly id: Snowflake;\n /**\n * Voice connection of this queue.\n */\n voice: DisTubeVoice;\n /**\n * List of songs in the queue (The first one is the playing song)\n */\n songs: Song[];\n /**\n * List of the previous songs.\n */\n previousSongs: Song[];\n /**\n * Whether stream is currently stopped.\n */\n stopped: boolean;\n /**\n * Whether or not the queue is active.\n *\n * Note: This remains `true` when paused. It only becomes `false` when stopped.\n * @deprecated Use `!queue.paused` to check if audio is playing. Will be removed in v6.0.\n */\n playing: boolean;\n /**\n * Whether or not the stream is currently paused.\n */\n paused: boolean;\n /**\n * Type of repeat mode (`0` is disabled, `1` is repeating a song, `2` is repeating\n * all the queue). Default value: `0` (disabled)\n */\n repeatMode: RepeatMode;\n /**\n * Whether or not the autoplay mode is enabled. Default value: `false`\n */\n autoplay: boolean;\n /**\n * FFmpeg arguments for the current queue. Default value is defined with {@link DisTubeOptions}.ffmpeg.args.\n * `af` output argument will be replaced with {@link Queue#filters} manager\n */\n ffmpegArgs: FFmpegArgs;\n /**\n * The text channel of the Queue. (Default: where the first command is called).\n */\n textChannel?: GuildTextBasedChannel;\n /**\n * What time in the song to begin (in seconds).\n * @internal\n */\n _beginTime: number;\n #filters: FilterManager;\n /**\n * Whether or not the queue is being updated manually (skip, jump, previous)\n * @internal\n */\n _manualUpdate: boolean;\n /**\n * Task queuing system\n * @internal\n */\n _taskQueue: TaskQueue;\n /**\n * {@link DisTubeVoice} listener\n * @internal\n */\n _listeners?: DisTubeVoiceEvents;\n /**\n * Create a queue for the guild\n * @param distube - DisTube\n * @param voice - Voice connection\n * @param textChannel - Default text channel\n */\n constructor(distube: DisTube, voice: DisTubeVoice, textChannel?: GuildTextBasedChannel) {\n super(distube);\n this.voice = voice;\n this.id = voice.id;\n this.volume = DEFAULT_VOLUME;\n this.songs = [];\n this.previousSongs = [];\n this.stopped = false;\n this._manualUpdate = false;\n this.playing = false;\n this.paused = false;\n this.repeatMode = RepeatMode.DISABLED;\n this.autoplay = false;\n this.#filters = new FilterManager(this);\n this._beginTime = 0;\n this.textChannel = textChannel;\n this._taskQueue = new TaskQueue();\n this._listeners = undefined;\n this.ffmpegArgs = {\n global: { ...this.options.ffmpeg.args.global },\