@hunghg255/distube
Version:
A Discord.js module to simplify your music commands and play songs with audio filters on Discord without any API key.
1,346 lines (1,333 loc) • 113 kB
JavaScript
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __typeError = (msg) => {
throw TypeError(msg);
};
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
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);
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
// package.json
var require_package = __commonJS({
"package.json"(exports2, module2) {
module2.exports = {
name: "@hunghg255/distube",
private: false,
version: "4.2.3",
description: "A Discord.js module to simplify your music commands and play songs with audio filters on Discord without any API key.",
main: "./dist/index.js",
types: "./dist/index.d.ts",
exports: "./dist/index.js",
directories: {
lib: "src",
test: "tests"
},
files: [
"dist"
],
scripts: {
test: "jest",
docs: "typedoc",
lint: "prettier --check . && eslint .",
"lint:fix": "eslint . --fix",
prettier: 'prettier --write "**/*.{ts,json,yml,yaml,md}"',
build: "tsup",
"build:check": "tsc --noEmit",
update: "pnpm up -L",
prepublishOnly: "pnpm run build",
"dev:add-docs-to-worktree": "git worktree add --track -b docs docs origin/docs"
},
repository: {
type: "git",
url: "git+https://github.com/skick1234/DisTube.git"
},
keywords: [
"youtube",
"music",
"discord",
"discordjs",
"bot",
"distube",
"queue",
"musicbot",
"discord-music-bot",
"music-bot",
"discord-js"
],
author: "Skick (https://github.com/skick1234)",
license: "MIT",
bugs: {
url: "https://github.com/skick1234/DisTube/issues"
},
funding: "https://github.com/skick1234/DisTube?sponsor=1",
homepage: "https://distube.js.org/",
dependencies: {
"@distube/ytdl-core": "^4.13.3",
"@distube/ytpl": "^1.2.1",
"@distube/ytsr": "^2.0.0",
"tiny-typed-emitter": "^2.1.0",
"tough-cookie": "^4.1.3",
undici: "^6.13.0",
tslib: "^2.8.1"
},
devDependencies: {
"@babel/core": "^7.24.4",
"@babel/plugin-transform-class-properties": "^7.24.1",
"@babel/plugin-transform-object-rest-spread": "^7.24.1",
"@babel/plugin-transform-private-methods": "^7.24.1",
"@babel/preset-env": "^7.24.4",
"@babel/preset-typescript": "^7.24.1",
"@commitlint/cli": "^19.2.2",
"@commitlint/config-conventional": "^19.2.2",
"@discordjs/voice": "^0.18.0",
"@types/jest": "^29.5.12",
"@types/node": "^20.12.7",
"@types/tough-cookie": "^4.0.5",
"@typescript-eslint/eslint-plugin": "^7.7.0",
"@typescript-eslint/parser": "^7.7.0",
"babel-jest": "^29.7.0",
"discord.js": "^14.18.0",
eslint: "^8.57.0",
"eslint-config-distube": "^1.7.0",
jest: "^29.7.0",
"nano-staged": "^0.8.0",
pinst: "^3.0.0",
prettier: "^3.2.5",
tsup: "^8.0.2",
typedoc: "^0.25.13",
"typedoc-material-theme": "^1.0.2",
typescript: "^5.4.5"
},
peerDependencies: {
"@discordjs/voice": "*",
"discord.js": "14"
},
"nano-staged": {
"*.ts": [
"prettier --write",
"eslint"
],
"*.{json,yml,yaml,md}": [
"prettier --write"
]
},
engines: {
node: ">=18.17"
},
packageManager: "pnpm@8.15.9"
};
}
});
// src/index.ts
var index_exports = {};
__export(index_exports, {
BaseManager: () => BaseManager,
CustomPlugin: () => CustomPlugin,
DirectLinkPlugin: () => DirectLinkPlugin,
DisTube: () => DisTube,
DisTubeBase: () => DisTubeBase,
DisTubeError: () => DisTubeError,
DisTubeHandler: () => DisTubeHandler,
DisTubeStream: () => DisTubeStream,
DisTubeVoice: () => DisTubeVoice,
DisTubeVoiceManager: () => DisTubeVoiceManager,
Events: () => Events,
ExtractorPlugin: () => ExtractorPlugin,
FilterManager: () => FilterManager,
GuildIdManager: () => GuildIdManager,
Options: () => Options,
Playlist: () => Playlist,
Plugin: () => Plugin,
PluginType: () => PluginType,
Queue: () => Queue,
QueueManager: () => QueueManager,
RepeatMode: () => RepeatMode,
SearchResultPlaylist: () => SearchResultPlaylist,
SearchResultType: () => SearchResultType,
SearchResultVideo: () => SearchResultVideo,
Song: () => Song,
StreamType: () => StreamType,
TaskQueue: () => TaskQueue,
checkFFmpeg: () => checkFFmpeg,
checkIntents: () => checkIntents,
checkInvalidKey: () => checkInvalidKey,
chooseBestVideoFormat: () => chooseBestVideoFormat,
default: () => DisTube,
defaultFilters: () => defaultFilters,
defaultOptions: () => defaultOptions,
formatDuration: () => formatDuration,
isClientInstance: () => isClientInstance,
isGuildInstance: () => isGuildInstance,
isMemberInstance: () => isMemberInstance,
isMessageInstance: () => isMessageInstance,
isNsfwChannel: () => isNsfwChannel,
isObject: () => isObject,
isRecord: () => isRecord,
isSnowflake: () => isSnowflake,
isSupportedVoiceChannel: () => isSupportedVoiceChannel,
isTextChannelInstance: () => isTextChannelInstance,
isTruthy: () => isTruthy,
isURL: () => isURL,
isVoiceChannelEmpty: () => isVoiceChannelEmpty,
objectKeys: () => objectKeys,
parseNumber: () => parseNumber,
resolveGuildId: () => resolveGuildId,
toSecond: () => toSecond,
version: () => version
});
module.exports = __toCommonJS(index_exports);
// src/type.ts
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["CUSTOM"] = "custom";
PluginType2["EXTRACTOR"] = "extractor";
return PluginType2;
})(PluginType || {});
var SearchResultType = /* @__PURE__ */ ((SearchResultType2) => {
SearchResultType2["VIDEO"] = "video";
SearchResultType2["PLAYLIST"] = "playlist";
return SearchResultType2;
})(SearchResultType || {});
var StreamType = /* @__PURE__ */ ((StreamType2) => {
StreamType2[StreamType2["OPUS"] = 0] = "OPUS";
StreamType2[StreamType2["RAW"] = 1] = "RAW";
return StreamType2;
})(StreamType || {});
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["SEARCH_CANCEL"] = "searchCancel";
Events2["SEARCH_NO_RESULT"] = "searchNoResult";
Events2["SEARCH_DONE"] = "searchDone";
Events2["SEARCH_INVALID_ANSWER"] = "searchInvalidAnswer";
Events2["SEARCH_RESULT"] = "searchResult";
Events2["FFMPEG_DEBUG"] = "ffmpegDebug";
return Events2;
})(Events || {});
// 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,
leaveOnEmpty: true,
leaveOnFinish: false,
leaveOnStop: true,
savePreviousSongs: true,
searchSongs: 0,
ytdlOptions: {},
searchCooldown: 60,
emptyCooldown: 60,
nsfw: false,
emitAddSongWhenCreatingQueue: true,
emitAddListWhenCreatingQueue: true,
joinNewVoiceChannel: true,
streamType: 0 /* OPUS */,
directLink: true
};
// 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_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"),
NO_QUEUE: "There is no playing queue in this guild",
QUEUE_EXIST: "This guild has a Queue 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: "There is no playing song in the queue",
NO_RESULT: "No result found",
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",
CANNOT_RESOLVE_SONG: /* @__PURE__ */ __name((t) => `Cannot resolve ${(0, import_node_util.inspect)(t)} to a Song`, "CANNOT_RESOLVE_SONG"),
NO_VALID_SONG: "'songs' array does not have any valid Song, SearchResult or 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 {
constructor(code, ...args) {
super(getErrorMessage(code, ...args));
__publicField(this, "errorCode");
this.errorCode = code;
if (Error.captureStackTrace) Error.captureStackTrace(this, _DisTubeError);
}
get name() {
return `DisTubeError [${this.errorCode}]`;
}
get code() {
return this.errorCode;
}
};
__name(_DisTubeError, "DisTubeError");
var DisTubeError = _DisTubeError;
// src/struct/TaskQueue.ts
var _Task = class _Task {
constructor(resolveInfo) {
__publicField(this, "resolve");
__publicField(this, "promise");
__publicField(this, "resolveInfo");
this.resolveInfo = resolveInfo;
this.promise = new Promise((res) => {
this.resolve = res;
});
}
};
__name(_Task, "Task");
var Task = _Task;
var _tasks;
var _TaskQueue = class _TaskQueue {
constructor() {
/**
* The task array
*/
__privateAdd(this, _tasks, []);
}
/**
* Waits for last task finished and queues a new task
*
* @param resolveInfo - Whether the task is a resolving info task
*/
queuing(resolveInfo = false) {
const next = this.remaining ? __privateGet(this, _tasks)[__privateGet(this, _tasks).length - 1].promise : Promise.resolve();
__privateGet(this, _tasks).push(new Task(resolveInfo));
return next;
}
/**
* Removes the finished task and processes the next task
*/
resolve() {
__privateGet(this, _tasks).shift()?.resolve();
}
/**
* The remaining number of tasks
*/
get remaining() {
return __privateGet(this, _tasks).length;
}
/**
* Whether or not having a resolving info task
*/
get hasResolveTask() {
return __privateGet(this, _tasks).some((t) => t.resolveInfo);
}
};
_tasks = new WeakMap();
__name(_TaskQueue, "TaskQueue");
var TaskQueue = _TaskQueue;
// src/struct/Playlist.ts
var _metadata, _member;
var _Playlist = class _Playlist {
/**
* Create a playlist
*
* @param playlist - Playlist
* @param options - Optional options
*/
constructor(playlist, options = {}) {
__publicField(this, "source");
__publicField(this, "songs");
__publicField(this, "name");
__privateAdd(this, _metadata);
__privateAdd(this, _member);
__publicField(this, "url");
__publicField(this, "thumbnail");
const { member, properties, metadata } = options;
if (typeof playlist !== "object" || !Array.isArray(playlist) && ["source", "songs"].some((key) => !(key in playlist))) {
throw new DisTubeError("INVALID_TYPE", ["Array<Song>", "PlaylistInfo"], playlist, "playlist");
}
if (typeof properties !== "undefined" && !isRecord(properties)) {
throw new DisTubeError("INVALID_TYPE", "object", properties, "properties");
}
if (Array.isArray(playlist)) {
this.source = "youtube";
if (!playlist.length) throw new DisTubeError("EMPTY_PLAYLIST");
this.songs = playlist;
this.name = this.songs[0].name ? `${this.songs[0].name} and ${this.songs.length - 1} more songs.` : `${this.songs.length} songs playlist`;
this.thumbnail = this.songs[0].thumbnail;
this.member = member;
} else {
this.source = playlist.source.toLowerCase();
if (!Array.isArray(playlist.songs) || !playlist.songs.length) throw new DisTubeError("EMPTY_PLAYLIST");
this.songs = playlist.songs;
this.name = playlist.name || // eslint-disable-next-line deprecation/deprecation
playlist.title || (this.songs[0].name ? `${this.songs[0].name} and ${this.songs.length - 1} more songs.` : `${this.songs.length} songs playlist`);
this.url = playlist.url || playlist.webpage_url;
this.thumbnail = playlist.thumbnail || this.songs[0].thumbnail;
this.member = member || playlist.member;
}
this.songs.forEach((s) => s.constructor.name === "Song" && (s.playlist = this));
if (properties) for (const [key, value] of Object.entries(properties)) this[key] = value;
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 __privateGet(this, _member);
}
set member(member) {
if (!isMemberInstance(member)) return;
__privateSet(this, _member, member);
this.songs.forEach((s) => s.constructor.name === "Song" && (s.member = this.member));
}
/**
* User requested.
*/
get user() {
return this.member?.user;
}
get metadata() {
return __privateGet(this, _metadata);
}
set metadata(metadata) {
__privateSet(this, _metadata, metadata);
this.songs.forEach((s) => s.constructor.name === "Song" && (s.metadata = metadata));
}
};
_metadata = new WeakMap();
_member = new WeakMap();
__name(_Playlist, "Playlist");
var Playlist = _Playlist;
// src/struct/SearchResult.ts
var _ISearchResult = class _ISearchResult {
/**
* Create a search result
*
* @param info - ytsr result
*/
constructor(info) {
__publicField(this, "source");
__publicField(this, "id");
__publicField(this, "name");
__publicField(this, "url");
__publicField(this, "uploader");
this.source = "youtube";
this.id = info.id;
this.name = info.name;
this.url = info.url;
this.uploader = {
name: void 0,
url: void 0
};
}
};
__name(_ISearchResult, "ISearchResult");
var ISearchResult = _ISearchResult;
var _SearchResultVideo = class _SearchResultVideo extends ISearchResult {
constructor(info) {
super(info);
__publicField(this, "type");
__publicField(this, "views");
__publicField(this, "isLive");
__publicField(this, "duration");
__publicField(this, "formattedDuration");
__publicField(this, "thumbnail");
if (info.type !== "video") throw new DisTubeError("INVALID_TYPE", "video", info.type, "type");
this.type = "video" /* VIDEO */;
this.views = info.views;
this.isLive = info.isLive;
this.duration = this.isLive ? 0 : toSecond(info.duration);
this.formattedDuration = this.isLive ? "Live" : formatDuration(this.duration);
this.thumbnail = info.thumbnail;
this.uploader = {
name: info.author?.name,
url: info.author?.url
};
}
};
__name(_SearchResultVideo, "SearchResultVideo");
var SearchResultVideo = _SearchResultVideo;
var _SearchResultPlaylist = class _SearchResultPlaylist extends ISearchResult {
constructor(info) {
super(info);
__publicField(this, "type");
__publicField(this, "length");
if (info.type !== "playlist") throw new DisTubeError("INVALID_TYPE", "playlist", info.type, "type");
this.type = "playlist" /* PLAYLIST */;
this.length = info.length;
this.uploader = {
name: info.owner?.name,
url: info.owner?.url
};
}
};
__name(_SearchResultPlaylist, "SearchResultPlaylist");
var SearchResultPlaylist = _SearchResultPlaylist;
// src/struct/Song.ts
var _metadata2, _member2, _playlist;
var _Song = class _Song {
/**
* Create a Song
*
* @param info - Raw info
* @param options - Optional options
*/
constructor(info, options = {}) {
__publicField(this, "source");
__privateAdd(this, _metadata2);
__publicField(this, "formats");
__privateAdd(this, _member2);
__publicField(this, "id");
__publicField(this, "name");
__publicField(this, "isLive");
__publicField(this, "duration");
__publicField(this, "formattedDuration");
__publicField(this, "url");
__publicField(this, "streamURL");
__publicField(this, "thumbnail");
__publicField(this, "related");
__publicField(this, "views");
__publicField(this, "likes");
__publicField(this, "dislikes");
__publicField(this, "uploader");
__publicField(this, "age_restricted");
__publicField(this, "chapters");
__publicField(this, "reposts");
__privateAdd(this, _playlist);
const { member, source, metadata } = { source: "youtube", ...options };
if (typeof source !== "string" || info.src && typeof info.src !== "string") {
throw new DisTubeError("INVALID_TYPE", "string", source, "source");
}
this.source = (info?.src || source).toLowerCase();
this.metadata = metadata;
this.member = member;
if (this.source === "youtube") {
this._patchYouTube(info);
} else {
this._patchOther(info);
}
}
_patchYouTube(i) {
const info = i;
if (info.full === true) {
this.formats = info.formats;
const err = require("@distube/ytdl-core/lib/utils").playError(info.player_response, [
"UNPLAYABLE",
"LIVE_STREAM_OFFLINE",
"LOGIN_REQUIRED"
]);
if (err) throw err;
if (!info.formats?.length) throw new DisTubeError("UNAVAILABLE_VIDEO");
}
const details = info.videoDetails || info;
this.id = details.videoId || details.id;
this.name = details.title || details.name;
this.isLive = Boolean(details.isLive);
this.duration = this.isLive ? 0 : toSecond(details.lengthSeconds || details.length_seconds || details.duration);
this.formattedDuration = this.isLive ? "Live" : formatDuration(this.duration);
this.url = `https://www.youtube.com/watch?v=${this.id}`;
this.streamURL = void 0;
this.thumbnail = details.thumbnails?.sort((a, b) => b.width - a.width)?.[0]?.url || details.thumbnail?.url || details.thumbnail;
this.related = info?.related_videos || details.related || [];
if (!Array.isArray(this.related)) throw new DisTubeError("INVALID_TYPE", "Array", this.related, "Song#related");
this.related = this.related.map((v) => new _Song(v, { source: this.source, metadata: this.metadata }));
this.views = parseNumber(details.viewCount || details.view_count || details.views);
this.likes = parseNumber(details.likes);
this.dislikes = parseNumber(details.dislikes);
this.uploader = {
name: info.uploader?.name || details.author?.name,
url: info.uploader?.url || details.author?.channel_url || details.author?.url
};
this.age_restricted = Boolean(details.age_restricted);
this.chapters = details.chapters || [];
this.reposts = 0;
}
/**
* Patch data from other source
*
* @param info - Video info
*/
_patchOther(info) {
this.id = info.id;
this.name = info.title || info.name;
this.isLive = Boolean(info.is_live || info.isLive);
this.duration = this.isLive ? 0 : toSecond(info._duration_raw || info.duration);
this.formattedDuration = this.isLive ? "Live" : formatDuration(this.duration);
this.url = info.webpage_url || info.url;
this.thumbnail = info.thumbnail;
this.related = info.related || [];
if (!Array.isArray(this.related)) throw new DisTubeError("INVALID_TYPE", "Array", this.related, "Song#related");
this.related = this.related.map((i) => new _Song(i, { source: this.source, metadata: this.metadata }));
this.views = parseNumber(info.view_count || info.views);
this.likes = parseNumber(info.like_count || info.likes);
this.dislikes = parseNumber(info.dislike_count || info.dislikes);
this.reposts = parseNumber(info.repost_count || info.reposts);
if (typeof info.uploader === "string") {
this.uploader = {
name: info.uploader,
url: info.uploader_url
};
} else {
this.uploader = {
name: info.uploader?.name,
url: info.uploader?.url
};
}
this.age_restricted = info.age_restricted || Boolean(info.age_limit) && parseNumber(info.age_limit) >= 18;
this.chapters = info.chapters || [];
}
/**
* The playlist added this song
*/
get playlist() {
return __privateGet(this, _playlist);
}
set playlist(playlist) {
if (!(playlist instanceof Playlist)) throw new DisTubeError("INVALID_TYPE", "Playlist", playlist, "Song#playlist");
__privateSet(this, _playlist, playlist);
this.member = playlist.member;
}
/**
* User requested.
*/
get member() {
return __privateGet(this, _member2);
}
set member(member) {
if (isMemberInstance(member)) __privateSet(this, _member2, member);
}
/**
* User requested.
*/
get user() {
return this.member?.user;
}
get metadata() {
return __privateGet(this, _metadata2);
}
set metadata(metadata) {
__privateSet(this, _metadata2, metadata);
}
};
_metadata2 = new WeakMap();
_member2 = new WeakMap();
_playlist = new WeakMap();
__name(_Song, "Song");
var Song = _Song;
// src/core/DisTubeBase.ts
var _DisTubeBase = class _DisTubeBase {
constructor(distube) {
__publicField(this, "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 channel - Text channel where the error is encountered.
*/
emitError(error, channel) {
this.distube.emitError(error, channel);
}
/**
* 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;
}
};
__name(_DisTubeBase, "DisTubeBase");
var DisTubeBase = _DisTubeBase;
// src/core/DisTubeVoice.ts
var import_discord = require("discord.js");
var import_tiny_typed_emitter = require("tiny-typed-emitter");
var import_voice = require("@discordjs/voice");
var _channel, _volume, _DisTubeVoice_instances, br_fn, join_fn;
var _DisTubeVoice = class _DisTubeVoice extends import_tiny_typed_emitter.TypedEmitter {
constructor(voiceManager, channel) {
super();
__privateAdd(this, _DisTubeVoice_instances);
__publicField(this, "id");
__publicField(this, "voices");
__publicField(this, "audioPlayer");
__publicField(this, "connection");
__publicField(this, "audioResource");
__publicField(this, "emittedError");
__publicField(this, "isDisconnected", false);
__publicField(this, "stream");
__privateAdd(this, _channel);
__privateAdd(this, _volume, 100);
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) {
delete this.audioResource;
this.emit("finish");
}
}).on(import_voice.AudioPlayerStatus.Playing, () => __privateMethod(this, _DisTubeVoice_instances, br_fn).call(this)).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, 5e3).catch(() => {
if (![import_voice.VoiceConnectionStatus.Ready, import_voice.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 !== 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 __privateGet(this, _channel);
if (__privateGet(this, _channel)?.id === this.channelId) return __privateGet(this, _channel);
const channel = this.voices.client.channels.cache.get(this.channelId);
if (!channel) return __privateGet(this, _channel);
for (const type of import_discord.Constants.VoiceBasedChannelTypes) {
if (channel.type === type) {
__privateSet(this, _channel, channel);
return channel;
}
}
return __privateGet(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 = __privateMethod(this, _DisTubeVoice_instances, join_fn).call(this, channel);
__privateSet(this, _channel, channel);
__privateMethod(this, _DisTubeVoice_instances, br_fn).call(this);
}
/**
* 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 (0, import_voice.entersState)(this.connection, import_voice.VoiceConnectionStatus.Ready, TIMEOUT);
} 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", 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 !== 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 DisTubeVoice#audioResource} has silence padding frames.
*/
stop(force = false) {
this.audioPlayer.stop(force);
this.stream?.kill?.();
}
/**
* Play a {@link DisTubeStream}
*
* @param dtStream - DisTubeStream
*/
play(dtStream) {
this.emittedError = false;
dtStream.on("error", (error) => {
if (this.emittedError || error.code === "ERR_STREAM_PREMATURE_CLOSE") return;
this.emittedError = true;
this.emit("error", error);
});
this.audioResource = (0, import_voice.createAudioResource)(dtStream.stream, {
inputType: dtStream.type,
inlineVolume: true
});
this.volume = __privateGet(this, _volume);
if (this.audioPlayer.state.status !== import_voice.AudioPlayerStatus.Paused) this.audioPlayer.play(this.audioResource);
this.stream?.kill?.();
this.stream = dtStream;
}
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);
}
__privateSet(this, _volume, volume);
this.audioResource?.volume?.setVolume(Math.pow(__privateGet(this, _volume) / 100, 0.5 / Math.log10(2)));
}
get volume() {
return __privateGet(this, _volume);
}
/**
* Playback duration of the audio resource in seconds
*/
get playbackDuration() {
return (this.audioResource?.playbackDuration ?? 0) / 1e3;
}
pause() {
this.audioPlayer.pause();
}
unpause() {
const state = this.audioPlayer.state;
if (state.status !== import_voice.AudioPlayerStatus.Paused) return;
if (this.audioResource && state.resource !== this.audioResource) this.audioPlayer.play(this.audioResource);
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;
}
};
_channel = new WeakMap();
_volume = new WeakMap();
_DisTubeVoice_instances = new WeakSet();
br_fn = /* @__PURE__ */ __name(function() {
if (this.audioResource?.encoder?.encoder) this.audioResource.encoder.setBitrate(this.channel.bitrate);
}, "#br");
join_fn = /* @__PURE__ */ __name(function(channel) {
return (0, import_voice.joinVoiceChannel)({
channelId: channel.id,
guildId: this.id,
adapterCreator: channel.guild.voiceAdapterCreator,
group: channel.client.user?.id
});
}, "#join");
__name(_DisTubeVoice, "DisTubeVoice");
var DisTubeVoice = _DisTubeVoice;
// src/core/DisTubeStream.ts
var import_node_stream = require("stream");
var import_child_process = require("child_process");
var import_tiny_typed_emitter2 = require("tiny-typed-emitter");
var import_voice2 = require("@discordjs/voice");
var chooseBestVideoFormat = /* @__PURE__ */ __name(({ duration, formats, isLive }) => formats && formats.filter((f) => f.hasAudio && (duration < 10 * 60 || f.hasVideo) && (!isLive || f.isHLS)).sort((a, b) => Number(b.audioBitrate) - Number(a.audioBitrate) || Number(a.bitrate) - Number(b.bitrate))[0], "chooseBestVideoFormat");
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_child_process.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}`);
if (result.includes("--enable-libopus")) {
debug("[test] ffmpeg supports libopus");
} else {
debug("[test] ffmpeg does not support libopus");
distube.options.streamType = 1 /* RAW */;
}
} 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 _DisTubeStream extends import_tiny_typed_emitter2.TypedEmitter {
/**
* Create a DisTubeStream to play with {@link DisTubeVoice}
*
* @param url - Stream URL
* @param options - Stream options
*/
constructor(url, { ffmpeg, seek, type }) {
super();
__publicField(this, "killed", false);
__publicField(this, "process");
__publicField(this, "stream");
__publicField(this, "type");
__publicField(this, "url");
this.url = url;
this.type = !type ? import_voice2.StreamType.OggOpus : import_voice2.StreamType.Raw;
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
};
if (!type) {
opts.f = "opus";
opts.acodec = "libopus";
} else {
opts.f = "s16le";
}
if (typeof seek === "number" && seek > 0) opts.ss = seek.toString();
this.process = (0, import_child_process.spawn)(
ffmpeg.path,
[
...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"
],
{ 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.stream = new import_node_stream.PassThrough();
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.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);
}
kill() {
if (this.killed) return;
this.process.kill("SIGKILL");
this.killed = true;
}
/**
* Create a stream from a YouTube {@link Song}
*
* @param song - A YouTube Song
* @param options - options
*/
static YouTube(song, options) {
if (song.source !== "youtube") throw new DisTubeError("INVALID_TYPE", "youtube", song.source, "Song#source");
if (!song.formats?.length) throw new DisTubeError("UNAVAILABLE_VIDEO");
if (!options || typeof options !== "object" || Array.isArray(options)) {
throw new DisTubeError("INVALID_TYPE", "object", options, "options");
}
const bestFormat = chooseBestVideoFormat(song);
if (!bestFormat) throw new DisTubeError("UNPLAYABLE_FORMATS");
return new _DisTubeStream(bestFormat.url, options);
}
/**
* Create a stream from a stream url
*
* @param url - stream url
* @param options - options
*/
static DirectLink(url, options) {
if (typeof url !== "string" || !isURL(url)) {
throw new DisTubeError("INVALID_TYPE", "an URL", url);
}
if (!options || typeof options !== "object" || Array.isArray(options)) {
throw new DisTubeError("INVALID_TYPE", "object", options, "options");
}
return new _DisTubeStream(url, options);
}
};
__name(_DisTubeStream, "DisTubeStream");
var DisTubeStream = _DisTubeStream;
// src/core/DisTubeHandler.ts
var import_ytpl = __toESM(require("@distube/ytpl"));
var import_ytdl_core = __toESM(require("@distube/ytdl-core"));
var import_tough_cookie = require("tough-cookie");
var _cookie;
var _DisTubeHandler = class _DisTubeHandler extends DisTubeBase {
constructor(distube) {
super(distube);
__privateAdd(this, _cookie, "");
const client = this.client;
if (this.options.leaveOnEmpty) {
client.on("voiceStateUpdate", (oldState) => {
if (!oldState?.channel) return;
const queue = this.queues.get(oldState);
if (!queue) {
if (isVoiceChannelEmpty(oldState)) {
setTimeout(() => {
if (!this.queues.get(oldState) && isVoiceChannelEmpty(oldState)) this.voices.leave(oldState);
}, this.options.emptyCooldown * 1e3).unref();
}
return;
}
if (queue._emptyTimeout) {
clearTimeout(queue._emptyTimeout);
delete queue._emptyTimeout;
}
if (isVoiceChannelEmpty(oldState)) {
queue._emptyTimeout = setTimeout(() => {
delete queue._emptyTimeout;
if (isVoiceChannelEmpty(oldState)) {
queue.voice.leave();
this.emit("empty" /* EMPTY */, queue);
if (queue.stopped) queue.remove();
}
}, this.options.emptyCooldown * 1e3).unref();
}
});
}
}
get ytdlOptions() {
const options = this.options.ytdlOptions;
if (this.options.youtubeCookie && this.options.youtubeCookie !== __privateGet(this, _cookie)) {
const cookies = __privateSet(this, _cookie, this.options.youtubeCookie);
if (typeof cookies === "string") {
console.warn(
"\x1B[33mWARNING:\x1B[0m You are using the old YouTube cookie format, please use the new one instead. (https://github.com/skick1234/DisTube/wiki/YouTube-Cookies)"
);
options.agent = import_ytdl_core.default.createAgent(
cookies.split(";").map((c) => import_tough_cookie.Cookie.parse(c)).filter(isTruthy)
);
} else {
options.agent = import_ytdl_core.default.createAgent(cookies);
}
}
return options;
}
get ytCookie() {
const agent = this.ytdlOptions.agent;
if (!agent) return "";
const { jar } = agent;
return jar.getCookieStringSync("https://www.youtube.com");
}
/**
* @param url - url
* @param basic - getBasicInfo?
*/
getYouTubeInfo(url, basic = false) {
if (basic) return import_ytdl_core.default.getBasicInfo(url, this.ytdlOptions);
return import_ytdl_core.default.getInfo(url, this.ytdlOptions);
}
/**
* Resolve a url or a supported object to a {@link Song} or {@link Playlist}
*
* @throws {@link DisTubeError}
*
* @param song - URL | {@link Song}| {@link SearchResult} | {@link Playlist}
* @param options - Optional options
*
* @returns Resolved
*/
async resolve(song, options = {}) {
if (song instanceof Song || song instanceof Playlist) {
if ("metadata" in options) song.metadata = options.metadata;
if ("member" in options) song.member = options.member;
return song;
}
if (song instanceof SearchResultVideo) return new Song(song, options);
if (song instanceof SearchResultPlaylist) return this.resolvePlaylist(song.url, options);
if (isObject(song)) {
if (!("url" in song) && !("id" in song)) throw new DisTubeError("CANNOT_RESOLVE_SONG", song);
return new Song(song, options);
}
if (import_ytpl.default.validateID(song)) return this.resolvePlaylist(song, options);
if (import_ytdl_core.default.validateURL(song)) return new Song(await this.getYouTubeInfo(song, true), options);
if (isURL(song)) {
for (const plugin of this.distube.extractorPlugins) {
if (await plugin.validate(song)) return plugin.resolve(song, options);
}
throw new DisTubeError("NOT_SUPPORTED_URL");
}
throw new DisTubeError("CANNOT_RESOLVE_SONG", song);
}
/**
* Resolve Song[] or YouTube playlist url to a Playlist
*
* @param playlist - Resolvable playlist
* @param options - Optional options
*/
async resolvePlaylist(playlist, options = {}) {
const { member, source, metadata } = { source: "youtube", ...options };
if (playlist instanceof Playlist) {
if ("metadata" in options) playlist.metadata = metadata;
if ("member" in options) playlist.member = member;
return playlist;
}
if (typeof playlist === "string") {
const info = await (0, import_ytpl.default)(playlist, { limit: Infinity, requestOptions: { headers: { cookie: this.ytCookie } } });
const songs = info.items.filter((v) => !v.thumbnail.includes("no_thumbnail")).map((v) => new Song(v, { member, metadata }));
return new Playlist(
{
source,
songs,
member,
name: info.title,
url: info.url,
thumbnail: songs[0].thumbnail
},
{ metadata }
);
}
return new Playlist(playlist, { member, properties: { source }, metadata });
}
/**
* Search for a song, fire {@link DisTube#error} if not found.
*
* @throws {@link DisTubeError}
*
* @param message - The original message from an user
* @param query - The query string
*
* @returns Song info
*/
async searchSong(message, query) {
if (!isMessageInstance(message)) throw new DisTubeError("INVALID_TYPE", "Discord.Message", message, "message");
if (typeof query !== "string") throw new DisTubeError("INVALID_TYPE", "string", query, "query");
if (query.length === 0) throw new DisTubeError("EMPTY_STRING", "query");
const limit = this.options.searchSongs > 1 ? this.options.searchSongs : 1;
const results = await this.distube.search(query, {
limit,
safeSearch: this.options.nsfw ? false : !isNsfwChannel(message.channel)
}).catch(() => {
if (!this.emit("searchNoResult" /* SEARCH_NO_RESULT */, message, query)) {
console.warn("searchNoResult event does not have any listeners! Emits `error` event instead.");
throw new DisTubeError("NO_RESULT");
}
});
if (!results) return null;
return this.createSearchMessageCollector(message, results, query);
}
/**
* Create a message collector for selecting search results.
*
* Needed events: {@link DisTube#searchResult}, {@link DisTube#searchCancel},
* {@link DisTube#searchInvalidAnswer}, {@link DisTube#searchDone}.
*
* @throws {@link DisTubeError}
*
* @param message - The original message from an user
* @param results - The search results
* @param query - The query string
*
* @returns Selected result
*/
async createSearchMessageCollector(message,