@kattowo/distube 0.0.0-stage → 6.0.0

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