@kattowo/distube 6.0.2 → 6.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.js CHANGED
@@ -34,6 +34,7 @@ __export(index_exports, {
34
34
  AUDIO_CHANNELS: () => AUDIO_CHANNELS,
35
35
  AUDIO_SAMPLE_RATE: () => AUDIO_SAMPLE_RATE,
36
36
  BaseManager: () => BaseManager,
37
+ DEFAULT_PRELOAD_THRESHOLD_SECS: () => DEFAULT_PRELOAD_THRESHOLD_SECS,
37
38
  DEFAULT_VOLUME: () => DEFAULT_VOLUME,
38
39
  DisTube: () => DisTube,
39
40
  DisTubeBase: () => DisTubeBase,
@@ -96,7 +97,7 @@ __export(index_exports, {
96
97
  module.exports = __toCommonJS(index_exports);
97
98
 
98
99
  // src/constant.ts
99
- var version = "6.0.2";
100
+ var version = "6.1.0";
100
101
  var AUDIO_SAMPLE_RATE = 48e3;
101
102
  var AUDIO_CHANNELS = 2;
102
103
  var DEFAULT_VOLUME = 50;
@@ -123,6 +124,7 @@ var defaultFilters = {
123
124
  surround: "surround",
124
125
  earwax: "earwax"
125
126
  };
127
+ var DEFAULT_PRELOAD_THRESHOLD_SECS = 15;
126
128
  var defaultOptions = {
127
129
  plugins: [],
128
130
  emitNewSongOnly: false,
@@ -132,7 +134,8 @@ var defaultOptions = {
132
134
  emitAddListWhenCreatingQueue: true,
133
135
  joinNewVoiceChannel: true,
134
136
  unhandledError: "log",
135
- stuckThreshold: STUCK_THRESHOLD_MS
137
+ stuckThreshold: STUCK_THRESHOLD_MS,
138
+ preload: false
136
139
  };
137
140
 
138
141
  // src/core/DisTubeBase.ts
@@ -702,7 +705,14 @@ var DisTubeVoice = class extends import_tiny_typed_emitter.TypedEmitter {
702
705
  if (newState.reason === import_voice.VoiceConnectionDisconnectReason.Manual) {
703
706
  this.leave();
704
707
  } else if (newState.reason === import_voice.VoiceConnectionDisconnectReason.WebSocketClose && newState.closeCode === 4014) {
705
- (0, import_voice.entersState)(this.connection, import_voice.VoiceConnectionStatus.Connecting, RECONNECT_TIMEOUT_MS).catch(() => {
708
+ (0, import_voice.entersState)(this.connection, import_voice.VoiceConnectionStatus.Connecting, RECONNECT_TIMEOUT_MS).then(() => (0, import_voice.entersState)(this.connection, import_voice.VoiceConnectionStatus.Ready, RECONNECT_TIMEOUT_MS)).then(() => {
709
+ const oldChannel = this.#channel;
710
+ const currentChannel = this.channel;
711
+ if (oldChannel && currentChannel && oldChannel.id !== currentChannel.id) {
712
+ this.#channel = currentChannel;
713
+ this.emit("channelChange", oldChannel, currentChannel);
714
+ }
715
+ }).catch(() => {
706
716
  if (![import_voice.VoiceConnectionStatus.Ready, import_voice.VoiceConnectionStatus.Connecting].includes(this.connection.state.status)) {
707
717
  this.leave();
708
718
  }
@@ -763,8 +773,12 @@ var DisTubeVoice = class extends import_tiny_typed_emitter.TypedEmitter {
763
773
  if (channel.full) throw new DisTubeError("VOICE_FULL");
764
774
  else throw new DisTubeError("VOICE_MISSING_PERMS");
765
775
  }
776
+ const oldChannel = this.#channel;
766
777
  this.connection = this.#join(channel);
767
778
  this.#channel = channel;
779
+ if (oldChannel && oldChannel.id !== channel.id) {
780
+ this.emit("channelChange", oldChannel, channel);
781
+ }
768
782
  }
769
783
  #join(channel) {
770
784
  return (0, import_voice.joinVoiceChannel)({
@@ -1052,7 +1066,13 @@ var FilterManager = class extends BaseManager {
1052
1066
  }
1053
1067
  throw new DisTubeError("INVALID_TYPE", "FilterResolvable", filter, "filter");
1054
1068
  }
1055
- #apply() {
1069
+ #apply(oldFilters) {
1070
+ this.queue._clearPreload?.();
1071
+ const newFilters = this.names;
1072
+ const changed = oldFilters.length !== newFilters.length || !oldFilters.every((f) => newFilters.includes(f));
1073
+ if (changed) {
1074
+ this.emit("changeFilter" /* CHANGE_FILTER */, this.queue, newFilters, oldFilters);
1075
+ }
1056
1076
  void this.queue._restartStream().catch((e) => {
1057
1077
  const errorMessage = e instanceof Error ? e.message : String(e);
1058
1078
  this.queue.debug(`[${this.queue.id}] Failed to apply the filters: ${errorMessage}`);
@@ -1070,6 +1090,7 @@ var FilterManager = class extends BaseManager {
1070
1090
  * ```
1071
1091
  */
1072
1092
  add(filterOrFilters, override = false) {
1093
+ const oldFilters = this.names;
1073
1094
  if (Array.isArray(filterOrFilters)) {
1074
1095
  for (const filter of filterOrFilters) {
1075
1096
  const ft = this.#resolve(filter);
@@ -1079,7 +1100,7 @@ var FilterManager = class extends BaseManager {
1079
1100
  const ft = this.#resolve(filterOrFilters);
1080
1101
  if (override || !this.has(ft)) this.collection.set(ft.name, ft);
1081
1102
  }
1082
- this.#apply();
1103
+ this.#apply(oldFilters);
1083
1104
  return this;
1084
1105
  }
1085
1106
  /**
@@ -1098,12 +1119,13 @@ var FilterManager = class extends BaseManager {
1098
1119
  */
1099
1120
  set(filters) {
1100
1121
  if (!Array.isArray(filters)) throw new DisTubeError("INVALID_TYPE", "Array<FilterResolvable>", filters, "filters");
1122
+ const oldFilters = this.names;
1101
1123
  this.collection.clear();
1102
1124
  for (const f of filters) {
1103
1125
  const filter = this.#resolve(f);
1104
1126
  this.collection.set(filter.name, filter);
1105
1127
  }
1106
- this.#apply();
1128
+ this.#apply(oldFilters);
1107
1129
  return this;
1108
1130
  }
1109
1131
  #removeFn(f) {
@@ -1116,9 +1138,10 @@ var FilterManager = class extends BaseManager {
1116
1138
  * @throws {@link DisTubeError} When a filter is not a valid {@link FilterResolvable}
1117
1139
  */
1118
1140
  remove(filterOrFilters) {
1141
+ const oldFilters = this.names;
1119
1142
  if (Array.isArray(filterOrFilters)) filterOrFilters.forEach((f) => this.#removeFn(f));
1120
1143
  else this.#removeFn(filterOrFilters);
1121
- this.#apply();
1144
+ this.#apply(oldFilters);
1122
1145
  return this;
1123
1146
  }
1124
1147
  /**
@@ -1174,6 +1197,17 @@ var Events = /* @__PURE__ */ ((Events2) => {
1174
1197
  Events2["DELETE_QUEUE"] = "deleteQueue";
1175
1198
  Events2["FFMPEG_DEBUG"] = "ffmpegDebug";
1176
1199
  Events2["DEBUG"] = "debug";
1200
+ Events2["PAUSE_SONG"] = "pauseSong";
1201
+ Events2["RESUME_SONG"] = "resumeSong";
1202
+ Events2["SEEK_SONG"] = "seekSong";
1203
+ Events2["SET_VOLUME"] = "setVolume";
1204
+ Events2["SHUFFLE_QUEUE"] = "shuffleQueue";
1205
+ Events2["SKIP_SONG"] = "skipSong";
1206
+ Events2["CHANGE_FILTER"] = "changeFilter";
1207
+ Events2["VOICE_CHANNEL_CHANGE"] = "voiceChannelChange";
1208
+ Events2["PRELOAD_SONG"] = "preloadSong";
1209
+ Events2["PRELOADED_SONG"] = "preloadedSong";
1210
+ Events2["PRELOAD_ERROR"] = "preloadError";
1177
1211
  return Events2;
1178
1212
  })(Events || {});
1179
1213
  var SearchResultType = /* @__PURE__ */ ((SearchResultType2) => {
@@ -1307,6 +1341,7 @@ var Queue = class extends DisTubeBase {
1307
1341
  */
1308
1342
  _beginTime;
1309
1343
  #filters;
1344
+ #initialized = false;
1310
1345
  /**
1311
1346
  * Whether or not the queue is being updated manually (skip, jump, previous)
1312
1347
  * @internal
@@ -1322,6 +1357,48 @@ var Queue = class extends DisTubeBase {
1322
1357
  * @internal
1323
1358
  */
1324
1359
  _listeners;
1360
+ /**
1361
+ * Preload timer
1362
+ * @internal
1363
+ */
1364
+ _preloadTimeout;
1365
+ /**
1366
+ * Preloaded song
1367
+ * @internal
1368
+ */
1369
+ _preloadedSong;
1370
+ /**
1371
+ * Preloaded stream
1372
+ * @internal
1373
+ */
1374
+ _preloadedStream;
1375
+ /**
1376
+ * In-flight preload task
1377
+ * @internal
1378
+ */
1379
+ _preloading;
1380
+ /**
1381
+ * Bumped whenever the preloaded state is invalidated, so an in-flight
1382
+ * preload task can detect that its result is no longer needed.
1383
+ * @internal
1384
+ */
1385
+ _preloadGeneration = 0;
1386
+ /**
1387
+ * Clear any preloaded song and cancel pending preload timer.
1388
+ * @internal
1389
+ */
1390
+ _clearPreload() {
1391
+ this._preloadGeneration++;
1392
+ if (this._preloadTimeout) {
1393
+ clearTimeout(this._preloadTimeout);
1394
+ this._preloadTimeout = void 0;
1395
+ }
1396
+ if (this._preloadedStream) {
1397
+ this._preloadedStream.kill();
1398
+ this._preloadedStream = void 0;
1399
+ }
1400
+ this._preloadedSong = void 0;
1401
+ }
1325
1402
  /**
1326
1403
  * Create a queue for the guild
1327
1404
  * @param distube - DisTube
@@ -1350,6 +1427,7 @@ var Queue = class extends DisTubeBase {
1350
1427
  input: { ...this.options.ffmpeg.args.input },
1351
1428
  output: { ...this.options.ffmpeg.args.output }
1352
1429
  };
1430
+ this.#initialized = true;
1353
1431
  }
1354
1432
  #addToPreviousSongs(songs) {
1355
1433
  if (Array.isArray(songs)) {
@@ -1432,7 +1510,11 @@ var Queue = class extends DisTubeBase {
1432
1510
  * negative
1433
1511
  */
1434
1512
  set volume(value) {
1513
+ const oldVolume = this.voice.volume;
1435
1514
  this.voice.volume = value;
1515
+ if (this.#initialized && oldVolume !== value) {
1516
+ this.emit("setVolume" /* SET_VOLUME */, this, value, oldVolume);
1517
+ }
1436
1518
  }
1437
1519
  /**
1438
1520
  * Add a song or a list of songs to the queue.
@@ -1461,6 +1543,7 @@ var Queue = class extends DisTubeBase {
1461
1543
  } else {
1462
1544
  this.songs.splice(position, 0, song);
1463
1545
  }
1546
+ this.queues?.schedulePreload?.(this);
1464
1547
  return this;
1465
1548
  }
1466
1549
  /**
@@ -1478,7 +1561,9 @@ var Queue = class extends DisTubeBase {
1478
1561
  try {
1479
1562
  if (this.paused) throw new DisTubeError("PAUSED");
1480
1563
  this.paused = true;
1564
+ this._clearPreload();
1481
1565
  this.voice.pause();
1566
+ this.emit("pauseSong" /* PAUSE_SONG */, this, this.songs[0]);
1482
1567
  return this;
1483
1568
  } finally {
1484
1569
  this._taskQueue.resolve();
@@ -1499,6 +1584,8 @@ var Queue = class extends DisTubeBase {
1499
1584
  if (!this.paused) throw new DisTubeError("RESUMED");
1500
1585
  this.paused = false;
1501
1586
  this.voice.unpause();
1587
+ this.emit("resumeSong" /* RESUME_SONG */, this, this.songs[0]);
1588
+ this.queues?.schedulePreload?.(this);
1502
1589
  return this;
1503
1590
  } finally {
1504
1591
  this._taskQueue.resolve();
@@ -1563,11 +1650,14 @@ var Queue = class extends DisTubeBase {
1563
1650
  try {
1564
1651
  const playing = this.songs.shift();
1565
1652
  if (playing === void 0) return this;
1653
+ this._clearPreload();
1566
1654
  for (let i = this.songs.length - 1; i > 0; i--) {
1567
1655
  const j = Math.floor(Math.random() * (i + 1));
1568
1656
  [this.songs[i], this.songs[j]] = [this.songs[j], this.songs[i]];
1569
1657
  }
1570
1658
  this.songs.unshift(playing);
1659
+ this.emit("shuffleQueue" /* SHUFFLE_QUEUE */, this, this.songs);
1660
+ this.queues?.schedulePreload?.(this);
1571
1661
  return this;
1572
1662
  } finally {
1573
1663
  this._taskQueue.resolve();
@@ -1605,12 +1695,14 @@ var Queue = class extends DisTubeBase {
1605
1695
  } else {
1606
1696
  this.#addToPreviousSongs(skipped);
1607
1697
  }
1698
+ this.emit("skipSong" /* SKIP_SONG */, this, skipped[0]);
1608
1699
  } else if (!this.options.savePreviousSongs) {
1609
1700
  throw new DisTubeError("DISABLED_OPTION", "savePreviousSongs");
1610
1701
  } else {
1611
1702
  const skipped = this.previousSongs.splice(position);
1612
1703
  this.songs.unshift(...skipped);
1613
1704
  }
1705
+ this._clearPreload();
1614
1706
  this.#stop();
1615
1707
  return this.songs[0];
1616
1708
  } finally {
@@ -1649,8 +1741,10 @@ var Queue = class extends DisTubeBase {
1649
1741
  try {
1650
1742
  if (typeof time !== "number") throw new DisTubeError("INVALID_TYPE", "number", time, "time");
1651
1743
  if (Number.isNaN(time) || time < 0) throw new DisTubeError("NUMBER_COMPARE", "time", "bigger or equal to", 0);
1744
+ this._clearPreload();
1652
1745
  this._beginTime = time;
1653
1746
  await this.play(false);
1747
+ this.emit("seekSong" /* SEEK_SONG */, this, this.songs[0], time);
1654
1748
  return this;
1655
1749
  } finally {
1656
1750
  this._taskQueue.resolve();
@@ -1721,6 +1815,7 @@ var Queue = class extends DisTubeBase {
1721
1815
  * @returns Nothing
1722
1816
  */
1723
1817
  remove() {
1818
+ this._clearPreload();
1724
1819
  this.paused = true;
1725
1820
  this.stopped = true;
1726
1821
  this.songs = [];
@@ -2394,6 +2489,11 @@ var Options = class {
2394
2489
  * @default 10000
2395
2490
  */
2396
2491
  stuckThreshold;
2492
+ /**
2493
+ * The preload options of the DisTube instance.
2494
+ * @default { enabled: false, threshold: 15 }
2495
+ */
2496
+ preload;
2397
2497
  /**
2398
2498
  * The FFmpeg options of the DisTube instance.
2399
2499
  */
@@ -2419,10 +2519,34 @@ var Options = class {
2419
2519
  this.joinNewVoiceChannel = opts.joinNewVoiceChannel;
2420
2520
  this.unhandledError = opts.unhandledError;
2421
2521
  this.stuckThreshold = opts.stuckThreshold;
2522
+ this.preload = this.#preloadOption(opts.preload);
2422
2523
  this.ffmpeg = this.#ffmpegOption(options);
2423
2524
  checkInvalidKey(opts, this, "DisTubeOptions");
2424
2525
  this.#validateOptions();
2425
2526
  }
2527
+ #preloadOption(value) {
2528
+ if (value === void 0 || value === false) {
2529
+ return { enabled: false, threshold: DEFAULT_PRELOAD_THRESHOLD_SECS };
2530
+ }
2531
+ if (value === true) {
2532
+ return { enabled: true, threshold: DEFAULT_PRELOAD_THRESHOLD_SECS };
2533
+ }
2534
+ if (typeof value !== "object" || Array.isArray(value)) {
2535
+ throw new DisTubeError("INVALID_TYPE", ["boolean", "object"], value, "DisTubeOptions.preload");
2536
+ }
2537
+ checkInvalidKey(value, ["enabled", "threshold"], "DisTubeOptions.preload");
2538
+ const { enabled, threshold } = value;
2539
+ if (enabled !== void 0 && typeof enabled !== "boolean") {
2540
+ throw new DisTubeError("INVALID_TYPE", "boolean", enabled, "DisTubeOptions.preload.enabled");
2541
+ }
2542
+ if (threshold !== void 0 && (typeof threshold !== "number" || Number.isNaN(threshold) || threshold < 0)) {
2543
+ throw new DisTubeError("INVALID_TYPE", "number", threshold, "DisTubeOptions.preload.threshold");
2544
+ }
2545
+ return {
2546
+ enabled: enabled ?? false,
2547
+ threshold: threshold ?? DEFAULT_PRELOAD_THRESHOLD_SECS
2548
+ };
2549
+ }
2426
2550
  #validateOptions(options = this) {
2427
2551
  const booleanOptions = /* @__PURE__ */ new Set([
2428
2552
  "emitNewSongOnly",
@@ -2434,7 +2558,7 @@ var Options = class {
2434
2558
  ]);
2435
2559
  const numberOptions = /* @__PURE__ */ new Set(["stuckThreshold"]);
2436
2560
  const stringOptions = /* @__PURE__ */ new Set(["unhandledError"]);
2437
- const objectOptions = /* @__PURE__ */ new Set(["customFilters", "ffmpeg"]);
2561
+ const objectOptions = /* @__PURE__ */ new Set(["customFilters", "ffmpeg", "preload"]);
2438
2562
  const optionalOptions = /* @__PURE__ */ new Set(["customFilters"]);
2439
2563
  for (const [key, value] of Object.entries(options)) {
2440
2564
  if (value === void 0 && optionalOptions.has(key)) continue;
@@ -2822,7 +2946,10 @@ var QueueManager = class extends GuildIdManager {
2822
2946
  if (error) this.emitError(error, queue, queue.songs?.[0]);
2823
2947
  }, "disconnect"),
2824
2948
  error: /* @__PURE__ */ __name((error) => void this.#handlePlayingError(queue, error), "error"),
2825
- finish: /* @__PURE__ */ __name(() => void this.handleSongFinish(queue), "finish")
2949
+ finish: /* @__PURE__ */ __name(() => void this.handleSongFinish(queue), "finish"),
2950
+ channelChange: /* @__PURE__ */ __name((oldChannel, newChannel) => {
2951
+ this.emit("voiceChannelChange" /* VOICE_CHANNEL_CHANGE */, queue, oldChannel, newChannel);
2952
+ }, "channelChange")
2826
2953
  };
2827
2954
  for (const event of objectKeys(queue._listeners)) {
2828
2955
  queue.voice.on(event, queue._listeners[event]);
@@ -2920,12 +3047,109 @@ var QueueManager = class extends GuildIdManager {
2920
3047
  }
2921
3048
  try {
2922
3049
  const song = queue.songs[0];
2923
- this.debug(`[${queue.id}] Getting stream from: ${song}`);
2924
- await this.handler.attachStreamInfo(song);
2925
- const willPlaySong = song.stream.playFromSource ? song : song.stream.song;
3050
+ let dtStream;
3051
+ if (queue._preloadedSong === song && queue._preloadedStream) {
3052
+ this.debug(`[${queue.id}] Using preloaded stream for: ${song}`);
3053
+ dtStream = queue._preloadedStream;
3054
+ queue._preloadedSong = void 0;
3055
+ queue._preloadedStream = void 0;
3056
+ } else {
3057
+ queue._clearPreload();
3058
+ this.debug(`[${queue.id}] Getting stream from: ${song}`);
3059
+ await this.handler.attachStreamInfo(song);
3060
+ const willPlaySong = song.stream.playFromSource ? song : song.stream.song;
3061
+ const stream = willPlaySong?.stream;
3062
+ if (!willPlaySong || !stream?.playFromSource || !stream.url) throw new DisTubeError("NO_STREAM_URL", `${song}`);
3063
+ this.debug(`[${queue.id}] Creating DisTubeStream for: ${willPlaySong}`);
3064
+ const streamOptions = {
3065
+ ffmpeg: {
3066
+ path: this.options.ffmpeg.path,
3067
+ args: {
3068
+ global: { ...queue.ffmpegArgs.global },
3069
+ input: { ...queue.ffmpegArgs.input },
3070
+ output: { ...queue.ffmpegArgs.output, ...queue.filters.ffmpegArgs }
3071
+ }
3072
+ },
3073
+ seek: willPlaySong.duration ? queue._beginTime : void 0
3074
+ };
3075
+ queue._beginTime = 0;
3076
+ dtStream = new DisTubeStream(stream.url, streamOptions);
3077
+ dtStream.on("debug", (data) => this.emit("ffmpegDebug" /* FFMPEG_DEBUG */, `[${queue.id}] ${data}`));
3078
+ }
3079
+ queue._beginTime = 0;
3080
+ this.debug(`[${queue.id}] Started playing: ${song}`);
3081
+ await queue.voice.play(dtStream);
3082
+ if (emitPlaySong) this.emit("playSong" /* PLAY_SONG */, queue, song);
3083
+ this.schedulePreload(queue);
3084
+ } catch (e) {
3085
+ const error = e instanceof Error ? e : new Error(String(e));
3086
+ if (options?.keepSongOnError) {
3087
+ this.debug(`[${queue.id}] Error while restarting the stream: ${error.stack || error.message}`);
3088
+ this.emitError(error, queue, queue.songs[0]);
3089
+ return;
3090
+ }
3091
+ await this.#handlePlayingError(queue, error);
3092
+ }
3093
+ }
3094
+ /**
3095
+ * Schedule preloading the next song in the queue before the current song finishes.
3096
+ * @param queue - The guild queue
3097
+ */
3098
+ schedulePreload(queue) {
3099
+ if (!this.options.preload.enabled || queue.stopped || queue.paused) return;
3100
+ if (queue._preloadTimeout) {
3101
+ clearTimeout(queue._preloadTimeout);
3102
+ queue._preloadTimeout = void 0;
3103
+ }
3104
+ const currentSong = queue.songs[0];
3105
+ const nextSong = queue.songs[1];
3106
+ if (!currentSong || !nextSong) return;
3107
+ if (queue._preloadedSong === nextSong && queue._preloadedStream) return;
3108
+ const threshold = this.options.preload.threshold;
3109
+ const duration = currentSong.duration ?? 0;
3110
+ const currentTime = queue.currentTime;
3111
+ const remaining = duration > 0 && Number.isFinite(currentTime) ? duration - currentTime : 0;
3112
+ if (duration === 0 || remaining <= threshold) {
3113
+ void this.preloadNextSong(queue);
3114
+ } else {
3115
+ const delayMs = Math.max(0, (remaining - threshold) * 1e3);
3116
+ queue._preloadTimeout = setTimeout(() => {
3117
+ queue._preloadTimeout = void 0;
3118
+ void this.preloadNextSong(queue);
3119
+ }, delayMs);
3120
+ queue._preloadTimeout.unref?.();
3121
+ }
3122
+ }
3123
+ /**
3124
+ * Preload the next song in the queue.
3125
+ * @param queue - The guild queue
3126
+ */
3127
+ async preloadNextSong(queue) {
3128
+ if (queue._preloading) return queue._preloading;
3129
+ const task = this.#preloadTask(queue);
3130
+ queue._preloading = task;
3131
+ try {
3132
+ await task;
3133
+ } finally {
3134
+ if (queue._preloading === task) queue._preloading = void 0;
3135
+ }
3136
+ }
3137
+ async #preloadTask(queue) {
3138
+ if (!this.options.preload.enabled || queue.stopped || queue.paused) return;
3139
+ const nextSong = queue.songs[1];
3140
+ if (!nextSong) return;
3141
+ if (queue._preloadedSong === nextSong && queue._preloadedStream) return;
3142
+ const generation = queue._preloadGeneration;
3143
+ this.debug(`[${queue.id}] Preloading next song: ${nextSong}`);
3144
+ this.emit("preloadSong" /* PRELOAD_SONG */, queue, nextSong);
3145
+ try {
3146
+ await this.handler.attachStreamInfo(nextSong);
3147
+ if (queue.stopped || queue.songs[1] !== nextSong || generation !== queue._preloadGeneration) return;
3148
+ const willPlaySong = nextSong.stream.playFromSource ? nextSong : nextSong.stream.song;
2926
3149
  const stream = willPlaySong?.stream;
2927
- if (!willPlaySong || !stream?.playFromSource || !stream.url) throw new DisTubeError("NO_STREAM_URL", `${song}`);
2928
- this.debug(`[${queue.id}] Creating DisTubeStream for: ${willPlaySong}`);
3150
+ if (!willPlaySong || !stream?.playFromSource || !stream.url) {
3151
+ throw new DisTubeError("NO_STREAM_URL", `${nextSong}`);
3152
+ }
2929
3153
  const streamOptions = {
2930
3154
  ffmpeg: {
2931
3155
  path: this.options.ffmpeg.path,
@@ -2935,22 +3159,27 @@ var QueueManager = class extends GuildIdManager {
2935
3159
  output: { ...queue.ffmpegArgs.output, ...queue.filters.ffmpegArgs }
2936
3160
  }
2937
3161
  },
2938
- seek: willPlaySong.duration ? queue._beginTime : void 0
3162
+ seek: void 0
2939
3163
  };
2940
- queue._beginTime = 0;
2941
3164
  const dtStream = new DisTubeStream(stream.url, streamOptions);
2942
- dtStream.on("debug", (data) => this.emit("ffmpegDebug" /* FFMPEG_DEBUG */, `[${queue.id}] ${data}`));
2943
- this.debug(`[${queue.id}] Started playing: ${willPlaySong}`);
2944
- await queue.voice.play(dtStream);
2945
- if (emitPlaySong) this.emit("playSong" /* PLAY_SONG */, queue, song);
2946
- } catch (e) {
2947
- const error = e instanceof Error ? e : new Error(String(e));
2948
- if (options?.keepSongOnError) {
2949
- this.debug(`[${queue.id}] Error while restarting the stream: ${error.stack || error.message}`);
2950
- this.emitError(error, queue, queue.songs[0]);
3165
+ dtStream.on("debug", (data) => this.emit("ffmpegDebug" /* FFMPEG_DEBUG */, `[${queue.id}] [preload] ${data}`));
3166
+ if (queue.stopped || queue.songs[1] !== nextSong || generation !== queue._preloadGeneration) {
3167
+ dtStream.kill();
2951
3168
  return;
2952
3169
  }
2953
- await this.#handlePlayingError(queue, error);
3170
+ queue._preloadedSong = nextSong;
3171
+ queue._preloadedStream = dtStream;
3172
+ this.debug(`[${queue.id}] Successfully preloaded next song: ${nextSong}`);
3173
+ this.emit("preloadedSong" /* PRELOADED_SONG */, queue, nextSong);
3174
+ } catch (e) {
3175
+ if (queue._preloadedSong === nextSong) {
3176
+ queue._preloadedSong = void 0;
3177
+ queue._preloadedStream?.kill();
3178
+ queue._preloadedStream = void 0;
3179
+ }
3180
+ const error = e instanceof Error ? e : new Error(String(e));
3181
+ this.debug(`[${queue.id}] Preload failed for ${nextSong}: ${error.message}`);
3182
+ this.emit("preloadError" /* PRELOAD_ERROR */, queue, nextSong, error);
2954
3183
  }
2955
3184
  }
2956
3185
  };
@@ -3967,6 +4196,7 @@ var PlayableExtractorPlugin = class extends ExtractorPlugin {
3967
4196
  AUDIO_CHANNELS,
3968
4197
  AUDIO_SAMPLE_RATE,
3969
4198
  BaseManager,
4199
+ DEFAULT_PRELOAD_THRESHOLD_SECS,
3970
4200
  DEFAULT_VOLUME,
3971
4201
  DisTube,
3972
4202
  DisTubeBase,