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.
- package/AI-Guide.md +407 -756
- package/README.md +275 -10
- 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 +95 -0
- package/dist/persistence/PersistenceManager.d.ts.map +1 -0
- package/dist/persistence/PersistenceManager.js +968 -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 +65 -14
- package/dist/structures/Player.d.ts.map +1 -1
- package/dist/structures/Player.js +330 -88
- package/dist/structures/Player.js.map +1 -1
- package/dist/structures/PlayerManager.d.ts +127 -91
- package/dist/structures/PlayerManager.d.ts.map +1 -1
- package/dist/structures/PlayerManager.js +437 -124
- 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 +46 -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 +74 -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 +1073 -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 +358 -94
- package/src/structures/PlayerManager.ts +535 -129
- package/src/structures/Queue.ts +300 -55
- package/src/types/index.ts +52 -10
- package/src/types/persistence.ts +83 -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");
|
|
@@ -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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
|
256
|
-
const
|
|
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
|
|
267
|
-
cacheAge:
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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?.
|
|
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
|
-
|
|
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
|
-
//
|
|
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 = "🔘"
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
1162
|
-
|
|
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:
|
|
1310
|
+
current: current,
|
|
1184
1311
|
total: total,
|
|
1185
|
-
format: this.formatTime(
|
|
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.
|
|
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.
|
|
1385
|
+
* const refreshed = await player.refreshPlayerResource(true, 1000);
|
|
1273
1386
|
* console.log(`Refreshed: ${refreshed}`);
|
|
1274
1387
|
*/
|
|
1275
|
-
async
|
|
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
|
|
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
|