ziplayer 0.2.7-dev.0 → 0.2.7-dev.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.
Files changed (54) hide show
  1. package/AI-Guide.md +407 -756
  2. package/README.md +275 -10
  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 +95 -0
  11. package/dist/persistence/PersistenceManager.d.ts.map +1 -0
  12. package/dist/persistence/PersistenceManager.js +968 -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 +204 -113
  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 +65 -14
  23. package/dist/structures/Player.d.ts.map +1 -1
  24. package/dist/structures/Player.js +330 -88
  25. package/dist/structures/Player.js.map +1 -1
  26. package/dist/structures/PlayerManager.d.ts +127 -91
  27. package/dist/structures/PlayerManager.d.ts.map +1 -1
  28. package/dist/structures/PlayerManager.js +437 -124
  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 +46 -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 +74 -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 +3 -2
  43. package/src/extensions/BaseExtension.ts +1 -0
  44. package/src/extensions/index.ts +320 -37
  45. package/src/persistence/PersistenceManager.ts +1073 -0
  46. package/src/plugins/BasePlugin.ts +1 -1
  47. package/src/plugins/index.ts +248 -133
  48. package/src/structures/FilterManager.ts +3 -3
  49. package/src/structures/Player.ts +358 -94
  50. package/src/structures/PlayerManager.ts +535 -129
  51. package/src/structures/Queue.ts +300 -55
  52. package/src/types/index.ts +52 -10
  53. package/src/types/persistence.ts +83 -0
  54. package/src/types/plugin.ts +1 -1
@@ -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");
@@ -51,15 +52,21 @@ class Player extends events_1.EventEmitter {
51
52
  this.volume = 100;
52
53
  this.isPlaying = false;
53
54
  this.isPaused = false;
55
+ this._lastActivity = Date.now();
54
56
  this.leaveTimeout = null;
55
57
  this.currentResource = null;
56
58
  this.volumeInterval = null;
59
+ this.stuckTimer = null;
57
60
  this.skipLoop = false;
58
- // Cache for search results to avoid duplicate calls
59
- this.searchCache = new Map();
61
+ this.refreshLock = false;
62
+ //preloaded resource
63
+ this.preloadedResource = null;
64
+ this.preloadedTrack = null;
60
65
  this.SEARCH_CACHE_TTL = 2 * 60 * 1000; // 2 minutes
61
- this.searchCacheTimestamps = new Map();
62
66
  this.ttsPlayer = null;
67
+ this.lastDuration = 0;
68
+ this.lastSaveTime = 0;
69
+ this.AUTO_SAVE_INTERVAL = 30000; // 30 seconds
63
70
  this.debug(`[Player] Constructor called for guildId: ${guildId}`);
64
71
  this.guildId = guildId;
65
72
  this.queue = new Queue_1.Queue();
@@ -84,7 +91,7 @@ class Player extends events_1.EventEmitter {
84
91
  createPlayer: false,
85
92
  interrupt: true,
86
93
  volume: 100,
87
- Max_Time_TTS: 60_000,
94
+ maxTimeTts: 60_000,
88
95
  ...(options?.tts || {}),
89
96
  },
90
97
  };
@@ -95,6 +102,10 @@ class Player extends events_1.EventEmitter {
95
102
  });
96
103
  this.volume = this.options.volume || 100;
97
104
  this.userdata = this.options.userdata;
105
+ this.searchCache = new lru_cache_1.LRUCache({
106
+ max: 200,
107
+ ttl: this.SEARCH_CACHE_TTL,
108
+ });
98
109
  this.setupEventListeners();
99
110
  // Initialize filters from options
100
111
  if (this.options.filters && this.options.filters.length > 0) {
@@ -114,11 +125,12 @@ class Player extends events_1.EventEmitter {
114
125
  * @private
115
126
  */
116
127
  destroyCurrentStream() {
128
+ this.audioPlayer.stop(true);
117
129
  if (!this.currentResource)
118
130
  return;
119
131
  const stream = this.currentResource?.metadata?.stream ?? this.currentResource?.stream;
120
- if (stream?.destroy) {
121
- stream.destroy();
132
+ if (stream && typeof stream.destroy === "function") {
133
+ stream.destroy().catch((e) => this.debug("Stream destroy error:", e));
122
134
  }
123
135
  this.currentResource = null;
124
136
  }
@@ -135,11 +147,6 @@ class Player extends events_1.EventEmitter {
135
147
  */
136
148
  async search(query, requestedBy) {
137
149
  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
150
  // Check cache first
144
151
  const cachedResult = this.getCachedSearchResult(query);
145
152
  if (cachedResult) {
@@ -198,14 +205,10 @@ class Player extends events_1.EventEmitter {
198
205
  */
199
206
  getCachedSearchResult(query) {
200
207
  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
- }
208
+ const cached = this.searchCache.get(cacheKey);
209
+ if (cached) {
210
+ this.debug(`[SearchCache] Using cached search result for: ${query}`);
211
+ return cached;
209
212
  }
210
213
  return null;
211
214
  }
@@ -216,23 +219,15 @@ class Player extends events_1.EventEmitter {
216
219
  */
217
220
  cacheSearchResult(query, result) {
218
221
  const cacheKey = query.toLowerCase().trim();
219
- const now = Date.now();
220
222
  this.searchCache.set(cacheKey, result);
221
- this.searchCacheTimestamps.set(cacheKey, now);
222
223
  this.debug(`[SearchCache] Cached search result for: ${query} (${result.tracks.length} tracks)`);
223
224
  }
224
225
  /**
225
226
  * Clear expired search cache entries
226
227
  */
227
228
  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
- }
229
+ this.searchCache.purgeStale();
230
+ this.debug(`[SearchCache] Purged stale search cache entries`);
236
231
  }
237
232
  /**
238
233
  * Clear all search cache entries
@@ -242,7 +237,6 @@ class Player extends events_1.EventEmitter {
242
237
  clearSearchCache() {
243
238
  const cacheSize = this.searchCache.size;
244
239
  this.searchCache.clear();
245
- this.searchCacheTimestamps.clear();
246
240
  this.debug(`[SearchCache] Cleared all ${cacheSize} search cache entries`);
247
241
  }
248
242
  /**
@@ -252,9 +246,8 @@ class Player extends events_1.EventEmitter {
252
246
  */
253
247
  debugSearchQuery(query) {
254
248
  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;
249
+ const cached = this.searchCache.get(cacheKey);
250
+ const isCached = !!cached;
258
251
  const allPlugins = this.pluginManager.getAll();
259
252
  const plugins = allPlugins.filter((p) => {
260
253
  if (p.name.toLowerCase() === "tts" && !query.toLowerCase().startsWith("tts:")) {
@@ -263,8 +256,8 @@ class Player extends events_1.EventEmitter {
263
256
  return true;
264
257
  });
265
258
  return {
266
- isCached: !!isCached,
267
- cacheAge: cachedTimestamp ? now - cachedTimestamp : undefined,
259
+ isCached,
260
+ cacheAge: undefined,
268
261
  pluginCount: plugins.length,
269
262
  ttsFiltered: allPlugins.length > plugins.length,
270
263
  };
@@ -334,7 +327,7 @@ class Player extends events_1.EventEmitter {
334
327
  }
335
328
  else {
336
329
  // Handle other types (string, Track)
337
- const hookOutcome = await this.extensionManager.BeforePlayHooks(effectiveRequest);
330
+ const hookOutcome = await this.extensionManager.beforePlayHooks(effectiveRequest);
338
331
  effectiveRequest = hookOutcome.request;
339
332
  hookResponse = hookOutcome.response;
340
333
  if (effectiveRequest.requestedBy === undefined) {
@@ -350,7 +343,7 @@ class Player extends events_1.EventEmitter {
350
343
  isPlaylist: hookResponse.isPlaylist ?? false,
351
344
  error: hookResponse.error,
352
345
  };
353
- await this.extensionManager.AfterPlayHooks(handledPayload);
346
+ await this.extensionManager.afterPlayHooks(handledPayload);
354
347
  if (hookResponse.error) {
355
348
  this.emit("playerError", hookResponse.error);
356
349
  }
@@ -393,7 +386,7 @@ class Player extends events_1.EventEmitter {
393
386
  (isTTS(tracksToAdd[0]) || queryLooksTTS)) {
394
387
  this.debug(`[Player] Interrupting with TTS: ${tracksToAdd[0].title}`);
395
388
  await this.interruptWithTTSTrack(tracksToAdd[0]);
396
- await this.extensionManager.AfterPlayHooks({
389
+ await this.extensionManager.afterPlayHooks({
397
390
  success: true,
398
391
  query: effectiveRequest.query,
399
392
  requestedBy: effectiveRequest.requestedBy,
@@ -411,7 +404,7 @@ class Player extends events_1.EventEmitter {
411
404
  this.emit("queueAdd", tracksToAdd[0]);
412
405
  }
413
406
  const started = !this.isPlaying ? await this.playNext() : true;
414
- await this.extensionManager.AfterPlayHooks({
407
+ await this.extensionManager.afterPlayHooks({
415
408
  success: started,
416
409
  query: effectiveRequest.query,
417
410
  requestedBy: effectiveRequest.requestedBy,
@@ -421,7 +414,7 @@ class Player extends events_1.EventEmitter {
421
414
  return started;
422
415
  }
423
416
  catch (error) {
424
- await this.extensionManager.AfterPlayHooks({
417
+ await this.extensionManager.afterPlayHooks({
425
418
  success: false,
426
419
  query: effectiveRequest.query,
427
420
  requestedBy: effectiveRequest.requestedBy,
@@ -434,6 +427,27 @@ class Player extends events_1.EventEmitter {
434
427
  return false;
435
428
  }
436
429
  }
430
+ async preloadNext() {
431
+ const next = this.queue.nextTrack;
432
+ if (!next)
433
+ return;
434
+ try {
435
+ const stream = await this.getStream(next);
436
+ if (!stream || !stream.stream) {
437
+ this.debug(`[Player] No stream available to preload for track: ${next.title}`);
438
+ return;
439
+ }
440
+ const resource = (0, voice_1.createAudioResource)(stream.stream, {
441
+ inlineVolume: true,
442
+ });
443
+ this.preloadedResource = resource;
444
+ this.preloadedTrack = next;
445
+ this.debug("Preloaded next track:", next.title);
446
+ }
447
+ catch (err) {
448
+ this.debug("Preload failed:", err);
449
+ }
450
+ }
437
451
  /**
438
452
  * Create AudioResource with filters and seek applied
439
453
  *
@@ -495,6 +509,26 @@ class Player extends events_1.EventEmitter {
495
509
  */
496
510
  async startTrack(track) {
497
511
  try {
512
+ if (this.preloadedResource &&
513
+ this.preloadedTrack?.id === track.id &&
514
+ this.preloadedResource.playStream?.readable !== false) {
515
+ this.debug(`[Player] Using preloaded resource for track: ${track.title}`);
516
+ this.audioPlayer.stop(true);
517
+ this.destroyCurrentStream();
518
+ this.currentResource = this.preloadedResource;
519
+ this.currentResource.volume?.setVolume(this.volume / 100);
520
+ this.audioPlayer.play(this.currentResource);
521
+ await (0, voice_1.entersState)(this.audioPlayer, voice_1.AudioPlayerStatus.Playing, 5_000);
522
+ if (this.preloadedResource) {
523
+ try {
524
+ this.preloadedResource.playStream?.destroy?.();
525
+ }
526
+ catch { }
527
+ }
528
+ this.preloadedResource = null;
529
+ this.preloadedTrack = null;
530
+ return true;
531
+ }
498
532
  let streamInfo = await this.getStream(track);
499
533
  this.debug(`[Player] Using stream for track: ${track.title}`);
500
534
  // Kiểm tra nếu có stream thực sự để tạo AudioResource
@@ -556,7 +590,7 @@ class Player extends events_1.EventEmitter {
556
590
  }
557
591
  }
558
592
  async playNext() {
559
- this.debug(`[Player] playNext called by ${new Error().stack?.split("\n")[2]?.trim()}`);
593
+ this.debug("[Player] playNext called");
560
594
  while (true) {
561
595
  const track = this.queue.next(this.skipLoop);
562
596
  this.skipLoop = false;
@@ -576,11 +610,17 @@ class Player extends events_1.EventEmitter {
576
610
  }
577
611
  return false;
578
612
  }
579
- this.generateWillNext();
613
+ this.generateWillNext().catch((err) => this.debug("[Player] generateWillNext error:", err));
580
614
  this.clearLeaveTimeout();
581
615
  this.debug(`[Player] playNext called for track: ${track.title}`);
582
616
  try {
583
- return await this.startTrack(track);
617
+ const started = await this.startTrack(track);
618
+ if (started) {
619
+ setImmediate(() => {
620
+ this.preloadNext();
621
+ });
622
+ }
623
+ return started;
584
624
  }
585
625
  catch (err) {
586
626
  this.debug(`[Player] playNext error:`, err);
@@ -624,12 +664,12 @@ class Player extends events_1.EventEmitter {
624
664
  // Build resource from plugin stream
625
665
  const streamInfo = await this.pluginManager.getStream(track);
626
666
  if (!streamInfo) {
627
- throw new Error("No stream available for track: ${track.title}");
667
+ throw new Error(`No stream available for track: ${track.title}`);
628
668
  }
629
669
  ttsStream = streamInfo.stream;
630
670
  const resource = await this.createResource(streamInfo, track);
631
671
  if (!resource) {
632
- throw new Error("No resource available for track: ${track.title}");
672
+ throw new Error(`No resource available for track: ${track.title}`);
633
673
  }
634
674
  ttsResource = resource;
635
675
  if (resource.volume) {
@@ -656,7 +696,7 @@ class Player extends events_1.EventEmitter {
656
696
  declared
657
697
  : declared * 1000
658
698
  : undefined;
659
- const cap = this.options?.tts?.Max_Time_TTS ?? 60_000;
699
+ const cap = this.options?.tts?.maxTimeTts ?? 60_000;
660
700
  const idleTimeout = declaredMs ? Math.min(cap, Math.max(1_000, declaredMs + 1_500)) : cap;
661
701
  await (0, voice_1.entersState)(ttsPlayer, voice_1.AudioPlayerStatus.Idle, idleTimeout).catch(() => null);
662
702
  // Swap back and resume if needed
@@ -769,7 +809,7 @@ class Player extends events_1.EventEmitter {
769
809
  const track = this.queue.currentTrack;
770
810
  if (track) {
771
811
  this.debug(`[Player] Player resumed on track: ${track.title}`);
772
- this.emit("playerResume", track);
812
+ // this.emit("playerResume", track); //đã có trong stateChange
773
813
  }
774
814
  }
775
815
  return result;
@@ -820,12 +860,7 @@ class Player extends events_1.EventEmitter {
820
860
  this.debug(`[Player] Invalid seek position: ${position}ms (track duration: ${totalDuration}ms)`);
821
861
  return false;
822
862
  }
823
- const streaminfo = await this.getStream(track);
824
- if (!streaminfo?.stream) {
825
- this.debug(`[Player] No stream to seek`);
826
- return false;
827
- }
828
- await this.refeshPlayerResource(true, position);
863
+ await this.refreshPlayerResource(true, position);
829
864
  return true;
830
865
  }
831
866
  /**
@@ -931,7 +966,7 @@ class Player extends events_1.EventEmitter {
931
966
  saveOptions = options;
932
967
  }
933
968
  try {
934
- // Try extensions first
969
+ // Skip extension manager for saving - we want the raw stream without filters/seek applied, and extensions may not support this
935
970
  let streamInfo = await this.pluginManager.getStream(track);
936
971
  if (!streamInfo || !streamInfo.stream) {
937
972
  throw new Error(`No save stream available for track: ${track.title}`);
@@ -1145,21 +1180,96 @@ class Player extends events_1.EventEmitter {
1145
1180
  * @example
1146
1181
  * const progressBar = player.getProgressBar();
1147
1182
  * console.log(`Progress bar: ${progressBar}`);
1183
+ *
1184
+ * // Custom options
1185
+ * const customBar = player.getProgressBar({
1186
+ * size: 30,
1187
+ * barChar: "─",
1188
+ * progressChar: "●",
1189
+ * timeFormat: "compact" // "compact" = 1:22:12, "full" = 01:22:12
1190
+ * });
1148
1191
  */
1149
1192
  getProgressBar(options = {}) {
1150
- const { size = 20, barChar = "▬", progressChar = "🔘" } = options;
1193
+ const { size = 20, barChar = "▬", progressChar = "🔘", timeFormat = "compact", // "compact" or "full"
1194
+ showPercentage = false, showTime = true, } = options;
1151
1195
  const track = this.queue.currentTrack;
1152
1196
  const resource = this.currentResource;
1153
- if (!track || !resource)
1197
+ // Handle live stream
1198
+ if (this.isLive || !track || !resource) {
1199
+ if (this.isLive)
1200
+ return "🔴 LIVE";
1154
1201
  return "";
1202
+ }
1155
1203
  const total = track.duration > 1000 ? track.duration : track.duration * 1000;
1156
1204
  if (!total)
1157
- return this.formatTime(resource.playbackDuration);
1205
+ return this.formatTimeCompact(resource.playbackDuration);
1158
1206
  const current = resource.playbackDuration;
1159
- const ratio = Math.min(current / total, 1);
1207
+ const ratio = Math.min(Math.max(current / total, 0), 1);
1160
1208
  const progress = Math.round(ratio * size);
1161
- const bar = barChar.repeat(progress) + progressChar + barChar.repeat(size - progress);
1162
- return `${this.formatTime(current)} | ${bar} | ${this.formatTime(total)}`;
1209
+ // Build progress bar
1210
+ let bar = "";
1211
+ if (progressChar === "none" || options.hideProgressChar) {
1212
+ // Continuous bar without separator
1213
+ const filled = barChar.repeat(progress);
1214
+ const empty = barChar.repeat(size - progress);
1215
+ bar = filled + empty;
1216
+ }
1217
+ else {
1218
+ // Bar with progress character
1219
+ const filled = barChar.repeat(progress);
1220
+ const empty = barChar.repeat(Math.max(0, size - progress));
1221
+ bar = filled + progressChar + empty;
1222
+ }
1223
+ // Format time based on option
1224
+ const formatTimeFn = timeFormat === "compact" ? this.formatTimeCompact.bind(this) : this.formatTime.bind(this);
1225
+ const currentTimeStr = formatTimeFn(current);
1226
+ const totalTimeStr = formatTimeFn(total);
1227
+ // Build result
1228
+ let result = "";
1229
+ if (showTime) {
1230
+ result = `${currentTimeStr} ${bar} ${totalTimeStr}`;
1231
+ }
1232
+ else {
1233
+ result = bar;
1234
+ }
1235
+ // Add percentage if requested
1236
+ if (showPercentage) {
1237
+ const percent = Math.round(ratio * 100);
1238
+ result += ` (${percent}%)`;
1239
+ }
1240
+ return result;
1241
+ }
1242
+ /**
1243
+ * Format time with leading zeros (00:00 or 00:00:00)
1244
+ * @param ms - Time in milliseconds
1245
+ * @returns Formatted time string with leading zeros
1246
+ */
1247
+ formatTime(ms) {
1248
+ const totalSeconds = Math.floor(ms / 1000);
1249
+ const hours = Math.floor(totalSeconds / 3600);
1250
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
1251
+ const seconds = totalSeconds % 60;
1252
+ const parts = [];
1253
+ if (hours > 0)
1254
+ parts.push(String(hours).padStart(2, "0"));
1255
+ parts.push(String(minutes).padStart(2, "0"));
1256
+ parts.push(String(seconds).padStart(2, "0"));
1257
+ return parts.join(":");
1258
+ }
1259
+ /**
1260
+ * Format time without leading zeros for hours (1:22:12 or 3:45)
1261
+ * @param ms - Time in milliseconds
1262
+ * @returns Compact formatted time string
1263
+ */
1264
+ formatTimeCompact(ms) {
1265
+ const totalSeconds = Math.floor(ms / 1000);
1266
+ const hours = Math.floor(totalSeconds / 3600);
1267
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
1268
+ const seconds = totalSeconds % 60;
1269
+ if (hours > 0) {
1270
+ return `${hours}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
1271
+ }
1272
+ return `${minutes}:${String(seconds).padStart(2, "0")}`;
1163
1273
  }
1164
1274
  /**
1165
1275
  * Get the time of the current track
@@ -1168,44 +1278,44 @@ class Player extends events_1.EventEmitter {
1168
1278
  * @example
1169
1279
  * const time = player.getTime();
1170
1280
  * console.log(`Time: ${time.current}`);
1281
+ * console.log(`Formatted: ${time.formatted.current}`); // "1:22:12" or "3:45"
1171
1282
  */
1172
1283
  getTime() {
1284
+ if (this.isLive)
1285
+ return {
1286
+ current: 0,
1287
+ total: 0,
1288
+ format: "LIVE",
1289
+ formatted: {
1290
+ current: "LIVE",
1291
+ total: "LIVE",
1292
+ },
1293
+ };
1173
1294
  const resource = this.currentResource;
1174
1295
  const track = this.queue.currentTrack;
1175
- if (!track || !resource)
1296
+ if (!track || !resource) {
1176
1297
  return {
1177
1298
  current: 0,
1178
1299
  total: 0,
1179
1300
  format: "00:00",
1301
+ formatted: {
1302
+ current: "00:00",
1303
+ total: "00:00",
1304
+ },
1180
1305
  };
1306
+ }
1181
1307
  const total = track.duration > 1000 ? track.duration : track.duration * 1000;
1308
+ const current = resource.playbackDuration;
1182
1309
  return {
1183
- current: resource?.playbackDuration,
1310
+ current: current,
1184
1311
  total: total,
1185
- format: this.formatTime(resource.playbackDuration),
1312
+ format: this.formatTime(current),
1313
+ formatted: {
1314
+ current: this.formatTimeCompact(current),
1315
+ total: this.formatTimeCompact(total),
1316
+ },
1186
1317
  };
1187
1318
  }
1188
- /**
1189
- * Format the time in the format of HH:MM:SS
1190
- *
1191
- * @param {number} ms - The time in milliseconds
1192
- * @returns {string} The formatted time
1193
- * @example
1194
- * const formattedTime = player.formatTime(1000);
1195
- * console.log(`Formatted time: ${formattedTime}`);
1196
- */
1197
- formatTime(ms) {
1198
- const totalSeconds = Math.floor(ms / 1000);
1199
- const hours = Math.floor(totalSeconds / 3600);
1200
- const minutes = Math.floor((totalSeconds % 3600) / 60);
1201
- const seconds = totalSeconds % 60;
1202
- const parts = [];
1203
- if (hours > 0)
1204
- parts.push(String(hours).padStart(2, "0"));
1205
- parts.push(String(minutes).padStart(2, "0"));
1206
- parts.push(String(seconds).padStart(2, "0"));
1207
- return parts.join(":");
1208
- }
1209
1319
  /**
1210
1320
  * Destroy the player
1211
1321
  *
@@ -1215,6 +1325,9 @@ class Player extends events_1.EventEmitter {
1215
1325
  */
1216
1326
  destroy() {
1217
1327
  this.debug(`[Player] destroy called`);
1328
+ if (this.manager.getPersistence()) {
1329
+ this.manager.getPersistence()?.markPlayerDestroyed(this.guildId, "player_destroy_called");
1330
+ }
1218
1331
  if (this.leaveTimeout) {
1219
1332
  clearTimeout(this.leaveTimeout);
1220
1333
  this.leaveTimeout = null;
@@ -1255,7 +1368,7 @@ class Player extends events_1.EventEmitter {
1255
1368
  if (this.leaveTimeout) {
1256
1369
  clearTimeout(this.leaveTimeout);
1257
1370
  }
1258
- if (this.options.leaveOnEmpty && this.options.leaveTimeout) {
1371
+ if (this.options.leaveOnEnd && this.options.leaveTimeout) {
1259
1372
  this.leaveTimeout = setTimeout(() => {
1260
1373
  this.debug(`[Player] Leaving voice channel after timeoutMs`);
1261
1374
  this.destroy();
@@ -1269,13 +1382,16 @@ class Player extends events_1.EventEmitter {
1269
1382
  * @param {number} position - Position to seek to in milliseconds
1270
1383
  * @returns {Promise<boolean>}
1271
1384
  * @example
1272
- * const refreshed = await player.refeshPlayerResource(true, 1000);
1385
+ * const refreshed = await player.refreshPlayerResource(true, 1000);
1273
1386
  * console.log(`Refreshed: ${refreshed}`);
1274
1387
  */
1275
- async refeshPlayerResource(applyToCurrent = true, position = -1) {
1388
+ async refreshPlayerResource(applyToCurrent = true, position = -1) {
1276
1389
  if (!applyToCurrent || !this.queue.currentTrack || !(this.isPlaying || this.isPaused)) {
1277
1390
  return false;
1278
1391
  }
1392
+ if (this.refreshLock)
1393
+ return false;
1394
+ this.refreshLock = true;
1279
1395
  try {
1280
1396
  const track = this.queue.currentTrack;
1281
1397
  this.debug(`[Player] Refreshing player resource for track: ${track.title}`);
@@ -1302,7 +1418,10 @@ class Player extends events_1.EventEmitter {
1302
1418
  }
1303
1419
  }
1304
1420
  catch (error) {
1305
- this.debug(`[Player] Error destroying old stream in refeshPlayerResource:`, error);
1421
+ this.debug(`[Player] Error destroying old stream in refreshPlayerResource:`, error);
1422
+ }
1423
+ finally {
1424
+ this.refreshLock = false;
1306
1425
  }
1307
1426
  this.currentResource = resource;
1308
1427
  // Subscribe to new resource
@@ -1419,6 +1538,19 @@ class Player extends events_1.EventEmitter {
1419
1538
  }
1420
1539
  else if (newState.status === voice_1.AudioPlayerStatus.Buffering) {
1421
1540
  this.debug(`[Player] AudioPlayerStatus.Buffering`);
1541
+ this.lastDuration = this.currentResource?.playbackDuration || 0;
1542
+ this.stuckTimer = setTimeout(() => {
1543
+ if (this.currentResource?.playbackDuration === this.lastDuration) {
1544
+ this.emit("trackStuck", this.currentTrack);
1545
+ this.skip();
1546
+ }
1547
+ }, 10000);
1548
+ }
1549
+ else {
1550
+ if (this.stuckTimer) {
1551
+ clearTimeout(this.stuckTimer);
1552
+ this.stuckTimer = null;
1553
+ }
1422
1554
  }
1423
1555
  });
1424
1556
  this.audioPlayer.on("error", (error) => {
@@ -1440,6 +1572,113 @@ class Player extends events_1.EventEmitter {
1440
1572
  this.debug(`[Player] Removing plugin: ${name}`);
1441
1573
  return this.pluginManager.unregister(name);
1442
1574
  }
1575
+ /**
1576
+ * Save the current session of the player, including queue, current track, position, volume, loop mode, auto-play mode, and active extensions/plugins.
1577
+ *
1578
+ * @returns {PlayerSession} The saved session data
1579
+ */
1580
+ saveSession() {
1581
+ return {
1582
+ guildId: this.guildId,
1583
+ currentTrack: this.currentTrack,
1584
+ position: this.currentResource?.playbackDuration || null,
1585
+ volume: this.volume,
1586
+ queue: this.queue.getTracks(),
1587
+ loopMode: this.queue.loop(),
1588
+ autoPlay: this.queue.autoPlay(),
1589
+ extensions: this.extensionManager.getAll().map((ext) => ext.name),
1590
+ plugins: this.pluginManager.getAll().map((plugin) => plugin.name),
1591
+ };
1592
+ }
1593
+ /**
1594
+ * Set persistence manager for auto-save
1595
+ */
1596
+ setPersistenceManager(manager) {
1597
+ this.persistenceManager = manager;
1598
+ this.startAutoSaveTracking();
1599
+ }
1600
+ startAutoSaveTracking() {
1601
+ // Track state changes for auto-save
1602
+ const trackChanges = () => {
1603
+ this.scheduleAutoSave();
1604
+ };
1605
+ this.on("trackStart", trackChanges);
1606
+ this.on("trackEnd", trackChanges);
1607
+ this.on("queueAdd", trackChanges);
1608
+ this.on("queueRemove", trackChanges);
1609
+ this.on("volumeChange", trackChanges);
1610
+ // Save periodically
1611
+ setInterval(() => {
1612
+ this.saveIfNeeded();
1613
+ }, this.AUTO_SAVE_INTERVAL);
1614
+ }
1615
+ scheduleAutoSave() {
1616
+ if (!this.persistenceManager)
1617
+ return;
1618
+ this.lastSaveTime = Date.now();
1619
+ // Can implement debounced save here
1620
+ }
1621
+ async saveIfNeeded() {
1622
+ if (!this.persistenceManager)
1623
+ return;
1624
+ if (Date.now() - this.lastSaveTime < this.AUTO_SAVE_INTERVAL)
1625
+ return;
1626
+ await this.persistenceManager.savePlayer(this);
1627
+ this.lastSaveTime = Date.now();
1628
+ }
1629
+ /**
1630
+ * Save current player state
1631
+ */
1632
+ async savePlayer() {
1633
+ if (!this.persistenceManager) {
1634
+ this.debug("[Player] No persistence manager configured");
1635
+ return false;
1636
+ }
1637
+ return await this.persistenceManager.savePlayer(this);
1638
+ }
1639
+ /**
1640
+ * Get serializable state (for manual persistence)
1641
+ */
1642
+ getSerializableState() {
1643
+ return {
1644
+ guildId: this.guildId,
1645
+ queue: this.queue.getTracks(),
1646
+ currentTrack: this.currentTrack,
1647
+ volume: this.volume,
1648
+ isPlaying: this.isPlaying,
1649
+ isPaused: this.isPaused,
1650
+ loopMode: this.queue.loop(),
1651
+ autoPlay: this.queue.autoPlay(),
1652
+ filters: this.filter.getFilterString(),
1653
+ timestamp: Date.now(),
1654
+ };
1655
+ }
1656
+ /**
1657
+ * Restore from saved state
1658
+ */
1659
+ async restoreState(state) {
1660
+ try {
1661
+ if (state.volume)
1662
+ this.setVolume(state.volume);
1663
+ if (state.loopMode)
1664
+ this.queue.loop(state.loopMode);
1665
+ if (typeof state.autoPlay === "boolean")
1666
+ this.queue.autoPlay(state.autoPlay);
1667
+ if (state.filters)
1668
+ await this.filter.applyFilters(state.filters.split(","));
1669
+ // Restore queue
1670
+ if (state.queue && Array.isArray(state.queue)) {
1671
+ this.queue.clear();
1672
+ this.queue.addMultiple(state.queue);
1673
+ }
1674
+ this.debug("[Player] State restored");
1675
+ return true;
1676
+ }
1677
+ catch (error) {
1678
+ this.debug("[Player] Failed to restore state:", error);
1679
+ return false;
1680
+ }
1681
+ }
1443
1682
  //#endregion
1444
1683
  //#region Getters
1445
1684
  /**
@@ -1519,6 +1758,9 @@ class Player extends events_1.EventEmitter {
1519
1758
  get relatedTracks() {
1520
1759
  return this.queue.relatedTracks();
1521
1760
  }
1761
+ get isLive() {
1762
+ return this.currentTrack?.isLive === true;
1763
+ }
1522
1764
  }
1523
1765
  exports.Player = Player;
1524
1766
  //# sourceMappingURL=Player.js.map