ziplayer 0.3.1 → 0.3.2

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.
@@ -9,6 +9,7 @@ const plugins_1 = require("../plugins");
9
9
  const extensions_1 = require("../extensions");
10
10
  const FilterManager_1 = require("./FilterManager");
11
11
  const StreamManager_1 = require("./StreamManager");
12
+ const PreloadManager_1 = require("./PreloadManager");
12
13
  /**
13
14
  * Represents a music player for a specific Discord guild.
14
15
  *
@@ -59,16 +60,6 @@ class Player extends events_1.EventEmitter {
59
60
  this.stuckTimer = null;
60
61
  this.skipLoop = false;
61
62
  this.refreshLock = false;
62
- //preloaded resource
63
- this.preloadState = {
64
- resource: null,
65
- track: null,
66
- abortController: null,
67
- timeoutId: null,
68
- isValid: false,
69
- isBeingUsed: false,
70
- };
71
- this.isPreloading = false;
72
63
  this.currentSlot = {
73
64
  resource: null,
74
65
  track: null,
@@ -78,16 +69,6 @@ class Player extends events_1.EventEmitter {
78
69
  isLoading: false,
79
70
  loadPromise: null,
80
71
  };
81
- this.preloadSlot = {
82
- resource: null,
83
- track: null,
84
- streamId: null,
85
- abortController: null,
86
- isValid: false,
87
- isLoading: false,
88
- loadPromise: null,
89
- };
90
- this.preloadLock = false;
91
72
  this.preloadEnabled = true;
92
73
  this.crossfadeEnabled = true;
93
74
  this.crossfadeDurationMs = 500;
@@ -122,6 +103,7 @@ class Player extends events_1.EventEmitter {
122
103
  this.loudnessMaxBoostDb = 8;
123
104
  this.loudnessMaxCutDb = 10;
124
105
  this.loudnessLimiterCeiling = 0.95;
106
+ this.destroyed = false;
125
107
  this.SEARCH_CACHE_TTL = 2 * 60 * 1000; // 2 minutes
126
108
  this.ttsPlayer = null;
127
109
  this.lastDuration = 0;
@@ -163,7 +145,7 @@ class Player extends events_1.EventEmitter {
163
145
  const crossfadeOptions = this.options.crossfade || {};
164
146
  const crossfadeAutoEnable = crossfadeOptions.autoEnable ?? true;
165
147
  const crossfadeAutoDisable = crossfadeOptions.autoDisableInLowPerformance ?? true;
166
- this.crossfadeDurationMs = Math.max(0, crossfadeOptions.durationMs ?? 5000);
148
+ this.crossfadeDurationMs = Math.max(0, crossfadeOptions.durationMs ?? 500);
167
149
  if (typeof crossfadeOptions.enabled === "boolean") {
168
150
  this.crossfadeEnabled = crossfadeOptions.enabled;
169
151
  }
@@ -205,12 +187,20 @@ class Player extends events_1.EventEmitter {
205
187
  extractorTimeout: this.options.extractorTimeout,
206
188
  });
207
189
  this.streamManager = new StreamManager_1.StreamManager({
208
- maxConcurrentStreams: 20,
190
+ maxConcurrentStreams: 2,
209
191
  streamTimeout: 5 * 60 * 1000,
210
192
  maxListenersPerStream: 15,
211
193
  enableMetrics: true,
212
194
  autoDestroy: true,
213
195
  });
196
+ this.preloadManager = new PreloadManager_1.PreloadManager({
197
+ streamManager: this.streamManager,
198
+ debug: this.debug.bind(this),
199
+ getNextTrack: () => this.queue.nextTrack,
200
+ getStream: (track) => this.getStream(track),
201
+ isDestroyed: () => this.destroyed,
202
+ isEnabled: () => this.preloadEnabled,
203
+ });
214
204
  this.volume = this.options.volume || 100;
215
205
  this.userdata = this.options.userdata;
216
206
  this.searchCache = new lru_cache_1.LRUCache({
@@ -495,6 +485,9 @@ class Player extends events_1.EventEmitter {
495
485
  this.queue.add(tracksToAdd[0]);
496
486
  this.emit("queueAdd", tracksToAdd[0]);
497
487
  }
488
+ if (this.isPlaying && !this.destroyed) {
489
+ void this.preloadNextTrack().catch((err) => this.debug("[Player] Preload after queue add error:", err));
490
+ }
498
491
  const started = !this.isPlaying ? await this.playNext() : true;
499
492
  await this.extensionManager.afterPlayHooks({
500
493
  success: started,
@@ -525,273 +518,20 @@ class Player extends events_1.EventEmitter {
525
518
  * Main preload method - only one at a time
526
519
  */
527
520
  async preloadNextTrack() {
528
- if (!this.preloadEnabled) {
529
- this.debug(`[Preload] Disabled by options/runtime profile`);
530
- return;
531
- }
532
- // Prevent concurrent preloads
533
- if (this.preloadLock) {
534
- this.debug(`[Preload] Already preloading, skipping`);
535
- return;
536
- }
537
- const nextTrack = this.queue.nextTrack;
538
- if (!nextTrack) {
539
- this.debug(`[Preload] No next track to preload`);
540
- return;
541
- }
542
- // Check if already preloaded correctly
543
- if (this.preloadSlot.isValid && this.preloadSlot.track?.id === nextTrack.id && this.preloadSlot.resource) {
544
- this.debug(`[Preload] Already have valid preload for: ${nextTrack.title}`);
545
- return;
546
- }
547
- // Check if currently loading the same track
548
- if (this.preloadSlot.isLoading && this.preloadSlot.track?.id === nextTrack.id) {
549
- this.debug(`[Preload] Currently loading same track, waiting...`);
550
- if (this.preloadSlot.loadPromise) {
551
- await this.preloadSlot.loadPromise;
552
- }
553
- return;
554
- }
555
- // Cancel old preload if different track
556
- if (this.preloadSlot.isValid && this.preloadSlot.track?.id !== nextTrack.id) {
557
- this.debug(`[Preload] Cancelling old preload for different track: ${this.preloadSlot.track?.title}`);
558
- await this.safeCancelPreload();
559
- }
560
- this.preloadLock = true;
561
- // Create new abort controller
562
- const abortController = new AbortController();
563
- // Setup preload slot
564
- this.preloadSlot.track = nextTrack;
565
- this.preloadSlot.abortController = abortController;
566
- this.preloadSlot.isLoading = true;
567
- // Create load promise
568
- const loadPromise = this.executePreload(nextTrack, abortController);
569
- this.preloadSlot.loadPromise = loadPromise;
570
- try {
571
- await loadPromise;
572
- }
573
- catch (err) {
574
- if (err instanceof Error && err.message === "PRELOAD_CANCELLED") {
575
- this.debug(`[Preload] Cancelled for ${nextTrack.title}`);
576
- }
577
- else {
578
- this.debug(`[Preload] Failed for ${nextTrack.title}:`, err);
579
- }
580
- this.clearSlot(this.preloadSlot);
581
- }
582
- finally {
583
- this.preloadLock = false;
584
- this.preloadSlot.isLoading = false;
585
- this.preloadSlot.loadPromise = null;
586
- }
587
- }
588
- /**
589
- * Execute actual preload
590
- */
591
- async executePreload(track, abortController) {
592
- this.debug(`[Preload] Starting preload for: ${track.title}`);
593
- // Check for cancellation
594
- if (abortController.signal.aborted) {
595
- throw new Error("PRELOAD_CANCELLED");
596
- }
597
- // Check if track still relevant
598
- if (this.queue.nextTrack?.id !== track.id) {
599
- this.debug(`[Preload] Track changed, cancelling`);
600
- throw new Error("PRELOAD_CANCELLED");
601
- }
602
- try {
603
- // Get stream with abort support - NO TIMEOUT
604
- const streamInfo = await this.getStreamWithCancel(track, abortController.signal);
605
- // Check cancellation
606
- if (abortController.signal.aborted) {
607
- throw new Error("PRELOAD_CANCELLED");
608
- }
609
- // Check track relevance again
610
- if (this.queue.nextTrack?.id !== track.id) {
611
- this.debug(`[Preload] Track changed after stream fetch`);
612
- throw new Error("PRELOAD_CANCELLED");
613
- }
614
- if (!streamInfo?.stream) {
615
- throw new Error(`No stream available`);
616
- }
617
- // Register with StreamManager as preload
618
- const streamId = this.streamManager.registerStream(streamInfo.stream, track, {
619
- source: track.source || "preload",
620
- isPreload: true,
621
- priority: 5,
622
- });
623
- // Create resource
624
- const resource = (0, voice_1.createAudioResource)(streamInfo.stream, {
625
- inlineVolume: true,
626
- metadata: { ...track, preloaded: true },
627
- });
628
- // Verify resource is valid
629
- if (!resource.playStream || resource.playStream.readable === false) {
630
- throw new Error("Resource not readable");
631
- }
632
- // Update preload slot
633
- this.preloadSlot.resource = resource;
634
- this.preloadSlot.streamId = streamId;
635
- this.preloadSlot.isValid = true;
636
- this.preloadSlot.track = track;
637
- this.debug(`[Preload] Successfully preloaded: ${track.title} (Stream ID: ${streamId})`);
638
- }
639
- catch (err) {
640
- if (err instanceof Error && err.message === "PRELOAD_CANCELLED") {
641
- throw err;
642
- }
643
- this.debug(`[Preload] Error during preload:`, err);
644
- throw err;
645
- }
521
+ await this.preloadManager.preloadNextTrack();
646
522
  }
647
523
  /**
648
524
  * Safe cancel preload - doesn't throw
649
525
  */
650
526
  async safeCancelPreload() {
651
- if (!this.preloadSlot.abortController && !this.preloadSlot.resource) {
652
- return;
653
- }
654
- this.debug(`[Preload] Safely cancelling preload for: ${this.preloadSlot.track?.title || "unknown"}`);
655
- // Abort the operation
656
- if (this.preloadSlot.abortController) {
657
- this.preloadSlot.abortController.abort();
658
- this.preloadSlot.abortController = null;
659
- }
660
- // Clean up stream
661
- if (this.preloadSlot.streamId && this.streamManager) {
662
- this.streamManager.unregisterStream(this.preloadSlot.streamId, true);
663
- }
664
- // Clean up resource
665
- if (this.preloadSlot.resource) {
666
- try {
667
- const stream = this.preloadSlot.resource.playStream;
668
- if (stream && typeof stream.destroy === "function" && !stream.destroyed) {
669
- stream.destroy();
670
- }
671
- }
672
- catch (err) {
673
- // Ignore destroy errors
674
- }
675
- }
676
- // Clear slot
677
- this.clearSlot(this.preloadSlot);
678
- }
679
- /**
680
- * Get stream with proper cancellation
681
- */
682
- async getStreamWithCancel(track, signal) {
683
- // Create abort promise
684
- const abortPromise = new Promise((_, reject) => {
685
- if (signal.aborted) {
686
- reject(new Error("PRELOAD_CANCELLED"));
687
- return;
688
- }
689
- const handler = () => {
690
- signal.removeEventListener("abort", handler);
691
- reject(new Error("PRELOAD_CANCELLED"));
692
- };
693
- signal.addEventListener("abort", handler);
694
- });
695
- try {
696
- // Check if stream already exists and is valid
697
- const existingStream = this.streamManager.getStreamByTrack(track.id || track.title);
698
- if (existingStream && !existingStream.destroyed && existingStream.readable !== false) {
699
- this.debug(`[Stream] Using existing stream for preload: ${track.title}`);
700
- return { stream: existingStream, type: "arbitrary" };
701
- }
702
- // Race between stream fetch and abort
703
- const streamPromise = this.getStream(track);
704
- const result = await Promise.race([streamPromise, abortPromise]);
705
- return result;
706
- }
707
- catch (err) {
708
- if (err instanceof Error && err.message === "PRELOAD_CANCELLED") {
709
- throw err;
710
- }
711
- throw err;
712
- }
527
+ await this.preloadManager.safeCancelPreload();
713
528
  }
529
+ // Preload stream fetch/cancel flow has been moved to PreloadManager.
714
530
  /**
715
531
  * Preload next track with proper error handling and cleanup
716
532
  */
717
533
  async preloadNext() {
718
- if (!this.preloadEnabled) {
719
- this.debug(`[Preload] Disabled by options/runtime profile`);
720
- return;
721
- }
722
- this.cancelPreload();
723
- const next = this.queue.nextTrack;
724
- if (!next || this.isPreloading) {
725
- this.debug(`[Preload] Skipped - ${!next ? "no next track" : "already preloading"}`);
726
- return;
727
- }
728
- this.isPreloading = true;
729
- // Create new AbortController
730
- const abortController = new AbortController();
731
- const timeoutId = setTimeout(() => {
732
- // this.debug(`[Preload] Timeout for track: ${next.title}`);
733
- // abortController.abort();
734
- }, 30000);
735
- this.preloadState.abortController = abortController;
736
- this.preloadState.timeoutId = timeoutId;
737
- try {
738
- this.debug(`[Preload] Starting preload for: ${next.title}`);
739
- // Check if already aborted
740
- if (abortController.signal.aborted) {
741
- throw new Error("Preload aborted before start");
742
- }
743
- // Check if this track is still the next one
744
- if (this.queue.nextTrack?.id !== next.id) {
745
- this.debug(`[Preload] Track changed, cancelling preload`);
746
- return;
747
- }
748
- const streamInfo = await this.getStreamWithCancel(next, abortController.signal);
749
- // Double check
750
- if (abortController.signal.aborted) {
751
- throw new Error("Preload aborted after stream fetch");
752
- }
753
- if (this.queue.nextTrack?.id !== next.id) {
754
- this.debug(`[Preload] Track changed after stream fetch`);
755
- return;
756
- }
757
- if (!streamInfo?.stream) {
758
- throw new Error(`No stream available`);
759
- }
760
- // Register with StreamManager
761
- const streamId = this.streamManager.registerStream(streamInfo.stream, next, {
762
- source: next.source || "preload",
763
- isPreload: true,
764
- priority: 8,
765
- });
766
- // Create resource
767
- const resource = (0, voice_1.createAudioResource)(streamInfo.stream, {
768
- inlineVolume: true,
769
- metadata: { ...next, preloaded: true },
770
- });
771
- // Store preload state
772
- this.preloadState = {
773
- resource,
774
- track: next,
775
- abortController,
776
- timeoutId,
777
- isValid: true,
778
- isBeingUsed: false,
779
- streamId,
780
- };
781
- this.debug(`[Preload] Successfully preloaded: ${next.title} (Stream ID: ${streamId})`);
782
- }
783
- catch (err) {
784
- if (err instanceof Error && err.message.includes("aborted")) {
785
- this.debug(`[Preload] Cancelled for ${next.title}`);
786
- }
787
- else {
788
- this.debug(`[Preload] Failed for ${next?.title}:`, err);
789
- }
790
- this.cancelPreload();
791
- }
792
- finally {
793
- this.isPreloading = false;
794
- }
534
+ await this.preloadManager.preloadNextTrack();
795
535
  }
796
536
  async fadeResourceVolume(resource, from, to, durationMs) {
797
537
  if (!resource.volume)
@@ -916,7 +656,7 @@ class Player extends events_1.EventEmitter {
916
656
  await new Promise((resolve) => setTimeout(resolve, this.antiStuckRetryDelayMs));
917
657
  }
918
658
  try {
919
- if (this.antiStuckReusePreloadFirst && this.preloadSlot.isValid && this.preloadSlot.track?.id === track.id) {
659
+ if (this.antiStuckReusePreloadFirst && this.preloadManager.hasValidPreload(track)) {
920
660
  const startedFromPreload = await this.startTrack(track);
921
661
  if (startedFromPreload) {
922
662
  this.antiStuckConsecutiveFailures = 0;
@@ -945,59 +685,11 @@ class Player extends events_1.EventEmitter {
945
685
  this.debug(`[AntiStuck] Keeping track for controlled retry window: ${track.title}`);
946
686
  return false;
947
687
  }
948
- /**
949
- * Clear preloaded resource with proper cleanup
950
- */
951
- clearPreload() {
952
- // Abort ongoing preload
953
- if (this.preloadState.abortController) {
954
- this.preloadState.abortController.abort();
955
- this.preloadState.abortController = null;
956
- }
957
- // Clean up stream
958
- const stream = this.preloadState.stream;
959
- if (stream && typeof stream.destroy === "function") {
960
- try {
961
- stream.destroy();
962
- }
963
- catch (err) {
964
- this.debug(`[Preload] Error destroying stream:`, err);
965
- }
966
- }
967
- // Clean up resource
968
- if (this.preloadState.resource) {
969
- try {
970
- const playStream = this.preloadState.resource.playStream;
971
- if (playStream && typeof playStream.destroy === "function") {
972
- playStream.destroy();
973
- }
974
- }
975
- catch (err) {
976
- this.debug(`[Preload] Error destroying resource:`, err);
977
- }
978
- }
979
- this.preloadState = {
980
- resource: null,
981
- track: null,
982
- abortController: null,
983
- timeoutId: null,
984
- isValid: false,
985
- isBeingUsed: false,
986
- streamId: undefined,
987
- };
988
- }
989
688
  /**
990
689
  * Cancel preload (when skipping or stopping)
991
690
  */
992
691
  cancelPreload() {
993
- if (this.preloadSlot.abortController) {
994
- this.debug(`[Preload] Cancelling preload for: ${this.preloadSlot.track?.title}`);
995
- this.preloadSlot.abortController.abort();
996
- }
997
- if (this.preloadSlot.streamId && this.streamManager) {
998
- this.streamManager.unregisterStream(this.preloadSlot.streamId, true);
999
- }
1000
- this.clearSlot(this.preloadSlot);
692
+ this.preloadManager.cancelPreload();
1001
693
  }
1002
694
  /**
1003
695
  * Clear a stream slot
@@ -1030,25 +722,8 @@ class Player extends events_1.EventEmitter {
1030
722
  * Promote preload slot to current slot without destroying promoted stream.
1031
723
  */
1032
724
  promotePreloadToCurrent(track) {
1033
- const promotedResource = this.preloadSlot.resource;
1034
- const promotedStreamId = this.preloadSlot.streamId;
1035
- // Move ownership to current slot.
1036
- this.currentSlot.resource = promotedResource;
1037
- this.currentSlot.track = track;
1038
- this.currentSlot.streamId = promotedStreamId;
1039
- this.currentSlot.abortController = null;
1040
- this.currentSlot.isValid = !!promotedResource;
1041
- this.currentSlot.isLoading = false;
1042
- this.currentSlot.loadPromise = null;
1043
- this.currentResource = promotedResource;
1044
- // Reset preload slot only (do not destroy promoted resource/stream).
1045
- this.preloadSlot.resource = null;
1046
- this.preloadSlot.track = null;
1047
- this.preloadSlot.streamId = null;
1048
- this.preloadSlot.abortController = null;
1049
- this.preloadSlot.isValid = false;
1050
- this.preloadSlot.isLoading = false;
1051
- this.preloadSlot.loadPromise = null;
725
+ const promoted = this.preloadManager.promoteToCurrent(track, this.currentSlot);
726
+ this.currentResource = promoted;
1052
727
  }
1053
728
  /**
1054
729
  * Create AudioResource with filters and seek applied
@@ -1098,6 +773,9 @@ class Player extends events_1.EventEmitter {
1098
773
  }
1099
774
  }
1100
775
  async getStream(track) {
776
+ if (this.destroyed) {
777
+ throw new Error("PLAYER_DESTROYED");
778
+ }
1101
779
  const trackId = track.id || track.url || track.title;
1102
780
  const existingStream = this.streamManager.getStreamByTrack(trackId);
1103
781
  if (existingStream && !existingStream.destroyed) {
@@ -1105,17 +783,23 @@ class Player extends events_1.EventEmitter {
1105
783
  return { stream: existingStream, type: "arbitrary" };
1106
784
  }
1107
785
  let stream = await this.extensionManager.provideStream(track);
786
+ if (this.destroyed) {
787
+ if (stream?.stream && typeof stream.stream.destroy === "function" && !stream.stream.destroyed) {
788
+ stream.stream.destroy();
789
+ }
790
+ throw new Error("PLAYER_DESTROYED");
791
+ }
1108
792
  if (stream?.stream) {
1109
- // Register with StreamManager
1110
- const streamId = this.streamManager.registerStream(stream.stream, track, {
1111
- source: "extension",
1112
- isPreload: false,
1113
- priority: 10,
1114
- });
1115
- this.debug(`[Stream] Extension stream registered with ID: ${streamId}`);
793
+ this.debug(`[Stream] Extension provided stream for: ${track.title}`);
1116
794
  return stream;
1117
795
  }
1118
796
  stream = await this.pluginManager.getStream(track);
797
+ if (this.destroyed) {
798
+ if (stream?.stream && typeof stream.stream.destroy === "function" && !stream.stream.destroyed) {
799
+ stream.stream.destroy();
800
+ }
801
+ throw new Error("PLAYER_DESTROYED");
802
+ }
1119
803
  if (stream?.stream) {
1120
804
  const existingAgain = this.streamManager.getStreamByTrack(trackId);
1121
805
  if (existingAgain && !existingAgain.destroyed) {
@@ -1124,12 +808,7 @@ class Player extends events_1.EventEmitter {
1124
808
  return { stream: existingAgain, type: "arbitrary" };
1125
809
  }
1126
810
  // Register with StreamManager
1127
- const streamId = this.streamManager.registerStream(stream.stream, track, {
1128
- source: track.source || "plugin",
1129
- isPreload: false,
1130
- priority: 5,
1131
- });
1132
- this.debug(`[Stream] Plugin stream registered with ID: ${streamId}`);
811
+ this.debug(`[Stream] Plugin provided stream for: ${track.title}`);
1133
812
  return stream;
1134
813
  }
1135
814
  if (!this.pluginManager.hasStreamCandidate(track)) {
@@ -1146,12 +825,11 @@ class Player extends events_1.EventEmitter {
1146
825
  * Start playing a specific track immediately, replacing the current resource.
1147
826
  */
1148
827
  async startTrack(track) {
828
+ if (this.destroyed)
829
+ return false;
1149
830
  try {
1150
831
  // Try to use preloaded resource
1151
- if (this.preloadSlot.isValid &&
1152
- this.preloadSlot.track?.id === track.id &&
1153
- this.preloadSlot.resource &&
1154
- this.preloadSlot.resource.playStream?.readable !== false) {
832
+ if (this.preloadManager.hasValidPreload(track)) {
1155
833
  this.debug(`[Player] Using preloaded stream for: ${track.title}`);
1156
834
  // Stop current playback
1157
835
  this.audioPlayer.stop(true);
@@ -1200,12 +878,10 @@ class Player extends events_1.EventEmitter {
1200
878
  * Swap preload slot to current slot
1201
879
  */
1202
880
  async swapToCurrent(track) {
1203
- // Store preload resource
1204
- const newResource = this.preloadSlot.resource;
1205
- const oldStreamId = this.currentSlot.streamId;
1206
- if (!newResource) {
881
+ if (!this.preloadManager.hasValidPreload(track)) {
1207
882
  return false;
1208
883
  }
884
+ const oldStreamId = this.currentSlot.streamId;
1209
885
  // Stop current playback
1210
886
  this.audioPlayer.stop(true);
1211
887
  // Clean up old current stream (but keep it for a moment)
@@ -1249,6 +925,8 @@ class Player extends events_1.EventEmitter {
1249
925
  * Load fresh stream when no preload available
1250
926
  */
1251
927
  async loadFreshStream(track) {
928
+ if (this.destroyed)
929
+ return false;
1252
930
  // Cancel preload to free resources
1253
931
  await this.safeCancelPreload();
1254
932
  try {
@@ -1286,9 +964,11 @@ class Player extends events_1.EventEmitter {
1286
964
  await (0, voice_1.entersState)(this.audioPlayer, voice_1.AudioPlayerStatus.Playing, 10_000);
1287
965
  await this.applyCrossfadeIn(resource, track);
1288
966
  // Preload next (async)
1289
- this.preloadNextTrack().catch((err) => {
1290
- this.debug(`[Player] Preload error:`, err);
1291
- });
967
+ if (!this.destroyed) {
968
+ this.preloadNextTrack().catch((err) => {
969
+ this.debug(`[Player] Preload error:`, err);
970
+ });
971
+ }
1292
972
  return true;
1293
973
  }
1294
974
  catch (error) {
@@ -1300,6 +980,8 @@ class Player extends events_1.EventEmitter {
1300
980
  * Play the next track in the queue, handling errors and edge cases gracefully
1301
981
  */
1302
982
  async playNext() {
983
+ if (this.destroyed)
984
+ return false;
1303
985
  this.debug("[Player] playNext called");
1304
986
  // Don't cancel preload here unless absolutely necessary
1305
987
  // Let startTrack handle it
@@ -1311,6 +993,14 @@ class Player extends events_1.EventEmitter {
1311
993
  const willnext = this.queue.willNextTrack();
1312
994
  if (willnext) {
1313
995
  this.queue.addMultiple([willnext]);
996
+ void this.preloadNextTrack().catch((err) => this.debug("[Player] Preload autoplay error:", err));
997
+ continue;
998
+ }
999
+ await this.generateWillNext().catch((err) => this.debug("[Player] generateWillNext autoplay fallback error:", err));
1000
+ const generatedNext = this.queue.willNextTrack();
1001
+ if (generatedNext) {
1002
+ this.queue.add(generatedNext);
1003
+ void this.preloadNextTrack().catch((err) => this.debug("[Player] Preload autoplay generated error:", err));
1314
1004
  continue;
1315
1005
  }
1316
1006
  }
@@ -2061,6 +1751,9 @@ class Player extends events_1.EventEmitter {
2061
1751
  */
2062
1752
  destroy() {
2063
1753
  this.debug(`[Player] destroy called`);
1754
+ if (this.destroyed)
1755
+ return;
1756
+ this.destroyed = true;
2064
1757
  if (this.leaveTimeout) {
2065
1758
  clearTimeout(this.leaveTimeout);
2066
1759
  this.leaveTimeout = null;
@@ -2069,10 +1762,10 @@ class Player extends events_1.EventEmitter {
2069
1762
  // Destroy current stream before stopping audio
2070
1763
  this.destroyCurrentStream();
2071
1764
  this.clearSlot(this.currentSlot);
2072
- this.clearSlot(this.preloadSlot);
1765
+ this.preloadManager.clearPreloadSlot();
2073
1766
  this.audioPlayer.removeAllListeners();
2074
1767
  this.audioPlayer.stop(true);
2075
- this.clearPreload();
1768
+ this.preloadManager.cancelPreload();
2076
1769
  if (this.ttsPlayer) {
2077
1770
  try {
2078
1771
  this.ttsPlayer.stop(true);
@@ -2230,6 +1923,8 @@ class Player extends events_1.EventEmitter {
2230
1923
  }
2231
1924
  setupEventListeners() {
2232
1925
  this.audioPlayer.on("stateChange", (oldState, newState) => {
1926
+ if (this.destroyed)
1927
+ return;
2233
1928
  this.debug(`[Player] AudioPlayer stateChange from ${oldState.status} to ${newState.status}`);
2234
1929
  if (newState.status === voice_1.AudioPlayerStatus.Idle && oldState.status !== voice_1.AudioPlayerStatus.Idle) {
2235
1930
  // Track ended
@@ -2238,7 +1933,7 @@ class Player extends events_1.EventEmitter {
2238
1933
  this.debug(`[Player] Track ended: ${track.title}`);
2239
1934
  this.emit("trackEnd", track);
2240
1935
  }
2241
- this.playNext();
1936
+ void this.playNext();
2242
1937
  }
2243
1938
  else if (newState.status === voice_1.AudioPlayerStatus.Playing &&
2244
1939
  (oldState.status === voice_1.AudioPlayerStatus.Idle || oldState.status === voice_1.AudioPlayerStatus.Buffering)) {
@@ -2300,18 +1995,20 @@ class Player extends events_1.EventEmitter {
2300
1995
  }
2301
1996
  });
2302
1997
  this.audioPlayer.on("error", (error) => {
1998
+ if (this.destroyed)
1999
+ return;
2303
2000
  this.debug(`[Player] AudioPlayer error:`, error);
2304
2001
  this.emit("playerError", error, this.queue.currentTrack || undefined);
2305
2002
  const track = this.queue.currentTrack;
2306
2003
  if (track && this.antiStuckEnabled) {
2307
2004
  void this.attemptTrackRecovery(track, error).then((recovered) => {
2308
2005
  if (!recovered) {
2309
- this.playNext();
2006
+ void this.playNext();
2310
2007
  }
2311
2008
  });
2312
2009
  return;
2313
2010
  }
2314
- this.playNext();
2011
+ void this.playNext();
2315
2012
  });
2316
2013
  this.audioPlayer.on("debug", (...args) => {
2317
2014
  if (this.manager.debugEnabled) {