ziplayer 0.2.6 → 0.2.7-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/AI-Guide.md +607 -0
  2. package/README.md +513 -196
  3. package/dist/extensions/BaseExtension.d.ts +1 -0
  4. package/dist/extensions/BaseExtension.d.ts.map +1 -1
  5. package/dist/extensions/BaseExtension.js.map +1 -1
  6. package/dist/extensions/index.d.ts +38 -3
  7. package/dist/extensions/index.d.ts.map +1 -1
  8. package/dist/extensions/index.js +259 -41
  9. package/dist/extensions/index.js.map +1 -1
  10. package/dist/persistence/PersistenceManager.d.ts +61 -0
  11. package/dist/persistence/PersistenceManager.d.ts.map +1 -0
  12. package/dist/persistence/PersistenceManager.js +551 -0
  13. package/dist/persistence/PersistenceManager.js.map +1 -0
  14. package/dist/plugins/BasePlugin.js +1 -1
  15. package/dist/plugins/BasePlugin.js.map +1 -1
  16. package/dist/plugins/index.d.ts +19 -4
  17. package/dist/plugins/index.d.ts.map +1 -1
  18. package/dist/plugins/index.js +273 -146
  19. package/dist/plugins/index.js.map +1 -1
  20. package/dist/structures/FilterManager.js +3 -3
  21. package/dist/structures/FilterManager.js.map +1 -1
  22. package/dist/structures/Player.d.ts +64 -14
  23. package/dist/structures/Player.d.ts.map +1 -1
  24. package/dist/structures/Player.js +344 -91
  25. package/dist/structures/Player.js.map +1 -1
  26. package/dist/structures/PlayerManager.d.ts +125 -91
  27. package/dist/structures/PlayerManager.d.ts.map +1 -1
  28. package/dist/structures/PlayerManager.js +406 -111
  29. package/dist/structures/PlayerManager.js.map +1 -1
  30. package/dist/structures/Queue.d.ts +136 -31
  31. package/dist/structures/Queue.d.ts.map +1 -1
  32. package/dist/structures/Queue.js +265 -46
  33. package/dist/structures/Queue.js.map +1 -1
  34. package/dist/types/index.d.ts +39 -6
  35. package/dist/types/index.d.ts.map +1 -1
  36. package/dist/types/index.js +1 -0
  37. package/dist/types/index.js.map +1 -1
  38. package/dist/types/persistence.d.ts +55 -0
  39. package/dist/types/persistence.d.ts.map +1 -0
  40. package/dist/types/persistence.js +3 -0
  41. package/dist/types/persistence.js.map +1 -0
  42. package/package.json +47 -46
  43. package/src/extensions/BaseExtension.ts +36 -35
  44. package/src/extensions/index.ts +473 -190
  45. package/src/index.ts +16 -16
  46. package/src/persistence/PersistenceManager.ts +572 -0
  47. package/src/plugins/BasePlugin.ts +27 -27
  48. package/src/plugins/index.ts +403 -236
  49. package/src/structures/FilterManager.ts +303 -303
  50. package/src/structures/Player.ts +1962 -1689
  51. package/src/structures/PlayerManager.ts +788 -416
  52. package/src/structures/Queue.ts +599 -354
  53. package/src/types/index.ts +406 -373
  54. package/src/types/persistence.ts +65 -0
  55. package/src/types/plugin.ts +1 -1
  56. package/src/utils/timeout.ts +10 -10
  57. package/tsconfig.json +22 -23
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Player = void 0;
4
4
  const events_1 = require("events");
5
5
  const voice_1 = require("@discordjs/voice");
6
+ const lru_cache_1 = require("lru-cache");
6
7
  const Queue_1 = require("./Queue");
7
8
  const plugins_1 = require("../plugins");
8
9
  const extensions_1 = require("../extensions");
@@ -54,12 +55,17 @@ class Player extends events_1.EventEmitter {
54
55
  this.leaveTimeout = null;
55
56
  this.currentResource = null;
56
57
  this.volumeInterval = null;
58
+ this.stuckTimer = null;
57
59
  this.skipLoop = false;
58
- // Cache for search results to avoid duplicate calls
59
- this.searchCache = new Map();
60
+ this.refreshLock = false;
61
+ //preloaded resource
62
+ this.preloadedResource = null;
63
+ this.preloadedTrack = null;
60
64
  this.SEARCH_CACHE_TTL = 2 * 60 * 1000; // 2 minutes
61
- this.searchCacheTimestamps = new Map();
62
65
  this.ttsPlayer = null;
66
+ this.lastDuration = 0;
67
+ this.lastSaveTime = 0;
68
+ this.AUTO_SAVE_INTERVAL = 30000; // 30 seconds
63
69
  this.debug(`[Player] Constructor called for guildId: ${guildId}`);
64
70
  this.guildId = guildId;
65
71
  this.queue = new Queue_1.Queue();
@@ -84,7 +90,7 @@ class Player extends events_1.EventEmitter {
84
90
  createPlayer: false,
85
91
  interrupt: true,
86
92
  volume: 100,
87
- Max_Time_TTS: 60_000,
93
+ maxTimeTts: 60_000,
88
94
  ...(options?.tts || {}),
89
95
  },
90
96
  };
@@ -95,6 +101,10 @@ class Player extends events_1.EventEmitter {
95
101
  });
96
102
  this.volume = this.options.volume || 100;
97
103
  this.userdata = this.options.userdata;
104
+ this.searchCache = new lru_cache_1.LRUCache({
105
+ max: 200,
106
+ ttl: this.SEARCH_CACHE_TTL,
107
+ });
98
108
  this.setupEventListeners();
99
109
  // Initialize filters from options
100
110
  if (this.options.filters && this.options.filters.length > 0) {
@@ -114,11 +124,12 @@ class Player extends events_1.EventEmitter {
114
124
  * @private
115
125
  */
116
126
  destroyCurrentStream() {
127
+ this.audioPlayer.stop(true);
117
128
  if (!this.currentResource)
118
129
  return;
119
130
  const stream = this.currentResource?.metadata?.stream ?? this.currentResource?.stream;
120
- if (stream?.destroy) {
121
- stream.destroy();
131
+ if (stream && typeof stream.destroy === "function") {
132
+ stream.destroy().catch((e) => this.debug("Stream destroy error:", e));
122
133
  }
123
134
  this.currentResource = null;
124
135
  }
@@ -135,11 +146,6 @@ class Player extends events_1.EventEmitter {
135
146
  */
136
147
  async search(query, requestedBy) {
137
148
  this.debug(`[Player] Search called with query: ${query}, requestedBy: ${requestedBy}`);
138
- // Clear expired search cache periodically
139
- if (Math.random() < 0.1) {
140
- // 10% chance to clean cache
141
- this.clearExpiredSearchCache();
142
- }
143
149
  // Check cache first
144
150
  const cachedResult = this.getCachedSearchResult(query);
145
151
  if (cachedResult) {
@@ -198,14 +204,10 @@ class Player extends events_1.EventEmitter {
198
204
  */
199
205
  getCachedSearchResult(query) {
200
206
  const cacheKey = query.toLowerCase().trim();
201
- const now = Date.now();
202
- const cachedTimestamp = this.searchCacheTimestamps.get(cacheKey);
203
- if (cachedTimestamp && now - cachedTimestamp < this.SEARCH_CACHE_TTL) {
204
- const cachedResult = this.searchCache.get(cacheKey);
205
- if (cachedResult) {
206
- this.debug(`[SearchCache] Using cached search result for: ${query}`);
207
- return cachedResult;
208
- }
207
+ const cached = this.searchCache.get(cacheKey);
208
+ if (cached) {
209
+ this.debug(`[SearchCache] Using cached search result for: ${query}`);
210
+ return cached;
209
211
  }
210
212
  return null;
211
213
  }
@@ -216,23 +218,15 @@ class Player extends events_1.EventEmitter {
216
218
  */
217
219
  cacheSearchResult(query, result) {
218
220
  const cacheKey = query.toLowerCase().trim();
219
- const now = Date.now();
220
221
  this.searchCache.set(cacheKey, result);
221
- this.searchCacheTimestamps.set(cacheKey, now);
222
222
  this.debug(`[SearchCache] Cached search result for: ${query} (${result.tracks.length} tracks)`);
223
223
  }
224
224
  /**
225
225
  * Clear expired search cache entries
226
226
  */
227
227
  clearExpiredSearchCache() {
228
- const now = Date.now();
229
- for (const [key, timestamp] of this.searchCacheTimestamps.entries()) {
230
- if (now - timestamp >= this.SEARCH_CACHE_TTL) {
231
- this.searchCache.delete(key);
232
- this.searchCacheTimestamps.delete(key);
233
- this.debug(`[SearchCache] Cleared expired cache entry: ${key}`);
234
- }
235
- }
228
+ this.searchCache.purgeStale();
229
+ this.debug(`[SearchCache] Purged stale search cache entries`);
236
230
  }
237
231
  /**
238
232
  * Clear all search cache entries
@@ -242,7 +236,6 @@ class Player extends events_1.EventEmitter {
242
236
  clearSearchCache() {
243
237
  const cacheSize = this.searchCache.size;
244
238
  this.searchCache.clear();
245
- this.searchCacheTimestamps.clear();
246
239
  this.debug(`[SearchCache] Cleared all ${cacheSize} search cache entries`);
247
240
  }
248
241
  /**
@@ -252,9 +245,8 @@ class Player extends events_1.EventEmitter {
252
245
  */
253
246
  debugSearchQuery(query) {
254
247
  const cacheKey = query.toLowerCase().trim();
255
- const now = Date.now();
256
- const cachedTimestamp = this.searchCacheTimestamps.get(cacheKey);
257
- const isCached = cachedTimestamp && now - cachedTimestamp < this.SEARCH_CACHE_TTL;
248
+ const cached = this.searchCache.get(cacheKey);
249
+ const isCached = !!cached;
258
250
  const allPlugins = this.pluginManager.getAll();
259
251
  const plugins = allPlugins.filter((p) => {
260
252
  if (p.name.toLowerCase() === "tts" && !query.toLowerCase().startsWith("tts:")) {
@@ -263,8 +255,8 @@ class Player extends events_1.EventEmitter {
263
255
  return true;
264
256
  });
265
257
  return {
266
- isCached: !!isCached,
267
- cacheAge: cachedTimestamp ? now - cachedTimestamp : undefined,
258
+ isCached,
259
+ cacheAge: undefined,
268
260
  pluginCount: plugins.length,
269
261
  ttsFiltered: allPlugins.length > plugins.length,
270
262
  };
@@ -334,7 +326,7 @@ class Player extends events_1.EventEmitter {
334
326
  }
335
327
  else {
336
328
  // Handle other types (string, Track)
337
- const hookOutcome = await this.extensionManager.BeforePlayHooks(effectiveRequest);
329
+ const hookOutcome = await this.extensionManager.beforePlayHooks(effectiveRequest);
338
330
  effectiveRequest = hookOutcome.request;
339
331
  hookResponse = hookOutcome.response;
340
332
  if (effectiveRequest.requestedBy === undefined) {
@@ -350,7 +342,7 @@ class Player extends events_1.EventEmitter {
350
342
  isPlaylist: hookResponse.isPlaylist ?? false,
351
343
  error: hookResponse.error,
352
344
  };
353
- await this.extensionManager.AfterPlayHooks(handledPayload);
345
+ await this.extensionManager.afterPlayHooks(handledPayload);
354
346
  if (hookResponse.error) {
355
347
  this.emit("playerError", hookResponse.error);
356
348
  }
@@ -393,7 +385,7 @@ class Player extends events_1.EventEmitter {
393
385
  (isTTS(tracksToAdd[0]) || queryLooksTTS)) {
394
386
  this.debug(`[Player] Interrupting with TTS: ${tracksToAdd[0].title}`);
395
387
  await this.interruptWithTTSTrack(tracksToAdd[0]);
396
- await this.extensionManager.AfterPlayHooks({
388
+ await this.extensionManager.afterPlayHooks({
397
389
  success: true,
398
390
  query: effectiveRequest.query,
399
391
  requestedBy: effectiveRequest.requestedBy,
@@ -411,7 +403,7 @@ class Player extends events_1.EventEmitter {
411
403
  this.emit("queueAdd", tracksToAdd[0]);
412
404
  }
413
405
  const started = !this.isPlaying ? await this.playNext() : true;
414
- await this.extensionManager.AfterPlayHooks({
406
+ await this.extensionManager.afterPlayHooks({
415
407
  success: started,
416
408
  query: effectiveRequest.query,
417
409
  requestedBy: effectiveRequest.requestedBy,
@@ -421,7 +413,7 @@ class Player extends events_1.EventEmitter {
421
413
  return started;
422
414
  }
423
415
  catch (error) {
424
- await this.extensionManager.AfterPlayHooks({
416
+ await this.extensionManager.afterPlayHooks({
425
417
  success: false,
426
418
  query: effectiveRequest.query,
427
419
  requestedBy: effectiveRequest.requestedBy,
@@ -434,6 +426,27 @@ class Player extends events_1.EventEmitter {
434
426
  return false;
435
427
  }
436
428
  }
429
+ async preloadNext() {
430
+ const next = this.queue.nextTrack;
431
+ if (!next)
432
+ return;
433
+ try {
434
+ const stream = await this.getStream(next);
435
+ if (!stream || !stream.stream) {
436
+ this.debug(`[Player] No stream available to preload for track: ${next.title}`);
437
+ return;
438
+ }
439
+ const resource = (0, voice_1.createAudioResource)(stream.stream, {
440
+ inlineVolume: true,
441
+ });
442
+ this.preloadedResource = resource;
443
+ this.preloadedTrack = next;
444
+ this.debug("Preloaded next track:", next.title);
445
+ }
446
+ catch (err) {
447
+ this.debug("Preload failed:", err);
448
+ }
449
+ }
437
450
  /**
438
451
  * Create AudioResource with filters and seek applied
439
452
  *
@@ -495,6 +508,26 @@ class Player extends events_1.EventEmitter {
495
508
  */
496
509
  async startTrack(track) {
497
510
  try {
511
+ if (this.preloadedResource &&
512
+ this.preloadedTrack?.id === track.id &&
513
+ this.preloadedResource.playStream?.readable !== false) {
514
+ this.debug(`[Player] Using preloaded resource for track: ${track.title}`);
515
+ this.audioPlayer.stop(true);
516
+ this.destroyCurrentStream();
517
+ this.currentResource = this.preloadedResource;
518
+ this.currentResource.volume?.setVolume(this.volume / 100);
519
+ this.audioPlayer.play(this.currentResource);
520
+ await (0, voice_1.entersState)(this.audioPlayer, voice_1.AudioPlayerStatus.Playing, 5_000);
521
+ if (this.preloadedResource) {
522
+ try {
523
+ this.preloadedResource.playStream?.destroy?.();
524
+ }
525
+ catch { }
526
+ }
527
+ this.preloadedResource = null;
528
+ this.preloadedTrack = null;
529
+ return true;
530
+ }
498
531
  let streamInfo = await this.getStream(track);
499
532
  this.debug(`[Player] Using stream for track: ${track.title}`);
500
533
  // Kiểm tra nếu có stream thực sự để tạo AudioResource
@@ -556,7 +589,7 @@ class Player extends events_1.EventEmitter {
556
589
  }
557
590
  }
558
591
  async playNext() {
559
- this.debug(`[Player] playNext called by ${new Error().stack?.split("\n")[2]?.trim()}`);
592
+ this.debug("[Player] playNext called");
560
593
  while (true) {
561
594
  const track = this.queue.next(this.skipLoop);
562
595
  this.skipLoop = false;
@@ -576,11 +609,17 @@ class Player extends events_1.EventEmitter {
576
609
  }
577
610
  return false;
578
611
  }
579
- this.generateWillNext();
612
+ this.generateWillNext().catch((err) => this.debug("[Player] generateWillNext error:", err));
580
613
  this.clearLeaveTimeout();
581
614
  this.debug(`[Player] playNext called for track: ${track.title}`);
582
615
  try {
583
- return await this.startTrack(track);
616
+ const started = await this.startTrack(track);
617
+ if (started) {
618
+ setImmediate(() => {
619
+ this.preloadNext();
620
+ });
621
+ }
622
+ return started;
584
623
  }
585
624
  catch (err) {
586
625
  this.debug(`[Player] playNext error:`, err);
@@ -624,12 +663,12 @@ class Player extends events_1.EventEmitter {
624
663
  // Build resource from plugin stream
625
664
  const streamInfo = await this.pluginManager.getStream(track);
626
665
  if (!streamInfo) {
627
- throw new Error("No stream available for track: ${track.title}");
666
+ throw new Error(`No stream available for track: ${track.title}`);
628
667
  }
629
668
  ttsStream = streamInfo.stream;
630
669
  const resource = await this.createResource(streamInfo, track);
631
670
  if (!resource) {
632
- throw new Error("No resource available for track: ${track.title}");
671
+ throw new Error(`No resource available for track: ${track.title}`);
633
672
  }
634
673
  ttsResource = resource;
635
674
  if (resource.volume) {
@@ -656,7 +695,7 @@ class Player extends events_1.EventEmitter {
656
695
  declared
657
696
  : declared * 1000
658
697
  : undefined;
659
- const cap = this.options?.tts?.Max_Time_TTS ?? 60_000;
698
+ const cap = this.options?.tts?.maxTimeTts ?? 60_000;
660
699
  const idleTimeout = declaredMs ? Math.min(cap, Math.max(1_000, declaredMs + 1_500)) : cap;
661
700
  await (0, voice_1.entersState)(ttsPlayer, voice_1.AudioPlayerStatus.Idle, idleTimeout).catch(() => null);
662
701
  // Swap back and resume if needed
@@ -707,9 +746,21 @@ class Player extends events_1.EventEmitter {
707
746
  });
708
747
  await (0, voice_1.entersState)(connection, voice_1.VoiceConnectionStatus.Ready, 50_000);
709
748
  this.connection = connection;
710
- connection.on(voice_1.VoiceConnectionStatus.Disconnected, () => {
711
- this.debug(`[Player] VoiceConnectionStatus.Disconnected`);
712
- this.destroy();
749
+ connection.on(voice_1.VoiceConnectionStatus.Disconnected, async () => {
750
+ try {
751
+ // move channel
752
+ await Promise.race([
753
+ (0, voice_1.entersState)(connection, voice_1.VoiceConnectionStatus.Signalling, 5_000),
754
+ (0, voice_1.entersState)(connection, voice_1.VoiceConnectionStatus.Connecting, 5_000),
755
+ ]);
756
+ // Signalling/Connecting → reconnect
757
+ this.debug(`[Player] Reconnecting after channel move...`);
758
+ }
759
+ catch {
760
+ // no reconnect in 5 giây → disconnect
761
+ this.debug(`[Player] Truly disconnected, destroying player`);
762
+ this.destroy();
763
+ }
713
764
  });
714
765
  connection.on("error", (error) => {
715
766
  this.debug(`[Player] Voice connection error:`, error);
@@ -757,7 +808,7 @@ class Player extends events_1.EventEmitter {
757
808
  const track = this.queue.currentTrack;
758
809
  if (track) {
759
810
  this.debug(`[Player] Player resumed on track: ${track.title}`);
760
- this.emit("playerResume", track);
811
+ // this.emit("playerResume", track); //đã có trong stateChange
761
812
  }
762
813
  }
763
814
  return result;
@@ -776,6 +827,8 @@ class Player extends events_1.EventEmitter {
776
827
  this.debug(`[Player] stop called`);
777
828
  this.queue.clear();
778
829
  const result = this.audioPlayer.stop();
830
+ this.destroyCurrentStream();
831
+ this.currentResource = null;
779
832
  this.isPlaying = false;
780
833
  this.isPaused = false;
781
834
  this.emit("playerStop");
@@ -806,12 +859,7 @@ class Player extends events_1.EventEmitter {
806
859
  this.debug(`[Player] Invalid seek position: ${position}ms (track duration: ${totalDuration}ms)`);
807
860
  return false;
808
861
  }
809
- const streaminfo = await this.getStream(track);
810
- if (!streaminfo?.stream) {
811
- this.debug(`[Player] No stream to seek`);
812
- return false;
813
- }
814
- await this.refeshPlayerResource(true, position);
862
+ await this.refreshPlayerResource(true, position);
815
863
  return true;
816
864
  }
817
865
  /**
@@ -917,7 +965,7 @@ class Player extends events_1.EventEmitter {
917
965
  saveOptions = options;
918
966
  }
919
967
  try {
920
- // Try extensions first
968
+ // Skip extension manager for saving - we want the raw stream without filters/seek applied, and extensions may not support this
921
969
  let streamInfo = await this.pluginManager.getStream(track);
922
970
  if (!streamInfo || !streamInfo.stream) {
923
971
  throw new Error(`No save stream available for track: ${track.title}`);
@@ -1131,21 +1179,96 @@ class Player extends events_1.EventEmitter {
1131
1179
  * @example
1132
1180
  * const progressBar = player.getProgressBar();
1133
1181
  * console.log(`Progress bar: ${progressBar}`);
1182
+ *
1183
+ * // Custom options
1184
+ * const customBar = player.getProgressBar({
1185
+ * size: 30,
1186
+ * barChar: "─",
1187
+ * progressChar: "●",
1188
+ * timeFormat: "compact" // "compact" = 1:22:12, "full" = 01:22:12
1189
+ * });
1134
1190
  */
1135
1191
  getProgressBar(options = {}) {
1136
- const { size = 20, barChar = "▬", progressChar = "🔘" } = options;
1192
+ const { size = 20, barChar = "▬", progressChar = "🔘", timeFormat = "compact", // "compact" or "full"
1193
+ showPercentage = false, showTime = true, } = options;
1137
1194
  const track = this.queue.currentTrack;
1138
1195
  const resource = this.currentResource;
1139
- if (!track || !resource)
1196
+ // Handle live stream
1197
+ if (this.isLive || !track || !resource) {
1198
+ if (this.isLive)
1199
+ return "🔴 LIVE";
1140
1200
  return "";
1201
+ }
1141
1202
  const total = track.duration > 1000 ? track.duration : track.duration * 1000;
1142
1203
  if (!total)
1143
- return this.formatTime(resource.playbackDuration);
1204
+ return this.formatTimeCompact(resource.playbackDuration);
1144
1205
  const current = resource.playbackDuration;
1145
- const ratio = Math.min(current / total, 1);
1206
+ const ratio = Math.min(Math.max(current / total, 0), 1);
1146
1207
  const progress = Math.round(ratio * size);
1147
- const bar = barChar.repeat(progress) + progressChar + barChar.repeat(size - progress);
1148
- return `${this.formatTime(current)} | ${bar} | ${this.formatTime(total)}`;
1208
+ // Build progress bar
1209
+ let bar = "";
1210
+ if (progressChar === "none" || options.hideProgressChar) {
1211
+ // Continuous bar without separator
1212
+ const filled = barChar.repeat(progress);
1213
+ const empty = barChar.repeat(size - progress);
1214
+ bar = filled + empty;
1215
+ }
1216
+ else {
1217
+ // Bar with progress character
1218
+ const filled = barChar.repeat(progress);
1219
+ const empty = barChar.repeat(Math.max(0, size - progress));
1220
+ bar = filled + progressChar + empty;
1221
+ }
1222
+ // Format time based on option
1223
+ const formatTimeFn = timeFormat === "compact" ? this.formatTimeCompact.bind(this) : this.formatTime.bind(this);
1224
+ const currentTimeStr = formatTimeFn(current);
1225
+ const totalTimeStr = formatTimeFn(total);
1226
+ // Build result
1227
+ let result = "";
1228
+ if (showTime) {
1229
+ result = `${currentTimeStr} ${bar} ${totalTimeStr}`;
1230
+ }
1231
+ else {
1232
+ result = bar;
1233
+ }
1234
+ // Add percentage if requested
1235
+ if (showPercentage) {
1236
+ const percent = Math.round(ratio * 100);
1237
+ result += ` (${percent}%)`;
1238
+ }
1239
+ return result;
1240
+ }
1241
+ /**
1242
+ * Format time with leading zeros (00:00 or 00:00:00)
1243
+ * @param ms - Time in milliseconds
1244
+ * @returns Formatted time string with leading zeros
1245
+ */
1246
+ formatTime(ms) {
1247
+ const totalSeconds = Math.floor(ms / 1000);
1248
+ const hours = Math.floor(totalSeconds / 3600);
1249
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
1250
+ const seconds = totalSeconds % 60;
1251
+ const parts = [];
1252
+ if (hours > 0)
1253
+ parts.push(String(hours).padStart(2, "0"));
1254
+ parts.push(String(minutes).padStart(2, "0"));
1255
+ parts.push(String(seconds).padStart(2, "0"));
1256
+ return parts.join(":");
1257
+ }
1258
+ /**
1259
+ * Format time without leading zeros for hours (1:22:12 or 3:45)
1260
+ * @param ms - Time in milliseconds
1261
+ * @returns Compact formatted time string
1262
+ */
1263
+ formatTimeCompact(ms) {
1264
+ const totalSeconds = Math.floor(ms / 1000);
1265
+ const hours = Math.floor(totalSeconds / 3600);
1266
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
1267
+ const seconds = totalSeconds % 60;
1268
+ if (hours > 0) {
1269
+ return `${hours}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
1270
+ }
1271
+ return `${minutes}:${String(seconds).padStart(2, "0")}`;
1149
1272
  }
1150
1273
  /**
1151
1274
  * Get the time of the current track
@@ -1154,44 +1277,44 @@ class Player extends events_1.EventEmitter {
1154
1277
  * @example
1155
1278
  * const time = player.getTime();
1156
1279
  * console.log(`Time: ${time.current}`);
1280
+ * console.log(`Formatted: ${time.formatted.current}`); // "1:22:12" or "3:45"
1157
1281
  */
1158
1282
  getTime() {
1283
+ if (this.isLive)
1284
+ return {
1285
+ current: 0,
1286
+ total: 0,
1287
+ format: "LIVE",
1288
+ formatted: {
1289
+ current: "LIVE",
1290
+ total: "LIVE",
1291
+ },
1292
+ };
1159
1293
  const resource = this.currentResource;
1160
1294
  const track = this.queue.currentTrack;
1161
- if (!track || !resource)
1295
+ if (!track || !resource) {
1162
1296
  return {
1163
1297
  current: 0,
1164
1298
  total: 0,
1165
1299
  format: "00:00",
1300
+ formatted: {
1301
+ current: "00:00",
1302
+ total: "00:00",
1303
+ },
1166
1304
  };
1305
+ }
1167
1306
  const total = track.duration > 1000 ? track.duration : track.duration * 1000;
1307
+ const current = resource.playbackDuration;
1168
1308
  return {
1169
- current: resource?.playbackDuration,
1309
+ current: current,
1170
1310
  total: total,
1171
- format: this.formatTime(resource.playbackDuration),
1311
+ format: this.formatTime(current),
1312
+ formatted: {
1313
+ current: this.formatTimeCompact(current),
1314
+ total: this.formatTimeCompact(total),
1315
+ },
1172
1316
  };
1173
1317
  }
1174
- /**
1175
- * Format the time in the format of HH:MM:SS
1176
- *
1177
- * @param {number} ms - The time in milliseconds
1178
- * @returns {string} The formatted time
1179
- * @example
1180
- * const formattedTime = player.formatTime(1000);
1181
- * console.log(`Formatted time: ${formattedTime}`);
1182
- */
1183
- formatTime(ms) {
1184
- const totalSeconds = Math.floor(ms / 1000);
1185
- const hours = Math.floor(totalSeconds / 3600);
1186
- const minutes = Math.floor((totalSeconds % 3600) / 60);
1187
- const seconds = totalSeconds % 60;
1188
- const parts = [];
1189
- if (hours > 0)
1190
- parts.push(String(hours).padStart(2, "0"));
1191
- parts.push(String(minutes).padStart(2, "0"));
1192
- parts.push(String(seconds).padStart(2, "0"));
1193
- return parts.join(":");
1194
- }
1195
1318
  /**
1196
1319
  * Destroy the player
1197
1320
  *
@@ -1207,6 +1330,7 @@ class Player extends events_1.EventEmitter {
1207
1330
  }
1208
1331
  // Destroy current stream before stopping audio
1209
1332
  this.destroyCurrentStream();
1333
+ this.audioPlayer.removeAllListeners();
1210
1334
  this.audioPlayer.stop(true);
1211
1335
  if (this.ttsPlayer) {
1212
1336
  try {
@@ -1240,7 +1364,7 @@ class Player extends events_1.EventEmitter {
1240
1364
  if (this.leaveTimeout) {
1241
1365
  clearTimeout(this.leaveTimeout);
1242
1366
  }
1243
- if (this.options.leaveOnEmpty && this.options.leaveTimeout) {
1367
+ if (this.options.leaveOnEnd && this.options.leaveTimeout) {
1244
1368
  this.leaveTimeout = setTimeout(() => {
1245
1369
  this.debug(`[Player] Leaving voice channel after timeoutMs`);
1246
1370
  this.destroy();
@@ -1254,13 +1378,16 @@ class Player extends events_1.EventEmitter {
1254
1378
  * @param {number} position - Position to seek to in milliseconds
1255
1379
  * @returns {Promise<boolean>}
1256
1380
  * @example
1257
- * const refreshed = await player.refeshPlayerResource(true, 1000);
1381
+ * const refreshed = await player.refreshPlayerResource(true, 1000);
1258
1382
  * console.log(`Refreshed: ${refreshed}`);
1259
1383
  */
1260
- async refeshPlayerResource(applyToCurrent = true, position = -1) {
1384
+ async refreshPlayerResource(applyToCurrent = true, position = -1) {
1261
1385
  if (!applyToCurrent || !this.queue.currentTrack || !(this.isPlaying || this.isPaused)) {
1262
1386
  return false;
1263
1387
  }
1388
+ if (this.refreshLock)
1389
+ return false;
1390
+ this.refreshLock = true;
1264
1391
  try {
1265
1392
  const track = this.queue.currentTrack;
1266
1393
  this.debug(`[Player] Refreshing player resource for track: ${track.title}`);
@@ -1287,7 +1414,10 @@ class Player extends events_1.EventEmitter {
1287
1414
  }
1288
1415
  }
1289
1416
  catch (error) {
1290
- this.debug(`[Player] Error destroying old stream in refeshPlayerResource:`, error);
1417
+ this.debug(`[Player] Error destroying old stream in refreshPlayerResource:`, error);
1418
+ }
1419
+ finally {
1420
+ this.refreshLock = false;
1291
1421
  }
1292
1422
  this.currentResource = resource;
1293
1423
  // Subscribe to new resource
@@ -1404,6 +1534,19 @@ class Player extends events_1.EventEmitter {
1404
1534
  }
1405
1535
  else if (newState.status === voice_1.AudioPlayerStatus.Buffering) {
1406
1536
  this.debug(`[Player] AudioPlayerStatus.Buffering`);
1537
+ this.lastDuration = this.currentResource?.playbackDuration || 0;
1538
+ this.stuckTimer = setTimeout(() => {
1539
+ if (this.currentResource?.playbackDuration === this.lastDuration) {
1540
+ this.emit("trackStuck", this.currentTrack);
1541
+ this.skip();
1542
+ }
1543
+ }, 10000);
1544
+ }
1545
+ else {
1546
+ if (this.stuckTimer) {
1547
+ clearTimeout(this.stuckTimer);
1548
+ this.stuckTimer = null;
1549
+ }
1407
1550
  }
1408
1551
  });
1409
1552
  this.audioPlayer.on("error", (error) => {
@@ -1425,6 +1568,113 @@ class Player extends events_1.EventEmitter {
1425
1568
  this.debug(`[Player] Removing plugin: ${name}`);
1426
1569
  return this.pluginManager.unregister(name);
1427
1570
  }
1571
+ /**
1572
+ * Save the current session of the player, including queue, current track, position, volume, loop mode, auto-play mode, and active extensions/plugins.
1573
+ *
1574
+ * @returns {PlayerSession} The saved session data
1575
+ */
1576
+ saveSession() {
1577
+ return {
1578
+ guildId: this.guildId,
1579
+ currentTrack: this.currentTrack,
1580
+ position: this.currentResource?.playbackDuration || null,
1581
+ volume: this.volume,
1582
+ queue: this.queue.getTracks(),
1583
+ loopMode: this.queue.loop(),
1584
+ autoPlay: this.queue.autoPlay(),
1585
+ extensions: this.extensionManager.getAll().map((ext) => ext.name),
1586
+ plugins: this.pluginManager.getAll().map((plugin) => plugin.name),
1587
+ };
1588
+ }
1589
+ /**
1590
+ * Set persistence manager for auto-save
1591
+ */
1592
+ setPersistenceManager(manager) {
1593
+ this.persistenceManager = manager;
1594
+ this.startAutoSaveTracking();
1595
+ }
1596
+ startAutoSaveTracking() {
1597
+ // Track state changes for auto-save
1598
+ const trackChanges = () => {
1599
+ this.scheduleAutoSave();
1600
+ };
1601
+ this.on("trackStart", trackChanges);
1602
+ this.on("trackEnd", trackChanges);
1603
+ this.on("queueAdd", trackChanges);
1604
+ this.on("queueRemove", trackChanges);
1605
+ this.on("volumeChange", trackChanges);
1606
+ // Save periodically
1607
+ setInterval(() => {
1608
+ this.saveIfNeeded();
1609
+ }, this.AUTO_SAVE_INTERVAL);
1610
+ }
1611
+ scheduleAutoSave() {
1612
+ if (!this.persistenceManager)
1613
+ return;
1614
+ this.lastSaveTime = Date.now();
1615
+ // Can implement debounced save here
1616
+ }
1617
+ async saveIfNeeded() {
1618
+ if (!this.persistenceManager)
1619
+ return;
1620
+ if (Date.now() - this.lastSaveTime < this.AUTO_SAVE_INTERVAL)
1621
+ return;
1622
+ await this.persistenceManager.savePlayer(this);
1623
+ this.lastSaveTime = Date.now();
1624
+ }
1625
+ /**
1626
+ * Save current player state
1627
+ */
1628
+ async savePlayer() {
1629
+ if (!this.persistenceManager) {
1630
+ this.debug("[Player] No persistence manager configured");
1631
+ return false;
1632
+ }
1633
+ return await this.persistenceManager.savePlayer(this);
1634
+ }
1635
+ /**
1636
+ * Get serializable state (for manual persistence)
1637
+ */
1638
+ getSerializableState() {
1639
+ return {
1640
+ guildId: this.guildId,
1641
+ queue: this.queue.getTracks(),
1642
+ currentTrack: this.currentTrack,
1643
+ volume: this.volume,
1644
+ isPlaying: this.isPlaying,
1645
+ isPaused: this.isPaused,
1646
+ loopMode: this.queue.loop(),
1647
+ autoPlay: this.queue.autoPlay(),
1648
+ filters: this.filter.getFilterString(),
1649
+ timestamp: Date.now(),
1650
+ };
1651
+ }
1652
+ /**
1653
+ * Restore from saved state
1654
+ */
1655
+ async restoreState(state) {
1656
+ try {
1657
+ if (state.volume)
1658
+ this.setVolume(state.volume);
1659
+ if (state.loopMode)
1660
+ this.queue.loop(state.loopMode);
1661
+ if (typeof state.autoPlay === "boolean")
1662
+ this.queue.autoPlay(state.autoPlay);
1663
+ if (state.filters)
1664
+ await this.filter.applyFilters(state.filters.split(","));
1665
+ // Restore queue
1666
+ if (state.queue && Array.isArray(state.queue)) {
1667
+ this.queue.clear();
1668
+ this.queue.addMultiple(state.queue);
1669
+ }
1670
+ this.debug("[Player] State restored");
1671
+ return true;
1672
+ }
1673
+ catch (error) {
1674
+ this.debug("[Player] Failed to restore state:", error);
1675
+ return false;
1676
+ }
1677
+ }
1428
1678
  //#endregion
1429
1679
  //#region Getters
1430
1680
  /**
@@ -1504,6 +1754,9 @@ class Player extends events_1.EventEmitter {
1504
1754
  get relatedTracks() {
1505
1755
  return this.queue.relatedTracks();
1506
1756
  }
1757
+ get isLive() {
1758
+ return this.currentTrack?.isLive === true;
1759
+ }
1507
1760
  }
1508
1761
  exports.Player = Player;
1509
1762
  //# sourceMappingURL=Player.js.map