distube 5.0.7 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs 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.0";
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,1838 @@ 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
+ * @returns The added song
1139
1005
  */
1140
- create(channel) {
1141
- const existing = this.get(channel.guildId);
1142
- if (existing) {
1143
- existing.channel = channel;
1144
- return existing;
1006
+ async addRelatedSong() {
1007
+ const current = this.songs?.[0];
1008
+ if (!current) throw new DisTubeError("NO_PLAYING_SONG");
1009
+ const prevIds = this.previousSongs.map((p) => p.id);
1010
+ const relatedSongs = (await this.#getRelatedSong(current)).filter((s) => !prevIds.includes(s.id));
1011
+ this.debug(`[${this.id}] Getting related songs from: ${current}`);
1012
+ if (!relatedSongs.length && !current.stream.playFromSource) {
1013
+ const altSong = current.stream.song;
1014
+ if (altSong) relatedSongs.push(...(await this.#getRelatedSong(altSong)).filter((s) => !prevIds.includes(s.id)));
1015
+ this.debug(`[${this.id}] Getting related songs from streamed song: ${altSong}`);
1145
1016
  }
1146
- if (getVoiceConnection(resolveGuildId(channel), this.client.user?.id) || getVoiceConnection(resolveGuildId(channel))) {
1147
- throw new DisTubeError("VOICE_ALREADY_CREATED");
1017
+ const song = relatedSongs[0];
1018
+ if (!song) throw new DisTubeError("NO_RELATED");
1019
+ song.metadata = current.metadata;
1020
+ song.member = this.clientMember;
1021
+ this.addToQueue(song);
1022
+ return song;
1023
+ }
1024
+ /**
1025
+ * Stop the guild stream and delete the queue
1026
+ */
1027
+ async stop() {
1028
+ await this._taskQueue.queuing();
1029
+ try {
1030
+ this.voice.stop();
1031
+ this.remove();
1032
+ } finally {
1033
+ this._taskQueue.resolve();
1148
1034
  }
1149
- return new DisTubeVoice(this, channel);
1150
1035
  }
1151
1036
  /**
1152
- * Join a voice channel and wait until the connection is ready
1153
- * @param channel - A voice channel to join
1037
+ * Remove the queue from the manager
1154
1038
  */
1155
- join(channel) {
1156
- const existing = this.get(channel.guildId);
1157
- if (existing) return existing.join(channel);
1158
- return this.create(channel).join();
1039
+ remove() {
1040
+ this.playing = false;
1041
+ this.paused = false;
1042
+ this.stopped = true;
1043
+ this.songs = [];
1044
+ this.previousSongs = [];
1045
+ if (this._listeners) for (const event of objectKeys(this._listeners)) this.voice.off(event, this._listeners[event]);
1046
+ this.queues.remove(this.id);
1047
+ this.emit("deleteQueue" /* DELETE_QUEUE */, this);
1159
1048
  }
1160
1049
  /**
1161
- * Leave the connected voice channel in a guild
1162
- * @param guild - Queue Resolvable
1050
+ * Toggle autoplay mode
1051
+ * @returns Autoplay mode state
1163
1052
  */
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
- }
1053
+ toggleAutoplay() {
1054
+ this.autoplay = !this.autoplay;
1055
+ return this.autoplay;
1056
+ }
1057
+ /**
1058
+ * Play the first song in the queue
1059
+ * @param emitPlaySong - Whether or not emit {@link Events.PLAY_SONG} event
1060
+ */
1061
+ play(emitPlaySong = true) {
1062
+ if (this.stopped) throw new DisTubeError("QUEUE_STOPPED");
1063
+ this.playing = true;
1064
+ return this.queues.playSong(this, emitPlaySong);
1174
1065
  }
1175
1066
  };
1176
1067
 
1177
- // src/core/manager/FilterManager.ts
1178
- var FilterManager = class extends BaseManager {
1179
- static {
1180
- __name(this, "FilterManager");
1068
+ // src/util.ts
1069
+ var formatInt = /* @__PURE__ */ __name((int) => int < 10 ? `0${int}` : int, "formatInt");
1070
+ function formatDuration(sec) {
1071
+ if (!sec || !Number(sec)) return "00:00";
1072
+ const seconds = Math.floor(sec % 60);
1073
+ const minutes = Math.floor(sec % 3600 / 60);
1074
+ const hours = Math.floor(sec / 3600);
1075
+ if (hours > 0) return `${formatInt(hours)}:${formatInt(minutes)}:${formatInt(seconds)}`;
1076
+ if (minutes > 0) return `${formatInt(minutes)}:${formatInt(seconds)}`;
1077
+ return `00:${formatInt(seconds)}`;
1078
+ }
1079
+ __name(formatDuration, "formatDuration");
1080
+ var SUPPORTED_PROTOCOL = ["https:", "http:", "file:"];
1081
+ function isURL(input) {
1082
+ if (typeof input !== "string" || input.includes(" ")) return false;
1083
+ try {
1084
+ const url = new URL2(input);
1085
+ if (!SUPPORTED_PROTOCOL.some((p) => p === url.protocol)) return false;
1086
+ } catch {
1087
+ return false;
1181
1088
  }
1182
- /**
1183
- * The queue to manage
1184
- */
1185
- queue;
1186
- constructor(queue) {
1187
- super(queue.distube);
1188
- this.queue = queue;
1089
+ return true;
1090
+ }
1091
+ __name(isURL, "isURL");
1092
+ function checkIntents(options) {
1093
+ const intents = options.intents instanceof IntentsBitField ? options.intents : new IntentsBitField(options.intents);
1094
+ if (!intents.has(GatewayIntentBits.GuildVoiceStates)) throw new DisTubeError("MISSING_INTENTS", "GuildVoiceStates");
1095
+ }
1096
+ __name(checkIntents, "checkIntents");
1097
+ function isVoiceChannelEmpty(voiceState) {
1098
+ const guild = voiceState.guild;
1099
+ const clientId = voiceState.client.user?.id;
1100
+ if (!guild || !clientId) return false;
1101
+ const voiceChannel = guild.members.me?.voice?.channel;
1102
+ if (!voiceChannel) return false;
1103
+ const members = voiceChannel.members.filter((m) => !m.user.bot);
1104
+ return !members.size;
1105
+ }
1106
+ __name(isVoiceChannelEmpty, "isVoiceChannelEmpty");
1107
+ function isSnowflake(id) {
1108
+ try {
1109
+ return SnowflakeUtil.deconstruct(id).timestamp > SnowflakeUtil.epoch;
1110
+ } catch {
1111
+ return false;
1189
1112
  }
1190
- #resolve(filter) {
1191
- if (typeof filter === "object" && typeof filter.name === "string" && typeof filter.value === "string") {
1192
- return filter;
1113
+ }
1114
+ __name(isSnowflake, "isSnowflake");
1115
+ function isMemberInstance(member) {
1116
+ return Boolean(member) && isSnowflake(member.id) && isSnowflake(member.guild?.id) && isSnowflake(member.user?.id) && member.id === member.user.id;
1117
+ }
1118
+ __name(isMemberInstance, "isMemberInstance");
1119
+ function isTextChannelInstance(channel) {
1120
+ 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");
1121
+ }
1122
+ __name(isTextChannelInstance, "isTextChannelInstance");
1123
+ function isMessageInstance(message) {
1124
+ 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;
1125
+ }
1126
+ __name(isMessageInstance, "isMessageInstance");
1127
+ function isSupportedVoiceChannel(channel) {
1128
+ return Boolean(channel) && isSnowflake(channel.id) && isSnowflake(channel.guildId || channel.guild?.id) && Constants2.VoiceBasedChannelTypes.includes(channel.type);
1129
+ }
1130
+ __name(isSupportedVoiceChannel, "isSupportedVoiceChannel");
1131
+ function isGuildInstance(guild) {
1132
+ return Boolean(guild) && isSnowflake(guild.id) && isSnowflake(guild.ownerId) && typeof guild.name === "string";
1133
+ }
1134
+ __name(isGuildInstance, "isGuildInstance");
1135
+ function resolveGuildId(resolvable) {
1136
+ let guildId;
1137
+ if (typeof resolvable === "string") {
1138
+ guildId = resolvable;
1139
+ } else if (isObject(resolvable)) {
1140
+ if ("guildId" in resolvable && resolvable.guildId) {
1141
+ guildId = resolvable.guildId;
1142
+ } else if (resolvable instanceof Queue || resolvable instanceof DisTubeVoice || isGuildInstance(resolvable)) {
1143
+ guildId = resolvable.id;
1144
+ } else if ("guild" in resolvable && isGuildInstance(resolvable.guild)) {
1145
+ guildId = resolvable.guild.id;
1193
1146
  }
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
- };
1147
+ }
1148
+ if (!isSnowflake(guildId)) throw new DisTubeError("INVALID_TYPE", "GuildIdResolvable", resolvable);
1149
+ return guildId;
1150
+ }
1151
+ __name(resolveGuildId, "resolveGuildId");
1152
+ function isClientInstance(client) {
1153
+ return Boolean(client) && typeof client.login === "function";
1154
+ }
1155
+ __name(isClientInstance, "isClientInstance");
1156
+ function checkInvalidKey(target, source, sourceName) {
1157
+ if (!isObject(target)) throw new DisTubeError("INVALID_TYPE", "object", target, sourceName);
1158
+ const sourceKeys = Array.isArray(source) ? source : objectKeys(source);
1159
+ const invalidKey = objectKeys(target).find((key) => !sourceKeys.includes(key));
1160
+ if (invalidKey) throw new DisTubeError("INVALID_KEY", sourceName, invalidKey);
1161
+ }
1162
+ __name(checkInvalidKey, "checkInvalidKey");
1163
+ function isObject(obj) {
1164
+ return typeof obj === "object" && obj !== null && !Array.isArray(obj);
1165
+ }
1166
+ __name(isObject, "isObject");
1167
+ function objectKeys(obj) {
1168
+ if (!isObject(obj)) return [];
1169
+ return Object.keys(obj);
1170
+ }
1171
+ __name(objectKeys, "objectKeys");
1172
+ function isNsfwChannel(channel) {
1173
+ if (!isTextChannelInstance(channel)) return false;
1174
+ if (channel.isThread()) return channel.parent?.nsfw ?? false;
1175
+ return channel.nsfw;
1176
+ }
1177
+ __name(isNsfwChannel, "isNsfwChannel");
1178
+ var isTruthy = /* @__PURE__ */ __name((x) => Boolean(x), "isTruthy");
1179
+ var checkEncryptionLibraries = /* @__PURE__ */ __name(async () => {
1180
+ if (await import("crypto").then((m) => m.getCiphers().includes("aes-256-gcm"))) return true;
1181
+ for (const lib of [
1182
+ "@noble/ciphers",
1183
+ "@stablelib/xchacha20poly1305",
1184
+ "sodium-native",
1185
+ "sodium",
1186
+ "libsodium-wrappers",
1187
+ "tweetnacl"
1188
+ ]) {
1189
+ try {
1190
+ await import(lib);
1191
+ return true;
1192
+ } catch {
1199
1193
  }
1200
- throw new DisTubeError("INVALID_TYPE", "FilterResolvable", filter, "filter");
1201
1194
  }
1202
- #apply() {
1203
- this.queue._beginTime = this.queue.currentTime;
1204
- this.queue.play(false);
1195
+ return false;
1196
+ }, "checkEncryptionLibraries");
1197
+
1198
+ // src/struct/Playlist.ts
1199
+ var Playlist = class {
1200
+ static {
1201
+ __name(this, "Playlist");
1205
1202
  }
1206
1203
  /**
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
1204
+ * Playlist source.
1210
1205
  */
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
- }
1206
+ source;
1224
1207
  /**
1225
- * Clear enabled filters of the manager
1208
+ * Songs in the playlist.
1226
1209
  */
1227
- clear() {
1228
- return this.set([]);
1229
- }
1210
+ songs;
1230
1211
  /**
1231
- * Set the filters applied to the manager
1232
- * @param filters - The filters to apply
1212
+ * Playlist ID.
1233
1213
  */
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
- }
1214
+ id;
1247
1215
  /**
1248
- * Disable a filter or multiple filters
1249
- * @param filterOrFilters - The filter or filters to disable
1216
+ * Playlist name.
1250
1217
  */
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
- }
1218
+ name;
1257
1219
  /**
1258
- * Check whether a filter enabled or not
1259
- * @param filter - The filter to check
1220
+ * Playlist URL.
1260
1221
  */
1261
- has(filter) {
1262
- return this.collection.has(typeof filter === "string" ? filter : this.#resolve(filter).name);
1263
- }
1222
+ url;
1264
1223
  /**
1265
- * Array of enabled filter names
1224
+ * Playlist thumbnail.
1266
1225
  */
1267
- get names() {
1268
- return [...this.collection.keys()];
1269
- }
1226
+ thumbnail;
1227
+ #metadata;
1228
+ #member;
1270
1229
  /**
1271
- * Array of enabled filters
1230
+ * Create a Playlist
1231
+ * @param playlist - Raw playlist info
1232
+ * @param options - Optional data
1272
1233
  */
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");
1234
+ constructor(playlist, { member, metadata } = {}) {
1235
+ if (!Array.isArray(playlist.songs) || !playlist.songs.length) throw new DisTubeError("EMPTY_PLAYLIST");
1236
+ this.source = playlist.source.toLowerCase();
1237
+ this.songs = playlist.songs;
1238
+ this.name = playlist.name;
1239
+ this.id = playlist.id;
1240
+ this.url = playlist.url;
1241
+ this.thumbnail = playlist.thumbnail;
1242
+ this.member = member;
1243
+ this.songs.forEach((s) => s.playlist = this);
1244
+ this.metadata = metadata;
1288
1245
  }
1289
1246
  /**
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
1247
+ * Playlist duration in second.
1294
1248
  */
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
- }
1249
+ get duration() {
1250
+ return this.songs.reduce((prev, next) => prev + next.duration, 0);
1312
1251
  }
1313
1252
  /**
1314
- * Listen to DisTubeVoice events and handle the Queue
1315
- * @param queue - Queue
1253
+ * Formatted duration string `hh:mm:ss`.
1316
1254
  */
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
- }
1255
+ get formattedDuration() {
1256
+ return formatDuration(this.duration);
1330
1257
  }
1331
1258
  /**
1332
- * Whether or not emit playSong event
1333
- * @param queue - Queue
1259
+ * User requested.
1334
1260
  */
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;
1261
+ get member() {
1262
+ return this.#member;
1339
1263
  }
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
- }
1264
+ set member(member) {
1265
+ if (!isMemberInstance(member)) return;
1266
+ this.#member = member;
1267
+ this.songs.forEach((s) => s.member = this.member);
1389
1268
  }
1390
1269
  /**
1391
- * Handle error while playing
1392
- * @param queue - queue
1393
- * @param error - error
1270
+ * User requested.
1394
1271
  */
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
- }
1272
+ get user() {
1273
+ return this.member?.user;
1412
1274
  }
1413
1275
  /**
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
1276
+ * Optional metadata that can be used to identify the playlist.
1417
1277
  */
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
- }
1278
+ get metadata() {
1279
+ return this.#metadata;
1280
+ }
1281
+ set metadata(metadata) {
1282
+ this.#metadata = metadata;
1283
+ this.songs.forEach((s) => s.metadata = metadata);
1284
+ }
1285
+ toString() {
1286
+ return `${this.name} (${this.songs.length} songs)`;
1451
1287
  }
1452
1288
  };
1453
1289
 
1454
- // src/struct/Queue.ts
1455
- var Queue = class extends DisTubeBase {
1290
+ // src/struct/Song.ts
1291
+ var Song = class {
1456
1292
  static {
1457
- __name(this, "Queue");
1293
+ __name(this, "Song");
1458
1294
  }
1459
1295
  /**
1460
- * Queue id (Guild id)
1296
+ * The source of this song info
1461
1297
  */
1462
- id;
1298
+ source;
1463
1299
  /**
1464
- * Voice connection of this queue.
1300
+ * Song ID.
1465
1301
  */
1466
- voice;
1302
+ id;
1467
1303
  /**
1468
- * List of songs in the queue (The first one is the playing song)
1304
+ * Song name.
1469
1305
  */
1470
- songs;
1306
+ name;
1471
1307
  /**
1472
- * List of the previous songs.
1308
+ * Indicates if the song is an active live.
1473
1309
  */
1474
- previousSongs;
1310
+ isLive;
1475
1311
  /**
1476
- * Whether stream is currently stopped.
1312
+ * Song duration.
1477
1313
  */
1478
- stopped;
1314
+ duration;
1479
1315
  /**
1480
- * Whether or not the stream is currently playing.
1316
+ * Formatted duration string (`hh:mm:ss`, `mm:ss` or `Live`).
1481
1317
  */
1482
- playing;
1318
+ formattedDuration;
1483
1319
  /**
1484
- * Whether or not the stream is currently paused.
1320
+ * Song URL.
1485
1321
  */
1486
- paused;
1322
+ url;
1487
1323
  /**
1488
- * Type of repeat mode (`0` is disabled, `1` is repeating a song, `2` is repeating
1489
- * all the queue). Default value: `0` (disabled)
1324
+ * Song thumbnail.
1490
1325
  */
1491
- repeatMode;
1326
+ thumbnail;
1492
1327
  /**
1493
- * Whether or not the autoplay mode is enabled. Default value: `false`
1328
+ * Song view count
1494
1329
  */
1495
- autoplay;
1330
+ views;
1496
1331
  /**
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
1332
+ * Song like count
1499
1333
  */
1500
- ffmpegArgs;
1334
+ likes;
1501
1335
  /**
1502
- * The text channel of the Queue. (Default: where the first command is called).
1336
+ * Song dislike count
1503
1337
  */
1504
- textChannel;
1505
- #filters;
1338
+ dislikes;
1506
1339
  /**
1507
- * What time in the song to begin (in seconds).
1340
+ * Song repost (share) count
1508
1341
  */
1509
- _beginTime;
1342
+ reposts;
1510
1343
  /**
1511
- * Whether or not the last song was skipped to next song.
1344
+ * Song uploader
1512
1345
  */
1513
- _next;
1346
+ uploader;
1514
1347
  /**
1515
- * Whether or not the last song was skipped to previous song.
1348
+ * Whether or not an age-restricted content
1516
1349
  */
1517
- _prev;
1350
+ ageRestricted;
1518
1351
  /**
1519
- * Task queuing system
1352
+ * Stream info
1520
1353
  */
1521
- _taskQueue;
1354
+ stream;
1522
1355
  /**
1523
- * {@link DisTubeVoice} listener
1356
+ * The plugin that created this song
1524
1357
  */
1525
- _listeners;
1358
+ plugin;
1359
+ #metadata;
1360
+ #member;
1361
+ #playlist;
1526
1362
  /**
1527
- * Create a queue for the guild
1528
- * @param distube - DisTube
1529
- * @param voice - Voice connection
1530
- * @param textChannel - Default text channel
1363
+ * Create a Song
1364
+ *
1365
+ * @param info - Raw song info
1366
+ * @param options - Optional data
1531
1367
  */
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 }
1368
+ constructor(info, { member, metadata } = {}) {
1369
+ this.source = info.source.toLowerCase();
1370
+ this.metadata = metadata;
1371
+ this.member = member;
1372
+ this.id = info.id;
1373
+ this.name = info.name;
1374
+ this.isLive = info.isLive;
1375
+ this.duration = this.isLive || !info.duration ? 0 : info.duration;
1376
+ this.formattedDuration = this.isLive ? "Live" : formatDuration(this.duration);
1377
+ this.url = info.url;
1378
+ this.thumbnail = info.thumbnail;
1379
+ this.views = info.views;
1380
+ this.likes = info.likes;
1381
+ this.dislikes = info.dislikes;
1382
+ this.reposts = info.reposts;
1383
+ this.uploader = {
1384
+ name: info.uploader?.name,
1385
+ url: info.uploader?.url
1555
1386
  };
1387
+ this.ageRestricted = info.ageRestricted;
1388
+ this.stream = { playFromSource: info.playFromSource };
1389
+ this.plugin = info.plugin;
1556
1390
  }
1557
1391
  /**
1558
- * The client user as a `GuildMember` of this queue's guild
1392
+ * The playlist this song belongs to
1559
1393
  */
1560
- get clientMember() {
1561
- return this.voice.channel.guild.members.me ?? void 0;
1394
+ get playlist() {
1395
+ return this.#playlist;
1562
1396
  }
1563
- /**
1564
- * The filter manager of the queue
1565
- */
1566
- get filters() {
1567
- return this.#filters;
1397
+ set playlist(playlist) {
1398
+ if (!(playlist instanceof Playlist)) throw new DisTubeError("INVALID_TYPE", "Playlist", playlist, "Song#playlist");
1399
+ this.#playlist = playlist;
1400
+ this.member = playlist.member;
1568
1401
  }
1569
1402
  /**
1570
- * Formatted duration string.
1403
+ * User requested to play this song.
1571
1404
  */
1572
- get formattedDuration() {
1573
- return formatDuration(this.duration);
1405
+ get member() {
1406
+ return this.#member;
1574
1407
  }
1575
- /**
1576
- * Queue's duration.
1577
- */
1578
- get duration() {
1579
- return this.songs.length ? this.songs.reduce((prev, next) => prev + next.duration, 0) : 0;
1408
+ set member(member) {
1409
+ if (isMemberInstance(member)) this.#member = member;
1580
1410
  }
1581
1411
  /**
1582
- * What time in the song is playing (in seconds).
1412
+ * User requested to play this song.
1583
1413
  */
1584
- get currentTime() {
1585
- return this.voice.playbackDuration + this._beginTime;
1414
+ get user() {
1415
+ return this.member?.user;
1586
1416
  }
1587
1417
  /**
1588
- * Formatted {@link Queue#currentTime} string.
1418
+ * Optional metadata that can be used to identify the song. This is attached by the
1419
+ * {@link DisTube#play} method.
1589
1420
  */
1590
- get formattedCurrentTime() {
1591
- return formatDuration(this.currentTime);
1421
+ get metadata() {
1422
+ return this.#metadata;
1592
1423
  }
1593
- /**
1594
- * The voice channel playing in.
1595
- */
1596
- get voiceChannel() {
1597
- return this.clientMember?.voice?.channel ?? null;
1424
+ set metadata(metadata) {
1425
+ this.#metadata = metadata;
1598
1426
  }
1599
- /**
1600
- * Get or set the stream volume. Default value: `50`.
1601
- */
1602
- get volume() {
1603
- return this.voice.volume;
1427
+ toString() {
1428
+ return this.name || this.url || this.id || "Unknown";
1604
1429
  }
1605
- set volume(value) {
1606
- this.voice.volume = value;
1430
+ };
1431
+
1432
+ // src/core/DisTubeHandler.ts
1433
+ var REDIRECT_CODES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
1434
+ var DisTubeHandler = class extends DisTubeBase {
1435
+ static {
1436
+ __name(this, "DisTubeHandler");
1607
1437
  }
1608
1438
  /**
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
1439
+ * Resolve a url or a supported object to a {@link Song} or {@link Playlist}
1440
+ * @throws {@link DisTubeError}
1441
+ * @param input - Resolvable input
1442
+ * @param options - Optional options
1443
+ * @returns Resolved
1613
1444
  */
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");
1445
+ async resolve(input, options = {}) {
1446
+ if (input instanceof Song || input instanceof Playlist) {
1447
+ if ("metadata" in options) input.metadata = options.metadata;
1448
+ if ("member" in options) input.member = options.member;
1449
+ return input;
1621
1450
  }
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);
1451
+ if (typeof input === "string") {
1452
+ if (isURL(input)) {
1453
+ const plugin = await this._getPluginFromURL(input) || await this._getPluginFromURL(input = await this.followRedirectLink(input));
1454
+ if (!plugin) throw new DisTubeError("NOT_SUPPORTED_URL");
1455
+ this.debug(`[${plugin.constructor.name}] Resolving from url: ${input}`);
1456
+ return plugin.resolve(input, options);
1457
+ }
1458
+ try {
1459
+ const song = await this.#searchSong(input, options);
1460
+ if (song) return song;
1461
+ } catch {
1462
+ throw new DisTubeError("NO_RESULT", input);
1463
+ }
1629
1464
  }
1630
- return this;
1631
- }
1632
- /**
1633
- * @returns `true` if the queue is playing
1634
- */
1635
- isPlaying() {
1636
- return this.playing;
1465
+ throw new DisTubeError("CANNOT_RESOLVE_SONG", input);
1637
1466
  }
1638
- /**
1639
- * @returns `true` if the queue is paused
1640
- */
1641
- isPaused() {
1642
- return this.paused;
1467
+ async _getPluginFromURL(url) {
1468
+ for (const plugin of this.plugins) if (await plugin.validate(url)) return plugin;
1469
+ return null;
1643
1470
  }
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();
1471
+ async _getPluginFromSong(song, types, validate = true) {
1472
+ if (!types || types.includes(song.plugin?.type)) return song.plugin;
1473
+ if (!song.url) return null;
1474
+ for (const plugin of this.plugins) {
1475
+ if ((!types || types.includes(plugin?.type)) && (!validate || await plugin.validate(song.url))) {
1476
+ return plugin;
1477
+ }
1657
1478
  }
1479
+ return null;
1658
1480
  }
1659
- /**
1660
- * Resume the guild stream
1661
- * @returns The guild queue
1662
- */
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();
1481
+ async #searchSong(query, options = {}, getStreamURL = false) {
1482
+ const plugins = this.plugins.filter((p) => p.type === "extractor" /* EXTRACTOR */);
1483
+ if (!plugins.length) throw new DisTubeError("NO_EXTRACTOR_PLUGIN");
1484
+ for (const plugin of plugins) {
1485
+ this.debug(`[${plugin.constructor.name}] Searching for song: ${query}`);
1486
+ const result = await plugin.searchSong(query, options);
1487
+ if (result) {
1488
+ if (getStreamURL && result.stream.playFromSource) result.stream.url = await plugin.getStreamURL(result);
1489
+ return result;
1490
+ }
1672
1491
  }
1492
+ return null;
1673
1493
  }
1674
1494
  /**
1675
- * Set the guild stream's volume
1676
- * @param percent - The percentage of volume you want to set
1677
- * @returns The guild queue
1495
+ * Get {@link Song}'s stream info and attach it to the song.
1496
+ * @param song - A Song
1678
1497
  */
1679
- setVolume(percent) {
1680
- this.volume = percent;
1681
- return this;
1498
+ async attachStreamInfo(song) {
1499
+ if (song.stream.playFromSource) {
1500
+ if (song.stream.url) return;
1501
+ this.debug(`[DisTubeHandler] Getting stream info: ${song}`);
1502
+ const plugin = await this._getPluginFromSong(song, ["extractor" /* EXTRACTOR */, "playable-extractor" /* PLAYABLE_EXTRACTOR */]);
1503
+ if (!plugin) throw new DisTubeError("NOT_SUPPORTED_SONG", song.toString());
1504
+ this.debug(`[${plugin.constructor.name}] Getting stream URL: ${song}`);
1505
+ song.stream.url = await plugin.getStreamURL(song);
1506
+ if (!song.stream.url) throw new DisTubeError("CANNOT_GET_STREAM_URL", song.toString());
1507
+ } else {
1508
+ if (song.stream.song?.stream?.playFromSource && song.stream.song.stream.url) return;
1509
+ this.debug(`[DisTubeHandler] Getting stream info: ${song}`);
1510
+ const plugin = await this._getPluginFromSong(song, ["info-extractor" /* INFO_EXTRACTOR */]);
1511
+ if (!plugin) throw new DisTubeError("NOT_SUPPORTED_SONG", song.toString());
1512
+ this.debug(`[${plugin.constructor.name}] Creating search query for: ${song}`);
1513
+ const query = await plugin.createSearchQuery(song);
1514
+ if (!query) throw new DisTubeError("CANNOT_GET_SEARCH_QUERY", song.toString());
1515
+ const altSong = await this.#searchSong(query, { metadata: song.metadata, member: song.member }, true);
1516
+ if (!altSong || !altSong.stream.playFromSource) throw new DisTubeError("NO_RESULT", query || song.toString());
1517
+ song.stream.song = altSong;
1518
+ }
1682
1519
  }
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");
1520
+ async followRedirectLink(url, maxRedirect = 5) {
1521
+ if (maxRedirect === 0) return url;
1522
+ const res = await request(url, {
1523
+ method: "HEAD",
1524
+ headers: {
1525
+ "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"
1695
1526
  }
1696
- const song = this.songs[1];
1697
- this._next = true;
1698
- this.voice.stop();
1699
- return song;
1700
- } finally {
1701
- this._taskQueue.resolve();
1527
+ });
1528
+ if (REDIRECT_CODES.has(res.statusCode ?? 200)) {
1529
+ let location = res.headers.location;
1530
+ if (typeof location !== "string") location = location?.[0] ?? url;
1531
+ return this.followRedirectLink(location, --maxRedirect);
1702
1532
  }
1533
+ return url;
1703
1534
  }
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");
1714
- }
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();
1535
+ };
1536
+
1537
+ // src/core/DisTubeOptions.ts
1538
+ var Options = class {
1539
+ static {
1540
+ __name(this, "Options");
1541
+ }
1542
+ plugins;
1543
+ emitNewSongOnly;
1544
+ savePreviousSongs;
1545
+ customFilters;
1546
+ nsfw;
1547
+ emitAddSongWhenCreatingQueue;
1548
+ emitAddListWhenCreatingQueue;
1549
+ joinNewVoiceChannel;
1550
+ ffmpeg;
1551
+ constructor(options) {
1552
+ if (typeof options !== "object" || Array.isArray(options)) {
1553
+ throw new DisTubeError("INVALID_TYPE", "object", options, "DisTubeOptions");
1721
1554
  }
1555
+ const opts = { ...defaultOptions, ...options };
1556
+ this.plugins = opts.plugins;
1557
+ this.emitNewSongOnly = opts.emitNewSongOnly;
1558
+ this.savePreviousSongs = opts.savePreviousSongs;
1559
+ this.customFilters = opts.customFilters;
1560
+ this.nsfw = opts.nsfw;
1561
+ this.emitAddSongWhenCreatingQueue = opts.emitAddSongWhenCreatingQueue;
1562
+ this.emitAddListWhenCreatingQueue = opts.emitAddListWhenCreatingQueue;
1563
+ this.joinNewVoiceChannel = opts.joinNewVoiceChannel;
1564
+ this.ffmpeg = this.#ffmpegOption(options);
1565
+ checkInvalidKey(opts, this, "DisTubeOptions");
1566
+ this.#validateOptions();
1722
1567
  }
1723
- /**
1724
- * Shuffle the queue's songs
1725
- * @returns The guild queue
1726
- */
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]];
1568
+ #validateOptions(options = this) {
1569
+ const booleanOptions = /* @__PURE__ */ new Set([
1570
+ "emitNewSongOnly",
1571
+ "savePreviousSongs",
1572
+ "joinNewVoiceChannel",
1573
+ "nsfw",
1574
+ "emitAddSongWhenCreatingQueue",
1575
+ "emitAddListWhenCreatingQueue"
1576
+ ]);
1577
+ const numberOptions = /* @__PURE__ */ new Set();
1578
+ const stringOptions = /* @__PURE__ */ new Set();
1579
+ const objectOptions = /* @__PURE__ */ new Set(["customFilters", "ffmpeg"]);
1580
+ const optionalOptions = /* @__PURE__ */ new Set(["customFilters"]);
1581
+ for (const [key, value] of Object.entries(options)) {
1582
+ if (value === void 0 && optionalOptions.has(key)) continue;
1583
+ if (key === "plugins" && !Array.isArray(value)) {
1584
+ throw new DisTubeError("INVALID_TYPE", "Array<Plugin>", value, `DisTubeOptions.${key}`);
1585
+ } else if (booleanOptions.has(key)) {
1586
+ if (typeof value !== "boolean") {
1587
+ throw new DisTubeError("INVALID_TYPE", "boolean", value, `DisTubeOptions.${key}`);
1588
+ }
1589
+ } else if (numberOptions.has(key)) {
1590
+ if (typeof value !== "number" || Number.isNaN(value)) {
1591
+ throw new DisTubeError("INVALID_TYPE", "number", value, `DisTubeOptions.${key}`);
1592
+ }
1593
+ } else if (stringOptions.has(key)) {
1594
+ if (typeof value !== "string") {
1595
+ throw new DisTubeError("INVALID_TYPE", "string", value, `DisTubeOptions.${key}`);
1596
+ }
1597
+ } else if (objectOptions.has(key)) {
1598
+ if (typeof value !== "object" || Array.isArray(value)) {
1599
+ throw new DisTubeError("INVALID_TYPE", "object", value, `DisTubeOptions.${key}`);
1600
+ }
1735
1601
  }
1736
- this.songs.unshift(playing);
1737
- return this;
1738
- } finally {
1739
- this._taskQueue.resolve();
1740
1602
  }
1741
1603
  }
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");
1604
+ #ffmpegOption(opts) {
1605
+ const args = { global: {}, input: {}, output: {} };
1606
+ if (opts.ffmpeg?.args) {
1607
+ if (opts.ffmpeg.args.global) args.global = opts.ffmpeg.args.global;
1608
+ if (opts.ffmpeg.args.input) args.input = opts.ffmpeg.args.input;
1609
+ if (opts.ffmpeg.args.output) args.output = opts.ffmpeg.args.output;
1610
+ }
1611
+ const path = opts.ffmpeg?.path ?? "ffmpeg";
1612
+ if (typeof path !== "string") {
1613
+ throw new DisTubeError("INVALID_TYPE", "string", path, "DisTubeOptions.ffmpeg.path");
1614
+ }
1615
+ for (const [key, value] of Object.entries(args)) {
1616
+ if (typeof value !== "object" || Array.isArray(value)) {
1617
+ throw new DisTubeError("INVALID_TYPE", "object", value, `DisTubeOptions.ffmpeg.${key}`);
1755
1618
  }
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 })));
1619
+ for (const [k, v] of Object.entries(value)) {
1620
+ if (typeof v !== "string" && typeof v !== "number" && typeof v !== "boolean" && !Array.isArray(v) && v !== null && v !== void 0) {
1621
+ throw new DisTubeError(
1622
+ "INVALID_TYPE",
1623
+ ["string", "number", "boolean", "Array<string | null | undefined>", "null", "undefined"],
1624
+ v,
1625
+ `DisTubeOptions.ffmpeg.${key}.${k}`
1626
+ );
1763
1627
  }
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];
1773
1628
  }
1774
- this.voice.stop();
1775
- return nextSong;
1776
- } finally {
1777
- this._taskQueue.resolve();
1778
1629
  }
1630
+ return { path, args };
1779
1631
  }
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;
1794
- }
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;
1806
- }
1807
- async #getRelatedSong(current) {
1808
- const plugin = await this.handler._getPluginFromSong(current);
1809
- if (plugin) return plugin.getRelatedSongs(current);
1810
- return [];
1632
+ };
1633
+
1634
+ // src/core/DisTubeStream.ts
1635
+ import { spawn, spawnSync } from "child_process";
1636
+ import { Transform } from "stream";
1637
+ import { createAudioResource, StreamType } from "@discordjs/voice";
1638
+ import { TypedEmitter as TypedEmitter2 } from "tiny-typed-emitter";
1639
+ var checked = process.env.NODE_ENV === "test";
1640
+ var checkFFmpeg = /* @__PURE__ */ __name((distube) => {
1641
+ if (checked) return;
1642
+ const path = distube.options.ffmpeg.path;
1643
+ const debug = /* @__PURE__ */ __name((str) => distube.emit("ffmpegDebug" /* FFMPEG_DEBUG */, str), "debug");
1644
+ try {
1645
+ debug(`[test] spawn ffmpeg at '${path}' path`);
1646
+ const process2 = spawnSync(path, ["-h"], {
1647
+ windowsHide: true,
1648
+ encoding: "utf-8"
1649
+ });
1650
+ if (process2.error) throw process2.error;
1651
+ if (process2.stderr && !process2.stdout) throw new Error(process2.stderr);
1652
+ const result = process2.output.join("\n");
1653
+ const version2 = /ffmpeg version (\S+)/iu.exec(result)?.[1];
1654
+ if (!version2) throw new Error("Invalid FFmpeg version");
1655
+ debug(`[test] ffmpeg version: ${version2}`);
1656
+ } catch (e) {
1657
+ debug(`[test] failed to spawn ffmpeg at '${path}': ${e?.stack ?? e}`);
1658
+ throw new DisTubeError("FFMPEG_NOT_INSTALLED", path);
1811
1659
  }
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;
1660
+ checked = true;
1661
+ }, "checkFFmpeg");
1662
+ var DisTubeStream = class extends TypedEmitter2 {
1663
+ static {
1664
+ __name(this, "DisTubeStream");
1833
1665
  }
1666
+ #ffmpegPath;
1667
+ #opts;
1668
+ process;
1669
+ stream;
1670
+ audioResource;
1834
1671
  /**
1835
- * Stop the guild stream and delete the queue
1672
+ * Create a DisTubeStream to play with {@link DisTubeVoice}
1673
+ * @param url - Stream URL
1674
+ * @param options - Stream options
1836
1675
  */
1837
- async stop() {
1838
- await this._taskQueue.queuing();
1839
- try {
1840
- this.voice.stop();
1841
- this.remove();
1842
- } finally {
1843
- this._taskQueue.resolve();
1676
+ constructor(url, options) {
1677
+ super();
1678
+ const { ffmpeg, seek } = options;
1679
+ const opts = {
1680
+ reconnect: 1,
1681
+ reconnect_streamed: 1,
1682
+ reconnect_delay_max: 5,
1683
+ analyzeduration: 0,
1684
+ hide_banner: true,
1685
+ ...ffmpeg.args.global,
1686
+ ...ffmpeg.args.input,
1687
+ i: url,
1688
+ ar: 48e3,
1689
+ ac: 2,
1690
+ ...ffmpeg.args.output,
1691
+ f: "s16le"
1692
+ };
1693
+ if (typeof seek === "number" && seek > 0) opts.ss = seek.toString();
1694
+ const fileUrl = new URL(url);
1695
+ if (fileUrl.protocol === "file:") {
1696
+ opts.reconnect = null;
1697
+ opts.reconnect_streamed = null;
1698
+ opts.reconnect_delay_max = null;
1699
+ opts.i = fileUrl.hostname + fileUrl.pathname;
1844
1700
  }
1701
+ this.#ffmpegPath = ffmpeg.path;
1702
+ this.#opts = [
1703
+ ...Object.entries(opts).flatMap(
1704
+ ([key, value]) => Array.isArray(value) ? value.filter(Boolean).map((v) => [`-${key}`, String(v)]) : value == null || value === false ? [] : [value === true ? `-${key}` : [`-${key}`, String(value)]]
1705
+ ).flat(),
1706
+ "pipe:1"
1707
+ ];
1708
+ this.stream = new VolumeTransformer();
1709
+ this.stream.on("close", () => this.kill()).on("error", (err) => {
1710
+ this.debug(`[stream] error: ${err.message}`);
1711
+ this.emit("error", err);
1712
+ }).on("finish", () => this.debug("[stream] log: stream finished"));
1713
+ this.audioResource = createAudioResource(this.stream, {
1714
+ inputType: StreamType.Raw,
1715
+ inlineVolume: false
1716
+ });
1845
1717
  }
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);
1718
+ spawn() {
1719
+ this.debug(`[process] spawn: ${this.#ffmpegPath} ${this.#opts.join(" ")}`);
1720
+ this.process = spawn(this.#ffmpegPath, this.#opts, {
1721
+ stdio: ["ignore", "pipe", "pipe"],
1722
+ shell: false,
1723
+ windowsHide: true
1724
+ }).on("error", (err) => {
1725
+ this.debug(`[process] error: ${err.message}`);
1726
+ this.emit("error", err);
1727
+ }).on("exit", (code, signal) => {
1728
+ this.debug(`[process] exit: code=${code ?? "unknown"} signal=${signal ?? "unknown"}`);
1729
+ if (!code || [0, 255].includes(code)) return;
1730
+ this.debug(`[process] error: ffmpeg exited with code ${code}`);
1731
+ this.emit("error", new DisTubeError("FFMPEG_EXITED", code));
1732
+ });
1733
+ if (!this.process.stdout || !this.process.stderr) {
1734
+ this.kill();
1735
+ throw new Error("Failed to create ffmpeg process");
1736
+ }
1737
+ this.process.stdout.pipe(this.stream);
1738
+ this.process.stderr.setEncoding("utf8")?.on("data", (data) => {
1739
+ const lines = data.split(/\r\n|\r|\n/u);
1740
+ for (const line of lines) {
1741
+ if (/^\s*$/.test(line)) continue;
1742
+ this.debug(`[ffmpeg] log: ${line}`);
1743
+ }
1744
+ });
1858
1745
  }
1859
- /**
1860
- * Toggle autoplay mode
1861
- * @returns Autoplay mode state
1862
- */
1863
- toggleAutoplay() {
1864
- this.autoplay = !this.autoplay;
1865
- return this.autoplay;
1746
+ debug(debug) {
1747
+ this.emit("debug", debug);
1866
1748
  }
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);
1749
+ setVolume(volume) {
1750
+ this.stream.vol = volume;
1751
+ }
1752
+ kill() {
1753
+ if (!this.stream.destroyed) this.stream.destroy();
1754
+ if (this.process && !this.process.killed) this.process.kill("SIGKILL");
1875
1755
  }
1876
1756
  };
1877
-
1878
- // src/struct/Plugin.ts
1879
- var Plugin = class {
1757
+ var VolumeTransformer = class extends Transform {
1880
1758
  static {
1881
- __name(this, "Plugin");
1759
+ __name(this, "VolumeTransformer");
1882
1760
  }
1883
- /**
1884
- * DisTube
1885
- */
1886
- distube;
1887
- init(distube) {
1888
- this.distube = distube;
1761
+ buffer = Buffer.allocUnsafe(0);
1762
+ extrema = [-(2 ** (16 - 1)), 2 ** (16 - 1) - 1];
1763
+ vol = 1;
1764
+ _transform(newChunk, _encoding, done) {
1765
+ const { vol } = this;
1766
+ if (vol === 1) {
1767
+ this.push(newChunk);
1768
+ done();
1769
+ return;
1770
+ }
1771
+ const bytes = 2;
1772
+ const chunk = Buffer.concat([this.buffer, newChunk]);
1773
+ const readableLength = Math.floor(chunk.length / bytes) * bytes;
1774
+ for (let i = 0; i < readableLength; i += bytes) {
1775
+ const value = chunk.readInt16LE(i);
1776
+ const clampedValue = Math.min(this.extrema[1], Math.max(this.extrema[0], value * vol));
1777
+ chunk.writeInt16LE(clampedValue, i);
1778
+ }
1779
+ this.buffer = chunk.subarray(readableLength);
1780
+ this.push(chunk.subarray(0, readableLength));
1781
+ done();
1889
1782
  }
1890
1783
  };
1891
1784
 
1892
- // src/struct/ExtractorPlugin.ts
1893
- var ExtractorPlugin = class extends Plugin {
1785
+ // src/core/manager/DisTubeVoiceManager.ts
1786
+ import { getVoiceConnection, VoiceConnectionStatus as VoiceConnectionStatus2 } from "@discordjs/voice";
1787
+
1788
+ // src/core/manager/GuildIdManager.ts
1789
+ var GuildIdManager = class extends BaseManager {
1894
1790
  static {
1895
- __name(this, "ExtractorPlugin");
1791
+ __name(this, "GuildIdManager");
1792
+ }
1793
+ add(idOrInstance, data) {
1794
+ const id = resolveGuildId(idOrInstance);
1795
+ const existing = this.get(id);
1796
+ if (existing) return this;
1797
+ this.collection.set(id, data);
1798
+ return this;
1799
+ }
1800
+ get(idOrInstance) {
1801
+ return this.collection.get(resolveGuildId(idOrInstance));
1802
+ }
1803
+ remove(idOrInstance) {
1804
+ return this.collection.delete(resolveGuildId(idOrInstance));
1805
+ }
1806
+ has(idOrInstance) {
1807
+ return this.collection.has(resolveGuildId(idOrInstance));
1896
1808
  }
1897
- type = "extractor" /* EXTRACTOR */;
1898
1809
  };
1899
1810
 
1900
- // src/struct/InfoExtratorPlugin.ts
1901
- var InfoExtractorPlugin = class extends Plugin {
1811
+ // src/core/manager/DisTubeVoiceManager.ts
1812
+ var DisTubeVoiceManager = class extends GuildIdManager {
1902
1813
  static {
1903
- __name(this, "InfoExtractorPlugin");
1814
+ __name(this, "DisTubeVoiceManager");
1815
+ }
1816
+ /**
1817
+ * Create a {@link DisTubeVoice} instance
1818
+ * @param channel - A voice channel to join
1819
+ */
1820
+ create(channel) {
1821
+ const existing = this.get(channel.guildId);
1822
+ if (existing) {
1823
+ existing.channel = channel;
1824
+ return existing;
1825
+ }
1826
+ if (getVoiceConnection(resolveGuildId(channel), this.client.user?.id) || getVoiceConnection(resolveGuildId(channel))) {
1827
+ throw new DisTubeError("VOICE_ALREADY_CREATED");
1828
+ }
1829
+ return new DisTubeVoice(this, channel);
1830
+ }
1831
+ /**
1832
+ * Join a voice channel and wait until the connection is ready
1833
+ * @param channel - A voice channel to join
1834
+ */
1835
+ join(channel) {
1836
+ const existing = this.get(channel.guildId);
1837
+ if (existing) return existing.join(channel);
1838
+ return this.create(channel).join();
1839
+ }
1840
+ /**
1841
+ * Leave the connected voice channel in a guild
1842
+ * @param guild - Queue Resolvable
1843
+ */
1844
+ leave(guild) {
1845
+ const voice = this.get(guild);
1846
+ if (voice) {
1847
+ voice.leave();
1848
+ } else {
1849
+ const connection = getVoiceConnection(resolveGuildId(guild), this.client.user?.id) ?? getVoiceConnection(resolveGuildId(guild));
1850
+ if (connection && connection.state.status !== VoiceConnectionStatus2.Destroyed) {
1851
+ connection.destroy();
1852
+ }
1853
+ }
1904
1854
  }
1905
- type = "info-extractor" /* INFO_EXTRACTOR */;
1906
1855
  };
1907
1856
 
1908
- // src/struct/PlayableExtratorPlugin.ts
1909
- var PlayableExtractorPlugin = class extends Plugin {
1857
+ // src/core/manager/QueueManager.ts
1858
+ var QueueManager = class extends GuildIdManager {
1910
1859
  static {
1911
- __name(this, "PlayableExtractorPlugin");
1860
+ __name(this, "QueueManager");
1912
1861
  }
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;
1862
+ /**
1863
+ * Create a {@link Queue}
1864
+ * @param channel - A voice channel
1865
+ * @param textChannel - Default text channel
1866
+ * @returns Returns `true` if encounter an error
1867
+ */
1868
+ async create(channel, textChannel) {
1869
+ if (this.has(channel.guildId)) throw new DisTubeError("QUEUE_EXIST");
1870
+ this.debug(`[QueueManager] Creating queue for guild: ${channel.guildId}`);
1871
+ const voice = this.voices.create(channel);
1872
+ const queue = new Queue(this.distube, voice, textChannel);
1873
+ await queue._taskQueue.queuing();
1874
+ try {
1875
+ checkFFmpeg(this.distube);
1876
+ this.debug(`[QueueManager] Joining voice channel: ${channel.id}`);
1877
+ await voice.join();
1878
+ this.#voiceEventHandler(queue);
1879
+ this.add(queue.id, queue);
1880
+ this.emit("initQueue" /* INIT_QUEUE */, queue);
1881
+ return queue;
1882
+ } finally {
1883
+ queue._taskQueue.resolve();
1884
+ }
1938
1885
  }
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;
1886
+ /**
1887
+ * Listen to DisTubeVoice events and handle the Queue
1888
+ * @param queue - Queue
1889
+ */
1890
+ #voiceEventHandler(queue) {
1891
+ queue._listeners = {
1892
+ disconnect: /* @__PURE__ */ __name((error) => {
1893
+ queue.remove();
1894
+ this.emit("disconnect" /* DISCONNECT */, queue);
1895
+ if (error) this.emitError(error, queue, queue.songs?.[0]);
1896
+ }, "disconnect"),
1897
+ error: /* @__PURE__ */ __name((error) => this.#handlePlayingError(queue, error), "error"),
1898
+ finish: /* @__PURE__ */ __name(() => this.handleSongFinish(queue), "finish")
1899
+ };
1900
+ for (const event of objectKeys(queue._listeners)) {
1901
+ queue.voice.on(event, queue._listeners[event]);
1902
+ }
1962
1903
  }
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;
1904
+ /**
1905
+ * Handle the queue when a Song finish
1906
+ * @param queue - queue
1907
+ */
1908
+ async handleSongFinish(queue) {
1909
+ if (queue._manualUpdate) {
1910
+ queue._manualUpdate = false;
1911
+ await this.playSong(queue);
1912
+ return;
1913
+ }
1914
+ this.debug(`[QueueManager] Handling song finish: ${queue.id}`);
1915
+ const song = queue.songs[0];
1916
+ this.emit("finishSong" /* FINISH_SONG */, queue, song);
1917
+ await queue._taskQueue.queuing();
1918
+ try {
1919
+ if (queue.stopped) return;
1920
+ if (queue.repeatMode === 2 /* QUEUE */) queue.songs.push(song);
1921
+ if (queue.repeatMode !== 1 /* SONG */) {
1922
+ const prev = queue.songs.shift();
1923
+ if (this.options.savePreviousSongs) queue.previousSongs.push(prev);
1924
+ else queue.previousSongs.push({ id: prev.id });
1925
+ }
1926
+ if (queue.songs.length === 0 && queue.autoplay) {
1927
+ try {
1928
+ this.debug(`[QueueManager] Adding related song: ${queue.id}`);
1929
+ await queue.addRelatedSong();
1930
+ } catch (e) {
1931
+ this.debug(`[${queue.id}] Add related song error: ${e.message}`);
1932
+ this.emit("noRelated" /* NO_RELATED */, queue, e);
1933
+ }
1934
+ }
1935
+ if (queue.songs.length === 0) {
1936
+ this.debug(`[${queue.id}] Queue is empty, stopping...`);
1937
+ if (!queue.autoplay) this.emit("finish" /* FINISH */, queue);
1938
+ queue.remove();
1939
+ return;
1940
+ }
1941
+ if (song !== queue.songs[0]) {
1942
+ const playedSong = song.stream.playFromSource ? song : song.stream.song;
1943
+ if (playedSong?.stream.playFromSource) delete playedSong.stream.url;
1944
+ }
1945
+ await this.playSong(queue, true);
1946
+ } finally {
1947
+ queue._taskQueue.resolve();
1996
1948
  }
1997
1949
  }
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
- ]) {
1950
+ /**
1951
+ * Handle error while playing
1952
+ * @param queue - queue
1953
+ * @param error - error
1954
+ */
1955
+ #handlePlayingError(queue, error) {
1956
+ const song = queue.songs.shift();
2039
1957
  try {
2040
- await import(lib);
2041
- return true;
1958
+ error.name = "PlayingError";
2042
1959
  } catch {
2043
1960
  }
1961
+ this.debug(`[${queue.id}] Error while playing: ${error.stack || error.message}`);
1962
+ this.emitError(error, queue, song);
1963
+ if (queue.songs.length > 0) {
1964
+ this.debug(`[${queue.id}] Playing next song: ${queue.songs[0]}`);
1965
+ this.playSong(queue);
1966
+ } else {
1967
+ this.debug(`[${queue.id}] Queue is empty, stopping...`);
1968
+ queue.stop();
1969
+ }
2044
1970
  }
2045
- return false;
2046
- }, "checkEncryptionLibraries");
1971
+ /**
1972
+ * Play a song on voice connection with queue properties
1973
+ * @param queue - The guild queue to play
1974
+ * @param emitPlaySong - Whether or not emit {@link Events.PLAY_SONG} event
1975
+ */
1976
+ async playSong(queue, emitPlaySong = true) {
1977
+ if (!queue) return;
1978
+ if (queue.stopped || !queue.songs.length) {
1979
+ queue.stop();
1980
+ return;
1981
+ }
1982
+ try {
1983
+ const song = queue.songs[0];
1984
+ this.debug(`[${queue.id}] Getting stream from: ${song}`);
1985
+ await this.handler.attachStreamInfo(song);
1986
+ const willPlaySong = song.stream.playFromSource ? song : song.stream.song;
1987
+ const stream = willPlaySong?.stream;
1988
+ if (!willPlaySong || !stream?.playFromSource || !stream.url) throw new DisTubeError("NO_STREAM_URL", `${song}`);
1989
+ this.debug(`[${queue.id}] Creating DisTubeStream for: ${willPlaySong}`);
1990
+ const streamOptions = {
1991
+ ffmpeg: {
1992
+ path: this.options.ffmpeg.path,
1993
+ args: {
1994
+ global: { ...queue.ffmpegArgs.global },
1995
+ input: { ...queue.ffmpegArgs.input },
1996
+ output: { ...queue.ffmpegArgs.output, ...queue.filters.ffmpegArgs }
1997
+ }
1998
+ },
1999
+ seek: willPlaySong.duration ? queue._beginTime : void 0
2000
+ };
2001
+ const dtStream = new DisTubeStream(stream.url, streamOptions);
2002
+ dtStream.on("debug", (data) => this.emit("ffmpegDebug" /* FFMPEG_DEBUG */, `[${queue.id}] ${data}`));
2003
+ this.debug(`[${queue.id}] Started playing: ${willPlaySong}`);
2004
+ await queue.voice.play(dtStream);
2005
+ if (emitPlaySong) this.emit("playSong" /* PLAY_SONG */, queue, song);
2006
+ } catch (e) {
2007
+ this.#handlePlayingError(queue, e);
2008
+ }
2009
+ }
2010
+ };
2047
2011
 
2048
2012
  // src/DisTube.ts
2049
2013
  import { TypedEmitter as TypedEmitter3 } from "tiny-typed-emitter";
@@ -2180,7 +2144,7 @@ var DisTube = class extends TypedEmitter3 {
2180
2144
  this.queues = new QueueManager(this);
2181
2145
  this.filters = { ...defaultFilters, ...this.options.customFilters };
2182
2146
  this.plugins = [...this.options.plugins];
2183
- this.plugins.forEach((p) => p.init(this));
2147
+ for (const p of this.plugins) p.init(this);
2184
2148
  }
2185
2149
  static get version() {
2186
2150
  return version;
@@ -2350,8 +2314,8 @@ ${e.message}`;
2350
2314
  * @param guild - The type can be resolved to give a {@link Queue}
2351
2315
  * @returns The new Song will be played
2352
2316
  */
2353
- skip(guild) {
2354
- return this.#getQueue(guild).skip();
2317
+ skip(guild, options) {
2318
+ return this.#getQueue(guild).skip(options);
2355
2319
  }
2356
2320
  /**
2357
2321
  * Play the previous song
@@ -2376,8 +2340,8 @@ ${e.message}`;
2376
2340
  * @param num - The song number to play
2377
2341
  * @returns The new Song will be played
2378
2342
  */
2379
- jump(guild, num) {
2380
- return this.#getQueue(guild).jump(num);
2343
+ jump(guild, num, options) {
2344
+ return this.#getQueue(guild).jump(num, options);
2381
2345
  }
2382
2346
  /**
2383
2347
  * Set the repeat mode of the guild queue.
@@ -2434,8 +2398,43 @@ ${e.message}`;
2434
2398
  }
2435
2399
  };
2436
2400
 
2437
- // src/index.ts
2438
- var version = "5.0.7";
2401
+ // src/struct/Plugin.ts
2402
+ var Plugin = class {
2403
+ static {
2404
+ __name(this, "Plugin");
2405
+ }
2406
+ /**
2407
+ * DisTube
2408
+ */
2409
+ distube;
2410
+ init(distube) {
2411
+ this.distube = distube;
2412
+ }
2413
+ };
2414
+
2415
+ // src/struct/ExtractorPlugin.ts
2416
+ var ExtractorPlugin = class extends Plugin {
2417
+ static {
2418
+ __name(this, "ExtractorPlugin");
2419
+ }
2420
+ type = "extractor" /* EXTRACTOR */;
2421
+ };
2422
+
2423
+ // src/struct/InfoExtratorPlugin.ts
2424
+ var InfoExtractorPlugin = class extends Plugin {
2425
+ static {
2426
+ __name(this, "InfoExtractorPlugin");
2427
+ }
2428
+ type = "info-extractor" /* INFO_EXTRACTOR */;
2429
+ };
2430
+
2431
+ // src/struct/PlayableExtratorPlugin.ts
2432
+ var PlayableExtractorPlugin = class extends Plugin {
2433
+ static {
2434
+ __name(this, "PlayableExtractorPlugin");
2435
+ }
2436
+ type = "playable-extractor" /* PLAYABLE_EXTRACTOR */;
2437
+ };
2439
2438
  export {
2440
2439
  BaseManager,
2441
2440
  DisTube,