@kattowo/distube 0.0.0-stage → 6.0.1

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/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.1";
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