distube 5.0.7 → 5.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -80,37 +80,8 @@ __export(index_exports, {
80
80
  });
81
81
  module.exports = __toCommonJS(index_exports);
82
82
 
83
- // src/type.ts
84
- var Events = /* @__PURE__ */ ((Events2) => {
85
- Events2["ERROR"] = "error";
86
- Events2["ADD_LIST"] = "addList";
87
- Events2["ADD_SONG"] = "addSong";
88
- Events2["PLAY_SONG"] = "playSong";
89
- Events2["FINISH_SONG"] = "finishSong";
90
- Events2["EMPTY"] = "empty";
91
- Events2["FINISH"] = "finish";
92
- Events2["INIT_QUEUE"] = "initQueue";
93
- Events2["NO_RELATED"] = "noRelated";
94
- Events2["DISCONNECT"] = "disconnect";
95
- Events2["DELETE_QUEUE"] = "deleteQueue";
96
- Events2["FFMPEG_DEBUG"] = "ffmpegDebug";
97
- Events2["DEBUG"] = "debug";
98
- return Events2;
99
- })(Events || {});
100
- var RepeatMode = /* @__PURE__ */ ((RepeatMode2) => {
101
- RepeatMode2[RepeatMode2["DISABLED"] = 0] = "DISABLED";
102
- RepeatMode2[RepeatMode2["SONG"] = 1] = "SONG";
103
- RepeatMode2[RepeatMode2["QUEUE"] = 2] = "QUEUE";
104
- return RepeatMode2;
105
- })(RepeatMode || {});
106
- var PluginType = /* @__PURE__ */ ((PluginType2) => {
107
- PluginType2["EXTRACTOR"] = "extractor";
108
- PluginType2["INFO_EXTRACTOR"] = "info-extractor";
109
- PluginType2["PLAYABLE_EXTRACTOR"] = "playable-extractor";
110
- return PluginType2;
111
- })(PluginType || {});
112
-
113
83
  // src/constant.ts
84
+ var version = "5.1.1";
114
85
  var defaultFilters = {
115
86
  "3d": "apulsator=hz=0.125",
116
87
  bassboost: "bass=g=10",
@@ -138,6 +109,80 @@ var defaultOptions = {
138
109
  joinNewVoiceChannel: true
139
110
  };
140
111
 
112
+ // src/core/DisTubeBase.ts
113
+ var DisTubeBase = class {
114
+ static {
115
+ __name(this, "DisTubeBase");
116
+ }
117
+ distube;
118
+ constructor(distube) {
119
+ this.distube = distube;
120
+ }
121
+ /**
122
+ * Emit the {@link DisTube} of this base
123
+ * @param eventName - Event name
124
+ * @param args - arguments
125
+ */
126
+ emit(eventName, ...args) {
127
+ return this.distube.emit(eventName, ...args);
128
+ }
129
+ /**
130
+ * Emit error event
131
+ * @param error - error
132
+ * @param queue - The queue encountered the error
133
+ * @param song - The playing song when encountered the error
134
+ */
135
+ emitError(error, queue, song) {
136
+ this.distube.emitError(error, queue, song);
137
+ }
138
+ /**
139
+ * Emit debug event
140
+ * @param message - debug message
141
+ */
142
+ debug(message) {
143
+ this.distube.debug(message);
144
+ }
145
+ /**
146
+ * The queue manager
147
+ */
148
+ get queues() {
149
+ return this.distube.queues;
150
+ }
151
+ /**
152
+ * The voice manager
153
+ */
154
+ get voices() {
155
+ return this.distube.voices;
156
+ }
157
+ /**
158
+ * Discord.js client
159
+ */
160
+ get client() {
161
+ return this.distube.client;
162
+ }
163
+ /**
164
+ * DisTube options
165
+ */
166
+ get options() {
167
+ return this.distube.options;
168
+ }
169
+ /**
170
+ * DisTube handler
171
+ */
172
+ get handler() {
173
+ return this.distube.handler;
174
+ }
175
+ /**
176
+ * DisTube plugins
177
+ */
178
+ get plugins() {
179
+ return this.distube.plugins;
180
+ }
181
+ };
182
+
183
+ // src/core/DisTubeHandler.ts
184
+ var import_undici = require("undici");
185
+
141
186
  // src/struct/DisTubeError.ts
142
187
  var import_node_util = require("util");
143
188
  var ERROR_MESSAGES = {
@@ -210,1912 +255,1832 @@ var DisTubeError = class _DisTubeError extends Error {
210
255
  }
211
256
  };
212
257
 
213
- // src/struct/TaskQueue.ts
214
- var Task = class {
258
+ // src/util.ts
259
+ var import_node_url = require("url");
260
+ var import_discord3 = require("discord.js");
261
+
262
+ // src/core/DisTubeVoice.ts
263
+ var import_voice = require("@discordjs/voice");
264
+ var import_discord = require("discord.js");
265
+ var import_tiny_typed_emitter = require("tiny-typed-emitter");
266
+ var DisTubeVoice = class extends import_tiny_typed_emitter.TypedEmitter {
215
267
  static {
216
- __name(this, "Task");
268
+ __name(this, "DisTubeVoice");
217
269
  }
218
- resolve;
219
- promise;
220
- isPlay;
221
- constructor(isPlay) {
222
- this.isPlay = isPlay;
223
- this.promise = new Promise((res) => {
224
- this.resolve = res;
270
+ id;
271
+ voices;
272
+ audioPlayer;
273
+ connection;
274
+ emittedError;
275
+ isDisconnected = false;
276
+ stream;
277
+ pausingStream;
278
+ #channel;
279
+ #volume = 100;
280
+ constructor(voiceManager, channel) {
281
+ super();
282
+ this.voices = voiceManager;
283
+ this.id = channel.guildId;
284
+ this.channel = channel;
285
+ this.voices.add(this.id, this);
286
+ this.audioPlayer = (0, import_voice.createAudioPlayer)().on(import_voice.AudioPlayerStatus.Idle, (oldState) => {
287
+ if (oldState.status !== import_voice.AudioPlayerStatus.Idle) this.emit("finish");
288
+ }).on("error", (error) => {
289
+ if (this.emittedError) return;
290
+ this.emittedError = true;
291
+ this.emit("error", error);
225
292
  });
226
- }
227
- };
228
- var TaskQueue = class {
229
- static {
230
- __name(this, "TaskQueue");
293
+ this.connection.on(import_voice.VoiceConnectionStatus.Disconnected, (_, newState) => {
294
+ if (newState.reason === import_voice.VoiceConnectionDisconnectReason.Manual) {
295
+ this.leave();
296
+ } else if (newState.reason === import_voice.VoiceConnectionDisconnectReason.WebSocketClose && newState.closeCode === 4014) {
297
+ (0, import_voice.entersState)(this.connection, import_voice.VoiceConnectionStatus.Connecting, 5e3).catch(() => {
298
+ if (![import_voice.VoiceConnectionStatus.Ready, import_voice.VoiceConnectionStatus.Connecting].includes(this.connection.state.status)) {
299
+ this.leave();
300
+ }
301
+ });
302
+ } else if (this.connection.rejoinAttempts < 5) {
303
+ setTimeout(
304
+ () => {
305
+ this.connection.rejoin();
306
+ },
307
+ (this.connection.rejoinAttempts + 1) * 5e3
308
+ ).unref();
309
+ } else if (this.connection.state.status !== import_voice.VoiceConnectionStatus.Destroyed) {
310
+ this.leave(new DisTubeError("VOICE_RECONNECT_FAILED"));
311
+ }
312
+ }).on(import_voice.VoiceConnectionStatus.Destroyed, () => {
313
+ this.leave();
314
+ }).on("error", () => void 0);
315
+ this.connection.subscribe(this.audioPlayer);
231
316
  }
232
317
  /**
233
- * The task array
318
+ * The voice channel id the bot is in
234
319
  */
235
- #tasks = [];
320
+ get channelId() {
321
+ return this.connection?.joinConfig?.channelId ?? void 0;
322
+ }
323
+ get channel() {
324
+ if (!this.channelId) return this.#channel;
325
+ if (this.#channel?.id === this.channelId) return this.#channel;
326
+ const channel = this.voices.client.channels.cache.get(this.channelId);
327
+ if (!channel) return this.#channel;
328
+ for (const type of import_discord.Constants.VoiceBasedChannelTypes) {
329
+ if (channel.type === type) {
330
+ this.#channel = channel;
331
+ return channel;
332
+ }
333
+ }
334
+ return this.#channel;
335
+ }
336
+ set channel(channel) {
337
+ if (!isSupportedVoiceChannel(channel)) {
338
+ throw new DisTubeError("INVALID_TYPE", "BaseGuildVoiceChannel", channel, "DisTubeVoice#channel");
339
+ }
340
+ if (channel.guildId !== this.id) throw new DisTubeError("VOICE_DIFFERENT_GUILD");
341
+ if (channel.client.user?.id !== this.voices.client.user?.id) throw new DisTubeError("VOICE_DIFFERENT_CLIENT");
342
+ if (channel.id === this.channelId) return;
343
+ if (!channel.joinable) {
344
+ if (channel.full) throw new DisTubeError("VOICE_FULL");
345
+ else throw new DisTubeError("VOICE_MISSING_PERMS");
346
+ }
347
+ this.connection = this.#join(channel);
348
+ this.#channel = channel;
349
+ }
350
+ #join(channel) {
351
+ return (0, import_voice.joinVoiceChannel)({
352
+ channelId: channel.id,
353
+ guildId: this.id,
354
+ adapterCreator: channel.guild.voiceAdapterCreator,
355
+ group: channel.client.user?.id
356
+ });
357
+ }
236
358
  /**
237
- * Waits for last task finished and queues a new task
359
+ * Join a voice channel with this connection
360
+ * @param channel - A voice channel
238
361
  */
239
- queuing(isPlay = false) {
240
- const next = this.remaining ? this.#tasks[this.#tasks.length - 1].promise : Promise.resolve();
241
- this.#tasks.push(new Task(isPlay));
242
- return next;
362
+ async join(channel) {
363
+ const TIMEOUT = 3e4;
364
+ if (channel) this.channel = channel;
365
+ try {
366
+ await (0, import_voice.entersState)(this.connection, import_voice.VoiceConnectionStatus.Ready, TIMEOUT);
367
+ } catch {
368
+ if (this.connection.state.status === import_voice.VoiceConnectionStatus.Ready) return this;
369
+ if (this.connection.state.status !== import_voice.VoiceConnectionStatus.Destroyed) this.connection.destroy();
370
+ this.voices.remove(this.id);
371
+ throw new DisTubeError("VOICE_CONNECT_FAILED", TIMEOUT / 1e3);
372
+ }
373
+ return this;
243
374
  }
244
375
  /**
245
- * Removes the finished task and processes the next task
376
+ * Leave the voice channel of this connection
377
+ * @param error - Optional, an error to emit with 'error' event.
246
378
  */
247
- resolve() {
248
- this.#tasks.shift()?.resolve();
379
+ leave(error) {
380
+ this.stop(true);
381
+ if (!this.isDisconnected) {
382
+ this.emit("disconnect", error);
383
+ this.isDisconnected = true;
384
+ }
385
+ if (this.connection.state.status !== import_voice.VoiceConnectionStatus.Destroyed) this.connection.destroy();
386
+ this.voices.remove(this.id);
249
387
  }
250
388
  /**
251
- * The remaining number of tasks
389
+ * Stop the playing stream
390
+ * @param force - If true, will force the {@link DisTubeVoice#audioPlayer} to enter the Idle state even
391
+ * if the {@link DisTubeStream#audioResource} has silence padding frames.
252
392
  */
253
- get remaining() {
254
- return this.#tasks.length;
393
+ stop(force = false) {
394
+ this.audioPlayer.stop(force);
255
395
  }
256
396
  /**
257
- * Whether or not having a play task
258
- */
259
- get hasPlayTask() {
260
- return this.#tasks.some((t) => t.isPlay);
397
+ * Play a {@link DisTubeStream}
398
+ * @param dtStream - DisTubeStream
399
+ */
400
+ async play(dtStream) {
401
+ if (!await checkEncryptionLibraries()) {
402
+ dtStream.kill();
403
+ throw new DisTubeError("ENCRYPTION_LIBRARIES_MISSING");
404
+ }
405
+ this.emittedError = false;
406
+ dtStream.on("error", (error) => {
407
+ if (this.emittedError || error.code === "ERR_STREAM_PREMATURE_CLOSE") return;
408
+ this.emittedError = true;
409
+ this.emit("error", error);
410
+ });
411
+ if (this.audioPlayer.state.status !== import_voice.AudioPlayerStatus.Paused) {
412
+ this.audioPlayer.play(dtStream.audioResource);
413
+ this.stream?.kill();
414
+ dtStream.spawn();
415
+ } else if (!this.pausingStream) {
416
+ this.pausingStream = this.stream;
417
+ }
418
+ this.stream = dtStream;
419
+ this.volume = this.#volume;
261
420
  }
262
- };
263
-
264
- // src/struct/Playlist.ts
265
- var Playlist = class {
266
- static {
267
- __name(this, "Playlist");
421
+ set volume(volume) {
422
+ if (typeof volume !== "number" || Number.isNaN(volume)) {
423
+ throw new DisTubeError("INVALID_TYPE", "number", volume, "volume");
424
+ }
425
+ if (volume < 0) {
426
+ throw new DisTubeError("NUMBER_COMPARE", "Volume", "bigger or equal to", 0);
427
+ }
428
+ this.#volume = volume;
429
+ this.stream?.setVolume((this.#volume / 100) ** (0.5 / Math.log10(2)));
268
430
  }
269
431
  /**
270
- * Playlist source.
271
- */
272
- source;
273
- /**
274
- * Songs in the playlist.
275
- */
276
- songs;
277
- /**
278
- * Playlist ID.
279
- */
280
- id;
281
- /**
282
- * Playlist name.
432
+ * Get or set the volume percentage
283
433
  */
284
- name;
434
+ get volume() {
435
+ return this.#volume;
436
+ }
285
437
  /**
286
- * Playlist URL.
438
+ * Playback duration of the audio resource in seconds
287
439
  */
288
- url;
440
+ get playbackDuration() {
441
+ return (this.stream?.audioResource?.playbackDuration ?? 0) / 1e3;
442
+ }
443
+ pause() {
444
+ this.audioPlayer.pause();
445
+ }
446
+ unpause() {
447
+ const state = this.audioPlayer.state;
448
+ if (state.status !== import_voice.AudioPlayerStatus.Paused) return;
449
+ if (this.stream?.audioResource && state.resource !== this.stream.audioResource) {
450
+ this.audioPlayer.play(this.stream.audioResource);
451
+ this.stream.spawn();
452
+ this.pausingStream?.kill();
453
+ delete this.pausingStream;
454
+ } else {
455
+ this.audioPlayer.unpause();
456
+ }
457
+ }
289
458
  /**
290
- * Playlist thumbnail.
459
+ * Whether the bot is self-deafened
291
460
  */
292
- thumbnail;
293
- #metadata;
294
- #member;
461
+ get selfDeaf() {
462
+ return this.connection.joinConfig.selfDeaf;
463
+ }
295
464
  /**
296
- * Create a Playlist
297
- * @param playlist - Raw playlist info
298
- * @param options - Optional data
465
+ * Whether the bot is self-muted
299
466
  */
300
- constructor(playlist, { member, metadata } = {}) {
301
- if (!Array.isArray(playlist.songs) || !playlist.songs.length) throw new DisTubeError("EMPTY_PLAYLIST");
302
- this.source = playlist.source.toLowerCase();
303
- this.songs = playlist.songs;
304
- this.name = playlist.name;
305
- this.id = playlist.id;
306
- this.url = playlist.url;
307
- this.thumbnail = playlist.thumbnail;
308
- this.member = member;
309
- this.songs.forEach((s) => s.playlist = this);
310
- this.metadata = metadata;
467
+ get selfMute() {
468
+ return this.connection.joinConfig.selfMute;
311
469
  }
312
470
  /**
313
- * Playlist duration in second.
471
+ * Self-deafens/undeafens the bot.
472
+ * @param selfDeaf - Whether or not the bot should be self-deafened
473
+ * @returns true if the voice state was successfully updated, otherwise false
314
474
  */
315
- get duration() {
316
- return this.songs.reduce((prev, next) => prev + next.duration, 0);
475
+ setSelfDeaf(selfDeaf) {
476
+ if (typeof selfDeaf !== "boolean") {
477
+ throw new DisTubeError("INVALID_TYPE", "boolean", selfDeaf, "selfDeaf");
478
+ }
479
+ return this.connection.rejoin({
480
+ ...this.connection.joinConfig,
481
+ selfDeaf
482
+ });
317
483
  }
318
484
  /**
319
- * Formatted duration string `hh:mm:ss`.
485
+ * Self-mutes/unmutes the bot.
486
+ * @param selfMute - Whether or not the bot should be self-muted
487
+ * @returns true if the voice state was successfully updated, otherwise false
320
488
  */
321
- get formattedDuration() {
322
- return formatDuration(this.duration);
489
+ setSelfMute(selfMute) {
490
+ if (typeof selfMute !== "boolean") {
491
+ throw new DisTubeError("INVALID_TYPE", "boolean", selfMute, "selfMute");
492
+ }
493
+ return this.connection.rejoin({
494
+ ...this.connection.joinConfig,
495
+ selfMute
496
+ });
323
497
  }
324
498
  /**
325
- * User requested.
499
+ * The voice state of this connection
326
500
  */
327
- get member() {
328
- return this.#member;
501
+ get voiceState() {
502
+ return this.channel?.guild?.members?.me?.voice;
329
503
  }
330
- set member(member) {
331
- if (!isMemberInstance(member)) return;
332
- this.#member = member;
333
- this.songs.forEach((s) => s.member = this.member);
504
+ };
505
+
506
+ // src/core/manager/BaseManager.ts
507
+ var import_discord2 = require("discord.js");
508
+ var BaseManager = class extends DisTubeBase {
509
+ static {
510
+ __name(this, "BaseManager");
334
511
  }
335
512
  /**
336
- * User requested.
513
+ * The collection of items for this manager.
337
514
  */
338
- get user() {
339
- return this.member?.user;
340
- }
515
+ collection = new import_discord2.Collection();
341
516
  /**
342
- * Optional metadata that can be used to identify the playlist.
517
+ * The size of the collection.
343
518
  */
344
- get metadata() {
345
- return this.#metadata;
346
- }
347
- set metadata(metadata) {
348
- this.#metadata = metadata;
349
- this.songs.forEach((s) => s.metadata = metadata);
350
- }
351
- toString() {
352
- return `${this.name} (${this.songs.length} songs)`;
519
+ get size() {
520
+ return this.collection.size;
353
521
  }
354
522
  };
355
523
 
356
- // src/struct/Song.ts
357
- var Song = class {
524
+ // src/core/manager/FilterManager.ts
525
+ var FilterManager = class extends BaseManager {
358
526
  static {
359
- __name(this, "Song");
527
+ __name(this, "FilterManager");
360
528
  }
361
529
  /**
362
- * The source of this song info
530
+ * The queue to manage
363
531
  */
364
- source;
532
+ queue;
533
+ constructor(queue) {
534
+ super(queue.distube);
535
+ this.queue = queue;
536
+ }
537
+ #resolve(filter) {
538
+ if (typeof filter === "object" && typeof filter.name === "string" && typeof filter.value === "string") {
539
+ return filter;
540
+ }
541
+ if (typeof filter === "string" && Object.hasOwn(this.distube.filters, filter)) {
542
+ return {
543
+ name: filter,
544
+ value: this.distube.filters[filter]
545
+ };
546
+ }
547
+ throw new DisTubeError("INVALID_TYPE", "FilterResolvable", filter, "filter");
548
+ }
549
+ #apply() {
550
+ this.queue._beginTime = this.queue.currentTime;
551
+ this.queue.play(false);
552
+ }
365
553
  /**
366
- * Song ID.
554
+ * Enable a filter or multiple filters to the manager
555
+ * @param filterOrFilters - The filter or filters to enable
556
+ * @param override - Wether or not override the applied filter with new filter value
367
557
  */
368
- id;
558
+ add(filterOrFilters, override = false) {
559
+ if (Array.isArray(filterOrFilters)) {
560
+ for (const filter of filterOrFilters) {
561
+ const ft = this.#resolve(filter);
562
+ if (override || !this.has(ft)) this.collection.set(ft.name, ft);
563
+ }
564
+ } else {
565
+ const ft = this.#resolve(filterOrFilters);
566
+ if (override || !this.has(ft)) this.collection.set(ft.name, ft);
567
+ }
568
+ this.#apply();
569
+ return this;
570
+ }
369
571
  /**
370
- * Song name.
572
+ * Clear enabled filters of the manager
371
573
  */
372
- name;
574
+ clear() {
575
+ return this.set([]);
576
+ }
373
577
  /**
374
- * Indicates if the song is an active live.
578
+ * Set the filters applied to the manager
579
+ * @param filters - The filters to apply
375
580
  */
376
- isLive;
581
+ set(filters) {
582
+ if (!Array.isArray(filters)) throw new DisTubeError("INVALID_TYPE", "Array<FilterResolvable>", filters, "filters");
583
+ this.collection.clear();
584
+ for (const f of filters) {
585
+ const filter = this.#resolve(f);
586
+ this.collection.set(filter.name, filter);
587
+ }
588
+ this.#apply();
589
+ return this;
590
+ }
591
+ #removeFn(f) {
592
+ return this.collection.delete(this.#resolve(f).name);
593
+ }
377
594
  /**
378
- * Song duration.
595
+ * Disable a filter or multiple filters
596
+ * @param filterOrFilters - The filter or filters to disable
379
597
  */
380
- duration;
598
+ remove(filterOrFilters) {
599
+ if (Array.isArray(filterOrFilters)) filterOrFilters.forEach((f) => this.#removeFn(f));
600
+ else this.#removeFn(filterOrFilters);
601
+ this.#apply();
602
+ return this;
603
+ }
381
604
  /**
382
- * Formatted duration string (`hh:mm:ss`, `mm:ss` or `Live`).
605
+ * Check whether a filter enabled or not
606
+ * @param filter - The filter to check
383
607
  */
384
- formattedDuration;
608
+ has(filter) {
609
+ return this.collection.has(typeof filter === "string" ? filter : this.#resolve(filter).name);
610
+ }
385
611
  /**
386
- * Song URL.
612
+ * Array of enabled filter names
387
613
  */
388
- url;
614
+ get names() {
615
+ return [...this.collection.keys()];
616
+ }
389
617
  /**
390
- * Song thumbnail.
618
+ * Array of enabled filters
391
619
  */
392
- thumbnail;
620
+ get values() {
621
+ return [...this.collection.values()];
622
+ }
623
+ get ffmpegArgs() {
624
+ return this.size ? { af: this.values.map((f) => f.value).join(",") } : {};
625
+ }
626
+ toString() {
627
+ return this.names.toString();
628
+ }
629
+ };
630
+
631
+ // src/type.ts
632
+ var Events = /* @__PURE__ */ ((Events2) => {
633
+ Events2["ERROR"] = "error";
634
+ Events2["ADD_LIST"] = "addList";
635
+ Events2["ADD_SONG"] = "addSong";
636
+ Events2["PLAY_SONG"] = "playSong";
637
+ Events2["FINISH_SONG"] = "finishSong";
638
+ Events2["EMPTY"] = "empty";
639
+ Events2["FINISH"] = "finish";
640
+ Events2["INIT_QUEUE"] = "initQueue";
641
+ Events2["NO_RELATED"] = "noRelated";
642
+ Events2["DISCONNECT"] = "disconnect";
643
+ Events2["DELETE_QUEUE"] = "deleteQueue";
644
+ Events2["FFMPEG_DEBUG"] = "ffmpegDebug";
645
+ Events2["DEBUG"] = "debug";
646
+ return Events2;
647
+ })(Events || {});
648
+ var RepeatMode = /* @__PURE__ */ ((RepeatMode2) => {
649
+ RepeatMode2[RepeatMode2["DISABLED"] = 0] = "DISABLED";
650
+ RepeatMode2[RepeatMode2["SONG"] = 1] = "SONG";
651
+ RepeatMode2[RepeatMode2["QUEUE"] = 2] = "QUEUE";
652
+ return RepeatMode2;
653
+ })(RepeatMode || {});
654
+ var PluginType = /* @__PURE__ */ ((PluginType2) => {
655
+ PluginType2["EXTRACTOR"] = "extractor";
656
+ PluginType2["INFO_EXTRACTOR"] = "info-extractor";
657
+ PluginType2["PLAYABLE_EXTRACTOR"] = "playable-extractor";
658
+ return PluginType2;
659
+ })(PluginType || {});
660
+
661
+ // src/struct/TaskQueue.ts
662
+ var Task = class {
663
+ static {
664
+ __name(this, "Task");
665
+ }
666
+ resolve;
667
+ promise;
668
+ isPlay;
669
+ constructor(isPlay) {
670
+ this.isPlay = isPlay;
671
+ this.promise = new Promise((res) => {
672
+ this.resolve = res;
673
+ });
674
+ }
675
+ };
676
+ var TaskQueue = class {
677
+ static {
678
+ __name(this, "TaskQueue");
679
+ }
393
680
  /**
394
- * Song view count
681
+ * The task array
395
682
  */
396
- views;
683
+ #tasks = [];
397
684
  /**
398
- * Song like count
685
+ * Waits for last task finished and queues a new task
399
686
  */
400
- likes;
687
+ queuing(isPlay = false) {
688
+ const next = this.remaining ? this.#tasks[this.#tasks.length - 1].promise : Promise.resolve();
689
+ this.#tasks.push(new Task(isPlay));
690
+ return next;
691
+ }
401
692
  /**
402
- * Song dislike count
693
+ * Removes the finished task and processes the next task
403
694
  */
404
- dislikes;
695
+ resolve() {
696
+ this.#tasks.shift()?.resolve();
697
+ }
405
698
  /**
406
- * Song repost (share) count
699
+ * The remaining number of tasks
407
700
  */
408
- reposts;
701
+ get remaining() {
702
+ return this.#tasks.length;
703
+ }
409
704
  /**
410
- * Song uploader
705
+ * Whether or not having a play task
411
706
  */
412
- uploader;
707
+ get hasPlayTask() {
708
+ return this.#tasks.some((t) => t.isPlay);
709
+ }
710
+ };
711
+
712
+ // src/struct/Queue.ts
713
+ var Queue = class extends DisTubeBase {
714
+ static {
715
+ __name(this, "Queue");
716
+ }
413
717
  /**
414
- * Whether or not an age-restricted content
718
+ * Queue id (Guild id)
415
719
  */
416
- ageRestricted;
720
+ id;
417
721
  /**
418
- * Stream info
722
+ * Voice connection of this queue.
419
723
  */
420
- stream;
724
+ voice;
421
725
  /**
422
- * The plugin that created this song
726
+ * List of songs in the queue (The first one is the playing song)
423
727
  */
424
- plugin;
425
- #metadata;
426
- #member;
427
- #playlist;
728
+ songs;
428
729
  /**
429
- * Create a Song
430
- *
431
- * @param info - Raw song info
432
- * @param options - Optional data
730
+ * List of the previous songs.
433
731
  */
434
- constructor(info, { member, metadata } = {}) {
435
- this.source = info.source.toLowerCase();
436
- this.metadata = metadata;
437
- this.member = member;
438
- this.id = info.id;
439
- this.name = info.name;
440
- this.isLive = info.isLive;
441
- this.duration = this.isLive || !info.duration ? 0 : info.duration;
442
- this.formattedDuration = this.isLive ? "Live" : formatDuration(this.duration);
443
- this.url = info.url;
444
- this.thumbnail = info.thumbnail;
445
- this.views = info.views;
446
- this.likes = info.likes;
447
- this.dislikes = info.dislikes;
448
- this.reposts = info.reposts;
449
- this.uploader = {
450
- name: info.uploader?.name,
451
- url: info.uploader?.url
452
- };
453
- this.ageRestricted = info.ageRestricted;
454
- this.stream = { playFromSource: info.playFromSource };
455
- this.plugin = info.plugin;
456
- }
732
+ previousSongs;
457
733
  /**
458
- * The playlist this song belongs to
734
+ * Whether stream is currently stopped.
459
735
  */
460
- get playlist() {
461
- return this.#playlist;
462
- }
463
- set playlist(playlist) {
464
- if (!(playlist instanceof Playlist)) throw new DisTubeError("INVALID_TYPE", "Playlist", playlist, "Song#playlist");
465
- this.#playlist = playlist;
466
- this.member = playlist.member;
467
- }
736
+ stopped;
468
737
  /**
469
- * User requested to play this song.
738
+ * Whether or not the stream is currently playing.
470
739
  */
471
- get member() {
472
- return this.#member;
473
- }
474
- set member(member) {
475
- if (isMemberInstance(member)) this.#member = member;
476
- }
740
+ playing;
477
741
  /**
478
- * User requested to play this song.
742
+ * Whether or not the stream is currently paused.
479
743
  */
480
- get user() {
481
- return this.member?.user;
482
- }
744
+ paused;
483
745
  /**
484
- * Optional metadata that can be used to identify the song. This is attached by the
485
- * {@link DisTube#play} method.
746
+ * Type of repeat mode (`0` is disabled, `1` is repeating a song, `2` is repeating
747
+ * all the queue). Default value: `0` (disabled)
486
748
  */
487
- get metadata() {
488
- return this.#metadata;
489
- }
490
- set metadata(metadata) {
491
- this.#metadata = metadata;
492
- }
493
- toString() {
494
- return this.name || this.url || this.id || "Unknown";
495
- }
496
- };
497
-
498
- // src/core/DisTubeBase.ts
499
- var DisTubeBase = class {
500
- static {
501
- __name(this, "DisTubeBase");
502
- }
503
- distube;
504
- constructor(distube) {
505
- this.distube = distube;
506
- }
749
+ repeatMode;
507
750
  /**
508
- * Emit the {@link DisTube} of this base
509
- * @param eventName - Event name
510
- * @param args - arguments
751
+ * Whether or not the autoplay mode is enabled. Default value: `false`
511
752
  */
512
- emit(eventName, ...args) {
513
- return this.distube.emit(eventName, ...args);
514
- }
753
+ autoplay;
515
754
  /**
516
- * Emit error event
517
- * @param error - error
518
- * @param queue - The queue encountered the error
519
- * @param song - The playing song when encountered the error
755
+ * FFmpeg arguments for the current queue. Default value is defined with {@link DisTubeOptions}.ffmpeg.args.
756
+ * `af` output argument will be replaced with {@link Queue#filters} manager
520
757
  */
521
- emitError(error, queue, song) {
522
- this.distube.emitError(error, queue, song);
523
- }
758
+ ffmpegArgs;
524
759
  /**
525
- * Emit debug event
526
- * @param message - debug message
760
+ * The text channel of the Queue. (Default: where the first command is called).
527
761
  */
528
- debug(message) {
529
- this.distube.debug(message);
530
- }
762
+ textChannel;
531
763
  /**
532
- * The queue manager
764
+ * What time in the song to begin (in seconds).
533
765
  */
534
- get queues() {
535
- return this.distube.queues;
536
- }
766
+ _beginTime;
767
+ #filters;
537
768
  /**
538
- * The voice manager
769
+ * Whether or not the queue is being updated manually (skip, jump, previous)
539
770
  */
540
- get voices() {
541
- return this.distube.voices;
542
- }
771
+ _manualUpdate;
543
772
  /**
544
- * Discord.js client
773
+ * Task queuing system
545
774
  */
546
- get client() {
547
- return this.distube.client;
548
- }
775
+ _taskQueue;
549
776
  /**
550
- * DisTube options
777
+ * {@link DisTubeVoice} listener
551
778
  */
552
- get options() {
553
- return this.distube.options;
554
- }
779
+ _listeners;
555
780
  /**
556
- * DisTube handler
781
+ * Create a queue for the guild
782
+ * @param distube - DisTube
783
+ * @param voice - Voice connection
784
+ * @param textChannel - Default text channel
557
785
  */
558
- get handler() {
559
- return this.distube.handler;
560
- }
561
- /**
562
- * DisTube plugins
563
- */
564
- get plugins() {
565
- return this.distube.plugins;
566
- }
567
- };
568
-
569
- // src/core/DisTubeVoice.ts
570
- var import_discord = require("discord.js");
571
- var import_tiny_typed_emitter = require("tiny-typed-emitter");
572
- var import_voice = require("@discordjs/voice");
573
- var DisTubeVoice = class extends import_tiny_typed_emitter.TypedEmitter {
574
- static {
575
- __name(this, "DisTubeVoice");
576
- }
577
- id;
578
- voices;
579
- audioPlayer;
580
- connection;
581
- emittedError;
582
- isDisconnected = false;
583
- stream;
584
- pausingStream;
585
- #channel;
586
- #volume = 100;
587
- constructor(voiceManager, channel) {
588
- super();
589
- this.voices = voiceManager;
590
- this.id = channel.guildId;
591
- this.channel = channel;
592
- this.voices.add(this.id, this);
593
- this.audioPlayer = (0, import_voice.createAudioPlayer)().on(import_voice.AudioPlayerStatus.Idle, (oldState) => {
594
- if (oldState.status !== import_voice.AudioPlayerStatus.Idle) this.emit("finish");
595
- }).on("error", (error) => {
596
- if (this.emittedError) return;
597
- this.emittedError = true;
598
- this.emit("error", error);
599
- });
600
- this.connection.on(import_voice.VoiceConnectionStatus.Disconnected, (_, newState) => {
601
- if (newState.reason === import_voice.VoiceConnectionDisconnectReason.Manual) {
602
- this.leave();
603
- } else if (newState.reason === import_voice.VoiceConnectionDisconnectReason.WebSocketClose && newState.closeCode === 4014) {
604
- (0, import_voice.entersState)(this.connection, import_voice.VoiceConnectionStatus.Connecting, 5e3).catch(() => {
605
- if (![import_voice.VoiceConnectionStatus.Ready, import_voice.VoiceConnectionStatus.Connecting].includes(this.connection.state.status)) {
606
- this.leave();
607
- }
608
- });
609
- } else if (this.connection.rejoinAttempts < 5) {
610
- setTimeout(
611
- () => {
612
- this.connection.rejoin();
613
- },
614
- (this.connection.rejoinAttempts + 1) * 5e3
615
- ).unref();
616
- } else if (this.connection.state.status !== import_voice.VoiceConnectionStatus.Destroyed) {
617
- this.leave(new DisTubeError("VOICE_RECONNECT_FAILED"));
618
- }
619
- }).on(import_voice.VoiceConnectionStatus.Destroyed, () => {
620
- this.leave();
621
- }).on("error", () => void 0);
622
- this.connection.subscribe(this.audioPlayer);
623
- }
624
- /**
625
- * The voice channel id the bot is in
626
- */
627
- get channelId() {
628
- return this.connection?.joinConfig?.channelId ?? void 0;
786
+ constructor(distube, voice, textChannel) {
787
+ super(distube);
788
+ this.voice = voice;
789
+ this.id = voice.id;
790
+ this.volume = 50;
791
+ this.songs = [];
792
+ this.previousSongs = [];
793
+ this.stopped = false;
794
+ this._manualUpdate = false;
795
+ this.playing = false;
796
+ this.paused = false;
797
+ this.repeatMode = 0 /* DISABLED */;
798
+ this.autoplay = false;
799
+ this.#filters = new FilterManager(this);
800
+ this._beginTime = 0;
801
+ this.textChannel = textChannel;
802
+ this._taskQueue = new TaskQueue();
803
+ this._listeners = void 0;
804
+ this.ffmpegArgs = {
805
+ global: { ...this.options.ffmpeg.args.global },
806
+ input: { ...this.options.ffmpeg.args.input },
807
+ output: { ...this.options.ffmpeg.args.output }
808
+ };
629
809
  }
630
- get channel() {
631
- if (!this.channelId) return this.#channel;
632
- if (this.#channel?.id === this.channelId) return this.#channel;
633
- const channel = this.voices.client.channels.cache.get(this.channelId);
634
- if (!channel) return this.#channel;
635
- for (const type of import_discord.Constants.VoiceBasedChannelTypes) {
636
- if (channel.type === type) {
637
- this.#channel = channel;
638
- return channel;
810
+ #addToPreviousSongs(songs) {
811
+ if (Array.isArray(songs)) {
812
+ if (this.options.savePreviousSongs) {
813
+ this.previousSongs.push(...songs);
814
+ } else {
815
+ this.previousSongs.push(...songs.map((s) => ({ id: s.id })));
639
816
  }
817
+ } else if (this.options.savePreviousSongs) {
818
+ this.previousSongs.push(songs);
819
+ } else {
820
+ this.previousSongs.push({ id: songs.id });
640
821
  }
641
- return this.#channel;
642
822
  }
643
- set channel(channel) {
644
- if (!isSupportedVoiceChannel(channel)) {
645
- throw new DisTubeError("INVALID_TYPE", "BaseGuildVoiceChannel", channel, "DisTubeVoice#channel");
646
- }
647
- if (channel.guildId !== this.id) throw new DisTubeError("VOICE_DIFFERENT_GUILD");
648
- if (channel.client.user?.id !== this.voices.client.user?.id) throw new DisTubeError("VOICE_DIFFERENT_CLIENT");
649
- if (channel.id === this.channelId) return;
650
- if (!channel.joinable) {
651
- if (channel.full) throw new DisTubeError("VOICE_FULL");
652
- else throw new DisTubeError("VOICE_MISSING_PERMS");
653
- }
654
- this.connection = this.#join(channel);
655
- this.#channel = channel;
823
+ #stop() {
824
+ this._manualUpdate = true;
825
+ this.voice.stop();
656
826
  }
657
- #join(channel) {
658
- return (0, import_voice.joinVoiceChannel)({
659
- channelId: channel.id,
660
- guildId: this.id,
661
- adapterCreator: channel.guild.voiceAdapterCreator,
662
- group: channel.client.user?.id
663
- });
827
+ /**
828
+ * The client user as a `GuildMember` of this queue's guild
829
+ */
830
+ get clientMember() {
831
+ return this.voice.channel.guild.members.me ?? void 0;
664
832
  }
665
833
  /**
666
- * Join a voice channel with this connection
667
- * @param channel - A voice channel
834
+ * The filter manager of the queue
668
835
  */
669
- async join(channel) {
670
- const TIMEOUT = 3e4;
671
- if (channel) this.channel = channel;
672
- try {
673
- await (0, import_voice.entersState)(this.connection, import_voice.VoiceConnectionStatus.Ready, TIMEOUT);
674
- } catch {
675
- if (this.connection.state.status === import_voice.VoiceConnectionStatus.Ready) return this;
676
- if (this.connection.state.status !== import_voice.VoiceConnectionStatus.Destroyed) this.connection.destroy();
677
- this.voices.remove(this.id);
678
- throw new DisTubeError("VOICE_CONNECT_FAILED", TIMEOUT / 1e3);
679
- }
680
- return this;
836
+ get filters() {
837
+ return this.#filters;
681
838
  }
682
839
  /**
683
- * Leave the voice channel of this connection
684
- * @param error - Optional, an error to emit with 'error' event.
840
+ * Formatted duration string.
685
841
  */
686
- leave(error) {
687
- this.stop(true);
688
- if (!this.isDisconnected) {
689
- this.emit("disconnect", error);
690
- this.isDisconnected = true;
691
- }
692
- if (this.connection.state.status !== import_voice.VoiceConnectionStatus.Destroyed) this.connection.destroy();
693
- this.voices.remove(this.id);
842
+ get formattedDuration() {
843
+ return formatDuration(this.duration);
694
844
  }
695
845
  /**
696
- * Stop the playing stream
697
- * @param force - If true, will force the {@link DisTubeVoice#audioPlayer} to enter the Idle state even
698
- * if the {@link DisTubeStream#audioResource} has silence padding frames.
846
+ * Queue's duration.
699
847
  */
700
- stop(force = false) {
701
- this.audioPlayer.stop(force);
848
+ get duration() {
849
+ return this.songs.length ? this.songs.reduce((prev, next) => prev + next.duration, 0) : 0;
702
850
  }
703
851
  /**
704
- * Play a {@link DisTubeStream}
705
- * @param dtStream - DisTubeStream
852
+ * What time in the song is playing (in seconds).
706
853
  */
707
- async play(dtStream) {
708
- if (!await checkEncryptionLibraries()) {
709
- dtStream.kill();
710
- throw new DisTubeError("ENCRYPTION_LIBRARIES_MISSING");
711
- }
712
- this.emittedError = false;
713
- dtStream.on("error", (error) => {
714
- if (this.emittedError || error.code === "ERR_STREAM_PREMATURE_CLOSE") return;
715
- this.emittedError = true;
716
- this.emit("error", error);
717
- });
718
- if (this.audioPlayer.state.status !== import_voice.AudioPlayerStatus.Paused) {
719
- this.audioPlayer.play(dtStream.audioResource);
720
- this.stream?.kill();
721
- dtStream.spawn();
722
- } else if (!this.pausingStream) {
723
- this.pausingStream = this.stream;
724
- }
725
- this.stream = dtStream;
726
- this.volume = this.#volume;
854
+ get currentTime() {
855
+ return this.voice.playbackDuration + this._beginTime;
727
856
  }
728
- set volume(volume) {
729
- if (typeof volume !== "number" || isNaN(volume)) {
730
- throw new DisTubeError("INVALID_TYPE", "number", volume, "volume");
731
- }
732
- if (volume < 0) {
733
- throw new DisTubeError("NUMBER_COMPARE", "Volume", "bigger or equal to", 0);
734
- }
735
- this.#volume = volume;
736
- this.stream?.setVolume(Math.pow(this.#volume / 100, 0.5 / Math.log10(2)));
857
+ /**
858
+ * Formatted {@link Queue#currentTime} string.
859
+ */
860
+ get formattedCurrentTime() {
861
+ return formatDuration(this.currentTime);
737
862
  }
738
863
  /**
739
- * Get or set the volume percentage
864
+ * The voice channel playing in.
740
865
  */
741
- get volume() {
742
- return this.#volume;
866
+ get voiceChannel() {
867
+ return this.clientMember?.voice?.channel ?? null;
743
868
  }
744
869
  /**
745
- * Playback duration of the audio resource in seconds
870
+ * Get or set the stream volume. Default value: `50`.
746
871
  */
747
- get playbackDuration() {
748
- return (this.stream?.audioResource?.playbackDuration ?? 0) / 1e3;
872
+ get volume() {
873
+ return this.voice.volume;
749
874
  }
750
- pause() {
751
- this.audioPlayer.pause();
875
+ set volume(value) {
876
+ this.voice.volume = value;
752
877
  }
753
- unpause() {
754
- const state = this.audioPlayer.state;
755
- if (state.status !== import_voice.AudioPlayerStatus.Paused) return;
756
- if (this.stream?.audioResource && state.resource !== this.stream.audioResource) {
757
- this.audioPlayer.play(this.stream.audioResource);
758
- this.stream.spawn();
759
- this.pausingStream?.kill();
760
- delete this.pausingStream;
878
+ /**
879
+ * @throws {DisTubeError}
880
+ * @param song - Song to add
881
+ * @param position - Position to add, \<= 0 to add to the end of the queue
882
+ * @returns The guild queue
883
+ */
884
+ addToQueue(song, position = 0) {
885
+ if (this.stopped) throw new DisTubeError("QUEUE_STOPPED");
886
+ if (!song || Array.isArray(song) && !song.length) {
887
+ throw new DisTubeError("INVALID_TYPE", ["Song", "Array<Song>"], song, "song");
888
+ }
889
+ if (typeof position !== "number" || !Number.isInteger(position)) {
890
+ throw new DisTubeError("INVALID_TYPE", "integer", position, "position");
891
+ }
892
+ if (position <= 0) {
893
+ if (Array.isArray(song)) this.songs.push(...song);
894
+ else this.songs.push(song);
895
+ } else if (Array.isArray(song)) {
896
+ this.songs.splice(position, 0, ...song);
761
897
  } else {
762
- this.audioPlayer.unpause();
898
+ this.songs.splice(position, 0, song);
763
899
  }
900
+ return this;
764
901
  }
765
902
  /**
766
- * Whether the bot is self-deafened
903
+ * @returns `true` if the queue is playing
767
904
  */
768
- get selfDeaf() {
769
- return this.connection.joinConfig.selfDeaf;
905
+ isPlaying() {
906
+ return this.playing;
770
907
  }
771
908
  /**
772
- * Whether the bot is self-muted
909
+ * @returns `true` if the queue is paused
773
910
  */
774
- get selfMute() {
775
- return this.connection.joinConfig.selfMute;
911
+ isPaused() {
912
+ return this.paused;
776
913
  }
777
914
  /**
778
- * Self-deafens/undeafens the bot.
779
- * @param selfDeaf - Whether or not the bot should be self-deafened
780
- * @returns true if the voice state was successfully updated, otherwise false
915
+ * Pause the guild stream
916
+ * @returns The guild queue
781
917
  */
782
- setSelfDeaf(selfDeaf) {
783
- if (typeof selfDeaf !== "boolean") {
784
- throw new DisTubeError("INVALID_TYPE", "boolean", selfDeaf, "selfDeaf");
918
+ async pause() {
919
+ await this._taskQueue.queuing();
920
+ try {
921
+ if (this.paused) throw new DisTubeError("PAUSED");
922
+ this.paused = true;
923
+ this.voice.pause();
924
+ return this;
925
+ } finally {
926
+ this._taskQueue.resolve();
785
927
  }
786
- return this.connection.rejoin({
787
- ...this.connection.joinConfig,
788
- selfDeaf
789
- });
790
928
  }
791
929
  /**
792
- * Self-mutes/unmutes the bot.
793
- * @param selfMute - Whether or not the bot should be self-muted
794
- * @returns true if the voice state was successfully updated, otherwise false
930
+ * Resume the guild stream
931
+ * @returns The guild queue
795
932
  */
796
- setSelfMute(selfMute) {
797
- if (typeof selfMute !== "boolean") {
798
- throw new DisTubeError("INVALID_TYPE", "boolean", selfMute, "selfMute");
933
+ async resume() {
934
+ await this._taskQueue.queuing();
935
+ try {
936
+ if (!this.paused) throw new DisTubeError("RESUMED");
937
+ this.paused = false;
938
+ this.voice.unpause();
939
+ return this;
940
+ } finally {
941
+ this._taskQueue.resolve();
799
942
  }
800
- return this.connection.rejoin({
801
- ...this.connection.joinConfig,
802
- selfMute
803
- });
804
943
  }
805
944
  /**
806
- * The voice state of this connection
945
+ * Set the guild stream's volume
946
+ * @param percent - The percentage of volume you want to set
947
+ * @returns The guild queue
807
948
  */
808
- get voiceState() {
809
- return this.channel?.guild?.members?.me?.voice;
810
- }
811
- };
812
-
813
- // src/core/DisTubeStream.ts
814
- var import_stream = require("stream");
815
- var import_child_process = require("child_process");
816
- var import_tiny_typed_emitter2 = require("tiny-typed-emitter");
817
- var import_voice2 = require("@discordjs/voice");
818
- var checked = process.env.NODE_ENV === "test";
819
- var checkFFmpeg = /* @__PURE__ */ __name((distube) => {
820
- if (checked) return;
821
- const path = distube.options.ffmpeg.path;
822
- const debug = /* @__PURE__ */ __name((str) => distube.emit("ffmpegDebug" /* FFMPEG_DEBUG */, str), "debug");
823
- try {
824
- debug(`[test] spawn ffmpeg at '${path}' path`);
825
- const process2 = (0, import_child_process.spawnSync)(path, ["-h"], { windowsHide: true, shell: true, encoding: "utf-8" });
826
- if (process2.error) throw process2.error;
827
- if (process2.stderr && !process2.stdout) throw new Error(process2.stderr);
828
- const result = process2.output.join("\n");
829
- const version2 = /ffmpeg version (\S+)/iu.exec(result)?.[1];
830
- if (!version2) throw new Error("Invalid FFmpeg version");
831
- debug(`[test] ffmpeg version: ${version2}`);
832
- } catch (e) {
833
- debug(`[test] failed to spawn ffmpeg at '${path}': ${e?.stack ?? e}`);
834
- throw new DisTubeError("FFMPEG_NOT_INSTALLED", path);
835
- }
836
- checked = true;
837
- }, "checkFFmpeg");
838
- var DisTubeStream = class extends import_tiny_typed_emitter2.TypedEmitter {
839
- static {
840
- __name(this, "DisTubeStream");
949
+ setVolume(percent) {
950
+ this.volume = percent;
951
+ return this;
841
952
  }
842
- #ffmpegPath;
843
- #opts;
844
- process;
845
- stream;
846
- audioResource;
847
953
  /**
848
- * Create a DisTubeStream to play with {@link DisTubeVoice}
849
- * @param url - Stream URL
850
- * @param options - Stream options
954
+ * Skip the playing song if there is a next song in the queue. <info>If {@link
955
+ * Queue#autoplay} is `true` and there is no up next song, DisTube will add and
956
+ * play a related song.</info>
957
+ * @param options - Skip options
958
+ * @returns The song will skip to
851
959
  */
852
- constructor(url, options) {
853
- super();
854
- const { ffmpeg, seek } = options;
855
- const opts = {
856
- reconnect: 1,
857
- reconnect_streamed: 1,
858
- reconnect_delay_max: 5,
859
- analyzeduration: 0,
860
- hide_banner: true,
861
- ...ffmpeg.args.global,
862
- ...ffmpeg.args.input,
863
- i: url,
864
- ar: 48e3,
865
- ac: 2,
866
- ...ffmpeg.args.output,
867
- f: "s16le"
868
- };
869
- if (typeof seek === "number" && seek > 0) opts.ss = seek.toString();
870
- const fileUrl = new URL(url);
871
- if (fileUrl.protocol === "file:") {
872
- opts.reconnect = null;
873
- opts.reconnect_streamed = null;
874
- opts.reconnect_delay_max = null;
875
- opts.i = fileUrl.hostname + fileUrl.pathname;
876
- }
877
- this.#ffmpegPath = ffmpeg.path;
878
- this.#opts = [
879
- ...Object.entries(opts).flatMap(
880
- ([key, value]) => Array.isArray(value) ? value.filter(Boolean).map((v) => [`-${key}`, String(v)]) : value == null || value === false ? [] : [value === true ? `-${key}` : [`-${key}`, String(value)]]
881
- ).flat(),
882
- "pipe:1"
883
- ];
884
- this.stream = new VolumeTransformer();
885
- this.stream.on("close", () => this.kill()).on("error", (err) => {
886
- this.debug(`[stream] error: ${err.message}`);
887
- this.emit("error", err);
888
- }).on("finish", () => this.debug("[stream] log: stream finished"));
889
- this.audioResource = (0, import_voice2.createAudioResource)(this.stream, { inputType: import_voice2.StreamType.Raw, inlineVolume: false });
960
+ async skip(options) {
961
+ return this.jump(1, options);
890
962
  }
891
- spawn() {
892
- this.debug(`[process] spawn: ${this.#ffmpegPath} ${this.#opts.join(" ")}`);
893
- this.process = (0, import_child_process.spawn)(this.#ffmpegPath, this.#opts, {
894
- stdio: ["ignore", "pipe", "pipe"],
895
- shell: false,
896
- windowsHide: true
897
- }).on("error", (err) => {
898
- this.debug(`[process] error: ${err.message}`);
899
- this.emit("error", err);
900
- }).on("exit", (code, signal) => {
901
- this.debug(`[process] exit: code=${code ?? "unknown"} signal=${signal ?? "unknown"}`);
902
- if (!code || [0, 255].includes(code)) return;
903
- this.debug(`[process] error: ffmpeg exited with code ${code}`);
904
- this.emit("error", new DisTubeError("FFMPEG_EXITED", code));
905
- });
906
- if (!this.process.stdout || !this.process.stderr) {
907
- this.kill();
908
- throw new Error("Failed to create ffmpeg process");
909
- }
910
- this.process.stdout.pipe(this.stream);
911
- this.process.stderr.setEncoding("utf8")?.on("data", (data) => {
912
- const lines = data.split(/\r\n|\r|\n/u);
913
- for (const line of lines) {
914
- if (/^\s*$/.test(line)) continue;
915
- this.debug(`[ffmpeg] log: ${line}`);
963
+ /**
964
+ * Play the previous song if exists
965
+ * @returns The guild queue
966
+ */
967
+ async previous() {
968
+ await this._taskQueue.queuing();
969
+ try {
970
+ if (!this.options.savePreviousSongs) throw new DisTubeError("DISABLED_OPTION", "savePreviousSongs");
971
+ if (this.previousSongs.length === 0 && this.repeatMode !== 2 /* QUEUE */) {
972
+ throw new DisTubeError("NO_PREVIOUS");
916
973
  }
917
- });
918
- }
919
- debug(debug) {
920
- this.emit("debug", debug);
921
- }
922
- setVolume(volume) {
923
- this.stream.vol = volume;
924
- }
925
- kill() {
926
- if (!this.stream.destroyed) this.stream.destroy();
927
- if (this.process && !this.process.killed) this.process.kill("SIGKILL");
928
- }
929
- };
930
- var VolumeTransformer = class extends import_stream.Transform {
931
- static {
932
- __name(this, "VolumeTransformer");
933
- }
934
- buffer = Buffer.allocUnsafe(0);
935
- extrema = [-Math.pow(2, 16 - 1), Math.pow(2, 16 - 1) - 1];
936
- vol = 1;
937
- _transform(newChunk, _encoding, done) {
938
- const { vol } = this;
939
- if (vol === 1) {
940
- this.push(newChunk);
941
- done();
942
- return;
943
- }
944
- const bytes = 2;
945
- const chunk = Buffer.concat([this.buffer, newChunk]);
946
- const readableLength = Math.floor(chunk.length / bytes) * bytes;
947
- for (let i = 0; i < readableLength; i += bytes) {
948
- const value = chunk.readInt16LE(i);
949
- const clampedValue = Math.min(this.extrema[1], Math.max(this.extrema[0], value * vol));
950
- chunk.writeInt16LE(clampedValue, i);
974
+ const song = this.repeatMode === 2 /* QUEUE */ && this.previousSongs.length === 0 ? this.songs[this.songs.length - 1] : this.previousSongs.pop();
975
+ this.songs.unshift(song);
976
+ this.#stop();
977
+ return song;
978
+ } finally {
979
+ this._taskQueue.resolve();
951
980
  }
952
- this.buffer = chunk.subarray(readableLength);
953
- this.push(chunk.subarray(0, readableLength));
954
- done();
955
- }
956
- };
957
-
958
- // src/core/DisTubeHandler.ts
959
- var import_undici = require("undici");
960
- var REDIRECT_CODES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
961
- var DisTubeHandler = class extends DisTubeBase {
962
- static {
963
- __name(this, "DisTubeHandler");
964
981
  }
965
982
  /**
966
- * Resolve a url or a supported object to a {@link Song} or {@link Playlist}
967
- * @throws {@link DisTubeError}
968
- * @param input - Resolvable input
969
- * @param options - Optional options
970
- * @returns Resolved
983
+ * Shuffle the queue's songs
984
+ * @returns The guild queue
971
985
  */
972
- async resolve(input, options = {}) {
973
- if (input instanceof Song || input instanceof Playlist) {
974
- if ("metadata" in options) input.metadata = options.metadata;
975
- if ("member" in options) input.member = options.member;
976
- return input;
977
- }
978
- if (typeof input === "string") {
979
- if (isURL(input)) {
980
- const plugin = await this._getPluginFromURL(input) || await this._getPluginFromURL(input = await this.followRedirectLink(input));
981
- if (!plugin) throw new DisTubeError("NOT_SUPPORTED_URL");
982
- this.debug(`[${plugin.constructor.name}] Resolving from url: ${input}`);
983
- return plugin.resolve(input, options);
984
- }
985
- try {
986
- const song = await this.#searchSong(input, options);
987
- if (song) return song;
988
- } catch {
989
- throw new DisTubeError("NO_RESULT", input);
986
+ async shuffle() {
987
+ await this._taskQueue.queuing();
988
+ try {
989
+ const playing = this.songs.shift();
990
+ if (playing === void 0) return this;
991
+ for (let i = this.songs.length - 1; i > 0; i--) {
992
+ const j = Math.floor(Math.random() * (i + 1));
993
+ [this.songs[i], this.songs[j]] = [this.songs[j], this.songs[i]];
990
994
  }
995
+ this.songs.unshift(playing);
996
+ return this;
997
+ } finally {
998
+ this._taskQueue.resolve();
991
999
  }
992
- throw new DisTubeError("CANNOT_RESOLVE_SONG", input);
993
1000
  }
994
- async _getPluginFromURL(url) {
995
- for (const plugin of this.plugins) if (await plugin.validate(url)) return plugin;
996
- return null;
997
- }
998
- async _getPluginFromSong(song, types, validate = true) {
999
- if (!types || types.includes(song.plugin?.type)) return song.plugin;
1000
- if (!song.url) return null;
1001
- for (const plugin of this.plugins) {
1002
- if ((!types || types.includes(plugin?.type)) && (!validate || await plugin.validate(song.url))) {
1003
- return plugin;
1001
+ /**
1002
+ * Jump to the song position in the queue. The next one is 1, 2,... The previous
1003
+ * one is -1, -2,...
1004
+ * if `num` is invalid number
1005
+ * @param position - The song position to play
1006
+ * @param options - Skip options
1007
+ * @returns The new Song will be played
1008
+ */
1009
+ async jump(position, options) {
1010
+ await this._taskQueue.queuing();
1011
+ try {
1012
+ if (typeof position !== "number") throw new DisTubeError("INVALID_TYPE", "number", position, "position");
1013
+ if (!position || position > this.songs.length || -position > this.previousSongs.length) {
1014
+ throw new DisTubeError("NO_SONG_POSITION");
1004
1015
  }
1005
- }
1006
- return null;
1007
- }
1008
- async #searchSong(query, options = {}, getStreamURL = false) {
1009
- const plugins = this.plugins.filter((p) => p.type === "extractor" /* EXTRACTOR */);
1010
- if (!plugins.length) throw new DisTubeError("NO_EXTRACTOR_PLUGIN");
1011
- for (const plugin of plugins) {
1012
- this.debug(`[${plugin.constructor.name}] Searching for song: ${query}`);
1013
- const result = await plugin.searchSong(query, options);
1014
- if (result) {
1015
- if (getStreamURL && result.stream.playFromSource) result.stream.url = await plugin.getStreamURL(result);
1016
- return result;
1016
+ if (position > 0) {
1017
+ if (position >= this.songs.length) {
1018
+ if (this.autoplay) {
1019
+ await this.addRelatedSong();
1020
+ } else {
1021
+ throw new DisTubeError("NO_UP_NEXT");
1022
+ }
1023
+ }
1024
+ const skipped = this.songs.splice(0, position);
1025
+ if (options?.requeue) {
1026
+ this.songs.push(...skipped);
1027
+ } else {
1028
+ this.#addToPreviousSongs(skipped);
1029
+ }
1030
+ } else if (!this.options.savePreviousSongs) {
1031
+ throw new DisTubeError("DISABLED_OPTION", "savePreviousSongs");
1032
+ } else {
1033
+ const skipped = this.previousSongs.splice(position);
1034
+ this.songs.unshift(...skipped);
1017
1035
  }
1036
+ this.#stop();
1037
+ return this.songs[0];
1038
+ } finally {
1039
+ this._taskQueue.resolve();
1018
1040
  }
1019
- return null;
1020
1041
  }
1021
1042
  /**
1022
- * Get {@link Song}'s stream info and attach it to the song.
1023
- * @param song - A Song
1043
+ * Set the repeat mode of the guild queue.
1044
+ * Toggle mode `(Disabled -> Song -> Queue -> Disabled ->...)` if `mode` is `undefined`
1045
+ * @param mode - The repeat modes (toggle if `undefined`)
1046
+ * @returns The new repeat mode
1024
1047
  */
1025
- async attachStreamInfo(song) {
1026
- if (song.stream.playFromSource) {
1027
- if (song.stream.url) return;
1028
- this.debug(`[DisTubeHandler] Getting stream info: ${song}`);
1029
- const plugin = await this._getPluginFromSong(song, ["extractor" /* EXTRACTOR */, "playable-extractor" /* PLAYABLE_EXTRACTOR */]);
1030
- if (!plugin) throw new DisTubeError("NOT_SUPPORTED_SONG", song.toString());
1031
- this.debug(`[${plugin.constructor.name}] Getting stream URL: ${song}`);
1032
- song.stream.url = await plugin.getStreamURL(song);
1033
- if (!song.stream.url) throw new DisTubeError("CANNOT_GET_STREAM_URL", song.toString());
1034
- } else {
1035
- if (song.stream.song?.stream?.playFromSource && song.stream.song.stream.url) return;
1036
- this.debug(`[DisTubeHandler] Getting stream info: ${song}`);
1037
- const plugin = await this._getPluginFromSong(song, ["info-extractor" /* INFO_EXTRACTOR */]);
1038
- if (!plugin) throw new DisTubeError("NOT_SUPPORTED_SONG", song.toString());
1039
- this.debug(`[${plugin.constructor.name}] Creating search query for: ${song}`);
1040
- const query = await plugin.createSearchQuery(song);
1041
- if (!query) throw new DisTubeError("CANNOT_GET_SEARCH_QUERY", song.toString());
1042
- const altSong = await this.#searchSong(query, { metadata: song.metadata, member: song.member }, true);
1043
- if (!altSong || !altSong.stream.playFromSource) throw new DisTubeError("NO_RESULT", query || song.toString());
1044
- song.stream.song = altSong;
1045
- }
1046
- }
1047
- async followRedirectLink(url, maxRedirect = 5) {
1048
- if (maxRedirect === 0) return url;
1049
- const res = await (0, import_undici.request)(url, {
1050
- method: "HEAD",
1051
- headers: {
1052
- "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"
1053
- }
1054
- });
1055
- if (REDIRECT_CODES.has(res.statusCode ?? 200)) {
1056
- let location = res.headers.location;
1057
- if (typeof location !== "string") location = location?.[0] ?? url;
1058
- return this.followRedirectLink(location, --maxRedirect);
1048
+ setRepeatMode(mode) {
1049
+ if (mode !== void 0 && !Object.values(RepeatMode).includes(mode)) {
1050
+ throw new DisTubeError("INVALID_TYPE", ["RepeatMode", "undefined"], mode, "mode");
1059
1051
  }
1060
- return url;
1052
+ if (mode === void 0) this.repeatMode = (this.repeatMode + 1) % 3;
1053
+ else if (this.repeatMode === mode) this.repeatMode = 0 /* DISABLED */;
1054
+ else this.repeatMode = mode;
1055
+ return this.repeatMode;
1061
1056
  }
1062
- };
1063
-
1064
- // src/core/DisTubeOptions.ts
1065
- var Options = class {
1066
- static {
1067
- __name(this, "Options");
1057
+ /**
1058
+ * Set the playing time to another position
1059
+ * @param time - Time in seconds
1060
+ * @returns The guild queue
1061
+ */
1062
+ seek(time) {
1063
+ if (typeof time !== "number") throw new DisTubeError("INVALID_TYPE", "number", time, "time");
1064
+ if (Number.isNaN(time) || time < 0) throw new DisTubeError("NUMBER_COMPARE", "time", "bigger or equal to", 0);
1065
+ this._beginTime = time;
1066
+ this.play(false);
1067
+ return this;
1068
1068
  }
1069
- plugins;
1070
- emitNewSongOnly;
1071
- savePreviousSongs;
1072
- customFilters;
1073
- nsfw;
1074
- emitAddSongWhenCreatingQueue;
1075
- emitAddListWhenCreatingQueue;
1076
- joinNewVoiceChannel;
1077
- ffmpeg;
1078
- constructor(options) {
1079
- if (typeof options !== "object" || Array.isArray(options)) {
1080
- throw new DisTubeError("INVALID_TYPE", "object", options, "DisTubeOptions");
1081
- }
1082
- const opts = { ...defaultOptions, ...options };
1083
- this.plugins = opts.plugins;
1084
- this.emitNewSongOnly = opts.emitNewSongOnly;
1085
- this.savePreviousSongs = opts.savePreviousSongs;
1086
- this.customFilters = opts.customFilters;
1087
- this.nsfw = opts.nsfw;
1088
- this.emitAddSongWhenCreatingQueue = opts.emitAddSongWhenCreatingQueue;
1089
- this.emitAddListWhenCreatingQueue = opts.emitAddListWhenCreatingQueue;
1090
- this.joinNewVoiceChannel = opts.joinNewVoiceChannel;
1091
- this.ffmpeg = this.#ffmpegOption(options);
1092
- checkInvalidKey(opts, this, "DisTubeOptions");
1093
- this.#validateOptions();
1069
+ async #getRelatedSong(current) {
1070
+ const plugin = await this.handler._getPluginFromSong(current);
1071
+ if (plugin) return plugin.getRelatedSongs(current);
1072
+ return [];
1094
1073
  }
1095
- #validateOptions(options = this) {
1096
- const booleanOptions = /* @__PURE__ */ new Set([
1097
- "emitNewSongOnly",
1098
- "savePreviousSongs",
1099
- "joinNewVoiceChannel",
1100
- "nsfw",
1101
- "emitAddSongWhenCreatingQueue",
1102
- "emitAddListWhenCreatingQueue"
1103
- ]);
1104
- const numberOptions = /* @__PURE__ */ new Set();
1105
- const stringOptions = /* @__PURE__ */ new Set();
1106
- const objectOptions = /* @__PURE__ */ new Set(["customFilters", "ffmpeg"]);
1107
- const optionalOptions = /* @__PURE__ */ new Set(["customFilters"]);
1108
- for (const [key, value] of Object.entries(options)) {
1109
- if (value === void 0 && optionalOptions.has(key)) continue;
1110
- if (key === "plugins" && !Array.isArray(value)) {
1111
- throw new DisTubeError("INVALID_TYPE", "Array<Plugin>", value, `DisTubeOptions.${key}`);
1112
- } else if (booleanOptions.has(key)) {
1113
- if (typeof value !== "boolean") {
1114
- throw new DisTubeError("INVALID_TYPE", "boolean", value, `DisTubeOptions.${key}`);
1115
- }
1116
- } else if (numberOptions.has(key)) {
1117
- if (typeof value !== "number" || isNaN(value)) {
1118
- throw new DisTubeError("INVALID_TYPE", "number", value, `DisTubeOptions.${key}`);
1119
- }
1120
- } else if (stringOptions.has(key)) {
1121
- if (typeof value !== "string") {
1122
- throw new DisTubeError("INVALID_TYPE", "string", value, `DisTubeOptions.${key}`);
1123
- }
1124
- } else if (objectOptions.has(key)) {
1125
- if (typeof value !== "object" || Array.isArray(value)) {
1126
- throw new DisTubeError("INVALID_TYPE", "object", value, `DisTubeOptions.${key}`);
1127
- }
1128
- }
1074
+ /**
1075
+ * Add a related song of the playing song to the queue
1076
+ * @param song - The song to get related songs from. Defaults to the current playing song.
1077
+ * @returns The added song
1078
+ */
1079
+ async addRelatedSong(song) {
1080
+ const current = song ?? this.songs?.[0];
1081
+ if (!current) throw new DisTubeError("NO_PLAYING_SONG");
1082
+ const prevIds = this.previousSongs.map((p) => p.id);
1083
+ const relatedSongs = (await this.#getRelatedSong(current)).filter((s) => !prevIds.includes(s.id));
1084
+ this.debug(`[${this.id}] Getting related songs from: ${current}`);
1085
+ if (!relatedSongs.length && !current.stream.playFromSource) {
1086
+ const altSong = current.stream.song;
1087
+ if (altSong) relatedSongs.push(...(await this.#getRelatedSong(altSong)).filter((s) => !prevIds.includes(s.id)));
1088
+ this.debug(`[${this.id}] Getting related songs from streamed song: ${altSong}`);
1129
1089
  }
1090
+ const nextSong = relatedSongs[0];
1091
+ if (!nextSong) throw new DisTubeError("NO_RELATED");
1092
+ nextSong.metadata = current.metadata;
1093
+ nextSong.member = this.clientMember;
1094
+ this.addToQueue(nextSong);
1095
+ return nextSong;
1130
1096
  }
1131
- #ffmpegOption(opts) {
1132
- const args = { global: {}, input: {}, output: {} };
1133
- if (opts.ffmpeg?.args) {
1134
- if (opts.ffmpeg.args.global) args.global = opts.ffmpeg.args.global;
1135
- if (opts.ffmpeg.args.input) args.input = opts.ffmpeg.args.input;
1136
- if (opts.ffmpeg.args.output) args.output = opts.ffmpeg.args.output;
1137
- }
1138
- const path = opts.ffmpeg?.path ?? "ffmpeg";
1139
- if (typeof path !== "string") {
1140
- throw new DisTubeError("INVALID_TYPE", "string", path, "DisTubeOptions.ffmpeg.path");
1141
- }
1142
- for (const [key, value] of Object.entries(args)) {
1143
- if (typeof value !== "object" || Array.isArray(value)) {
1144
- throw new DisTubeError("INVALID_TYPE", "object", value, `DisTubeOptions.ffmpeg.${key}`);
1145
- }
1146
- for (const [k, v] of Object.entries(value)) {
1147
- if (typeof v !== "string" && typeof v !== "number" && typeof v !== "boolean" && !Array.isArray(v) && v !== null && v !== void 0) {
1148
- throw new DisTubeError(
1149
- "INVALID_TYPE",
1150
- ["string", "number", "boolean", "Array<string | null | undefined>", "null", "undefined"],
1151
- v,
1152
- `DisTubeOptions.ffmpeg.${key}.${k}`
1153
- );
1154
- }
1155
- }
1097
+ /**
1098
+ * Stop the guild stream and delete the queue
1099
+ */
1100
+ async stop() {
1101
+ await this._taskQueue.queuing();
1102
+ try {
1103
+ this.voice.stop();
1104
+ this.remove();
1105
+ } finally {
1106
+ this._taskQueue.resolve();
1156
1107
  }
1157
- return { path, args };
1158
1108
  }
1159
- };
1160
-
1161
- // src/core/manager/BaseManager.ts
1162
- var import_discord2 = require("discord.js");
1163
- var BaseManager = class extends DisTubeBase {
1164
- static {
1165
- __name(this, "BaseManager");
1109
+ /**
1110
+ * Remove the queue from the manager
1111
+ */
1112
+ remove() {
1113
+ this.playing = false;
1114
+ this.paused = false;
1115
+ this.stopped = true;
1116
+ this.songs = [];
1117
+ this.previousSongs = [];
1118
+ if (this._listeners) for (const event of objectKeys(this._listeners)) this.voice.off(event, this._listeners[event]);
1119
+ this.queues.remove(this.id);
1120
+ this.emit("deleteQueue" /* DELETE_QUEUE */, this);
1166
1121
  }
1167
1122
  /**
1168
- * The collection of items for this manager.
1123
+ * Toggle autoplay mode
1124
+ * @returns Autoplay mode state
1169
1125
  */
1170
- collection = new import_discord2.Collection();
1126
+ toggleAutoplay() {
1127
+ this.autoplay = !this.autoplay;
1128
+ return this.autoplay;
1129
+ }
1171
1130
  /**
1172
- * The size of the collection.
1131
+ * Play the first song in the queue
1132
+ * @param emitPlaySong - Whether or not emit {@link Events.PLAY_SONG} event
1173
1133
  */
1174
- get size() {
1175
- return this.collection.size;
1134
+ play(emitPlaySong = true) {
1135
+ if (this.stopped) throw new DisTubeError("QUEUE_STOPPED");
1136
+ this.playing = true;
1137
+ return this.queues.playSong(this, emitPlaySong);
1176
1138
  }
1177
1139
  };
1178
1140
 
1179
- // src/core/manager/GuildIdManager.ts
1180
- var GuildIdManager = class extends BaseManager {
1181
- static {
1182
- __name(this, "GuildIdManager");
1183
- }
1184
- add(idOrInstance, data) {
1185
- const id = resolveGuildId(idOrInstance);
1186
- const existing = this.get(id);
1187
- if (existing) return this;
1188
- this.collection.set(id, data);
1189
- return this;
1141
+ // src/util.ts
1142
+ var formatInt = /* @__PURE__ */ __name((int) => int < 10 ? `0${int}` : int, "formatInt");
1143
+ function formatDuration(sec) {
1144
+ if (!sec || !Number(sec)) return "00:00";
1145
+ const seconds = Math.floor(sec % 60);
1146
+ const minutes = Math.floor(sec % 3600 / 60);
1147
+ const hours = Math.floor(sec / 3600);
1148
+ if (hours > 0) return `${formatInt(hours)}:${formatInt(minutes)}:${formatInt(seconds)}`;
1149
+ if (minutes > 0) return `${formatInt(minutes)}:${formatInt(seconds)}`;
1150
+ return `00:${formatInt(seconds)}`;
1151
+ }
1152
+ __name(formatDuration, "formatDuration");
1153
+ var SUPPORTED_PROTOCOL = ["https:", "http:", "file:"];
1154
+ function isURL(input) {
1155
+ if (typeof input !== "string" || input.includes(" ")) return false;
1156
+ try {
1157
+ const url = new import_node_url.URL(input);
1158
+ if (!SUPPORTED_PROTOCOL.some((p) => p === url.protocol)) return false;
1159
+ } catch {
1160
+ return false;
1190
1161
  }
1191
- get(idOrInstance) {
1192
- return this.collection.get(resolveGuildId(idOrInstance));
1162
+ return true;
1163
+ }
1164
+ __name(isURL, "isURL");
1165
+ function checkIntents(options) {
1166
+ const intents = options.intents instanceof import_discord3.IntentsBitField ? options.intents : new import_discord3.IntentsBitField(options.intents);
1167
+ if (!intents.has(import_discord3.GatewayIntentBits.GuildVoiceStates)) throw new DisTubeError("MISSING_INTENTS", "GuildVoiceStates");
1168
+ }
1169
+ __name(checkIntents, "checkIntents");
1170
+ function isVoiceChannelEmpty(voiceState) {
1171
+ const guild = voiceState.guild;
1172
+ const clientId = voiceState.client.user?.id;
1173
+ if (!guild || !clientId) return false;
1174
+ const voiceChannel = guild.members.me?.voice?.channel;
1175
+ if (!voiceChannel) return false;
1176
+ const members = voiceChannel.members.filter((m) => !m.user.bot);
1177
+ return !members.size;
1178
+ }
1179
+ __name(isVoiceChannelEmpty, "isVoiceChannelEmpty");
1180
+ function isSnowflake(id) {
1181
+ try {
1182
+ return import_discord3.SnowflakeUtil.deconstruct(id).timestamp > import_discord3.SnowflakeUtil.epoch;
1183
+ } catch {
1184
+ return false;
1193
1185
  }
1194
- remove(idOrInstance) {
1195
- return this.collection.delete(resolveGuildId(idOrInstance));
1186
+ }
1187
+ __name(isSnowflake, "isSnowflake");
1188
+ function isMemberInstance(member) {
1189
+ return Boolean(member) && isSnowflake(member.id) && isSnowflake(member.guild?.id) && isSnowflake(member.user?.id) && member.id === member.user.id;
1190
+ }
1191
+ __name(isMemberInstance, "isMemberInstance");
1192
+ function isTextChannelInstance(channel) {
1193
+ return Boolean(channel) && isSnowflake(channel.id) && isSnowflake(channel.guildId || channel.guild?.id) && import_discord3.Constants.TextBasedChannelTypes.includes(channel.type) && typeof channel.send === "function" && (typeof channel.nsfw === "boolean" || typeof channel.parent?.nsfw === "boolean");
1194
+ }
1195
+ __name(isTextChannelInstance, "isTextChannelInstance");
1196
+ function isMessageInstance(message) {
1197
+ return Boolean(message) && isSnowflake(message.id) && isSnowflake(message.guildId || message.guild?.id) && isMemberInstance(message.member) && isTextChannelInstance(message.channel) && import_discord3.Constants.NonSystemMessageTypes.includes(message.type) && message.member.id === message.author?.id;
1198
+ }
1199
+ __name(isMessageInstance, "isMessageInstance");
1200
+ function isSupportedVoiceChannel(channel) {
1201
+ return Boolean(channel) && isSnowflake(channel.id) && isSnowflake(channel.guildId || channel.guild?.id) && import_discord3.Constants.VoiceBasedChannelTypes.includes(channel.type);
1202
+ }
1203
+ __name(isSupportedVoiceChannel, "isSupportedVoiceChannel");
1204
+ function isGuildInstance(guild) {
1205
+ return Boolean(guild) && isSnowflake(guild.id) && isSnowflake(guild.ownerId) && typeof guild.name === "string";
1206
+ }
1207
+ __name(isGuildInstance, "isGuildInstance");
1208
+ function resolveGuildId(resolvable) {
1209
+ let guildId;
1210
+ if (typeof resolvable === "string") {
1211
+ guildId = resolvable;
1212
+ } else if (isObject(resolvable)) {
1213
+ if ("guildId" in resolvable && resolvable.guildId) {
1214
+ guildId = resolvable.guildId;
1215
+ } else if (resolvable instanceof Queue || resolvable instanceof DisTubeVoice || isGuildInstance(resolvable)) {
1216
+ guildId = resolvable.id;
1217
+ } else if ("guild" in resolvable && isGuildInstance(resolvable.guild)) {
1218
+ guildId = resolvable.guild.id;
1219
+ }
1196
1220
  }
1197
- has(idOrInstance) {
1198
- return this.collection.has(resolveGuildId(idOrInstance));
1221
+ if (!isSnowflake(guildId)) throw new DisTubeError("INVALID_TYPE", "GuildIdResolvable", resolvable);
1222
+ return guildId;
1223
+ }
1224
+ __name(resolveGuildId, "resolveGuildId");
1225
+ function isClientInstance(client) {
1226
+ return Boolean(client) && typeof client.login === "function";
1227
+ }
1228
+ __name(isClientInstance, "isClientInstance");
1229
+ function checkInvalidKey(target, source, sourceName) {
1230
+ if (!isObject(target)) throw new DisTubeError("INVALID_TYPE", "object", target, sourceName);
1231
+ const sourceKeys = Array.isArray(source) ? source : objectKeys(source);
1232
+ const invalidKey = objectKeys(target).find((key) => !sourceKeys.includes(key));
1233
+ if (invalidKey) throw new DisTubeError("INVALID_KEY", sourceName, invalidKey);
1234
+ }
1235
+ __name(checkInvalidKey, "checkInvalidKey");
1236
+ function isObject(obj) {
1237
+ return typeof obj === "object" && obj !== null && !Array.isArray(obj);
1238
+ }
1239
+ __name(isObject, "isObject");
1240
+ function objectKeys(obj) {
1241
+ if (!isObject(obj)) return [];
1242
+ return Object.keys(obj);
1243
+ }
1244
+ __name(objectKeys, "objectKeys");
1245
+ function isNsfwChannel(channel) {
1246
+ if (!isTextChannelInstance(channel)) return false;
1247
+ if (channel.isThread()) return channel.parent?.nsfw ?? false;
1248
+ return channel.nsfw;
1249
+ }
1250
+ __name(isNsfwChannel, "isNsfwChannel");
1251
+ var isTruthy = /* @__PURE__ */ __name((x) => Boolean(x), "isTruthy");
1252
+ var checkEncryptionLibraries = /* @__PURE__ */ __name(async () => {
1253
+ if (await import("crypto").then((m) => m.getCiphers().includes("aes-256-gcm"))) return true;
1254
+ for (const lib of [
1255
+ "@noble/ciphers",
1256
+ "@stablelib/xchacha20poly1305",
1257
+ "sodium-native",
1258
+ "sodium",
1259
+ "libsodium-wrappers",
1260
+ "tweetnacl"
1261
+ ]) {
1262
+ try {
1263
+ await import(lib);
1264
+ return true;
1265
+ } catch {
1266
+ }
1199
1267
  }
1200
- };
1268
+ return false;
1269
+ }, "checkEncryptionLibraries");
1201
1270
 
1202
- // src/core/manager/DisTubeVoiceManager.ts
1203
- var import_voice3 = require("@discordjs/voice");
1204
- var DisTubeVoiceManager = class extends GuildIdManager {
1271
+ // src/struct/Playlist.ts
1272
+ var Playlist = class {
1205
1273
  static {
1206
- __name(this, "DisTubeVoiceManager");
1274
+ __name(this, "Playlist");
1207
1275
  }
1208
1276
  /**
1209
- * Create a {@link DisTubeVoice} instance
1210
- * @param channel - A voice channel to join
1277
+ * Playlist source.
1211
1278
  */
1212
- create(channel) {
1213
- const existing = this.get(channel.guildId);
1214
- if (existing) {
1215
- existing.channel = channel;
1216
- return existing;
1217
- }
1218
- if ((0, import_voice3.getVoiceConnection)(resolveGuildId(channel), this.client.user?.id) || (0, import_voice3.getVoiceConnection)(resolveGuildId(channel))) {
1219
- throw new DisTubeError("VOICE_ALREADY_CREATED");
1220
- }
1221
- return new DisTubeVoice(this, channel);
1222
- }
1279
+ source;
1223
1280
  /**
1224
- * Join a voice channel and wait until the connection is ready
1225
- * @param channel - A voice channel to join
1281
+ * Songs in the playlist.
1226
1282
  */
1227
- join(channel) {
1228
- const existing = this.get(channel.guildId);
1229
- if (existing) return existing.join(channel);
1230
- return this.create(channel).join();
1231
- }
1283
+ songs;
1232
1284
  /**
1233
- * Leave the connected voice channel in a guild
1234
- * @param guild - Queue Resolvable
1285
+ * Playlist ID.
1235
1286
  */
1236
- leave(guild) {
1237
- const voice = this.get(guild);
1238
- if (voice) {
1239
- voice.leave();
1240
- } else {
1241
- const connection = (0, import_voice3.getVoiceConnection)(resolveGuildId(guild), this.client.user?.id) ?? (0, import_voice3.getVoiceConnection)(resolveGuildId(guild));
1242
- if (connection && connection.state.status !== import_voice3.VoiceConnectionStatus.Destroyed) {
1243
- connection.destroy();
1244
- }
1245
- }
1246
- }
1247
- };
1248
-
1249
- // src/core/manager/FilterManager.ts
1250
- var FilterManager = class extends BaseManager {
1251
- static {
1252
- __name(this, "FilterManager");
1253
- }
1287
+ id;
1254
1288
  /**
1255
- * The queue to manage
1289
+ * Playlist name.
1256
1290
  */
1257
- queue;
1258
- constructor(queue) {
1259
- super(queue.distube);
1260
- this.queue = queue;
1261
- }
1262
- #resolve(filter) {
1263
- if (typeof filter === "object" && typeof filter.name === "string" && typeof filter.value === "string") {
1264
- return filter;
1265
- }
1266
- if (typeof filter === "string" && Object.prototype.hasOwnProperty.call(this.distube.filters, filter)) {
1267
- return {
1268
- name: filter,
1269
- value: this.distube.filters[filter]
1270
- };
1271
- }
1272
- throw new DisTubeError("INVALID_TYPE", "FilterResolvable", filter, "filter");
1273
- }
1274
- #apply() {
1275
- this.queue._beginTime = this.queue.currentTime;
1276
- this.queue.play(false);
1277
- }
1291
+ name;
1278
1292
  /**
1279
- * Enable a filter or multiple filters to the manager
1280
- * @param filterOrFilters - The filter or filters to enable
1281
- * @param override - Wether or not override the applied filter with new filter value
1293
+ * Playlist URL.
1282
1294
  */
1283
- add(filterOrFilters, override = false) {
1284
- if (Array.isArray(filterOrFilters)) {
1285
- for (const filter of filterOrFilters) {
1286
- const ft = this.#resolve(filter);
1287
- if (override || !this.has(ft)) this.collection.set(ft.name, ft);
1288
- }
1289
- } else {
1290
- const ft = this.#resolve(filterOrFilters);
1291
- if (override || !this.has(ft)) this.collection.set(ft.name, ft);
1292
- }
1293
- this.#apply();
1294
- return this;
1295
- }
1295
+ url;
1296
1296
  /**
1297
- * Clear enabled filters of the manager
1297
+ * Playlist thumbnail.
1298
1298
  */
1299
- clear() {
1300
- return this.set([]);
1299
+ thumbnail;
1300
+ #metadata;
1301
+ #member;
1302
+ /**
1303
+ * Create a Playlist
1304
+ * @param playlist - Raw playlist info
1305
+ * @param options - Optional data
1306
+ */
1307
+ constructor(playlist, { member, metadata } = {}) {
1308
+ if (!Array.isArray(playlist.songs) || !playlist.songs.length) throw new DisTubeError("EMPTY_PLAYLIST");
1309
+ this.source = playlist.source.toLowerCase();
1310
+ this.songs = playlist.songs;
1311
+ this.name = playlist.name;
1312
+ this.id = playlist.id;
1313
+ this.url = playlist.url;
1314
+ this.thumbnail = playlist.thumbnail;
1315
+ this.member = member;
1316
+ this.songs.forEach((s) => s.playlist = this);
1317
+ this.metadata = metadata;
1301
1318
  }
1302
1319
  /**
1303
- * Set the filters applied to the manager
1304
- * @param filters - The filters to apply
1320
+ * Playlist duration in second.
1305
1321
  */
1306
- set(filters) {
1307
- if (!Array.isArray(filters)) throw new DisTubeError("INVALID_TYPE", "Array<FilterResolvable>", filters, "filters");
1308
- this.collection.clear();
1309
- for (const f of filters) {
1310
- const filter = this.#resolve(f);
1311
- this.collection.set(filter.name, filter);
1312
- }
1313
- this.#apply();
1314
- return this;
1315
- }
1316
- #removeFn(f) {
1317
- return this.collection.delete(this.#resolve(f).name);
1322
+ get duration() {
1323
+ return this.songs.reduce((prev, next) => prev + next.duration, 0);
1318
1324
  }
1319
1325
  /**
1320
- * Disable a filter or multiple filters
1321
- * @param filterOrFilters - The filter or filters to disable
1326
+ * Formatted duration string `hh:mm:ss`.
1322
1327
  */
1323
- remove(filterOrFilters) {
1324
- if (Array.isArray(filterOrFilters)) filterOrFilters.forEach((f) => this.#removeFn(f));
1325
- else this.#removeFn(filterOrFilters);
1326
- this.#apply();
1327
- return this;
1328
+ get formattedDuration() {
1329
+ return formatDuration(this.duration);
1328
1330
  }
1329
1331
  /**
1330
- * Check whether a filter enabled or not
1331
- * @param filter - The filter to check
1332
+ * User requested.
1332
1333
  */
1333
- has(filter) {
1334
- return this.collection.has(typeof filter === "string" ? filter : this.#resolve(filter).name);
1334
+ get member() {
1335
+ return this.#member;
1336
+ }
1337
+ set member(member) {
1338
+ if (!isMemberInstance(member)) return;
1339
+ this.#member = member;
1340
+ this.songs.forEach((s) => s.member = this.member);
1335
1341
  }
1336
1342
  /**
1337
- * Array of enabled filter names
1343
+ * User requested.
1338
1344
  */
1339
- get names() {
1340
- return [...this.collection.keys()];
1345
+ get user() {
1346
+ return this.member?.user;
1341
1347
  }
1342
1348
  /**
1343
- * Array of enabled filters
1349
+ * Optional metadata that can be used to identify the playlist.
1344
1350
  */
1345
- get values() {
1346
- return [...this.collection.values()];
1351
+ get metadata() {
1352
+ return this.#metadata;
1347
1353
  }
1348
- get ffmpegArgs() {
1349
- return this.size ? { af: this.values.map((f) => f.value).join(",") } : {};
1354
+ set metadata(metadata) {
1355
+ this.#metadata = metadata;
1356
+ this.songs.forEach((s) => s.metadata = metadata);
1350
1357
  }
1351
1358
  toString() {
1352
- return this.names.toString();
1359
+ return `${this.name} (${this.songs.length} songs)`;
1353
1360
  }
1354
1361
  };
1355
1362
 
1356
- // src/core/manager/QueueManager.ts
1357
- var QueueManager = class extends GuildIdManager {
1363
+ // src/struct/Song.ts
1364
+ var Song = class {
1358
1365
  static {
1359
- __name(this, "QueueManager");
1360
- }
1361
- /**
1362
- * Create a {@link Queue}
1363
- * @param channel - A voice channel
1364
- * @param textChannel - Default text channel
1365
- * @returns Returns `true` if encounter an error
1366
- */
1367
- async create(channel, textChannel) {
1368
- if (this.has(channel.guildId)) throw new DisTubeError("QUEUE_EXIST");
1369
- this.debug(`[QueueManager] Creating queue for guild: ${channel.guildId}`);
1370
- const voice = this.voices.create(channel);
1371
- const queue = new Queue(this.distube, voice, textChannel);
1372
- await queue._taskQueue.queuing();
1373
- try {
1374
- checkFFmpeg(this.distube);
1375
- this.debug(`[QueueManager] Joining voice channel: ${channel.id}`);
1376
- await voice.join();
1377
- this.#voiceEventHandler(queue);
1378
- this.add(queue.id, queue);
1379
- this.emit("initQueue" /* INIT_QUEUE */, queue);
1380
- return queue;
1381
- } finally {
1382
- queue._taskQueue.resolve();
1383
- }
1384
- }
1385
- /**
1386
- * Listen to DisTubeVoice events and handle the Queue
1387
- * @param queue - Queue
1388
- */
1389
- #voiceEventHandler(queue) {
1390
- queue._listeners = {
1391
- disconnect: /* @__PURE__ */ __name((error) => {
1392
- queue.remove();
1393
- this.emit("disconnect" /* DISCONNECT */, queue);
1394
- if (error) this.emitError(error, queue, queue.songs?.[0]);
1395
- }, "disconnect"),
1396
- error: /* @__PURE__ */ __name((error) => this.#handlePlayingError(queue, error), "error"),
1397
- finish: /* @__PURE__ */ __name(() => this.#handleSongFinish(queue), "finish")
1398
- };
1399
- for (const event of objectKeys(queue._listeners)) {
1400
- queue.voice.on(event, queue._listeners[event]);
1401
- }
1402
- }
1403
- /**
1404
- * Whether or not emit playSong event
1405
- * @param queue - Queue
1406
- */
1407
- #emitPlaySong(queue) {
1408
- if (!this.options.emitNewSongOnly) return true;
1409
- if (queue.repeatMode === 1 /* SONG */) return queue._next || queue._prev;
1410
- return queue.songs[0].id !== queue.songs[1].id;
1411
- }
1412
- /**
1413
- * Handle the queue when a Song finish
1414
- * @param queue - queue
1415
- */
1416
- async #handleSongFinish(queue) {
1417
- this.debug(`[QueueManager] Handling song finish: ${queue.id}`);
1418
- const song = queue.songs[0];
1419
- this.emit("finishSong" /* FINISH_SONG */, queue, queue.songs[0]);
1420
- await queue._taskQueue.queuing();
1421
- try {
1422
- if (queue.stopped) return;
1423
- if (queue.repeatMode === 2 /* QUEUE */ && !queue._prev) queue.songs.push(song);
1424
- if (queue._prev) {
1425
- if (queue.repeatMode === 2 /* QUEUE */) queue.songs.unshift(queue.songs.pop());
1426
- else queue.songs.unshift(queue.previousSongs.pop());
1427
- }
1428
- if (queue.songs.length <= 1 && (queue._next || queue.repeatMode === 0 /* DISABLED */)) {
1429
- if (queue.autoplay) {
1430
- try {
1431
- this.debug(`[QueueManager] Adding related song: ${queue.id}`);
1432
- await queue.addRelatedSong();
1433
- } catch (e) {
1434
- this.debug(`[${queue.id}] Add related song error: ${e.message}`);
1435
- this.emit("noRelated" /* NO_RELATED */, queue, e);
1436
- }
1437
- }
1438
- if (queue.songs.length <= 1) {
1439
- this.debug(`[${queue.id}] Queue is empty, stopping...`);
1440
- if (!queue.autoplay) this.emit("finish" /* FINISH */, queue);
1441
- queue.remove();
1442
- return;
1443
- }
1444
- }
1445
- const emitPlaySong = this.#emitPlaySong(queue);
1446
- if (!queue._prev && (queue.repeatMode !== 1 /* SONG */ || queue._next)) {
1447
- const prev = queue.songs.shift();
1448
- if (this.options.savePreviousSongs) queue.previousSongs.push(prev);
1449
- else queue.previousSongs.push({ id: prev.id });
1450
- }
1451
- queue._next = queue._prev = false;
1452
- queue._beginTime = 0;
1453
- if (song !== queue.songs[0]) {
1454
- const playedSong = song.stream.playFromSource ? song : song.stream.song;
1455
- if (playedSong?.stream.playFromSource) delete playedSong.stream.url;
1456
- }
1457
- await this.playSong(queue, emitPlaySong);
1458
- } finally {
1459
- queue._taskQueue.resolve();
1460
- }
1461
- }
1462
- /**
1463
- * Handle error while playing
1464
- * @param queue - queue
1465
- * @param error - error
1466
- */
1467
- #handlePlayingError(queue, error) {
1468
- const song = queue.songs.shift();
1469
- try {
1470
- error.name = "PlayingError";
1471
- } catch {
1472
- }
1473
- this.debug(`[${queue.id}] Error while playing: ${error.stack || error.message}`);
1474
- this.emitError(error, queue, song);
1475
- if (queue.songs.length > 0) {
1476
- this.debug(`[${queue.id}] Playing next song: ${queue.songs[0]}`);
1477
- queue._next = queue._prev = false;
1478
- queue._beginTime = 0;
1479
- this.playSong(queue);
1480
- } else {
1481
- this.debug(`[${queue.id}] Queue is empty, stopping...`);
1482
- queue.stop();
1483
- }
1366
+ __name(this, "Song");
1484
1367
  }
1485
1368
  /**
1486
- * Play a song on voice connection with queue properties
1487
- * @param queue - The guild queue to play
1488
- * @param emitPlaySong - Whether or not emit {@link Events.PLAY_SONG} event
1369
+ * The source of this song info
1489
1370
  */
1490
- async playSong(queue, emitPlaySong = true) {
1491
- if (!queue) return;
1492
- if (queue.stopped || !queue.songs.length) {
1493
- queue.stop();
1494
- return;
1495
- }
1496
- try {
1497
- const song = queue.songs[0];
1498
- this.debug(`[${queue.id}] Getting stream from: ${song}`);
1499
- await this.handler.attachStreamInfo(song);
1500
- const willPlaySong = song.stream.playFromSource ? song : song.stream.song;
1501
- const stream = willPlaySong?.stream;
1502
- if (!willPlaySong || !stream?.playFromSource || !stream.url) throw new DisTubeError("NO_STREAM_URL", `${song}`);
1503
- this.debug(`[${queue.id}] Creating DisTubeStream for: ${willPlaySong}`);
1504
- const streamOptions = {
1505
- ffmpeg: {
1506
- path: this.options.ffmpeg.path,
1507
- args: {
1508
- global: { ...queue.ffmpegArgs.global },
1509
- input: { ...queue.ffmpegArgs.input },
1510
- output: { ...queue.ffmpegArgs.output, ...queue.filters.ffmpegArgs }
1511
- }
1512
- },
1513
- seek: willPlaySong.duration ? queue._beginTime : void 0
1514
- };
1515
- const dtStream = new DisTubeStream(stream.url, streamOptions);
1516
- dtStream.on("debug", (data) => this.emit("ffmpegDebug" /* FFMPEG_DEBUG */, `[${queue.id}] ${data}`));
1517
- this.debug(`[${queue.id}] Started playing: ${willPlaySong}`);
1518
- await queue.voice.play(dtStream);
1519
- if (emitPlaySong) this.emit("playSong" /* PLAY_SONG */, queue, song);
1520
- } catch (e) {
1521
- this.#handlePlayingError(queue, e);
1522
- }
1523
- }
1524
- };
1525
-
1526
- // src/struct/Queue.ts
1527
- var Queue = class extends DisTubeBase {
1528
- static {
1529
- __name(this, "Queue");
1530
- }
1371
+ source;
1531
1372
  /**
1532
- * Queue id (Guild id)
1373
+ * Song ID.
1533
1374
  */
1534
1375
  id;
1535
1376
  /**
1536
- * Voice connection of this queue.
1377
+ * Song name.
1537
1378
  */
1538
- voice;
1379
+ name;
1539
1380
  /**
1540
- * List of songs in the queue (The first one is the playing song)
1381
+ * Indicates if the song is an active live.
1541
1382
  */
1542
- songs;
1383
+ isLive;
1543
1384
  /**
1544
- * List of the previous songs.
1385
+ * Song duration.
1545
1386
  */
1546
- previousSongs;
1387
+ duration;
1547
1388
  /**
1548
- * Whether stream is currently stopped.
1389
+ * Formatted duration string (`hh:mm:ss`, `mm:ss` or `Live`).
1549
1390
  */
1550
- stopped;
1391
+ formattedDuration;
1551
1392
  /**
1552
- * Whether or not the stream is currently playing.
1393
+ * Song URL.
1553
1394
  */
1554
- playing;
1395
+ url;
1555
1396
  /**
1556
- * Whether or not the stream is currently paused.
1397
+ * Song thumbnail.
1557
1398
  */
1558
- paused;
1399
+ thumbnail;
1559
1400
  /**
1560
- * Type of repeat mode (`0` is disabled, `1` is repeating a song, `2` is repeating
1561
- * all the queue). Default value: `0` (disabled)
1401
+ * Song view count
1562
1402
  */
1563
- repeatMode;
1403
+ views;
1564
1404
  /**
1565
- * Whether or not the autoplay mode is enabled. Default value: `false`
1405
+ * Song like count
1566
1406
  */
1567
- autoplay;
1407
+ likes;
1568
1408
  /**
1569
- * FFmpeg arguments for the current queue. Default value is defined with {@link DisTubeOptions}.ffmpeg.args.
1570
- * `af` output argument will be replaced with {@link Queue#filters} manager
1409
+ * Song dislike count
1571
1410
  */
1572
- ffmpegArgs;
1411
+ dislikes;
1573
1412
  /**
1574
- * The text channel of the Queue. (Default: where the first command is called).
1413
+ * Song repost (share) count
1575
1414
  */
1576
- textChannel;
1577
- #filters;
1415
+ reposts;
1578
1416
  /**
1579
- * What time in the song to begin (in seconds).
1417
+ * Song uploader
1580
1418
  */
1581
- _beginTime;
1419
+ uploader;
1582
1420
  /**
1583
- * Whether or not the last song was skipped to next song.
1421
+ * Whether or not an age-restricted content
1584
1422
  */
1585
- _next;
1423
+ ageRestricted;
1586
1424
  /**
1587
- * Whether or not the last song was skipped to previous song.
1425
+ * Stream info
1588
1426
  */
1589
- _prev;
1427
+ stream;
1590
1428
  /**
1591
- * Task queuing system
1429
+ * The plugin that created this song
1592
1430
  */
1593
- _taskQueue;
1431
+ plugin;
1432
+ #metadata;
1433
+ #member;
1434
+ #playlist;
1594
1435
  /**
1595
- * {@link DisTubeVoice} listener
1596
- */
1597
- _listeners;
1598
- /**
1599
- * Create a queue for the guild
1600
- * @param distube - DisTube
1601
- * @param voice - Voice connection
1602
- * @param textChannel - Default text channel
1436
+ * Create a Song
1437
+ *
1438
+ * @param info - Raw song info
1439
+ * @param options - Optional data
1603
1440
  */
1604
- constructor(distube, voice, textChannel) {
1605
- super(distube);
1606
- this.voice = voice;
1607
- this.id = voice.id;
1608
- this.volume = 50;
1609
- this.songs = [];
1610
- this.previousSongs = [];
1611
- this.stopped = false;
1612
- this._next = false;
1613
- this._prev = false;
1614
- this.playing = false;
1615
- this.paused = false;
1616
- this.repeatMode = 0 /* DISABLED */;
1617
- this.autoplay = false;
1618
- this.#filters = new FilterManager(this);
1619
- this._beginTime = 0;
1620
- this.textChannel = textChannel;
1621
- this._taskQueue = new TaskQueue();
1622
- this._listeners = void 0;
1623
- this.ffmpegArgs = {
1624
- global: { ...this.options.ffmpeg.args.global },
1625
- input: { ...this.options.ffmpeg.args.input },
1626
- output: { ...this.options.ffmpeg.args.output }
1441
+ constructor(info, { member, metadata } = {}) {
1442
+ this.source = info.source.toLowerCase();
1443
+ this.metadata = metadata;
1444
+ this.member = member;
1445
+ this.id = info.id;
1446
+ this.name = info.name;
1447
+ this.isLive = info.isLive;
1448
+ this.duration = this.isLive || !info.duration ? 0 : info.duration;
1449
+ this.formattedDuration = this.isLive ? "Live" : formatDuration(this.duration);
1450
+ this.url = info.url;
1451
+ this.thumbnail = info.thumbnail;
1452
+ this.views = info.views;
1453
+ this.likes = info.likes;
1454
+ this.dislikes = info.dislikes;
1455
+ this.reposts = info.reposts;
1456
+ this.uploader = {
1457
+ name: info.uploader?.name,
1458
+ url: info.uploader?.url
1627
1459
  };
1460
+ this.ageRestricted = info.ageRestricted;
1461
+ this.stream = { playFromSource: info.playFromSource };
1462
+ this.plugin = info.plugin;
1628
1463
  }
1629
1464
  /**
1630
- * The client user as a `GuildMember` of this queue's guild
1465
+ * The playlist this song belongs to
1631
1466
  */
1632
- get clientMember() {
1633
- return this.voice.channel.guild.members.me ?? void 0;
1467
+ get playlist() {
1468
+ return this.#playlist;
1634
1469
  }
1635
- /**
1636
- * The filter manager of the queue
1637
- */
1638
- get filters() {
1639
- return this.#filters;
1470
+ set playlist(playlist) {
1471
+ if (!(playlist instanceof Playlist)) throw new DisTubeError("INVALID_TYPE", "Playlist", playlist, "Song#playlist");
1472
+ this.#playlist = playlist;
1473
+ this.member = playlist.member;
1640
1474
  }
1641
1475
  /**
1642
- * Formatted duration string.
1476
+ * User requested to play this song.
1643
1477
  */
1644
- get formattedDuration() {
1645
- return formatDuration(this.duration);
1478
+ get member() {
1479
+ return this.#member;
1646
1480
  }
1647
- /**
1648
- * Queue's duration.
1649
- */
1650
- get duration() {
1651
- return this.songs.length ? this.songs.reduce((prev, next) => prev + next.duration, 0) : 0;
1481
+ set member(member) {
1482
+ if (isMemberInstance(member)) this.#member = member;
1652
1483
  }
1653
1484
  /**
1654
- * What time in the song is playing (in seconds).
1485
+ * User requested to play this song.
1655
1486
  */
1656
- get currentTime() {
1657
- return this.voice.playbackDuration + this._beginTime;
1487
+ get user() {
1488
+ return this.member?.user;
1658
1489
  }
1659
1490
  /**
1660
- * Formatted {@link Queue#currentTime} string.
1491
+ * Optional metadata that can be used to identify the song. This is attached by the
1492
+ * {@link DisTube#play} method.
1661
1493
  */
1662
- get formattedCurrentTime() {
1663
- return formatDuration(this.currentTime);
1494
+ get metadata() {
1495
+ return this.#metadata;
1664
1496
  }
1665
- /**
1666
- * The voice channel playing in.
1667
- */
1668
- get voiceChannel() {
1669
- return this.clientMember?.voice?.channel ?? null;
1497
+ set metadata(metadata) {
1498
+ this.#metadata = metadata;
1670
1499
  }
1671
- /**
1672
- * Get or set the stream volume. Default value: `50`.
1673
- */
1674
- get volume() {
1675
- return this.voice.volume;
1500
+ toString() {
1501
+ return this.name || this.url || this.id || "Unknown";
1676
1502
  }
1677
- set volume(value) {
1678
- this.voice.volume = value;
1503
+ };
1504
+
1505
+ // src/core/DisTubeHandler.ts
1506
+ var REDIRECT_CODES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
1507
+ var DisTubeHandler = class extends DisTubeBase {
1508
+ static {
1509
+ __name(this, "DisTubeHandler");
1679
1510
  }
1680
1511
  /**
1681
- * @throws {DisTubeError}
1682
- * @param song - Song to add
1683
- * @param position - Position to add, \<= 0 to add to the end of the queue
1684
- * @returns The guild queue
1512
+ * Resolve a url or a supported object to a {@link Song} or {@link Playlist}
1513
+ * @throws {@link DisTubeError}
1514
+ * @param input - Resolvable input
1515
+ * @param options - Optional options
1516
+ * @returns Resolved
1685
1517
  */
1686
- addToQueue(song, position = 0) {
1687
- if (this.stopped) throw new DisTubeError("QUEUE_STOPPED");
1688
- if (!song || Array.isArray(song) && !song.length) {
1689
- throw new DisTubeError("INVALID_TYPE", ["Song", "Array<Song>"], song, "song");
1690
- }
1691
- if (typeof position !== "number" || !Number.isInteger(position)) {
1692
- throw new DisTubeError("INVALID_TYPE", "integer", position, "position");
1518
+ async resolve(input, options = {}) {
1519
+ if (input instanceof Song || input instanceof Playlist) {
1520
+ if ("metadata" in options) input.metadata = options.metadata;
1521
+ if ("member" in options) input.member = options.member;
1522
+ return input;
1693
1523
  }
1694
- if (position <= 0) {
1695
- if (Array.isArray(song)) this.songs.push(...song);
1696
- else this.songs.push(song);
1697
- } else if (Array.isArray(song)) {
1698
- this.songs.splice(position, 0, ...song);
1699
- } else {
1700
- this.songs.splice(position, 0, song);
1524
+ if (typeof input === "string") {
1525
+ if (isURL(input)) {
1526
+ const plugin = await this._getPluginFromURL(input) || await this._getPluginFromURL(input = await this.followRedirectLink(input));
1527
+ if (!plugin) throw new DisTubeError("NOT_SUPPORTED_URL");
1528
+ this.debug(`[${plugin.constructor.name}] Resolving from url: ${input}`);
1529
+ return plugin.resolve(input, options);
1530
+ }
1531
+ try {
1532
+ const song = await this.#searchSong(input, options);
1533
+ if (song) return song;
1534
+ } catch {
1535
+ throw new DisTubeError("NO_RESULT", input);
1536
+ }
1701
1537
  }
1702
- return this;
1703
- }
1704
- /**
1705
- * @returns `true` if the queue is playing
1706
- */
1707
- isPlaying() {
1708
- return this.playing;
1538
+ throw new DisTubeError("CANNOT_RESOLVE_SONG", input);
1709
1539
  }
1710
- /**
1711
- * @returns `true` if the queue is paused
1712
- */
1713
- isPaused() {
1714
- return this.paused;
1540
+ async _getPluginFromURL(url) {
1541
+ for (const plugin of this.plugins) if (await plugin.validate(url)) return plugin;
1542
+ return null;
1715
1543
  }
1716
- /**
1717
- * Pause the guild stream
1718
- * @returns The guild queue
1719
- */
1720
- async pause() {
1721
- await this._taskQueue.queuing();
1722
- try {
1723
- if (this.paused) throw new DisTubeError("PAUSED");
1724
- this.paused = true;
1725
- this.voice.pause();
1726
- return this;
1727
- } finally {
1728
- this._taskQueue.resolve();
1544
+ async _getPluginFromSong(song, types, validate = true) {
1545
+ if (!types || types.includes(song.plugin?.type)) return song.plugin;
1546
+ if (!song.url) return null;
1547
+ for (const plugin of this.plugins) {
1548
+ if ((!types || types.includes(plugin?.type)) && (!validate || await plugin.validate(song.url))) {
1549
+ return plugin;
1550
+ }
1729
1551
  }
1552
+ return null;
1730
1553
  }
1731
- /**
1732
- * Resume the guild stream
1733
- * @returns The guild queue
1734
- */
1735
- async resume() {
1736
- await this._taskQueue.queuing();
1737
- try {
1738
- if (!this.paused) throw new DisTubeError("RESUMED");
1739
- this.paused = false;
1740
- this.voice.unpause();
1741
- return this;
1742
- } finally {
1743
- this._taskQueue.resolve();
1554
+ async #searchSong(query, options = {}, getStreamURL = false) {
1555
+ const plugins = this.plugins.filter((p) => p.type === "extractor" /* EXTRACTOR */);
1556
+ if (!plugins.length) throw new DisTubeError("NO_EXTRACTOR_PLUGIN");
1557
+ for (const plugin of plugins) {
1558
+ this.debug(`[${plugin.constructor.name}] Searching for song: ${query}`);
1559
+ const result = await plugin.searchSong(query, options);
1560
+ if (result) {
1561
+ if (getStreamURL && result.stream.playFromSource) result.stream.url = await plugin.getStreamURL(result);
1562
+ return result;
1563
+ }
1744
1564
  }
1565
+ return null;
1745
1566
  }
1746
1567
  /**
1747
- * Set the guild stream's volume
1748
- * @param percent - The percentage of volume you want to set
1749
- * @returns The guild queue
1568
+ * Get {@link Song}'s stream info and attach it to the song.
1569
+ * @param song - A Song
1750
1570
  */
1751
- setVolume(percent) {
1752
- this.volume = percent;
1753
- return this;
1571
+ async attachStreamInfo(song) {
1572
+ if (song.stream.playFromSource) {
1573
+ if (song.stream.url) return;
1574
+ this.debug(`[DisTubeHandler] Getting stream info: ${song}`);
1575
+ const plugin = await this._getPluginFromSong(song, ["extractor" /* EXTRACTOR */, "playable-extractor" /* PLAYABLE_EXTRACTOR */]);
1576
+ if (!plugin) throw new DisTubeError("NOT_SUPPORTED_SONG", song.toString());
1577
+ this.debug(`[${plugin.constructor.name}] Getting stream URL: ${song}`);
1578
+ song.stream.url = await plugin.getStreamURL(song);
1579
+ if (!song.stream.url) throw new DisTubeError("CANNOT_GET_STREAM_URL", song.toString());
1580
+ } else {
1581
+ if (song.stream.song?.stream?.playFromSource && song.stream.song.stream.url) return;
1582
+ this.debug(`[DisTubeHandler] Getting stream info: ${song}`);
1583
+ const plugin = await this._getPluginFromSong(song, ["info-extractor" /* INFO_EXTRACTOR */]);
1584
+ if (!plugin) throw new DisTubeError("NOT_SUPPORTED_SONG", song.toString());
1585
+ this.debug(`[${plugin.constructor.name}] Creating search query for: ${song}`);
1586
+ const query = await plugin.createSearchQuery(song);
1587
+ if (!query) throw new DisTubeError("CANNOT_GET_SEARCH_QUERY", song.toString());
1588
+ const altSong = await this.#searchSong(query, { metadata: song.metadata, member: song.member }, true);
1589
+ if (!altSong || !altSong.stream.playFromSource) throw new DisTubeError("NO_RESULT", query || song.toString());
1590
+ song.stream.song = altSong;
1591
+ }
1754
1592
  }
1755
- /**
1756
- * Skip the playing song if there is a next song in the queue. <info>If {@link
1757
- * Queue#autoplay} is `true` and there is no up next song, DisTube will add and
1758
- * play a related song.</info>
1759
- * @returns The song will skip to
1760
- */
1761
- async skip() {
1762
- await this._taskQueue.queuing();
1763
- try {
1764
- if (this.songs.length <= 1) {
1765
- if (this.autoplay) await this.addRelatedSong();
1766
- else throw new DisTubeError("NO_UP_NEXT");
1593
+ async followRedirectLink(url, maxRedirect = 5) {
1594
+ if (maxRedirect === 0) return url;
1595
+ const res = await (0, import_undici.request)(url, {
1596
+ method: "HEAD",
1597
+ headers: {
1598
+ "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"
1767
1599
  }
1768
- const song = this.songs[1];
1769
- this._next = true;
1770
- this.voice.stop();
1771
- return song;
1772
- } finally {
1773
- this._taskQueue.resolve();
1600
+ });
1601
+ if (REDIRECT_CODES.has(res.statusCode ?? 200)) {
1602
+ let location = res.headers.location;
1603
+ if (typeof location !== "string") location = location?.[0] ?? url;
1604
+ return this.followRedirectLink(location, --maxRedirect);
1774
1605
  }
1606
+ return url;
1775
1607
  }
1776
- /**
1777
- * Play the previous song if exists
1778
- * @returns The guild queue
1779
- */
1780
- async previous() {
1781
- await this._taskQueue.queuing();
1782
- try {
1783
- if (!this.options.savePreviousSongs) throw new DisTubeError("DISABLED_OPTION", "savePreviousSongs");
1784
- if (this.previousSongs?.length === 0 && this.repeatMode !== 2 /* QUEUE */) {
1785
- throw new DisTubeError("NO_PREVIOUS");
1786
- }
1787
- const song = this.repeatMode === 2 ? this.songs[this.songs.length - 1] : this.previousSongs[this.previousSongs.length - 1];
1788
- this._prev = true;
1789
- this.voice.stop();
1790
- return song;
1791
- } finally {
1792
- this._taskQueue.resolve();
1608
+ };
1609
+
1610
+ // src/core/DisTubeOptions.ts
1611
+ var Options = class {
1612
+ static {
1613
+ __name(this, "Options");
1614
+ }
1615
+ plugins;
1616
+ emitNewSongOnly;
1617
+ savePreviousSongs;
1618
+ customFilters;
1619
+ nsfw;
1620
+ emitAddSongWhenCreatingQueue;
1621
+ emitAddListWhenCreatingQueue;
1622
+ joinNewVoiceChannel;
1623
+ ffmpeg;
1624
+ constructor(options) {
1625
+ if (typeof options !== "object" || Array.isArray(options)) {
1626
+ throw new DisTubeError("INVALID_TYPE", "object", options, "DisTubeOptions");
1793
1627
  }
1628
+ const opts = { ...defaultOptions, ...options };
1629
+ this.plugins = opts.plugins;
1630
+ this.emitNewSongOnly = opts.emitNewSongOnly;
1631
+ this.savePreviousSongs = opts.savePreviousSongs;
1632
+ this.customFilters = opts.customFilters;
1633
+ this.nsfw = opts.nsfw;
1634
+ this.emitAddSongWhenCreatingQueue = opts.emitAddSongWhenCreatingQueue;
1635
+ this.emitAddListWhenCreatingQueue = opts.emitAddListWhenCreatingQueue;
1636
+ this.joinNewVoiceChannel = opts.joinNewVoiceChannel;
1637
+ this.ffmpeg = this.#ffmpegOption(options);
1638
+ checkInvalidKey(opts, this, "DisTubeOptions");
1639
+ this.#validateOptions();
1794
1640
  }
1795
- /**
1796
- * Shuffle the queue's songs
1797
- * @returns The guild queue
1798
- */
1799
- async shuffle() {
1800
- await this._taskQueue.queuing();
1801
- try {
1802
- const playing = this.songs.shift();
1803
- if (playing === void 0) return this;
1804
- for (let i = this.songs.length - 1; i > 0; i--) {
1805
- const j = Math.floor(Math.random() * (i + 1));
1806
- [this.songs[i], this.songs[j]] = [this.songs[j], this.songs[i]];
1641
+ #validateOptions(options = this) {
1642
+ const booleanOptions = /* @__PURE__ */ new Set([
1643
+ "emitNewSongOnly",
1644
+ "savePreviousSongs",
1645
+ "joinNewVoiceChannel",
1646
+ "nsfw",
1647
+ "emitAddSongWhenCreatingQueue",
1648
+ "emitAddListWhenCreatingQueue"
1649
+ ]);
1650
+ const numberOptions = /* @__PURE__ */ new Set();
1651
+ const stringOptions = /* @__PURE__ */ new Set();
1652
+ const objectOptions = /* @__PURE__ */ new Set(["customFilters", "ffmpeg"]);
1653
+ const optionalOptions = /* @__PURE__ */ new Set(["customFilters"]);
1654
+ for (const [key, value] of Object.entries(options)) {
1655
+ if (value === void 0 && optionalOptions.has(key)) continue;
1656
+ if (key === "plugins" && !Array.isArray(value)) {
1657
+ throw new DisTubeError("INVALID_TYPE", "Array<Plugin>", value, `DisTubeOptions.${key}`);
1658
+ } else if (booleanOptions.has(key)) {
1659
+ if (typeof value !== "boolean") {
1660
+ throw new DisTubeError("INVALID_TYPE", "boolean", value, `DisTubeOptions.${key}`);
1661
+ }
1662
+ } else if (numberOptions.has(key)) {
1663
+ if (typeof value !== "number" || Number.isNaN(value)) {
1664
+ throw new DisTubeError("INVALID_TYPE", "number", value, `DisTubeOptions.${key}`);
1665
+ }
1666
+ } else if (stringOptions.has(key)) {
1667
+ if (typeof value !== "string") {
1668
+ throw new DisTubeError("INVALID_TYPE", "string", value, `DisTubeOptions.${key}`);
1669
+ }
1670
+ } else if (objectOptions.has(key)) {
1671
+ if (typeof value !== "object" || Array.isArray(value)) {
1672
+ throw new DisTubeError("INVALID_TYPE", "object", value, `DisTubeOptions.${key}`);
1673
+ }
1807
1674
  }
1808
- this.songs.unshift(playing);
1809
- return this;
1810
- } finally {
1811
- this._taskQueue.resolve();
1812
1675
  }
1813
1676
  }
1814
- /**
1815
- * Jump to the song position in the queue. The next one is 1, 2,... The previous
1816
- * one is -1, -2,...
1817
- * if `num` is invalid number
1818
- * @param position - The song position to play
1819
- * @returns The new Song will be played
1820
- */
1821
- async jump(position) {
1822
- await this._taskQueue.queuing();
1823
- try {
1824
- if (typeof position !== "number") throw new DisTubeError("INVALID_TYPE", "number", position, "position");
1825
- if (!position || position > this.songs.length || -position > this.previousSongs.length) {
1826
- throw new DisTubeError("NO_SONG_POSITION");
1677
+ #ffmpegOption(opts) {
1678
+ const args = { global: {}, input: {}, output: {} };
1679
+ if (opts.ffmpeg?.args) {
1680
+ if (opts.ffmpeg.args.global) args.global = opts.ffmpeg.args.global;
1681
+ if (opts.ffmpeg.args.input) args.input = opts.ffmpeg.args.input;
1682
+ if (opts.ffmpeg.args.output) args.output = opts.ffmpeg.args.output;
1683
+ }
1684
+ const path = opts.ffmpeg?.path ?? "ffmpeg";
1685
+ if (typeof path !== "string") {
1686
+ throw new DisTubeError("INVALID_TYPE", "string", path, "DisTubeOptions.ffmpeg.path");
1687
+ }
1688
+ for (const [key, value] of Object.entries(args)) {
1689
+ if (typeof value !== "object" || Array.isArray(value)) {
1690
+ throw new DisTubeError("INVALID_TYPE", "object", value, `DisTubeOptions.ffmpeg.${key}`);
1827
1691
  }
1828
- let nextSong;
1829
- if (position > 0) {
1830
- const nextSongs = this.songs.splice(position - 1);
1831
- if (this.options.savePreviousSongs) {
1832
- this.previousSongs.push(...this.songs);
1833
- } else {
1834
- this.previousSongs.push(...this.songs.map((s) => ({ id: s.id })));
1692
+ for (const [k, v] of Object.entries(value)) {
1693
+ if (typeof v !== "string" && typeof v !== "number" && typeof v !== "boolean" && !Array.isArray(v) && v !== null && v !== void 0) {
1694
+ throw new DisTubeError(
1695
+ "INVALID_TYPE",
1696
+ ["string", "number", "boolean", "Array<string | null | undefined>", "null", "undefined"],
1697
+ v,
1698
+ `DisTubeOptions.ffmpeg.${key}.${k}`
1699
+ );
1835
1700
  }
1836
- this.songs = nextSongs;
1837
- this._next = true;
1838
- nextSong = nextSongs[1];
1839
- } else if (!this.options.savePreviousSongs) {
1840
- throw new DisTubeError("DISABLED_OPTION", "savePreviousSongs");
1841
- } else {
1842
- this._prev = true;
1843
- if (position !== -1) this.songs.unshift(...this.previousSongs.splice(position + 1));
1844
- nextSong = this.previousSongs[this.previousSongs.length - 1];
1845
1701
  }
1846
- this.voice.stop();
1847
- return nextSong;
1848
- } finally {
1849
- this._taskQueue.resolve();
1850
1702
  }
1703
+ return { path, args };
1851
1704
  }
1852
- /**
1853
- * Set the repeat mode of the guild queue.
1854
- * Toggle mode `(Disabled -> Song -> Queue -> Disabled ->...)` if `mode` is `undefined`
1855
- * @param mode - The repeat modes (toggle if `undefined`)
1856
- * @returns The new repeat mode
1857
- */
1858
- setRepeatMode(mode) {
1859
- if (mode !== void 0 && !Object.values(RepeatMode).includes(mode)) {
1860
- throw new DisTubeError("INVALID_TYPE", ["RepeatMode", "undefined"], mode, "mode");
1861
- }
1862
- if (mode === void 0) this.repeatMode = (this.repeatMode + 1) % 3;
1863
- else if (this.repeatMode === mode) this.repeatMode = 0 /* DISABLED */;
1864
- else this.repeatMode = mode;
1865
- return this.repeatMode;
1866
- }
1867
- /**
1868
- * Set the playing time to another position
1869
- * @param time - Time in seconds
1870
- * @returns The guild queue
1871
- */
1872
- seek(time) {
1873
- if (typeof time !== "number") throw new DisTubeError("INVALID_TYPE", "number", time, "time");
1874
- if (isNaN(time) || time < 0) throw new DisTubeError("NUMBER_COMPARE", "time", "bigger or equal to", 0);
1875
- this._beginTime = time;
1876
- this.play(false);
1877
- return this;
1705
+ };
1706
+
1707
+ // src/core/DisTubeStream.ts
1708
+ var import_node_child_process = require("child_process");
1709
+ var import_node_stream = require("stream");
1710
+ var import_voice2 = require("@discordjs/voice");
1711
+ var import_tiny_typed_emitter2 = require("tiny-typed-emitter");
1712
+ var checked = process.env.NODE_ENV === "test";
1713
+ var checkFFmpeg = /* @__PURE__ */ __name((distube) => {
1714
+ if (checked) return;
1715
+ const path = distube.options.ffmpeg.path;
1716
+ const debug = /* @__PURE__ */ __name((str) => distube.emit("ffmpegDebug" /* FFMPEG_DEBUG */, str), "debug");
1717
+ try {
1718
+ debug(`[test] spawn ffmpeg at '${path}' path`);
1719
+ const process2 = (0, import_node_child_process.spawnSync)(path, ["-h"], {
1720
+ windowsHide: true,
1721
+ encoding: "utf-8"
1722
+ });
1723
+ if (process2.error) throw process2.error;
1724
+ if (process2.stderr && !process2.stdout) throw new Error(process2.stderr);
1725
+ const result = process2.output.join("\n");
1726
+ const version2 = /ffmpeg version (\S+)/iu.exec(result)?.[1];
1727
+ if (!version2) throw new Error("Invalid FFmpeg version");
1728
+ debug(`[test] ffmpeg version: ${version2}`);
1729
+ } catch (e) {
1730
+ debug(`[test] failed to spawn ffmpeg at '${path}': ${e?.stack ?? e}`);
1731
+ throw new DisTubeError("FFMPEG_NOT_INSTALLED", path);
1878
1732
  }
1879
- async #getRelatedSong(current) {
1880
- const plugin = await this.handler._getPluginFromSong(current);
1881
- if (plugin) return plugin.getRelatedSongs(current);
1882
- return [];
1733
+ checked = true;
1734
+ }, "checkFFmpeg");
1735
+ var DisTubeStream = class extends import_tiny_typed_emitter2.TypedEmitter {
1736
+ static {
1737
+ __name(this, "DisTubeStream");
1883
1738
  }
1739
+ #ffmpegPath;
1740
+ #opts;
1741
+ process;
1742
+ stream;
1743
+ audioResource;
1884
1744
  /**
1885
- * Add a related song of the playing song to the queue
1886
- * @returns The added song
1745
+ * Create a DisTubeStream to play with {@link DisTubeVoice}
1746
+ * @param url - Stream URL
1747
+ * @param options - Stream options
1887
1748
  */
1888
- async addRelatedSong() {
1889
- const current = this.songs?.[0];
1890
- if (!current) throw new DisTubeError("NO_PLAYING_SONG");
1891
- const prevIds = this.previousSongs.map((p) => p.id);
1892
- const relatedSongs = (await this.#getRelatedSong(current)).filter((s) => !prevIds.includes(s.id));
1893
- this.debug(`[${this.id}] Getting related songs from: ${current}`);
1894
- if (!relatedSongs.length && !current.stream.playFromSource) {
1895
- const altSong = current.stream.song;
1896
- if (altSong) relatedSongs.push(...(await this.#getRelatedSong(altSong)).filter((s) => !prevIds.includes(s.id)));
1897
- this.debug(`[${this.id}] Getting related songs from streamed song: ${altSong}`);
1749
+ constructor(url, options) {
1750
+ super();
1751
+ const { ffmpeg, seek } = options;
1752
+ const opts = {
1753
+ reconnect: 1,
1754
+ reconnect_streamed: 1,
1755
+ reconnect_delay_max: 5,
1756
+ analyzeduration: 0,
1757
+ hide_banner: true,
1758
+ ...ffmpeg.args.global,
1759
+ ...ffmpeg.args.input,
1760
+ i: url,
1761
+ ar: 48e3,
1762
+ ac: 2,
1763
+ ...ffmpeg.args.output,
1764
+ f: "s16le"
1765
+ };
1766
+ if (typeof seek === "number" && seek > 0) opts.ss = seek.toString();
1767
+ const fileUrl = new URL(url);
1768
+ if (fileUrl.protocol === "file:") {
1769
+ opts.reconnect = null;
1770
+ opts.reconnect_streamed = null;
1771
+ opts.reconnect_delay_max = null;
1772
+ opts.i = fileUrl.hostname + fileUrl.pathname;
1898
1773
  }
1899
- const song = relatedSongs[0];
1900
- if (!song) throw new DisTubeError("NO_RELATED");
1901
- song.metadata = current.metadata;
1902
- song.member = this.clientMember;
1903
- this.addToQueue(song);
1904
- return song;
1774
+ this.#ffmpegPath = ffmpeg.path;
1775
+ this.#opts = [
1776
+ ...Object.entries(opts).flatMap(
1777
+ ([key, value]) => Array.isArray(value) ? value.filter(Boolean).map((v) => [`-${key}`, String(v)]) : value == null || value === false ? [] : [value === true ? `-${key}` : [`-${key}`, String(value)]]
1778
+ ).flat(),
1779
+ "pipe:1"
1780
+ ];
1781
+ this.stream = new VolumeTransformer();
1782
+ this.stream.on("close", () => this.kill()).on("error", (err) => {
1783
+ this.debug(`[stream] error: ${err.message}`);
1784
+ this.emit("error", err);
1785
+ }).on("finish", () => this.debug("[stream] log: stream finished"));
1786
+ this.audioResource = (0, import_voice2.createAudioResource)(this.stream, {
1787
+ inputType: import_voice2.StreamType.Raw,
1788
+ inlineVolume: false
1789
+ });
1905
1790
  }
1906
- /**
1907
- * Stop the guild stream and delete the queue
1908
- */
1909
- async stop() {
1910
- await this._taskQueue.queuing();
1911
- try {
1912
- this.voice.stop();
1913
- this.remove();
1914
- } finally {
1915
- this._taskQueue.resolve();
1791
+ spawn() {
1792
+ this.debug(`[process] spawn: ${this.#ffmpegPath} ${this.#opts.join(" ")}`);
1793
+ this.process = (0, import_node_child_process.spawn)(this.#ffmpegPath, this.#opts, {
1794
+ stdio: ["ignore", "pipe", "pipe"],
1795
+ shell: false,
1796
+ windowsHide: true
1797
+ }).on("error", (err) => {
1798
+ this.debug(`[process] error: ${err.message}`);
1799
+ this.emit("error", err);
1800
+ }).on("exit", (code, signal) => {
1801
+ this.debug(`[process] exit: code=${code ?? "unknown"} signal=${signal ?? "unknown"}`);
1802
+ if (!code || [0, 255].includes(code)) return;
1803
+ this.debug(`[process] error: ffmpeg exited with code ${code}`);
1804
+ this.emit("error", new DisTubeError("FFMPEG_EXITED", code));
1805
+ });
1806
+ if (!this.process.stdout || !this.process.stderr) {
1807
+ this.kill();
1808
+ throw new Error("Failed to create ffmpeg process");
1916
1809
  }
1810
+ this.process.stdout.pipe(this.stream);
1811
+ this.process.stderr.setEncoding("utf8")?.on("data", (data) => {
1812
+ const lines = data.split(/\r\n|\r|\n/u);
1813
+ for (const line of lines) {
1814
+ if (/^\s*$/.test(line)) continue;
1815
+ this.debug(`[ffmpeg] log: ${line}`);
1816
+ }
1817
+ });
1917
1818
  }
1918
- /**
1919
- * Remove the queue from the manager
1920
- */
1921
- remove() {
1922
- this.playing = false;
1923
- this.paused = false;
1924
- this.stopped = true;
1925
- this.songs = [];
1926
- this.previousSongs = [];
1927
- if (this._listeners) for (const event of objectKeys(this._listeners)) this.voice.off(event, this._listeners[event]);
1928
- this.queues.remove(this.id);
1929
- this.emit("deleteQueue" /* DELETE_QUEUE */, this);
1819
+ debug(debug) {
1820
+ this.emit("debug", debug);
1930
1821
  }
1931
- /**
1932
- * Toggle autoplay mode
1933
- * @returns Autoplay mode state
1934
- */
1935
- toggleAutoplay() {
1936
- this.autoplay = !this.autoplay;
1937
- return this.autoplay;
1822
+ setVolume(volume) {
1823
+ this.stream.vol = volume;
1938
1824
  }
1939
- /**
1940
- * Play the first song in the queue
1941
- * @param emitPlaySong - Whether or not emit {@link Events.PLAY_SONG} event
1942
- */
1943
- play(emitPlaySong = true) {
1944
- if (this.stopped) throw new DisTubeError("QUEUE_STOPPED");
1945
- this.playing = true;
1946
- return this.queues.playSong(this, emitPlaySong);
1825
+ kill() {
1826
+ if (!this.stream.destroyed) this.stream.destroy();
1827
+ if (this.process && !this.process.killed) this.process.kill("SIGKILL");
1947
1828
  }
1948
1829
  };
1949
-
1950
- // src/struct/Plugin.ts
1951
- var Plugin = class {
1830
+ var VolumeTransformer = class extends import_node_stream.Transform {
1952
1831
  static {
1953
- __name(this, "Plugin");
1832
+ __name(this, "VolumeTransformer");
1954
1833
  }
1955
- /**
1956
- * DisTube
1957
- */
1958
- distube;
1959
- init(distube) {
1960
- this.distube = distube;
1834
+ buffer = Buffer.allocUnsafe(0);
1835
+ extrema = [-(2 ** (16 - 1)), 2 ** (16 - 1) - 1];
1836
+ vol = 1;
1837
+ _transform(newChunk, _encoding, done) {
1838
+ const { vol } = this;
1839
+ if (vol === 1) {
1840
+ this.push(newChunk);
1841
+ done();
1842
+ return;
1843
+ }
1844
+ const bytes = 2;
1845
+ const chunk = Buffer.concat([this.buffer, newChunk]);
1846
+ const readableLength = Math.floor(chunk.length / bytes) * bytes;
1847
+ for (let i = 0; i < readableLength; i += bytes) {
1848
+ const value = chunk.readInt16LE(i);
1849
+ const clampedValue = Math.min(this.extrema[1], Math.max(this.extrema[0], value * vol));
1850
+ chunk.writeInt16LE(clampedValue, i);
1851
+ }
1852
+ this.buffer = chunk.subarray(readableLength);
1853
+ this.push(chunk.subarray(0, readableLength));
1854
+ done();
1961
1855
  }
1962
1856
  };
1963
1857
 
1964
- // src/struct/ExtractorPlugin.ts
1965
- var ExtractorPlugin = class extends Plugin {
1858
+ // src/core/manager/DisTubeVoiceManager.ts
1859
+ var import_voice3 = require("@discordjs/voice");
1860
+
1861
+ // src/core/manager/GuildIdManager.ts
1862
+ var GuildIdManager = class extends BaseManager {
1966
1863
  static {
1967
- __name(this, "ExtractorPlugin");
1864
+ __name(this, "GuildIdManager");
1865
+ }
1866
+ add(idOrInstance, data) {
1867
+ const id = resolveGuildId(idOrInstance);
1868
+ const existing = this.get(id);
1869
+ if (existing) return this;
1870
+ this.collection.set(id, data);
1871
+ return this;
1872
+ }
1873
+ get(idOrInstance) {
1874
+ return this.collection.get(resolveGuildId(idOrInstance));
1875
+ }
1876
+ remove(idOrInstance) {
1877
+ return this.collection.delete(resolveGuildId(idOrInstance));
1878
+ }
1879
+ has(idOrInstance) {
1880
+ return this.collection.has(resolveGuildId(idOrInstance));
1968
1881
  }
1969
- type = "extractor" /* EXTRACTOR */;
1970
1882
  };
1971
1883
 
1972
- // src/struct/InfoExtratorPlugin.ts
1973
- var InfoExtractorPlugin = class extends Plugin {
1884
+ // src/core/manager/DisTubeVoiceManager.ts
1885
+ var DisTubeVoiceManager = class extends GuildIdManager {
1974
1886
  static {
1975
- __name(this, "InfoExtractorPlugin");
1887
+ __name(this, "DisTubeVoiceManager");
1888
+ }
1889
+ /**
1890
+ * Create a {@link DisTubeVoice} instance
1891
+ * @param channel - A voice channel to join
1892
+ */
1893
+ create(channel) {
1894
+ const existing = this.get(channel.guildId);
1895
+ if (existing) {
1896
+ existing.channel = channel;
1897
+ return existing;
1898
+ }
1899
+ if ((0, import_voice3.getVoiceConnection)(resolveGuildId(channel), this.client.user?.id) || (0, import_voice3.getVoiceConnection)(resolveGuildId(channel))) {
1900
+ throw new DisTubeError("VOICE_ALREADY_CREATED");
1901
+ }
1902
+ return new DisTubeVoice(this, channel);
1903
+ }
1904
+ /**
1905
+ * Join a voice channel and wait until the connection is ready
1906
+ * @param channel - A voice channel to join
1907
+ */
1908
+ join(channel) {
1909
+ const existing = this.get(channel.guildId);
1910
+ if (existing) return existing.join(channel);
1911
+ return this.create(channel).join();
1912
+ }
1913
+ /**
1914
+ * Leave the connected voice channel in a guild
1915
+ * @param guild - Queue Resolvable
1916
+ */
1917
+ leave(guild) {
1918
+ const voice = this.get(guild);
1919
+ if (voice) {
1920
+ voice.leave();
1921
+ } else {
1922
+ const connection = (0, import_voice3.getVoiceConnection)(resolveGuildId(guild), this.client.user?.id) ?? (0, import_voice3.getVoiceConnection)(resolveGuildId(guild));
1923
+ if (connection && connection.state.status !== import_voice3.VoiceConnectionStatus.Destroyed) {
1924
+ connection.destroy();
1925
+ }
1926
+ }
1976
1927
  }
1977
- type = "info-extractor" /* INFO_EXTRACTOR */;
1978
1928
  };
1979
1929
 
1980
- // src/struct/PlayableExtratorPlugin.ts
1981
- var PlayableExtractorPlugin = class extends Plugin {
1930
+ // src/core/manager/QueueManager.ts
1931
+ var QueueManager = class extends GuildIdManager {
1982
1932
  static {
1983
- __name(this, "PlayableExtractorPlugin");
1933
+ __name(this, "QueueManager");
1984
1934
  }
1985
- type = "playable-extractor" /* PLAYABLE_EXTRACTOR */;
1986
- };
1987
-
1988
- // src/util.ts
1989
- var import_url = require("url");
1990
- var import_discord3 = require("discord.js");
1991
- var formatInt = /* @__PURE__ */ __name((int) => int < 10 ? `0${int}` : int, "formatInt");
1992
- function formatDuration(sec) {
1993
- if (!sec || !Number(sec)) return "00:00";
1994
- const seconds = Math.floor(sec % 60);
1995
- const minutes = Math.floor(sec % 3600 / 60);
1996
- const hours = Math.floor(sec / 3600);
1997
- if (hours > 0) return `${formatInt(hours)}:${formatInt(minutes)}:${formatInt(seconds)}`;
1998
- if (minutes > 0) return `${formatInt(minutes)}:${formatInt(seconds)}`;
1999
- return `00:${formatInt(seconds)}`;
2000
- }
2001
- __name(formatDuration, "formatDuration");
2002
- var SUPPORTED_PROTOCOL = ["https:", "http:", "file:"];
2003
- function isURL(input) {
2004
- if (typeof input !== "string" || input.includes(" ")) return false;
2005
- try {
2006
- const url = new import_url.URL(input);
2007
- if (!SUPPORTED_PROTOCOL.some((p) => p === url.protocol)) return false;
2008
- } catch {
2009
- return false;
1935
+ /**
1936
+ * Create a {@link Queue}
1937
+ * @param channel - A voice channel
1938
+ * @param textChannel - Default text channel
1939
+ * @returns Returns `true` if encounter an error
1940
+ */
1941
+ async create(channel, textChannel) {
1942
+ if (this.has(channel.guildId)) throw new DisTubeError("QUEUE_EXIST");
1943
+ this.debug(`[QueueManager] Creating queue for guild: ${channel.guildId}`);
1944
+ const voice = this.voices.create(channel);
1945
+ const queue = new Queue(this.distube, voice, textChannel);
1946
+ await queue._taskQueue.queuing();
1947
+ try {
1948
+ checkFFmpeg(this.distube);
1949
+ this.debug(`[QueueManager] Joining voice channel: ${channel.id}`);
1950
+ await voice.join();
1951
+ this.#voiceEventHandler(queue);
1952
+ this.add(queue.id, queue);
1953
+ this.emit("initQueue" /* INIT_QUEUE */, queue);
1954
+ return queue;
1955
+ } finally {
1956
+ queue._taskQueue.resolve();
1957
+ }
2010
1958
  }
2011
- return true;
2012
- }
2013
- __name(isURL, "isURL");
2014
- function checkIntents(options) {
2015
- const intents = options.intents instanceof import_discord3.IntentsBitField ? options.intents : new import_discord3.IntentsBitField(options.intents);
2016
- if (!intents.has(import_discord3.GatewayIntentBits.GuildVoiceStates)) throw new DisTubeError("MISSING_INTENTS", "GuildVoiceStates");
2017
- }
2018
- __name(checkIntents, "checkIntents");
2019
- function isVoiceChannelEmpty(voiceState) {
2020
- const guild = voiceState.guild;
2021
- const clientId = voiceState.client.user?.id;
2022
- if (!guild || !clientId) return false;
2023
- const voiceChannel = guild.members.me?.voice?.channel;
2024
- if (!voiceChannel) return false;
2025
- const members = voiceChannel.members.filter((m) => !m.user.bot);
2026
- return !members.size;
2027
- }
2028
- __name(isVoiceChannelEmpty, "isVoiceChannelEmpty");
2029
- function isSnowflake(id) {
2030
- try {
2031
- return import_discord3.SnowflakeUtil.deconstruct(id).timestamp > import_discord3.SnowflakeUtil.epoch;
2032
- } catch {
2033
- return false;
1959
+ /**
1960
+ * Listen to DisTubeVoice events and handle the Queue
1961
+ * @param queue - Queue
1962
+ */
1963
+ #voiceEventHandler(queue) {
1964
+ queue._listeners = {
1965
+ disconnect: /* @__PURE__ */ __name((error) => {
1966
+ queue.remove();
1967
+ this.emit("disconnect" /* DISCONNECT */, queue);
1968
+ if (error) this.emitError(error, queue, queue.songs?.[0]);
1969
+ }, "disconnect"),
1970
+ error: /* @__PURE__ */ __name((error) => this.#handlePlayingError(queue, error), "error"),
1971
+ finish: /* @__PURE__ */ __name(() => this.handleSongFinish(queue), "finish")
1972
+ };
1973
+ for (const event of objectKeys(queue._listeners)) {
1974
+ queue.voice.on(event, queue._listeners[event]);
1975
+ }
2034
1976
  }
2035
- }
2036
- __name(isSnowflake, "isSnowflake");
2037
- function isMemberInstance(member) {
2038
- return Boolean(member) && isSnowflake(member.id) && isSnowflake(member.guild?.id) && isSnowflake(member.user?.id) && member.id === member.user.id;
2039
- }
2040
- __name(isMemberInstance, "isMemberInstance");
2041
- function isTextChannelInstance(channel) {
2042
- return Boolean(channel) && isSnowflake(channel.id) && isSnowflake(channel.guildId || channel.guild?.id) && import_discord3.Constants.TextBasedChannelTypes.includes(channel.type) && typeof channel.send === "function" && (typeof channel.nsfw === "boolean" || typeof channel.parent?.nsfw === "boolean");
2043
- }
2044
- __name(isTextChannelInstance, "isTextChannelInstance");
2045
- function isMessageInstance(message) {
2046
- return Boolean(message) && isSnowflake(message.id) && isSnowflake(message.guildId || message.guild?.id) && isMemberInstance(message.member) && isTextChannelInstance(message.channel) && import_discord3.Constants.NonSystemMessageTypes.includes(message.type) && message.member.id === message.author?.id;
2047
- }
2048
- __name(isMessageInstance, "isMessageInstance");
2049
- function isSupportedVoiceChannel(channel) {
2050
- return Boolean(channel) && isSnowflake(channel.id) && isSnowflake(channel.guildId || channel.guild?.id) && import_discord3.Constants.VoiceBasedChannelTypes.includes(channel.type);
2051
- }
2052
- __name(isSupportedVoiceChannel, "isSupportedVoiceChannel");
2053
- function isGuildInstance(guild) {
2054
- return Boolean(guild) && isSnowflake(guild.id) && isSnowflake(guild.ownerId) && typeof guild.name === "string";
2055
- }
2056
- __name(isGuildInstance, "isGuildInstance");
2057
- function resolveGuildId(resolvable) {
2058
- let guildId;
2059
- if (typeof resolvable === "string") {
2060
- guildId = resolvable;
2061
- } else if (isObject(resolvable)) {
2062
- if ("guildId" in resolvable && resolvable.guildId) {
2063
- guildId = resolvable.guildId;
2064
- } else if (resolvable instanceof Queue || resolvable instanceof DisTubeVoice || isGuildInstance(resolvable)) {
2065
- guildId = resolvable.id;
2066
- } else if ("guild" in resolvable && isGuildInstance(resolvable.guild)) {
2067
- guildId = resolvable.guild.id;
1977
+ /**
1978
+ * Handle the queue when a Song finish
1979
+ * @param queue - queue
1980
+ */
1981
+ async handleSongFinish(queue) {
1982
+ if (queue._manualUpdate) {
1983
+ queue._manualUpdate = false;
1984
+ await this.playSong(queue);
1985
+ return;
1986
+ }
1987
+ this.debug(`[QueueManager] Handling song finish: ${queue.id}`);
1988
+ const song = queue.songs[0];
1989
+ this.emit("finishSong" /* FINISH_SONG */, queue, song);
1990
+ await queue._taskQueue.queuing();
1991
+ try {
1992
+ if (queue.stopped) return;
1993
+ if (queue.repeatMode === 2 /* QUEUE */) queue.songs.push(song);
1994
+ if (queue.repeatMode !== 1 /* SONG */) {
1995
+ const prev = queue.songs.shift();
1996
+ if (this.options.savePreviousSongs) queue.previousSongs.push(prev);
1997
+ else queue.previousSongs.push({ id: prev.id });
1998
+ }
1999
+ if (queue.songs.length === 0 && queue.autoplay) {
2000
+ try {
2001
+ this.debug(`[QueueManager] Adding related song: ${queue.id}`);
2002
+ await queue.addRelatedSong(song);
2003
+ } catch (e) {
2004
+ this.debug(`[${queue.id}] Add related song error: ${e.message}`);
2005
+ this.emit("noRelated" /* NO_RELATED */, queue, e);
2006
+ }
2007
+ }
2008
+ if (queue.songs.length === 0) {
2009
+ this.debug(`[${queue.id}] Queue is empty, stopping...`);
2010
+ if (!queue.autoplay) this.emit("finish" /* FINISH */, queue);
2011
+ queue.remove();
2012
+ return;
2013
+ }
2014
+ if (song !== queue.songs[0]) {
2015
+ const playedSong = song.stream.playFromSource ? song : song.stream.song;
2016
+ if (playedSong?.stream.playFromSource) delete playedSong.stream.url;
2017
+ }
2018
+ await this.playSong(queue, true);
2019
+ } finally {
2020
+ queue._taskQueue.resolve();
2068
2021
  }
2069
2022
  }
2070
- if (!isSnowflake(guildId)) throw new DisTubeError("INVALID_TYPE", "GuildIdResolvable", resolvable);
2071
- return guildId;
2072
- }
2073
- __name(resolveGuildId, "resolveGuildId");
2074
- function isClientInstance(client) {
2075
- return Boolean(client) && typeof client.login === "function";
2076
- }
2077
- __name(isClientInstance, "isClientInstance");
2078
- function checkInvalidKey(target, source, sourceName) {
2079
- if (!isObject(target)) throw new DisTubeError("INVALID_TYPE", "object", target, sourceName);
2080
- const sourceKeys = Array.isArray(source) ? source : objectKeys(source);
2081
- const invalidKey = objectKeys(target).find((key) => !sourceKeys.includes(key));
2082
- if (invalidKey) throw new DisTubeError("INVALID_KEY", sourceName, invalidKey);
2083
- }
2084
- __name(checkInvalidKey, "checkInvalidKey");
2085
- function isObject(obj) {
2086
- return typeof obj === "object" && obj !== null && !Array.isArray(obj);
2087
- }
2088
- __name(isObject, "isObject");
2089
- function objectKeys(obj) {
2090
- if (!isObject(obj)) return [];
2091
- return Object.keys(obj);
2092
- }
2093
- __name(objectKeys, "objectKeys");
2094
- function isNsfwChannel(channel) {
2095
- if (!isTextChannelInstance(channel)) return false;
2096
- if (channel.isThread()) return channel.parent?.nsfw ?? false;
2097
- return channel.nsfw;
2098
- }
2099
- __name(isNsfwChannel, "isNsfwChannel");
2100
- var isTruthy = /* @__PURE__ */ __name((x) => Boolean(x), "isTruthy");
2101
- var checkEncryptionLibraries = /* @__PURE__ */ __name(async () => {
2102
- if (await import("crypto").then((m) => m.getCiphers().includes("aes-256-gcm"))) return true;
2103
- for (const lib of [
2104
- "@noble/ciphers",
2105
- "@stablelib/xchacha20poly1305",
2106
- "sodium-native",
2107
- "sodium",
2108
- "libsodium-wrappers",
2109
- "tweetnacl"
2110
- ]) {
2023
+ /**
2024
+ * Handle error while playing
2025
+ * @param queue - queue
2026
+ * @param error - error
2027
+ */
2028
+ #handlePlayingError(queue, error) {
2029
+ const song = queue.songs.shift();
2111
2030
  try {
2112
- await import(lib);
2113
- return true;
2031
+ error.name = "PlayingError";
2114
2032
  } catch {
2115
2033
  }
2034
+ this.debug(`[${queue.id}] Error while playing: ${error.stack || error.message}`);
2035
+ this.emitError(error, queue, song);
2036
+ if (queue.songs.length > 0) {
2037
+ this.debug(`[${queue.id}] Playing next song: ${queue.songs[0]}`);
2038
+ this.playSong(queue);
2039
+ } else {
2040
+ this.debug(`[${queue.id}] Queue is empty, stopping...`);
2041
+ queue.stop();
2042
+ }
2116
2043
  }
2117
- return false;
2118
- }, "checkEncryptionLibraries");
2044
+ /**
2045
+ * Play a song on voice connection with queue properties
2046
+ * @param queue - The guild queue to play
2047
+ * @param emitPlaySong - Whether or not emit {@link Events.PLAY_SONG} event
2048
+ */
2049
+ async playSong(queue, emitPlaySong = true) {
2050
+ if (!queue) return;
2051
+ if (queue.stopped || !queue.songs.length) {
2052
+ queue.stop();
2053
+ return;
2054
+ }
2055
+ try {
2056
+ const song = queue.songs[0];
2057
+ this.debug(`[${queue.id}] Getting stream from: ${song}`);
2058
+ await this.handler.attachStreamInfo(song);
2059
+ const willPlaySong = song.stream.playFromSource ? song : song.stream.song;
2060
+ const stream = willPlaySong?.stream;
2061
+ if (!willPlaySong || !stream?.playFromSource || !stream.url) throw new DisTubeError("NO_STREAM_URL", `${song}`);
2062
+ this.debug(`[${queue.id}] Creating DisTubeStream for: ${willPlaySong}`);
2063
+ const streamOptions = {
2064
+ ffmpeg: {
2065
+ path: this.options.ffmpeg.path,
2066
+ args: {
2067
+ global: { ...queue.ffmpegArgs.global },
2068
+ input: { ...queue.ffmpegArgs.input },
2069
+ output: { ...queue.ffmpegArgs.output, ...queue.filters.ffmpegArgs }
2070
+ }
2071
+ },
2072
+ seek: willPlaySong.duration ? queue._beginTime : void 0
2073
+ };
2074
+ const dtStream = new DisTubeStream(stream.url, streamOptions);
2075
+ dtStream.on("debug", (data) => this.emit("ffmpegDebug" /* FFMPEG_DEBUG */, `[${queue.id}] ${data}`));
2076
+ this.debug(`[${queue.id}] Started playing: ${willPlaySong}`);
2077
+ await queue.voice.play(dtStream);
2078
+ if (emitPlaySong) this.emit("playSong" /* PLAY_SONG */, queue, song);
2079
+ } catch (e) {
2080
+ this.#handlePlayingError(queue, e);
2081
+ }
2082
+ }
2083
+ };
2119
2084
 
2120
2085
  // src/DisTube.ts
2121
2086
  var import_tiny_typed_emitter3 = require("tiny-typed-emitter");
@@ -2252,7 +2217,7 @@ var DisTube = class extends import_tiny_typed_emitter3.TypedEmitter {
2252
2217
  this.queues = new QueueManager(this);
2253
2218
  this.filters = { ...defaultFilters, ...this.options.customFilters };
2254
2219
  this.plugins = [...this.options.plugins];
2255
- this.plugins.forEach((p) => p.init(this));
2220
+ for (const p of this.plugins) p.init(this);
2256
2221
  }
2257
2222
  static get version() {
2258
2223
  return version;
@@ -2422,8 +2387,8 @@ ${e.message}`;
2422
2387
  * @param guild - The type can be resolved to give a {@link Queue}
2423
2388
  * @returns The new Song will be played
2424
2389
  */
2425
- skip(guild) {
2426
- return this.#getQueue(guild).skip();
2390
+ skip(guild, options) {
2391
+ return this.#getQueue(guild).skip(options);
2427
2392
  }
2428
2393
  /**
2429
2394
  * Play the previous song
@@ -2448,8 +2413,8 @@ ${e.message}`;
2448
2413
  * @param num - The song number to play
2449
2414
  * @returns The new Song will be played
2450
2415
  */
2451
- jump(guild, num) {
2452
- return this.#getQueue(guild).jump(num);
2416
+ jump(guild, num, options) {
2417
+ return this.#getQueue(guild).jump(num, options);
2453
2418
  }
2454
2419
  /**
2455
2420
  * Set the repeat mode of the guild queue.
@@ -2506,8 +2471,43 @@ ${e.message}`;
2506
2471
  }
2507
2472
  };
2508
2473
 
2509
- // src/index.ts
2510
- var version = "5.0.7";
2474
+ // src/struct/Plugin.ts
2475
+ var Plugin = class {
2476
+ static {
2477
+ __name(this, "Plugin");
2478
+ }
2479
+ /**
2480
+ * DisTube
2481
+ */
2482
+ distube;
2483
+ init(distube) {
2484
+ this.distube = distube;
2485
+ }
2486
+ };
2487
+
2488
+ // src/struct/ExtractorPlugin.ts
2489
+ var ExtractorPlugin = class extends Plugin {
2490
+ static {
2491
+ __name(this, "ExtractorPlugin");
2492
+ }
2493
+ type = "extractor" /* EXTRACTOR */;
2494
+ };
2495
+
2496
+ // src/struct/InfoExtratorPlugin.ts
2497
+ var InfoExtractorPlugin = class extends Plugin {
2498
+ static {
2499
+ __name(this, "InfoExtractorPlugin");
2500
+ }
2501
+ type = "info-extractor" /* INFO_EXTRACTOR */;
2502
+ };
2503
+
2504
+ // src/struct/PlayableExtratorPlugin.ts
2505
+ var PlayableExtractorPlugin = class extends Plugin {
2506
+ static {
2507
+ __name(this, "PlayableExtractorPlugin");
2508
+ }
2509
+ type = "playable-extractor" /* PLAYABLE_EXTRACTOR */;
2510
+ };
2511
2511
  // Annotate the CommonJS export names for ESM import in node:
2512
2512
  0 && (module.exports = {
2513
2513
  BaseManager,