ziplayer 0.2.7-dev.0 → 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 (54) hide show
  1. package/AI-Guide.md +407 -756
  2. package/README.md +265 -11
  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 +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 +64 -14
  23. package/dist/structures/Player.d.ts.map +1 -1
  24. package/dist/structures/Player.js +326 -88
  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 +3 -2
  43. package/src/extensions/BaseExtension.ts +1 -0
  44. package/src/extensions/index.ts +320 -37
  45. package/src/persistence/PersistenceManager.ts +572 -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 +352 -94
  50. package/src/structures/PlayerManager.ts +488 -116
  51. package/src/structures/Queue.ts +300 -55
  52. package/src/types/index.ts +43 -10
  53. package/src/types/persistence.ts +65 -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");
@@ -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
@@ -769,7 +808,7 @@ class Player extends events_1.EventEmitter {
769
808
  const track = this.queue.currentTrack;
770
809
  if (track) {
771
810
  this.debug(`[Player] Player resumed on track: ${track.title}`);
772
- this.emit("playerResume", track);
811
+ // this.emit("playerResume", track); //đã có trong stateChange
773
812
  }
774
813
  }
775
814
  return result;
@@ -820,12 +859,7 @@ class Player extends events_1.EventEmitter {
820
859
  this.debug(`[Player] Invalid seek position: ${position}ms (track duration: ${totalDuration}ms)`);
821
860
  return false;
822
861
  }
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);
862
+ await this.refreshPlayerResource(true, position);
829
863
  return true;
830
864
  }
831
865
  /**
@@ -931,7 +965,7 @@ class Player extends events_1.EventEmitter {
931
965
  saveOptions = options;
932
966
  }
933
967
  try {
934
- // Try extensions first
968
+ // Skip extension manager for saving - we want the raw stream without filters/seek applied, and extensions may not support this
935
969
  let streamInfo = await this.pluginManager.getStream(track);
936
970
  if (!streamInfo || !streamInfo.stream) {
937
971
  throw new Error(`No save stream available for track: ${track.title}`);
@@ -1145,21 +1179,96 @@ class Player extends events_1.EventEmitter {
1145
1179
  * @example
1146
1180
  * const progressBar = player.getProgressBar();
1147
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
+ * });
1148
1190
  */
1149
1191
  getProgressBar(options = {}) {
1150
- const { size = 20, barChar = "▬", progressChar = "🔘" } = options;
1192
+ const { size = 20, barChar = "▬", progressChar = "🔘", timeFormat = "compact", // "compact" or "full"
1193
+ showPercentage = false, showTime = true, } = options;
1151
1194
  const track = this.queue.currentTrack;
1152
1195
  const resource = this.currentResource;
1153
- if (!track || !resource)
1196
+ // Handle live stream
1197
+ if (this.isLive || !track || !resource) {
1198
+ if (this.isLive)
1199
+ return "🔴 LIVE";
1154
1200
  return "";
1201
+ }
1155
1202
  const total = track.duration > 1000 ? track.duration : track.duration * 1000;
1156
1203
  if (!total)
1157
- return this.formatTime(resource.playbackDuration);
1204
+ return this.formatTimeCompact(resource.playbackDuration);
1158
1205
  const current = resource.playbackDuration;
1159
- const ratio = Math.min(current / total, 1);
1206
+ const ratio = Math.min(Math.max(current / total, 0), 1);
1160
1207
  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)}`;
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")}`;
1163
1272
  }
1164
1273
  /**
1165
1274
  * Get the time of the current track
@@ -1168,44 +1277,44 @@ class Player extends events_1.EventEmitter {
1168
1277
  * @example
1169
1278
  * const time = player.getTime();
1170
1279
  * console.log(`Time: ${time.current}`);
1280
+ * console.log(`Formatted: ${time.formatted.current}`); // "1:22:12" or "3:45"
1171
1281
  */
1172
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
+ };
1173
1293
  const resource = this.currentResource;
1174
1294
  const track = this.queue.currentTrack;
1175
- if (!track || !resource)
1295
+ if (!track || !resource) {
1176
1296
  return {
1177
1297
  current: 0,
1178
1298
  total: 0,
1179
1299
  format: "00:00",
1300
+ formatted: {
1301
+ current: "00:00",
1302
+ total: "00:00",
1303
+ },
1180
1304
  };
1305
+ }
1181
1306
  const total = track.duration > 1000 ? track.duration : track.duration * 1000;
1307
+ const current = resource.playbackDuration;
1182
1308
  return {
1183
- current: resource?.playbackDuration,
1309
+ current: current,
1184
1310
  total: total,
1185
- format: this.formatTime(resource.playbackDuration),
1311
+ format: this.formatTime(current),
1312
+ formatted: {
1313
+ current: this.formatTimeCompact(current),
1314
+ total: this.formatTimeCompact(total),
1315
+ },
1186
1316
  };
1187
1317
  }
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
1318
  /**
1210
1319
  * Destroy the player
1211
1320
  *
@@ -1255,7 +1364,7 @@ class Player extends events_1.EventEmitter {
1255
1364
  if (this.leaveTimeout) {
1256
1365
  clearTimeout(this.leaveTimeout);
1257
1366
  }
1258
- if (this.options.leaveOnEmpty && this.options.leaveTimeout) {
1367
+ if (this.options.leaveOnEnd && this.options.leaveTimeout) {
1259
1368
  this.leaveTimeout = setTimeout(() => {
1260
1369
  this.debug(`[Player] Leaving voice channel after timeoutMs`);
1261
1370
  this.destroy();
@@ -1269,13 +1378,16 @@ class Player extends events_1.EventEmitter {
1269
1378
  * @param {number} position - Position to seek to in milliseconds
1270
1379
  * @returns {Promise<boolean>}
1271
1380
  * @example
1272
- * const refreshed = await player.refeshPlayerResource(true, 1000);
1381
+ * const refreshed = await player.refreshPlayerResource(true, 1000);
1273
1382
  * console.log(`Refreshed: ${refreshed}`);
1274
1383
  */
1275
- async refeshPlayerResource(applyToCurrent = true, position = -1) {
1384
+ async refreshPlayerResource(applyToCurrent = true, position = -1) {
1276
1385
  if (!applyToCurrent || !this.queue.currentTrack || !(this.isPlaying || this.isPaused)) {
1277
1386
  return false;
1278
1387
  }
1388
+ if (this.refreshLock)
1389
+ return false;
1390
+ this.refreshLock = true;
1279
1391
  try {
1280
1392
  const track = this.queue.currentTrack;
1281
1393
  this.debug(`[Player] Refreshing player resource for track: ${track.title}`);
@@ -1302,7 +1414,10 @@ class Player extends events_1.EventEmitter {
1302
1414
  }
1303
1415
  }
1304
1416
  catch (error) {
1305
- 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;
1306
1421
  }
1307
1422
  this.currentResource = resource;
1308
1423
  // Subscribe to new resource
@@ -1419,6 +1534,19 @@ class Player extends events_1.EventEmitter {
1419
1534
  }
1420
1535
  else if (newState.status === voice_1.AudioPlayerStatus.Buffering) {
1421
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
+ }
1422
1550
  }
1423
1551
  });
1424
1552
  this.audioPlayer.on("error", (error) => {
@@ -1440,6 +1568,113 @@ class Player extends events_1.EventEmitter {
1440
1568
  this.debug(`[Player] Removing plugin: ${name}`);
1441
1569
  return this.pluginManager.unregister(name);
1442
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
+ }
1443
1678
  //#endregion
1444
1679
  //#region Getters
1445
1680
  /**
@@ -1519,6 +1754,9 @@ class Player extends events_1.EventEmitter {
1519
1754
  get relatedTracks() {
1520
1755
  return this.queue.relatedTracks();
1521
1756
  }
1757
+ get isLive() {
1758
+ return this.currentTrack?.isLive === true;
1759
+ }
1522
1760
  }
1523
1761
  exports.Player = Player;
1524
1762
  //# sourceMappingURL=Player.js.map