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