Flatlogic Bot ca3a32f23e V 4
2026-02-16 06:55:36 +00:00

2570 lines
81 KiB
JavaScript

var __defProp = Object.defineProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
// 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
import { request } from "undici";
// src/struct/DisTubeError.ts
import { inspect } from "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 ${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 ${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
import { URL as URL2 } from "url";
import { Constants as Constants2, GatewayIntentBits, IntentsBitField, SnowflakeUtil } from "discord.js";
// src/core/DisTubeVoice.ts
import {
AudioPlayerStatus,
createAudioPlayer,
entersState,
joinVoiceChannel,
VoiceConnectionDisconnectReason,
VoiceConnectionStatus
} from "@discordjs/voice";
import { Constants } from "discord.js";
import { TypedEmitter } from "tiny-typed-emitter";
var DisTubeVoice = class extends 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 = createAudioPlayer().on(AudioPlayerStatus.Idle, (oldState) => {
if (oldState.status !== AudioPlayerStatus.Idle) this.emit("finish");
}).on("error", (error) => {
if (this.emittedError) return;
this.emittedError = true;
this.emit("error", error);
});
this.connection.on(VoiceConnectionStatus.Disconnected, (_, newState) => {
if (newState.reason === VoiceConnectionDisconnectReason.Manual) {
this.leave();
} else if (newState.reason === VoiceConnectionDisconnectReason.WebSocketClose && newState.closeCode === 4014) {
entersState(this.connection, VoiceConnectionStatus.Connecting, RECONNECT_TIMEOUT_MS).catch(() => {
if (![VoiceConnectionStatus.Ready, 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 !== VoiceConnectionStatus.Destroyed) {
this.leave(new DisTubeError("VOICE_RECONNECT_FAILED"));
}
}).on(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 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 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 entersState(this.connection, VoiceConnectionStatus.Ready, JOIN_TIMEOUT_MS);
} catch {
if (this.connection.state.status === VoiceConnectionStatus.Ready) return this;
if (this.connection.state.status !== 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 !== 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 !== 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 !== 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
import { Collection } from "discord.js";
var BaseManager = class extends DisTubeBase {
static {
__name(this, "BaseManager");
}
/**
* The collection of items for this manager.
*/
collection = new 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 URL2(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 IntentsBitField ? options.intents : new IntentsBitField(options.intents);
if (!intents.has(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 SnowflakeUtil.deconstruct(id).timestamp > 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) && Constants2.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) && Constants2.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) && Constants2.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._getPluginFromURL(input = await this.followRedirectLink(input));
if (!plugin) throw new DisTubeError("NOT_SUPPORTED_URL");
this.debug(`[${plugin.constructor.name}] Resolving from url: ${input}`);
return plugin.resolve(input, options);
}
try {
const song = await this.#searchSong(input, options);
if (song) return song;
} catch {
throw new DisTubeError("NO_RESULT", input);
}
}
throw new DisTubeError("CANNOT_RESOLVE_SONG", input);
}
async _getPluginFromURL(url) {
for (const plugin of this.plugins) if (await plugin.validate(url)) return plugin;
return null;
}
async _getPluginFromSong(song, types, validate = true) {
if (!types || types.includes(song.plugin?.type)) return song.plugin;
if (!song.url) return null;
for (const plugin of this.plugins) {
if ((!types || types.includes(plugin?.type)) && (!validate || await plugin.validate(song.url))) {
return plugin;
}
}
return null;
}
async #searchSong(query, options = {}, getStreamURL = false) {
const plugins = this.plugins.filter((p) => p.type === "extractor" /* EXTRACTOR */);
if (!plugins.length) throw new DisTubeError("NO_EXTRACTOR_PLUGIN");
for (const plugin of plugins) {
this.debug(`[${plugin.constructor.name}] Searching for song: ${query}`);
const result = await plugin.searchSong(query, options);
if (result) {
if (getStreamURL && result.stream.playFromSource) result.stream.url = await plugin.getStreamURL(result);
return result;
}
}
return null;
}
/**
* Get {@link Song}'s stream info and attach it to the song.
* @param song - A Song
*/
async attachStreamInfo(song) {
if (song.stream.playFromSource) {
if (song.stream.url) return;
this.debug(`[DisTubeHandler] Getting stream info: ${song}`);
const plugin = await this._getPluginFromSong(song, ["extractor" /* EXTRACTOR */, "playable-extractor" /* PLAYABLE_EXTRACTOR */]);
if (!plugin) throw new DisTubeError("NOT_SUPPORTED_SONG", song.toString());
this.debug(`[${plugin.constructor.name}] Getting stream URL: ${song}`);
song.stream.url = await plugin.getStreamURL(song);
if (!song.stream.url) throw new DisTubeError("CANNOT_GET_STREAM_URL", song.toString());
} else {
if (song.stream.song?.stream?.playFromSource && song.stream.song.stream.url) return;
this.debug(`[DisTubeHandler] Getting stream info: ${song}`);
const plugin = await this._getPluginFromSong(song, ["info-extractor" /* INFO_EXTRACTOR */]);
if (!plugin) throw new DisTubeError("NOT_SUPPORTED_SONG", song.toString());
this.debug(`[${plugin.constructor.name}] Creating search query for: ${song}`);
const query = await plugin.createSearchQuery(song);
if (!query) throw new DisTubeError("CANNOT_GET_SEARCH_QUERY", song.toString());
const altSong = await this.#searchSong(query, { metadata: song.metadata, member: song.member }, true);
if (!altSong || !altSong.stream.playFromSource) throw new DisTubeError("NO_RESULT", query || song.toString());
song.stream.song = altSong;
}
}
async followRedirectLink(url, maxRedirect = MAX_REDIRECT_DEPTH) {
if (maxRedirect === 0) return url;
const res = await request(url, {
method: "HEAD",
headers: {
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.3"
}
});
if (HTTP_REDIRECT_CODES.has(res.statusCode ?? 200)) {
let location = res.headers.location;
if (typeof location !== "string") location = location?.[0] ?? url;
return this.followRedirectLink(location, --maxRedirect);
}
return url;
}
};
// src/core/DisTubeOptions.ts
var Options = class {
static {
__name(this, "Options");
}
plugins;
emitNewSongOnly;
savePreviousSongs;
customFilters;
nsfw;
emitAddSongWhenCreatingQueue;
emitAddListWhenCreatingQueue;
joinNewVoiceChannel;
ffmpeg;
constructor(options) {
if (typeof options !== "object" || Array.isArray(options)) {
throw new DisTubeError("INVALID_TYPE", "object", options, "DisTubeOptions");
}
const opts = { ...defaultOptions, ...options };
this.plugins = opts.plugins;
this.emitNewSongOnly = opts.emitNewSongOnly;
this.savePreviousSongs = opts.savePreviousSongs;
this.customFilters = opts.customFilters;
this.nsfw = opts.nsfw;
this.emitAddSongWhenCreatingQueue = opts.emitAddSongWhenCreatingQueue;
this.emitAddListWhenCreatingQueue = opts.emitAddListWhenCreatingQueue;
this.joinNewVoiceChannel = opts.joinNewVoiceChannel;
this.ffmpeg = this.#ffmpegOption(options);
checkInvalidKey(opts, this, "DisTubeOptions");
this.#validateOptions();
}
#validateOptions(options = this) {
const booleanOptions = /* @__PURE__ */ new Set([
"emitNewSongOnly",
"savePreviousSongs",
"joinNewVoiceChannel",
"nsfw",
"emitAddSongWhenCreatingQueue",
"emitAddListWhenCreatingQueue"
]);
const numberOptions = /* @__PURE__ */ new Set();
const stringOptions = /* @__PURE__ */ new Set();
const objectOptions = /* @__PURE__ */ new Set(["customFilters", "ffmpeg"]);
const optionalOptions = /* @__PURE__ */ new Set(["customFilters"]);
for (const [key, value] of Object.entries(options)) {
if (value === void 0 && optionalOptions.has(key)) continue;
if (key === "plugins" && !Array.isArray(value)) {
throw new DisTubeError("INVALID_TYPE", "Array<Plugin>", value, `DisTubeOptions.${key}`);
} else if (booleanOptions.has(key)) {
if (typeof value !== "boolean") {
throw new DisTubeError("INVALID_TYPE", "boolean", value, `DisTubeOptions.${key}`);
}
} else if (numberOptions.has(key)) {
if (typeof value !== "number" || Number.isNaN(value)) {
throw new DisTubeError("INVALID_TYPE", "number", value, `DisTubeOptions.${key}`);
}
} else if (stringOptions.has(key)) {
if (typeof value !== "string") {
throw new DisTubeError("INVALID_TYPE", "string", value, `DisTubeOptions.${key}`);
}
} else if (objectOptions.has(key)) {
if (typeof value !== "object" || Array.isArray(value)) {
throw new DisTubeError("INVALID_TYPE", "object", value, `DisTubeOptions.${key}`);
}
}
}
}
#ffmpegOption(opts) {
const args = { global: {}, input: {}, output: {} };
if (opts.ffmpeg?.args) {
if (opts.ffmpeg.args.global) args.global = opts.ffmpeg.args.global;
if (opts.ffmpeg.args.input) args.input = opts.ffmpeg.args.input;
if (opts.ffmpeg.args.output) args.output = opts.ffmpeg.args.output;
}
const path = opts.ffmpeg?.path ?? "ffmpeg";
if (typeof path !== "string") {
throw new DisTubeError("INVALID_TYPE", "string", path, "DisTubeOptions.ffmpeg.path");
}
for (const [key, value] of Object.entries(args)) {
if (typeof value !== "object" || Array.isArray(value)) {
throw new DisTubeError("INVALID_TYPE", "object", value, `DisTubeOptions.ffmpeg.${key}`);
}
for (const [k, v] of Object.entries(value)) {
if (typeof v !== "string" && typeof v !== "number" && typeof v !== "boolean" && !Array.isArray(v) && v !== null && v !== void 0) {
throw new DisTubeError(
"INVALID_TYPE",
["string", "number", "boolean", "Array<string | null | undefined>", "null", "undefined"],
v,
`DisTubeOptions.ffmpeg.${key}.${k}`
);
}
}
}
return { path, args };
}
};
// src/core/DisTubeStream.ts
import { spawn, spawnSync } from "child_process";
import { Transform } from "stream";
import { createAudioResource, StreamType } from "@discordjs/voice";
import { TypedEmitter as TypedEmitter2 } from "tiny-typed-emitter";
var checked = process.env.NODE_ENV === "test";
var checkFFmpeg = /* @__PURE__ */ __name((distube) => {
if (checked) return;
const path = distube.options.ffmpeg.path;
const debug = /* @__PURE__ */ __name((str) => distube.emit("ffmpegDebug" /* FFMPEG_DEBUG */, str), "debug");
try {
debug(`[test] spawn ffmpeg at '${path}' path`);
const process2 = spawnSync(path, ["-h"], {
windowsHide: true,
encoding: "utf-8"
});
if (process2.error) throw process2.error;
if (process2.stderr && !process2.stdout) throw new Error(process2.stderr);
const result = process2.output.join("\n");
const version2 = /ffmpeg version (\S+)/iu.exec(result)?.[1];
if (!version2) throw new Error("Invalid FFmpeg version");
debug(`[test] ffmpeg version: ${version2}`);
} catch (e) {
const errorMessage = e instanceof Error ? e.stack ?? e.message : String(e);
debug(`[test] failed to spawn ffmpeg at '${path}': ${errorMessage}`);
throw new DisTubeError("FFMPEG_NOT_INSTALLED", path);
}
checked = true;
}, "checkFFmpeg");
var DisTubeStream = class extends TypedEmitter2 {
static {
__name(this, "DisTubeStream");
}
#ffmpegPath;
#opts;
process;
stream;
audioResource;
/**
* The seek time in seconds that this stream started from
*/
seekTime;
/**
* Create a DisTubeStream to play with {@link DisTubeVoice}
* @param url - Stream URL
* @param options - Stream options
*/
constructor(url, options) {
super();
const { ffmpeg, seek } = options;
this.seekTime = typeof seek === "number" && seek > 0 ? seek : 0;
const opts = {
reconnect: 1,
reconnect_streamed: 1,
reconnect_delay_max: 5,
analyzeduration: 0,
hide_banner: true,
...ffmpeg.args.global,
...ffmpeg.args.input,
i: url,
ar: AUDIO_SAMPLE_RATE,
ac: AUDIO_CHANNELS,
...ffmpeg.args.output,
f: "s16le"
};
if (typeof seek === "number" && seek > 0) opts.ss = seek.toString();
const fileUrl = new URL(url);
if (fileUrl.protocol === "file:") {
opts.reconnect = null;
opts.reconnect_streamed = null;
opts.reconnect_delay_max = null;
opts.i = fileUrl.hostname + fileUrl.pathname;
}
this.#ffmpegPath = ffmpeg.path;
this.#opts = [
...Object.entries(opts).flatMap(
([key, value]) => Array.isArray(value) ? value.filter(Boolean).map((v) => [`-${key}`, String(v)]) : value == null || value === false ? [] : [value === true ? `-${key}` : [`-${key}`, String(value)]]
).flat(),
"pipe:1"
];
this.stream = new VolumeTransformer();
this.stream.on("close", () => this.kill()).on("error", (err) => {
this.debug(`[stream] error: ${err.message}`);
this.emit("error", err);
}).on("finish", () => this.debug("[stream] log: stream finished"));
this.audioResource = createAudioResource(this.stream, {
inputType: StreamType.Raw,
inlineVolume: false
});
}
spawn() {
this.debug(`[process] spawn: ${this.#ffmpegPath} ${this.#opts.join(" ")}`);
this.process = spawn(this.#ffmpegPath, this.#opts, {
stdio: ["ignore", "pipe", "pipe"],
shell: false,
windowsHide: true
}).on("error", (err) => {
this.debug(`[process] error: ${err.message}`);
this.emit("error", err);
}).on("exit", (code, signal) => {
this.debug(`[process] exit: code=${code ?? "unknown"} signal=${signal ?? "unknown"}`);
if (!code || [0, 255].includes(code)) return;
this.debug(`[process] error: ffmpeg exited with code ${code}`);
this.emit("error", new DisTubeError("FFMPEG_EXITED", code));
});
if (!this.process.stdout || !this.process.stderr) {
this.kill();
throw new Error("Failed to create ffmpeg process");
}
this.process.stdout.pipe(this.stream);
this.process.stderr.setEncoding("utf8")?.on("data", (data) => {
const lines = data.split(/\r\n|\r|\n/u);
for (const line of lines) {
if (/^\s*$/.test(line)) continue;
this.debug(`[ffmpeg] log: ${line}`);
}
});
}
debug(debug) {
this.emit("debug", debug);
}
setVolume(volume) {
this.stream.vol = volume;
}
kill() {
if (!this.stream.destroyed) this.stream.destroy();
if (this.process && !this.process.killed) this.process.kill("SIGKILL");
}
};
var VolumeTransformer = class extends Transform {
static {
__name(this, "VolumeTransformer");
}
buffer = Buffer.allocUnsafe(0);
extrema = [-(2 ** (16 - 1)), 2 ** (16 - 1) - 1];
vol = 1;
_transform(newChunk, _encoding, done) {
const { vol } = this;
if (vol === 1) {
this.push(newChunk);
done();
return;
}
const bytes = 2;
const chunk = Buffer.concat([this.buffer, newChunk]);
const readableLength = Math.floor(chunk.length / bytes) * bytes;
for (let i = 0; i < readableLength; i += bytes) {
const value = chunk.readInt16LE(i);
const clampedValue = Math.min(this.extrema[1], Math.max(this.extrema[0], value * vol));
chunk.writeInt16LE(clampedValue, i);
}
this.buffer = chunk.subarray(readableLength);
this.push(chunk.subarray(0, readableLength));
done();
}
};
// src/core/manager/DisTubeVoiceManager.ts
import { getVoiceConnection, VoiceConnectionStatus as VoiceConnectionStatus2 } from "@discordjs/voice";
// src/core/manager/GuildIdManager.ts
var GuildIdManager = class extends BaseManager {
static {
__name(this, "GuildIdManager");
}
add(idOrInstance, data) {
const id = resolveGuildId(idOrInstance);
const existing = this.get(id);
if (existing) return this;
this.collection.set(id, data);
return this;
}
get(idOrInstance) {
return this.collection.get(resolveGuildId(idOrInstance));
}
remove(idOrInstance) {
return this.collection.delete(resolveGuildId(idOrInstance));
}
has(idOrInstance) {
return this.collection.has(resolveGuildId(idOrInstance));
}
};
// src/core/manager/DisTubeVoiceManager.ts
var DisTubeVoiceManager = class extends GuildIdManager {
static {
__name(this, "DisTubeVoiceManager");
}
/**
* Create a {@link DisTubeVoice} instance
* @param channel - A voice channel to join
*/
create(channel) {
const existing = this.get(channel.guildId);
if (existing) {
existing.channel = channel;
return existing;
}
if (getVoiceConnection(resolveGuildId(channel), this.client.user?.id) || getVoiceConnection(resolveGuildId(channel))) {
throw new DisTubeError("VOICE_ALREADY_CREATED");
}
return new DisTubeVoice(this, channel);
}
/**
* Join a voice channel and wait until the connection is ready
* @param channel - A voice channel to join
*/
join(channel) {
const existing = this.get(channel.guildId);
if (existing) return existing.join(channel);
return this.create(channel).join();
}
/**
* Leave the connected voice channel in a guild
* @param guild - Queue Resolvable
*/
leave(guild) {
const voice = this.get(guild);
if (voice) {
voice.leave();
} else {
const connection = getVoiceConnection(resolveGuildId(guild), this.client.user?.id) ?? getVoiceConnection(resolveGuildId(guild));
if (connection && connection.state.status !== VoiceConnectionStatus2.Destroyed) {
connection.destroy();
}
}
}
};
// src/core/manager/QueueManager.ts
var QueueManager = class extends GuildIdManager {
static {
__name(this, "QueueManager");
}
/**
* Create a {@link Queue}
* @param channel - A voice channel
* @param textChannel - Default text channel
* @returns Returns `true` if encounter an error
*/
async create(channel, textChannel) {
if (this.has(channel.guildId)) throw new DisTubeError("QUEUE_EXIST");
this.debug(`[QueueManager] Creating queue for guild: ${channel.guildId}`);
const voice = this.voices.create(channel);
const queue = new Queue(this.distube, voice, textChannel);
await queue._taskQueue.queuing();
try {
checkFFmpeg(this.distube);
this.debug(`[QueueManager] Joining voice channel: ${channel.id}`);
await voice.join();
this.#voiceEventHandler(queue);
this.add(queue.id, queue);
this.emit("initQueue" /* INIT_QUEUE */, queue);
return queue;
} finally {
queue._taskQueue.resolve();
}
}
/**
* Listen to DisTubeVoice events and handle the Queue
* @param queue - Queue
*/
#voiceEventHandler(queue) {
queue._listeners = {
disconnect: /* @__PURE__ */ __name((error) => {
queue.remove();
this.emit("disconnect" /* DISCONNECT */, queue);
if (error) this.emitError(error, queue, queue.songs?.[0]);
}, "disconnect"),
error: /* @__PURE__ */ __name((error) => void this.#handlePlayingError(queue, error), "error"),
finish: /* @__PURE__ */ __name(() => void this.handleSongFinish(queue), "finish")
};
for (const event of objectKeys(queue._listeners)) {
queue.voice.on(event, queue._listeners[event]);
}
}
/**
* Handle the queue when a Song finish
* @param queue - queue
*/
async handleSongFinish(queue) {
if (queue._manualUpdate) {
queue._manualUpdate = false;
await this.playSong(queue);
return;
}
this.debug(`[QueueManager] Handling song finish: ${queue.id}`);
const song = queue.songs[0];
this.emit("finishSong" /* FINISH_SONG */, queue, song);
await queue._taskQueue.queuing();
try {
if (queue.stopped) return;
if (queue.repeatMode === 2 /* QUEUE */) queue.songs.push(song);
if (queue.repeatMode !== 1 /* SONG */) {
const prev = queue.songs.shift();
if (this.options.savePreviousSongs) queue.previousSongs.push(prev);
else queue.previousSongs.push({ id: prev.id });
}
if (queue.songs.length === 0 && queue.autoplay) {
try {
this.debug(`[QueueManager] Adding related song: ${queue.id}`);
await queue._addRelatedSong(song);
} catch (e) {
const errorMessage = e instanceof Error ? e.message : String(e);
this.debug(`[${queue.id}] Add related song error: ${errorMessage}`);
if (e instanceof DisTubeError) {
this.emit("noRelated" /* NO_RELATED */, queue, e);
} else {
this.emit("noRelated" /* NO_RELATED */, queue, new DisTubeError("NO_RELATED"));
}
}
}
if (queue.songs.length === 0) {
this.debug(`[${queue.id}] Queue is empty, stopping...`);
if (!queue.autoplay) this.emit("finish" /* FINISH */, queue);
queue.remove();
return;
}
if (song !== queue.songs[0]) {
const playedSong = song.stream.playFromSource ? song : song.stream.song;
if (playedSong?.stream.playFromSource) delete playedSong.stream.url;
}
await this.playSong(queue, true);
} finally {
queue._taskQueue.resolve();
}
}
/**
* Handle error while playing
* @param queue - queue
* @param error - error
*/
async #handlePlayingError(queue, error) {
const song = queue.songs.shift();
try {
error.name = "PlayingError";
} catch {
}
this.debug(`[${queue.id}] Error while playing: ${error.stack || error.message}`);
this.emitError(error, queue, song);
if (queue.songs.length > 0) {
this.debug(`[${queue.id}] Playing next song: ${queue.songs[0]}`);
await this.playSong(queue);
} else {
this.debug(`[${queue.id}] Queue is empty, stopping...`);
await queue.stop();
}
}
/**
* Play a song on voice connection with queue properties
* @param queue - The guild queue to play
* @param emitPlaySong - Whether or not emit {@link Events.PLAY_SONG} event
*/
async playSong(queue, emitPlaySong = true) {
if (!queue) return;
if (queue.stopped || !queue.songs.length) {
await queue.stop();
return;
}
try {
const song = queue.songs[0];
this.debug(`[${queue.id}] Getting stream from: ${song}`);
await this.handler.attachStreamInfo(song);
const willPlaySong = song.stream.playFromSource ? song : song.stream.song;
const stream = willPlaySong?.stream;
if (!willPlaySong || !stream?.playFromSource || !stream.url) throw new DisTubeError("NO_STREAM_URL", `${song}`);
this.debug(`[${queue.id}] Creating DisTubeStream for: ${willPlaySong}`);
const streamOptions = {
ffmpeg: {
path: this.options.ffmpeg.path,
args: {
global: { ...queue.ffmpegArgs.global },
input: { ...queue.ffmpegArgs.input },
output: { ...queue.ffmpegArgs.output, ...queue.filters.ffmpegArgs }
}
},
seek: willPlaySong.duration ? queue._beginTime : void 0
};
queue._beginTime = 0;
const dtStream = new DisTubeStream(stream.url, streamOptions);
dtStream.on("debug", (data) => this.emit("ffmpegDebug" /* FFMPEG_DEBUG */, `[${queue.id}] ${data}`));
this.debug(`[${queue.id}] Started playing: ${willPlaySong}`);
await queue.voice.play(dtStream);
if (emitPlaySong) this.emit("playSong" /* PLAY_SONG */, queue, song);
} catch (e) {
const error = e instanceof Error ? e : new Error(String(e));
this.#handlePlayingError(queue, error);
}
}
};
// src/DisTube.ts
import { TypedEmitter as TypedEmitter3 } from "tiny-typed-emitter";
var DisTube = class extends TypedEmitter3 {
static {
__name(this, "DisTube");
}
/**
* @event
* Emitted after DisTube add a new playlist to the playing {@link Queue}.
* @param queue - The guild queue
* @param playlist - Playlist info
*/
static ["addList" /* ADD_LIST */];
/**
* @event
* Emitted after DisTube add a new song to the playing {@link Queue}.
* @param queue - The guild queue
* @param song - Added song
*/
static ["addSong" /* ADD_SONG */];
/**
* @event
* Emitted when a {@link Queue} is deleted with any reasons.
* @param queue - The guild queue
*/
static ["deleteQueue" /* DELETE_QUEUE */];
/**
* @event
* Emitted when the bot is disconnected to a voice channel.
* @param queue - The guild queue
*/
static ["disconnect" /* DISCONNECT */];
/**
* @event
* Emitted when DisTube encounters an error while playing songs.
* @param error - error
* @param queue - The queue encountered the error
* @param song - The playing song when encountered the error
*/
static ["error" /* ERROR */];
/**
* @event
* Emitted for logging FFmpeg debug information.
* @param debug - Debug message string.
*/
static ["ffmpegDebug" /* FFMPEG_DEBUG */];
/**
* @event
* Emitted to provide debug information from DisTube's operation.
* Useful for troubleshooting or logging purposes.
*
* @param debug - Debug message string.
*/
static ["debug" /* DEBUG */];
/**
* @event
* Emitted when there is no more song in the queue and {@link Queue#autoplay} is `false`.
* @param queue - The guild queue
*/
static ["finish" /* FINISH */];
/**
* @event
* Emitted when DisTube finished a song.
* @param queue - The guild queue
* @param song - Finished song
*/
static ["finishSong" /* FINISH_SONG */];
/**
* @event
* Emitted when DisTube initialize a queue to change queue default properties.
* @param queue - The guild queue
*/
static ["initQueue" /* INIT_QUEUE */];
/**
* @event
* Emitted when {@link Queue#autoplay} is `true`, {@link Queue#songs} is empty, and
* DisTube cannot find related songs to play.
* @param queue - The guild queue
*/
static ["noRelated" /* NO_RELATED */];
/**
* @event
* Emitted when DisTube play a song.
* If {@link DisTubeOptions}.emitNewSongOnly is `true`, this event is not emitted
* when looping a song or next song is the previous one.
* @param queue - The guild queue
* @param song - Playing song
*/
static ["playSong" /* PLAY_SONG */];
/**
* DisTube internal handler
*/
handler;
/**
* DisTube options
*/
options;
/**
* Discord.js v14 client
*/
client;
/**
* Queues manager
*/
queues;
/**
* DisTube voice connections manager
*/
voices;
/**
* DisTube plugins
*/
plugins;
/**
* DisTube ffmpeg audio filters
*/
filters;
/**
* Create a new DisTube class.
* @throws {@link DisTubeError}
* @param client - Discord.JS client
* @param opts - Custom DisTube options
*/
constructor(client, opts = {}) {
super();
this.setMaxListeners(1);
if (!isClientInstance(client)) throw new DisTubeError("INVALID_TYPE", "Discord.Client", client, "client");
this.client = client;
checkIntents(client.options);
this.options = new Options(opts);
this.voices = new DisTubeVoiceManager(this);
this.handler = new DisTubeHandler(this);
this.queues = new QueueManager(this);
this.filters = { ...defaultFilters, ...this.options.customFilters };
this.plugins = [...this.options.plugins];
for (const p of this.plugins) p.init(this);
}
static get version() {
return version;
}
/**
* DisTube version
*/
get version() {
return version;
}
/**
* Play / add a song or playlist from url.
* Search and play a song (with {@link ExtractorPlugin}) if it is not a valid url.
* @throws {@link DisTubeError}
* @param voiceChannel - The channel will be joined if the bot isn't in any channels, the bot will be
* moved to this channel if {@link DisTubeOptions}.joinNewVoiceChannel is `true`
* @param song - URL | Search string | {@link Song} | {@link Playlist}
* @param options - Optional options
*/
async play(voiceChannel, song, options = {}) {
if (!isSupportedVoiceChannel(voiceChannel)) {
throw new DisTubeError("INVALID_TYPE", "BaseGuildVoiceChannel", voiceChannel, "voiceChannel");
}
if (!isObject(options)) throw new DisTubeError("INVALID_TYPE", "object", options, "options");
const { textChannel, member, skip, message, metadata } = {
member: voiceChannel.guild.members.me ?? void 0,
textChannel: options?.message?.channel,
skip: false,
...options
};
const position = Number(options.position) || (skip ? 1 : 0);
if (message && !isMessageInstance(message)) {
throw new DisTubeError("INVALID_TYPE", ["Discord.Message", "a falsy value"], message, "options.message");
}
if (textChannel && !isTextChannelInstance(textChannel)) {
throw new DisTubeError("INVALID_TYPE", "Discord.GuildTextBasedChannel", textChannel, "options.textChannel");
}
if (member && !isMemberInstance(member)) {
throw new DisTubeError("INVALID_TYPE", "Discord.GuildMember", member, "options.member");
}
const queue = this.getQueue(voiceChannel) || await this.queues.create(voiceChannel, textChannel);
await queue._taskQueue.queuing(true);
try {
this.debug(`[${queue.id}] Playing input: ${song}`);
const resolved = await this.handler.resolve(song, { member, metadata });
const isNsfw = isNsfwChannel(queue?.textChannel || textChannel);
const isFirstSong = queue.songs.length === 0;
if (resolved instanceof Playlist) {
if (!this.options.nsfw && !isNsfw) {
resolved.songs = resolved.songs.filter((s) => !s.ageRestricted);
if (!resolved.songs.length) throw new DisTubeError("EMPTY_FILTERED_PLAYLIST");
}
if (!resolved.songs.length) throw new DisTubeError("EMPTY_PLAYLIST");
this.debug(`[${queue.id}] Adding playlist to queue: ${resolved.songs.length} songs`);
queue.addToQueue(resolved.songs, position);
if (!isFirstSong || this.options.emitAddListWhenCreatingQueue) this.emit("addList" /* ADD_LIST */, queue, resolved);
} else {
if (!this.options.nsfw && resolved.ageRestricted && !isNsfwChannel(queue?.textChannel || textChannel)) {
throw new DisTubeError("NON_NSFW");
}
this.debug(`[${queue.id}] Adding song to queue: ${resolved.name || resolved.url || resolved.id || resolved}`);
queue.addToQueue(resolved, position);
if (!isFirstSong || this.options.emitAddSongWhenCreatingQueue) this.emit("addSong" /* ADD_SONG */, queue, resolved);
}
if (isFirstSong) await queue.play();
else if (skip) await queue.skip();
} catch (e) {
if (!(e instanceof DisTubeError)) {
const errorMessage = e instanceof Error ? e.stack ?? e.message : String(e);
this.debug(`[${queue.id}] Unexpected error while playing song: ${errorMessage}`);
if (e instanceof Error) {
try {
e.name = "PlayError";
e.message = `${typeof song === "string" ? song : song.url}
${e.message}`;
} catch {
}
}
}
throw e;
} finally {
if (!queue.songs.length && !queue._taskQueue.hasPlayTask) queue.remove();
queue._taskQueue.resolve();
}
}
/**
* Create a custom playlist
* @param songs - Array of url or Song
* @param options - Optional options
*/
async createCustomPlaylist(songs, { member, parallel, metadata, name, source, url, thumbnail } = {}) {
if (!Array.isArray(songs)) throw new DisTubeError("INVALID_TYPE", "Array", songs, "songs");
if (!songs.length) throw new DisTubeError("EMPTY_ARRAY", "songs");
const filteredSongs = songs.filter((song) => song instanceof Song || isURL(song));
if (!filteredSongs.length) throw new DisTubeError("NO_VALID_SONG");
if (member && !isMemberInstance(member)) {
throw new DisTubeError("INVALID_TYPE", "Discord.Member", member, "options.member");
}
let resolvedSongs;
if (parallel !== false) {
const promises = filteredSongs.map(
(song) => this.handler.resolve(song, { member, metadata }).catch(() => void 0)
);
resolvedSongs = (await Promise.all(promises)).filter((s) => s instanceof Song);
} else {
resolvedSongs = [];
for (const song of filteredSongs) {
const resolved = await this.handler.resolve(song, { member, metadata }).catch(() => void 0);
if (resolved instanceof Song) resolvedSongs.push(resolved);
}
}
return new Playlist(
{
source: source || "custom",
name,
url,
thumbnail: thumbnail || resolvedSongs.find((s) => s.thumbnail)?.thumbnail,
songs: resolvedSongs
},
{ member, metadata }
);
}
/**
* Get the guild queue
* @param guild - The type can be resolved to give a {@link Queue}
*/
getQueue(guild) {
return this.queues.get(guild);
}
#getQueue(guild) {
const queue = this.getQueue(guild);
if (!queue) throw new DisTubeError("NO_QUEUE");
return queue;
}
/**
* Pause the guild stream
* @param guild - The type can be resolved to give a {@link Queue}
* @returns The guild queue
* @deprecated Use `distube.getQueue(guild).pause()` instead. Will be removed in v6.0.
*/
pause(guild) {
return this.#getQueue(guild).pause();
}
/**
* Resume the guild stream
* @param guild - The type can be resolved to give a {@link Queue}
* @returns The guild queue
* @deprecated Use `distube.getQueue(guild).resume()` instead. Will be removed in v6.0.
*/
resume(guild) {
return this.#getQueue(guild).resume();
}
/**
* Stop the guild stream
* @param guild - The type can be resolved to give a {@link Queue}
* @deprecated Use `distube.getQueue(guild).stop()` instead. Will be removed in v6.0.
*/
stop(guild) {
return this.#getQueue(guild).stop();
}
/**
* Set the guild stream's volume
* @param guild - The type can be resolved to give a {@link Queue}
* @param percent - The percentage of volume you want to set
* @returns The guild queue
* @deprecated Use `distube.getQueue(guild).setVolume(percent)` instead. Will be removed in v6.0.
*/
setVolume(guild, percent) {
return this.#getQueue(guild).setVolume(percent);
}
/**
* 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 guild - The type can be resolved to give a {@link Queue}
* @returns The new Song will be played
* @deprecated Use `distube.getQueue(guild).skip(options)` instead. Will be removed in v6.0.
*/
skip(guild, options) {
return this.#getQueue(guild).skip(options);
}
/**
* Play the previous song
* @param guild - The type can be resolved to give a {@link Queue}
* @returns The new Song will be played
* @deprecated Use `distube.getQueue(guild).previous()` instead. Will be removed in v6.0.
*/
previous(guild) {
return this.#getQueue(guild).previous();
}
/**
* Shuffle the guild queue songs
* @param guild - The type can be resolved to give a {@link Queue}
* @returns The guild queue
* @deprecated Use `distube.getQueue(guild).shuffle()` instead. Will be removed in v6.0.
*/
shuffle(guild) {
return this.#getQueue(guild).shuffle();
}
/**
* Jump to the song number in the queue. The next one is 1, 2,... The previous one
* is -1, -2,...
* @param guild - The type can be resolved to give a {@link Queue}
* @param num - The song number to play
* @returns The new Song will be played
* @deprecated Use `distube.getQueue(guild).jump(num, options)` instead. Will be removed in v6.0.
*/
jump(guild, num, options) {
return this.#getQueue(guild).jump(num, options);
}
/**
* Set the repeat mode of the guild queue.
* Toggle mode `(Disabled -> Song -> Queue -> Disabled ->...)` if `mode` is `undefined`
* @param guild - The type can be resolved to give a {@link Queue}
* @param mode - The repeat modes (toggle if `undefined`)
* @returns The new repeat mode
* @deprecated Use `distube.getQueue(guild).setRepeatMode(mode)` instead. Will be removed in v6.0.
*/
setRepeatMode(guild, mode) {
return this.#getQueue(guild).setRepeatMode(mode);
}
/**
* Toggle autoplay mode
* @param guild - The type can be resolved to give a {@link Queue}
* @returns Autoplay mode state
* @deprecated Use `distube.getQueue(guild).toggleAutoplay()` instead. Will be removed in v6.0.
*/
toggleAutoplay(guild) {
const queue = this.#getQueue(guild);
queue.autoplay = !queue.autoplay;
return queue.autoplay;
}
/**
* Add related song to the queue
* @param guild - The type can be resolved to give a {@link Queue}
* @returns The guild queue
* @deprecated Use `distube.getQueue(guild).addRelatedSong()` instead. Will be removed in v6.0.
*/
addRelatedSong(guild) {
return this.#getQueue(guild).addRelatedSong();
}
/**
* Set the playing time to another position
* @param guild - The type can be resolved to give a {@link Queue}
* @param time - Time in seconds
* @returns Seeked queue
* @deprecated Use `distube.getQueue(guild).seek(time)` instead. Will be removed in v6.0.
*/
seek(guild, time) {
return this.#getQueue(guild).seek(time);
}
/**
* 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.emit("error" /* ERROR */, error, queue, song);
}
/**
* Emit debug event
* @param message - debug message
*/
debug(message) {
this.emit("debug" /* DEBUG */, message);
}
};
// src/struct/Plugin.ts
var Plugin = class {
static {
__name(this, "Plugin");
}
/**
* DisTube
*/
distube;
init(distube) {
this.distube = distube;
}
};
// src/struct/ExtractorPlugin.ts
var ExtractorPlugin = class extends Plugin {
static {
__name(this, "ExtractorPlugin");
}
type = "extractor" /* EXTRACTOR */;
};
// src/struct/InfoExtractorPlugin.ts
var InfoExtractorPlugin = class extends Plugin {
static {
__name(this, "InfoExtractorPlugin");
}
type = "info-extractor" /* INFO_EXTRACTOR */;
};
// src/struct/PlayableExtractorPlugin.ts
var PlayableExtractorPlugin = class extends Plugin {
static {
__name(this, "PlayableExtractorPlugin");
}
type = "playable-extractor" /* PLAYABLE_EXTRACTOR */;
};
export {
AUDIO_CHANNELS,
AUDIO_SAMPLE_RATE,
BaseManager,
DEFAULT_VOLUME,
DisTube,
DisTubeBase,
DisTubeError,
DisTubeHandler,
DisTubeStream,
DisTubeVoice,
DisTubeVoiceManager,
Events,
ExtractorPlugin,
FilterManager,
GuildIdManager,
HTTP_REDIRECT_CODES,
InfoExtractorPlugin,
InfoExtractorPlugin as InfoExtratorPlugin,
JOIN_TIMEOUT_MS,
MAX_REDIRECT_DEPTH,
Options,
PlayableExtractorPlugin,
PlayableExtractorPlugin as PlayableExtratorPlugin,
Playlist,
Plugin,
PluginType,
Queue,
QueueManager,
RECONNECT_MAX_ATTEMPTS,
RECONNECT_TIMEOUT_MS,
RepeatMode,
Song,
TaskQueue,
checkEncryptionLibraries,
checkFFmpeg,
checkIntents,
checkInvalidKey,
DisTube as default,
defaultFilters,
defaultOptions,
formatDuration,
isClientInstance,
isGuildInstance,
isMemberInstance,
isMessageInstance,
isNsfwChannel,
isObject,
isSnowflake,
isSupportedVoiceChannel,
isTextChannelInstance,
isTruthy,
isURL,
isVoiceChannelEmpty,
objectKeys,
resolveGuildId,
version
};
//# sourceMappingURL=index.mjs.map