ziplayer 0.3.0 → 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,26 +808,28 @@ 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
  }
814
+ if (!this.pluginManager.hasStreamCandidate(track)) {
815
+ throw new Error(`UNRECOVERABLE_NO_PLUGIN:${track.title}`);
816
+ }
1135
817
  throw new Error(`No stream available for track: ${track.title}`);
1136
818
  }
819
+ isUnrecoverableStreamError(error) {
820
+ if (!(error instanceof Error))
821
+ return false;
822
+ return error.message.startsWith("UNRECOVERABLE_NO_PLUGIN:");
823
+ }
1137
824
  /**
1138
825
  * Start playing a specific track immediately, replacing the current resource.
1139
826
  */
1140
827
  async startTrack(track) {
828
+ if (this.destroyed)
829
+ return false;
1141
830
  try {
1142
831
  // Try to use preloaded resource
1143
- if (this.preloadSlot.isValid &&
1144
- this.preloadSlot.track?.id === track.id &&
1145
- this.preloadSlot.resource &&
1146
- this.preloadSlot.resource.playStream?.readable !== false) {
832
+ if (this.preloadManager.hasValidPreload(track)) {
1147
833
  this.debug(`[Player] Using preloaded stream for: ${track.title}`);
1148
834
  // Stop current playback
1149
835
  this.audioPlayer.stop(true);
@@ -1192,12 +878,10 @@ class Player extends events_1.EventEmitter {
1192
878
  * Swap preload slot to current slot
1193
879
  */
1194
880
  async swapToCurrent(track) {
1195
- // Store preload resource
1196
- const newResource = this.preloadSlot.resource;
1197
- const oldStreamId = this.currentSlot.streamId;
1198
- if (!newResource) {
881
+ if (!this.preloadManager.hasValidPreload(track)) {
1199
882
  return false;
1200
883
  }
884
+ const oldStreamId = this.currentSlot.streamId;
1201
885
  // Stop current playback
1202
886
  this.audioPlayer.stop(true);
1203
887
  // Clean up old current stream (but keep it for a moment)
@@ -1241,6 +925,8 @@ class Player extends events_1.EventEmitter {
1241
925
  * Load fresh stream when no preload available
1242
926
  */
1243
927
  async loadFreshStream(track) {
928
+ if (this.destroyed)
929
+ return false;
1244
930
  // Cancel preload to free resources
1245
931
  await this.safeCancelPreload();
1246
932
  try {
@@ -1278,9 +964,11 @@ class Player extends events_1.EventEmitter {
1278
964
  await (0, voice_1.entersState)(this.audioPlayer, voice_1.AudioPlayerStatus.Playing, 10_000);
1279
965
  await this.applyCrossfadeIn(resource, track);
1280
966
  // Preload next (async)
1281
- this.preloadNextTrack().catch((err) => {
1282
- this.debug(`[Player] Preload error:`, err);
1283
- });
967
+ if (!this.destroyed) {
968
+ this.preloadNextTrack().catch((err) => {
969
+ this.debug(`[Player] Preload error:`, err);
970
+ });
971
+ }
1284
972
  return true;
1285
973
  }
1286
974
  catch (error) {
@@ -1292,6 +980,8 @@ class Player extends events_1.EventEmitter {
1292
980
  * Play the next track in the queue, handling errors and edge cases gracefully
1293
981
  */
1294
982
  async playNext() {
983
+ if (this.destroyed)
984
+ return false;
1295
985
  this.debug("[Player] playNext called");
1296
986
  // Don't cancel preload here unless absolutely necessary
1297
987
  // Let startTrack handle it
@@ -1303,6 +993,14 @@ class Player extends events_1.EventEmitter {
1303
993
  const willnext = this.queue.willNextTrack();
1304
994
  if (willnext) {
1305
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));
1306
1004
  continue;
1307
1005
  }
1308
1006
  }
@@ -1340,6 +1038,10 @@ class Player extends events_1.EventEmitter {
1340
1038
  catch (err) {
1341
1039
  this.debug(`[Player] playNext error:`, err);
1342
1040
  this.emit("playerError", err, track);
1041
+ if (this.isUnrecoverableStreamError(err)) {
1042
+ this.debug(`[Player] Skipping unrecoverable track (no plugin): ${track.title}`);
1043
+ continue;
1044
+ }
1343
1045
  const recovered = await this.attemptTrackRecovery(track, err);
1344
1046
  if (recovered) {
1345
1047
  return true;
@@ -2049,6 +1751,9 @@ class Player extends events_1.EventEmitter {
2049
1751
  */
2050
1752
  destroy() {
2051
1753
  this.debug(`[Player] destroy called`);
1754
+ if (this.destroyed)
1755
+ return;
1756
+ this.destroyed = true;
2052
1757
  if (this.leaveTimeout) {
2053
1758
  clearTimeout(this.leaveTimeout);
2054
1759
  this.leaveTimeout = null;
@@ -2057,10 +1762,10 @@ class Player extends events_1.EventEmitter {
2057
1762
  // Destroy current stream before stopping audio
2058
1763
  this.destroyCurrentStream();
2059
1764
  this.clearSlot(this.currentSlot);
2060
- this.clearSlot(this.preloadSlot);
1765
+ this.preloadManager.clearPreloadSlot();
2061
1766
  this.audioPlayer.removeAllListeners();
2062
1767
  this.audioPlayer.stop(true);
2063
- this.clearPreload();
1768
+ this.preloadManager.cancelPreload();
2064
1769
  if (this.ttsPlayer) {
2065
1770
  try {
2066
1771
  this.ttsPlayer.stop(true);
@@ -2218,6 +1923,8 @@ class Player extends events_1.EventEmitter {
2218
1923
  }
2219
1924
  setupEventListeners() {
2220
1925
  this.audioPlayer.on("stateChange", (oldState, newState) => {
1926
+ if (this.destroyed)
1927
+ return;
2221
1928
  this.debug(`[Player] AudioPlayer stateChange from ${oldState.status} to ${newState.status}`);
2222
1929
  if (newState.status === voice_1.AudioPlayerStatus.Idle && oldState.status !== voice_1.AudioPlayerStatus.Idle) {
2223
1930
  // Track ended
@@ -2226,7 +1933,7 @@ class Player extends events_1.EventEmitter {
2226
1933
  this.debug(`[Player] Track ended: ${track.title}`);
2227
1934
  this.emit("trackEnd", track);
2228
1935
  }
2229
- this.playNext();
1936
+ void this.playNext();
2230
1937
  }
2231
1938
  else if (newState.status === voice_1.AudioPlayerStatus.Playing &&
2232
1939
  (oldState.status === voice_1.AudioPlayerStatus.Idle || oldState.status === voice_1.AudioPlayerStatus.Buffering)) {
@@ -2288,18 +1995,20 @@ class Player extends events_1.EventEmitter {
2288
1995
  }
2289
1996
  });
2290
1997
  this.audioPlayer.on("error", (error) => {
1998
+ if (this.destroyed)
1999
+ return;
2291
2000
  this.debug(`[Player] AudioPlayer error:`, error);
2292
2001
  this.emit("playerError", error, this.queue.currentTrack || undefined);
2293
2002
  const track = this.queue.currentTrack;
2294
2003
  if (track && this.antiStuckEnabled) {
2295
2004
  void this.attemptTrackRecovery(track, error).then((recovered) => {
2296
2005
  if (!recovered) {
2297
- this.playNext();
2006
+ void this.playNext();
2298
2007
  }
2299
2008
  });
2300
2009
  return;
2301
2010
  }
2302
- this.playNext();
2011
+ void this.playNext();
2303
2012
  });
2304
2013
  this.audioPlayer.on("debug", (...args) => {
2305
2014
  if (this.manager.debugEnabled) {