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.
- package/AI-Guide.md +407 -756
- package/README.md +265 -11
- package/dist/extensions/BaseExtension.d.ts +1 -0
- package/dist/extensions/BaseExtension.d.ts.map +1 -1
- package/dist/extensions/BaseExtension.js.map +1 -1
- package/dist/extensions/index.d.ts +38 -3
- package/dist/extensions/index.d.ts.map +1 -1
- package/dist/extensions/index.js +259 -41
- package/dist/extensions/index.js.map +1 -1
- package/dist/persistence/PersistenceManager.d.ts +61 -0
- package/dist/persistence/PersistenceManager.d.ts.map +1 -0
- package/dist/persistence/PersistenceManager.js +551 -0
- package/dist/persistence/PersistenceManager.js.map +1 -0
- package/dist/plugins/BasePlugin.js +1 -1
- package/dist/plugins/BasePlugin.js.map +1 -1
- package/dist/plugins/index.d.ts +19 -4
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +204 -113
- package/dist/plugins/index.js.map +1 -1
- package/dist/structures/FilterManager.js +3 -3
- package/dist/structures/FilterManager.js.map +1 -1
- package/dist/structures/Player.d.ts +64 -14
- package/dist/structures/Player.d.ts.map +1 -1
- package/dist/structures/Player.js +326 -88
- package/dist/structures/Player.js.map +1 -1
- package/dist/structures/PlayerManager.d.ts +125 -91
- package/dist/structures/PlayerManager.d.ts.map +1 -1
- package/dist/structures/PlayerManager.js +406 -111
- package/dist/structures/PlayerManager.js.map +1 -1
- package/dist/structures/Queue.d.ts +136 -31
- package/dist/structures/Queue.d.ts.map +1 -1
- package/dist/structures/Queue.js +265 -46
- package/dist/structures/Queue.js.map +1 -1
- package/dist/types/index.d.ts +39 -6
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/persistence.d.ts +55 -0
- package/dist/types/persistence.d.ts.map +1 -0
- package/dist/types/persistence.js +3 -0
- package/dist/types/persistence.js.map +1 -0
- package/package.json +3 -2
- package/src/extensions/BaseExtension.ts +1 -0
- package/src/extensions/index.ts +320 -37
- package/src/persistence/PersistenceManager.ts +572 -0
- package/src/plugins/BasePlugin.ts +1 -1
- package/src/plugins/index.ts +248 -133
- package/src/structures/FilterManager.ts +3 -3
- package/src/structures/Player.ts +352 -94
- package/src/structures/PlayerManager.ts +488 -116
- package/src/structures/Queue.ts +300 -55
- package/src/types/index.ts +43 -10
- package/src/types/persistence.ts +65 -0
- 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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
229
|
-
|
|
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
|
|
256
|
-
const
|
|
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
|
|
267
|
-
cacheAge:
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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?.
|
|
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
|
-
|
|
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
|
-
//
|
|
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 = "🔘"
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
1162
|
-
|
|
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:
|
|
1309
|
+
current: current,
|
|
1184
1310
|
total: total,
|
|
1185
|
-
format: this.formatTime(
|
|
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.
|
|
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.
|
|
1381
|
+
* const refreshed = await player.refreshPlayerResource(true, 1000);
|
|
1273
1382
|
* console.log(`Refreshed: ${refreshed}`);
|
|
1274
1383
|
*/
|
|
1275
|
-
async
|
|
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
|
|
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
|