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.
@@ -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
- enabledPlugins: [],
62
- nodes: [
62
+ ...options,
63
+ enabledPlugins: options.enabledPlugins ?? [],
64
+ nodes: options.nodes ?? [
63
65
  {
64
- identifier: "default",
65
- host: "localhost",
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: "Magmastream",
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
- stateStorage: { type: Enums_1.StateStorageType.Collection },
77
- ...options,
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("Error during shutdown:", 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("Error during SIGTERM shutdown:", 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('"clusterId" is not a valid number, defaulting to 0.');
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?.type === Enums_1.StateStorageType.Redis) {
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.Collection:
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.Collection:
461
+ case Enums_1.StateStorageType.JSON:
451
462
  {
452
- const playerStatesDir = path_1.default.join(process.cwd(), "magmastream", "dist", "sessionData", "players");
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 (lavaPlayer) {
495
- if (lavaPlayer.track) {
496
- tracks.push(...queueTracks);
497
- if (currentTrack && currentTrack.uri === lavaPlayer.track.info.uri) {
498
- await player.queue.setCurrent(Utils_1.TrackUtils.build(lavaPlayer.track, currentTrack.requester));
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
- else {
502
- if (!currentTrack) {
503
- const payload = {
504
- reason: Enums_1.TrackEndReasonTypes.Finished,
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
- if (!currentTrack) {
515
- const payload = {
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
- await node.queueEnd(player, currentTrack, payload);
532
+ type: "TrackEndEvent",
533
+ });
519
534
  }
520
535
  else {
521
- tracks.push(currentTrack, ...queueTracks);
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 (lavaPlayer) {
665
- if (lavaPlayer.track) {
666
- tracks.push(...queueTracks);
667
- if (currentTrack && currentTrack.uri === lavaPlayer.track.info.uri) {
668
- await player.queue.setCurrent(Utils_1.TrackUtils.build(lavaPlayer.track, currentTrack.requester));
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
- else {
672
- if (!currentTrack) {
673
- const payload = {
674
- reason: Enums_1.TrackEndReasonTypes.Finished,
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
- if (!currentTrack) {
685
- const payload = {
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
- await node.queueEnd(player, currentTrack, payload);
731
+ type: "TrackEndEvent",
732
+ });
689
733
  }
690
734
  else {
691
- tracks.push(currentTrack, ...queueTracks);
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
- if (this.options.stateStorage.type === Enums_1.StateStorageType.Collection) {
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("Unexpected error during shutdown:", 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", "players");
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("Error ensuring player data directory exists:", err);
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: value.current || null,
1029
- tracks: Array.isArray(value) ? [...value] : [],
1030
- previous: Array.isArray(value.previous) ? [...value.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
- * Checks for players that are no longer active and deletes their saved state files.
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
- const playerStatesDir = path_1.default.join(process.cwd(), "magmastream", "dist", "sessionData", "players");
1047
- try {
1048
- // Check if the directory exists, and create it if it doesn't
1049
- await promises_1.default.access(playerStatesDir).catch(async () => {
1050
- await promises_1.default.mkdir(playerStatesDir, { recursive: true });
1051
- this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Created directory: ${playerStatesDir}`);
1052
- });
1053
- // Get the list of player state files
1054
- const playerFiles = await promises_1.default.readdir(playerStatesDir);
1055
- // Get the set of active guild IDs from the manager's player collection
1056
- const activeGuildIds = new Set(this.players.keys());
1057
- // Iterate over the player state files
1058
- for (const file of playerFiles) {
1059
- // Get the guild ID from the file name
1060
- const guildId = path_1.default.basename(file, ".json");
1061
- // If the guild ID is not in the set of active guild IDs, delete the file
1062
- if (!activeGuildIds.has(guildId)) {
1063
- const filePath = path_1.default.join(playerStatesDir, file);
1064
- await promises_1.default.unlink(filePath); // Delete the file asynchronously
1065
- this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Deleting inactive player: ${guildId}`);
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
- catch (error) {
1070
- this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Error cleaning up inactive players: ${error}`);
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 clearAllPlayerStates() {
1238
+ async clearAllStoredPlayers() {
1078
1239
  switch (this.options.stateStorage.type) {
1079
- case Enums_1.StateStorageType.Collection: {
1080
- const configDir = path_1.default.join(process.cwd(), "magmastream", "dist", "sessionData", "players");
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
  }