@kattowo/distube 0.0.0-stage → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +129 -2
- package/dist/index.d.mts +3260 -0
- package/dist/index.d.ts +3262 -0
- package/dist/index.js +4028 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +3941 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +110 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,4028 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
13
|
+
var __copyProps = (to, from, except, desc) => {
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (let key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
17
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
18
|
+
}
|
|
19
|
+
return to;
|
|
20
|
+
};
|
|
21
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
22
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
23
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
24
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
25
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
26
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
27
|
+
mod
|
|
28
|
+
));
|
|
29
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
30
|
+
|
|
31
|
+
// src/index.ts
|
|
32
|
+
var index_exports = {};
|
|
33
|
+
__export(index_exports, {
|
|
34
|
+
AUDIO_CHANNELS: () => AUDIO_CHANNELS,
|
|
35
|
+
AUDIO_SAMPLE_RATE: () => AUDIO_SAMPLE_RATE,
|
|
36
|
+
BaseManager: () => BaseManager,
|
|
37
|
+
DEFAULT_VOLUME: () => DEFAULT_VOLUME,
|
|
38
|
+
DisTube: () => DisTube,
|
|
39
|
+
DisTubeBase: () => DisTubeBase,
|
|
40
|
+
DisTubeError: () => DisTubeError,
|
|
41
|
+
DisTubeHandler: () => DisTubeHandler,
|
|
42
|
+
DisTubeStream: () => DisTubeStream,
|
|
43
|
+
DisTubeVoice: () => DisTubeVoice,
|
|
44
|
+
DisTubeVoiceManager: () => DisTubeVoiceManager,
|
|
45
|
+
Events: () => Events,
|
|
46
|
+
ExtractorPlugin: () => ExtractorPlugin,
|
|
47
|
+
FilterManager: () => FilterManager,
|
|
48
|
+
GuildIdManager: () => GuildIdManager,
|
|
49
|
+
HTTP_REDIRECT_CODES: () => HTTP_REDIRECT_CODES,
|
|
50
|
+
InfoExtractorPlugin: () => InfoExtractorPlugin,
|
|
51
|
+
JOIN_TIMEOUT_MS: () => JOIN_TIMEOUT_MS,
|
|
52
|
+
MAX_REDIRECT_DEPTH: () => MAX_REDIRECT_DEPTH,
|
|
53
|
+
Options: () => Options,
|
|
54
|
+
PlayableExtractorPlugin: () => PlayableExtractorPlugin,
|
|
55
|
+
Playlist: () => Playlist,
|
|
56
|
+
Plugin: () => Plugin,
|
|
57
|
+
PluginError: () => PluginError,
|
|
58
|
+
PluginErrorCode: () => PluginErrorCode,
|
|
59
|
+
PluginType: () => PluginType,
|
|
60
|
+
Queue: () => Queue,
|
|
61
|
+
QueueManager: () => QueueManager,
|
|
62
|
+
RECONNECT_MAX_ATTEMPTS: () => RECONNECT_MAX_ATTEMPTS,
|
|
63
|
+
RECONNECT_TIMEOUT_MS: () => RECONNECT_TIMEOUT_MS,
|
|
64
|
+
RepeatMode: () => RepeatMode,
|
|
65
|
+
STUCK_THRESHOLD_MS: () => STUCK_THRESHOLD_MS,
|
|
66
|
+
SearchResult: () => SearchResult,
|
|
67
|
+
SearchResultType: () => SearchResultType,
|
|
68
|
+
Song: () => Song,
|
|
69
|
+
StreamServer: () => StreamServer,
|
|
70
|
+
TaskQueue: () => TaskQueue,
|
|
71
|
+
checkEncryptionLibraries: () => checkEncryptionLibraries,
|
|
72
|
+
checkFFmpeg: () => checkFFmpeg,
|
|
73
|
+
checkIntents: () => checkIntents,
|
|
74
|
+
checkInvalidKey: () => checkInvalidKey,
|
|
75
|
+
default: () => DisTube,
|
|
76
|
+
defaultFilters: () => defaultFilters,
|
|
77
|
+
defaultOptions: () => defaultOptions,
|
|
78
|
+
formatDuration: () => formatDuration,
|
|
79
|
+
handleUnhandledError: () => handleUnhandledError,
|
|
80
|
+
isClientInstance: () => isClientInstance,
|
|
81
|
+
isGuildInstance: () => isGuildInstance,
|
|
82
|
+
isMemberInstance: () => isMemberInstance,
|
|
83
|
+
isMessageInstance: () => isMessageInstance,
|
|
84
|
+
isNsfwChannel: () => isNsfwChannel,
|
|
85
|
+
isObject: () => isObject,
|
|
86
|
+
isSnowflake: () => isSnowflake,
|
|
87
|
+
isSupportedVoiceChannel: () => isSupportedVoiceChannel,
|
|
88
|
+
isTextChannelInstance: () => isTextChannelInstance,
|
|
89
|
+
isTruthy: () => isTruthy,
|
|
90
|
+
isURL: () => isURL,
|
|
91
|
+
isVoiceChannelEmpty: () => isVoiceChannelEmpty,
|
|
92
|
+
objectKeys: () => objectKeys,
|
|
93
|
+
resolveGuildId: () => resolveGuildId,
|
|
94
|
+
version: () => version
|
|
95
|
+
});
|
|
96
|
+
module.exports = __toCommonJS(index_exports);
|
|
97
|
+
|
|
98
|
+
// src/constant.ts
|
|
99
|
+
var version = "6.0.0";
|
|
100
|
+
var AUDIO_SAMPLE_RATE = 48e3;
|
|
101
|
+
var AUDIO_CHANNELS = 2;
|
|
102
|
+
var DEFAULT_VOLUME = 50;
|
|
103
|
+
var JOIN_TIMEOUT_MS = 3e4;
|
|
104
|
+
var RECONNECT_TIMEOUT_MS = 5e3;
|
|
105
|
+
var RECONNECT_MAX_ATTEMPTS = 5;
|
|
106
|
+
var STUCK_THRESHOLD_MS = 1e4;
|
|
107
|
+
var HTTP_REDIRECT_CODES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
108
|
+
var MAX_REDIRECT_DEPTH = 5;
|
|
109
|
+
var defaultFilters = {
|
|
110
|
+
"3d": "apulsator=hz=0.125",
|
|
111
|
+
bassboost: "bass=g=10",
|
|
112
|
+
echo: "aecho=0.8:0.9:1000:0.3",
|
|
113
|
+
flanger: "flanger",
|
|
114
|
+
gate: "agate",
|
|
115
|
+
haas: "haas",
|
|
116
|
+
karaoke: "stereotools=mlev=0.1",
|
|
117
|
+
nightcore: "asetrate=48000*1.25,aresample=48000,bass=g=5",
|
|
118
|
+
reverse: "areverse",
|
|
119
|
+
vaporwave: "asetrate=48000*0.8,aresample=48000,atempo=1.1",
|
|
120
|
+
mcompand: "mcompand",
|
|
121
|
+
phaser: "aphaser",
|
|
122
|
+
tremolo: "tremolo",
|
|
123
|
+
surround: "surround",
|
|
124
|
+
earwax: "earwax"
|
|
125
|
+
};
|
|
126
|
+
var defaultOptions = {
|
|
127
|
+
plugins: [],
|
|
128
|
+
emitNewSongOnly: false,
|
|
129
|
+
savePreviousSongs: true,
|
|
130
|
+
nsfw: false,
|
|
131
|
+
emitAddSongWhenCreatingQueue: true,
|
|
132
|
+
emitAddListWhenCreatingQueue: true,
|
|
133
|
+
joinNewVoiceChannel: true,
|
|
134
|
+
unhandledError: "log",
|
|
135
|
+
stuckThreshold: STUCK_THRESHOLD_MS
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// src/core/DisTubeBase.ts
|
|
139
|
+
var DisTubeBase = class {
|
|
140
|
+
static {
|
|
141
|
+
__name(this, "DisTubeBase");
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* The DisTube instance this object belongs to.
|
|
145
|
+
*/
|
|
146
|
+
distube;
|
|
147
|
+
/**
|
|
148
|
+
* Create a new base object.
|
|
149
|
+
* @param distube - The DisTube instance
|
|
150
|
+
*/
|
|
151
|
+
constructor(distube) {
|
|
152
|
+
this.distube = distube;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Emit the {@link DisTube} of this base
|
|
156
|
+
* @param eventName - Event name
|
|
157
|
+
* @param args - arguments
|
|
158
|
+
* @returns `true` when the event had listeners, `false` otherwise
|
|
159
|
+
*/
|
|
160
|
+
emit(eventName, ...args) {
|
|
161
|
+
return this.distube.emit(eventName, ...args);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Emit error event
|
|
165
|
+
* @param error - error
|
|
166
|
+
* @param queue - The queue encountered the error
|
|
167
|
+
* @param song - The playing song when encountered the error
|
|
168
|
+
* @returns Nothing
|
|
169
|
+
*/
|
|
170
|
+
emitError(error, queue, song) {
|
|
171
|
+
this.distube.emitError(error, queue, song);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Emit debug event
|
|
175
|
+
* @param message - debug message
|
|
176
|
+
* @returns Nothing
|
|
177
|
+
*/
|
|
178
|
+
debug(message) {
|
|
179
|
+
this.distube.debug(message);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* The queue manager
|
|
183
|
+
*/
|
|
184
|
+
get queues() {
|
|
185
|
+
return this.distube.queues;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* The voice manager
|
|
189
|
+
*/
|
|
190
|
+
get voices() {
|
|
191
|
+
return this.distube.voices;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Discord.js client
|
|
195
|
+
*/
|
|
196
|
+
get client() {
|
|
197
|
+
return this.distube.client;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* DisTube options
|
|
201
|
+
*/
|
|
202
|
+
get options() {
|
|
203
|
+
return this.distube.options;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* DisTube handler
|
|
207
|
+
*/
|
|
208
|
+
get handler() {
|
|
209
|
+
return this.distube.handler;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* DisTube plugins
|
|
213
|
+
*/
|
|
214
|
+
get plugins() {
|
|
215
|
+
return this.distube.plugins;
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// src/core/DisTubeHandler.ts
|
|
220
|
+
var import_undici = require("undici");
|
|
221
|
+
|
|
222
|
+
// src/struct/DisTubeError.ts
|
|
223
|
+
var import_node_util = require("util");
|
|
224
|
+
var ERROR_MESSAGES = {
|
|
225
|
+
/**
|
|
226
|
+
* A value does not have the expected type.
|
|
227
|
+
*
|
|
228
|
+
* It is raised when an argument of a public method is not of the type that
|
|
229
|
+
* the method expects, e.g. when a voice channel is not a voice channel.
|
|
230
|
+
*/
|
|
231
|
+
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"),
|
|
232
|
+
/**
|
|
233
|
+
* A number is not in the expected range.
|
|
234
|
+
*
|
|
235
|
+
* It is raised when a numeric argument is out of the range that the method
|
|
236
|
+
* accepts, e.g. a negative volume.
|
|
237
|
+
*/
|
|
238
|
+
NUMBER_COMPARE: /* @__PURE__ */ __name((name, expected, value) => `'${name}' must be ${expected} ${value}`, "NUMBER_COMPARE"),
|
|
239
|
+
/**
|
|
240
|
+
* An array is empty.
|
|
241
|
+
*
|
|
242
|
+
* It is raised when an argument that must hold at least one element is
|
|
243
|
+
* empty, e.g. the songs of a custom playlist.
|
|
244
|
+
*/
|
|
245
|
+
EMPTY_ARRAY: /* @__PURE__ */ __name((name) => `'${name}' is an empty array`, "EMPTY_ARRAY"),
|
|
246
|
+
/**
|
|
247
|
+
* An array does not have any valid element.
|
|
248
|
+
*
|
|
249
|
+
* It is raised when every element of an array was filtered out, e.g. when
|
|
250
|
+
* all the songs of an array are invalid.
|
|
251
|
+
*/
|
|
252
|
+
EMPTY_FILTERED_ARRAY: /* @__PURE__ */ __name((name, type) => `There is no valid '${type}' in the '${name}' array`, "EMPTY_FILTERED_ARRAY"),
|
|
253
|
+
/**
|
|
254
|
+
* A string is empty.
|
|
255
|
+
*
|
|
256
|
+
* It is raised when a string argument that must not be empty is empty.
|
|
257
|
+
*/
|
|
258
|
+
EMPTY_STRING: /* @__PURE__ */ __name((name) => `'${name}' string must not be empty`, "EMPTY_STRING"),
|
|
259
|
+
/**
|
|
260
|
+
* An object has a key that is not allowed.
|
|
261
|
+
*
|
|
262
|
+
* It is raised when an options object has a key that DisTube does not know,
|
|
263
|
+
* e.g. `{ emitNewSong: true }` instead of `{ emitNewSongOnly: true }`.
|
|
264
|
+
*/
|
|
265
|
+
INVALID_KEY: /* @__PURE__ */ __name((obj, key) => `'${key}' does not need to be provided in ${obj}`, "INVALID_KEY"),
|
|
266
|
+
/**
|
|
267
|
+
* A required key is missing.
|
|
268
|
+
*
|
|
269
|
+
* It is raised when an object does not have a key that DisTube needs.
|
|
270
|
+
*/
|
|
271
|
+
MISSING_KEY: /* @__PURE__ */ __name((obj, key) => `'${key}' needs to be provided in ${obj}`, "MISSING_KEY"),
|
|
272
|
+
/**
|
|
273
|
+
* Several required keys are missing.
|
|
274
|
+
*
|
|
275
|
+
* It is raised when an object does not have the keys that DisTube needs.
|
|
276
|
+
*/
|
|
277
|
+
MISSING_KEYS: /* @__PURE__ */ __name((obj, key, all) => `${key.map((k) => `'${k}'`).join(all ? " and " : " or ")} need to be provided in ${obj}`, "MISSING_KEYS"),
|
|
278
|
+
/**
|
|
279
|
+
* The Discord.js client does not have the required intents.
|
|
280
|
+
*
|
|
281
|
+
* It is raised when the `GuildVoiceStates` intent is missing: DisTube
|
|
282
|
+
* cannot work without it.
|
|
283
|
+
*/
|
|
284
|
+
MISSING_INTENTS: /* @__PURE__ */ __name((i) => `${i} intent must be provided for the Client`, "MISSING_INTENTS"),
|
|
285
|
+
/**
|
|
286
|
+
* A DisTube option that is needed is disabled.
|
|
287
|
+
*
|
|
288
|
+
* It is raised when a feature cannot work because one of its options is
|
|
289
|
+
* disabled, e.g. {@link Queue#previous} when
|
|
290
|
+
* {@link DisTubeOptions.savePreviousSongs} is `false`.
|
|
291
|
+
*/
|
|
292
|
+
DISABLED_OPTION: /* @__PURE__ */ __name((o) => `DisTubeOptions.${o} is disabled`, "DISABLED_OPTION"),
|
|
293
|
+
/**
|
|
294
|
+
* A DisTube option that must be disabled is enabled.
|
|
295
|
+
*
|
|
296
|
+
* It is raised when a feature cannot work because one of its options is
|
|
297
|
+
* enabled.
|
|
298
|
+
*/
|
|
299
|
+
ENABLED_OPTION: /* @__PURE__ */ __name((o) => `DisTubeOptions.${o} is enabled`, "ENABLED_OPTION"),
|
|
300
|
+
/**
|
|
301
|
+
* The user is not in any voice channel.
|
|
302
|
+
*
|
|
303
|
+
* It is raised when a command needs the voice channel of the user, but the
|
|
304
|
+
* user is not connected to any voice channel.
|
|
305
|
+
*/
|
|
306
|
+
NOT_IN_VOICE: "User is not in any voice channel",
|
|
307
|
+
/**
|
|
308
|
+
* The voice channel is full.
|
|
309
|
+
*
|
|
310
|
+
* It is raised when the bot cannot join a voice channel because it is
|
|
311
|
+
* full.
|
|
312
|
+
*/
|
|
313
|
+
VOICE_FULL: "The voice channel is full",
|
|
314
|
+
/**
|
|
315
|
+
* The guild already has a voice connection that DisTube does not manage.
|
|
316
|
+
*
|
|
317
|
+
* It is raised when DisTube cannot create a voice connection because
|
|
318
|
+
* another one already exists in the guild.
|
|
319
|
+
*/
|
|
320
|
+
VOICE_ALREADY_CREATED: "This guild already has a voice connection which is not managed by DisTube",
|
|
321
|
+
/**
|
|
322
|
+
* The voice connection could not be established in time.
|
|
323
|
+
*
|
|
324
|
+
* It is raised when the connection did not become ready before
|
|
325
|
+
* {@link JOIN_TIMEOUT_MS}.
|
|
326
|
+
*/
|
|
327
|
+
VOICE_CONNECT_FAILED: /* @__PURE__ */ __name((s) => `Cannot connect to the voice channel after ${s} seconds`, "VOICE_CONNECT_FAILED"),
|
|
328
|
+
/**
|
|
329
|
+
* The bot does not have the permission to join the voice channel.
|
|
330
|
+
*
|
|
331
|
+
* It is raised when the bot cannot join a voice channel because of a missing
|
|
332
|
+
* permission.
|
|
333
|
+
*/
|
|
334
|
+
VOICE_MISSING_PERMS: "I do not have permission to join this voice channel",
|
|
335
|
+
/**
|
|
336
|
+
* The voice connection could not be restored.
|
|
337
|
+
*
|
|
338
|
+
* It is raised when every reconnection attempt of
|
|
339
|
+
* {@link RECONNECT_MAX_ATTEMPTS} failed.
|
|
340
|
+
*/
|
|
341
|
+
VOICE_RECONNECT_FAILED: "Cannot reconnect to the voice channel",
|
|
342
|
+
/**
|
|
343
|
+
* The voice channel belongs to another guild.
|
|
344
|
+
*
|
|
345
|
+
* It is raised when a voice connection is asked to join a voice channel of
|
|
346
|
+
* another guild.
|
|
347
|
+
*/
|
|
348
|
+
VOICE_DIFFERENT_GUILD: "Cannot join a voice channel in a different guild",
|
|
349
|
+
/**
|
|
350
|
+
* The voice channel belongs to another client.
|
|
351
|
+
*
|
|
352
|
+
* It is raised when a voice connection is asked to join a voice channel
|
|
353
|
+
* that was created by another Discord.js client.
|
|
354
|
+
*/
|
|
355
|
+
VOICE_DIFFERENT_CLIENT: "Cannot join a voice channel created by a different client",
|
|
356
|
+
/**
|
|
357
|
+
* The FFmpeg process exited with an unexpected code.
|
|
358
|
+
*
|
|
359
|
+
* It is raised when the FFmpeg process that decodes the audio exits with a
|
|
360
|
+
* code that is neither `0` nor `255`.
|
|
361
|
+
*/
|
|
362
|
+
FFMPEG_EXITED: /* @__PURE__ */ __name((code) => `ffmpeg exited with code ${code}`, "FFMPEG_EXITED"),
|
|
363
|
+
/**
|
|
364
|
+
* FFmpeg is not installed at the given path.
|
|
365
|
+
*
|
|
366
|
+
* It is raised when the FFmpeg executable cannot be spawned, e.g. when
|
|
367
|
+
* {@link DisTubeOptions.ffmpeg} points to a path that does not exist.
|
|
368
|
+
*/
|
|
369
|
+
FFMPEG_NOT_INSTALLED: /* @__PURE__ */ __name((path) => `ffmpeg is not installed at '${path}' path`, "FFMPEG_NOT_INSTALLED"),
|
|
370
|
+
/**
|
|
371
|
+
* No encryption library is available.
|
|
372
|
+
*
|
|
373
|
+
* It is raised when Discord cannot decrypt the audio: neither the
|
|
374
|
+
* `aes-256-gcm` cipher of Node.js nor one of the supported third-party
|
|
375
|
+
* encryption libraries is available.
|
|
376
|
+
*/
|
|
377
|
+
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.",
|
|
378
|
+
/**
|
|
379
|
+
* The guild does not have a playing queue.
|
|
380
|
+
*
|
|
381
|
+
* It is raised when a queue operation is used in a guild that has no
|
|
382
|
+
* {@link Queue}.
|
|
383
|
+
*/
|
|
384
|
+
NO_QUEUE: "There is no playing queue in this guild",
|
|
385
|
+
/**
|
|
386
|
+
* The guild already has a queue.
|
|
387
|
+
*
|
|
388
|
+
* It is raised when a {@link Queue} is created for a guild that already
|
|
389
|
+
* has one.
|
|
390
|
+
*/
|
|
391
|
+
QUEUE_EXIST: "This guild has a Queue already",
|
|
392
|
+
/**
|
|
393
|
+
* The queue has been stopped already.
|
|
394
|
+
*
|
|
395
|
+
* It is raised when an operation needs a running queue, but the queue was
|
|
396
|
+
* stopped.
|
|
397
|
+
*/
|
|
398
|
+
QUEUE_STOPPED: "The queue has been stopped already",
|
|
399
|
+
/**
|
|
400
|
+
* The queue is already paused.
|
|
401
|
+
*
|
|
402
|
+
* It is raised when {@link Queue#pause} is used on a paused queue.
|
|
403
|
+
*/
|
|
404
|
+
PAUSED: "The queue has been paused already",
|
|
405
|
+
/**
|
|
406
|
+
* The queue is already playing.
|
|
407
|
+
*
|
|
408
|
+
* It is raised when {@link Queue#resume} is used on a queue that is not
|
|
409
|
+
* paused.
|
|
410
|
+
*/
|
|
411
|
+
RESUMED: "The queue has been playing already",
|
|
412
|
+
/**
|
|
413
|
+
* The queue does not have any previous song.
|
|
414
|
+
*
|
|
415
|
+
* It is raised when {@link Queue#previous} is used on a queue without any
|
|
416
|
+
* history.
|
|
417
|
+
*/
|
|
418
|
+
NO_PREVIOUS: "There is no previous song in this queue",
|
|
419
|
+
/**
|
|
420
|
+
* The queue does not have any up next song.
|
|
421
|
+
*
|
|
422
|
+
* It is raised when a jump goes past the end of the queue while the
|
|
423
|
+
* autoplay mode is disabled.
|
|
424
|
+
*/
|
|
425
|
+
NO_UP_NEXT: "There is no up next song",
|
|
426
|
+
/**
|
|
427
|
+
* The queue does not have any song at the given position.
|
|
428
|
+
*
|
|
429
|
+
* It is raised when {@link Queue#jump} is used with a position that is out
|
|
430
|
+
* of the bounds of the queue.
|
|
431
|
+
*/
|
|
432
|
+
NO_SONG_POSITION: "Does not have any song at this position",
|
|
433
|
+
/**
|
|
434
|
+
* The queue does not have any playing song.
|
|
435
|
+
*
|
|
436
|
+
* It is raised when a related song is requested but the queue is empty.
|
|
437
|
+
*/
|
|
438
|
+
NO_PLAYING_SONG: "There is no playing song in the queue",
|
|
439
|
+
/**
|
|
440
|
+
* The track is stuck and does not produce any audio.
|
|
441
|
+
*
|
|
442
|
+
* It is raised when a track stays in the "buffering" state for longer than
|
|
443
|
+
* {@link DisTubeOptions.stuckThreshold} milliseconds. The queue skips it.
|
|
444
|
+
*/
|
|
445
|
+
TRACK_STUCK: "The track is stuck and not producing any audio",
|
|
446
|
+
/**
|
|
447
|
+
* There is no extractor plugin.
|
|
448
|
+
*
|
|
449
|
+
* It is raised when a search is requested but no {@link ExtractorPlugin} is
|
|
450
|
+
* registered in {@link DisTubeOptions.plugins}.
|
|
451
|
+
*/
|
|
452
|
+
NO_EXTRACTOR_PLUGIN: "There is no extractor plugin in the DisTubeOptions.plugins, please add one for searching songs",
|
|
453
|
+
/**
|
|
454
|
+
* No related song could be found.
|
|
455
|
+
*
|
|
456
|
+
* It is raised when the autoplay mode is enabled and no plugin could find
|
|
457
|
+
* a related song for the current one.
|
|
458
|
+
*/
|
|
459
|
+
NO_RELATED: "Cannot find any related songs",
|
|
460
|
+
/**
|
|
461
|
+
* The related song cannot be played.
|
|
462
|
+
*
|
|
463
|
+
* It is raised when the related song of the autoplay mode cannot be
|
|
464
|
+
* played.
|
|
465
|
+
*/
|
|
466
|
+
CANNOT_PLAY_RELATED: "Cannot play the related song",
|
|
467
|
+
/**
|
|
468
|
+
* The video is unavailable.
|
|
469
|
+
*
|
|
470
|
+
* It is raised when the source of the song does not have the video anymore,
|
|
471
|
+
* e.g. when it was removed or when it is private.
|
|
472
|
+
*/
|
|
473
|
+
UNAVAILABLE_VIDEO: "This video is unavailable",
|
|
474
|
+
/**
|
|
475
|
+
* No playable format was found.
|
|
476
|
+
*
|
|
477
|
+
* It is raised when the source of the song does not have any format that
|
|
478
|
+
* FFmpeg can play.
|
|
479
|
+
*/
|
|
480
|
+
UNPLAYABLE_FORMATS: "No playable format found",
|
|
481
|
+
/**
|
|
482
|
+
* The content is age-restricted and cannot be played in this channel.
|
|
483
|
+
*
|
|
484
|
+
* It is raised when an age-restricted song is played in a non-NSFW channel
|
|
485
|
+
* while {@link DisTubeOptions.nsfw} is `false`.
|
|
486
|
+
*/
|
|
487
|
+
NON_NSFW: "Cannot play age-restricted content in non-NSFW channel",
|
|
488
|
+
/**
|
|
489
|
+
* The url is not supported.
|
|
490
|
+
*
|
|
491
|
+
* It is raised when no plugin of {@link DisTubeOptions.plugins} can handle
|
|
492
|
+
* the url.
|
|
493
|
+
*/
|
|
494
|
+
NOT_SUPPORTED_URL: "This url is not supported",
|
|
495
|
+
/**
|
|
496
|
+
* No plugin supports the song.
|
|
497
|
+
*
|
|
498
|
+
* It is raised when no plugin of {@link DisTubeOptions.plugins} can play
|
|
499
|
+
* the song.
|
|
500
|
+
*/
|
|
501
|
+
NOT_SUPPORTED_SONG: /* @__PURE__ */ __name((song) => `There is no plugin supporting this song (${song})`, "NOT_SUPPORTED_SONG"),
|
|
502
|
+
/**
|
|
503
|
+
* The array does not have any valid song.
|
|
504
|
+
*
|
|
505
|
+
* It is raised when every element of the `songs` array of
|
|
506
|
+
* {@link DisTube#createCustomPlaylist} is neither a {@link Song} nor a
|
|
507
|
+
* valid url.
|
|
508
|
+
*/
|
|
509
|
+
NO_VALID_SONG: "'songs' array does not have any valid Song or url",
|
|
510
|
+
/**
|
|
511
|
+
* The input cannot be resolved to a song or a playlist.
|
|
512
|
+
*
|
|
513
|
+
* It is raised when the input of {@link DisTubeHandler#resolve} is neither
|
|
514
|
+
* a url, nor a {@link Song}, nor a {@link Playlist}.
|
|
515
|
+
*/
|
|
516
|
+
CANNOT_RESOLVE_SONG: /* @__PURE__ */ __name((t) => `Cannot resolve ${(0, import_node_util.inspect)(t)} to a Song`, "CANNOT_RESOLVE_SONG"),
|
|
517
|
+
/**
|
|
518
|
+
* The stream url of the song cannot be found.
|
|
519
|
+
*
|
|
520
|
+
* It is raised when the plugin of the song does not return a stream url.
|
|
521
|
+
*/
|
|
522
|
+
CANNOT_GET_STREAM_URL: /* @__PURE__ */ __name((song) => `Cannot get stream url with this song (${song})`, "CANNOT_GET_STREAM_URL"),
|
|
523
|
+
/**
|
|
524
|
+
* The search query of the song cannot be built.
|
|
525
|
+
*
|
|
526
|
+
* It is raised when the plugin of the song cannot build a query to look for
|
|
527
|
+
* a playable version of it.
|
|
528
|
+
*/
|
|
529
|
+
CANNOT_GET_SEARCH_QUERY: /* @__PURE__ */ __name((song) => `Cannot get search query with this song (${song})`, "CANNOT_GET_SEARCH_QUERY"),
|
|
530
|
+
/**
|
|
531
|
+
* No song was found for the query.
|
|
532
|
+
*
|
|
533
|
+
* It is raised when no plugin of {@link DisTubeOptions.plugins} has any
|
|
534
|
+
* result for the search query.
|
|
535
|
+
*/
|
|
536
|
+
NO_RESULT: /* @__PURE__ */ __name((query) => `Cannot find any song with this query (${query})`, "NO_RESULT"),
|
|
537
|
+
/**
|
|
538
|
+
* The song does not have any stream url.
|
|
539
|
+
*
|
|
540
|
+
* It is raised when a song is played while it does not have a stream url
|
|
541
|
+
* attached to it.
|
|
542
|
+
*/
|
|
543
|
+
NO_STREAM_URL: /* @__PURE__ */ __name((song) => `No stream url attached (${song})`, "NO_STREAM_URL"),
|
|
544
|
+
/**
|
|
545
|
+
* Every video of the playlist was filtered out.
|
|
546
|
+
*
|
|
547
|
+
* It is raised when a playlist does not have any valid video left, e.g.
|
|
548
|
+
* because all of them were age-restricted and the channel is not NSFW.
|
|
549
|
+
*/
|
|
550
|
+
EMPTY_FILTERED_PLAYLIST: "There is no valid video in the playlist\nMaybe age-restricted contents is filtered because you are in non-NSFW channel",
|
|
551
|
+
/**
|
|
552
|
+
* The playlist does not have any valid video.
|
|
553
|
+
*
|
|
554
|
+
* It is raised when a playlist is empty.
|
|
555
|
+
*/
|
|
556
|
+
EMPTY_PLAYLIST: "There is no valid video in the playlist"
|
|
557
|
+
};
|
|
558
|
+
var haveCode = /* @__PURE__ */ __name((code) => Object.keys(ERROR_MESSAGES).includes(code), "haveCode");
|
|
559
|
+
var parseMessage = /* @__PURE__ */ __name((m, ...args) => typeof m === "string" ? m : m(...args), "parseMessage");
|
|
560
|
+
var getErrorMessage = /* @__PURE__ */ __name((code, ...args) => {
|
|
561
|
+
if (!haveCode(code)) return args[0];
|
|
562
|
+
try {
|
|
563
|
+
return parseMessage(ERROR_MESSAGES[code], ...args);
|
|
564
|
+
} catch {
|
|
565
|
+
return args[0] ?? code;
|
|
566
|
+
}
|
|
567
|
+
}, "getErrorMessage");
|
|
568
|
+
var DisTubeError = class _DisTubeError extends Error {
|
|
569
|
+
static {
|
|
570
|
+
__name(this, "DisTubeError");
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* The code of the error, e.g. `NO_RESULT`.
|
|
574
|
+
*/
|
|
575
|
+
errorCode;
|
|
576
|
+
/**
|
|
577
|
+
* Create a DisTube error.
|
|
578
|
+
* @param code - The code of the error
|
|
579
|
+
* @param args - The arguments of the message template of the code
|
|
580
|
+
*/
|
|
581
|
+
constructor(code, ...args) {
|
|
582
|
+
super(getErrorMessage(code, ...args));
|
|
583
|
+
this.errorCode = code;
|
|
584
|
+
if (Error.captureStackTrace) Error.captureStackTrace(this, _DisTubeError);
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Create a {@link DisTubeError} with a custom message.
|
|
588
|
+
*
|
|
589
|
+
* The default message of an error code is a generic template. Use this factory
|
|
590
|
+
* when you need to tell the user what exactly went wrong, or when you want to
|
|
591
|
+
* keep the original error as the `cause` of the error.
|
|
592
|
+
*
|
|
593
|
+
* It is polymorphic: calling it on a subclass creates an instance of that
|
|
594
|
+
* subclass.
|
|
595
|
+
*
|
|
596
|
+
* @param code - The error code
|
|
597
|
+
* @param message - The custom message
|
|
598
|
+
* @param options - Optional options
|
|
599
|
+
* @param options.cause - The original error that caused this one
|
|
600
|
+
* @returns The created error
|
|
601
|
+
*/
|
|
602
|
+
static withMessage(code, message, options) {
|
|
603
|
+
const error = new this(code, message);
|
|
604
|
+
error.message = message;
|
|
605
|
+
if (options && "cause" in options) error.cause = options.cause;
|
|
606
|
+
return error;
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* The name of the error, with its code.
|
|
610
|
+
* @returns The name of the error, e.g. `DisTubeError [NO_RESULT]`
|
|
611
|
+
*/
|
|
612
|
+
get name() {
|
|
613
|
+
return `DisTubeError [${this.errorCode}]`;
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* The code of the error, e.g. `NO_RESULT`.
|
|
617
|
+
*
|
|
618
|
+
* It is an alias of {@link DisTubeError.errorCode}.
|
|
619
|
+
* @returns The code of the error
|
|
620
|
+
*/
|
|
621
|
+
get code() {
|
|
622
|
+
return this.errorCode;
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
// src/util.ts
|
|
627
|
+
var import_node_url = require("url");
|
|
628
|
+
var import_discord3 = require("discord.js");
|
|
629
|
+
|
|
630
|
+
// src/core/DisTubeVoice.ts
|
|
631
|
+
var import_voice = require("@discordjs/voice");
|
|
632
|
+
var import_discord = require("discord.js");
|
|
633
|
+
var import_tiny_typed_emitter = require("tiny-typed-emitter");
|
|
634
|
+
var DisTubeVoice = class extends import_tiny_typed_emitter.TypedEmitter {
|
|
635
|
+
static {
|
|
636
|
+
__name(this, "DisTubeVoice");
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* The id of the guild of this voice connection.
|
|
640
|
+
*/
|
|
641
|
+
id;
|
|
642
|
+
/**
|
|
643
|
+
* The voice manager that instantiated this connection.
|
|
644
|
+
*/
|
|
645
|
+
voices;
|
|
646
|
+
/**
|
|
647
|
+
* The audio player that plays the audio of this connection.
|
|
648
|
+
*/
|
|
649
|
+
audioPlayer;
|
|
650
|
+
/**
|
|
651
|
+
* The Discord.js voice connection.
|
|
652
|
+
*/
|
|
653
|
+
connection;
|
|
654
|
+
/**
|
|
655
|
+
* Whether or not an error was already emitted for the current stream.
|
|
656
|
+
*
|
|
657
|
+
* It prevents the same error from being emitted several times.
|
|
658
|
+
*/
|
|
659
|
+
emittedError;
|
|
660
|
+
/**
|
|
661
|
+
* Whether or not the voice connection has been disconnected.
|
|
662
|
+
*/
|
|
663
|
+
isDisconnected = false;
|
|
664
|
+
/**
|
|
665
|
+
* The stream that is currently playing.
|
|
666
|
+
*/
|
|
667
|
+
stream;
|
|
668
|
+
/**
|
|
669
|
+
* The stream that was playing when the playback was paused.
|
|
670
|
+
*/
|
|
671
|
+
pausingStream;
|
|
672
|
+
#channel;
|
|
673
|
+
#volume = 100;
|
|
674
|
+
/**
|
|
675
|
+
* Timer used to detect a track that is stuck in the "buffering" state.
|
|
676
|
+
*/
|
|
677
|
+
#stuckTimer;
|
|
678
|
+
/**
|
|
679
|
+
* Create a voice connection to the voice channel.
|
|
680
|
+
* @param voiceManager - The voice manager of the DisTube instance
|
|
681
|
+
* @param channel - The voice channel to join
|
|
682
|
+
*/
|
|
683
|
+
constructor(voiceManager, channel) {
|
|
684
|
+
super();
|
|
685
|
+
this.voices = voiceManager;
|
|
686
|
+
this.id = channel.guildId;
|
|
687
|
+
this.channel = channel;
|
|
688
|
+
this.voices.add(this.id, this);
|
|
689
|
+
this.audioPlayer = (0, import_voice.createAudioPlayer)().on(import_voice.AudioPlayerStatus.Idle, (oldState) => {
|
|
690
|
+
this.#clearStuckTimer();
|
|
691
|
+
if (oldState.status !== import_voice.AudioPlayerStatus.Idle) this.emit("finish");
|
|
692
|
+
}).on(import_voice.AudioPlayerStatus.Buffering, () => {
|
|
693
|
+
this.#startStuckTimer();
|
|
694
|
+
}).on(import_voice.AudioPlayerStatus.Playing, () => {
|
|
695
|
+
this.#clearStuckTimer();
|
|
696
|
+
}).on("error", (error) => {
|
|
697
|
+
if (this.emittedError) return;
|
|
698
|
+
this.emittedError = true;
|
|
699
|
+
this.emit("error", error);
|
|
700
|
+
});
|
|
701
|
+
this.connection.on(import_voice.VoiceConnectionStatus.Disconnected, (_, newState) => {
|
|
702
|
+
if (newState.reason === import_voice.VoiceConnectionDisconnectReason.Manual) {
|
|
703
|
+
this.leave();
|
|
704
|
+
} else if (newState.reason === import_voice.VoiceConnectionDisconnectReason.WebSocketClose && newState.closeCode === 4014) {
|
|
705
|
+
(0, import_voice.entersState)(this.connection, import_voice.VoiceConnectionStatus.Connecting, RECONNECT_TIMEOUT_MS).catch(() => {
|
|
706
|
+
if (![import_voice.VoiceConnectionStatus.Ready, import_voice.VoiceConnectionStatus.Connecting].includes(this.connection.state.status)) {
|
|
707
|
+
this.leave();
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
} else if (this.connection.rejoinAttempts < RECONNECT_MAX_ATTEMPTS) {
|
|
711
|
+
setTimeout(
|
|
712
|
+
() => {
|
|
713
|
+
this.connection.rejoin();
|
|
714
|
+
},
|
|
715
|
+
(this.connection.rejoinAttempts + 1) * RECONNECT_TIMEOUT_MS
|
|
716
|
+
).unref();
|
|
717
|
+
} else if (this.connection.state.status !== import_voice.VoiceConnectionStatus.Destroyed) {
|
|
718
|
+
this.leave(new DisTubeError("VOICE_RECONNECT_FAILED"));
|
|
719
|
+
}
|
|
720
|
+
}).on(import_voice.VoiceConnectionStatus.Destroyed, () => {
|
|
721
|
+
this.leave();
|
|
722
|
+
}).on("error", () => void 0);
|
|
723
|
+
this.connection.subscribe(this.audioPlayer);
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* The voice channel id the bot is in
|
|
727
|
+
*/
|
|
728
|
+
get channelId() {
|
|
729
|
+
return this.connection?.joinConfig?.channelId ?? void 0;
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* The voice channel the bot is in.
|
|
733
|
+
*
|
|
734
|
+
* Setting it moves the bot to another voice channel.
|
|
735
|
+
*/
|
|
736
|
+
get channel() {
|
|
737
|
+
if (!this.channelId) return this.#channel;
|
|
738
|
+
if (this.#channel?.id === this.channelId) return this.#channel;
|
|
739
|
+
const channel = this.voices.client.channels.cache.get(this.channelId);
|
|
740
|
+
if (!channel) return this.#channel;
|
|
741
|
+
for (const type of import_discord.Constants.VoiceBasedChannelTypes) {
|
|
742
|
+
if (channel.type === type) {
|
|
743
|
+
this.#channel = channel;
|
|
744
|
+
return channel;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
return this.#channel;
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Move the bot to another voice channel.
|
|
751
|
+
* @param channel - The voice channel to join
|
|
752
|
+
* @throws {@link DisTubeError} When the channel is invalid, when it belongs to
|
|
753
|
+
* another guild or client, or when the bot cannot join it
|
|
754
|
+
*/
|
|
755
|
+
set channel(channel) {
|
|
756
|
+
if (!isSupportedVoiceChannel(channel)) {
|
|
757
|
+
throw new DisTubeError("INVALID_TYPE", "BaseGuildVoiceChannel", channel, "DisTubeVoice#channel");
|
|
758
|
+
}
|
|
759
|
+
if (channel.guildId !== this.id) throw new DisTubeError("VOICE_DIFFERENT_GUILD");
|
|
760
|
+
if (channel.client.user?.id !== this.voices.client.user?.id) throw new DisTubeError("VOICE_DIFFERENT_CLIENT");
|
|
761
|
+
if (channel.id === this.channelId) return;
|
|
762
|
+
if (!channel.joinable) {
|
|
763
|
+
if (channel.full) throw new DisTubeError("VOICE_FULL");
|
|
764
|
+
else throw new DisTubeError("VOICE_MISSING_PERMS");
|
|
765
|
+
}
|
|
766
|
+
this.connection = this.#join(channel);
|
|
767
|
+
this.#channel = channel;
|
|
768
|
+
}
|
|
769
|
+
#join(channel) {
|
|
770
|
+
return (0, import_voice.joinVoiceChannel)({
|
|
771
|
+
channelId: channel.id,
|
|
772
|
+
guildId: this.id,
|
|
773
|
+
adapterCreator: channel.guild.voiceAdapterCreator,
|
|
774
|
+
group: channel.client.user?.id,
|
|
775
|
+
// DAVE is the end-to-end encryption protocol used by Discord.
|
|
776
|
+
// It is enabled explicitly so that a change of the upstream default
|
|
777
|
+
// can never silently disable the encryption.
|
|
778
|
+
daveEncryption: true
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Join a voice channel with this connection
|
|
783
|
+
* @param channel - A voice channel
|
|
784
|
+
* @returns The voice connection
|
|
785
|
+
* @throws {@link DisTubeError} When the connection cannot be established
|
|
786
|
+
*/
|
|
787
|
+
async join(channel) {
|
|
788
|
+
if (channel) this.channel = channel;
|
|
789
|
+
try {
|
|
790
|
+
await (0, import_voice.entersState)(this.connection, import_voice.VoiceConnectionStatus.Ready, JOIN_TIMEOUT_MS);
|
|
791
|
+
} catch {
|
|
792
|
+
if (this.connection.state.status === import_voice.VoiceConnectionStatus.Ready) return this;
|
|
793
|
+
if (this.connection.state.status !== import_voice.VoiceConnectionStatus.Destroyed) this.connection.destroy();
|
|
794
|
+
this.voices.remove(this.id);
|
|
795
|
+
throw new DisTubeError("VOICE_CONNECT_FAILED", JOIN_TIMEOUT_MS / 1e3);
|
|
796
|
+
}
|
|
797
|
+
return this;
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Leave the voice channel of this connection
|
|
801
|
+
* @param error - Optional, an error to emit with 'error' event.
|
|
802
|
+
* @returns Nothing
|
|
803
|
+
*/
|
|
804
|
+
leave(error) {
|
|
805
|
+
this.stop(true);
|
|
806
|
+
if (!this.isDisconnected) {
|
|
807
|
+
this.emit("disconnect", error);
|
|
808
|
+
this.isDisconnected = true;
|
|
809
|
+
}
|
|
810
|
+
if (this.connection.state.status !== import_voice.VoiceConnectionStatus.Destroyed) this.connection.destroy();
|
|
811
|
+
this.voices.remove(this.id);
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Stop the playing stream
|
|
815
|
+
* @param force - If true, will force the {@link DisTubeVoice#audioPlayer} to enter the Idle state even
|
|
816
|
+
* if the {@link DisTubeStream#audioResource} has silence padding frames.
|
|
817
|
+
* @returns Nothing
|
|
818
|
+
*/
|
|
819
|
+
stop(force = false) {
|
|
820
|
+
this.audioPlayer.stop(force);
|
|
821
|
+
}
|
|
822
|
+
#streamErrorHandler;
|
|
823
|
+
/**
|
|
824
|
+
* Play a {@link DisTubeStream}
|
|
825
|
+
* @param dtStream - DisTubeStream
|
|
826
|
+
* @returns A promise that resolves when the stream is playing
|
|
827
|
+
* @throws {@link DisTubeError} When no encryption library is available
|
|
828
|
+
*/
|
|
829
|
+
async play(dtStream) {
|
|
830
|
+
if (!await checkEncryptionLibraries()) {
|
|
831
|
+
dtStream.kill();
|
|
832
|
+
throw new DisTubeError("ENCRYPTION_LIBRARIES_MISSING");
|
|
833
|
+
}
|
|
834
|
+
this.emittedError = false;
|
|
835
|
+
if (this.stream && this.#streamErrorHandler) {
|
|
836
|
+
this.stream.off("error", this.#streamErrorHandler);
|
|
837
|
+
}
|
|
838
|
+
this.#streamErrorHandler = (error) => {
|
|
839
|
+
if (this.emittedError || error.code === "ERR_STREAM_PREMATURE_CLOSE") return;
|
|
840
|
+
this.emittedError = true;
|
|
841
|
+
this.emit("error", error);
|
|
842
|
+
};
|
|
843
|
+
dtStream.on("error", this.#streamErrorHandler);
|
|
844
|
+
if (this.audioPlayer.state.status !== import_voice.AudioPlayerStatus.Paused) {
|
|
845
|
+
this.audioPlayer.play(dtStream.audioResource);
|
|
846
|
+
this.stream?.kill();
|
|
847
|
+
dtStream.spawn();
|
|
848
|
+
} else if (!this.pausingStream) {
|
|
849
|
+
this.pausingStream = this.stream;
|
|
850
|
+
}
|
|
851
|
+
this.stream = dtStream;
|
|
852
|
+
this.volume = this.#volume;
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Set the volume percentage of the stream.
|
|
856
|
+
* @param volume - The volume percentage, from `0` to `100`
|
|
857
|
+
* @throws {@link DisTubeError} When the volume is not a number, or when it is
|
|
858
|
+
* negative
|
|
859
|
+
*/
|
|
860
|
+
set volume(volume) {
|
|
861
|
+
if (typeof volume !== "number" || Number.isNaN(volume)) {
|
|
862
|
+
throw new DisTubeError("INVALID_TYPE", "number", volume, "volume");
|
|
863
|
+
}
|
|
864
|
+
if (volume < 0) {
|
|
865
|
+
throw new DisTubeError("NUMBER_COMPARE", "Volume", "bigger or equal to", 0);
|
|
866
|
+
}
|
|
867
|
+
this.#volume = volume;
|
|
868
|
+
this.stream?.setVolume((this.#volume / 100) ** (0.5 / Math.log10(2)));
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Get or set the volume percentage
|
|
872
|
+
*/
|
|
873
|
+
get volume() {
|
|
874
|
+
return this.#volume;
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Playback duration of the audio resource in seconds (time since playback started)
|
|
878
|
+
*/
|
|
879
|
+
get playbackDuration() {
|
|
880
|
+
return (this.stream?.audioResource?.playbackDuration ?? 0) / 1e3;
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Current playback time in seconds, accounting for seek offset
|
|
884
|
+
*/
|
|
885
|
+
get playbackTime() {
|
|
886
|
+
return this.playbackDuration + (this.stream?.seekTime ?? 0);
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Pause the playback of the audio player.
|
|
890
|
+
* @returns Nothing
|
|
891
|
+
*/
|
|
892
|
+
pause() {
|
|
893
|
+
this.audioPlayer.pause();
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Resume the playback of the audio player.
|
|
897
|
+
*
|
|
898
|
+
* The FFmpeg process is spawned again when the stream was killed while the
|
|
899
|
+
* playback was paused.
|
|
900
|
+
* @returns Nothing
|
|
901
|
+
*/
|
|
902
|
+
unpause() {
|
|
903
|
+
const state = this.audioPlayer.state;
|
|
904
|
+
if (state.status !== import_voice.AudioPlayerStatus.Paused) return;
|
|
905
|
+
if (this.stream?.audioResource && state.resource !== this.stream.audioResource) {
|
|
906
|
+
this.audioPlayer.play(this.stream.audioResource);
|
|
907
|
+
this.stream.spawn();
|
|
908
|
+
this.pausingStream?.kill();
|
|
909
|
+
delete this.pausingStream;
|
|
910
|
+
} else {
|
|
911
|
+
this.audioPlayer.unpause();
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Emit an event of the voice connection.
|
|
916
|
+
*
|
|
917
|
+
* The special `"error"` event never crashes the process when there is no
|
|
918
|
+
* listener for it, even when the voice connection is used outside of a
|
|
919
|
+
* {@link Queue}.
|
|
920
|
+
* @param event - The event name
|
|
921
|
+
* @param args - The event arguments
|
|
922
|
+
* @returns `true` when the event had listeners, `false` otherwise
|
|
923
|
+
*/
|
|
924
|
+
emit(event, ...args) {
|
|
925
|
+
if (event === "error" && this.listenerCount("error") === 0) {
|
|
926
|
+
handleUnhandledError(args[0], this.voices.options?.unhandledError ?? "log");
|
|
927
|
+
return false;
|
|
928
|
+
}
|
|
929
|
+
return super.emit(event, ...args);
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Start the timer that detects a track stuck in the "buffering" state.
|
|
933
|
+
*
|
|
934
|
+
* When the track does not start playing within
|
|
935
|
+
* {@link DisTubeOptions.stuckThreshold} milliseconds, it is reported as an error
|
|
936
|
+
* so that the queue can skip it.
|
|
937
|
+
*/
|
|
938
|
+
#startStuckTimer() {
|
|
939
|
+
const threshold = this.voices.options.stuckThreshold;
|
|
940
|
+
if (typeof threshold !== "number" || threshold <= 0) return;
|
|
941
|
+
this.#clearStuckTimer();
|
|
942
|
+
this.#stuckTimer = setTimeout(() => {
|
|
943
|
+
this.#stuckTimer = void 0;
|
|
944
|
+
if (this.emittedError) return;
|
|
945
|
+
this.emittedError = true;
|
|
946
|
+
this.emit("error", new DisTubeError("TRACK_STUCK"));
|
|
947
|
+
}, threshold);
|
|
948
|
+
this.#stuckTimer.unref?.();
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Clear the stuck-track detection timer.
|
|
952
|
+
*/
|
|
953
|
+
#clearStuckTimer() {
|
|
954
|
+
if (!this.#stuckTimer) return;
|
|
955
|
+
clearTimeout(this.#stuckTimer);
|
|
956
|
+
this.#stuckTimer = void 0;
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Whether the bot is self-deafened
|
|
960
|
+
*/
|
|
961
|
+
get selfDeaf() {
|
|
962
|
+
return this.connection.joinConfig.selfDeaf;
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Whether the bot is self-muted
|
|
966
|
+
*/
|
|
967
|
+
get selfMute() {
|
|
968
|
+
return this.connection.joinConfig.selfMute;
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Self-deafens/undeafens the bot.
|
|
972
|
+
* @param selfDeaf - Whether or not the bot should be self-deafened
|
|
973
|
+
* @returns true if the voice state was successfully updated, otherwise false
|
|
974
|
+
* @throws {@link DisTubeError} When the value is not a boolean
|
|
975
|
+
*/
|
|
976
|
+
setSelfDeaf(selfDeaf) {
|
|
977
|
+
if (typeof selfDeaf !== "boolean") {
|
|
978
|
+
throw new DisTubeError("INVALID_TYPE", "boolean", selfDeaf, "selfDeaf");
|
|
979
|
+
}
|
|
980
|
+
return this.connection.rejoin({
|
|
981
|
+
...this.connection.joinConfig,
|
|
982
|
+
selfDeaf
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Self-mutes/unmutes the bot.
|
|
987
|
+
* @param selfMute - Whether or not the bot should be self-muted
|
|
988
|
+
* @returns true if the voice state was successfully updated, otherwise false
|
|
989
|
+
* @throws {@link DisTubeError} When the value is not a boolean
|
|
990
|
+
*/
|
|
991
|
+
setSelfMute(selfMute) {
|
|
992
|
+
if (typeof selfMute !== "boolean") {
|
|
993
|
+
throw new DisTubeError("INVALID_TYPE", "boolean", selfMute, "selfMute");
|
|
994
|
+
}
|
|
995
|
+
return this.connection.rejoin({
|
|
996
|
+
...this.connection.joinConfig,
|
|
997
|
+
selfMute
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* The voice state of this connection
|
|
1002
|
+
*/
|
|
1003
|
+
get voiceState() {
|
|
1004
|
+
return this.channel?.guild?.members?.me?.voice;
|
|
1005
|
+
}
|
|
1006
|
+
};
|
|
1007
|
+
|
|
1008
|
+
// src/core/manager/BaseManager.ts
|
|
1009
|
+
var import_discord2 = require("discord.js");
|
|
1010
|
+
var BaseManager = class extends DisTubeBase {
|
|
1011
|
+
static {
|
|
1012
|
+
__name(this, "BaseManager");
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* The collection of items for this manager.
|
|
1016
|
+
*/
|
|
1017
|
+
collection = new import_discord2.Collection();
|
|
1018
|
+
/**
|
|
1019
|
+
* The size of the collection.
|
|
1020
|
+
*/
|
|
1021
|
+
get size() {
|
|
1022
|
+
return this.collection.size;
|
|
1023
|
+
}
|
|
1024
|
+
};
|
|
1025
|
+
|
|
1026
|
+
// src/core/manager/FilterManager.ts
|
|
1027
|
+
var FilterManager = class extends BaseManager {
|
|
1028
|
+
static {
|
|
1029
|
+
__name(this, "FilterManager");
|
|
1030
|
+
}
|
|
1031
|
+
/**
|
|
1032
|
+
* The queue to manage
|
|
1033
|
+
*/
|
|
1034
|
+
queue;
|
|
1035
|
+
/**
|
|
1036
|
+
* Create a filter manager for a queue.
|
|
1037
|
+
* @param queue - The queue to manage
|
|
1038
|
+
*/
|
|
1039
|
+
constructor(queue) {
|
|
1040
|
+
super(queue.distube);
|
|
1041
|
+
this.queue = queue;
|
|
1042
|
+
}
|
|
1043
|
+
#resolve(filter) {
|
|
1044
|
+
if (typeof filter === "object" && typeof filter.name === "string" && typeof filter.value === "string") {
|
|
1045
|
+
return filter;
|
|
1046
|
+
}
|
|
1047
|
+
if (typeof filter === "string" && Object.hasOwn(this.distube.filters, filter)) {
|
|
1048
|
+
return {
|
|
1049
|
+
name: filter,
|
|
1050
|
+
value: this.distube.filters[filter]
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
throw new DisTubeError("INVALID_TYPE", "FilterResolvable", filter, "filter");
|
|
1054
|
+
}
|
|
1055
|
+
#apply() {
|
|
1056
|
+
void this.queue._restartStream().catch((e) => {
|
|
1057
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
1058
|
+
this.queue.debug(`[${this.queue.id}] Failed to apply the filters: ${errorMessage}`);
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
/**
|
|
1062
|
+
* Enable a filter or multiple filters to the manager
|
|
1063
|
+
* @param filterOrFilters - The filter or filters to enable
|
|
1064
|
+
* @param override - Wether or not override the applied filter with new filter value
|
|
1065
|
+
* @returns This manager, to allow chaining
|
|
1066
|
+
* @throws {@link DisTubeError} When a filter is not a valid {@link FilterResolvable}
|
|
1067
|
+
* @example
|
|
1068
|
+
* ```ts
|
|
1069
|
+
* queue.filters.add("bassboost");
|
|
1070
|
+
* ```
|
|
1071
|
+
*/
|
|
1072
|
+
add(filterOrFilters, override = false) {
|
|
1073
|
+
if (Array.isArray(filterOrFilters)) {
|
|
1074
|
+
for (const filter of filterOrFilters) {
|
|
1075
|
+
const ft = this.#resolve(filter);
|
|
1076
|
+
if (override || !this.has(ft)) this.collection.set(ft.name, ft);
|
|
1077
|
+
}
|
|
1078
|
+
} else {
|
|
1079
|
+
const ft = this.#resolve(filterOrFilters);
|
|
1080
|
+
if (override || !this.has(ft)) this.collection.set(ft.name, ft);
|
|
1081
|
+
}
|
|
1082
|
+
this.#apply();
|
|
1083
|
+
return this;
|
|
1084
|
+
}
|
|
1085
|
+
/**
|
|
1086
|
+
* Clear enabled filters of the manager
|
|
1087
|
+
* @returns This manager, to allow chaining
|
|
1088
|
+
*/
|
|
1089
|
+
clear() {
|
|
1090
|
+
return this.set([]);
|
|
1091
|
+
}
|
|
1092
|
+
/**
|
|
1093
|
+
* Set the filters applied to the manager
|
|
1094
|
+
* @param filters - The filters to apply
|
|
1095
|
+
* @returns This manager, to allow chaining
|
|
1096
|
+
* @throws {@link DisTubeError} When the array is invalid, or when a filter is
|
|
1097
|
+
* not a valid {@link FilterResolvable}
|
|
1098
|
+
*/
|
|
1099
|
+
set(filters) {
|
|
1100
|
+
if (!Array.isArray(filters)) throw new DisTubeError("INVALID_TYPE", "Array<FilterResolvable>", filters, "filters");
|
|
1101
|
+
this.collection.clear();
|
|
1102
|
+
for (const f of filters) {
|
|
1103
|
+
const filter = this.#resolve(f);
|
|
1104
|
+
this.collection.set(filter.name, filter);
|
|
1105
|
+
}
|
|
1106
|
+
this.#apply();
|
|
1107
|
+
return this;
|
|
1108
|
+
}
|
|
1109
|
+
#removeFn(f) {
|
|
1110
|
+
return this.collection.delete(this.#resolve(f).name);
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Disable a filter or multiple filters
|
|
1114
|
+
* @param filterOrFilters - The filter or filters to disable
|
|
1115
|
+
* @returns This manager, to allow chaining
|
|
1116
|
+
* @throws {@link DisTubeError} When a filter is not a valid {@link FilterResolvable}
|
|
1117
|
+
*/
|
|
1118
|
+
remove(filterOrFilters) {
|
|
1119
|
+
if (Array.isArray(filterOrFilters)) filterOrFilters.forEach((f) => this.#removeFn(f));
|
|
1120
|
+
else this.#removeFn(filterOrFilters);
|
|
1121
|
+
this.#apply();
|
|
1122
|
+
return this;
|
|
1123
|
+
}
|
|
1124
|
+
/**
|
|
1125
|
+
* Check whether a filter enabled or not
|
|
1126
|
+
* @param filter - The filter to check
|
|
1127
|
+
* @returns `true` when the filter is enabled
|
|
1128
|
+
* @throws {@link DisTubeError} When the filter is not a valid {@link FilterResolvable}
|
|
1129
|
+
*/
|
|
1130
|
+
has(filter) {
|
|
1131
|
+
return this.collection.has(typeof filter === "string" ? filter : this.#resolve(filter).name);
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Array of enabled filter names
|
|
1135
|
+
*/
|
|
1136
|
+
get names() {
|
|
1137
|
+
return [...this.collection.keys()];
|
|
1138
|
+
}
|
|
1139
|
+
/**
|
|
1140
|
+
* Array of enabled filters
|
|
1141
|
+
*/
|
|
1142
|
+
get values() {
|
|
1143
|
+
return [...this.collection.values()];
|
|
1144
|
+
}
|
|
1145
|
+
/**
|
|
1146
|
+
* The FFmpeg arguments of the enabled filters.
|
|
1147
|
+
*
|
|
1148
|
+
* It is the `af` output argument of the FFmpeg process.
|
|
1149
|
+
*/
|
|
1150
|
+
get ffmpegArgs() {
|
|
1151
|
+
return this.size ? { af: this.values.map((f) => f.value).join(",") } : {};
|
|
1152
|
+
}
|
|
1153
|
+
/**
|
|
1154
|
+
* The names of the enabled filters, as a comma separated string.
|
|
1155
|
+
* @returns The names of the enabled filters
|
|
1156
|
+
*/
|
|
1157
|
+
toString() {
|
|
1158
|
+
return this.names.toString();
|
|
1159
|
+
}
|
|
1160
|
+
};
|
|
1161
|
+
|
|
1162
|
+
// src/type.ts
|
|
1163
|
+
var Events = /* @__PURE__ */ ((Events2) => {
|
|
1164
|
+
Events2["ERROR"] = "error";
|
|
1165
|
+
Events2["ADD_LIST"] = "addList";
|
|
1166
|
+
Events2["ADD_SONG"] = "addSong";
|
|
1167
|
+
Events2["PLAY_SONG"] = "playSong";
|
|
1168
|
+
Events2["FINISH_SONG"] = "finishSong";
|
|
1169
|
+
Events2["EMPTY"] = "empty";
|
|
1170
|
+
Events2["FINISH"] = "finish";
|
|
1171
|
+
Events2["INIT_QUEUE"] = "initQueue";
|
|
1172
|
+
Events2["NO_RELATED"] = "noRelated";
|
|
1173
|
+
Events2["DISCONNECT"] = "disconnect";
|
|
1174
|
+
Events2["DELETE_QUEUE"] = "deleteQueue";
|
|
1175
|
+
Events2["FFMPEG_DEBUG"] = "ffmpegDebug";
|
|
1176
|
+
Events2["DEBUG"] = "debug";
|
|
1177
|
+
return Events2;
|
|
1178
|
+
})(Events || {});
|
|
1179
|
+
var SearchResultType = /* @__PURE__ */ ((SearchResultType2) => {
|
|
1180
|
+
SearchResultType2["TRACK"] = "track";
|
|
1181
|
+
SearchResultType2["VIDEO"] = "video";
|
|
1182
|
+
SearchResultType2["PLAYLIST"] = "playlist";
|
|
1183
|
+
SearchResultType2["ALBUM"] = "album";
|
|
1184
|
+
SearchResultType2["ARTIST"] = "artist";
|
|
1185
|
+
return SearchResultType2;
|
|
1186
|
+
})(SearchResultType || {});
|
|
1187
|
+
var RepeatMode = /* @__PURE__ */ ((RepeatMode2) => {
|
|
1188
|
+
RepeatMode2[RepeatMode2["DISABLED"] = 0] = "DISABLED";
|
|
1189
|
+
RepeatMode2[RepeatMode2["SONG"] = 1] = "SONG";
|
|
1190
|
+
RepeatMode2[RepeatMode2["QUEUE"] = 2] = "QUEUE";
|
|
1191
|
+
return RepeatMode2;
|
|
1192
|
+
})(RepeatMode || {});
|
|
1193
|
+
var PluginType = /* @__PURE__ */ ((PluginType2) => {
|
|
1194
|
+
PluginType2["EXTRACTOR"] = "extractor";
|
|
1195
|
+
PluginType2["INFO_EXTRACTOR"] = "info-extractor";
|
|
1196
|
+
PluginType2["PLAYABLE_EXTRACTOR"] = "playable-extractor";
|
|
1197
|
+
return PluginType2;
|
|
1198
|
+
})(PluginType || {});
|
|
1199
|
+
|
|
1200
|
+
// src/struct/TaskQueue.ts
|
|
1201
|
+
var Task = class {
|
|
1202
|
+
static {
|
|
1203
|
+
__name(this, "Task");
|
|
1204
|
+
}
|
|
1205
|
+
resolve;
|
|
1206
|
+
promise;
|
|
1207
|
+
isPlay;
|
|
1208
|
+
constructor(isPlay) {
|
|
1209
|
+
this.isPlay = isPlay;
|
|
1210
|
+
this.promise = new Promise((res) => {
|
|
1211
|
+
this.resolve = res;
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
};
|
|
1215
|
+
var TaskQueue = class {
|
|
1216
|
+
static {
|
|
1217
|
+
__name(this, "TaskQueue");
|
|
1218
|
+
}
|
|
1219
|
+
/**
|
|
1220
|
+
* The task array
|
|
1221
|
+
*/
|
|
1222
|
+
#tasks = [];
|
|
1223
|
+
/**
|
|
1224
|
+
* Waits for last task finished and queues a new task
|
|
1225
|
+
* @param isPlay - Whether the task is a play task
|
|
1226
|
+
* @returns A promise that resolves when the last queued task is finished
|
|
1227
|
+
*/
|
|
1228
|
+
queuing(isPlay = false) {
|
|
1229
|
+
const next = this.remaining ? this.#tasks[this.#tasks.length - 1].promise : Promise.resolve();
|
|
1230
|
+
this.#tasks.push(new Task(isPlay));
|
|
1231
|
+
return next;
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* Removes the finished task and processes the next task
|
|
1235
|
+
* @returns Nothing
|
|
1236
|
+
*/
|
|
1237
|
+
resolve() {
|
|
1238
|
+
this.#tasks.shift()?.resolve();
|
|
1239
|
+
}
|
|
1240
|
+
/**
|
|
1241
|
+
* The remaining number of tasks
|
|
1242
|
+
*/
|
|
1243
|
+
get remaining() {
|
|
1244
|
+
return this.#tasks.length;
|
|
1245
|
+
}
|
|
1246
|
+
/**
|
|
1247
|
+
* Whether or not having a play task
|
|
1248
|
+
*/
|
|
1249
|
+
get hasPlayTask() {
|
|
1250
|
+
return this.#tasks.some((t) => t.isPlay);
|
|
1251
|
+
}
|
|
1252
|
+
};
|
|
1253
|
+
|
|
1254
|
+
// src/struct/Queue.ts
|
|
1255
|
+
var Queue = class extends DisTubeBase {
|
|
1256
|
+
static {
|
|
1257
|
+
__name(this, "Queue");
|
|
1258
|
+
}
|
|
1259
|
+
/**
|
|
1260
|
+
* Queue id (Guild id)
|
|
1261
|
+
*/
|
|
1262
|
+
id;
|
|
1263
|
+
/**
|
|
1264
|
+
* Voice connection of this queue.
|
|
1265
|
+
*/
|
|
1266
|
+
voice;
|
|
1267
|
+
/**
|
|
1268
|
+
* List of songs in the queue (The first one is the playing song)
|
|
1269
|
+
*/
|
|
1270
|
+
songs;
|
|
1271
|
+
/**
|
|
1272
|
+
* List of the previous songs.
|
|
1273
|
+
*/
|
|
1274
|
+
previousSongs;
|
|
1275
|
+
/**
|
|
1276
|
+
* Whether stream is currently stopped.
|
|
1277
|
+
*/
|
|
1278
|
+
stopped;
|
|
1279
|
+
/**
|
|
1280
|
+
* Whether or not the stream is currently paused.
|
|
1281
|
+
*
|
|
1282
|
+
* It is also `true` once the queue has been stopped, so that `!queue.paused`
|
|
1283
|
+
* stays `false` as soon as the queue is not playing anymore.
|
|
1284
|
+
*/
|
|
1285
|
+
paused;
|
|
1286
|
+
/**
|
|
1287
|
+
* Type of repeat mode (`0` is disabled, `1` is repeating a song, `2` is repeating
|
|
1288
|
+
* all the queue). Default value: `0` (disabled)
|
|
1289
|
+
*/
|
|
1290
|
+
repeatMode;
|
|
1291
|
+
/**
|
|
1292
|
+
* Whether or not the autoplay mode is enabled. Default value: `false`
|
|
1293
|
+
*/
|
|
1294
|
+
autoplay;
|
|
1295
|
+
/**
|
|
1296
|
+
* FFmpeg arguments for the current queue. Default value is defined with {@link DisTubeOptions}.ffmpeg.args.
|
|
1297
|
+
* `af` output argument will be replaced with {@link Queue#filters} manager
|
|
1298
|
+
*/
|
|
1299
|
+
ffmpegArgs;
|
|
1300
|
+
/**
|
|
1301
|
+
* The text channel of the Queue. (Default: where the first command is called).
|
|
1302
|
+
*/
|
|
1303
|
+
textChannel;
|
|
1304
|
+
/**
|
|
1305
|
+
* What time in the song to begin (in seconds).
|
|
1306
|
+
* @internal
|
|
1307
|
+
*/
|
|
1308
|
+
_beginTime;
|
|
1309
|
+
#filters;
|
|
1310
|
+
/**
|
|
1311
|
+
* Whether or not the queue is being updated manually (skip, jump, previous)
|
|
1312
|
+
* @internal
|
|
1313
|
+
*/
|
|
1314
|
+
_manualUpdate;
|
|
1315
|
+
/**
|
|
1316
|
+
* Task queuing system
|
|
1317
|
+
* @internal
|
|
1318
|
+
*/
|
|
1319
|
+
_taskQueue;
|
|
1320
|
+
/**
|
|
1321
|
+
* {@link DisTubeVoice} listener
|
|
1322
|
+
* @internal
|
|
1323
|
+
*/
|
|
1324
|
+
_listeners;
|
|
1325
|
+
/**
|
|
1326
|
+
* Create a queue for the guild
|
|
1327
|
+
* @param distube - DisTube
|
|
1328
|
+
* @param voice - Voice connection
|
|
1329
|
+
* @param textChannel - Default text channel
|
|
1330
|
+
*/
|
|
1331
|
+
constructor(distube, voice, textChannel) {
|
|
1332
|
+
super(distube);
|
|
1333
|
+
this.voice = voice;
|
|
1334
|
+
this.id = voice.id;
|
|
1335
|
+
this.volume = DEFAULT_VOLUME;
|
|
1336
|
+
this.songs = [];
|
|
1337
|
+
this.previousSongs = [];
|
|
1338
|
+
this.stopped = false;
|
|
1339
|
+
this._manualUpdate = false;
|
|
1340
|
+
this.paused = false;
|
|
1341
|
+
this.repeatMode = 0 /* DISABLED */;
|
|
1342
|
+
this.autoplay = false;
|
|
1343
|
+
this.#filters = new FilterManager(this);
|
|
1344
|
+
this._beginTime = 0;
|
|
1345
|
+
this.textChannel = textChannel;
|
|
1346
|
+
this._taskQueue = new TaskQueue();
|
|
1347
|
+
this._listeners = void 0;
|
|
1348
|
+
this.ffmpegArgs = {
|
|
1349
|
+
global: { ...this.options.ffmpeg.args.global },
|
|
1350
|
+
input: { ...this.options.ffmpeg.args.input },
|
|
1351
|
+
output: { ...this.options.ffmpeg.args.output }
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
#addToPreviousSongs(songs) {
|
|
1355
|
+
if (Array.isArray(songs)) {
|
|
1356
|
+
if (this.options.savePreviousSongs) {
|
|
1357
|
+
this.previousSongs.push(...songs);
|
|
1358
|
+
} else {
|
|
1359
|
+
this.previousSongs.push(...songs.map((s) => ({ id: s.id })));
|
|
1360
|
+
}
|
|
1361
|
+
} else if (this.options.savePreviousSongs) {
|
|
1362
|
+
this.previousSongs.push(songs);
|
|
1363
|
+
} else {
|
|
1364
|
+
this.previousSongs.push({ id: songs.id });
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
#stop() {
|
|
1368
|
+
this._manualUpdate = true;
|
|
1369
|
+
this.voice.stop();
|
|
1370
|
+
}
|
|
1371
|
+
/**
|
|
1372
|
+
* The client user as a `GuildMember` of this queue's guild
|
|
1373
|
+
*/
|
|
1374
|
+
get clientMember() {
|
|
1375
|
+
return this.voice.channel.guild.members.me ?? void 0;
|
|
1376
|
+
}
|
|
1377
|
+
/**
|
|
1378
|
+
* The filter manager of the queue
|
|
1379
|
+
*/
|
|
1380
|
+
get filters() {
|
|
1381
|
+
return this.#filters;
|
|
1382
|
+
}
|
|
1383
|
+
/**
|
|
1384
|
+
* Whether or not the queue is still active.
|
|
1385
|
+
*
|
|
1386
|
+
* It is `false` only when the queue has been stopped. A paused queue is still
|
|
1387
|
+
* active.
|
|
1388
|
+
*/
|
|
1389
|
+
get isActive() {
|
|
1390
|
+
return !this.stopped;
|
|
1391
|
+
}
|
|
1392
|
+
/**
|
|
1393
|
+
* Formatted duration string.
|
|
1394
|
+
*/
|
|
1395
|
+
get formattedDuration() {
|
|
1396
|
+
return formatDuration(this.duration);
|
|
1397
|
+
}
|
|
1398
|
+
/**
|
|
1399
|
+
* Queue's duration.
|
|
1400
|
+
*/
|
|
1401
|
+
get duration() {
|
|
1402
|
+
return this.songs.length ? this.songs.reduce((prev, next) => prev + next.duration, 0) : 0;
|
|
1403
|
+
}
|
|
1404
|
+
/**
|
|
1405
|
+
* What time in the song is playing (in seconds).
|
|
1406
|
+
*/
|
|
1407
|
+
get currentTime() {
|
|
1408
|
+
return this.voice.playbackTime;
|
|
1409
|
+
}
|
|
1410
|
+
/**
|
|
1411
|
+
* Formatted {@link Queue#currentTime} string.
|
|
1412
|
+
*/
|
|
1413
|
+
get formattedCurrentTime() {
|
|
1414
|
+
return formatDuration(this.currentTime);
|
|
1415
|
+
}
|
|
1416
|
+
/**
|
|
1417
|
+
* The voice channel playing in.
|
|
1418
|
+
*/
|
|
1419
|
+
get voiceChannel() {
|
|
1420
|
+
return this.clientMember?.voice?.channel ?? null;
|
|
1421
|
+
}
|
|
1422
|
+
/**
|
|
1423
|
+
* Get or set the stream volume. Default value: `50`.
|
|
1424
|
+
*/
|
|
1425
|
+
get volume() {
|
|
1426
|
+
return this.voice.volume;
|
|
1427
|
+
}
|
|
1428
|
+
/**
|
|
1429
|
+
* Set the stream volume.
|
|
1430
|
+
* @param value - The volume percentage, from `0` to `100`
|
|
1431
|
+
* @throws {@link DisTubeError} When the volume is not a number, or when it is
|
|
1432
|
+
* negative
|
|
1433
|
+
*/
|
|
1434
|
+
set volume(value) {
|
|
1435
|
+
this.voice.volume = value;
|
|
1436
|
+
}
|
|
1437
|
+
/**
|
|
1438
|
+
* Add a song or a list of songs to the queue.
|
|
1439
|
+
* @throws {DisTubeError}
|
|
1440
|
+
* @param song - Song to add
|
|
1441
|
+
* @param position - Position to add, \<= 0 to add to the end of the queue
|
|
1442
|
+
* @returns The guild queue
|
|
1443
|
+
* @example
|
|
1444
|
+
* ```ts
|
|
1445
|
+
* queue.addToQueue(song);
|
|
1446
|
+
* ```
|
|
1447
|
+
*/
|
|
1448
|
+
addToQueue(song, position = 0) {
|
|
1449
|
+
if (this.stopped) throw new DisTubeError("QUEUE_STOPPED");
|
|
1450
|
+
if (!song || Array.isArray(song) && !song.length) {
|
|
1451
|
+
throw new DisTubeError("INVALID_TYPE", ["Song", "Array<Song>"], song, "song");
|
|
1452
|
+
}
|
|
1453
|
+
if (typeof position !== "number" || !Number.isInteger(position)) {
|
|
1454
|
+
throw new DisTubeError("INVALID_TYPE", "integer", position, "position");
|
|
1455
|
+
}
|
|
1456
|
+
if (position <= 0) {
|
|
1457
|
+
if (Array.isArray(song)) this.songs.push(...song);
|
|
1458
|
+
else this.songs.push(song);
|
|
1459
|
+
} else if (Array.isArray(song)) {
|
|
1460
|
+
this.songs.splice(position, 0, ...song);
|
|
1461
|
+
} else {
|
|
1462
|
+
this.songs.splice(position, 0, song);
|
|
1463
|
+
}
|
|
1464
|
+
return this;
|
|
1465
|
+
}
|
|
1466
|
+
/**
|
|
1467
|
+
/**
|
|
1468
|
+
* Pause the guild stream
|
|
1469
|
+
* @returns The guild queue
|
|
1470
|
+
* @throws {@link DisTubeError} When the queue is already paused
|
|
1471
|
+
* @example
|
|
1472
|
+
* ```ts
|
|
1473
|
+
* await queue.pause();
|
|
1474
|
+
* ```
|
|
1475
|
+
*/
|
|
1476
|
+
async pause() {
|
|
1477
|
+
await this._taskQueue.queuing();
|
|
1478
|
+
try {
|
|
1479
|
+
if (this.paused) throw new DisTubeError("PAUSED");
|
|
1480
|
+
this.paused = true;
|
|
1481
|
+
this.voice.pause();
|
|
1482
|
+
return this;
|
|
1483
|
+
} finally {
|
|
1484
|
+
this._taskQueue.resolve();
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
/**
|
|
1488
|
+
* Resume the guild stream
|
|
1489
|
+
* @returns The guild queue
|
|
1490
|
+
* @throws {@link DisTubeError} When the queue is not paused
|
|
1491
|
+
* @example
|
|
1492
|
+
* ```ts
|
|
1493
|
+
* await queue.resume();
|
|
1494
|
+
* ```
|
|
1495
|
+
*/
|
|
1496
|
+
async resume() {
|
|
1497
|
+
await this._taskQueue.queuing();
|
|
1498
|
+
try {
|
|
1499
|
+
if (!this.paused) throw new DisTubeError("RESUMED");
|
|
1500
|
+
this.paused = false;
|
|
1501
|
+
this.voice.unpause();
|
|
1502
|
+
return this;
|
|
1503
|
+
} finally {
|
|
1504
|
+
this._taskQueue.resolve();
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
/**
|
|
1508
|
+
* Set the guild stream's volume
|
|
1509
|
+
* @param percent - The percentage of volume you want to set
|
|
1510
|
+
* @returns The guild queue
|
|
1511
|
+
* @throws {@link DisTubeError} When the volume is not a number, or when it is
|
|
1512
|
+
* negative
|
|
1513
|
+
*/
|
|
1514
|
+
setVolume(percent) {
|
|
1515
|
+
this.volume = percent;
|
|
1516
|
+
return this;
|
|
1517
|
+
}
|
|
1518
|
+
/**
|
|
1519
|
+
* Skip the playing song if there is a next song in the queue. <info>If {@link
|
|
1520
|
+
* Queue#autoplay} is `true` and there is no up next song, DisTube will add and
|
|
1521
|
+
* play a related song.</info>
|
|
1522
|
+
* @param options - Skip options
|
|
1523
|
+
* @returns The song will skip to
|
|
1524
|
+
* @throws {@link DisTubeError} When there is no up next song, or when the
|
|
1525
|
+
* position is out of the bounds of the queue
|
|
1526
|
+
* @example
|
|
1527
|
+
* ```ts
|
|
1528
|
+
* await queue.skip();
|
|
1529
|
+
* ```
|
|
1530
|
+
*/
|
|
1531
|
+
async skip(options) {
|
|
1532
|
+
return this.jump(1, options);
|
|
1533
|
+
}
|
|
1534
|
+
/**
|
|
1535
|
+
* Play the previous song if exists
|
|
1536
|
+
* @returns The guild queue
|
|
1537
|
+
* @throws {@link DisTubeError} When {@link DisTubeOptions.savePreviousSongs} is
|
|
1538
|
+
* disabled, or when the queue does not have any previous song
|
|
1539
|
+
*/
|
|
1540
|
+
async previous() {
|
|
1541
|
+
await this._taskQueue.queuing();
|
|
1542
|
+
try {
|
|
1543
|
+
if (!this.options.savePreviousSongs) throw new DisTubeError("DISABLED_OPTION", "savePreviousSongs");
|
|
1544
|
+
const hasPrevious = this.previousSongs.length > 0;
|
|
1545
|
+
if (!hasPrevious && this.repeatMode !== 2 /* QUEUE */) {
|
|
1546
|
+
throw new DisTubeError("NO_PREVIOUS");
|
|
1547
|
+
}
|
|
1548
|
+
const song = hasPrevious ? this.previousSongs.pop() : this.songs.pop();
|
|
1549
|
+
if (!song) throw new DisTubeError("NO_PREVIOUS");
|
|
1550
|
+
this.songs.unshift(song);
|
|
1551
|
+
this.#stop();
|
|
1552
|
+
return song;
|
|
1553
|
+
} finally {
|
|
1554
|
+
this._taskQueue.resolve();
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
/**
|
|
1558
|
+
* Shuffle the queue's songs
|
|
1559
|
+
* @returns The guild queue
|
|
1560
|
+
*/
|
|
1561
|
+
async shuffle() {
|
|
1562
|
+
await this._taskQueue.queuing();
|
|
1563
|
+
try {
|
|
1564
|
+
const playing = this.songs.shift();
|
|
1565
|
+
if (playing === void 0) return this;
|
|
1566
|
+
for (let i = this.songs.length - 1; i > 0; i--) {
|
|
1567
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
1568
|
+
[this.songs[i], this.songs[j]] = [this.songs[j], this.songs[i]];
|
|
1569
|
+
}
|
|
1570
|
+
this.songs.unshift(playing);
|
|
1571
|
+
return this;
|
|
1572
|
+
} finally {
|
|
1573
|
+
this._taskQueue.resolve();
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
/**
|
|
1577
|
+
* Jump to the song position in the queue. The next one is 1, 2,... The previous
|
|
1578
|
+
* one is -1, -2,...
|
|
1579
|
+
* if `num` is invalid number
|
|
1580
|
+
* @param position - The song position to play
|
|
1581
|
+
* @param options - Skip options
|
|
1582
|
+
* @returns The new Song will be played
|
|
1583
|
+
* @throws {@link DisTubeError} When the position is invalid, when there is no
|
|
1584
|
+
* up next song, or when {@link DisTubeOptions.savePreviousSongs} is disabled
|
|
1585
|
+
* and the jump is backwards
|
|
1586
|
+
*/
|
|
1587
|
+
async jump(position, options) {
|
|
1588
|
+
await this._taskQueue.queuing();
|
|
1589
|
+
try {
|
|
1590
|
+
if (typeof position !== "number") throw new DisTubeError("INVALID_TYPE", "number", position, "position");
|
|
1591
|
+
if (!position || position > this.songs.length || -position > this.previousSongs.length) {
|
|
1592
|
+
throw new DisTubeError("NO_SONG_POSITION");
|
|
1593
|
+
}
|
|
1594
|
+
if (position > 0) {
|
|
1595
|
+
if (position >= this.songs.length) {
|
|
1596
|
+
if (this.autoplay) {
|
|
1597
|
+
await this._addRelatedSong();
|
|
1598
|
+
} else {
|
|
1599
|
+
throw new DisTubeError("NO_UP_NEXT");
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
const skipped = this.songs.splice(0, position);
|
|
1603
|
+
if (options?.requeue) {
|
|
1604
|
+
this.songs.push(...skipped);
|
|
1605
|
+
} else {
|
|
1606
|
+
this.#addToPreviousSongs(skipped);
|
|
1607
|
+
}
|
|
1608
|
+
} else if (!this.options.savePreviousSongs) {
|
|
1609
|
+
throw new DisTubeError("DISABLED_OPTION", "savePreviousSongs");
|
|
1610
|
+
} else {
|
|
1611
|
+
const skipped = this.previousSongs.splice(position);
|
|
1612
|
+
this.songs.unshift(...skipped);
|
|
1613
|
+
}
|
|
1614
|
+
this.#stop();
|
|
1615
|
+
return this.songs[0];
|
|
1616
|
+
} finally {
|
|
1617
|
+
this._taskQueue.resolve();
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1621
|
+
* Set the repeat mode of the guild queue.
|
|
1622
|
+
* Toggle mode `(Disabled -> Song -> Queue -> Disabled ->...)` if `mode` is `undefined`
|
|
1623
|
+
* @param mode - The repeat modes (toggle if `undefined`)
|
|
1624
|
+
* @returns The new repeat mode
|
|
1625
|
+
* @throws {@link DisTubeError} When the mode is not a valid {@link RepeatMode}
|
|
1626
|
+
*/
|
|
1627
|
+
setRepeatMode(mode) {
|
|
1628
|
+
if (mode !== void 0 && !Object.values(RepeatMode).includes(mode)) {
|
|
1629
|
+
throw new DisTubeError("INVALID_TYPE", ["RepeatMode", "undefined"], mode, "mode");
|
|
1630
|
+
}
|
|
1631
|
+
if (mode === void 0) this.repeatMode = (this.repeatMode + 1) % 3;
|
|
1632
|
+
else if (this.repeatMode === mode) this.repeatMode = 0 /* DISABLED */;
|
|
1633
|
+
else this.repeatMode = mode;
|
|
1634
|
+
return this.repeatMode;
|
|
1635
|
+
}
|
|
1636
|
+
/**
|
|
1637
|
+
* Set the playing time to another position
|
|
1638
|
+
* @param time - Time in seconds
|
|
1639
|
+
* @returns The guild queue
|
|
1640
|
+
* @throws {@link DisTubeError} When the time is not a number, or when it is
|
|
1641
|
+
* negative
|
|
1642
|
+
* @example
|
|
1643
|
+
* ```ts
|
|
1644
|
+
* await queue.seek(60);
|
|
1645
|
+
* ```
|
|
1646
|
+
*/
|
|
1647
|
+
async seek(time) {
|
|
1648
|
+
await this._taskQueue.queuing();
|
|
1649
|
+
try {
|
|
1650
|
+
if (typeof time !== "number") throw new DisTubeError("INVALID_TYPE", "number", time, "time");
|
|
1651
|
+
if (Number.isNaN(time) || time < 0) throw new DisTubeError("NUMBER_COMPARE", "time", "bigger or equal to", 0);
|
|
1652
|
+
this._beginTime = time;
|
|
1653
|
+
await this.play(false);
|
|
1654
|
+
return this;
|
|
1655
|
+
} finally {
|
|
1656
|
+
this._taskQueue.resolve();
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
async #getRelatedSong(current) {
|
|
1660
|
+
const plugin = await this.handler._getPluginFromSong(current);
|
|
1661
|
+
if (plugin) return plugin.getRelatedSongs(current);
|
|
1662
|
+
return [];
|
|
1663
|
+
}
|
|
1664
|
+
/**
|
|
1665
|
+
* Internal implementation of addRelatedSong without task queue protection.
|
|
1666
|
+
* Used by methods that already hold the task queue lock.
|
|
1667
|
+
* @param song - The song to get related songs from. Defaults to the current playing song.
|
|
1668
|
+
* @returns A promise that resolves to the added song
|
|
1669
|
+
* @throws {@link DisTubeError} When the queue does not have any playing song,
|
|
1670
|
+
* or when no related song could be found
|
|
1671
|
+
* @internal
|
|
1672
|
+
*/
|
|
1673
|
+
async _addRelatedSong(song) {
|
|
1674
|
+
const current = song ?? this.songs?.[0];
|
|
1675
|
+
if (!current) throw new DisTubeError("NO_PLAYING_SONG");
|
|
1676
|
+
const prevIds = this.previousSongs.map((p) => p.id);
|
|
1677
|
+
const relatedSongs = (await this.#getRelatedSong(current)).filter((s) => !prevIds.includes(s.id));
|
|
1678
|
+
this.debug(`[${this.id}] Getting related songs from: ${current}`);
|
|
1679
|
+
if (!relatedSongs.length && !current.stream.playFromSource) {
|
|
1680
|
+
const altSong = current.stream.song;
|
|
1681
|
+
if (altSong) relatedSongs.push(...(await this.#getRelatedSong(altSong)).filter((s) => !prevIds.includes(s.id)));
|
|
1682
|
+
this.debug(`[${this.id}] Getting related songs from streamed song: ${altSong}`);
|
|
1683
|
+
}
|
|
1684
|
+
const nextSong = relatedSongs[0];
|
|
1685
|
+
if (!nextSong) throw new DisTubeError("NO_RELATED");
|
|
1686
|
+
nextSong.metadata = current.metadata;
|
|
1687
|
+
nextSong.member = this.clientMember;
|
|
1688
|
+
this.addToQueue(nextSong);
|
|
1689
|
+
return nextSong;
|
|
1690
|
+
}
|
|
1691
|
+
/**
|
|
1692
|
+
* Add a related song of the playing song to the queue
|
|
1693
|
+
* @param song - The song to get related songs from. Defaults to the current playing song.
|
|
1694
|
+
* @returns The added song
|
|
1695
|
+
* @throws {@link DisTubeError} When the queue does not have any playing song,
|
|
1696
|
+
* or when no related song could be found
|
|
1697
|
+
*/
|
|
1698
|
+
async addRelatedSong(song) {
|
|
1699
|
+
await this._taskQueue.queuing();
|
|
1700
|
+
try {
|
|
1701
|
+
return await this._addRelatedSong(song);
|
|
1702
|
+
} finally {
|
|
1703
|
+
this._taskQueue.resolve();
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
/**
|
|
1707
|
+
* Stop the guild stream and delete the queue
|
|
1708
|
+
* @returns A promise that resolves when the queue is stopped
|
|
1709
|
+
*/
|
|
1710
|
+
async stop() {
|
|
1711
|
+
await this._taskQueue.queuing();
|
|
1712
|
+
try {
|
|
1713
|
+
this.voice.stop();
|
|
1714
|
+
this.remove();
|
|
1715
|
+
} finally {
|
|
1716
|
+
this._taskQueue.resolve();
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
/**
|
|
1720
|
+
* Remove the queue from the manager
|
|
1721
|
+
* @returns Nothing
|
|
1722
|
+
*/
|
|
1723
|
+
remove() {
|
|
1724
|
+
this.paused = true;
|
|
1725
|
+
this.stopped = true;
|
|
1726
|
+
this.songs = [];
|
|
1727
|
+
this.previousSongs = [];
|
|
1728
|
+
if (this._listeners) for (const event of objectKeys(this._listeners)) this.voice.off(event, this._listeners[event]);
|
|
1729
|
+
this.queues.remove(this.id);
|
|
1730
|
+
this.emit("deleteQueue" /* DELETE_QUEUE */, this);
|
|
1731
|
+
}
|
|
1732
|
+
/**
|
|
1733
|
+
* Toggle autoplay mode
|
|
1734
|
+
* @returns Autoplay mode state
|
|
1735
|
+
*/
|
|
1736
|
+
toggleAutoplay() {
|
|
1737
|
+
this.autoplay = !this.autoplay;
|
|
1738
|
+
return this.autoplay;
|
|
1739
|
+
}
|
|
1740
|
+
/**
|
|
1741
|
+
* Play the first song in the queue
|
|
1742
|
+
* @param emitPlaySong - Whether or not emit {@link Events.PLAY_SONG} event
|
|
1743
|
+
* @returns A promise that resolves when the song is playing
|
|
1744
|
+
* @throws {@link DisTubeError} When the queue has been stopped
|
|
1745
|
+
*/
|
|
1746
|
+
play(emitPlaySong = true) {
|
|
1747
|
+
if (this.stopped) throw new DisTubeError("QUEUE_STOPPED");
|
|
1748
|
+
return this.queues.playSong(this, emitPlaySong);
|
|
1749
|
+
}
|
|
1750
|
+
/**
|
|
1751
|
+
* Restart the current stream with the current FFmpeg arguments, keeping the
|
|
1752
|
+
* playback position.
|
|
1753
|
+
*
|
|
1754
|
+
* The FFmpeg audio filters are applied when the stream is created, so the stream
|
|
1755
|
+
* has to be recreated whenever the filters change.
|
|
1756
|
+
*
|
|
1757
|
+
* Unlike a regular playback error, a failure while restarting the stream does
|
|
1758
|
+
* **not** remove the current song from the queue.
|
|
1759
|
+
* @returns A promise that resolves when the stream is restarted
|
|
1760
|
+
* @internal
|
|
1761
|
+
*/
|
|
1762
|
+
async _restartStream() {
|
|
1763
|
+
await this._taskQueue.queuing(true);
|
|
1764
|
+
try {
|
|
1765
|
+
if (this.stopped || !this.songs.length) return;
|
|
1766
|
+
this._beginTime = this.currentTime;
|
|
1767
|
+
await this.queues.playSong(this, false, { keepSongOnError: true });
|
|
1768
|
+
} finally {
|
|
1769
|
+
this._taskQueue.resolve();
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
};
|
|
1773
|
+
|
|
1774
|
+
// src/util.ts
|
|
1775
|
+
var formatInt = /* @__PURE__ */ __name((int) => int < 10 ? `0${int}` : int, "formatInt");
|
|
1776
|
+
function formatDuration(sec) {
|
|
1777
|
+
if (!sec || !Number(sec)) return "00:00";
|
|
1778
|
+
const seconds = Math.floor(sec % 60);
|
|
1779
|
+
const minutes = Math.floor(sec % 3600 / 60);
|
|
1780
|
+
const hours = Math.floor(sec / 3600);
|
|
1781
|
+
if (hours > 0) return `${formatInt(hours)}:${formatInt(minutes)}:${formatInt(seconds)}`;
|
|
1782
|
+
if (minutes > 0) return `${formatInt(minutes)}:${formatInt(seconds)}`;
|
|
1783
|
+
return `00:${formatInt(seconds)}`;
|
|
1784
|
+
}
|
|
1785
|
+
__name(formatDuration, "formatDuration");
|
|
1786
|
+
var SUPPORTED_PROTOCOL = ["https:", "http:", "file:"];
|
|
1787
|
+
function isURL(input) {
|
|
1788
|
+
if (typeof input !== "string" || input.includes(" ")) return false;
|
|
1789
|
+
try {
|
|
1790
|
+
const url = new import_node_url.URL(input);
|
|
1791
|
+
if (!SUPPORTED_PROTOCOL.some((p) => p === url.protocol)) return false;
|
|
1792
|
+
} catch {
|
|
1793
|
+
return false;
|
|
1794
|
+
}
|
|
1795
|
+
return true;
|
|
1796
|
+
}
|
|
1797
|
+
__name(isURL, "isURL");
|
|
1798
|
+
function checkIntents(options) {
|
|
1799
|
+
const intents = options.intents;
|
|
1800
|
+
const hasVoiceStates = typeof intents?.has === "function" ? intents.has(import_discord3.GatewayIntentBits.GuildVoiceStates) : new import_discord3.IntentsBitField(options.intents).has(import_discord3.GatewayIntentBits.GuildVoiceStates);
|
|
1801
|
+
if (!hasVoiceStates) throw new DisTubeError("MISSING_INTENTS", "GuildVoiceStates");
|
|
1802
|
+
}
|
|
1803
|
+
__name(checkIntents, "checkIntents");
|
|
1804
|
+
function isVoiceChannelEmpty(voiceState) {
|
|
1805
|
+
const guild = voiceState.guild;
|
|
1806
|
+
const clientId = voiceState.client.user?.id;
|
|
1807
|
+
if (!guild || !clientId) return false;
|
|
1808
|
+
const voiceChannel = guild.members.me?.voice?.channel;
|
|
1809
|
+
if (!voiceChannel) return false;
|
|
1810
|
+
const members = voiceChannel.members.filter((m) => !m.user.bot);
|
|
1811
|
+
return !members.size;
|
|
1812
|
+
}
|
|
1813
|
+
__name(isVoiceChannelEmpty, "isVoiceChannelEmpty");
|
|
1814
|
+
function isSnowflake(id) {
|
|
1815
|
+
try {
|
|
1816
|
+
return import_discord3.SnowflakeUtil.deconstruct(id).timestamp > import_discord3.SnowflakeUtil.epoch;
|
|
1817
|
+
} catch {
|
|
1818
|
+
return false;
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
__name(isSnowflake, "isSnowflake");
|
|
1822
|
+
function isMemberInstance(member) {
|
|
1823
|
+
return Boolean(member) && isSnowflake(member.id) && isSnowflake(member.guild?.id) && isSnowflake(member.user?.id) && member.id === member.user.id;
|
|
1824
|
+
}
|
|
1825
|
+
__name(isMemberInstance, "isMemberInstance");
|
|
1826
|
+
function isTextChannelInstance(channel) {
|
|
1827
|
+
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");
|
|
1828
|
+
}
|
|
1829
|
+
__name(isTextChannelInstance, "isTextChannelInstance");
|
|
1830
|
+
function isMessageInstance(message) {
|
|
1831
|
+
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;
|
|
1832
|
+
}
|
|
1833
|
+
__name(isMessageInstance, "isMessageInstance");
|
|
1834
|
+
function isSupportedVoiceChannel(channel) {
|
|
1835
|
+
return Boolean(channel) && isSnowflake(channel.id) && isSnowflake(channel.guildId || channel.guild?.id) && import_discord3.Constants.VoiceBasedChannelTypes.includes(channel.type);
|
|
1836
|
+
}
|
|
1837
|
+
__name(isSupportedVoiceChannel, "isSupportedVoiceChannel");
|
|
1838
|
+
function isGuildInstance(guild) {
|
|
1839
|
+
return Boolean(guild) && isSnowflake(guild.id) && isSnowflake(guild.ownerId) && typeof guild.name === "string";
|
|
1840
|
+
}
|
|
1841
|
+
__name(isGuildInstance, "isGuildInstance");
|
|
1842
|
+
function resolveGuildId(resolvable) {
|
|
1843
|
+
let guildId;
|
|
1844
|
+
if (typeof resolvable === "string") {
|
|
1845
|
+
guildId = resolvable;
|
|
1846
|
+
} else if (isObject(resolvable)) {
|
|
1847
|
+
if ("guildId" in resolvable && resolvable.guildId) {
|
|
1848
|
+
guildId = resolvable.guildId;
|
|
1849
|
+
} else if (resolvable instanceof Queue || resolvable instanceof DisTubeVoice || isGuildInstance(resolvable)) {
|
|
1850
|
+
guildId = resolvable.id;
|
|
1851
|
+
} else if ("guild" in resolvable && isGuildInstance(resolvable.guild)) {
|
|
1852
|
+
guildId = resolvable.guild.id;
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
if (!isSnowflake(guildId)) throw new DisTubeError("INVALID_TYPE", "GuildIdResolvable", resolvable);
|
|
1856
|
+
return guildId;
|
|
1857
|
+
}
|
|
1858
|
+
__name(resolveGuildId, "resolveGuildId");
|
|
1859
|
+
function isClientInstance(client) {
|
|
1860
|
+
return Boolean(client) && typeof client.login === "function";
|
|
1861
|
+
}
|
|
1862
|
+
__name(isClientInstance, "isClientInstance");
|
|
1863
|
+
function checkInvalidKey(target, source, sourceName) {
|
|
1864
|
+
if (!isObject(target)) throw new DisTubeError("INVALID_TYPE", "object", target, sourceName);
|
|
1865
|
+
const sourceKeys = Array.isArray(source) ? source : objectKeys(source);
|
|
1866
|
+
const invalidKey = objectKeys(target).find((key) => !sourceKeys.includes(key));
|
|
1867
|
+
if (invalidKey) throw new DisTubeError("INVALID_KEY", sourceName, invalidKey);
|
|
1868
|
+
}
|
|
1869
|
+
__name(checkInvalidKey, "checkInvalidKey");
|
|
1870
|
+
function isObject(obj) {
|
|
1871
|
+
return typeof obj === "object" && obj !== null && !Array.isArray(obj);
|
|
1872
|
+
}
|
|
1873
|
+
__name(isObject, "isObject");
|
|
1874
|
+
function objectKeys(obj) {
|
|
1875
|
+
if (!isObject(obj)) return [];
|
|
1876
|
+
return Object.keys(obj);
|
|
1877
|
+
}
|
|
1878
|
+
__name(objectKeys, "objectKeys");
|
|
1879
|
+
function isNsfwChannel(channel) {
|
|
1880
|
+
if (!isTextChannelInstance(channel)) return false;
|
|
1881
|
+
if (channel.isThread()) return channel.parent?.nsfw ?? false;
|
|
1882
|
+
return channel.nsfw;
|
|
1883
|
+
}
|
|
1884
|
+
__name(isNsfwChannel, "isNsfwChannel");
|
|
1885
|
+
var isTruthy = /* @__PURE__ */ __name((x) => Boolean(x), "isTruthy");
|
|
1886
|
+
function handleUnhandledError(error, behavior = "log") {
|
|
1887
|
+
if (behavior === "throw") throw error;
|
|
1888
|
+
if (behavior === "ignore") return;
|
|
1889
|
+
process.emitWarning(
|
|
1890
|
+
"The 'error' event was emitted but there is no listener for it. Add a listener with `distube.on(Events.ERROR, (error, queue, song) => { ... })` to handle DisTube errors, or set `unhandledError` to 'throw' to restore the default behavior.",
|
|
1891
|
+
{
|
|
1892
|
+
code: "DIS_TUBE_UNHANDLED_ERROR",
|
|
1893
|
+
detail: error?.stack ?? error?.message ?? String(error)
|
|
1894
|
+
}
|
|
1895
|
+
);
|
|
1896
|
+
}
|
|
1897
|
+
__name(handleUnhandledError, "handleUnhandledError");
|
|
1898
|
+
var checkEncryptionLibraries = /* @__PURE__ */ __name(async () => {
|
|
1899
|
+
if (await import("crypto").then((m) => m.getCiphers().includes("aes-256-gcm"))) return true;
|
|
1900
|
+
for (const lib of [
|
|
1901
|
+
"@noble/ciphers",
|
|
1902
|
+
"@stablelib/xchacha20poly1305",
|
|
1903
|
+
"sodium-native",
|
|
1904
|
+
"sodium",
|
|
1905
|
+
"libsodium-wrappers",
|
|
1906
|
+
"tweetnacl"
|
|
1907
|
+
]) {
|
|
1908
|
+
try {
|
|
1909
|
+
await import(lib);
|
|
1910
|
+
return true;
|
|
1911
|
+
} catch {
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
return false;
|
|
1915
|
+
}, "checkEncryptionLibraries");
|
|
1916
|
+
|
|
1917
|
+
// src/struct/Playlist.ts
|
|
1918
|
+
var Playlist = class {
|
|
1919
|
+
static {
|
|
1920
|
+
__name(this, "Playlist");
|
|
1921
|
+
}
|
|
1922
|
+
/**
|
|
1923
|
+
* Playlist source.
|
|
1924
|
+
*/
|
|
1925
|
+
source;
|
|
1926
|
+
/**
|
|
1927
|
+
* Songs in the playlist.
|
|
1928
|
+
*/
|
|
1929
|
+
songs;
|
|
1930
|
+
/**
|
|
1931
|
+
* Playlist ID.
|
|
1932
|
+
*/
|
|
1933
|
+
id;
|
|
1934
|
+
/**
|
|
1935
|
+
* Playlist name.
|
|
1936
|
+
*/
|
|
1937
|
+
name;
|
|
1938
|
+
/**
|
|
1939
|
+
* Playlist URL.
|
|
1940
|
+
*/
|
|
1941
|
+
url;
|
|
1942
|
+
/**
|
|
1943
|
+
* Playlist thumbnail.
|
|
1944
|
+
*/
|
|
1945
|
+
thumbnail;
|
|
1946
|
+
#metadata;
|
|
1947
|
+
#member;
|
|
1948
|
+
/**
|
|
1949
|
+
* Create a Playlist
|
|
1950
|
+
* @param playlist - Raw playlist info
|
|
1951
|
+
* @param options - Optional data
|
|
1952
|
+
* @param options.member - The user that requested the playlist
|
|
1953
|
+
* @param options.metadata - The metadata of the playlist
|
|
1954
|
+
* @throws {@link DisTubeError} When the playlist does not have any song
|
|
1955
|
+
*/
|
|
1956
|
+
constructor(playlist, { member, metadata } = {}) {
|
|
1957
|
+
if (!Array.isArray(playlist.songs) || !playlist.songs.length) throw new DisTubeError("EMPTY_PLAYLIST");
|
|
1958
|
+
this.source = playlist.source.toLowerCase();
|
|
1959
|
+
this.songs = playlist.songs;
|
|
1960
|
+
this.name = playlist.name;
|
|
1961
|
+
this.id = playlist.id;
|
|
1962
|
+
this.url = playlist.url;
|
|
1963
|
+
this.thumbnail = playlist.thumbnail;
|
|
1964
|
+
this.member = member;
|
|
1965
|
+
this.songs.forEach((s) => s.playlist = this);
|
|
1966
|
+
this.metadata = metadata;
|
|
1967
|
+
}
|
|
1968
|
+
/**
|
|
1969
|
+
* Playlist duration in second.
|
|
1970
|
+
*/
|
|
1971
|
+
get duration() {
|
|
1972
|
+
return this.songs.reduce((prev, next) => prev + next.duration, 0);
|
|
1973
|
+
}
|
|
1974
|
+
/**
|
|
1975
|
+
* Formatted duration string `hh:mm:ss`.
|
|
1976
|
+
*/
|
|
1977
|
+
get formattedDuration() {
|
|
1978
|
+
return formatDuration(this.duration);
|
|
1979
|
+
}
|
|
1980
|
+
/**
|
|
1981
|
+
* User requested.
|
|
1982
|
+
*/
|
|
1983
|
+
get member() {
|
|
1984
|
+
return this.#member;
|
|
1985
|
+
}
|
|
1986
|
+
/**
|
|
1987
|
+
* Set the user that requested the playlist.
|
|
1988
|
+
*
|
|
1989
|
+
* The value is ignored when it is not a valid guild member.
|
|
1990
|
+
* @param member - The member
|
|
1991
|
+
*/
|
|
1992
|
+
set member(member) {
|
|
1993
|
+
if (!isMemberInstance(member)) return;
|
|
1994
|
+
this.#member = member;
|
|
1995
|
+
this.songs.forEach((s) => s.member = this.member);
|
|
1996
|
+
}
|
|
1997
|
+
/**
|
|
1998
|
+
* User requested.
|
|
1999
|
+
*/
|
|
2000
|
+
get user() {
|
|
2001
|
+
return this.member?.user;
|
|
2002
|
+
}
|
|
2003
|
+
/**
|
|
2004
|
+
* Optional metadata that can be used to identify the playlist.
|
|
2005
|
+
*/
|
|
2006
|
+
get metadata() {
|
|
2007
|
+
return this.#metadata;
|
|
2008
|
+
}
|
|
2009
|
+
/**
|
|
2010
|
+
* Set the metadata of the playlist.
|
|
2011
|
+
*
|
|
2012
|
+
* The metadata of the songs is not changed when the metadata is `undefined`.
|
|
2013
|
+
* @param metadata - The metadata
|
|
2014
|
+
*/
|
|
2015
|
+
set metadata(metadata) {
|
|
2016
|
+
this.#metadata = metadata;
|
|
2017
|
+
if (metadata === void 0) return;
|
|
2018
|
+
this.songs.forEach((s) => s.metadata = metadata);
|
|
2019
|
+
}
|
|
2020
|
+
/**
|
|
2021
|
+
* The name of the playlist and the number of its songs.
|
|
2022
|
+
* @returns The string representation of the playlist
|
|
2023
|
+
*/
|
|
2024
|
+
toString() {
|
|
2025
|
+
return `${this.name} (${this.songs.length} songs)`;
|
|
2026
|
+
}
|
|
2027
|
+
};
|
|
2028
|
+
|
|
2029
|
+
// src/struct/Song.ts
|
|
2030
|
+
var Song = class {
|
|
2031
|
+
static {
|
|
2032
|
+
__name(this, "Song");
|
|
2033
|
+
}
|
|
2034
|
+
/**
|
|
2035
|
+
* The source of this song info
|
|
2036
|
+
*/
|
|
2037
|
+
source;
|
|
2038
|
+
/**
|
|
2039
|
+
* Song ID.
|
|
2040
|
+
*/
|
|
2041
|
+
id;
|
|
2042
|
+
/**
|
|
2043
|
+
* Song name.
|
|
2044
|
+
*/
|
|
2045
|
+
name;
|
|
2046
|
+
/**
|
|
2047
|
+
* Indicates if the song is an active live.
|
|
2048
|
+
*/
|
|
2049
|
+
isLive;
|
|
2050
|
+
/**
|
|
2051
|
+
* Song duration.
|
|
2052
|
+
*/
|
|
2053
|
+
duration;
|
|
2054
|
+
/**
|
|
2055
|
+
* Formatted duration string (`hh:mm:ss`, `mm:ss` or `Live`).
|
|
2056
|
+
*/
|
|
2057
|
+
formattedDuration;
|
|
2058
|
+
/**
|
|
2059
|
+
* Song URL.
|
|
2060
|
+
*/
|
|
2061
|
+
url;
|
|
2062
|
+
/**
|
|
2063
|
+
* Song thumbnail.
|
|
2064
|
+
*/
|
|
2065
|
+
thumbnail;
|
|
2066
|
+
/**
|
|
2067
|
+
* Song view count
|
|
2068
|
+
*/
|
|
2069
|
+
views;
|
|
2070
|
+
/**
|
|
2071
|
+
* Song like count
|
|
2072
|
+
*/
|
|
2073
|
+
likes;
|
|
2074
|
+
/**
|
|
2075
|
+
* Song dislike count
|
|
2076
|
+
*/
|
|
2077
|
+
dislikes;
|
|
2078
|
+
/**
|
|
2079
|
+
* Song repost (share) count
|
|
2080
|
+
*/
|
|
2081
|
+
reposts;
|
|
2082
|
+
/**
|
|
2083
|
+
* Song uploader
|
|
2084
|
+
*/
|
|
2085
|
+
uploader;
|
|
2086
|
+
/**
|
|
2087
|
+
* Whether or not an age-restricted content
|
|
2088
|
+
*/
|
|
2089
|
+
ageRestricted;
|
|
2090
|
+
/**
|
|
2091
|
+
* Stream info
|
|
2092
|
+
*/
|
|
2093
|
+
stream;
|
|
2094
|
+
/**
|
|
2095
|
+
* The plugin that created this song
|
|
2096
|
+
*/
|
|
2097
|
+
plugin;
|
|
2098
|
+
#metadata;
|
|
2099
|
+
#member;
|
|
2100
|
+
#playlist;
|
|
2101
|
+
/**
|
|
2102
|
+
* Create a Song
|
|
2103
|
+
*
|
|
2104
|
+
* @param info - Raw song info
|
|
2105
|
+
* @param options - Optional data
|
|
2106
|
+
* @param options.member - The user that requested the song
|
|
2107
|
+
* @param options.metadata - The metadata of the song
|
|
2108
|
+
*/
|
|
2109
|
+
constructor(info, { member, metadata } = {}) {
|
|
2110
|
+
this.source = info.source.toLowerCase();
|
|
2111
|
+
this.metadata = metadata ?? info.metadata;
|
|
2112
|
+
this.member = member;
|
|
2113
|
+
this.id = info.id;
|
|
2114
|
+
this.name = info.name;
|
|
2115
|
+
this.isLive = info.isLive;
|
|
2116
|
+
this.duration = this.isLive || !info.duration ? 0 : info.duration;
|
|
2117
|
+
this.formattedDuration = this.isLive ? "Live" : formatDuration(this.duration);
|
|
2118
|
+
this.url = info.url;
|
|
2119
|
+
this.thumbnail = info.thumbnail;
|
|
2120
|
+
this.views = info.views;
|
|
2121
|
+
this.likes = info.likes;
|
|
2122
|
+
this.dislikes = info.dislikes;
|
|
2123
|
+
this.reposts = info.reposts;
|
|
2124
|
+
this.uploader = {
|
|
2125
|
+
name: info.uploader?.name,
|
|
2126
|
+
url: info.uploader?.url
|
|
2127
|
+
};
|
|
2128
|
+
this.ageRestricted = info.ageRestricted;
|
|
2129
|
+
this.stream = { playFromSource: info.playFromSource && info.plugin?.playable !== false };
|
|
2130
|
+
this.plugin = info.plugin;
|
|
2131
|
+
}
|
|
2132
|
+
/**
|
|
2133
|
+
* The playlist this song belongs to
|
|
2134
|
+
*/
|
|
2135
|
+
get playlist() {
|
|
2136
|
+
return this.#playlist;
|
|
2137
|
+
}
|
|
2138
|
+
/**
|
|
2139
|
+
* Set the playlist this song belongs to.
|
|
2140
|
+
* @param playlist - The playlist
|
|
2141
|
+
* @throws {@link DisTubeError} When the value is not a {@link Playlist}
|
|
2142
|
+
*/
|
|
2143
|
+
set playlist(playlist) {
|
|
2144
|
+
if (!(playlist instanceof Playlist)) throw new DisTubeError("INVALID_TYPE", "Playlist", playlist, "Song#playlist");
|
|
2145
|
+
this.#playlist = playlist;
|
|
2146
|
+
this.member = playlist.member;
|
|
2147
|
+
}
|
|
2148
|
+
/**
|
|
2149
|
+
* User requested to play this song.
|
|
2150
|
+
*/
|
|
2151
|
+
get member() {
|
|
2152
|
+
return this.#member;
|
|
2153
|
+
}
|
|
2154
|
+
/**
|
|
2155
|
+
* Set the user that requested this song.
|
|
2156
|
+
*
|
|
2157
|
+
* The value is ignored when it is not a valid guild member.
|
|
2158
|
+
* @param member - The member
|
|
2159
|
+
*/
|
|
2160
|
+
set member(member) {
|
|
2161
|
+
if (isMemberInstance(member)) this.#member = member;
|
|
2162
|
+
}
|
|
2163
|
+
/**
|
|
2164
|
+
* User requested to play this song.
|
|
2165
|
+
*/
|
|
2166
|
+
get user() {
|
|
2167
|
+
return this.member?.user;
|
|
2168
|
+
}
|
|
2169
|
+
/**
|
|
2170
|
+
* Optional metadata that can be used to identify the song. This is attached by the
|
|
2171
|
+
* {@link DisTube#play} method.
|
|
2172
|
+
*/
|
|
2173
|
+
get metadata() {
|
|
2174
|
+
return this.#metadata;
|
|
2175
|
+
}
|
|
2176
|
+
/**
|
|
2177
|
+
* Set the metadata of the song.
|
|
2178
|
+
* @param metadata - The metadata
|
|
2179
|
+
*/
|
|
2180
|
+
set metadata(metadata) {
|
|
2181
|
+
this.#metadata = metadata;
|
|
2182
|
+
}
|
|
2183
|
+
/**
|
|
2184
|
+
* The name of the song, or its url, or its id.
|
|
2185
|
+
* @returns The string representation of the song
|
|
2186
|
+
*/
|
|
2187
|
+
toString() {
|
|
2188
|
+
return this.name || this.url || this.id || "Unknown";
|
|
2189
|
+
}
|
|
2190
|
+
};
|
|
2191
|
+
|
|
2192
|
+
// src/core/DisTubeHandler.ts
|
|
2193
|
+
var DisTubeHandler = class extends DisTubeBase {
|
|
2194
|
+
static {
|
|
2195
|
+
__name(this, "DisTubeHandler");
|
|
2196
|
+
}
|
|
2197
|
+
/**
|
|
2198
|
+
* Resolve a url or a supported object to a {@link Song} or {@link Playlist}
|
|
2199
|
+
* @throws {@link DisTubeError}
|
|
2200
|
+
* @param input - Resolvable input
|
|
2201
|
+
* @param options - Optional options
|
|
2202
|
+
* @returns Resolved
|
|
2203
|
+
*/
|
|
2204
|
+
async resolve(input, options = {}) {
|
|
2205
|
+
if (input instanceof Song || input instanceof Playlist) {
|
|
2206
|
+
if ("metadata" in options) input.metadata = options.metadata;
|
|
2207
|
+
if ("member" in options) input.member = options.member;
|
|
2208
|
+
return input;
|
|
2209
|
+
}
|
|
2210
|
+
if (typeof input === "string") {
|
|
2211
|
+
if (isURL(input)) {
|
|
2212
|
+
const plugin = await this._getPluginFromURL(input) || await this._getPluginFromURL(input = await this.followRedirectLink(input));
|
|
2213
|
+
if (!plugin) throw new DisTubeError("NOT_SUPPORTED_URL");
|
|
2214
|
+
this.debug(`[${plugin.constructor.name}] Resolving from url: ${input}`);
|
|
2215
|
+
return plugin.resolve(input, options);
|
|
2216
|
+
}
|
|
2217
|
+
try {
|
|
2218
|
+
const song = await this.#searchSong(input, options);
|
|
2219
|
+
if (song) return song;
|
|
2220
|
+
} catch (e) {
|
|
2221
|
+
const error = new DisTubeError("NO_RESULT", input);
|
|
2222
|
+
error.cause = e;
|
|
2223
|
+
throw error;
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
throw new DisTubeError("CANNOT_RESOLVE_SONG", input);
|
|
2227
|
+
}
|
|
2228
|
+
/**
|
|
2229
|
+
* Find the plugin that supports the given url.
|
|
2230
|
+
* @param url - The url to check
|
|
2231
|
+
* @returns The plugin, or `null` when there is none
|
|
2232
|
+
*/
|
|
2233
|
+
async _getPluginFromURL(url) {
|
|
2234
|
+
for (const plugin of this.plugins) if (await plugin.validate(url)) return plugin;
|
|
2235
|
+
return null;
|
|
2236
|
+
}
|
|
2237
|
+
async _getPluginFromSong(song, types, validate = true) {
|
|
2238
|
+
if (!types || types.includes(song.plugin?.type)) return song.plugin;
|
|
2239
|
+
if (!song.url) return null;
|
|
2240
|
+
for (const plugin of this.plugins) {
|
|
2241
|
+
if ((!types || types.includes(plugin?.type)) && (!validate || await plugin.validate(song.url))) {
|
|
2242
|
+
return plugin;
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
return null;
|
|
2246
|
+
}
|
|
2247
|
+
async #searchSong(query, options = {}, getStreamURL = false) {
|
|
2248
|
+
const source = options.source?.toLowerCase();
|
|
2249
|
+
const plugins = this.plugins.filter(
|
|
2250
|
+
(plugin) => plugin.type === "extractor" /* EXTRACTOR */ && (!source || plugin.identifiers.includes(source))
|
|
2251
|
+
);
|
|
2252
|
+
if (!plugins.length) throw new DisTubeError("NO_EXTRACTOR_PLUGIN");
|
|
2253
|
+
for (const plugin of plugins) {
|
|
2254
|
+
this.debug(`[${plugin.constructor.name}] Searching for song: ${query}`);
|
|
2255
|
+
const result = await plugin.searchSong(query, options);
|
|
2256
|
+
if (result) {
|
|
2257
|
+
if (getStreamURL && result.stream.playFromSource) result.stream.url = await plugin.getStreamURL(result);
|
|
2258
|
+
return result;
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
return null;
|
|
2262
|
+
}
|
|
2263
|
+
/**
|
|
2264
|
+
* Get {@link Song}'s stream info and attach it to the song.
|
|
2265
|
+
* @param song - A Song
|
|
2266
|
+
* @returns A promise that resolves when the stream info is attached
|
|
2267
|
+
* @throws {@link DisTubeError} When no plugin supports the song, or when its
|
|
2268
|
+
* stream URL cannot be found
|
|
2269
|
+
*/
|
|
2270
|
+
async attachStreamInfo(song) {
|
|
2271
|
+
if (song.stream.playFromSource) {
|
|
2272
|
+
if (song.stream.url) return;
|
|
2273
|
+
this.debug(`[DisTubeHandler] Getting stream info: ${song}`);
|
|
2274
|
+
const plugin = await this._getPluginFromSong(song, ["extractor" /* EXTRACTOR */, "playable-extractor" /* PLAYABLE_EXTRACTOR */]);
|
|
2275
|
+
if (!plugin) throw new DisTubeError("NOT_SUPPORTED_SONG", song.toString());
|
|
2276
|
+
this.debug(`[${plugin.constructor.name}] Getting stream URL: ${song}`);
|
|
2277
|
+
song.stream.url = await plugin.getStreamURL(song);
|
|
2278
|
+
if (!song.stream.url) throw new DisTubeError("CANNOT_GET_STREAM_URL", song.toString());
|
|
2279
|
+
} else {
|
|
2280
|
+
if (song.stream.song?.stream?.playFromSource && song.stream.song.stream.url) return;
|
|
2281
|
+
this.debug(`[DisTubeHandler] Getting stream info: ${song}`);
|
|
2282
|
+
const source = song.plugin;
|
|
2283
|
+
const query = source ? await source.createSearchQuery(song) : `${song.name ?? ""} ${song.uploader?.name ?? ""}`.trim();
|
|
2284
|
+
if (!query) throw new DisTubeError("CANNOT_GET_SEARCH_QUERY", song.toString());
|
|
2285
|
+
const altSong = await this.#findPlayableSong(query, song, source);
|
|
2286
|
+
song.stream.song = altSong;
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
/**
|
|
2290
|
+
* Look for a playable version of a song with the plugins that can search.
|
|
2291
|
+
*
|
|
2292
|
+
* Every searchable plugin is tried, in the order they were registered, until
|
|
2293
|
+
* one of them has a result.
|
|
2294
|
+
* @param query - The search query
|
|
2295
|
+
* @param song - The song that cannot be played
|
|
2296
|
+
* @param source - The plugin that resolved the song
|
|
2297
|
+
* @returns The playable song
|
|
2298
|
+
*/
|
|
2299
|
+
async #findPlayableSong(query, song, source) {
|
|
2300
|
+
const options = { metadata: song.metadata, member: song.member };
|
|
2301
|
+
for (const plugin of this.plugins) {
|
|
2302
|
+
if (plugin === source) continue;
|
|
2303
|
+
if (plugin.playable === false) continue;
|
|
2304
|
+
const result = await plugin.searchSong(query, options);
|
|
2305
|
+
if (!result) continue;
|
|
2306
|
+
if (!result.stream.playFromSource) continue;
|
|
2307
|
+
this.debug(`[${plugin.constructor.name}] Found a playable version of "${query}"`);
|
|
2308
|
+
result.stream.url = await plugin.getStreamURL(result);
|
|
2309
|
+
return result;
|
|
2310
|
+
}
|
|
2311
|
+
throw new DisTubeError("NO_RESULT", query);
|
|
2312
|
+
}
|
|
2313
|
+
/**
|
|
2314
|
+
* Follow the redirections of an url.
|
|
2315
|
+
* @param url - The url to follow
|
|
2316
|
+
* @param maxRedirect - The maximum number of redirections to follow
|
|
2317
|
+
* @returns The final url
|
|
2318
|
+
*/
|
|
2319
|
+
async followRedirectLink(url, maxRedirect = MAX_REDIRECT_DEPTH) {
|
|
2320
|
+
if (maxRedirect === 0) return url;
|
|
2321
|
+
const res = await (0, import_undici.request)(url, {
|
|
2322
|
+
method: "HEAD",
|
|
2323
|
+
headers: {
|
|
2324
|
+
"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"
|
|
2325
|
+
}
|
|
2326
|
+
});
|
|
2327
|
+
if (HTTP_REDIRECT_CODES.has(res.statusCode ?? 200)) {
|
|
2328
|
+
let location = res.headers.location;
|
|
2329
|
+
if (typeof location !== "string") location = location?.[0] ?? url;
|
|
2330
|
+
return this.followRedirectLink(location, --maxRedirect);
|
|
2331
|
+
}
|
|
2332
|
+
return url;
|
|
2333
|
+
}
|
|
2334
|
+
};
|
|
2335
|
+
|
|
2336
|
+
// src/core/DisTubeOptions.ts
|
|
2337
|
+
var UNHANDLED_ERROR_BEHAVIORS = ["log", "throw", "ignore"];
|
|
2338
|
+
var Options = class {
|
|
2339
|
+
static {
|
|
2340
|
+
__name(this, "Options");
|
|
2341
|
+
}
|
|
2342
|
+
/**
|
|
2343
|
+
* The plugins of the DisTube instance.
|
|
2344
|
+
*/
|
|
2345
|
+
plugins;
|
|
2346
|
+
/**
|
|
2347
|
+
* Whether or not the {@link Events.PLAY_SONG} event is emitted when looping a
|
|
2348
|
+
* song or when the next song is the same as the previous one.
|
|
2349
|
+
* @default false
|
|
2350
|
+
*/
|
|
2351
|
+
emitNewSongOnly;
|
|
2352
|
+
/**
|
|
2353
|
+
* Whether or not the previous songs of a {@link Queue} are saved.
|
|
2354
|
+
* @default true
|
|
2355
|
+
*/
|
|
2356
|
+
savePreviousSongs;
|
|
2357
|
+
/**
|
|
2358
|
+
* The custom FFmpeg audio filters, that override the {@link defaultFilters}.
|
|
2359
|
+
*/
|
|
2360
|
+
customFilters;
|
|
2361
|
+
/**
|
|
2362
|
+
* Whether or not age-restricted content can be played in a non-NSFW channel.
|
|
2363
|
+
* @default false
|
|
2364
|
+
*/
|
|
2365
|
+
nsfw;
|
|
2366
|
+
/**
|
|
2367
|
+
* Whether or not the {@link Events.ADD_SONG} event is emitted when a new
|
|
2368
|
+
* {@link Queue} is created.
|
|
2369
|
+
* @default true
|
|
2370
|
+
*/
|
|
2371
|
+
emitAddSongWhenCreatingQueue;
|
|
2372
|
+
/**
|
|
2373
|
+
* Whether or not the {@link Events.ADD_LIST} event is emitted when a new
|
|
2374
|
+
* {@link Queue} is created.
|
|
2375
|
+
* @default true
|
|
2376
|
+
*/
|
|
2377
|
+
emitAddListWhenCreatingQueue;
|
|
2378
|
+
/**
|
|
2379
|
+
* Whether or not the bot joins the new voice channel when
|
|
2380
|
+
* {@link DisTube#play} is used.
|
|
2381
|
+
* @default true
|
|
2382
|
+
*/
|
|
2383
|
+
joinNewVoiceChannel;
|
|
2384
|
+
/**
|
|
2385
|
+
* The behavior of the {@link Events.ERROR} event when it has no listener.
|
|
2386
|
+
* @default "log"
|
|
2387
|
+
*/
|
|
2388
|
+
unhandledError;
|
|
2389
|
+
/**
|
|
2390
|
+
* The time (in milliseconds) a track may stay in the "buffering" state before
|
|
2391
|
+
* it is considered stuck and gets skipped.
|
|
2392
|
+
*
|
|
2393
|
+
* Set to `0` to disable the stuck-track detection.
|
|
2394
|
+
* @default 10000
|
|
2395
|
+
*/
|
|
2396
|
+
stuckThreshold;
|
|
2397
|
+
/**
|
|
2398
|
+
* The FFmpeg options of the DisTube instance.
|
|
2399
|
+
*/
|
|
2400
|
+
ffmpeg;
|
|
2401
|
+
/**
|
|
2402
|
+
* Create the options of a {@link DisTube} instance.
|
|
2403
|
+
* @param options - The options given by the user
|
|
2404
|
+
* @throws {@link DisTubeError} When an option has an invalid type, or when it
|
|
2405
|
+
* is not a known option
|
|
2406
|
+
*/
|
|
2407
|
+
constructor(options) {
|
|
2408
|
+
if (typeof options !== "object" || Array.isArray(options)) {
|
|
2409
|
+
throw new DisTubeError("INVALID_TYPE", "object", options, "DisTubeOptions");
|
|
2410
|
+
}
|
|
2411
|
+
const opts = { ...defaultOptions, ...options };
|
|
2412
|
+
this.plugins = opts.plugins;
|
|
2413
|
+
this.emitNewSongOnly = opts.emitNewSongOnly;
|
|
2414
|
+
this.savePreviousSongs = opts.savePreviousSongs;
|
|
2415
|
+
this.customFilters = opts.customFilters;
|
|
2416
|
+
this.nsfw = opts.nsfw;
|
|
2417
|
+
this.emitAddSongWhenCreatingQueue = opts.emitAddSongWhenCreatingQueue;
|
|
2418
|
+
this.emitAddListWhenCreatingQueue = opts.emitAddListWhenCreatingQueue;
|
|
2419
|
+
this.joinNewVoiceChannel = opts.joinNewVoiceChannel;
|
|
2420
|
+
this.unhandledError = opts.unhandledError;
|
|
2421
|
+
this.stuckThreshold = opts.stuckThreshold;
|
|
2422
|
+
this.ffmpeg = this.#ffmpegOption(options);
|
|
2423
|
+
checkInvalidKey(opts, this, "DisTubeOptions");
|
|
2424
|
+
this.#validateOptions();
|
|
2425
|
+
}
|
|
2426
|
+
#validateOptions(options = this) {
|
|
2427
|
+
const booleanOptions = /* @__PURE__ */ new Set([
|
|
2428
|
+
"emitNewSongOnly",
|
|
2429
|
+
"savePreviousSongs",
|
|
2430
|
+
"joinNewVoiceChannel",
|
|
2431
|
+
"nsfw",
|
|
2432
|
+
"emitAddSongWhenCreatingQueue",
|
|
2433
|
+
"emitAddListWhenCreatingQueue"
|
|
2434
|
+
]);
|
|
2435
|
+
const numberOptions = /* @__PURE__ */ new Set(["stuckThreshold"]);
|
|
2436
|
+
const stringOptions = /* @__PURE__ */ new Set(["unhandledError"]);
|
|
2437
|
+
const objectOptions = /* @__PURE__ */ new Set(["customFilters", "ffmpeg"]);
|
|
2438
|
+
const optionalOptions = /* @__PURE__ */ new Set(["customFilters"]);
|
|
2439
|
+
for (const [key, value] of Object.entries(options)) {
|
|
2440
|
+
if (value === void 0 && optionalOptions.has(key)) continue;
|
|
2441
|
+
if (key === "plugins" && !Array.isArray(value)) {
|
|
2442
|
+
throw new DisTubeError("INVALID_TYPE", "Array<Plugin>", value, `DisTubeOptions.${key}`);
|
|
2443
|
+
} else if (booleanOptions.has(key)) {
|
|
2444
|
+
if (typeof value !== "boolean") {
|
|
2445
|
+
throw new DisTubeError("INVALID_TYPE", "boolean", value, `DisTubeOptions.${key}`);
|
|
2446
|
+
}
|
|
2447
|
+
} else if (numberOptions.has(key)) {
|
|
2448
|
+
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
2449
|
+
throw new DisTubeError("INVALID_TYPE", "number", value, `DisTubeOptions.${key}`);
|
|
2450
|
+
}
|
|
2451
|
+
} else if (stringOptions.has(key)) {
|
|
2452
|
+
if (typeof value !== "string") {
|
|
2453
|
+
throw new DisTubeError("INVALID_TYPE", "string", value, `DisTubeOptions.${key}`);
|
|
2454
|
+
}
|
|
2455
|
+
if (!UNHANDLED_ERROR_BEHAVIORS.includes(value)) {
|
|
2456
|
+
throw new DisTubeError("INVALID_TYPE", UNHANDLED_ERROR_BEHAVIORS, value, `DisTubeOptions.${key}`);
|
|
2457
|
+
}
|
|
2458
|
+
} else if (objectOptions.has(key)) {
|
|
2459
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
2460
|
+
throw new DisTubeError("INVALID_TYPE", "object", value, `DisTubeOptions.${key}`);
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
#ffmpegOption(opts) {
|
|
2466
|
+
const args = { global: {}, input: {}, output: {} };
|
|
2467
|
+
if (opts.ffmpeg?.args) {
|
|
2468
|
+
if (opts.ffmpeg.args.global) args.global = opts.ffmpeg.args.global;
|
|
2469
|
+
if (opts.ffmpeg.args.input) args.input = opts.ffmpeg.args.input;
|
|
2470
|
+
if (opts.ffmpeg.args.output) args.output = opts.ffmpeg.args.output;
|
|
2471
|
+
}
|
|
2472
|
+
const path = opts.ffmpeg?.path ?? "ffmpeg";
|
|
2473
|
+
if (typeof path !== "string") {
|
|
2474
|
+
throw new DisTubeError("INVALID_TYPE", "string", path, "DisTubeOptions.ffmpeg.path");
|
|
2475
|
+
}
|
|
2476
|
+
for (const [key, value] of Object.entries(args)) {
|
|
2477
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
2478
|
+
throw new DisTubeError("INVALID_TYPE", "object", value, `DisTubeOptions.ffmpeg.${key}`);
|
|
2479
|
+
}
|
|
2480
|
+
for (const [k, v] of Object.entries(value)) {
|
|
2481
|
+
if (typeof v !== "string" && typeof v !== "number" && typeof v !== "boolean" && !Array.isArray(v) && v !== null && v !== void 0) {
|
|
2482
|
+
throw new DisTubeError(
|
|
2483
|
+
"INVALID_TYPE",
|
|
2484
|
+
["string", "number", "boolean", "Array<string | null | undefined>", "null", "undefined"],
|
|
2485
|
+
v,
|
|
2486
|
+
`DisTubeOptions.ffmpeg.${key}.${k}`
|
|
2487
|
+
);
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
return { path, args };
|
|
2492
|
+
}
|
|
2493
|
+
};
|
|
2494
|
+
|
|
2495
|
+
// src/core/DisTubeStream.ts
|
|
2496
|
+
var import_node_child_process = require("child_process");
|
|
2497
|
+
var import_node_stream = require("stream");
|
|
2498
|
+
var import_voice2 = require("@discordjs/voice");
|
|
2499
|
+
var import_tiny_typed_emitter2 = require("tiny-typed-emitter");
|
|
2500
|
+
var checked = process.env.NODE_ENV === "test";
|
|
2501
|
+
var checkFFmpeg = /* @__PURE__ */ __name((distube) => {
|
|
2502
|
+
if (checked) return;
|
|
2503
|
+
const path = distube.options.ffmpeg.path;
|
|
2504
|
+
const debug = /* @__PURE__ */ __name((str) => distube.emit("ffmpegDebug" /* FFMPEG_DEBUG */, str), "debug");
|
|
2505
|
+
try {
|
|
2506
|
+
debug(`[test] spawn ffmpeg at '${path}' path`);
|
|
2507
|
+
const process2 = (0, import_node_child_process.spawnSync)(path, ["-h"], {
|
|
2508
|
+
windowsHide: true,
|
|
2509
|
+
encoding: "utf-8"
|
|
2510
|
+
});
|
|
2511
|
+
if (process2.error) throw process2.error;
|
|
2512
|
+
if (process2.stderr && !process2.stdout) throw new Error(process2.stderr);
|
|
2513
|
+
const result = process2.output.join("\n");
|
|
2514
|
+
const version2 = /ffmpeg version (\S+)/iu.exec(result)?.[1];
|
|
2515
|
+
if (!version2) throw new Error("Invalid FFmpeg version");
|
|
2516
|
+
debug(`[test] ffmpeg version: ${version2}`);
|
|
2517
|
+
} catch (e) {
|
|
2518
|
+
const errorMessage = e instanceof Error ? e.stack ?? e.message : String(e);
|
|
2519
|
+
debug(`[test] failed to spawn ffmpeg at '${path}': ${errorMessage}`);
|
|
2520
|
+
throw new DisTubeError("FFMPEG_NOT_INSTALLED", path);
|
|
2521
|
+
}
|
|
2522
|
+
checked = true;
|
|
2523
|
+
}, "checkFFmpeg");
|
|
2524
|
+
var DisTubeStream = class extends import_tiny_typed_emitter2.TypedEmitter {
|
|
2525
|
+
static {
|
|
2526
|
+
__name(this, "DisTubeStream");
|
|
2527
|
+
}
|
|
2528
|
+
#ffmpegPath;
|
|
2529
|
+
#opts;
|
|
2530
|
+
/**
|
|
2531
|
+
* The FFmpeg process that decodes the audio.
|
|
2532
|
+
*
|
|
2533
|
+
* It is `undefined` until {@link DisTubeStream#spawn} is called.
|
|
2534
|
+
*/
|
|
2535
|
+
process;
|
|
2536
|
+
/**
|
|
2537
|
+
* The stream of the decoded audio, with the volume applied to it.
|
|
2538
|
+
*/
|
|
2539
|
+
stream;
|
|
2540
|
+
/**
|
|
2541
|
+
* The Discord.js audio resource that is played by the audio player.
|
|
2542
|
+
*/
|
|
2543
|
+
audioResource;
|
|
2544
|
+
/**
|
|
2545
|
+
* The seek time in seconds that this stream started from
|
|
2546
|
+
*/
|
|
2547
|
+
seekTime;
|
|
2548
|
+
/**
|
|
2549
|
+
* Create a DisTubeStream to play with {@link DisTubeVoice}
|
|
2550
|
+
* @param url - Stream URL
|
|
2551
|
+
* @param options - Stream options
|
|
2552
|
+
*/
|
|
2553
|
+
constructor(url, options) {
|
|
2554
|
+
super();
|
|
2555
|
+
const { ffmpeg, seek } = options;
|
|
2556
|
+
this.seekTime = typeof seek === "number" && seek > 0 ? seek : 0;
|
|
2557
|
+
const opts = {
|
|
2558
|
+
reconnect: 1,
|
|
2559
|
+
reconnect_streamed: 1,
|
|
2560
|
+
reconnect_delay_max: 5,
|
|
2561
|
+
analyzeduration: 0,
|
|
2562
|
+
hide_banner: true,
|
|
2563
|
+
...ffmpeg.args.global,
|
|
2564
|
+
...ffmpeg.args.input,
|
|
2565
|
+
i: url,
|
|
2566
|
+
ar: AUDIO_SAMPLE_RATE,
|
|
2567
|
+
ac: AUDIO_CHANNELS,
|
|
2568
|
+
...ffmpeg.args.output,
|
|
2569
|
+
f: "s16le"
|
|
2570
|
+
};
|
|
2571
|
+
if (typeof seek === "number" && seek > 0) opts.ss = seek.toString();
|
|
2572
|
+
const fileUrl = new URL(url);
|
|
2573
|
+
if (fileUrl.protocol === "file:") {
|
|
2574
|
+
opts.reconnect = null;
|
|
2575
|
+
opts.reconnect_streamed = null;
|
|
2576
|
+
opts.reconnect_delay_max = null;
|
|
2577
|
+
opts.i = fileUrl.hostname + fileUrl.pathname;
|
|
2578
|
+
}
|
|
2579
|
+
this.#ffmpegPath = ffmpeg.path;
|
|
2580
|
+
this.#opts = [
|
|
2581
|
+
...Object.entries(opts).flatMap(
|
|
2582
|
+
([key, value]) => Array.isArray(value) ? value.filter(Boolean).map((v) => [`-${key}`, String(v)]) : value == null || value === false ? [] : [value === true ? `-${key}` : [`-${key}`, String(value)]]
|
|
2583
|
+
).flat(),
|
|
2584
|
+
"pipe:1"
|
|
2585
|
+
];
|
|
2586
|
+
this.stream = new VolumeTransformer();
|
|
2587
|
+
this.stream.on("close", () => this.kill()).on("error", (err) => {
|
|
2588
|
+
this.debug(`[stream] error: ${err.message}`);
|
|
2589
|
+
this.emit("error", err);
|
|
2590
|
+
}).on("finish", () => this.debug("[stream] log: stream finished"));
|
|
2591
|
+
this.audioResource = (0, import_voice2.createAudioResource)(this.stream, {
|
|
2592
|
+
inputType: import_voice2.StreamType.Raw,
|
|
2593
|
+
inlineVolume: false
|
|
2594
|
+
});
|
|
2595
|
+
}
|
|
2596
|
+
/**
|
|
2597
|
+
* Spawn the FFmpeg process that decodes the audio.
|
|
2598
|
+
* @returns Nothing
|
|
2599
|
+
* @throws {Error} When the FFmpeg process cannot be created
|
|
2600
|
+
*/
|
|
2601
|
+
spawn() {
|
|
2602
|
+
this.debug(`[process] spawn: ${this.#ffmpegPath} ${this.#opts.join(" ")}`);
|
|
2603
|
+
this.process = (0, import_node_child_process.spawn)(this.#ffmpegPath, this.#opts, {
|
|
2604
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2605
|
+
shell: false,
|
|
2606
|
+
windowsHide: true
|
|
2607
|
+
}).on("error", (err) => {
|
|
2608
|
+
this.debug(`[process] error: ${err.message}`);
|
|
2609
|
+
this.emit("error", err);
|
|
2610
|
+
}).on("exit", (code, signal) => {
|
|
2611
|
+
this.debug(`[process] exit: code=${code ?? "unknown"} signal=${signal ?? "unknown"}`);
|
|
2612
|
+
if (!code || [0, 255].includes(code)) return;
|
|
2613
|
+
this.debug(`[process] error: ffmpeg exited with code ${code}`);
|
|
2614
|
+
this.emit("error", new DisTubeError("FFMPEG_EXITED", code));
|
|
2615
|
+
});
|
|
2616
|
+
if (!this.process.stdout || !this.process.stderr) {
|
|
2617
|
+
this.kill();
|
|
2618
|
+
throw new Error("Failed to create ffmpeg process");
|
|
2619
|
+
}
|
|
2620
|
+
this.process.stdout.pipe(this.stream);
|
|
2621
|
+
this.process.stderr.setEncoding("utf8")?.on("data", (data) => {
|
|
2622
|
+
const lines = data.split(/\r\n|\r|\n/u);
|
|
2623
|
+
for (const line of lines) {
|
|
2624
|
+
if (/^\s*$/.test(line)) continue;
|
|
2625
|
+
this.debug(`[ffmpeg] log: ${line}`);
|
|
2626
|
+
}
|
|
2627
|
+
});
|
|
2628
|
+
}
|
|
2629
|
+
debug(debug) {
|
|
2630
|
+
this.emit("debug", debug);
|
|
2631
|
+
}
|
|
2632
|
+
/**
|
|
2633
|
+
* Set the volume of the stream.
|
|
2634
|
+
* @param volume - The volume of the stream, between `0` and `1`
|
|
2635
|
+
* @returns Nothing
|
|
2636
|
+
*/
|
|
2637
|
+
setVolume(volume) {
|
|
2638
|
+
this.stream.vol = volume;
|
|
2639
|
+
}
|
|
2640
|
+
/**
|
|
2641
|
+
* Kill the FFmpeg process and destroy the stream.
|
|
2642
|
+
* @returns Nothing
|
|
2643
|
+
*/
|
|
2644
|
+
kill() {
|
|
2645
|
+
if (!this.stream.destroyed) this.stream.destroy();
|
|
2646
|
+
if (this.process && !this.process.killed) this.process.kill("SIGKILL");
|
|
2647
|
+
}
|
|
2648
|
+
};
|
|
2649
|
+
var VolumeTransformer = class extends import_node_stream.Transform {
|
|
2650
|
+
static {
|
|
2651
|
+
__name(this, "VolumeTransformer");
|
|
2652
|
+
}
|
|
2653
|
+
buffer = Buffer.allocUnsafe(0);
|
|
2654
|
+
extrema = [-(2 ** (16 - 1)), 2 ** (16 - 1) - 1];
|
|
2655
|
+
vol = 1;
|
|
2656
|
+
_transform(newChunk, _encoding, done) {
|
|
2657
|
+
const { vol } = this;
|
|
2658
|
+
if (vol === 1) {
|
|
2659
|
+
this.push(newChunk);
|
|
2660
|
+
done();
|
|
2661
|
+
return;
|
|
2662
|
+
}
|
|
2663
|
+
const bytes = 2;
|
|
2664
|
+
const chunk = Buffer.concat([this.buffer, newChunk]);
|
|
2665
|
+
const readableLength = Math.floor(chunk.length / bytes) * bytes;
|
|
2666
|
+
for (let i = 0; i < readableLength; i += bytes) {
|
|
2667
|
+
const value = chunk.readInt16LE(i);
|
|
2668
|
+
const clampedValue = Math.min(this.extrema[1], Math.max(this.extrema[0], value * vol));
|
|
2669
|
+
chunk.writeInt16LE(clampedValue, i);
|
|
2670
|
+
}
|
|
2671
|
+
this.buffer = chunk.subarray(readableLength);
|
|
2672
|
+
this.push(chunk.subarray(0, readableLength));
|
|
2673
|
+
done();
|
|
2674
|
+
}
|
|
2675
|
+
};
|
|
2676
|
+
|
|
2677
|
+
// src/core/manager/DisTubeVoiceManager.ts
|
|
2678
|
+
var import_voice3 = require("@discordjs/voice");
|
|
2679
|
+
|
|
2680
|
+
// src/core/manager/GuildIdManager.ts
|
|
2681
|
+
var GuildIdManager = class extends BaseManager {
|
|
2682
|
+
static {
|
|
2683
|
+
__name(this, "GuildIdManager");
|
|
2684
|
+
}
|
|
2685
|
+
/**
|
|
2686
|
+
* Add an item to the collection.
|
|
2687
|
+
*
|
|
2688
|
+
* The item is not added when there is already one for the same guild.
|
|
2689
|
+
* @param idOrInstance - The guild id, or anything that can be resolved to a guild id
|
|
2690
|
+
* @param data - The item to add
|
|
2691
|
+
* @returns This manager, to allow chaining
|
|
2692
|
+
* @throws {@link DisTubeError} When the value cannot be resolved to a guild id
|
|
2693
|
+
*/
|
|
2694
|
+
add(idOrInstance, data) {
|
|
2695
|
+
const id = resolveGuildId(idOrInstance);
|
|
2696
|
+
const existing = this.get(id);
|
|
2697
|
+
if (existing) return this;
|
|
2698
|
+
this.collection.set(id, data);
|
|
2699
|
+
return this;
|
|
2700
|
+
}
|
|
2701
|
+
/**
|
|
2702
|
+
* Get the item of a guild.
|
|
2703
|
+
* @param idOrInstance - The guild id, or anything that can be resolved to a guild id
|
|
2704
|
+
* @returns The item of the guild, or `undefined` when there is none
|
|
2705
|
+
* @throws {@link DisTubeError} When the value cannot be resolved to a guild id
|
|
2706
|
+
*/
|
|
2707
|
+
get(idOrInstance) {
|
|
2708
|
+
return this.collection.get(resolveGuildId(idOrInstance));
|
|
2709
|
+
}
|
|
2710
|
+
/**
|
|
2711
|
+
* Remove the item of a guild.
|
|
2712
|
+
* @param idOrInstance - The guild id, or anything that can be resolved to a guild id
|
|
2713
|
+
* @returns `true` when an item was removed, `false` otherwise
|
|
2714
|
+
* @throws {@link DisTubeError} When the value cannot be resolved to a guild id
|
|
2715
|
+
*/
|
|
2716
|
+
remove(idOrInstance) {
|
|
2717
|
+
return this.collection.delete(resolveGuildId(idOrInstance));
|
|
2718
|
+
}
|
|
2719
|
+
/**
|
|
2720
|
+
* Check whether a guild has an item.
|
|
2721
|
+
* @param idOrInstance - The guild id, or anything that can be resolved to a guild id
|
|
2722
|
+
* @returns `true` when there is an item for the guild
|
|
2723
|
+
* @throws {@link DisTubeError} When the value cannot be resolved to a guild id
|
|
2724
|
+
*/
|
|
2725
|
+
has(idOrInstance) {
|
|
2726
|
+
return this.collection.has(resolveGuildId(idOrInstance));
|
|
2727
|
+
}
|
|
2728
|
+
};
|
|
2729
|
+
|
|
2730
|
+
// src/core/manager/DisTubeVoiceManager.ts
|
|
2731
|
+
var DisTubeVoiceManager = class extends GuildIdManager {
|
|
2732
|
+
static {
|
|
2733
|
+
__name(this, "DisTubeVoiceManager");
|
|
2734
|
+
}
|
|
2735
|
+
/**
|
|
2736
|
+
* Create a {@link DisTubeVoice} instance
|
|
2737
|
+
* @param channel - A voice channel to join
|
|
2738
|
+
* @returns The voice connection of the guild
|
|
2739
|
+
* @throws {@link DisTubeError} When the guild already has a voice connection
|
|
2740
|
+
* that is not managed by DisTube
|
|
2741
|
+
*/
|
|
2742
|
+
create(channel) {
|
|
2743
|
+
const existing = this.get(channel.guildId);
|
|
2744
|
+
if (existing) {
|
|
2745
|
+
existing.channel = channel;
|
|
2746
|
+
return existing;
|
|
2747
|
+
}
|
|
2748
|
+
if ((0, import_voice3.getVoiceConnection)(resolveGuildId(channel), this.client.user?.id) || (0, import_voice3.getVoiceConnection)(resolveGuildId(channel))) {
|
|
2749
|
+
throw new DisTubeError("VOICE_ALREADY_CREATED");
|
|
2750
|
+
}
|
|
2751
|
+
return new DisTubeVoice(this, channel);
|
|
2752
|
+
}
|
|
2753
|
+
/**
|
|
2754
|
+
* Join a voice channel and wait until the connection is ready
|
|
2755
|
+
* @param channel - A voice channel to join
|
|
2756
|
+
* @returns The voice connection of the guild
|
|
2757
|
+
* @throws {@link DisTubeError} When the connection cannot be established
|
|
2758
|
+
*/
|
|
2759
|
+
join(channel) {
|
|
2760
|
+
const existing = this.get(channel.guildId);
|
|
2761
|
+
if (existing) return existing.join(channel);
|
|
2762
|
+
return this.create(channel).join();
|
|
2763
|
+
}
|
|
2764
|
+
/**
|
|
2765
|
+
* Leave the connected voice channel in a guild
|
|
2766
|
+
* @param guild - Queue Resolvable
|
|
2767
|
+
* @returns Nothing
|
|
2768
|
+
*/
|
|
2769
|
+
leave(guild) {
|
|
2770
|
+
const voice = this.get(guild);
|
|
2771
|
+
if (voice) {
|
|
2772
|
+
voice.leave();
|
|
2773
|
+
} else {
|
|
2774
|
+
const connection = (0, import_voice3.getVoiceConnection)(resolveGuildId(guild), this.client.user?.id) ?? (0, import_voice3.getVoiceConnection)(resolveGuildId(guild));
|
|
2775
|
+
if (connection && connection.state.status !== import_voice3.VoiceConnectionStatus.Destroyed) {
|
|
2776
|
+
connection.destroy();
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
};
|
|
2781
|
+
|
|
2782
|
+
// src/core/manager/QueueManager.ts
|
|
2783
|
+
var QueueManager = class extends GuildIdManager {
|
|
2784
|
+
static {
|
|
2785
|
+
__name(this, "QueueManager");
|
|
2786
|
+
}
|
|
2787
|
+
/**
|
|
2788
|
+
* Create a {@link Queue}
|
|
2789
|
+
* @param channel - A voice channel
|
|
2790
|
+
* @param textChannel - Default text channel
|
|
2791
|
+
* @returns The created queue
|
|
2792
|
+
* @throws {@link DisTubeError} When the guild already has a queue, or when
|
|
2793
|
+
* the voice channel cannot be joined
|
|
2794
|
+
*/
|
|
2795
|
+
async create(channel, textChannel) {
|
|
2796
|
+
if (this.has(channel.guildId)) throw new DisTubeError("QUEUE_EXIST");
|
|
2797
|
+
this.debug(`[QueueManager] Creating queue for guild: ${channel.guildId}`);
|
|
2798
|
+
const voice = this.voices.create(channel);
|
|
2799
|
+
const queue = new Queue(this.distube, voice, textChannel);
|
|
2800
|
+
await queue._taskQueue.queuing();
|
|
2801
|
+
try {
|
|
2802
|
+
checkFFmpeg(this.distube);
|
|
2803
|
+
this.debug(`[QueueManager] Joining voice channel: ${channel.id}`);
|
|
2804
|
+
await voice.join();
|
|
2805
|
+
this.#voiceEventHandler(queue);
|
|
2806
|
+
this.add(queue.id, queue);
|
|
2807
|
+
this.emit("initQueue" /* INIT_QUEUE */, queue);
|
|
2808
|
+
return queue;
|
|
2809
|
+
} finally {
|
|
2810
|
+
queue._taskQueue.resolve();
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
/**
|
|
2814
|
+
* Listen to DisTubeVoice events and handle the Queue
|
|
2815
|
+
* @param queue - Queue
|
|
2816
|
+
*/
|
|
2817
|
+
#voiceEventHandler(queue) {
|
|
2818
|
+
queue._listeners = {
|
|
2819
|
+
disconnect: /* @__PURE__ */ __name((error) => {
|
|
2820
|
+
queue.remove();
|
|
2821
|
+
this.emit("disconnect" /* DISCONNECT */, queue);
|
|
2822
|
+
if (error) this.emitError(error, queue, queue.songs?.[0]);
|
|
2823
|
+
}, "disconnect"),
|
|
2824
|
+
error: /* @__PURE__ */ __name((error) => void this.#handlePlayingError(queue, error), "error"),
|
|
2825
|
+
finish: /* @__PURE__ */ __name(() => void this.handleSongFinish(queue), "finish")
|
|
2826
|
+
};
|
|
2827
|
+
for (const event of objectKeys(queue._listeners)) {
|
|
2828
|
+
queue.voice.on(event, queue._listeners[event]);
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
/**
|
|
2832
|
+
* Handle the queue when a Song finish
|
|
2833
|
+
* @param queue - queue
|
|
2834
|
+
* @returns A promise that resolves when the next song is playing
|
|
2835
|
+
*/
|
|
2836
|
+
async handleSongFinish(queue) {
|
|
2837
|
+
if (queue._manualUpdate) {
|
|
2838
|
+
queue._manualUpdate = false;
|
|
2839
|
+
await this.playSong(queue);
|
|
2840
|
+
return;
|
|
2841
|
+
}
|
|
2842
|
+
this.debug(`[QueueManager] Handling song finish: ${queue.id}`);
|
|
2843
|
+
const song = queue.songs[0];
|
|
2844
|
+
this.emit("finishSong" /* FINISH_SONG */, queue, song);
|
|
2845
|
+
await queue._taskQueue.queuing();
|
|
2846
|
+
try {
|
|
2847
|
+
if (queue.stopped) return;
|
|
2848
|
+
if (queue.repeatMode === 2 /* QUEUE */) queue.songs.push(song);
|
|
2849
|
+
if (queue.repeatMode !== 1 /* SONG */) {
|
|
2850
|
+
const prev = queue.songs.shift();
|
|
2851
|
+
if (this.options.savePreviousSongs) queue.previousSongs.push(prev);
|
|
2852
|
+
else queue.previousSongs.push({ id: prev.id });
|
|
2853
|
+
}
|
|
2854
|
+
if (queue.songs.length === 0 && queue.autoplay) {
|
|
2855
|
+
try {
|
|
2856
|
+
this.debug(`[QueueManager] Adding related song: ${queue.id}`);
|
|
2857
|
+
await queue._addRelatedSong(song);
|
|
2858
|
+
} catch (e) {
|
|
2859
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
2860
|
+
this.debug(`[${queue.id}] Add related song error: ${errorMessage}`);
|
|
2861
|
+
if (e instanceof DisTubeError) {
|
|
2862
|
+
this.emit("noRelated" /* NO_RELATED */, queue, e);
|
|
2863
|
+
} else {
|
|
2864
|
+
this.emit("noRelated" /* NO_RELATED */, queue, new DisTubeError("NO_RELATED"));
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
if (queue.songs.length === 0) {
|
|
2869
|
+
this.debug(`[${queue.id}] Queue is empty, stopping...`);
|
|
2870
|
+
this.emit("empty" /* EMPTY */, queue);
|
|
2871
|
+
if (!queue.autoplay) this.emit("finish" /* FINISH */, queue);
|
|
2872
|
+
queue.remove();
|
|
2873
|
+
return;
|
|
2874
|
+
}
|
|
2875
|
+
if (song !== queue.songs[0]) {
|
|
2876
|
+
const playedSong = song.stream.playFromSource ? song : song.stream.song;
|
|
2877
|
+
if (playedSong?.stream.playFromSource) delete playedSong.stream.url;
|
|
2878
|
+
}
|
|
2879
|
+
await this.playSong(queue, true);
|
|
2880
|
+
} finally {
|
|
2881
|
+
queue._taskQueue.resolve();
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
/**
|
|
2885
|
+
* Handle error while playing
|
|
2886
|
+
* @param queue - queue
|
|
2887
|
+
* @param error - error
|
|
2888
|
+
*/
|
|
2889
|
+
async #handlePlayingError(queue, error) {
|
|
2890
|
+
const song = queue.songs.shift();
|
|
2891
|
+
try {
|
|
2892
|
+
error.name = "PlayingError";
|
|
2893
|
+
} catch {
|
|
2894
|
+
}
|
|
2895
|
+
this.debug(`[${queue.id}] Error while playing: ${error.stack || error.message}`);
|
|
2896
|
+
this.emitError(error, queue, song);
|
|
2897
|
+
if (queue.songs.length > 0) {
|
|
2898
|
+
this.debug(`[${queue.id}] Playing next song: ${queue.songs[0]}`);
|
|
2899
|
+
await this.playSong(queue);
|
|
2900
|
+
} else {
|
|
2901
|
+
this.debug(`[${queue.id}] Queue is empty, stopping...`);
|
|
2902
|
+
await queue.stop();
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
/**
|
|
2906
|
+
* Play a song on voice connection with queue properties
|
|
2907
|
+
* @param queue - The guild queue to play
|
|
2908
|
+
* @param emitPlaySong - Whether or not emit {@link Events.PLAY_SONG} event
|
|
2909
|
+
* @param options - Optional options
|
|
2910
|
+
* @param options.keepSongOnError - Whether the current song has to stay in the
|
|
2911
|
+
* queue when the stream cannot be started
|
|
2912
|
+
* @returns A promise that resolves when the song is playing
|
|
2913
|
+
* @throws {@link DisTubeError} When the song does not have any stream URL
|
|
2914
|
+
*/
|
|
2915
|
+
async playSong(queue, emitPlaySong = true, options) {
|
|
2916
|
+
if (!queue) return;
|
|
2917
|
+
if (queue.stopped || !queue.songs.length) {
|
|
2918
|
+
await queue.stop();
|
|
2919
|
+
return;
|
|
2920
|
+
}
|
|
2921
|
+
try {
|
|
2922
|
+
const song = queue.songs[0];
|
|
2923
|
+
this.debug(`[${queue.id}] Getting stream from: ${song}`);
|
|
2924
|
+
await this.handler.attachStreamInfo(song);
|
|
2925
|
+
const willPlaySong = song.stream.playFromSource ? song : song.stream.song;
|
|
2926
|
+
const stream = willPlaySong?.stream;
|
|
2927
|
+
if (!willPlaySong || !stream?.playFromSource || !stream.url) throw new DisTubeError("NO_STREAM_URL", `${song}`);
|
|
2928
|
+
this.debug(`[${queue.id}] Creating DisTubeStream for: ${willPlaySong}`);
|
|
2929
|
+
const streamOptions = {
|
|
2930
|
+
ffmpeg: {
|
|
2931
|
+
path: this.options.ffmpeg.path,
|
|
2932
|
+
args: {
|
|
2933
|
+
global: { ...queue.ffmpegArgs.global },
|
|
2934
|
+
input: { ...queue.ffmpegArgs.input },
|
|
2935
|
+
output: { ...queue.ffmpegArgs.output, ...queue.filters.ffmpegArgs }
|
|
2936
|
+
}
|
|
2937
|
+
},
|
|
2938
|
+
seek: willPlaySong.duration ? queue._beginTime : void 0
|
|
2939
|
+
};
|
|
2940
|
+
queue._beginTime = 0;
|
|
2941
|
+
const dtStream = new DisTubeStream(stream.url, streamOptions);
|
|
2942
|
+
dtStream.on("debug", (data) => this.emit("ffmpegDebug" /* FFMPEG_DEBUG */, `[${queue.id}] ${data}`));
|
|
2943
|
+
this.debug(`[${queue.id}] Started playing: ${willPlaySong}`);
|
|
2944
|
+
await queue.voice.play(dtStream);
|
|
2945
|
+
if (emitPlaySong) this.emit("playSong" /* PLAY_SONG */, queue, song);
|
|
2946
|
+
} catch (e) {
|
|
2947
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
2948
|
+
if (options?.keepSongOnError) {
|
|
2949
|
+
this.debug(`[${queue.id}] Error while restarting the stream: ${error.stack || error.message}`);
|
|
2950
|
+
this.emitError(error, queue, queue.songs[0]);
|
|
2951
|
+
return;
|
|
2952
|
+
}
|
|
2953
|
+
await this.#handlePlayingError(queue, error);
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
};
|
|
2957
|
+
|
|
2958
|
+
// src/core/StreamServer.ts
|
|
2959
|
+
var import_node_crypto = require("crypto");
|
|
2960
|
+
var import_node_http = require("http");
|
|
2961
|
+
var import_node_stream2 = require("stream");
|
|
2962
|
+
var parseRange = /* @__PURE__ */ __name((header) => {
|
|
2963
|
+
if (!header) return 0;
|
|
2964
|
+
const match = /^bytes=(\d+)-/u.exec(header);
|
|
2965
|
+
return match ? Number(match[1]) : 0;
|
|
2966
|
+
}, "parseRange");
|
|
2967
|
+
var StreamServer = class {
|
|
2968
|
+
static {
|
|
2969
|
+
__name(this, "StreamServer");
|
|
2970
|
+
}
|
|
2971
|
+
#handlers = /* @__PURE__ */ new Map();
|
|
2972
|
+
#port;
|
|
2973
|
+
#prefix;
|
|
2974
|
+
#server;
|
|
2975
|
+
#ready;
|
|
2976
|
+
#boundPort;
|
|
2977
|
+
#closed = false;
|
|
2978
|
+
/**
|
|
2979
|
+
* Create a stream server.
|
|
2980
|
+
* @param options - Optional options
|
|
2981
|
+
*/
|
|
2982
|
+
constructor({ port = 0, prefix } = {}) {
|
|
2983
|
+
this.#port = port;
|
|
2984
|
+
this.#prefix = prefix ?? (0, import_node_crypto.randomBytes)(8).toString("hex");
|
|
2985
|
+
}
|
|
2986
|
+
/**
|
|
2987
|
+
* The prefix of the URLs of the server.
|
|
2988
|
+
*/
|
|
2989
|
+
get prefix() {
|
|
2990
|
+
return this.#prefix;
|
|
2991
|
+
}
|
|
2992
|
+
/**
|
|
2993
|
+
* The base URL of the server, e.g. `http://127.0.0.1:51234`.
|
|
2994
|
+
*
|
|
2995
|
+
* @throws When the server is not started yet.
|
|
2996
|
+
*/
|
|
2997
|
+
get baseUrl() {
|
|
2998
|
+
if (!this.#boundPort) throw new Error("The stream server is not started yet.");
|
|
2999
|
+
return `http://127.0.0.1:${this.#boundPort}`;
|
|
3000
|
+
}
|
|
3001
|
+
/**
|
|
3002
|
+
* Register a source.
|
|
3003
|
+
*
|
|
3004
|
+
* The handler is called for every request of that source. Return `null` to
|
|
3005
|
+
* answer a `404`.
|
|
3006
|
+
* @param source - The identifier of the source
|
|
3007
|
+
* @param handler - The handler of the source
|
|
3008
|
+
* @returns A handle to unregister the source
|
|
3009
|
+
* @example
|
|
3010
|
+
* ```ts
|
|
3011
|
+
* const disposable = distube.streams.register("my-source", async req => ({
|
|
3012
|
+
* body: Readable.from(Buffer.from("audio")),
|
|
3013
|
+
* contentType: "audio/mpeg",
|
|
3014
|
+
* }));
|
|
3015
|
+
* ```
|
|
3016
|
+
*/
|
|
3017
|
+
register(source, handler) {
|
|
3018
|
+
const key = source.toLowerCase();
|
|
3019
|
+
this.#handlers.set(key, handler);
|
|
3020
|
+
void this.#start();
|
|
3021
|
+
return {
|
|
3022
|
+
dispose: /* @__PURE__ */ __name(() => {
|
|
3023
|
+
if (this.#handlers.get(key) === handler) this.#handlers.delete(key);
|
|
3024
|
+
}, "dispose")
|
|
3025
|
+
};
|
|
3026
|
+
}
|
|
3027
|
+
/**
|
|
3028
|
+
* Build the URL of a stream.
|
|
3029
|
+
*
|
|
3030
|
+
* The server is started when it is not running yet.
|
|
3031
|
+
* @param source - The identifier of the source
|
|
3032
|
+
* @param path - The path after the source
|
|
3033
|
+
* @param query - The query parameters
|
|
3034
|
+
* @returns The URL of the stream
|
|
3035
|
+
* @example
|
|
3036
|
+
* ```ts
|
|
3037
|
+
* const url = await distube.streams.url("my-source", "abc123", { token: "secret" });
|
|
3038
|
+
* ```
|
|
3039
|
+
*/
|
|
3040
|
+
async url(source, path, query) {
|
|
3041
|
+
await this.#start();
|
|
3042
|
+
const url = new URL(`${this.baseUrl}/${this.#prefix}/${source.toLowerCase()}`);
|
|
3043
|
+
if (path) url.pathname = `${url.pathname}/${path.replace(/^\/+/u, "")}`;
|
|
3044
|
+
for (const [key, value] of Object.entries(query ?? {})) url.searchParams.set(key, String(value));
|
|
3045
|
+
return url.toString();
|
|
3046
|
+
}
|
|
3047
|
+
/**
|
|
3048
|
+
* Close the server and release every handler.
|
|
3049
|
+
* @returns A promise that resolves when the server is closed
|
|
3050
|
+
*/
|
|
3051
|
+
async close() {
|
|
3052
|
+
this.#closed = true;
|
|
3053
|
+
this.#handlers.clear();
|
|
3054
|
+
const server = this.#server;
|
|
3055
|
+
this.#server = void 0;
|
|
3056
|
+
this.#ready = void 0;
|
|
3057
|
+
this.#boundPort = void 0;
|
|
3058
|
+
if (!server) return;
|
|
3059
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
3060
|
+
}
|
|
3061
|
+
async #start() {
|
|
3062
|
+
if (this.#closed) throw new Error("The stream server is closed.");
|
|
3063
|
+
if (!this.#ready) {
|
|
3064
|
+
this.#ready = new Promise((resolve, reject) => {
|
|
3065
|
+
const server = (0, import_node_http.createServer)((req, res) => void this.#handle(req, res));
|
|
3066
|
+
server.once("error", reject);
|
|
3067
|
+
server.listen(this.#port, "127.0.0.1", () => {
|
|
3068
|
+
const address = server.address();
|
|
3069
|
+
this.#boundPort = typeof address === "object" && address ? address.port : this.#port;
|
|
3070
|
+
this.#server = server;
|
|
3071
|
+
server.unref?.();
|
|
3072
|
+
resolve();
|
|
3073
|
+
});
|
|
3074
|
+
});
|
|
3075
|
+
}
|
|
3076
|
+
return this.#ready;
|
|
3077
|
+
}
|
|
3078
|
+
async #handle(req, res) {
|
|
3079
|
+
const controller = new AbortController();
|
|
3080
|
+
req.on("close", () => controller.abort());
|
|
3081
|
+
try {
|
|
3082
|
+
const requestUrl = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
3083
|
+
const segments = requestUrl.pathname.split("/").filter(Boolean);
|
|
3084
|
+
if (segments[0] !== this.#prefix) {
|
|
3085
|
+
res.writeHead(404, { "Content-Type": "text/plain" }).end("Not Found");
|
|
3086
|
+
return;
|
|
3087
|
+
}
|
|
3088
|
+
const handler = this.#handlers.get((segments[1] ?? "").toLowerCase());
|
|
3089
|
+
if (!handler) {
|
|
3090
|
+
res.writeHead(404, { "Content-Type": "text/plain" }).end("Not Found");
|
|
3091
|
+
return;
|
|
3092
|
+
}
|
|
3093
|
+
const result = await handler({
|
|
3094
|
+
path: segments.slice(2).join("/"),
|
|
3095
|
+
query: requestUrl.searchParams,
|
|
3096
|
+
offset: parseRange(req.headers.range),
|
|
3097
|
+
signal: controller.signal,
|
|
3098
|
+
request: req
|
|
3099
|
+
});
|
|
3100
|
+
if (!result) {
|
|
3101
|
+
res.writeHead(404, { "Content-Type": "text/plain" }).end("Not Found");
|
|
3102
|
+
return;
|
|
3103
|
+
}
|
|
3104
|
+
const headers = {
|
|
3105
|
+
"Content-Type": result.contentType ?? "application/octet-stream",
|
|
3106
|
+
...result.headers
|
|
3107
|
+
};
|
|
3108
|
+
if (result.size !== void 0) headers["Accept-Ranges"] = "bytes";
|
|
3109
|
+
if (result.size !== void 0 && result.size > 0) {
|
|
3110
|
+
const offset = Math.min(parseRange(req.headers.range), Math.max(result.size - 1, 0));
|
|
3111
|
+
if (offset > 0) {
|
|
3112
|
+
headers["Content-Range"] = `bytes ${offset}-${result.size - 1}/${result.size}`;
|
|
3113
|
+
res.writeHead(206, headers);
|
|
3114
|
+
} else {
|
|
3115
|
+
res.writeHead(result.status ?? 200, headers);
|
|
3116
|
+
}
|
|
3117
|
+
} else {
|
|
3118
|
+
res.writeHead(result.status ?? 200, headers);
|
|
3119
|
+
}
|
|
3120
|
+
const body = result.body instanceof import_node_stream2.Readable ? result.body : import_node_stream2.Readable.from(result.body);
|
|
3121
|
+
body.on("error", () => res.end());
|
|
3122
|
+
body.pipe(res);
|
|
3123
|
+
} catch {
|
|
3124
|
+
if (!res.headersSent) res.writeHead(500, { "Content-Type": "text/plain" });
|
|
3125
|
+
res.end();
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
};
|
|
3129
|
+
|
|
3130
|
+
// src/DisTube.ts
|
|
3131
|
+
var import_tiny_typed_emitter3 = require("tiny-typed-emitter");
|
|
3132
|
+
|
|
3133
|
+
// src/struct/SearchResult.ts
|
|
3134
|
+
var SearchResult = class {
|
|
3135
|
+
static {
|
|
3136
|
+
__name(this, "SearchResult");
|
|
3137
|
+
}
|
|
3138
|
+
/**
|
|
3139
|
+
* The query of the search.
|
|
3140
|
+
*/
|
|
3141
|
+
query;
|
|
3142
|
+
/**
|
|
3143
|
+
* The kind of result that was looked for.
|
|
3144
|
+
*/
|
|
3145
|
+
queryType;
|
|
3146
|
+
/**
|
|
3147
|
+
* The songs that were found.
|
|
3148
|
+
*/
|
|
3149
|
+
songs;
|
|
3150
|
+
/**
|
|
3151
|
+
* The playlist that was found, when the search returned one.
|
|
3152
|
+
*/
|
|
3153
|
+
playlist;
|
|
3154
|
+
/**
|
|
3155
|
+
* The identifiers of the plugins that contributed to the result.
|
|
3156
|
+
*/
|
|
3157
|
+
sources;
|
|
3158
|
+
/**
|
|
3159
|
+
* Create a search result.
|
|
3160
|
+
* @param query - The query of the search
|
|
3161
|
+
* @param options - Optional data
|
|
3162
|
+
* @param options.songs - The songs that were found
|
|
3163
|
+
* @param options.playlist - The playlist that was found
|
|
3164
|
+
* @param options.queryType - The kind of result that was looked for
|
|
3165
|
+
* @param options.sources - The identifiers of the plugins that contributed
|
|
3166
|
+
* to the result
|
|
3167
|
+
* @example
|
|
3168
|
+
* ```ts
|
|
3169
|
+
* const result = new SearchResult("Never gonna give you up", { songs: [song] });
|
|
3170
|
+
* ```
|
|
3171
|
+
*/
|
|
3172
|
+
constructor(query, {
|
|
3173
|
+
songs = [],
|
|
3174
|
+
playlist,
|
|
3175
|
+
queryType,
|
|
3176
|
+
sources = []
|
|
3177
|
+
} = {}) {
|
|
3178
|
+
this.query = query;
|
|
3179
|
+
this.songs = songs;
|
|
3180
|
+
this.playlist = playlist;
|
|
3181
|
+
this.queryType = queryType ?? "track" /* TRACK */;
|
|
3182
|
+
this.sources = sources;
|
|
3183
|
+
}
|
|
3184
|
+
/**
|
|
3185
|
+
* Whether the search returned nothing.
|
|
3186
|
+
* @returns `true` when the result has neither songs nor a playlist
|
|
3187
|
+
*/
|
|
3188
|
+
isEmpty() {
|
|
3189
|
+
return this.songs.length === 0 && !this.playlist;
|
|
3190
|
+
}
|
|
3191
|
+
/**
|
|
3192
|
+
* Whether the search returned songs.
|
|
3193
|
+
* @returns `true` when the result has at least one song
|
|
3194
|
+
*/
|
|
3195
|
+
hasSongs() {
|
|
3196
|
+
return this.songs.length > 0;
|
|
3197
|
+
}
|
|
3198
|
+
/**
|
|
3199
|
+
* Whether the search returned a playlist.
|
|
3200
|
+
* @returns `true` when the result has a playlist
|
|
3201
|
+
*/
|
|
3202
|
+
hasPlaylist() {
|
|
3203
|
+
return Boolean(this.playlist);
|
|
3204
|
+
}
|
|
3205
|
+
/**
|
|
3206
|
+
* A plain object representation of the result.
|
|
3207
|
+
* @returns The plain object representation of the result
|
|
3208
|
+
*/
|
|
3209
|
+
toJSON() {
|
|
3210
|
+
return {
|
|
3211
|
+
query: this.query,
|
|
3212
|
+
queryType: this.queryType,
|
|
3213
|
+
sources: [...this.sources],
|
|
3214
|
+
songs: this.songs.map((song) => ({
|
|
3215
|
+
id: song.id,
|
|
3216
|
+
name: song.name,
|
|
3217
|
+
url: song.url,
|
|
3218
|
+
duration: song.duration,
|
|
3219
|
+
formattedDuration: song.formattedDuration,
|
|
3220
|
+
thumbnail: song.thumbnail,
|
|
3221
|
+
source: song.source,
|
|
3222
|
+
uploader: song.uploader
|
|
3223
|
+
})),
|
|
3224
|
+
playlist: this.playlist ? {
|
|
3225
|
+
id: this.playlist.id,
|
|
3226
|
+
name: this.playlist.name,
|
|
3227
|
+
url: this.playlist.url,
|
|
3228
|
+
thumbnail: this.playlist.thumbnail,
|
|
3229
|
+
songs: this.playlist.songs.length
|
|
3230
|
+
} : void 0
|
|
3231
|
+
};
|
|
3232
|
+
}
|
|
3233
|
+
};
|
|
3234
|
+
|
|
3235
|
+
// src/DisTube.ts
|
|
3236
|
+
var DisTube = class _DisTube extends import_tiny_typed_emitter3.TypedEmitter {
|
|
3237
|
+
static {
|
|
3238
|
+
__name(this, "DisTube");
|
|
3239
|
+
}
|
|
3240
|
+
/**
|
|
3241
|
+
* @event
|
|
3242
|
+
* Emitted after DisTube add a new playlist to the playing {@link Queue}.
|
|
3243
|
+
* @param queue - The guild queue
|
|
3244
|
+
* @param playlist - Playlist info
|
|
3245
|
+
*/
|
|
3246
|
+
static ["addList" /* ADD_LIST */];
|
|
3247
|
+
/**
|
|
3248
|
+
* @event
|
|
3249
|
+
* Emitted after DisTube add a new song to the playing {@link Queue}.
|
|
3250
|
+
* @param queue - The guild queue
|
|
3251
|
+
* @param song - Added song
|
|
3252
|
+
*/
|
|
3253
|
+
static ["addSong" /* ADD_SONG */];
|
|
3254
|
+
/**
|
|
3255
|
+
* @event
|
|
3256
|
+
* Emitted when the {@link Queue} is deleted with any reasons.
|
|
3257
|
+
* @param queue - The guild queue
|
|
3258
|
+
*/
|
|
3259
|
+
static ["deleteQueue" /* DELETE_QUEUE */];
|
|
3260
|
+
/**
|
|
3261
|
+
* @event
|
|
3262
|
+
* Emitted when the bot is disconnected to a voice channel.
|
|
3263
|
+
* @param queue - The guild queue
|
|
3264
|
+
*/
|
|
3265
|
+
static ["disconnect" /* DISCONNECT */];
|
|
3266
|
+
/**
|
|
3267
|
+
* @event
|
|
3268
|
+
* Emitted when the {@link Queue} runs out of songs to play.
|
|
3269
|
+
* @param queue - The guild queue
|
|
3270
|
+
*/
|
|
3271
|
+
static ["empty" /* EMPTY */];
|
|
3272
|
+
/**
|
|
3273
|
+
* @event
|
|
3274
|
+
* Emitted when DisTube encounters an error while playing songs.
|
|
3275
|
+
* @param error - error
|
|
3276
|
+
* @param queue - The queue encountered the error
|
|
3277
|
+
* @param song - The playing song when encountered the error
|
|
3278
|
+
*/
|
|
3279
|
+
static ["error" /* ERROR */];
|
|
3280
|
+
/**
|
|
3281
|
+
* @event
|
|
3282
|
+
* Emitted for logging FFmpeg debug information.
|
|
3283
|
+
* @param debug - Debug message string.
|
|
3284
|
+
*/
|
|
3285
|
+
static ["ffmpegDebug" /* FFMPEG_DEBUG */];
|
|
3286
|
+
/**
|
|
3287
|
+
* @event
|
|
3288
|
+
* Emitted to provide debug information from DisTube's operation.
|
|
3289
|
+
* Useful for troubleshooting or logging purposes.
|
|
3290
|
+
*
|
|
3291
|
+
* @param debug - Debug message string.
|
|
3292
|
+
*/
|
|
3293
|
+
static ["debug" /* DEBUG */];
|
|
3294
|
+
/**
|
|
3295
|
+
* @event
|
|
3296
|
+
* Emitted when there is no more song in the queue and {@link Queue#autoplay} is `false`.
|
|
3297
|
+
* @param queue - The guild queue
|
|
3298
|
+
*/
|
|
3299
|
+
static ["finish" /* FINISH */];
|
|
3300
|
+
/**
|
|
3301
|
+
* @event
|
|
3302
|
+
* Emitted when DisTube finished a song.
|
|
3303
|
+
* @param queue - The guild queue
|
|
3304
|
+
* @param song - Finished song
|
|
3305
|
+
*/
|
|
3306
|
+
static ["finishSong" /* FINISH_SONG */];
|
|
3307
|
+
/**
|
|
3308
|
+
* @event
|
|
3309
|
+
* Emitted when DisTube initialize a queue to change queue default properties.
|
|
3310
|
+
* @param queue - The guild queue
|
|
3311
|
+
*/
|
|
3312
|
+
static ["initQueue" /* INIT_QUEUE */];
|
|
3313
|
+
/**
|
|
3314
|
+
* @event
|
|
3315
|
+
* Emitted when {@link Queue#autoplay} is `true`, {@link Queue#songs} is empty, and
|
|
3316
|
+
* DisTube cannot find related songs to play.
|
|
3317
|
+
* @param queue - The guild queue
|
|
3318
|
+
* @param error - The error that occurred while looking for related songs
|
|
3319
|
+
*/
|
|
3320
|
+
static ["noRelated" /* NO_RELATED */];
|
|
3321
|
+
/**
|
|
3322
|
+
* @event
|
|
3323
|
+
* Emitted when DisTube play a song.
|
|
3324
|
+
* If {@link DisTubeOptions}.emitNewSongOnly is `true`, this event is not emitted
|
|
3325
|
+
* when looping a song or next song is the previous one.
|
|
3326
|
+
* @param queue - The guild queue
|
|
3327
|
+
* @param song - Playing song
|
|
3328
|
+
*/
|
|
3329
|
+
static ["playSong" /* PLAY_SONG */];
|
|
3330
|
+
/**
|
|
3331
|
+
* DisTube internal handler
|
|
3332
|
+
*/
|
|
3333
|
+
handler;
|
|
3334
|
+
/**
|
|
3335
|
+
* DisTube options
|
|
3336
|
+
*/
|
|
3337
|
+
options;
|
|
3338
|
+
/**
|
|
3339
|
+
* Discord.js v14 client
|
|
3340
|
+
*/
|
|
3341
|
+
client;
|
|
3342
|
+
/**
|
|
3343
|
+
* Queues manager
|
|
3344
|
+
*/
|
|
3345
|
+
queues;
|
|
3346
|
+
/**
|
|
3347
|
+
* DisTube voice connections manager
|
|
3348
|
+
*/
|
|
3349
|
+
voices;
|
|
3350
|
+
/**
|
|
3351
|
+
* DisTube plugins
|
|
3352
|
+
*/
|
|
3353
|
+
plugins;
|
|
3354
|
+
/**
|
|
3355
|
+
* The local HTTP server the plugins use to stream the audio.
|
|
3356
|
+
*
|
|
3357
|
+
* A plugin that cannot hand out a static URL to FFmpeg registers a handler on
|
|
3358
|
+
* it, instead of running its own server.
|
|
3359
|
+
*/
|
|
3360
|
+
streams;
|
|
3361
|
+
/**
|
|
3362
|
+
* DisTube ffmpeg audio filters
|
|
3363
|
+
*/
|
|
3364
|
+
filters;
|
|
3365
|
+
/**
|
|
3366
|
+
* Create a new DisTube class.
|
|
3367
|
+
* @throws {@link DisTubeError}
|
|
3368
|
+
* @param client - Discord.JS client
|
|
3369
|
+
* @param opts - Custom DisTube options
|
|
3370
|
+
* @example
|
|
3371
|
+
* ```ts
|
|
3372
|
+
* const distube = new DisTube(client, { nsfw: false });
|
|
3373
|
+
* ```
|
|
3374
|
+
*/
|
|
3375
|
+
constructor(client, opts = {}) {
|
|
3376
|
+
super();
|
|
3377
|
+
this.setMaxListeners(0);
|
|
3378
|
+
if (!isClientInstance(client)) throw new DisTubeError("INVALID_TYPE", "Discord.Client", client, "client");
|
|
3379
|
+
this.client = client;
|
|
3380
|
+
checkIntents(client.options);
|
|
3381
|
+
this.options = new Options(opts);
|
|
3382
|
+
this.voices = new DisTubeVoiceManager(this);
|
|
3383
|
+
this.handler = new DisTubeHandler(this);
|
|
3384
|
+
this.queues = new QueueManager(this);
|
|
3385
|
+
this.filters = { ...defaultFilters, ...this.options.customFilters };
|
|
3386
|
+
this.plugins = [...this.options.plugins];
|
|
3387
|
+
this.streams = new StreamServer();
|
|
3388
|
+
for (const p of this.plugins) {
|
|
3389
|
+
const result = p.init(this);
|
|
3390
|
+
if (result && typeof result.catch === "function") {
|
|
3391
|
+
result.catch((e) => {
|
|
3392
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
3393
|
+
this.debug(`[DisTube] The plugin ${p.constructor.name} failed to initialize: ${error.message}`);
|
|
3394
|
+
process.emitWarning(`The plugin ${p.constructor.name} failed to initialize: ${error.message}`, {
|
|
3395
|
+
code: "DIS_TUBE_PLUGIN_INIT_FAILED",
|
|
3396
|
+
detail: error.stack ?? error.message
|
|
3397
|
+
});
|
|
3398
|
+
});
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
/**
|
|
3403
|
+
* Create a DisTube instance and wait for the plugins to be initialized.
|
|
3404
|
+
*
|
|
3405
|
+
* Use it when a plugin does asynchronous work in its `init` method: the
|
|
3406
|
+
* constructor cannot await it.
|
|
3407
|
+
* @param client - The Discord.js client
|
|
3408
|
+
* @param opts - Optional options
|
|
3409
|
+
* @returns The DisTube instance
|
|
3410
|
+
* @example
|
|
3411
|
+
* ```ts
|
|
3412
|
+
* const distube = await DisTube.create(client, { plugins: [new YoutubePlugin()] });
|
|
3413
|
+
* ```
|
|
3414
|
+
*/
|
|
3415
|
+
static async create(client, opts = {}) {
|
|
3416
|
+
const distube = new _DisTube(client, opts);
|
|
3417
|
+
await Promise.all(
|
|
3418
|
+
distube.plugins.map(async (plugin) => {
|
|
3419
|
+
try {
|
|
3420
|
+
await plugin.init(distube);
|
|
3421
|
+
} catch (e) {
|
|
3422
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
3423
|
+
distube.debug(`[DisTube] The plugin ${plugin.constructor.name} failed to initialize: ${error.message}`);
|
|
3424
|
+
process.emitWarning(`The plugin ${plugin.constructor.name} failed to initialize: ${error.message}`, {
|
|
3425
|
+
code: "DIS_TUBE_PLUGIN_INIT_FAILED",
|
|
3426
|
+
detail: error.stack ?? error.message
|
|
3427
|
+
});
|
|
3428
|
+
}
|
|
3429
|
+
})
|
|
3430
|
+
);
|
|
3431
|
+
return distube;
|
|
3432
|
+
}
|
|
3433
|
+
/**
|
|
3434
|
+
* Emit an event of the DisTube instance.
|
|
3435
|
+
*
|
|
3436
|
+
* The special {@link Events.ERROR} event is handled differently than the other
|
|
3437
|
+
* events: Node.js throws an `ERR_UNHANDLED_ERROR` when the special `"error"`
|
|
3438
|
+
* event is emitted without any listener, which crashes the whole process.
|
|
3439
|
+
*
|
|
3440
|
+
* Instead of crashing, DisTube follows {@link DisTubeOptions.unhandledError}
|
|
3441
|
+
* (`"log"` by default).
|
|
3442
|
+
* @param event - The event name
|
|
3443
|
+
* @param args - The event arguments
|
|
3444
|
+
* @returns `true` when the event had listeners, `false` otherwise
|
|
3445
|
+
*/
|
|
3446
|
+
emit(event, ...args) {
|
|
3447
|
+
if (event === "error" /* ERROR */ && this.listenerCount("error" /* ERROR */) === 0) {
|
|
3448
|
+
this.#handleUnhandledError(args[0]);
|
|
3449
|
+
return false;
|
|
3450
|
+
}
|
|
3451
|
+
return super.emit(event, ...args);
|
|
3452
|
+
}
|
|
3453
|
+
/**
|
|
3454
|
+
* Handle an {@link Events.ERROR} event that has no listener.
|
|
3455
|
+
* @param error - The error
|
|
3456
|
+
*/
|
|
3457
|
+
#handleUnhandledError(error) {
|
|
3458
|
+
handleUnhandledError(error, this.options?.unhandledError ?? "log");
|
|
3459
|
+
}
|
|
3460
|
+
/**
|
|
3461
|
+
* DisTube version
|
|
3462
|
+
*/
|
|
3463
|
+
static get version() {
|
|
3464
|
+
return version;
|
|
3465
|
+
}
|
|
3466
|
+
/**
|
|
3467
|
+
* DisTube version
|
|
3468
|
+
*/
|
|
3469
|
+
get version() {
|
|
3470
|
+
return version;
|
|
3471
|
+
}
|
|
3472
|
+
/**
|
|
3473
|
+
* Play / add a song or playlist from url.
|
|
3474
|
+
* Search and play a song (with {@link ExtractorPlugin}) if it is not a valid url.
|
|
3475
|
+
* @throws {@link DisTubeError}
|
|
3476
|
+
* @param voiceChannel - The channel will be joined if the bot isn't in any channels, the bot will be
|
|
3477
|
+
* moved to this channel if {@link DisTubeOptions}.joinNewVoiceChannel is `true`
|
|
3478
|
+
* @param song - URL | Search string | {@link Song} | {@link Playlist}
|
|
3479
|
+
* @param options - Optional options
|
|
3480
|
+
* @returns A promise that resolves when the song is added to the queue
|
|
3481
|
+
* @example
|
|
3482
|
+
* ```ts
|
|
3483
|
+
* await distube.play(channel, "Never gonna give you up", { message });
|
|
3484
|
+
* ```
|
|
3485
|
+
*/
|
|
3486
|
+
async play(voiceChannel, song, options = {}) {
|
|
3487
|
+
if (!isSupportedVoiceChannel(voiceChannel)) {
|
|
3488
|
+
throw new DisTubeError("INVALID_TYPE", "BaseGuildVoiceChannel", voiceChannel, "voiceChannel");
|
|
3489
|
+
}
|
|
3490
|
+
if (!isObject(options)) throw new DisTubeError("INVALID_TYPE", "object", options, "options");
|
|
3491
|
+
const { textChannel, member, skip, message, metadata, source } = {
|
|
3492
|
+
member: voiceChannel.guild.members.me ?? void 0,
|
|
3493
|
+
textChannel: options?.message?.channel,
|
|
3494
|
+
skip: false,
|
|
3495
|
+
...options
|
|
3496
|
+
};
|
|
3497
|
+
const position = Number(options.position) || (skip ? 1 : 0);
|
|
3498
|
+
if (message && !isMessageInstance(message)) {
|
|
3499
|
+
throw new DisTubeError("INVALID_TYPE", ["Discord.Message", "a falsy value"], message, "options.message");
|
|
3500
|
+
}
|
|
3501
|
+
if (textChannel && !isTextChannelInstance(textChannel)) {
|
|
3502
|
+
throw new DisTubeError("INVALID_TYPE", "Discord.GuildTextBasedChannel", textChannel, "options.textChannel");
|
|
3503
|
+
}
|
|
3504
|
+
if (member && !isMemberInstance(member)) {
|
|
3505
|
+
throw new DisTubeError("INVALID_TYPE", "Discord.GuildMember", member, "options.member");
|
|
3506
|
+
}
|
|
3507
|
+
const queue = this.getQueue(voiceChannel) || await this.queues.create(voiceChannel, textChannel);
|
|
3508
|
+
await queue._taskQueue.queuing(true);
|
|
3509
|
+
try {
|
|
3510
|
+
this.debug(`[${queue.id}] Playing input: ${song}`);
|
|
3511
|
+
const resolved = await this.handler.resolve(song, { member, metadata, source });
|
|
3512
|
+
const isNsfw = isNsfwChannel(queue?.textChannel || textChannel);
|
|
3513
|
+
const isFirstSong = queue.songs.length === 0;
|
|
3514
|
+
if (resolved instanceof Playlist) {
|
|
3515
|
+
if (!this.options.nsfw && !isNsfw) {
|
|
3516
|
+
resolved.songs = resolved.songs.filter((s) => !s.ageRestricted);
|
|
3517
|
+
if (!resolved.songs.length) throw new DisTubeError("EMPTY_FILTERED_PLAYLIST");
|
|
3518
|
+
}
|
|
3519
|
+
if (!resolved.songs.length) throw new DisTubeError("EMPTY_PLAYLIST");
|
|
3520
|
+
this.debug(`[${queue.id}] Adding playlist to queue: ${resolved.songs.length} songs`);
|
|
3521
|
+
queue.addToQueue(resolved.songs, position);
|
|
3522
|
+
if (!isFirstSong || this.options.emitAddListWhenCreatingQueue) this.emit("addList" /* ADD_LIST */, queue, resolved);
|
|
3523
|
+
} else {
|
|
3524
|
+
if (!this.options.nsfw && resolved.ageRestricted && !isNsfwChannel(queue?.textChannel || textChannel)) {
|
|
3525
|
+
throw new DisTubeError("NON_NSFW");
|
|
3526
|
+
}
|
|
3527
|
+
this.debug(`[${queue.id}] Adding song to queue: ${resolved.name || resolved.url || resolved.id || resolved}`);
|
|
3528
|
+
queue.addToQueue(resolved, position);
|
|
3529
|
+
if (!isFirstSong || this.options.emitAddSongWhenCreatingQueue) this.emit("addSong" /* ADD_SONG */, queue, resolved);
|
|
3530
|
+
}
|
|
3531
|
+
if (isFirstSong) await queue.play();
|
|
3532
|
+
else if (skip) await queue.skip();
|
|
3533
|
+
} catch (e) {
|
|
3534
|
+
if (!(e instanceof DisTubeError)) {
|
|
3535
|
+
const errorMessage = e instanceof Error ? e.stack ?? e.message : String(e);
|
|
3536
|
+
this.debug(`[${queue.id}] Unexpected error while playing song: ${errorMessage}`);
|
|
3537
|
+
if (e instanceof Error) {
|
|
3538
|
+
try {
|
|
3539
|
+
e.name = "PlayError";
|
|
3540
|
+
e.message = `${typeof song === "string" ? song : song.url}
|
|
3541
|
+
${e.message}`;
|
|
3542
|
+
} catch {
|
|
3543
|
+
}
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
throw e;
|
|
3547
|
+
} finally {
|
|
3548
|
+
if (!queue.songs.length && !queue._taskQueue.hasPlayTask) queue.remove();
|
|
3549
|
+
queue._taskQueue.resolve();
|
|
3550
|
+
}
|
|
3551
|
+
}
|
|
3552
|
+
/**
|
|
3553
|
+
* Create a custom playlist
|
|
3554
|
+
* @param songs - Array of url or Song
|
|
3555
|
+
* @param options - Optional options
|
|
3556
|
+
* @returns The created playlist
|
|
3557
|
+
* @throws {@link DisTubeError} When the array is empty, or when it does not
|
|
3558
|
+
* have any valid song
|
|
3559
|
+
*/
|
|
3560
|
+
async createCustomPlaylist(songs, { member, parallel, metadata, name, source, url, thumbnail } = {}) {
|
|
3561
|
+
if (!Array.isArray(songs)) throw new DisTubeError("INVALID_TYPE", "Array", songs, "songs");
|
|
3562
|
+
if (!songs.length) throw new DisTubeError("EMPTY_ARRAY", "songs");
|
|
3563
|
+
const filteredSongs = songs.filter((song) => song instanceof Song || isURL(song));
|
|
3564
|
+
if (!filteredSongs.length) throw new DisTubeError("NO_VALID_SONG");
|
|
3565
|
+
if (member && !isMemberInstance(member)) {
|
|
3566
|
+
throw new DisTubeError("INVALID_TYPE", "Discord.Member", member, "options.member");
|
|
3567
|
+
}
|
|
3568
|
+
let resolvedSongs;
|
|
3569
|
+
if (parallel !== false) {
|
|
3570
|
+
const promises = filteredSongs.map(
|
|
3571
|
+
(song) => this.handler.resolve(song, { member, metadata }).catch(() => void 0)
|
|
3572
|
+
);
|
|
3573
|
+
resolvedSongs = (await Promise.all(promises)).filter((s) => s instanceof Song);
|
|
3574
|
+
} else {
|
|
3575
|
+
resolvedSongs = [];
|
|
3576
|
+
for (const song of filteredSongs) {
|
|
3577
|
+
const resolved = await this.handler.resolve(song, { member, metadata }).catch(() => void 0);
|
|
3578
|
+
if (resolved instanceof Song) resolvedSongs.push(resolved);
|
|
3579
|
+
}
|
|
3580
|
+
}
|
|
3581
|
+
return new Playlist(
|
|
3582
|
+
{
|
|
3583
|
+
source: source || "custom",
|
|
3584
|
+
name,
|
|
3585
|
+
url,
|
|
3586
|
+
thumbnail: thumbnail || resolvedSongs.find((s) => s.thumbnail)?.thumbnail,
|
|
3587
|
+
songs: resolvedSongs
|
|
3588
|
+
},
|
|
3589
|
+
{ member, metadata }
|
|
3590
|
+
);
|
|
3591
|
+
}
|
|
3592
|
+
/**
|
|
3593
|
+
* Get the guild queue
|
|
3594
|
+
* @param guild - The type can be resolved to give a {@link Queue}
|
|
3595
|
+
* @returns The queue of the guild, or `undefined` when there is none
|
|
3596
|
+
* @example
|
|
3597
|
+
* ```ts
|
|
3598
|
+
* const queue = distube.getQueue(guildId);
|
|
3599
|
+
* ```
|
|
3600
|
+
*/
|
|
3601
|
+
getQueue(guild) {
|
|
3602
|
+
return this.queues.get(guild);
|
|
3603
|
+
}
|
|
3604
|
+
/**
|
|
3605
|
+
* Get a plugin by the identifier of its source.
|
|
3606
|
+
*
|
|
3607
|
+
* It matches the {@link Plugin.source} of the plugin, or one of its
|
|
3608
|
+
* {@link Plugin.aliases}. The identifier of the class is used when the
|
|
3609
|
+
* plugin does not define a source (`YoutubePlugin` -> `youtube`).
|
|
3610
|
+
* @param source - The identifier of the source
|
|
3611
|
+
* @returns The plugin, or `undefined` when there is none
|
|
3612
|
+
* @example
|
|
3613
|
+
* ```ts
|
|
3614
|
+
* const plugin = distube.getPlugin("youtube");
|
|
3615
|
+
* ```
|
|
3616
|
+
*/
|
|
3617
|
+
getPlugin(source) {
|
|
3618
|
+
if (typeof source !== "string" || !source) return void 0;
|
|
3619
|
+
const identifier = source.toLowerCase();
|
|
3620
|
+
return this.plugins.find((plugin) => plugin.identifiers.includes(identifier));
|
|
3621
|
+
}
|
|
3622
|
+
/**
|
|
3623
|
+
* Search for the songs of the plugins.
|
|
3624
|
+
*
|
|
3625
|
+
* The plugins are used in the order they were registered. When
|
|
3626
|
+
* {@link ResolveOptions.source} is given, only the matching plugin is used.
|
|
3627
|
+
* @param query - The search query
|
|
3628
|
+
* @param options - Optional options
|
|
3629
|
+
* @returns The result of the search
|
|
3630
|
+
* @example
|
|
3631
|
+
* ```ts
|
|
3632
|
+
* const result = await distube.search("Never gonna give you up", { limit: 5 });
|
|
3633
|
+
* ```
|
|
3634
|
+
*/
|
|
3635
|
+
async search(query, options = {}) {
|
|
3636
|
+
const limit = options.limit ?? 10;
|
|
3637
|
+
const plugins = options.source ? [this.getPlugin(options.source)].filter((plugin) => Boolean(plugin)) : this.plugins;
|
|
3638
|
+
const songs = [];
|
|
3639
|
+
const sources = [];
|
|
3640
|
+
let playlist;
|
|
3641
|
+
for (const plugin of plugins) {
|
|
3642
|
+
if (songs.length >= limit) break;
|
|
3643
|
+
const found = await plugin.search(query, { ...options, limit: limit - songs.length });
|
|
3644
|
+
if (!found) continue;
|
|
3645
|
+
if (found.songs.length) {
|
|
3646
|
+
songs.push(...found.songs);
|
|
3647
|
+
sources.push(plugin.identifiers[0] ?? plugin.constructor.name);
|
|
3648
|
+
}
|
|
3649
|
+
playlist ??= found.playlist;
|
|
3650
|
+
}
|
|
3651
|
+
return new SearchResult(query, {
|
|
3652
|
+
songs: songs.slice(0, limit),
|
|
3653
|
+
playlist,
|
|
3654
|
+
queryType: options.type,
|
|
3655
|
+
sources
|
|
3656
|
+
});
|
|
3657
|
+
}
|
|
3658
|
+
/**
|
|
3659
|
+
/**
|
|
3660
|
+
/**
|
|
3661
|
+
/**
|
|
3662
|
+
* Emit error event
|
|
3663
|
+
* @param error - error
|
|
3664
|
+
* @param queue - The queue encountered the error
|
|
3665
|
+
* @param song - The playing song when encountered the error
|
|
3666
|
+
* @returns Nothing
|
|
3667
|
+
*/
|
|
3668
|
+
emitError(error, queue, song) {
|
|
3669
|
+
this.emit("error" /* ERROR */, error, queue, song);
|
|
3670
|
+
}
|
|
3671
|
+
/**
|
|
3672
|
+
* Emit debug event
|
|
3673
|
+
* @param message - debug message
|
|
3674
|
+
* @returns Nothing
|
|
3675
|
+
*/
|
|
3676
|
+
debug(message) {
|
|
3677
|
+
this.emit("debug" /* DEBUG */, message);
|
|
3678
|
+
}
|
|
3679
|
+
/**
|
|
3680
|
+
* Destroy the DisTube instance.
|
|
3681
|
+
*
|
|
3682
|
+
* Stops every queue, leaves every voice channel, destroys every plugin and
|
|
3683
|
+
* removes every listener.
|
|
3684
|
+
* Call it before your Discord.js client is destroyed to release all resources.
|
|
3685
|
+
* @returns A promise that resolves when every resource is released
|
|
3686
|
+
* @example
|
|
3687
|
+
* ```ts
|
|
3688
|
+
* await distube.destroy();
|
|
3689
|
+
* ```
|
|
3690
|
+
*/
|
|
3691
|
+
async destroy() {
|
|
3692
|
+
this.debug("[DisTube] Destroying the DisTube instance...");
|
|
3693
|
+
const queues = [...this.queues.collection.values()];
|
|
3694
|
+
for (const queue of queues) {
|
|
3695
|
+
try {
|
|
3696
|
+
await queue.stop();
|
|
3697
|
+
} catch (e) {
|
|
3698
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
3699
|
+
this.debug(`[DisTube] Failed to stop the queue ${queue.id}: ${errorMessage}`);
|
|
3700
|
+
}
|
|
3701
|
+
}
|
|
3702
|
+
const voices = [...this.voices.collection.values()];
|
|
3703
|
+
for (const voice of voices) {
|
|
3704
|
+
try {
|
|
3705
|
+
voice.leave();
|
|
3706
|
+
} catch (e) {
|
|
3707
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
3708
|
+
this.debug(`[DisTube] Failed to leave the voice connection ${voice.id}: ${errorMessage}`);
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
for (const plugin of this.plugins) {
|
|
3712
|
+
try {
|
|
3713
|
+
await plugin.destroy();
|
|
3714
|
+
} catch (e) {
|
|
3715
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
3716
|
+
this.debug(`[DisTube] Failed to destroy the plugin ${plugin.constructor.name}: ${errorMessage}`);
|
|
3717
|
+
}
|
|
3718
|
+
}
|
|
3719
|
+
await this.streams.close();
|
|
3720
|
+
this.removeAllListeners();
|
|
3721
|
+
}
|
|
3722
|
+
};
|
|
3723
|
+
|
|
3724
|
+
// src/struct/Plugin.ts
|
|
3725
|
+
var Plugin = class {
|
|
3726
|
+
static {
|
|
3727
|
+
__name(this, "Plugin");
|
|
3728
|
+
}
|
|
3729
|
+
/**
|
|
3730
|
+
* The identifier of the source of the plugin.
|
|
3731
|
+
*
|
|
3732
|
+
* It defaults to the name of the class, lowercased, without the `Plugin`
|
|
3733
|
+
* suffix (`YoutubePlugin` -> `youtube`).
|
|
3734
|
+
*
|
|
3735
|
+
* It is used to route a search to a plugin, and to find a plugin with
|
|
3736
|
+
* {@link DisTube#getPlugin}.
|
|
3737
|
+
*/
|
|
3738
|
+
source;
|
|
3739
|
+
/**
|
|
3740
|
+
* Additional identifiers of the source of the plugin, e.g. `["yt"]`.
|
|
3741
|
+
*/
|
|
3742
|
+
aliases;
|
|
3743
|
+
/**
|
|
3744
|
+
* Whether the songs of this plugin can be played directly.
|
|
3745
|
+
*
|
|
3746
|
+
* When it is `false`, the songs of the plugin are resolved to a playable
|
|
3747
|
+
* version by another plugin. It defaults to `true`.
|
|
3748
|
+
*/
|
|
3749
|
+
playable;
|
|
3750
|
+
/**
|
|
3751
|
+
* DisTube
|
|
3752
|
+
*/
|
|
3753
|
+
distube;
|
|
3754
|
+
/**
|
|
3755
|
+
* Initialize the plugin.
|
|
3756
|
+
*
|
|
3757
|
+
* It can be asynchronous: use {@link DisTube.create} to wait for the
|
|
3758
|
+
* initialization of the plugins.
|
|
3759
|
+
* @param distube - The DisTube instance
|
|
3760
|
+
* @returns Nothing, or a promise that resolves when the plugin is initialized
|
|
3761
|
+
* @example
|
|
3762
|
+
* ```ts
|
|
3763
|
+
* override async init(distube: DisTube) {
|
|
3764
|
+
* this.client = await createSession();
|
|
3765
|
+
* }
|
|
3766
|
+
* ```
|
|
3767
|
+
*/
|
|
3768
|
+
init(distube) {
|
|
3769
|
+
this.distube = distube;
|
|
3770
|
+
}
|
|
3771
|
+
/**
|
|
3772
|
+
* Every identifier of this plugin: its {@link Plugin.source} and its
|
|
3773
|
+
* {@link Plugin.aliases}, lowercased.
|
|
3774
|
+
*
|
|
3775
|
+
* The static properties of the class are used when the instance ones are not
|
|
3776
|
+
* defined, so a plugin can declare them either way.
|
|
3777
|
+
*/
|
|
3778
|
+
get identifiers() {
|
|
3779
|
+
const names = /* @__PURE__ */ new Set();
|
|
3780
|
+
const ctor = this.constructor;
|
|
3781
|
+
const source = this.source ?? ctor.source ?? this.constructor.name.replace(/Plugin$/iu, "");
|
|
3782
|
+
if (source) names.add(source.toLowerCase());
|
|
3783
|
+
for (const alias of this.aliases ?? ctor.aliases ?? []) {
|
|
3784
|
+
if (typeof alias === "string" && alias) names.add(alias.toLowerCase());
|
|
3785
|
+
}
|
|
3786
|
+
return [...names];
|
|
3787
|
+
}
|
|
3788
|
+
/**
|
|
3789
|
+
* The local HTTP server of the DisTube instance.
|
|
3790
|
+
*
|
|
3791
|
+
* Register a handler on it when the plugin cannot hand out a static URL to
|
|
3792
|
+
* FFmpeg, instead of running its own server.
|
|
3793
|
+
*/
|
|
3794
|
+
get streams() {
|
|
3795
|
+
return this.distube.streams;
|
|
3796
|
+
}
|
|
3797
|
+
/**
|
|
3798
|
+
* Search for a song of the source.
|
|
3799
|
+
*
|
|
3800
|
+
* Override it to enable the search of your source. It returns `null` by
|
|
3801
|
+
* default, because a source that cannot be searched does not have any result.
|
|
3802
|
+
* @param _query - Search query
|
|
3803
|
+
* @param _options - Optional options
|
|
3804
|
+
* @returns The song that was found, or `null`
|
|
3805
|
+
* @example
|
|
3806
|
+
* ```ts
|
|
3807
|
+
* override async searchSong(query: string): Promise<Song | null> {
|
|
3808
|
+
* return findSong(query);
|
|
3809
|
+
* }
|
|
3810
|
+
* ```
|
|
3811
|
+
*/
|
|
3812
|
+
searchSong(_query, _options) {
|
|
3813
|
+
return null;
|
|
3814
|
+
}
|
|
3815
|
+
/**
|
|
3816
|
+
* Search for the songs of the source.
|
|
3817
|
+
*
|
|
3818
|
+
* The default implementation returns the single result of
|
|
3819
|
+
* {@link Plugin#searchSong}. Override it to return more results, or to return
|
|
3820
|
+
* a playlist.
|
|
3821
|
+
* @param query - Search query
|
|
3822
|
+
* @param options - Optional options
|
|
3823
|
+
* @returns The result of the search
|
|
3824
|
+
* @example
|
|
3825
|
+
* ```ts
|
|
3826
|
+
* const result = await plugin.search("Never gonna give you up", { limit: 5 });
|
|
3827
|
+
* ```
|
|
3828
|
+
*/
|
|
3829
|
+
async search(query, options = {}) {
|
|
3830
|
+
const song = await this.searchSong(query, options);
|
|
3831
|
+
return new SearchResult(query, {
|
|
3832
|
+
songs: song ? [song] : [],
|
|
3833
|
+
queryType: options.type
|
|
3834
|
+
});
|
|
3835
|
+
}
|
|
3836
|
+
/**
|
|
3837
|
+
* Create the query used to find a playable version of a song.
|
|
3838
|
+
*
|
|
3839
|
+
* It is used when the plugin cannot play the songs it resolves.
|
|
3840
|
+
* @param song - Input song
|
|
3841
|
+
* @returns The search query
|
|
3842
|
+
* @example
|
|
3843
|
+
* ```ts
|
|
3844
|
+
* override createSearchQuery(song: Song): string {
|
|
3845
|
+
* return `${song.name} ${song.uploader?.name}`;
|
|
3846
|
+
* }
|
|
3847
|
+
* ```
|
|
3848
|
+
*/
|
|
3849
|
+
createSearchQuery(song) {
|
|
3850
|
+
return `${song.name ?? ""} ${song.uploader?.name ?? ""}`.trim();
|
|
3851
|
+
}
|
|
3852
|
+
/**
|
|
3853
|
+
* Get related songs from a supported url.
|
|
3854
|
+
*
|
|
3855
|
+
* Override it to enable the autoplay for your source. Return an empty array
|
|
3856
|
+
* when your source does not have any related songs.
|
|
3857
|
+
* @param _song - Input song
|
|
3858
|
+
* @returns The related songs
|
|
3859
|
+
*/
|
|
3860
|
+
getRelatedSongs(_song) {
|
|
3861
|
+
return [];
|
|
3862
|
+
}
|
|
3863
|
+
/**
|
|
3864
|
+
* Destroy the plugin.
|
|
3865
|
+
*
|
|
3866
|
+
* It is called when the {@link DisTube} instance is destroyed. Override it to
|
|
3867
|
+
* release every resource held by the plugin (HTTP servers, timers, browser
|
|
3868
|
+
* sessions, ...).
|
|
3869
|
+
* @returns Nothing, or a promise that resolves when the plugin is destroyed
|
|
3870
|
+
*/
|
|
3871
|
+
destroy() {
|
|
3872
|
+
}
|
|
3873
|
+
};
|
|
3874
|
+
|
|
3875
|
+
// src/struct/ExtractorPlugin.ts
|
|
3876
|
+
var ExtractorPlugin = class extends Plugin {
|
|
3877
|
+
static {
|
|
3878
|
+
__name(this, "ExtractorPlugin");
|
|
3879
|
+
}
|
|
3880
|
+
/**
|
|
3881
|
+
* The type of the plugin.
|
|
3882
|
+
*/
|
|
3883
|
+
type = "extractor" /* EXTRACTOR */;
|
|
3884
|
+
};
|
|
3885
|
+
|
|
3886
|
+
// src/struct/PluginError.ts
|
|
3887
|
+
var PluginErrorCode = /* @__PURE__ */ ((PluginErrorCode2) => {
|
|
3888
|
+
PluginErrorCode2["UNSUPPORTED_URL"] = "UNSUPPORTED_URL";
|
|
3889
|
+
PluginErrorCode2["NOT_FOUND"] = "NOT_FOUND";
|
|
3890
|
+
PluginErrorCode2["NO_STREAM"] = "NO_STREAM";
|
|
3891
|
+
PluginErrorCode2["SOURCE_UNAVAILABLE"] = "SOURCE_UNAVAILABLE";
|
|
3892
|
+
PluginErrorCode2["AGE_RESTRICTED"] = "AGE_RESTRICTED";
|
|
3893
|
+
return PluginErrorCode2;
|
|
3894
|
+
})(PluginErrorCode || {});
|
|
3895
|
+
var DEFAULT_MESSAGES = {
|
|
3896
|
+
["UNSUPPORTED_URL" /* UNSUPPORTED_URL */]: "This URL is not supported by the plugin",
|
|
3897
|
+
["NOT_FOUND" /* NOT_FOUND */]: "The song was not found",
|
|
3898
|
+
["NO_STREAM" /* NO_STREAM */]: "No playable stream was found for the song",
|
|
3899
|
+
["SOURCE_UNAVAILABLE" /* SOURCE_UNAVAILABLE */]: "The source is temporarily unavailable",
|
|
3900
|
+
["AGE_RESTRICTED" /* AGE_RESTRICTED */]: "The content is age-restricted"
|
|
3901
|
+
};
|
|
3902
|
+
var PluginError = class extends DisTubeError {
|
|
3903
|
+
static {
|
|
3904
|
+
__name(this, "PluginError");
|
|
3905
|
+
}
|
|
3906
|
+
/**
|
|
3907
|
+
* Create a plugin error.
|
|
3908
|
+
* @param code - The error code
|
|
3909
|
+
* @param message - The message. It defaults to the one of the error code.
|
|
3910
|
+
* @param options - Optional options
|
|
3911
|
+
* @param options.cause - The original error that caused this one
|
|
3912
|
+
* @example
|
|
3913
|
+
* ```ts
|
|
3914
|
+
* throw new PluginError(PluginErrorCode.NOT_FOUND);
|
|
3915
|
+
* ```
|
|
3916
|
+
*/
|
|
3917
|
+
constructor(code, message, options) {
|
|
3918
|
+
super(code, message ?? DEFAULT_MESSAGES[code]);
|
|
3919
|
+
if (options && "cause" in options) this.cause = options.cause;
|
|
3920
|
+
}
|
|
3921
|
+
/**
|
|
3922
|
+
* The name of the error, with its code.
|
|
3923
|
+
* @returns The name of the error, e.g. `PluginError [NOT_FOUND]`
|
|
3924
|
+
*/
|
|
3925
|
+
get name() {
|
|
3926
|
+
return `PluginError [${this.errorCode}]`;
|
|
3927
|
+
}
|
|
3928
|
+
};
|
|
3929
|
+
|
|
3930
|
+
// src/struct/InfoExtractorPlugin.ts
|
|
3931
|
+
var InfoExtractorPlugin = class extends ExtractorPlugin {
|
|
3932
|
+
static {
|
|
3933
|
+
__name(this, "InfoExtractorPlugin");
|
|
3934
|
+
}
|
|
3935
|
+
/**
|
|
3936
|
+
* The type of the plugin.
|
|
3937
|
+
*/
|
|
3938
|
+
type = "info-extractor" /* INFO_EXTRACTOR */;
|
|
3939
|
+
/**
|
|
3940
|
+
* The songs of this plugin are resolved to a playable version by another
|
|
3941
|
+
* plugin.
|
|
3942
|
+
*/
|
|
3943
|
+
playable = false;
|
|
3944
|
+
/**
|
|
3945
|
+
* This plugin cannot play the songs it resolves, so it never has a stream url.
|
|
3946
|
+
* @param _song - Input song
|
|
3947
|
+
* @returns Nothing: it always throws
|
|
3948
|
+
* @throws {@link PluginError} Always, with the code `NO_STREAM`
|
|
3949
|
+
*/
|
|
3950
|
+
async getStreamURL(_song) {
|
|
3951
|
+
throw new PluginError("NO_STREAM" /* NO_STREAM */);
|
|
3952
|
+
}
|
|
3953
|
+
};
|
|
3954
|
+
|
|
3955
|
+
// src/struct/PlayableExtractorPlugin.ts
|
|
3956
|
+
var PlayableExtractorPlugin = class extends ExtractorPlugin {
|
|
3957
|
+
static {
|
|
3958
|
+
__name(this, "PlayableExtractorPlugin");
|
|
3959
|
+
}
|
|
3960
|
+
/**
|
|
3961
|
+
* The type of the plugin.
|
|
3962
|
+
*/
|
|
3963
|
+
type = "playable-extractor" /* PLAYABLE_EXTRACTOR */;
|
|
3964
|
+
};
|
|
3965
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
3966
|
+
0 && (module.exports = {
|
|
3967
|
+
AUDIO_CHANNELS,
|
|
3968
|
+
AUDIO_SAMPLE_RATE,
|
|
3969
|
+
BaseManager,
|
|
3970
|
+
DEFAULT_VOLUME,
|
|
3971
|
+
DisTube,
|
|
3972
|
+
DisTubeBase,
|
|
3973
|
+
DisTubeError,
|
|
3974
|
+
DisTubeHandler,
|
|
3975
|
+
DisTubeStream,
|
|
3976
|
+
DisTubeVoice,
|
|
3977
|
+
DisTubeVoiceManager,
|
|
3978
|
+
Events,
|
|
3979
|
+
ExtractorPlugin,
|
|
3980
|
+
FilterManager,
|
|
3981
|
+
GuildIdManager,
|
|
3982
|
+
HTTP_REDIRECT_CODES,
|
|
3983
|
+
InfoExtractorPlugin,
|
|
3984
|
+
JOIN_TIMEOUT_MS,
|
|
3985
|
+
MAX_REDIRECT_DEPTH,
|
|
3986
|
+
Options,
|
|
3987
|
+
PlayableExtractorPlugin,
|
|
3988
|
+
Playlist,
|
|
3989
|
+
Plugin,
|
|
3990
|
+
PluginError,
|
|
3991
|
+
PluginErrorCode,
|
|
3992
|
+
PluginType,
|
|
3993
|
+
Queue,
|
|
3994
|
+
QueueManager,
|
|
3995
|
+
RECONNECT_MAX_ATTEMPTS,
|
|
3996
|
+
RECONNECT_TIMEOUT_MS,
|
|
3997
|
+
RepeatMode,
|
|
3998
|
+
STUCK_THRESHOLD_MS,
|
|
3999
|
+
SearchResult,
|
|
4000
|
+
SearchResultType,
|
|
4001
|
+
Song,
|
|
4002
|
+
StreamServer,
|
|
4003
|
+
TaskQueue,
|
|
4004
|
+
checkEncryptionLibraries,
|
|
4005
|
+
checkFFmpeg,
|
|
4006
|
+
checkIntents,
|
|
4007
|
+
checkInvalidKey,
|
|
4008
|
+
defaultFilters,
|
|
4009
|
+
defaultOptions,
|
|
4010
|
+
formatDuration,
|
|
4011
|
+
handleUnhandledError,
|
|
4012
|
+
isClientInstance,
|
|
4013
|
+
isGuildInstance,
|
|
4014
|
+
isMemberInstance,
|
|
4015
|
+
isMessageInstance,
|
|
4016
|
+
isNsfwChannel,
|
|
4017
|
+
isObject,
|
|
4018
|
+
isSnowflake,
|
|
4019
|
+
isSupportedVoiceChannel,
|
|
4020
|
+
isTextChannelInstance,
|
|
4021
|
+
isTruthy,
|
|
4022
|
+
isURL,
|
|
4023
|
+
isVoiceChannelEmpty,
|
|
4024
|
+
objectKeys,
|
|
4025
|
+
resolveGuildId,
|
|
4026
|
+
version
|
|
4027
|
+
});
|
|
4028
|
+
//# sourceMappingURL=index.js.map
|