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