ziplayer 0.2.6 → 0.2.7-dev.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AI-Guide.md +607 -0
- package/README.md +513 -196
- 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 +273 -146
- 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 +344 -91
- 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 +47 -46
- package/src/extensions/BaseExtension.ts +36 -35
- package/src/extensions/index.ts +473 -190
- package/src/index.ts +16 -16
- package/src/persistence/PersistenceManager.ts +572 -0
- package/src/plugins/BasePlugin.ts +27 -27
- package/src/plugins/index.ts +403 -236
- package/src/structures/FilterManager.ts +303 -303
- package/src/structures/Player.ts +1962 -1689
- package/src/structures/PlayerManager.ts +788 -416
- package/src/structures/Queue.ts +599 -354
- package/src/types/index.ts +406 -373
- package/src/types/persistence.ts +65 -0
- package/src/types/plugin.ts +1 -1
- package/src/utils/timeout.ts +10 -10
- package/tsconfig.json +22 -23
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.Player = void 0;
|
|
4
4
|
const events_1 = require("events");
|
|
5
5
|
const voice_1 = require("@discordjs/voice");
|
|
6
|
+
const lru_cache_1 = require("lru-cache");
|
|
6
7
|
const Queue_1 = require("./Queue");
|
|
7
8
|
const plugins_1 = require("../plugins");
|
|
8
9
|
const extensions_1 = require("../extensions");
|
|
@@ -54,12 +55,17 @@ class Player extends events_1.EventEmitter {
|
|
|
54
55
|
this.leaveTimeout = null;
|
|
55
56
|
this.currentResource = null;
|
|
56
57
|
this.volumeInterval = null;
|
|
58
|
+
this.stuckTimer = null;
|
|
57
59
|
this.skipLoop = false;
|
|
58
|
-
|
|
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
|
|
@@ -707,9 +746,21 @@ class Player extends events_1.EventEmitter {
|
|
|
707
746
|
});
|
|
708
747
|
await (0, voice_1.entersState)(connection, voice_1.VoiceConnectionStatus.Ready, 50_000);
|
|
709
748
|
this.connection = connection;
|
|
710
|
-
connection.on(voice_1.VoiceConnectionStatus.Disconnected, () => {
|
|
711
|
-
|
|
712
|
-
|
|
749
|
+
connection.on(voice_1.VoiceConnectionStatus.Disconnected, async () => {
|
|
750
|
+
try {
|
|
751
|
+
// move channel
|
|
752
|
+
await Promise.race([
|
|
753
|
+
(0, voice_1.entersState)(connection, voice_1.VoiceConnectionStatus.Signalling, 5_000),
|
|
754
|
+
(0, voice_1.entersState)(connection, voice_1.VoiceConnectionStatus.Connecting, 5_000),
|
|
755
|
+
]);
|
|
756
|
+
// Signalling/Connecting → reconnect
|
|
757
|
+
this.debug(`[Player] Reconnecting after channel move...`);
|
|
758
|
+
}
|
|
759
|
+
catch {
|
|
760
|
+
// no reconnect in 5 giây → disconnect
|
|
761
|
+
this.debug(`[Player] Truly disconnected, destroying player`);
|
|
762
|
+
this.destroy();
|
|
763
|
+
}
|
|
713
764
|
});
|
|
714
765
|
connection.on("error", (error) => {
|
|
715
766
|
this.debug(`[Player] Voice connection error:`, error);
|
|
@@ -757,7 +808,7 @@ class Player extends events_1.EventEmitter {
|
|
|
757
808
|
const track = this.queue.currentTrack;
|
|
758
809
|
if (track) {
|
|
759
810
|
this.debug(`[Player] Player resumed on track: ${track.title}`);
|
|
760
|
-
this.emit("playerResume", track);
|
|
811
|
+
// this.emit("playerResume", track); //đã có trong stateChange
|
|
761
812
|
}
|
|
762
813
|
}
|
|
763
814
|
return result;
|
|
@@ -776,6 +827,8 @@ class Player extends events_1.EventEmitter {
|
|
|
776
827
|
this.debug(`[Player] stop called`);
|
|
777
828
|
this.queue.clear();
|
|
778
829
|
const result = this.audioPlayer.stop();
|
|
830
|
+
this.destroyCurrentStream();
|
|
831
|
+
this.currentResource = null;
|
|
779
832
|
this.isPlaying = false;
|
|
780
833
|
this.isPaused = false;
|
|
781
834
|
this.emit("playerStop");
|
|
@@ -806,12 +859,7 @@ class Player extends events_1.EventEmitter {
|
|
|
806
859
|
this.debug(`[Player] Invalid seek position: ${position}ms (track duration: ${totalDuration}ms)`);
|
|
807
860
|
return false;
|
|
808
861
|
}
|
|
809
|
-
|
|
810
|
-
if (!streaminfo?.stream) {
|
|
811
|
-
this.debug(`[Player] No stream to seek`);
|
|
812
|
-
return false;
|
|
813
|
-
}
|
|
814
|
-
await this.refeshPlayerResource(true, position);
|
|
862
|
+
await this.refreshPlayerResource(true, position);
|
|
815
863
|
return true;
|
|
816
864
|
}
|
|
817
865
|
/**
|
|
@@ -917,7 +965,7 @@ class Player extends events_1.EventEmitter {
|
|
|
917
965
|
saveOptions = options;
|
|
918
966
|
}
|
|
919
967
|
try {
|
|
920
|
-
//
|
|
968
|
+
// Skip extension manager for saving - we want the raw stream without filters/seek applied, and extensions may not support this
|
|
921
969
|
let streamInfo = await this.pluginManager.getStream(track);
|
|
922
970
|
if (!streamInfo || !streamInfo.stream) {
|
|
923
971
|
throw new Error(`No save stream available for track: ${track.title}`);
|
|
@@ -1131,21 +1179,96 @@ class Player extends events_1.EventEmitter {
|
|
|
1131
1179
|
* @example
|
|
1132
1180
|
* const progressBar = player.getProgressBar();
|
|
1133
1181
|
* console.log(`Progress bar: ${progressBar}`);
|
|
1182
|
+
*
|
|
1183
|
+
* // Custom options
|
|
1184
|
+
* const customBar = player.getProgressBar({
|
|
1185
|
+
* size: 30,
|
|
1186
|
+
* barChar: "─",
|
|
1187
|
+
* progressChar: "●",
|
|
1188
|
+
* timeFormat: "compact" // "compact" = 1:22:12, "full" = 01:22:12
|
|
1189
|
+
* });
|
|
1134
1190
|
*/
|
|
1135
1191
|
getProgressBar(options = {}) {
|
|
1136
|
-
const { size = 20, barChar = "▬", progressChar = "🔘"
|
|
1192
|
+
const { size = 20, barChar = "▬", progressChar = "🔘", timeFormat = "compact", // "compact" or "full"
|
|
1193
|
+
showPercentage = false, showTime = true, } = options;
|
|
1137
1194
|
const track = this.queue.currentTrack;
|
|
1138
1195
|
const resource = this.currentResource;
|
|
1139
|
-
|
|
1196
|
+
// Handle live stream
|
|
1197
|
+
if (this.isLive || !track || !resource) {
|
|
1198
|
+
if (this.isLive)
|
|
1199
|
+
return "🔴 LIVE";
|
|
1140
1200
|
return "";
|
|
1201
|
+
}
|
|
1141
1202
|
const total = track.duration > 1000 ? track.duration : track.duration * 1000;
|
|
1142
1203
|
if (!total)
|
|
1143
|
-
return this.
|
|
1204
|
+
return this.formatTimeCompact(resource.playbackDuration);
|
|
1144
1205
|
const current = resource.playbackDuration;
|
|
1145
|
-
const ratio = Math.min(current / total, 1);
|
|
1206
|
+
const ratio = Math.min(Math.max(current / total, 0), 1);
|
|
1146
1207
|
const progress = Math.round(ratio * size);
|
|
1147
|
-
|
|
1148
|
-
|
|
1208
|
+
// Build progress bar
|
|
1209
|
+
let bar = "";
|
|
1210
|
+
if (progressChar === "none" || options.hideProgressChar) {
|
|
1211
|
+
// Continuous bar without separator
|
|
1212
|
+
const filled = barChar.repeat(progress);
|
|
1213
|
+
const empty = barChar.repeat(size - progress);
|
|
1214
|
+
bar = filled + empty;
|
|
1215
|
+
}
|
|
1216
|
+
else {
|
|
1217
|
+
// Bar with progress character
|
|
1218
|
+
const filled = barChar.repeat(progress);
|
|
1219
|
+
const empty = barChar.repeat(Math.max(0, size - progress));
|
|
1220
|
+
bar = filled + progressChar + empty;
|
|
1221
|
+
}
|
|
1222
|
+
// Format time based on option
|
|
1223
|
+
const formatTimeFn = timeFormat === "compact" ? this.formatTimeCompact.bind(this) : this.formatTime.bind(this);
|
|
1224
|
+
const currentTimeStr = formatTimeFn(current);
|
|
1225
|
+
const totalTimeStr = formatTimeFn(total);
|
|
1226
|
+
// Build result
|
|
1227
|
+
let result = "";
|
|
1228
|
+
if (showTime) {
|
|
1229
|
+
result = `${currentTimeStr} ${bar} ${totalTimeStr}`;
|
|
1230
|
+
}
|
|
1231
|
+
else {
|
|
1232
|
+
result = bar;
|
|
1233
|
+
}
|
|
1234
|
+
// Add percentage if requested
|
|
1235
|
+
if (showPercentage) {
|
|
1236
|
+
const percent = Math.round(ratio * 100);
|
|
1237
|
+
result += ` (${percent}%)`;
|
|
1238
|
+
}
|
|
1239
|
+
return result;
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* Format time with leading zeros (00:00 or 00:00:00)
|
|
1243
|
+
* @param ms - Time in milliseconds
|
|
1244
|
+
* @returns Formatted time string with leading zeros
|
|
1245
|
+
*/
|
|
1246
|
+
formatTime(ms) {
|
|
1247
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
1248
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
1249
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
1250
|
+
const seconds = totalSeconds % 60;
|
|
1251
|
+
const parts = [];
|
|
1252
|
+
if (hours > 0)
|
|
1253
|
+
parts.push(String(hours).padStart(2, "0"));
|
|
1254
|
+
parts.push(String(minutes).padStart(2, "0"));
|
|
1255
|
+
parts.push(String(seconds).padStart(2, "0"));
|
|
1256
|
+
return parts.join(":");
|
|
1257
|
+
}
|
|
1258
|
+
/**
|
|
1259
|
+
* Format time without leading zeros for hours (1:22:12 or 3:45)
|
|
1260
|
+
* @param ms - Time in milliseconds
|
|
1261
|
+
* @returns Compact formatted time string
|
|
1262
|
+
*/
|
|
1263
|
+
formatTimeCompact(ms) {
|
|
1264
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
1265
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
1266
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
1267
|
+
const seconds = totalSeconds % 60;
|
|
1268
|
+
if (hours > 0) {
|
|
1269
|
+
return `${hours}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
|
|
1270
|
+
}
|
|
1271
|
+
return `${minutes}:${String(seconds).padStart(2, "0")}`;
|
|
1149
1272
|
}
|
|
1150
1273
|
/**
|
|
1151
1274
|
* Get the time of the current track
|
|
@@ -1154,44 +1277,44 @@ class Player extends events_1.EventEmitter {
|
|
|
1154
1277
|
* @example
|
|
1155
1278
|
* const time = player.getTime();
|
|
1156
1279
|
* console.log(`Time: ${time.current}`);
|
|
1280
|
+
* console.log(`Formatted: ${time.formatted.current}`); // "1:22:12" or "3:45"
|
|
1157
1281
|
*/
|
|
1158
1282
|
getTime() {
|
|
1283
|
+
if (this.isLive)
|
|
1284
|
+
return {
|
|
1285
|
+
current: 0,
|
|
1286
|
+
total: 0,
|
|
1287
|
+
format: "LIVE",
|
|
1288
|
+
formatted: {
|
|
1289
|
+
current: "LIVE",
|
|
1290
|
+
total: "LIVE",
|
|
1291
|
+
},
|
|
1292
|
+
};
|
|
1159
1293
|
const resource = this.currentResource;
|
|
1160
1294
|
const track = this.queue.currentTrack;
|
|
1161
|
-
if (!track || !resource)
|
|
1295
|
+
if (!track || !resource) {
|
|
1162
1296
|
return {
|
|
1163
1297
|
current: 0,
|
|
1164
1298
|
total: 0,
|
|
1165
1299
|
format: "00:00",
|
|
1300
|
+
formatted: {
|
|
1301
|
+
current: "00:00",
|
|
1302
|
+
total: "00:00",
|
|
1303
|
+
},
|
|
1166
1304
|
};
|
|
1305
|
+
}
|
|
1167
1306
|
const total = track.duration > 1000 ? track.duration : track.duration * 1000;
|
|
1307
|
+
const current = resource.playbackDuration;
|
|
1168
1308
|
return {
|
|
1169
|
-
current:
|
|
1309
|
+
current: current,
|
|
1170
1310
|
total: total,
|
|
1171
|
-
format: this.formatTime(
|
|
1311
|
+
format: this.formatTime(current),
|
|
1312
|
+
formatted: {
|
|
1313
|
+
current: this.formatTimeCompact(current),
|
|
1314
|
+
total: this.formatTimeCompact(total),
|
|
1315
|
+
},
|
|
1172
1316
|
};
|
|
1173
1317
|
}
|
|
1174
|
-
/**
|
|
1175
|
-
* Format the time in the format of HH:MM:SS
|
|
1176
|
-
*
|
|
1177
|
-
* @param {number} ms - The time in milliseconds
|
|
1178
|
-
* @returns {string} The formatted time
|
|
1179
|
-
* @example
|
|
1180
|
-
* const formattedTime = player.formatTime(1000);
|
|
1181
|
-
* console.log(`Formatted time: ${formattedTime}`);
|
|
1182
|
-
*/
|
|
1183
|
-
formatTime(ms) {
|
|
1184
|
-
const totalSeconds = Math.floor(ms / 1000);
|
|
1185
|
-
const hours = Math.floor(totalSeconds / 3600);
|
|
1186
|
-
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
1187
|
-
const seconds = totalSeconds % 60;
|
|
1188
|
-
const parts = [];
|
|
1189
|
-
if (hours > 0)
|
|
1190
|
-
parts.push(String(hours).padStart(2, "0"));
|
|
1191
|
-
parts.push(String(minutes).padStart(2, "0"));
|
|
1192
|
-
parts.push(String(seconds).padStart(2, "0"));
|
|
1193
|
-
return parts.join(":");
|
|
1194
|
-
}
|
|
1195
1318
|
/**
|
|
1196
1319
|
* Destroy the player
|
|
1197
1320
|
*
|
|
@@ -1207,6 +1330,7 @@ class Player extends events_1.EventEmitter {
|
|
|
1207
1330
|
}
|
|
1208
1331
|
// Destroy current stream before stopping audio
|
|
1209
1332
|
this.destroyCurrentStream();
|
|
1333
|
+
this.audioPlayer.removeAllListeners();
|
|
1210
1334
|
this.audioPlayer.stop(true);
|
|
1211
1335
|
if (this.ttsPlayer) {
|
|
1212
1336
|
try {
|
|
@@ -1240,7 +1364,7 @@ class Player extends events_1.EventEmitter {
|
|
|
1240
1364
|
if (this.leaveTimeout) {
|
|
1241
1365
|
clearTimeout(this.leaveTimeout);
|
|
1242
1366
|
}
|
|
1243
|
-
if (this.options.
|
|
1367
|
+
if (this.options.leaveOnEnd && this.options.leaveTimeout) {
|
|
1244
1368
|
this.leaveTimeout = setTimeout(() => {
|
|
1245
1369
|
this.debug(`[Player] Leaving voice channel after timeoutMs`);
|
|
1246
1370
|
this.destroy();
|
|
@@ -1254,13 +1378,16 @@ class Player extends events_1.EventEmitter {
|
|
|
1254
1378
|
* @param {number} position - Position to seek to in milliseconds
|
|
1255
1379
|
* @returns {Promise<boolean>}
|
|
1256
1380
|
* @example
|
|
1257
|
-
* const refreshed = await player.
|
|
1381
|
+
* const refreshed = await player.refreshPlayerResource(true, 1000);
|
|
1258
1382
|
* console.log(`Refreshed: ${refreshed}`);
|
|
1259
1383
|
*/
|
|
1260
|
-
async
|
|
1384
|
+
async refreshPlayerResource(applyToCurrent = true, position = -1) {
|
|
1261
1385
|
if (!applyToCurrent || !this.queue.currentTrack || !(this.isPlaying || this.isPaused)) {
|
|
1262
1386
|
return false;
|
|
1263
1387
|
}
|
|
1388
|
+
if (this.refreshLock)
|
|
1389
|
+
return false;
|
|
1390
|
+
this.refreshLock = true;
|
|
1264
1391
|
try {
|
|
1265
1392
|
const track = this.queue.currentTrack;
|
|
1266
1393
|
this.debug(`[Player] Refreshing player resource for track: ${track.title}`);
|
|
@@ -1287,7 +1414,10 @@ class Player extends events_1.EventEmitter {
|
|
|
1287
1414
|
}
|
|
1288
1415
|
}
|
|
1289
1416
|
catch (error) {
|
|
1290
|
-
this.debug(`[Player] Error destroying old stream in
|
|
1417
|
+
this.debug(`[Player] Error destroying old stream in refreshPlayerResource:`, error);
|
|
1418
|
+
}
|
|
1419
|
+
finally {
|
|
1420
|
+
this.refreshLock = false;
|
|
1291
1421
|
}
|
|
1292
1422
|
this.currentResource = resource;
|
|
1293
1423
|
// Subscribe to new resource
|
|
@@ -1404,6 +1534,19 @@ class Player extends events_1.EventEmitter {
|
|
|
1404
1534
|
}
|
|
1405
1535
|
else if (newState.status === voice_1.AudioPlayerStatus.Buffering) {
|
|
1406
1536
|
this.debug(`[Player] AudioPlayerStatus.Buffering`);
|
|
1537
|
+
this.lastDuration = this.currentResource?.playbackDuration || 0;
|
|
1538
|
+
this.stuckTimer = setTimeout(() => {
|
|
1539
|
+
if (this.currentResource?.playbackDuration === this.lastDuration) {
|
|
1540
|
+
this.emit("trackStuck", this.currentTrack);
|
|
1541
|
+
this.skip();
|
|
1542
|
+
}
|
|
1543
|
+
}, 10000);
|
|
1544
|
+
}
|
|
1545
|
+
else {
|
|
1546
|
+
if (this.stuckTimer) {
|
|
1547
|
+
clearTimeout(this.stuckTimer);
|
|
1548
|
+
this.stuckTimer = null;
|
|
1549
|
+
}
|
|
1407
1550
|
}
|
|
1408
1551
|
});
|
|
1409
1552
|
this.audioPlayer.on("error", (error) => {
|
|
@@ -1425,6 +1568,113 @@ class Player extends events_1.EventEmitter {
|
|
|
1425
1568
|
this.debug(`[Player] Removing plugin: ${name}`);
|
|
1426
1569
|
return this.pluginManager.unregister(name);
|
|
1427
1570
|
}
|
|
1571
|
+
/**
|
|
1572
|
+
* Save the current session of the player, including queue, current track, position, volume, loop mode, auto-play mode, and active extensions/plugins.
|
|
1573
|
+
*
|
|
1574
|
+
* @returns {PlayerSession} The saved session data
|
|
1575
|
+
*/
|
|
1576
|
+
saveSession() {
|
|
1577
|
+
return {
|
|
1578
|
+
guildId: this.guildId,
|
|
1579
|
+
currentTrack: this.currentTrack,
|
|
1580
|
+
position: this.currentResource?.playbackDuration || null,
|
|
1581
|
+
volume: this.volume,
|
|
1582
|
+
queue: this.queue.getTracks(),
|
|
1583
|
+
loopMode: this.queue.loop(),
|
|
1584
|
+
autoPlay: this.queue.autoPlay(),
|
|
1585
|
+
extensions: this.extensionManager.getAll().map((ext) => ext.name),
|
|
1586
|
+
plugins: this.pluginManager.getAll().map((plugin) => plugin.name),
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
/**
|
|
1590
|
+
* Set persistence manager for auto-save
|
|
1591
|
+
*/
|
|
1592
|
+
setPersistenceManager(manager) {
|
|
1593
|
+
this.persistenceManager = manager;
|
|
1594
|
+
this.startAutoSaveTracking();
|
|
1595
|
+
}
|
|
1596
|
+
startAutoSaveTracking() {
|
|
1597
|
+
// Track state changes for auto-save
|
|
1598
|
+
const trackChanges = () => {
|
|
1599
|
+
this.scheduleAutoSave();
|
|
1600
|
+
};
|
|
1601
|
+
this.on("trackStart", trackChanges);
|
|
1602
|
+
this.on("trackEnd", trackChanges);
|
|
1603
|
+
this.on("queueAdd", trackChanges);
|
|
1604
|
+
this.on("queueRemove", trackChanges);
|
|
1605
|
+
this.on("volumeChange", trackChanges);
|
|
1606
|
+
// Save periodically
|
|
1607
|
+
setInterval(() => {
|
|
1608
|
+
this.saveIfNeeded();
|
|
1609
|
+
}, this.AUTO_SAVE_INTERVAL);
|
|
1610
|
+
}
|
|
1611
|
+
scheduleAutoSave() {
|
|
1612
|
+
if (!this.persistenceManager)
|
|
1613
|
+
return;
|
|
1614
|
+
this.lastSaveTime = Date.now();
|
|
1615
|
+
// Can implement debounced save here
|
|
1616
|
+
}
|
|
1617
|
+
async saveIfNeeded() {
|
|
1618
|
+
if (!this.persistenceManager)
|
|
1619
|
+
return;
|
|
1620
|
+
if (Date.now() - this.lastSaveTime < this.AUTO_SAVE_INTERVAL)
|
|
1621
|
+
return;
|
|
1622
|
+
await this.persistenceManager.savePlayer(this);
|
|
1623
|
+
this.lastSaveTime = Date.now();
|
|
1624
|
+
}
|
|
1625
|
+
/**
|
|
1626
|
+
* Save current player state
|
|
1627
|
+
*/
|
|
1628
|
+
async savePlayer() {
|
|
1629
|
+
if (!this.persistenceManager) {
|
|
1630
|
+
this.debug("[Player] No persistence manager configured");
|
|
1631
|
+
return false;
|
|
1632
|
+
}
|
|
1633
|
+
return await this.persistenceManager.savePlayer(this);
|
|
1634
|
+
}
|
|
1635
|
+
/**
|
|
1636
|
+
* Get serializable state (for manual persistence)
|
|
1637
|
+
*/
|
|
1638
|
+
getSerializableState() {
|
|
1639
|
+
return {
|
|
1640
|
+
guildId: this.guildId,
|
|
1641
|
+
queue: this.queue.getTracks(),
|
|
1642
|
+
currentTrack: this.currentTrack,
|
|
1643
|
+
volume: this.volume,
|
|
1644
|
+
isPlaying: this.isPlaying,
|
|
1645
|
+
isPaused: this.isPaused,
|
|
1646
|
+
loopMode: this.queue.loop(),
|
|
1647
|
+
autoPlay: this.queue.autoPlay(),
|
|
1648
|
+
filters: this.filter.getFilterString(),
|
|
1649
|
+
timestamp: Date.now(),
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
/**
|
|
1653
|
+
* Restore from saved state
|
|
1654
|
+
*/
|
|
1655
|
+
async restoreState(state) {
|
|
1656
|
+
try {
|
|
1657
|
+
if (state.volume)
|
|
1658
|
+
this.setVolume(state.volume);
|
|
1659
|
+
if (state.loopMode)
|
|
1660
|
+
this.queue.loop(state.loopMode);
|
|
1661
|
+
if (typeof state.autoPlay === "boolean")
|
|
1662
|
+
this.queue.autoPlay(state.autoPlay);
|
|
1663
|
+
if (state.filters)
|
|
1664
|
+
await this.filter.applyFilters(state.filters.split(","));
|
|
1665
|
+
// Restore queue
|
|
1666
|
+
if (state.queue && Array.isArray(state.queue)) {
|
|
1667
|
+
this.queue.clear();
|
|
1668
|
+
this.queue.addMultiple(state.queue);
|
|
1669
|
+
}
|
|
1670
|
+
this.debug("[Player] State restored");
|
|
1671
|
+
return true;
|
|
1672
|
+
}
|
|
1673
|
+
catch (error) {
|
|
1674
|
+
this.debug("[Player] Failed to restore state:", error);
|
|
1675
|
+
return false;
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1428
1678
|
//#endregion
|
|
1429
1679
|
//#region Getters
|
|
1430
1680
|
/**
|
|
@@ -1504,6 +1754,9 @@ class Player extends events_1.EventEmitter {
|
|
|
1504
1754
|
get relatedTracks() {
|
|
1505
1755
|
return this.queue.relatedTracks();
|
|
1506
1756
|
}
|
|
1757
|
+
get isLive() {
|
|
1758
|
+
return this.currentTrack?.isLive === true;
|
|
1759
|
+
}
|
|
1507
1760
|
}
|
|
1508
1761
|
exports.Player = Player;
|
|
1509
1762
|
//# sourceMappingURL=Player.js.map
|