magmastream 2.9.0-dev.34 → 2.9.0-dev.36
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/dist/index.d.ts +550 -91
- package/dist/index.js +6 -1
- package/dist/statestorage/JsonQueue.js +436 -0
- package/dist/{structures/Queue.js → statestorage/MemoryQueue.js} +87 -24
- package/dist/{structures → statestorage}/RedisQueue.js +127 -9
- package/dist/structures/Enums.js +2 -1
- package/dist/structures/Manager.js +287 -125
- package/dist/structures/Node.js +18 -10
- package/dist/structures/Player.js +49 -30
- package/dist/structures/Rest.js +2 -1
- package/dist/structures/Utils.js +5 -5
- package/dist/wrappers/seyfert.js +43 -0
- package/package.json +3 -3
|
@@ -13,8 +13,9 @@ const promises_1 = tslib_1.__importDefault(require("fs/promises"));
|
|
|
13
13
|
const path_1 = tslib_1.__importDefault(require("path"));
|
|
14
14
|
const ioredis_1 = tslib_1.__importDefault(require("ioredis"));
|
|
15
15
|
const Enums_1 = require("./Enums");
|
|
16
|
+
const package_json_1 = require("../../package.json");
|
|
16
17
|
/**
|
|
17
|
-
* The main hub for interacting with Lavalink and using Magmastream
|
|
18
|
+
* The main hub for interacting with Lavalink and using Magmastream.
|
|
18
19
|
*/
|
|
19
20
|
class Manager extends events_1.EventEmitter {
|
|
20
21
|
/** The map of players. */
|
|
@@ -58,23 +59,33 @@ class Manager extends events_1.EventEmitter {
|
|
|
58
59
|
if (options.send && !this._send)
|
|
59
60
|
this._send = options.send;
|
|
60
61
|
this.options = {
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
...options,
|
|
63
|
+
enabledPlugins: options.enabledPlugins ?? [],
|
|
64
|
+
nodes: options.nodes ?? [
|
|
63
65
|
{
|
|
64
|
-
identifier: "
|
|
65
|
-
host: "
|
|
66
|
+
identifier: "Cheap lavalink hosting @",
|
|
67
|
+
host: "https://blackforthosting.com/products?category=lavalink",
|
|
68
|
+
port: 443,
|
|
69
|
+
password: "Try BlackForHosting",
|
|
70
|
+
useSSL: true,
|
|
66
71
|
enableSessionResumeOption: false,
|
|
67
72
|
sessionTimeoutMs: 1000,
|
|
73
|
+
nodePriority: 69,
|
|
68
74
|
},
|
|
69
75
|
],
|
|
70
|
-
playNextOnEnd: true,
|
|
71
|
-
enablePriorityMode: false,
|
|
72
|
-
clientName:
|
|
73
|
-
defaultSearchPlatform: Enums_1.SearchPlatform.YouTube,
|
|
74
|
-
useNode: Enums_1.UseNodeOptions.LeastPlayers,
|
|
76
|
+
playNextOnEnd: options.playNextOnEnd ?? true,
|
|
77
|
+
enablePriorityMode: options.enablePriorityMode ?? false,
|
|
78
|
+
clientName: options.clientName ?? `Magmastream@${package_json_1.version}`,
|
|
79
|
+
defaultSearchPlatform: options.defaultSearchPlatform ?? Enums_1.SearchPlatform.YouTube,
|
|
80
|
+
useNode: options.useNode ?? Enums_1.UseNodeOptions.LeastPlayers,
|
|
75
81
|
maxPreviousTracks: options.maxPreviousTracks ?? 20,
|
|
76
|
-
|
|
77
|
-
|
|
82
|
+
normalizeYouTubeTitles: options.normalizeYouTubeTitles ?? false,
|
|
83
|
+
stateStorage: {
|
|
84
|
+
...options.stateStorage,
|
|
85
|
+
type: options.stateStorage?.type ?? Enums_1.StateStorageType.Memory,
|
|
86
|
+
deleteInactivePlayers: options.stateStorage?.deleteInactivePlayers ?? true,
|
|
87
|
+
},
|
|
88
|
+
autoPlaySearchPlatforms: options.autoPlaySearchPlatforms ?? [Enums_1.AutoPlayPlatform.YouTube],
|
|
78
89
|
};
|
|
79
90
|
Utils_1.AutoPlayUtils.init(this);
|
|
80
91
|
if (this.options.nodes) {
|
|
@@ -92,7 +103,7 @@ class Manager extends events_1.EventEmitter {
|
|
|
92
103
|
}, 2000);
|
|
93
104
|
}
|
|
94
105
|
catch (error) {
|
|
95
|
-
console.error(
|
|
106
|
+
console.error(`[MANAGER] Error during shutdown: ${error}`);
|
|
96
107
|
process.exit(1);
|
|
97
108
|
}
|
|
98
109
|
});
|
|
@@ -104,7 +115,7 @@ class Manager extends events_1.EventEmitter {
|
|
|
104
115
|
process.exit(0);
|
|
105
116
|
}
|
|
106
117
|
catch (error) {
|
|
107
|
-
console.error(
|
|
118
|
+
console.error(`[MANAGER] Error during SIGTERM shutdown: ${error}`);
|
|
108
119
|
process.exit(1);
|
|
109
120
|
}
|
|
110
121
|
});
|
|
@@ -127,7 +138,7 @@ class Manager extends events_1.EventEmitter {
|
|
|
127
138
|
this.options.clientId = clientId;
|
|
128
139
|
}
|
|
129
140
|
if (typeof clusterId !== "number") {
|
|
130
|
-
console.warn(
|
|
141
|
+
console.warn(`[MANAGER] "clusterId" is not a valid number, defaulting to 0.`);
|
|
131
142
|
this.options.clusterId = 0;
|
|
132
143
|
}
|
|
133
144
|
else {
|
|
@@ -148,7 +159,7 @@ class Manager extends events_1.EventEmitter {
|
|
|
148
159
|
plugin.load(this);
|
|
149
160
|
}
|
|
150
161
|
}
|
|
151
|
-
if (this.options.stateStorage
|
|
162
|
+
if (this.options.stateStorage.type === Enums_1.StateStorageType.Redis) {
|
|
152
163
|
const config = this.options.stateStorage.redisConfig;
|
|
153
164
|
this.redis = new ioredis_1.default({
|
|
154
165
|
host: config.host,
|
|
@@ -278,7 +289,6 @@ class Manager extends events_1.EventEmitter {
|
|
|
278
289
|
if (!player)
|
|
279
290
|
return;
|
|
280
291
|
await player.destroy();
|
|
281
|
-
await this.cleanupInactivePlayers();
|
|
282
292
|
}
|
|
283
293
|
/**
|
|
284
294
|
* Creates a new node or returns an existing one if it already exists.
|
|
@@ -385,21 +395,22 @@ class Manager extends events_1.EventEmitter {
|
|
|
385
395
|
*/
|
|
386
396
|
async savePlayerState(guildId) {
|
|
387
397
|
switch (this.options.stateStorage.type) {
|
|
388
|
-
case Enums_1.StateStorageType.
|
|
398
|
+
case Enums_1.StateStorageType.Memory:
|
|
399
|
+
case Enums_1.StateStorageType.JSON:
|
|
389
400
|
{
|
|
390
401
|
try {
|
|
391
402
|
const playerStateFilePath = await this.getPlayerFilePath(guildId);
|
|
392
403
|
const player = this.getPlayer(guildId);
|
|
393
404
|
if (!player || player.state === Enums_1.StateTypes.Disconnected || !player.voiceChannelId) {
|
|
394
|
-
console.warn(`Skipping save for inactive player: ${guildId}`);
|
|
405
|
+
console.warn(`[MANAGER] Skipping save for inactive player: ${guildId}`);
|
|
395
406
|
return;
|
|
396
407
|
}
|
|
397
|
-
const serializedPlayer = this.serializePlayer(player);
|
|
408
|
+
const serializedPlayer = await this.serializePlayer(player);
|
|
398
409
|
await promises_1.default.writeFile(playerStateFilePath, JSON.stringify(serializedPlayer, null, 2), "utf-8");
|
|
399
410
|
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Player state saved: ${guildId}`);
|
|
400
411
|
}
|
|
401
412
|
catch (error) {
|
|
402
|
-
console.error(`Error saving player state for guild ${guildId}:`, error);
|
|
413
|
+
console.error(`[MANAGER] Error saving player state for guild ${guildId}:`, error);
|
|
403
414
|
}
|
|
404
415
|
}
|
|
405
416
|
break;
|
|
@@ -408,10 +419,10 @@ class Manager extends events_1.EventEmitter {
|
|
|
408
419
|
try {
|
|
409
420
|
const player = this.getPlayer(guildId);
|
|
410
421
|
if (!player || player.state === Enums_1.StateTypes.Disconnected || !player.voiceChannelId) {
|
|
411
|
-
console.warn(`Skipping save for inactive player: ${guildId}`);
|
|
422
|
+
console.warn(`[MANAGER] Skipping save for inactive player: ${guildId}`);
|
|
412
423
|
return;
|
|
413
424
|
}
|
|
414
|
-
const serializedPlayer = this.serializePlayer(player);
|
|
425
|
+
const serializedPlayer = await this.serializePlayer(player);
|
|
415
426
|
const redisKey = `${this.options.stateStorage.redisConfig.prefix?.endsWith(":")
|
|
416
427
|
? this.options.stateStorage.redisConfig.prefix
|
|
417
428
|
: this.options.stateStorage.redisConfig.prefix ?? "magmastream:"}playerstore:${guildId}`;
|
|
@@ -419,7 +430,7 @@ class Manager extends events_1.EventEmitter {
|
|
|
419
430
|
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Player state saved to Redis: ${guildId}`);
|
|
420
431
|
}
|
|
421
432
|
catch (error) {
|
|
422
|
-
console.error(`Error saving player state to Redis for guild ${guildId}:`, error);
|
|
433
|
+
console.error(`[MANAGER] Error saving player state to Redis for guild ${guildId}:`, error);
|
|
423
434
|
}
|
|
424
435
|
}
|
|
425
436
|
break;
|
|
@@ -447,9 +458,9 @@ class Manager extends events_1.EventEmitter {
|
|
|
447
458
|
throw new Error(`Could not find node: ${nodeId}`);
|
|
448
459
|
const info = (await node.rest.getAllPlayers());
|
|
449
460
|
switch (this.options.stateStorage.type) {
|
|
450
|
-
case Enums_1.StateStorageType.
|
|
461
|
+
case Enums_1.StateStorageType.JSON:
|
|
451
462
|
{
|
|
452
|
-
const playerStatesDir = path_1.default.join(process.cwd(), "magmastream", "dist", "sessionData", "
|
|
463
|
+
const playerStatesDir = path_1.default.join(process.cwd(), "magmastream", "dist", "sessionData", "playerStore");
|
|
453
464
|
try {
|
|
454
465
|
// Check if the directory exists, and create it if it doesn't
|
|
455
466
|
await promises_1.default.access(playerStatesDir).catch(async () => {
|
|
@@ -491,39 +502,72 @@ class Manager extends events_1.EventEmitter {
|
|
|
491
502
|
const tracks = [];
|
|
492
503
|
const currentTrack = state.queue.current;
|
|
493
504
|
const queueTracks = state.queue.tracks;
|
|
494
|
-
if (
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
505
|
+
if (state.isAutoplay) {
|
|
506
|
+
Object.setPrototypeOf(state.data.clientUser, { constructor: { name: "User" } });
|
|
507
|
+
player.setAutoplay(true, state.data.clientUser, state.autoplayTries);
|
|
508
|
+
}
|
|
509
|
+
if (lavaPlayer?.track) {
|
|
510
|
+
// If lavaPlayer has a track, push all queue tracks
|
|
511
|
+
tracks.push(...queueTracks);
|
|
512
|
+
// Set current track if matches lavaPlayer's track URI
|
|
513
|
+
if (currentTrack && currentTrack.uri === lavaPlayer.track.info.uri) {
|
|
514
|
+
await player.queue.setCurrent(Utils_1.TrackUtils.build(lavaPlayer.track, currentTrack.requester));
|
|
500
515
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
};
|
|
506
|
-
await node.queueEnd(player, currentTrack, payload);
|
|
507
|
-
}
|
|
508
|
-
else {
|
|
509
|
-
tracks.push(currentTrack, ...queueTracks);
|
|
510
|
-
}
|
|
516
|
+
// Add tracks to queue
|
|
517
|
+
if (tracks.length > 0) {
|
|
518
|
+
await player.queue.clear();
|
|
519
|
+
await player.queue.add(tracks);
|
|
511
520
|
}
|
|
512
521
|
}
|
|
513
522
|
else {
|
|
514
|
-
|
|
515
|
-
|
|
523
|
+
// LavaPlayer missing track or lavaPlayer is falsy
|
|
524
|
+
if (currentTrack) {
|
|
525
|
+
if (queueTracks.length > 0) {
|
|
526
|
+
tracks.push(...queueTracks);
|
|
527
|
+
await player.queue.clear();
|
|
528
|
+
await player.queue.add(tracks);
|
|
529
|
+
}
|
|
530
|
+
await node.trackEnd(player, currentTrack, {
|
|
516
531
|
reason: Enums_1.TrackEndReasonTypes.Finished,
|
|
517
|
-
|
|
518
|
-
|
|
532
|
+
type: "TrackEndEvent",
|
|
533
|
+
});
|
|
519
534
|
}
|
|
520
535
|
else {
|
|
521
|
-
|
|
536
|
+
// No current track, check previous queue for last track
|
|
537
|
+
const previousQueue = await player.queue.getPrevious();
|
|
538
|
+
const lastTrack = previousQueue?.at(-1);
|
|
539
|
+
if (lastTrack) {
|
|
540
|
+
if (queueTracks.length === 0) {
|
|
541
|
+
// If no tracks in queue, end last track
|
|
542
|
+
await node.trackEnd(player, lastTrack, {
|
|
543
|
+
reason: Enums_1.TrackEndReasonTypes.Finished,
|
|
544
|
+
type: "TrackEndEvent",
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
// If there are queued tracks, add them
|
|
549
|
+
tracks.push(...queueTracks);
|
|
550
|
+
if (tracks.length > 0) {
|
|
551
|
+
await player.queue.clear();
|
|
552
|
+
await player.queue.add(tracks);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
if (queueTracks.length > 0) {
|
|
558
|
+
tracks.push(...queueTracks);
|
|
559
|
+
if (tracks.length > 0) {
|
|
560
|
+
await player.queue.clear();
|
|
561
|
+
await player.queue.add(tracks);
|
|
562
|
+
}
|
|
563
|
+
await node.trackEnd(player, lastTrack, {
|
|
564
|
+
reason: Enums_1.TrackEndReasonTypes.Finished,
|
|
565
|
+
type: "TrackEndEvent",
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
}
|
|
522
569
|
}
|
|
523
570
|
}
|
|
524
|
-
if (tracks.length > 0) {
|
|
525
|
-
await player.queue.add(tracks);
|
|
526
|
-
}
|
|
527
571
|
if (state.queue.previous.length > 0) {
|
|
528
572
|
await player.queue.addPrevious(state.queue.previous);
|
|
529
573
|
}
|
|
@@ -543,10 +587,6 @@ class Manager extends events_1.EventEmitter {
|
|
|
543
587
|
if (state.dynamicRepeat) {
|
|
544
588
|
player.setDynamicRepeat(state.dynamicRepeat, state.dynamicLoopInterval._idleTimeout);
|
|
545
589
|
}
|
|
546
|
-
if (state.isAutoplay) {
|
|
547
|
-
Object.setPrototypeOf(state.data.clientUser, { constructor: { name: "User" } });
|
|
548
|
-
player.setAutoplay(true, state.data.clientUser, state.autoplayTries);
|
|
549
|
-
}
|
|
550
590
|
if (state.data) {
|
|
551
591
|
for (const [name, value] of Object.entries(state.data)) {
|
|
552
592
|
player.set(name, value);
|
|
@@ -661,39 +701,72 @@ class Manager extends events_1.EventEmitter {
|
|
|
661
701
|
const tracks = [];
|
|
662
702
|
const currentTrack = state.queue.current;
|
|
663
703
|
const queueTracks = state.queue.tracks;
|
|
664
|
-
if (
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
704
|
+
if (state.isAutoplay) {
|
|
705
|
+
Object.setPrototypeOf(state.data.clientUser, { constructor: { name: "User" } });
|
|
706
|
+
player.setAutoplay(true, state.data.clientUser, state.autoplayTries);
|
|
707
|
+
}
|
|
708
|
+
if (lavaPlayer?.track) {
|
|
709
|
+
// If lavaPlayer has a track, push all queue tracks
|
|
710
|
+
tracks.push(...queueTracks);
|
|
711
|
+
// Set current track if matches lavaPlayer's track URI
|
|
712
|
+
if (currentTrack && currentTrack.uri === lavaPlayer.track.info.uri) {
|
|
713
|
+
await player.queue.setCurrent(Utils_1.TrackUtils.build(lavaPlayer.track, currentTrack.requester));
|
|
670
714
|
}
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
};
|
|
676
|
-
await node.queueEnd(player, currentTrack, payload);
|
|
677
|
-
}
|
|
678
|
-
else {
|
|
679
|
-
tracks.push(currentTrack, ...queueTracks);
|
|
680
|
-
}
|
|
715
|
+
// Add tracks to queue
|
|
716
|
+
if (tracks.length > 0) {
|
|
717
|
+
await player.queue.clear();
|
|
718
|
+
await player.queue.add(tracks);
|
|
681
719
|
}
|
|
682
720
|
}
|
|
683
721
|
else {
|
|
684
|
-
|
|
685
|
-
|
|
722
|
+
// LavaPlayer missing track or lavaPlayer is falsy
|
|
723
|
+
if (currentTrack) {
|
|
724
|
+
if (queueTracks.length > 0) {
|
|
725
|
+
tracks.push(...queueTracks);
|
|
726
|
+
await player.queue.clear();
|
|
727
|
+
await player.queue.add(tracks);
|
|
728
|
+
}
|
|
729
|
+
await node.trackEnd(player, currentTrack, {
|
|
686
730
|
reason: Enums_1.TrackEndReasonTypes.Finished,
|
|
687
|
-
|
|
688
|
-
|
|
731
|
+
type: "TrackEndEvent",
|
|
732
|
+
});
|
|
689
733
|
}
|
|
690
734
|
else {
|
|
691
|
-
|
|
735
|
+
// No current track, check previous queue for last track
|
|
736
|
+
const previousQueue = await player.queue.getPrevious();
|
|
737
|
+
const lastTrack = previousQueue?.at(-1);
|
|
738
|
+
if (lastTrack) {
|
|
739
|
+
if (queueTracks.length === 0) {
|
|
740
|
+
// If no tracks in queue, end last track
|
|
741
|
+
await node.trackEnd(player, lastTrack, {
|
|
742
|
+
reason: Enums_1.TrackEndReasonTypes.Finished,
|
|
743
|
+
type: "TrackEndEvent",
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
else {
|
|
747
|
+
// If there are queued tracks, add them
|
|
748
|
+
tracks.push(...queueTracks);
|
|
749
|
+
if (tracks.length > 0) {
|
|
750
|
+
await player.queue.clear();
|
|
751
|
+
await player.queue.add(tracks);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
else {
|
|
756
|
+
if (queueTracks.length > 0) {
|
|
757
|
+
tracks.push(...queueTracks);
|
|
758
|
+
if (tracks.length > 0) {
|
|
759
|
+
await player.queue.clear();
|
|
760
|
+
await player.queue.add(tracks);
|
|
761
|
+
}
|
|
762
|
+
await node.trackEnd(player, lastTrack, {
|
|
763
|
+
reason: Enums_1.TrackEndReasonTypes.Finished,
|
|
764
|
+
type: "TrackEndEvent",
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
}
|
|
692
768
|
}
|
|
693
769
|
}
|
|
694
|
-
if (tracks.length > 0) {
|
|
695
|
-
await player.queue.add(tracks);
|
|
696
|
-
}
|
|
697
770
|
if (state.queue.previous.length > 0) {
|
|
698
771
|
await player.queue.addPrevious(state.queue.previous);
|
|
699
772
|
}
|
|
@@ -713,10 +786,6 @@ class Manager extends events_1.EventEmitter {
|
|
|
713
786
|
if (state.dynamicRepeat) {
|
|
714
787
|
player.setDynamicRepeat(state.dynamicRepeat, state.dynamicLoopInterval._idleTimeout);
|
|
715
788
|
}
|
|
716
|
-
if (state.isAutoplay) {
|
|
717
|
-
Object.setPrototypeOf(state.data.clientUser, { constructor: { name: "User" } });
|
|
718
|
-
player.setAutoplay(true, state.data.clientUser, state.autoplayTries);
|
|
719
|
-
}
|
|
720
789
|
if (state.data) {
|
|
721
790
|
for (const [name, value] of Object.entries(state.data)) {
|
|
722
791
|
player.set(name, value);
|
|
@@ -804,26 +873,25 @@ class Manager extends events_1.EventEmitter {
|
|
|
804
873
|
async handleShutdown() {
|
|
805
874
|
console.warn("\x1b[31m%s\x1b[0m", "MAGMASTREAM WARNING: Shutting down! Please wait, saving active players...");
|
|
806
875
|
try {
|
|
807
|
-
|
|
808
|
-
await this.clearAllPlayerStates();
|
|
809
|
-
}
|
|
876
|
+
await this.clearAllStoredPlayers();
|
|
810
877
|
const savePromises = Array.from(this.players.keys()).map(async (guildId) => {
|
|
811
878
|
try {
|
|
812
879
|
await this.savePlayerState(guildId);
|
|
813
880
|
}
|
|
814
881
|
catch (error) {
|
|
815
|
-
console.error(`Error saving player state for guild ${guildId}:`, error);
|
|
882
|
+
console.error(`[MANAGER] Error saving player state for guild ${guildId}:`, error);
|
|
816
883
|
}
|
|
817
884
|
});
|
|
885
|
+
if (this.options.stateStorage.deleteInactivePlayers)
|
|
886
|
+
await this.cleanupInactivePlayers();
|
|
818
887
|
await Promise.allSettled(savePromises);
|
|
819
|
-
await this.cleanupInactivePlayers();
|
|
820
888
|
setTimeout(() => {
|
|
821
889
|
console.warn("\x1b[32m%s\x1b[0m", "MAGMASTREAM INFO: Shutting down complete, exiting...");
|
|
822
890
|
process.exit(0);
|
|
823
891
|
}, 500);
|
|
824
892
|
}
|
|
825
893
|
catch (error) {
|
|
826
|
-
console.error(
|
|
894
|
+
console.error(`[MANAGER] Unexpected error during shutdown:`, error);
|
|
827
895
|
process.exit(1);
|
|
828
896
|
}
|
|
829
897
|
}
|
|
@@ -973,13 +1041,13 @@ class Manager extends events_1.EventEmitter {
|
|
|
973
1041
|
* @returns {string} The path to the player's JSON file
|
|
974
1042
|
*/
|
|
975
1043
|
async getPlayerFilePath(guildId) {
|
|
976
|
-
const configDir = path_1.default.join(process.cwd(), "magmastream", "dist", "sessionData", "
|
|
1044
|
+
const configDir = path_1.default.join(process.cwd(), "magmastream", "dist", "sessionData", "playerStore");
|
|
977
1045
|
try {
|
|
978
1046
|
await promises_1.default.mkdir(configDir, { recursive: true });
|
|
979
1047
|
return path_1.default.join(configDir, `${guildId}.json`);
|
|
980
1048
|
}
|
|
981
1049
|
catch (err) {
|
|
982
|
-
console.error(
|
|
1050
|
+
console.error(`[MANAGER] Error ensuring player data directory exists: ${err}`);
|
|
983
1051
|
throw new Error(`Failed to resolve player file path for guild ${guildId}`);
|
|
984
1052
|
}
|
|
985
1053
|
}
|
|
@@ -988,8 +1056,12 @@ class Manager extends events_1.EventEmitter {
|
|
|
988
1056
|
* @param player The Player instance to serialize
|
|
989
1057
|
* @returns The serialized Player instance
|
|
990
1058
|
*/
|
|
991
|
-
serializePlayer(player) {
|
|
1059
|
+
async serializePlayer(player) {
|
|
992
1060
|
const seen = new WeakSet();
|
|
1061
|
+
// Fetch async queue data once before serializing
|
|
1062
|
+
const current = await player.queue.getCurrent();
|
|
1063
|
+
const tracks = Array.isArray(await player.queue.getTracks()) ? await player.queue.getTracks() : [];
|
|
1064
|
+
const previous = Array.isArray(await player.queue.getPrevious()) ? await player.queue.getPrevious() : [];
|
|
993
1065
|
/**
|
|
994
1066
|
* Recursively serializes an object, avoiding circular references.
|
|
995
1067
|
* @param obj The object to serialize
|
|
@@ -1025,9 +1097,9 @@ class Manager extends events_1.EventEmitter {
|
|
|
1025
1097
|
}
|
|
1026
1098
|
if (key === "queue") {
|
|
1027
1099
|
return {
|
|
1028
|
-
current
|
|
1029
|
-
tracks
|
|
1030
|
-
previous
|
|
1100
|
+
current,
|
|
1101
|
+
tracks,
|
|
1102
|
+
previous,
|
|
1031
1103
|
};
|
|
1032
1104
|
}
|
|
1033
1105
|
if (key === "data") {
|
|
@@ -1039,45 +1111,135 @@ class Manager extends events_1.EventEmitter {
|
|
|
1039
1111
|
}));
|
|
1040
1112
|
}
|
|
1041
1113
|
/**
|
|
1042
|
-
*
|
|
1114
|
+
* Cleans up inactive players by removing their state files from the file system.
|
|
1043
1115
|
* This is done to prevent stale state files from accumulating on the file system.
|
|
1044
1116
|
*/
|
|
1045
1117
|
async cleanupInactivePlayers() {
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1118
|
+
switch (this.options.stateStorage.type) {
|
|
1119
|
+
case Enums_1.StateStorageType.JSON:
|
|
1120
|
+
{
|
|
1121
|
+
const playersStoreDir = path_1.default.join(process.cwd(), "magmastream", "dist", "sessionData", "playersStore");
|
|
1122
|
+
const playersDataDir = this.options.stateStorage?.jsonConfig?.path ?? path_1.default.join(process.cwd(), "magmastream", "dist", "sessionData", "players");
|
|
1123
|
+
try {
|
|
1124
|
+
await promises_1.default.mkdir(playersStoreDir, { recursive: true });
|
|
1125
|
+
await promises_1.default.mkdir(playersDataDir, { recursive: true });
|
|
1126
|
+
const activeGuildIds = new Set(this.players.keys());
|
|
1127
|
+
// Clean up playersStore/*.json
|
|
1128
|
+
const playerStateFiles = await promises_1.default.readdir(playersStoreDir);
|
|
1129
|
+
for (const file of playerStateFiles) {
|
|
1130
|
+
const guildId = path_1.default.basename(file, ".json");
|
|
1131
|
+
if (!activeGuildIds.has(guildId)) {
|
|
1132
|
+
const filePath = path_1.default.join(playersStoreDir, file);
|
|
1133
|
+
await promises_1.default.unlink(filePath);
|
|
1134
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Deleted inactive player state: ${guildId}`);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
// Clean up players/<guildId>/ folders
|
|
1138
|
+
const guildDirs = await promises_1.default.readdir(playersDataDir, { withFileTypes: true });
|
|
1139
|
+
for (const dirent of guildDirs) {
|
|
1140
|
+
if (!dirent.isDirectory())
|
|
1141
|
+
continue;
|
|
1142
|
+
const guildId = dirent.name;
|
|
1143
|
+
if (!activeGuildIds.has(guildId)) {
|
|
1144
|
+
const guildPath = path_1.default.join(playersDataDir, guildId);
|
|
1145
|
+
await promises_1.default.rm(guildPath, { recursive: true, force: true });
|
|
1146
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Deleted inactive player data folder: ${guildId}`);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
catch (error) {
|
|
1151
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Error cleaning up inactive JSON players: ${error}`);
|
|
1152
|
+
}
|
|
1153
|
+
return;
|
|
1066
1154
|
}
|
|
1067
|
-
|
|
1155
|
+
break;
|
|
1156
|
+
case Enums_1.StateStorageType.Redis:
|
|
1157
|
+
{
|
|
1158
|
+
const prefix = this.options.stateStorage.redisConfig.prefix?.endsWith(":")
|
|
1159
|
+
? this.options.stateStorage.redisConfig.prefix
|
|
1160
|
+
: this.options.stateStorage.redisConfig.prefix ?? "magmastream:";
|
|
1161
|
+
const pattern = `${prefix}queue:*:current`;
|
|
1162
|
+
const stream = this.redis.scanStream({
|
|
1163
|
+
match: pattern,
|
|
1164
|
+
count: 100,
|
|
1165
|
+
});
|
|
1166
|
+
for await (const keys of stream) {
|
|
1167
|
+
for (const key of keys) {
|
|
1168
|
+
// Extract guildId from queue key
|
|
1169
|
+
const match = key.match(new RegExp(`^${prefix}queue:(.+):current$`));
|
|
1170
|
+
if (!match)
|
|
1171
|
+
continue;
|
|
1172
|
+
const guildId = match[1];
|
|
1173
|
+
// If player is not active in memory, clean up all keys
|
|
1174
|
+
if (!this.players.has(guildId)) {
|
|
1175
|
+
await this.redis.del(`${prefix}playerstore:${guildId}`, `${prefix}queue:${guildId}:current`, `${prefix}queue:${guildId}:tracks`, `${prefix}queue:${guildId}:previous`);
|
|
1176
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Cleaned inactive Redis player data: ${guildId}`);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
break;
|
|
1183
|
+
default:
|
|
1184
|
+
break;
|
|
1068
1185
|
}
|
|
1069
|
-
|
|
1070
|
-
|
|
1186
|
+
}
|
|
1187
|
+
/**
|
|
1188
|
+
* Cleans up an inactive player by removing its state files from the file system.
|
|
1189
|
+
* This is done to prevent stale state files from accumulating on the file system.
|
|
1190
|
+
* @param guildId The guild ID of the player to clean up.
|
|
1191
|
+
*/
|
|
1192
|
+
async cleanupInactivePlayer(guildId) {
|
|
1193
|
+
switch (this.options.stateStorage.type) {
|
|
1194
|
+
case Enums_1.StateStorageType.JSON:
|
|
1195
|
+
{
|
|
1196
|
+
const playersStoreDir = path_1.default.join(process.cwd(), "magmastream", "dist", "sessionData", "playersStore");
|
|
1197
|
+
const playersDataDir = this.options.stateStorage?.jsonConfig?.path ?? path_1.default.join(process.cwd(), "magmastream", "dist", "sessionData", "players");
|
|
1198
|
+
try {
|
|
1199
|
+
if (!this.players.has(guildId)) {
|
|
1200
|
+
await promises_1.default.unlink(path_1.default.join(playersStoreDir, `${guildId}.json`));
|
|
1201
|
+
await promises_1.default.rm(path_1.default.join(playersDataDir, guildId), { recursive: true, force: true });
|
|
1202
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Deleting inactive player files: ${guildId}`);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
catch (error) {
|
|
1206
|
+
if (error.code !== "ENOENT") {
|
|
1207
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Error deleting player files for ${guildId}: ${error}`);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
break;
|
|
1212
|
+
case Enums_1.StateStorageType.Redis:
|
|
1213
|
+
{
|
|
1214
|
+
const player = this.getPlayer(guildId);
|
|
1215
|
+
if (!player) {
|
|
1216
|
+
const prefix = this.options.stateStorage.redisConfig.prefix?.endsWith(":")
|
|
1217
|
+
? this.options.stateStorage.redisConfig.prefix
|
|
1218
|
+
: `${this.options.stateStorage.redisConfig.prefix ?? "magmastream"}:`;
|
|
1219
|
+
const keysToDelete = [
|
|
1220
|
+
`${prefix}playerstore:${guildId}`,
|
|
1221
|
+
`${prefix}queue:${guildId}:tracks`,
|
|
1222
|
+
`${prefix}queue:${guildId}:current`,
|
|
1223
|
+
`${prefix}queue:${guildId}:previous`,
|
|
1224
|
+
];
|
|
1225
|
+
await this.redis.del(...keysToDelete);
|
|
1226
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Deleted Redis player and queue data for: ${guildId}`);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
break;
|
|
1230
|
+
default:
|
|
1231
|
+
break;
|
|
1071
1232
|
}
|
|
1072
1233
|
}
|
|
1073
1234
|
/**
|
|
1074
1235
|
* Clears all player states from the file system.
|
|
1075
1236
|
* This is done to prevent stale state files from accumulating on the file system.
|
|
1076
1237
|
*/
|
|
1077
|
-
async
|
|
1238
|
+
async clearAllStoredPlayers() {
|
|
1078
1239
|
switch (this.options.stateStorage.type) {
|
|
1079
|
-
case Enums_1.StateStorageType.
|
|
1080
|
-
|
|
1240
|
+
case Enums_1.StateStorageType.Memory:
|
|
1241
|
+
case Enums_1.StateStorageType.JSON: {
|
|
1242
|
+
const configDir = path_1.default.join(process.cwd(), "magmastream", "dist", "sessionData", "playerStore");
|
|
1081
1243
|
try {
|
|
1082
1244
|
// Check if the directory exists, and create it if it doesn't
|
|
1083
1245
|
await promises_1.default.access(configDir).catch(async () => {
|
|
@@ -1085,11 +1247,11 @@ class Manager extends events_1.EventEmitter {
|
|
|
1085
1247
|
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Created directory: ${configDir}`);
|
|
1086
1248
|
});
|
|
1087
1249
|
const files = await promises_1.default.readdir(configDir);
|
|
1088
|
-
await Promise.all(files.map((file) => promises_1.default.unlink(path_1.default.join(configDir, file)).catch((err) => console.warn(`Failed to delete file ${file}:`, err))));
|
|
1250
|
+
await Promise.all(files.map((file) => promises_1.default.unlink(path_1.default.join(configDir, file)).catch((err) => console.warn(`[MANAGER] Failed to delete file ${file}:`, err))));
|
|
1089
1251
|
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Cleared all player state files in ${configDir}`);
|
|
1090
1252
|
}
|
|
1091
1253
|
catch (err) {
|
|
1092
|
-
console.error("Error clearing player state files:", err);
|
|
1254
|
+
console.error("[MANAGER] Error clearing player state files:", err);
|
|
1093
1255
|
}
|
|
1094
1256
|
break;
|
|
1095
1257
|
}
|
|
@@ -1116,11 +1278,11 @@ class Manager extends events_1.EventEmitter {
|
|
|
1116
1278
|
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Cleared ${totalDeleted} Redis player state keys (pattern: ${pattern})`);
|
|
1117
1279
|
});
|
|
1118
1280
|
stream.on("error", (err) => {
|
|
1119
|
-
console.error("Error during Redis SCAN stream:", err);
|
|
1281
|
+
console.error("[MANAGER] Error during Redis SCAN stream:", err);
|
|
1120
1282
|
});
|
|
1121
1283
|
}
|
|
1122
1284
|
catch (err) {
|
|
1123
|
-
console.error("Failed to clear Redis player state keys:", err);
|
|
1285
|
+
console.error("[MANAGER] Failed to clear Redis player state keys:", err);
|
|
1124
1286
|
}
|
|
1125
1287
|
break;
|
|
1126
1288
|
}
|