distube
Version:
A powerful Discord.js module for simplifying music commands and effortless playback of various sources with integrated audio filters.
1,552 lines (1,535 loc) • 79.9 kB
JavaScript
var __defProp = Object.defineProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
// 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/constant.ts
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/struct/DisTubeError.ts
import { inspect } from "node: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/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/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/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/DisTubeVoice.ts
import { Constants } from "discord.js";
import { TypedEmitter } from "tiny-typed-emitter";
import {
AudioPlayerStatus,
VoiceConnectionDisconnectReason,
VoiceConnectionStatus,
createAudioPlayer,
entersState,
joinVoiceChannel
} from "@discordjs/voice";
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, 5e3).catch(() => {
if (![VoiceConnectionStatus.Ready, VoiceConnectionStatus.Connecting].includes(this.connection.state.status)) {
this.leave();
}
});
} else if (this.connection.rejoinAttempts < 5) {
setTimeout(
() => {
this.connection.rejoin();
},
(this.connection.rejoinAttempts + 1) * 5e3
).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) {
const TIMEOUT = 3e4;
if (channel) this.channel = channel;
try {
await entersState(this.connection, VoiceConnectionStatus.Ready, TIMEOUT);
} 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", TIMEOUT / 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);
}
/**
* Play a {@link DisTubeStream}
* @param dtStream - DisTubeStream
*/
async play(dtStream) {
if (!await checkEncryptionLibraries()) {
dtStream.kill();
throw new DisTubeError("ENCRYPTION_LIBRARIES_MISSING");
}
this.emittedError = false;
dtStream.on("error", (error) => {
if (this.emittedError || error.code === "ERR_STREAM_PREMATURE_CLOSE") return;
this.emittedError = true;
this.emit("error", error);
});
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" || 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(Math.pow(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
*/
get playbackDuration() {
return (this.stream?.audioResource?.playbackDuration ?? 0) / 1e3;
}
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/DisTubeStream.ts
import { Transform } from "stream";
import { spawn, spawnSync } from "child_process";
import { TypedEmitter as TypedEmitter2 } from "tiny-typed-emitter";
import { StreamType, createAudioResource } from "@discordjs/voice";
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, shell: 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) {
debug(`[test] failed to spawn ffmpeg at '${path}': ${e?.stack ?? e}`);
throw new DisTubeError("FFMPEG_NOT_INSTALLED", path);
}
checked = true;
}, "checkFFmpeg");
var DisTubeStream = class extends TypedEmitter2 {
static {
__name(this, "DisTubeStream");
}
#ffmpegPath;
#opts;
process;
stream;
audioResource;
/**
* Create a DisTubeStream to play with {@link DisTubeVoice}
* @param url - Stream URL
* @param options - Stream options
*/
constructor(url, options) {
super();
const { ffmpeg, seek } = options;
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: 48e3,
ac: 2,
...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 = [-Math.pow(2, 16 - 1), Math.pow(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/DisTubeHandler.ts
import { request } from "undici";
var REDIRECT_CODES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
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 = 5) {
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 (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" || 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/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/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
import { VoiceConnectionStatus as VoiceConnectionStatus2, getVoiceConnection } from "@discordjs/voice";
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/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.prototype.hasOwnProperty.call(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/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) => this.#handlePlayingError(queue, error), "error"),
finish: /* @__PURE__ */ __name(() => this.#handleSongFinish(queue), "finish")
};
for (const event of objectKeys(queue._listeners)) {
queue.voice.on(event, queue._listeners[event]);
}
}
/**
* Whether or not emit playSong event
* @param queue - Queue
*/
#emitPlaySong(queue) {
if (!this.options.emitNewSongOnly) return true;
if (queue.repeatMode === 1 /* SONG */) return queue._next || queue._prev;
return queue.songs[0].id !== queue.songs[1].id;
}
/**
* Handle the queue when a Song finish
* @param queue - queue
*/
async #handleSongFinish(queue) {
this.debug(`[QueueManager] Handling song finish: ${queue.id}`);
const song = queue.songs[0];
this.emit("finishSong" /* FINISH_SONG */, queue, queue.songs[0]);
await queue._taskQueue.queuing();
try {
if (queue.stopped) return;
if (queue.repeatMode === 2 /* QUEUE */ && !queue._prev) queue.songs.push(song);
if (queue._prev) {
if (queue.repeatMode === 2 /* QUEUE */) queue.songs.unshift(queue.songs.pop());
else queue.songs.unshift(queue.previousSongs.pop());
}
if (queue.songs.length <= 1 && (queue._next || queue.repeatMode === 0 /* DISABLED */)) {
if (queue.autoplay) {
try {
this.debug(`[QueueManager] Adding related song: ${queue.id}`);
await queue.addRelatedSong();
} catch (e) {
this.debug(`[${queue.id}] Add related song error: ${e.message}`);
this.emit("noRelated" /* NO_RELATED */, queue, e);
}
}
if (queue.songs.length <= 1) {
this.debug(`[${queue.id}] Queue is empty, stopping...`);
if (!queue.autoplay) this.emit("finish" /* FINISH */, queue);
queue.remove();
return;
}
}
const emitPlaySong = this.#emitPlaySong(queue);
if (!queue._prev && (queue.repeatMode !== 1 /* SONG */ || queue._next)) {
const prev = queue.songs.shift();
if (this.options.savePreviousSongs) queue.previousSongs.push(prev);
else queue.previousSongs.push({ id: prev.id });
}
queue._next = queue._prev = false;
queue._beginTime = 0;
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, emitPlaySong);
} finally {
queue._taskQueue.resolve();
}
}
/**
* Handle error while playing
* @param queue - queue
* @param error - error
*/
#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]}`);
queue._next = queue._prev = false;
queue._beginTime = 0;
this.playSong(queue);
} else {
this.debug(`[${queue.id}] Queue is empty, stopping...`);
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) {
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
};
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) {
this.#handlePlayingError(queue, e);
}
}
};
// 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 stream is currently playing.
*/
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;
#filters;
/**
* What time in the song to begin (in seconds).
*/
_beginTime;
/**
* Whether or not the last song was skipped to next song.
*/
_next;
/**
* Whether or not the last song was skipped to previous song.
*/
_prev;
/**
* Task queuing system
*/
_taskQueue;
/**
* {@link DisTubeVoice} listener
*/
_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 = 50;
this.songs = [];
this.previousSongs = [];
this.stopped = false;
this._next = false;
this._prev = 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 = {
glob