UNPKG

@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
"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,