distube
Version:
A powerful Discord.js module for simplifying music commands and effortless playback of various sources with integrated audio filters.
1,580 lines (1,564 loc) • 87.6 kB
JavaScript
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
AUDIO_CHANNELS: () => AUDIO_CHANNELS,
AUDIO_SAMPLE_RATE: () => AUDIO_SAMPLE_RATE,
BaseManager: () => BaseManager,
DEFAULT_VOLUME: () => DEFAULT_VOLUME,
DisTube: () => DisTube,
DisTubeBase: () => DisTubeBase,
DisTubeError: () => DisTubeError,
DisTubeHandler: () => DisTubeHandler,
DisTubeStream: () => DisTubeStream,
DisTubeVoice: () => DisTubeVoice,
DisTubeVoiceManager: () => DisTubeVoiceManager,
Events: () => Events,
ExtractorPlugin: () => ExtractorPlugin,
FilterManager: () => FilterManager,
GuildIdManager: () => GuildIdManager,
HTTP_REDIRECT_CODES: () => HTTP_REDIRECT_CODES,
InfoExtractorPlugin: () => InfoExtractorPlugin,
InfoExtratorPlugin: () => InfoExtractorPlugin,
JOIN_TIMEOUT_MS: () => JOIN_TIMEOUT_MS,
MAX_REDIRECT_DEPTH: () => MAX_REDIRECT_DEPTH,
Options: () => Options,
PlayableExtractorPlugin: () => PlayableExtractorPlugin,
PlayableExtratorPlugin: () => PlayableExtractorPlugin,
Playlist: () => Playlist,
Plugin: () => Plugin,
PluginType: () => PluginType,
Queue: () => Queue,
QueueManager: () => QueueManager,
RECONNECT_MAX_ATTEMPTS: () => RECONNECT_MAX_ATTEMPTS,
RECONNECT_TIMEOUT_MS: () => RECONNECT_TIMEOUT_MS,
RepeatMode: () => RepeatMode,
Song: () => Song,
TaskQueue: () => TaskQueue,
checkEncryptionLibraries: () => checkEncryptionLibraries,
checkFFmpeg: () => checkFFmpeg,
checkIntents: () => checkIntents,
checkInvalidKey: () => checkInvalidKey,
default: () => DisTube,
defaultFilters: () => defaultFilters,
defaultOptions: () => defaultOptions,
formatDuration: () => formatDuration,
isClientInstance: () => isClientInstance,
isGuildInstance: () => isGuildInstance,
isMemberInstance: () => isMemberInstance,
isMessageInstance: () => isMessageInstance,
isNsfwChannel: () => isNsfwChannel,
isObject: () => isObject,
isSnowflake: () => isSnowflake,
isSupportedVoiceChannel: () => isSupportedVoiceChannel,
isTextChannelInstance: () => isTextChannelInstance,
isTruthy: () => isTruthy,
isURL: () => isURL,
isVoiceChannelEmpty: () => isVoiceChannelEmpty,
objectKeys: () => objectKeys,
resolveGuildId: () => resolveGuildId,
version: () => version
});
module.exports = __toCommonJS(index_exports);
// src/constant.ts
var version = "5.2.3";
var AUDIO_SAMPLE_RATE = 48e3;
var AUDIO_CHANNELS = 2;
var DEFAULT_VOLUME = 50;
var JOIN_TIMEOUT_MS = 3e4;
var RECONNECT_TIMEOUT_MS = 5e3;
var RECONNECT_MAX_ATTEMPTS = 5;
var HTTP_REDIRECT_CODES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
var MAX_REDIRECT_DEPTH = 5;
var defaultFilters = {
"3d": "apulsator=hz=0.125",
bassboost: "bass=g=10",
echo: "aecho=0.8:0.9:1000:0.3",
flanger: "flanger",
gate: "agate",
haas: "haas",
karaoke: "stereotools=mlev=0.1",
nightcore: "asetrate=48000*1.25,aresample=48000,bass=g=5",
reverse: "areverse",
vaporwave: "asetrate=48000*0.8,aresample=48000,atempo=1.1",
mcompand: "mcompand",
phaser: "aphaser",
tremolo: "tremolo",
surround: "surround",
earwax: "earwax"
};
var defaultOptions = {
plugins: [],
emitNewSongOnly: false,
savePreviousSongs: true,
nsfw: false,
emitAddSongWhenCreatingQueue: true,
emitAddListWhenCreatingQueue: true,
joinNewVoiceChannel: true
};
// src/core/DisTubeBase.ts
var DisTubeBase = class {
static {
__name(this, "DisTubeBase");
}
distube;
constructor(distube) {
this.distube = distube;
}
/**
* Emit the {@link DisTube} of this base
* @param eventName - Event name
* @param args - arguments
*/
emit(eventName, ...args) {
return this.distube.emit(eventName, ...args);
}
/**
* Emit error event
* @param error - error
* @param queue - The queue encountered the error
* @param song - The playing song when encountered the error
*/
emitError(error, queue, song) {
this.distube.emitError(error, queue, song);
}
/**
* Emit debug event
* @param message - debug message
*/
debug(message) {
this.distube.debug(message);
}
/**
* The queue manager
*/
get queues() {
return this.distube.queues;
}
/**
* The voice manager
*/
get voices() {
return this.distube.voices;
}
/**
* Discord.js client
*/
get client() {
return this.distube.client;
}
/**
* DisTube options
*/
get options() {
return this.distube.options;
}
/**
* DisTube handler
*/
get handler() {
return this.distube.handler;
}
/**
* DisTube plugins
*/
get plugins() {
return this.distube.plugins;
}
};
// src/core/DisTubeHandler.ts
var import_undici = require("undici");
// src/struct/DisTubeError.ts
var import_node_util = require("util");
var ERROR_MESSAGES = {
INVALID_TYPE: /* @__PURE__ */ __name((expected, got, name) => `Expected ${Array.isArray(expected) ? expected.map((e) => typeof e === "number" ? e : `'${e}'`).join(" or ") : `'${expected}'`}${name ? ` for '${name}'` : ""}, but got ${(0, import_node_util.inspect)(got)} (${typeof got})`, "INVALID_TYPE"),
NUMBER_COMPARE: /* @__PURE__ */ __name((name, expected, value) => `'${name}' must be ${expected} ${value}`, "NUMBER_COMPARE"),
EMPTY_ARRAY: /* @__PURE__ */ __name((name) => `'${name}' is an empty array`, "EMPTY_ARRAY"),
EMPTY_FILTERED_ARRAY: /* @__PURE__ */ __name((name, type) => `There is no valid '${type}' in the '${name}' array`, "EMPTY_FILTERED_ARRAY"),
EMPTY_STRING: /* @__PURE__ */ __name((name) => `'${name}' string must not be empty`, "EMPTY_STRING"),
INVALID_KEY: /* @__PURE__ */ __name((obj, key) => `'${key}' does not need to be provided in ${obj}`, "INVALID_KEY"),
MISSING_KEY: /* @__PURE__ */ __name((obj, key) => `'${key}' needs to be provided in ${obj}`, "MISSING_KEY"),
MISSING_KEYS: /* @__PURE__ */ __name((obj, key, all) => `${key.map((k) => `'${k}'`).join(all ? " and " : " or ")} need to be provided in ${obj}`, "MISSING_KEYS"),
MISSING_INTENTS: /* @__PURE__ */ __name((i) => `${i} intent must be provided for the Client`, "MISSING_INTENTS"),
DISABLED_OPTION: /* @__PURE__ */ __name((o) => `DisTubeOptions.${o} is disabled`, "DISABLED_OPTION"),
ENABLED_OPTION: /* @__PURE__ */ __name((o) => `DisTubeOptions.${o} is enabled`, "ENABLED_OPTION"),
NOT_IN_VOICE: "User is not in any voice channel",
VOICE_FULL: "The voice channel is full",
VOICE_ALREADY_CREATED: "This guild already has a voice connection which is not managed by DisTube",
VOICE_CONNECT_FAILED: /* @__PURE__ */ __name((s) => `Cannot connect to the voice channel after ${s} seconds`, "VOICE_CONNECT_FAILED"),
VOICE_MISSING_PERMS: "I do not have permission to join this voice channel",
VOICE_RECONNECT_FAILED: "Cannot reconnect to the voice channel",
VOICE_DIFFERENT_GUILD: "Cannot join a voice channel in a different guild",
VOICE_DIFFERENT_CLIENT: "Cannot join a voice channel created by a different client",
FFMPEG_EXITED: /* @__PURE__ */ __name((code) => `ffmpeg exited with code ${code}`, "FFMPEG_EXITED"),
FFMPEG_NOT_INSTALLED: /* @__PURE__ */ __name((path) => `ffmpeg is not installed at '${path}' path`, "FFMPEG_NOT_INSTALLED"),
ENCRYPTION_LIBRARIES_MISSING: "Cannot play audio as no valid encryption package is installed and your node doesn't support aes-256-gcm.\nPlease install @noble/ciphers, @stablelib/xchacha20poly1305, sodium-native or libsodium-wrappers.",
NO_QUEUE: "There is no playing queue in this guild",
QUEUE_EXIST: "This guild has a Queue already",
QUEUE_STOPPED: "The queue has been stopped already",
PAUSED: "The queue has been paused already",
RESUMED: "The queue has been playing already",
NO_PREVIOUS: "There is no previous song in this queue",
NO_UP_NEXT: "There is no up next song",
NO_SONG_POSITION: "Does not have any song at this position",
NO_PLAYING_SONG: "There is no playing song in the queue",
NO_EXTRACTOR_PLUGIN: "There is no extractor plugin in the DisTubeOptions.plugins, please add one for searching songs",
NO_RELATED: "Cannot find any related songs",
CANNOT_PLAY_RELATED: "Cannot play the related song",
UNAVAILABLE_VIDEO: "This video is unavailable",
UNPLAYABLE_FORMATS: "No playable format found",
NON_NSFW: "Cannot play age-restricted content in non-NSFW channel",
NOT_SUPPORTED_URL: "This url is not supported",
NOT_SUPPORTED_SONG: /* @__PURE__ */ __name((song) => `There is no plugin supporting this song (${song})`, "NOT_SUPPORTED_SONG"),
NO_VALID_SONG: "'songs' array does not have any valid Song or url",
CANNOT_RESOLVE_SONG: /* @__PURE__ */ __name((t) => `Cannot resolve ${(0, import_node_util.inspect)(t)} to a Song`, "CANNOT_RESOLVE_SONG"),
CANNOT_GET_STREAM_URL: /* @__PURE__ */ __name((song) => `Cannot get stream url with this song (${song})`, "CANNOT_GET_STREAM_URL"),
CANNOT_GET_SEARCH_QUERY: /* @__PURE__ */ __name((song) => `Cannot get search query with this song (${song})`, "CANNOT_GET_SEARCH_QUERY"),
NO_RESULT: /* @__PURE__ */ __name((query) => `Cannot find any song with this query (${query})`, "NO_RESULT"),
NO_STREAM_URL: /* @__PURE__ */ __name((song) => `No stream url attached (${song})`, "NO_STREAM_URL"),
EMPTY_FILTERED_PLAYLIST: "There is no valid video in the playlist\nMaybe age-restricted contents is filtered because you are in non-NSFW channel",
EMPTY_PLAYLIST: "There is no valid video in the playlist"
};
var haveCode = /* @__PURE__ */ __name((code) => Object.keys(ERROR_MESSAGES).includes(code), "haveCode");
var parseMessage = /* @__PURE__ */ __name((m, ...args) => typeof m === "string" ? m : m(...args), "parseMessage");
var getErrorMessage = /* @__PURE__ */ __name((code, ...args) => haveCode(code) ? parseMessage(ERROR_MESSAGES[code], ...args) : args[0], "getErrorMessage");
var DisTubeError = class _DisTubeError extends Error {
static {
__name(this, "DisTubeError");
}
errorCode;
constructor(code, ...args) {
super(getErrorMessage(code, ...args));
this.errorCode = code;
if (Error.captureStackTrace) Error.captureStackTrace(this, _DisTubeError);
}
get name() {
return `DisTubeError [${this.errorCode}]`;
}
get code() {
return this.errorCode;
}
};
// src/util.ts
var import_node_url = require("url");
var import_discord3 = require("discord.js");
// src/core/DisTubeVoice.ts
var import_voice = require("@discordjs/voice");
var import_discord = require("discord.js");
var import_tiny_typed_emitter = require("tiny-typed-emitter");
var DisTubeVoice = class extends import_tiny_typed_emitter.TypedEmitter {
static {
__name(this, "DisTubeVoice");
}
id;
voices;
audioPlayer;
connection;
emittedError;
isDisconnected = false;
stream;
pausingStream;
#channel;
#volume = 100;
constructor(voiceManager, channel) {
super();
this.voices = voiceManager;
this.id = channel.guildId;
this.channel = channel;
this.voices.add(this.id, this);
this.audioPlayer = (0, import_voice.createAudioPlayer)().on(import_voice.AudioPlayerStatus.Idle, (oldState) => {
if (oldState.status !== import_voice.AudioPlayerStatus.Idle) this.emit("finish");
}).on("error", (error) => {
if (this.emittedError) return;
this.emittedError = true;
this.emit("error", error);
});
this.connection.on(import_voice.VoiceConnectionStatus.Disconnected, (_, newState) => {
if (newState.reason === import_voice.VoiceConnectionDisconnectReason.Manual) {
this.leave();
} else if (newState.reason === import_voice.VoiceConnectionDisconnectReason.WebSocketClose && newState.closeCode === 4014) {
(0, import_voice.entersState)(this.connection, import_voice.VoiceConnectionStatus.Connecting, RECONNECT_TIMEOUT_MS).catch(() => {
if (![import_voice.VoiceConnectionStatus.Ready, import_voice.VoiceConnectionStatus.Connecting].includes(this.connection.state.status)) {
this.leave();
}
});
} else if (this.connection.rejoinAttempts < RECONNECT_MAX_ATTEMPTS) {
setTimeout(
() => {
this.connection.rejoin();
},
(this.connection.rejoinAttempts + 1) * RECONNECT_TIMEOUT_MS
).unref();
} else if (this.connection.state.status !== import_voice.VoiceConnectionStatus.Destroyed) {
this.leave(new DisTubeError("VOICE_RECONNECT_FAILED"));
}
}).on(import_voice.VoiceConnectionStatus.Destroyed, () => {
this.leave();
}).on("error", () => void 0);
this.connection.subscribe(this.audioPlayer);
}
/**
* The voice channel id the bot is in
*/
get channelId() {
return this.connection?.joinConfig?.channelId ?? void 0;
}
get channel() {
if (!this.channelId) return this.#channel;
if (this.#channel?.id === this.channelId) return this.#channel;
const channel = this.voices.client.channels.cache.get(this.channelId);
if (!channel) return this.#channel;
for (const type of import_discord.Constants.VoiceBasedChannelTypes) {
if (channel.type === type) {
this.#channel = channel;
return channel;
}
}
return this.#channel;
}
set channel(channel) {
if (!isSupportedVoiceChannel(channel)) {
throw new DisTubeError("INVALID_TYPE", "BaseGuildVoiceChannel", channel, "DisTubeVoice#channel");
}
if (channel.guildId !== this.id) throw new DisTubeError("VOICE_DIFFERENT_GUILD");
if (channel.client.user?.id !== this.voices.client.user?.id) throw new DisTubeError("VOICE_DIFFERENT_CLIENT");
if (channel.id === this.channelId) return;
if (!channel.joinable) {
if (channel.full) throw new DisTubeError("VOICE_FULL");
else throw new DisTubeError("VOICE_MISSING_PERMS");
}
this.connection = this.#join(channel);
this.#channel = channel;
}
#join(channel) {
return (0, import_voice.joinVoiceChannel)({
channelId: channel.id,
guildId: this.id,
adapterCreator: channel.guild.voiceAdapterCreator,
group: channel.client.user?.id
});
}
/**
* Join a voice channel with this connection
* @param channel - A voice channel
*/
async join(channel) {
if (channel) this.channel = channel;
try {
await (0, import_voice.entersState)(this.connection, import_voice.VoiceConnectionStatus.Ready, JOIN_TIMEOUT_MS);
} catch {
if (this.connection.state.status === import_voice.VoiceConnectionStatus.Ready) return this;
if (this.connection.state.status !== import_voice.VoiceConnectionStatus.Destroyed) this.connection.destroy();
this.voices.remove(this.id);
throw new DisTubeError("VOICE_CONNECT_FAILED", JOIN_TIMEOUT_MS / 1e3);
}
return this;
}
/**
* Leave the voice channel of this connection
* @param error - Optional, an error to emit with 'error' event.
*/
leave(error) {
this.stop(true);
if (!this.isDisconnected) {
this.emit("disconnect", error);
this.isDisconnected = true;
}
if (this.connection.state.status !== import_voice.VoiceConnectionStatus.Destroyed) this.connection.destroy();
this.voices.remove(this.id);
}
/**
* Stop the playing stream
* @param force - If true, will force the {@link DisTubeVoice#audioPlayer} to enter the Idle state even
* if the {@link DisTubeStream#audioResource} has silence padding frames.
*/
stop(force = false) {
this.audioPlayer.stop(force);
}
#streamErrorHandler;
/**
* Play a {@link DisTubeStream}
* @param dtStream - DisTubeStream
*/
async play(dtStream) {
if (!await checkEncryptionLibraries()) {
dtStream.kill();
throw new DisTubeError("ENCRYPTION_LIBRARIES_MISSING");
}
this.emittedError = false;
if (this.stream && this.#streamErrorHandler) {
this.stream.off("error", this.#streamErrorHandler);
}
this.#streamErrorHandler = (error) => {
if (this.emittedError || error.code === "ERR_STREAM_PREMATURE_CLOSE") return;
this.emittedError = true;
this.emit("error", error);
};
dtStream.on("error", this.#streamErrorHandler);
if (this.audioPlayer.state.status !== import_voice.AudioPlayerStatus.Paused) {
this.audioPlayer.play(dtStream.audioResource);
this.stream?.kill();
dtStream.spawn();
} else if (!this.pausingStream) {
this.pausingStream = this.stream;
}
this.stream = dtStream;
this.volume = this.#volume;
}
set volume(volume) {
if (typeof volume !== "number" || Number.isNaN(volume)) {
throw new DisTubeError("INVALID_TYPE", "number", volume, "volume");
}
if (volume < 0) {
throw new DisTubeError("NUMBER_COMPARE", "Volume", "bigger or equal to", 0);
}
this.#volume = volume;
this.stream?.setVolume((this.#volume / 100) ** (0.5 / Math.log10(2)));
}
/**
* Get or set the volume percentage
*/
get volume() {
return this.#volume;
}
/**
* Playback duration of the audio resource in seconds (time since playback started)
*/
get playbackDuration() {
return (this.stream?.audioResource?.playbackDuration ?? 0) / 1e3;
}
/**
* Current playback time in seconds, accounting for seek offset
*/
get playbackTime() {
return this.playbackDuration + (this.stream?.seekTime ?? 0);
}
pause() {
this.audioPlayer.pause();
}
unpause() {
const state = this.audioPlayer.state;
if (state.status !== import_voice.AudioPlayerStatus.Paused) return;
if (this.stream?.audioResource && state.resource !== this.stream.audioResource) {
this.audioPlayer.play(this.stream.audioResource);
this.stream.spawn();
this.pausingStream?.kill();
delete this.pausingStream;
} else {
this.audioPlayer.unpause();
}
}
/**
* Whether the bot is self-deafened
*/
get selfDeaf() {
return this.connection.joinConfig.selfDeaf;
}
/**
* Whether the bot is self-muted
*/
get selfMute() {
return this.connection.joinConfig.selfMute;
}
/**
* Self-deafens/undeafens the bot.
* @param selfDeaf - Whether or not the bot should be self-deafened
* @returns true if the voice state was successfully updated, otherwise false
*/
setSelfDeaf(selfDeaf) {
if (typeof selfDeaf !== "boolean") {
throw new DisTubeError("INVALID_TYPE", "boolean", selfDeaf, "selfDeaf");
}
return this.connection.rejoin({
...this.connection.joinConfig,
selfDeaf
});
}
/**
* Self-mutes/unmutes the bot.
* @param selfMute - Whether or not the bot should be self-muted
* @returns true if the voice state was successfully updated, otherwise false
*/
setSelfMute(selfMute) {
if (typeof selfMute !== "boolean") {
throw new DisTubeError("INVALID_TYPE", "boolean", selfMute, "selfMute");
}
return this.connection.rejoin({
...this.connection.joinConfig,
selfMute
});
}
/**
* The voice state of this connection
*/
get voiceState() {
return this.channel?.guild?.members?.me?.voice;
}
};
// src/core/manager/BaseManager.ts
var import_discord2 = require("discord.js");
var BaseManager = class extends DisTubeBase {
static {
__name(this, "BaseManager");
}
/**
* The collection of items for this manager.
*/
collection = new import_discord2.Collection();
/**
* The size of the collection.
*/
get size() {
return this.collection.size;
}
};
// src/core/manager/FilterManager.ts
var FilterManager = class extends BaseManager {
static {
__name(this, "FilterManager");
}
/**
* The queue to manage
*/
queue;
constructor(queue) {
super(queue.distube);
this.queue = queue;
}
#resolve(filter) {
if (typeof filter === "object" && typeof filter.name === "string" && typeof filter.value === "string") {
return filter;
}
if (typeof filter === "string" && Object.hasOwn(this.distube.filters, filter)) {
return {
name: filter,
value: this.distube.filters[filter]
};
}
throw new DisTubeError("INVALID_TYPE", "FilterResolvable", filter, "filter");
}
#apply() {
this.queue._beginTime = this.queue.currentTime;
this.queue.play(false);
}
/**
* Enable a filter or multiple filters to the manager
* @param filterOrFilters - The filter or filters to enable
* @param override - Wether or not override the applied filter with new filter value
*/
add(filterOrFilters, override = false) {
if (Array.isArray(filterOrFilters)) {
for (const filter of filterOrFilters) {
const ft = this.#resolve(filter);
if (override || !this.has(ft)) this.collection.set(ft.name, ft);
}
} else {
const ft = this.#resolve(filterOrFilters);
if (override || !this.has(ft)) this.collection.set(ft.name, ft);
}
this.#apply();
return this;
}
/**
* Clear enabled filters of the manager
*/
clear() {
return this.set([]);
}
/**
* Set the filters applied to the manager
* @param filters - The filters to apply
*/
set(filters) {
if (!Array.isArray(filters)) throw new DisTubeError("INVALID_TYPE", "Array<FilterResolvable>", filters, "filters");
this.collection.clear();
for (const f of filters) {
const filter = this.#resolve(f);
this.collection.set(filter.name, filter);
}
this.#apply();
return this;
}
#removeFn(f) {
return this.collection.delete(this.#resolve(f).name);
}
/**
* Disable a filter or multiple filters
* @param filterOrFilters - The filter or filters to disable
*/
remove(filterOrFilters) {
if (Array.isArray(filterOrFilters)) filterOrFilters.forEach((f) => this.#removeFn(f));
else this.#removeFn(filterOrFilters);
this.#apply();
return this;
}
/**
* Check whether a filter enabled or not
* @param filter - The filter to check
*/
has(filter) {
return this.collection.has(typeof filter === "string" ? filter : this.#resolve(filter).name);
}
/**
* Array of enabled filter names
*/
get names() {
return [...this.collection.keys()];
}
/**
* Array of enabled filters
*/
get values() {
return [...this.collection.values()];
}
get ffmpegArgs() {
return this.size ? { af: this.values.map((f) => f.value).join(",") } : {};
}
toString() {
return this.names.toString();
}
};
// src/type.ts
var Events = /* @__PURE__ */ ((Events2) => {
Events2["ERROR"] = "error";
Events2["ADD_LIST"] = "addList";
Events2["ADD_SONG"] = "addSong";
Events2["PLAY_SONG"] = "playSong";
Events2["FINISH_SONG"] = "finishSong";
Events2["EMPTY"] = "empty";
Events2["FINISH"] = "finish";
Events2["INIT_QUEUE"] = "initQueue";
Events2["NO_RELATED"] = "noRelated";
Events2["DISCONNECT"] = "disconnect";
Events2["DELETE_QUEUE"] = "deleteQueue";
Events2["FFMPEG_DEBUG"] = "ffmpegDebug";
Events2["DEBUG"] = "debug";
return Events2;
})(Events || {});
var RepeatMode = /* @__PURE__ */ ((RepeatMode2) => {
RepeatMode2[RepeatMode2["DISABLED"] = 0] = "DISABLED";
RepeatMode2[RepeatMode2["SONG"] = 1] = "SONG";
RepeatMode2[RepeatMode2["QUEUE"] = 2] = "QUEUE";
return RepeatMode2;
})(RepeatMode || {});
var PluginType = /* @__PURE__ */ ((PluginType2) => {
PluginType2["EXTRACTOR"] = "extractor";
PluginType2["INFO_EXTRACTOR"] = "info-extractor";
PluginType2["PLAYABLE_EXTRACTOR"] = "playable-extractor";
return PluginType2;
})(PluginType || {});
// src/struct/TaskQueue.ts
var Task = class {
static {
__name(this, "Task");
}
resolve;
promise;
isPlay;
constructor(isPlay) {
this.isPlay = isPlay;
this.promise = new Promise((res) => {
this.resolve = res;
});
}
};
var TaskQueue = class {
static {
__name(this, "TaskQueue");
}
/**
* The task array
*/
#tasks = [];
/**
* Waits for last task finished and queues a new task
*/
queuing(isPlay = false) {
const next = this.remaining ? this.#tasks[this.#tasks.length - 1].promise : Promise.resolve();
this.#tasks.push(new Task(isPlay));
return next;
}
/**
* Removes the finished task and processes the next task
*/
resolve() {
this.#tasks.shift()?.resolve();
}
/**
* The remaining number of tasks
*/
get remaining() {
return this.#tasks.length;
}
/**
* Whether or not having a play task
*/
get hasPlayTask() {
return this.#tasks.some((t) => t.isPlay);
}
};
// src/struct/Queue.ts
var Queue = class extends DisTubeBase {
static {
__name(this, "Queue");
}
/**
* Queue id (Guild id)
*/
id;
/**
* Voice connection of this queue.
*/
voice;
/**
* List of songs in the queue (The first one is the playing song)
*/
songs;
/**
* List of the previous songs.
*/
previousSongs;
/**
* Whether stream is currently stopped.
*/
stopped;
/**
* Whether or not the queue is active.
*
* Note: This remains `true` when paused. It only becomes `false` when stopped.
* @deprecated Use `!queue.paused` to check if audio is playing. Will be removed in v6.0.
*/
playing;
/**
* Whether or not the stream is currently paused.
*/
paused;
/**
* Type of repeat mode (`0` is disabled, `1` is repeating a song, `2` is repeating
* all the queue). Default value: `0` (disabled)
*/
repeatMode;
/**
* Whether or not the autoplay mode is enabled. Default value: `false`
*/
autoplay;
/**
* FFmpeg arguments for the current queue. Default value is defined with {@link DisTubeOptions}.ffmpeg.args.
* `af` output argument will be replaced with {@link Queue#filters} manager
*/
ffmpegArgs;
/**
* The text channel of the Queue. (Default: where the first command is called).
*/
textChannel;
/**
* What time in the song to begin (in seconds).
* @internal
*/
_beginTime;
#filters;
/**
* Whether or not the queue is being updated manually (skip, jump, previous)
* @internal
*/
_manualUpdate;
/**
* Task queuing system
* @internal
*/
_taskQueue;
/**
* {@link DisTubeVoice} listener
* @internal
*/
_listeners;
/**
* Create a queue for the guild
* @param distube - DisTube
* @param voice - Voice connection
* @param textChannel - Default text channel
*/
constructor(distube, voice, textChannel) {
super(distube);
this.voice = voice;
this.id = voice.id;
this.volume = DEFAULT_VOLUME;
this.songs = [];
this.previousSongs = [];
this.stopped = false;
this._manualUpdate = false;
this.playing = false;
this.paused = false;
this.repeatMode = 0 /* DISABLED */;
this.autoplay = false;
this.#filters = new FilterManager(this);
this._beginTime = 0;
this.textChannel = textChannel;
this._taskQueue = new TaskQueue();
this._listeners = void 0;
this.ffmpegArgs = {
global: { ...this.options.ffmpeg.args.global },
input: { ...this.options.ffmpeg.args.input },
output: { ...this.options.ffmpeg.args.output }
};
}
#addToPreviousSongs(songs) {
if (Array.isArray(songs)) {
if (this.options.savePreviousSongs) {
this.previousSongs.push(...songs);
} else {
this.previousSongs.push(...songs.map((s) => ({ id: s.id })));
}
} else if (this.options.savePreviousSongs) {
this.previousSongs.push(songs);
} else {
this.previousSongs.push({ id: songs.id });
}
}
#stop() {
this._manualUpdate = true;
this.voice.stop();
}
/**
* The client user as a `GuildMember` of this queue's guild
*/
get clientMember() {
return this.voice.channel.guild.members.me ?? void 0;
}
/**
* The filter manager of the queue
*/
get filters() {
return this.#filters;
}
/**
* Formatted duration string.
*/
get formattedDuration() {
return formatDuration(this.duration);
}
/**
* Queue's duration.
*/
get duration() {
return this.songs.length ? this.songs.reduce((prev, next) => prev + next.duration, 0) : 0;
}
/**
* What time in the song is playing (in seconds).
*/
get currentTime() {
return this.voice.playbackTime;
}
/**
* Formatted {@link Queue#currentTime} string.
*/
get formattedCurrentTime() {
return formatDuration(this.currentTime);
}
/**
* The voice channel playing in.
*/
get voiceChannel() {
return this.clientMember?.voice?.channel ?? null;
}
/**
* Get or set the stream volume. Default value: `50`.
*/
get volume() {
return this.voice.volume;
}
set volume(value) {
this.voice.volume = value;
}
/**
* @throws {DisTubeError}
* @param song - Song to add
* @param position - Position to add, \<= 0 to add to the end of the queue
* @returns The guild queue
*/
addToQueue(song, position = 0) {
if (this.stopped) throw new DisTubeError("QUEUE_STOPPED");
if (!song || Array.isArray(song) && !song.length) {
throw new DisTubeError("INVALID_TYPE", ["Song", "Array<Song>"], song, "song");
}
if (typeof position !== "number" || !Number.isInteger(position)) {
throw new DisTubeError("INVALID_TYPE", "integer", position, "position");
}
if (position <= 0) {
if (Array.isArray(song)) this.songs.push(...song);
else this.songs.push(song);
} else if (Array.isArray(song)) {
this.songs.splice(position, 0, ...song);
} else {
this.songs.splice(position, 0, song);
}
return this;
}
/**
* @returns `true` if the queue is active (not stopped)
* @deprecated Use `!queue.paused` to check if audio is playing. Will be removed in v6.0.
*/
isPlaying() {
return this.playing;
}
/**
* @returns `true` if the queue is paused
* @deprecated Use `queue.paused` property instead. Will be removed in v6.0.
*/
isPaused() {
return this.paused;
}
/**
* Pause the guild stream
* @returns The guild queue
*/
async pause() {
await this._taskQueue.queuing();
try {
if (this.paused) throw new DisTubeError("PAUSED");
this.paused = true;
this.voice.pause();
return this;
} finally {
this._taskQueue.resolve();
}
}
/**
* Resume the guild stream
* @returns The guild queue
*/
async resume() {
await this._taskQueue.queuing();
try {
if (!this.paused) throw new DisTubeError("RESUMED");
this.paused = false;
this.voice.unpause();
return this;
} finally {
this._taskQueue.resolve();
}
}
/**
* Set the guild stream's volume
* @param percent - The percentage of volume you want to set
* @returns The guild queue
*/
setVolume(percent) {
this.volume = percent;
return this;
}
/**
* Skip the playing song if there is a next song in the queue. <info>If {@link
* Queue#autoplay} is `true` and there is no up next song, DisTube will add and
* play a related song.</info>
* @param options - Skip options
* @returns The song will skip to
*/
async skip(options) {
return this.jump(1, options);
}
/**
* Play the previous song if exists
* @returns The guild queue
*/
async previous() {
await this._taskQueue.queuing();
try {
if (!this.options.savePreviousSongs) throw new DisTubeError("DISABLED_OPTION", "savePreviousSongs");
if (this.previousSongs.length === 0 && this.repeatMode !== 2 /* QUEUE */) {
throw new DisTubeError("NO_PREVIOUS");
}
const song = this.repeatMode === 2 /* QUEUE */ && this.previousSongs.length === 0 ? this.songs[this.songs.length - 1] : this.previousSongs.pop();
this.songs.unshift(song);
this.#stop();
return song;
} finally {
this._taskQueue.resolve();
}
}
/**
* Shuffle the queue's songs
* @returns The guild queue
*/
async shuffle() {
await this._taskQueue.queuing();
try {
const playing = this.songs.shift();
if (playing === void 0) return this;
for (let i = this.songs.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[this.songs[i], this.songs[j]] = [this.songs[j], this.songs[i]];
}
this.songs.unshift(playing);
return this;
} finally {
this._taskQueue.resolve();
}
}
/**
* Jump to the song position in the queue. The next one is 1, 2,... The previous
* one is -1, -2,...
* if `num` is invalid number
* @param position - The song position to play
* @param options - Skip options
* @returns The new Song will be played
*/
async jump(position, options) {
await this._taskQueue.queuing();
try {
if (typeof position !== "number") throw new DisTubeError("INVALID_TYPE", "number", position, "position");
if (!position || position > this.songs.length || -position > this.previousSongs.length) {
throw new DisTubeError("NO_SONG_POSITION");
}
if (position > 0) {
if (position >= this.songs.length) {
if (this.autoplay) {
await this._addRelatedSong();
} else {
throw new DisTubeError("NO_UP_NEXT");
}
}
const skipped = this.songs.splice(0, position);
if (options?.requeue) {
this.songs.push(...skipped);
} else {
this.#addToPreviousSongs(skipped);
}
} else if (!this.options.savePreviousSongs) {
throw new DisTubeError("DISABLED_OPTION", "savePreviousSongs");
} else {
const skipped = this.previousSongs.splice(position);
this.songs.unshift(...skipped);
}
this.#stop();
return this.songs[0];
} finally {
this._taskQueue.resolve();
}
}
/**
* Set the repeat mode of the guild queue.
* Toggle mode `(Disabled -> Song -> Queue -> Disabled ->...)` if `mode` is `undefined`
* @param mode - The repeat modes (toggle if `undefined`)
* @returns The new repeat mode
*/
setRepeatMode(mode) {
if (mode !== void 0 && !Object.values(RepeatMode).includes(mode)) {
throw new DisTubeError("INVALID_TYPE", ["RepeatMode", "undefined"], mode, "mode");
}
if (mode === void 0) this.repeatMode = (this.repeatMode + 1) % 3;
else if (this.repeatMode === mode) this.repeatMode = 0 /* DISABLED */;
else this.repeatMode = mode;
return this.repeatMode;
}
/**
* Set the playing time to another position
* @param time - Time in seconds
* @returns The guild queue
*/
async seek(time) {
await this._taskQueue.queuing();
try {
if (typeof time !== "number") throw new DisTubeError("INVALID_TYPE", "number", time, "time");
if (Number.isNaN(time) || time < 0) throw new DisTubeError("NUMBER_COMPARE", "time", "bigger or equal to", 0);
this._beginTime = time;
await this.play(false);
return this;
} finally {
this._taskQueue.resolve();
}
}
async #getRelatedSong(current) {
const plugin = await this.handler._getPluginFromSong(current);
if (plugin) return plugin.getRelatedSongs(current);
return [];
}
/**
* Internal implementation of addRelatedSong without task queue protection.
* Used by methods that already hold the task queue lock.
* @internal
*/
async _addRelatedSong(song) {
const current = song ?? this.songs?.[0];
if (!current) throw new DisTubeError("NO_PLAYING_SONG");
const prevIds = this.previousSongs.map((p) => p.id);
const relatedSongs = (await this.#getRelatedSong(current)).filter((s) => !prevIds.includes(s.id));
this.debug(`[${this.id}] Getting related songs from: ${current}`);
if (!relatedSongs.length && !current.stream.playFromSource) {
const altSong = current.stream.song;
if (altSong) relatedSongs.push(...(await this.#getRelatedSong(altSong)).filter((s) => !prevIds.includes(s.id)));
this.debug(`[${this.id}] Getting related songs from streamed song: ${altSong}`);
}
const nextSong = relatedSongs[0];
if (!nextSong) throw new DisTubeError("NO_RELATED");
nextSong.metadata = current.metadata;
nextSong.member = this.clientMember;
this.addToQueue(nextSong);
return nextSong;
}
/**
* Add a related song of the playing song to the queue
* @param song - The song to get related songs from. Defaults to the current playing song.
* @returns The added song
*/
async addRelatedSong(song) {
await this._taskQueue.queuing();
try {
return await this._addRelatedSong(song);
} finally {
this._taskQueue.resolve();
}
}
/**
* Stop the guild stream and delete the queue
*/
async stop() {
await this._taskQueue.queuing();
try {
this.voice.stop();
this.remove();
} finally {
this._taskQueue.resolve();
}
}
/**
* Remove the queue from the manager
*/
remove() {
this.playing = false;
this.paused = true;
this.stopped = true;
this.songs = [];
this.previousSongs = [];
if (this._listeners) for (const event of objectKeys(this._listeners)) this.voice.off(event, this._listeners[event]);
this.queues.remove(this.id);
this.emit("deleteQueue" /* DELETE_QUEUE */, this);
}
/**
* Toggle autoplay mode
* @returns Autoplay mode state
*/
toggleAutoplay() {
this.autoplay = !this.autoplay;
return this.autoplay;
}
/**
* Play the first song in the queue
* @param emitPlaySong - Whether or not emit {@link Events.PLAY_SONG} event
*/
play(emitPlaySong = true) {
if (this.stopped) throw new DisTubeError("QUEUE_STOPPED");
this.playing = true;
return this.queues.playSong(this, emitPlaySong);
}
};
// src/util.ts
var formatInt = /* @__PURE__ */ __name((int) => int < 10 ? `0${int}` : int, "formatInt");
function formatDuration(sec) {
if (!sec || !Number(sec)) return "00:00";
const seconds = Math.floor(sec % 60);
const minutes = Math.floor(sec % 3600 / 60);
const hours = Math.floor(sec / 3600);
if (hours > 0) return `${formatInt(hours)}:${formatInt(minutes)}:${formatInt(seconds)}`;
if (minutes > 0) return `${formatInt(minutes)}:${formatInt(seconds)}`;
return `00:${formatInt(seconds)}`;
}
__name(formatDuration, "formatDuration");
var SUPPORTED_PROTOCOL = ["https:", "http:", "file:"];
function isURL(input) {
if (typeof input !== "string" || input.includes(" ")) return false;
try {
const url = new import_node_url.URL(input);
if (!SUPPORTED_PROTOCOL.some((p) => p === url.protocol)) return false;
} catch {
return false;
}
return true;
}
__name(isURL, "isURL");
function checkIntents(options) {
const intents = options.intents instanceof import_discord3.IntentsBitField ? options.intents : new import_discord3.IntentsBitField(options.intents);
if (!intents.has(import_discord3.GatewayIntentBits.GuildVoiceStates)) throw new DisTubeError("MISSING_INTENTS", "GuildVoiceStates");
}
__name(checkIntents, "checkIntents");
function isVoiceChannelEmpty(voiceState) {
const guild = voiceState.guild;
const clientId = voiceState.client.user?.id;
if (!guild || !clientId) return false;
const voiceChannel = guild.members.me?.voice?.channel;
if (!voiceChannel) return false;
const members = voiceChannel.members.filter((m) => !m.user.bot);
return !members.size;
}
__name(isVoiceChannelEmpty, "isVoiceChannelEmpty");
function isSnowflake(id) {
try {
return import_discord3.SnowflakeUtil.deconstruct(id).timestamp > import_discord3.SnowflakeUtil.epoch;
} catch {
return false;
}
}
__name(isSnowflake, "isSnowflake");
function isMemberInstance(member) {
return Boolean(member) && isSnowflake(member.id) && isSnowflake(member.guild?.id) && isSnowflake(member.user?.id) && member.id === member.user.id;
}
__name(isMemberInstance, "isMemberInstance");
function isTextChannelInstance(channel) {
return Boolean(channel) && isSnowflake(channel.id) && isSnowflake(channel.guildId || channel.guild?.id) && import_discord3.Constants.TextBasedChannelTypes.includes(channel.type) && typeof channel.send === "function" && (typeof channel.nsfw === "boolean" || typeof channel.parent?.nsfw === "boolean");
}
__name(isTextChannelInstance, "isTextChannelInstance");
function isMessageInstance(message) {
return Boolean(message) && isSnowflake(message.id) && isSnowflake(message.guildId || message.guild?.id) && isMemberInstance(message.member) && isTextChannelInstance(message.channel) && import_discord3.Constants.NonSystemMessageTypes.includes(message.type) && message.member.id === message.author?.id;
}
__name(isMessageInstance, "isMessageInstance");
function isSupportedVoiceChannel(channel) {
return Boolean(channel) && isSnowflake(channel.id) && isSnowflake(channel.guildId || channel.guild?.id) && import_discord3.Constants.VoiceBasedChannelTypes.includes(channel.type);
}
__name(isSupportedVoiceChannel, "isSupportedVoiceChannel");
function isGuildInstance(guild) {
return Boolean(guild) && isSnowflake(guild.id) && isSnowflake(guild.ownerId) && typeof guild.name === "string";
}
__name(isGuildInstance, "isGuildInstance");
function resolveGuildId(resolvable) {
let guildId;
if (typeof resolvable === "string") {
guildId = resolvable;
} else if (isObject(resolvable)) {
if ("guildId" in resolvable && resolvable.guildId) {
guildId = resolvable.guildId;
} else if (resolvable instanceof Queue || resolvable instanceof DisTubeVoice || isGuildInstance(resolvable)) {
guildId = resolvable.id;
} else if ("guild" in resolvable && isGuildInstance(resolvable.guild)) {
guildId = resolvable.guild.id;
}
}
if (!isSnowflake(guildId)) throw new DisTubeError("INVALID_TYPE", "GuildIdResolvable", resolvable);
return guildId;
}
__name(resolveGuildId, "resolveGuildId");
function isClientInstance(client) {
return Boolean(client) && typeof client.login === "function";
}
__name(isClientInstance, "isClientInstance");
function checkInvalidKey(target, source, sourceName) {
if (!isObject(target)) throw new DisTubeError("INVALID_TYPE", "object", target, sourceName);
const sourceKeys = Array.isArray(source) ? source : objectKeys(source);
const invalidKey = objectKeys(target).find((key) => !sourceKeys.includes(key));
if (invalidKey) throw new DisTubeError("INVALID_KEY", sourceName, invalidKey);
}
__name(checkInvalidKey, "checkInvalidKey");
function isObject(obj) {
return typeof obj === "object" && obj !== null && !Array.isArray(obj);
}
__name(isObject, "isObject");
function objectKeys(obj) {
if (!isObject(obj)) return [];
return Object.keys(obj);
}
__name(objectKeys, "objectKeys");
function isNsfwChannel(channel) {
if (!isTextChannelInstance(channel)) return false;
if (channel.isThread()) return channel.parent?.nsfw ?? false;
return channel.nsfw;
}
__name(isNsfwChannel, "isNsfwChannel");
var isTruthy = /* @__PURE__ */ __name((x) => Boolean(x), "isTruthy");
var checkEncryptionLibraries = /* @__PURE__ */ __name(async () => {
if (await import("crypto").then((m) => m.getCiphers().includes("aes-256-gcm"))) return true;
for (const lib of [
"@noble/ciphers",
"@stablelib/xchacha20poly1305",
"sodium-native",
"sodium",
"libsodium-wrappers",
"tweetnacl"
]) {
try {
await import(lib);
return true;
} catch {
}
}
return false;
}, "checkEncryptionLibraries");
// src/struct/Playlist.ts
var Playlist = class {
static {
__name(this, "Playlist");
}
/**
* Playlist source.
*/
source;
/**
* Songs in the playlist.
*/
songs;
/**
* Playlist ID.
*/
id;
/**
* Playlist name.
*/
name;
/**
* Playlist URL.
*/
url;
/**
* Playlist thumbnail.
*/
thumbnail;
#metadata;
#member;
/**
* Create a Playlist
* @param playlist - Raw playlist info
* @param options - Optional data
*/
constructor(playlist, { member, metadata } = {}) {
if (!Array.isArray(playlist.songs) || !playlist.songs.length) throw new DisTubeError("EMPTY_PLAYLIST");
this.source = playlist.source.toLowerCase();
this.songs = playlist.songs;
this.name = playlist.name;
this.id = playlist.id;
this.url = playlist.url;
this.thumbnail = playlist.thumbnail;
this.member = member;
this.songs.forEach((s) => s.playlist = this);
this.metadata = metadata;
}
/**
* Playlist duration in second.
*/
get duration() {
return this.songs.reduce((prev, next) => prev + next.duration, 0);
}
/**
* Formatted duration string `hh:mm:ss`.
*/
get formattedDuration() {
return formatDuration(this.duration);
}
/**
* User requested.
*/
get member() {
return this.#member;
}
set member(member) {
if (!isMemberInstance(member)) return;
this.#member = member;
this.songs.forEach((s) => s.member = this.member);
}
/**
* User requested.
*/
get user() {
return this.member?.user;
}
/**
* Optional metadata that can be used to identify the playlist.
*/
get metadata() {
return this.#metadata;
}
set metadata(metadata) {
this.#metadata = metadata;
this.songs.forEach((s) => s.metadata = metadata);
}
toString() {
return `${this.name} (${this.songs.length} songs)`;
}
};
// src/struct/Song.ts
var Song = class {
static {
__name(this, "Song");
}
/**
* The source of this song info
*/
source;
/**
* Song ID.
*/
id;
/**
* Song name.
*/
name;
/**
* Indicates if the song is an active live.
*/
isLive;
/**
* Song duration.
*/
duration;
/**
* Formatted duration string (`hh:mm:ss`, `mm:ss` or `Live`).
*/
formattedDuration;
/**
* Song URL.
*/
url;
/**
* Song thumbnail.
*/
thumbnail;
/**
* Song view count
*/
views;
/**
* Song like count
*/
likes;
/**
* Song dislike count
*/
dislikes;
/**
* Song repost (share) count
*/
reposts;
/**
* Song uploader
*/
uploader;
/**
* Whether or not an age-restricted content
*/
ageRestricted;
/**
* Stream info
*/
stream;
/**
* The plugin that created this song
*/
plugin;
#metadata;
#member;
#playlist;
/**
* Create a Song
*
* @param info - Raw song info
* @param options - Optional data
*/
constructor(info, { member, metadata } = {}) {
this.source = info.source.toLowerCase();
this.metadata = metadata;
this.member = member;
this.id = info.id;
this.name = info.name;
this.isLive = info.isLive;
this.duration = this.isLive || !info.duration ? 0 : info.duration;
this.formattedDuration = this.isLive ? "Live" : formatDuration(this.duration);
this.url = info.url;
this.thumbnail = info.thumbnail;
this.views = info.views;
this.likes = info.likes;
this.dislikes = info.dislikes;
this.reposts = info.reposts;
this.uploader = {
name: info.uploader?.name,
url: info.uploader?.url
};
this.ageRestricted = info.ageRestricted;
this.stream = { playFromSource: info.playFromSource };
this.plugin = info.plugin;
}
/**
* The playlist this song belongs to
*/
get playlist() {
return this.#playlist;
}
set playlist(playlist) {
if (!(playlist instanceof Playlist)) throw new DisTubeError("INVALID_TYPE", "Playlist", playlist, "Song#playlist");
this.#playlist = playlist;
this.member = playlist.member;
}
/**
* User requested to play this song.
*/
get member() {
return this.#member;
}
set member(member) {
if (isMemberInstance(member)) this.#member = member;
}
/**
* User requested to play this song.
*/
get user() {
return this.member?.user;
}
/**
* Optional metadata that can be used to identify the song. This is attached by the
* {@link DisTube#play} method.
*/
get metadata() {
return this.#metadata;
}
set metadata(metadata) {
this.#metadata = metadata;
}
toString() {
return this.name || this.url || this.id || "Unknown";
}
};
// src/core/DisTubeHandler.ts
var DisTubeHandler = class extends DisTubeBase {
static {
__name(this, "DisTubeHandler");
}
/**
* Resolve a url or a supported object to a {@link Song} or {@link Playlist}
* @throws {@link DisTubeError}
* @param input - Resolvable input
* @param options - Optional options
* @returns Resolved
*/
async resolve(input, options = {}) {
if (input instanceof Song || input instanceof Playlist) {
if ("metadata" in options) input.metadata = options.metadata;
if ("member" in options) input.member = options.member;
return input;
}
if (typeof input === "string") {
if (isURL(input)) {
const plugin = await this._getPluginFromURL(input) || await this._getPluginFromU