magmastream 2.9.3-dev.2 → 2.9.3-dev.21

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.
@@ -28,6 +28,8 @@ class Manager extends events_1.EventEmitter {
28
28
  initiated = false;
29
29
  redis;
30
30
  _send;
31
+ _getUser;
32
+ _getGuild;
31
33
  loadedPlugins = new Set();
32
34
  /**
33
35
  * Initiates the Manager class.
@@ -61,6 +63,10 @@ class Manager extends events_1.EventEmitter {
61
63
  this.options.clusterId = options.clusterId;
62
64
  if (options.send && !this._send)
63
65
  this._send = options.send;
66
+ if (options.getUser && !this._getUser)
67
+ this._getUser = options.getUser;
68
+ if (options.getGuild && !this._getGuild)
69
+ this._getGuild = options.getGuild;
64
70
  this.options = {
65
71
  ...options,
66
72
  enabledPlugins: options.enabledPlugins ?? [],
@@ -177,11 +183,15 @@ class Manager extends events_1.EventEmitter {
177
183
  db: config.db ?? 0,
178
184
  });
179
185
  }
180
- for (const node of this.nodes.values()) {
181
- try {
182
- await node.connect();
183
- }
184
- catch (err) {
186
+ const results = await Promise.allSettled([...this.nodes.values()].map(async (node) => {
187
+ await node.connect();
188
+ return node;
189
+ }));
190
+ for (let i = 0; i < results.length; i++) {
191
+ const result = results[i];
192
+ const node = [...this.nodes.values()][i];
193
+ if (result.status === "rejected") {
194
+ const err = result.reason;
185
195
  const error = err instanceof MagmastreamError_1.MagmaStreamError
186
196
  ? err
187
197
  : new MagmastreamError_1.MagmaStreamError({
@@ -277,9 +287,7 @@ class Manager extends events_1.EventEmitter {
277
287
  result.playlist.tracks = result.playlist.tracks.map(processTrack);
278
288
  }
279
289
  }
280
- const summary = "tracks" in result
281
- ? result.tracks.map((t) => Object.fromEntries(Object.entries(t).filter(([key]) => key !== "requester")))
282
- : [];
290
+ const summary = "tracks" in result ? result.tracks.map((t) => Object.fromEntries(Object.entries(t).filter(([key]) => key !== "requester"))) : [];
283
291
  this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Result search for ${_query.query}: ${Utils_1.JSONUtils.safe(summary, 2)}`);
284
292
  return result;
285
293
  }
@@ -466,9 +474,7 @@ class Manager extends events_1.EventEmitter {
466
474
  case Enums_1.StateStorageType.Redis:
467
475
  {
468
476
  try {
469
- const redisKey = `${this.options.stateStorage.redisConfig.prefix?.endsWith(":")
470
- ? this.options.stateStorage.redisConfig.prefix
471
- : this.options.stateStorage.redisConfig.prefix ?? "magmastream:"}playerstore:${guildId}`;
477
+ const redisKey = `${Utils_1.PlayerUtils.getRedisKey()}playerstore:${guildId}`;
472
478
  await this.redis.set(redisKey, JSON.stringify(serializedPlayer));
473
479
  this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Player state saved to Redis: ${guildId}`);
474
480
  }
@@ -530,10 +536,11 @@ class Manager extends events_1.EventEmitter {
530
536
  await promises_1.default.access(stateFilePath);
531
537
  const rawData = await promises_1.default.readFile(stateFilePath, "utf-8");
532
538
  const state = JSON.parse(rawData);
533
- if (state.clusterId !== this.options.clusterId)
534
- continue;
535
539
  if (!state.guildId || state.node?.options?.identifier !== nodeId)
536
540
  continue;
541
+ const hasGuild = this.resolveGuild(state.guildId);
542
+ if (!hasGuild)
543
+ continue;
537
544
  const lavaPlayer = await node.rest.getPlayer(state.guildId);
538
545
  if (!lavaPlayer) {
539
546
  await this.destroy(state.guildId);
@@ -558,6 +565,7 @@ class Manager extends events_1.EventEmitter {
558
565
  token: state.voiceState.event.token,
559
566
  endpoint: state.voiceState.event.endpoint,
560
567
  sessionId: state.voiceState.sessionId,
568
+ channelId: state.voiceState.event.channel_id,
561
569
  },
562
570
  },
563
571
  });
@@ -579,7 +587,7 @@ class Manager extends events_1.EventEmitter {
579
587
  if (lavaPlayer.track) {
580
588
  await player.queue.clear();
581
589
  if (currentTrack) {
582
- await player.queue.add(Utils_1.TrackUtils.build(lavaPlayer.track, currentTrack.requester));
590
+ await player.queue.add(Utils_1.TrackUtils.build(lavaPlayer.track, currentTrack.requester, currentTrack.isAutoplay));
583
591
  }
584
592
  const remainingQueue = queueTracks.filter((t) => t.uri !== lavaPlayer.track.info.uri);
585
593
  if (remainingQueue.length > 0) {
@@ -682,31 +690,18 @@ class Manager extends events_1.EventEmitter {
682
690
  filterActions[filter](true);
683
691
  }
684
692
  }
685
- this.emit(Enums_1.ManagerEventTypes.PlayerRestored, player, node);
686
- await this.sleep(1000);
687
- }
688
- catch (error) {
689
- this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Error processing player state for guild ${guildId}: ${error}`);
690
- continue;
691
- }
692
- }
693
- // Cleanup old player state files from guild directories whose nodeId matches
694
- for (const dirent of guildDirs) {
695
- if (!dirent.isDirectory())
696
- continue;
697
- const guildId = dirent.name;
698
- const stateFilePath = Utils_1.PlayerUtils.getPlayerStatePath(guildId);
699
- try {
700
- await promises_1.default.access(stateFilePath);
701
- const data = await promises_1.default.readFile(stateFilePath, "utf-8");
702
- const state = JSON.parse(data);
703
- if (state && typeof state === "object" && state.node?.options?.identifier === nodeId) {
693
+ try {
704
694
  await promises_1.default.rm(Utils_1.PlayerUtils.getPlayerStatePath(guildId), { force: true });
705
695
  this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Deleted player state folder for guild ${guildId}`);
706
696
  }
697
+ catch (error) {
698
+ this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Error deleting player state for guild ${guildId}: ${error}`);
699
+ }
700
+ this.emit(Enums_1.ManagerEventTypes.PlayerRestored, player, node);
701
+ await this.sleep(1000);
707
702
  }
708
703
  catch (error) {
709
- this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Error deleting player state for guild ${guildId}: ${error}`);
704
+ this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Error processing player state for guild ${guildId}: ${error}`);
710
705
  continue;
711
706
  }
712
707
  }
@@ -720,9 +715,7 @@ class Manager extends events_1.EventEmitter {
720
715
  {
721
716
  try {
722
717
  // Get all keys matching our pattern
723
- const redisKeyPattern = `${this.options.stateStorage.redisConfig.prefix?.endsWith(":")
724
- ? this.options.stateStorage.redisConfig.prefix
725
- : this.options.stateStorage.redisConfig.prefix ?? "magmastream:"}playerstore:*`;
718
+ const redisKeyPattern = `${Utils_1.PlayerUtils.getRedisKey()}playerstore:*`;
726
719
  const keys = await this.redis.keys(redisKeyPattern);
727
720
  for (const key of keys) {
728
721
  try {
@@ -730,11 +723,14 @@ class Manager extends events_1.EventEmitter {
730
723
  if (!data)
731
724
  continue;
732
725
  const state = JSON.parse(data);
733
- if (!state || typeof state !== "object" || state.clusterId !== this.options.clusterId)
726
+ if (!state || typeof state !== "object")
734
727
  continue;
735
728
  const guildId = key.split(":").pop();
736
729
  if (!guildId || state.node.options.identifier !== nodeId)
737
730
  continue;
731
+ const hasGuild = this.resolveGuild(guildId);
732
+ if (!hasGuild)
733
+ continue;
738
734
  const lavaPlayer = await node.rest.getPlayer(state.guildId);
739
735
  if (!lavaPlayer) {
740
736
  await this.destroy(guildId);
@@ -753,7 +749,14 @@ class Manager extends events_1.EventEmitter {
753
749
  const player = this.create(playerOptions);
754
750
  await player.node.rest.updatePlayer({
755
751
  guildId: state.options.guildId,
756
- data: { voice: { token: state.voiceState.event.token, endpoint: state.voiceState.event.endpoint, sessionId: state.voiceState.sessionId } },
752
+ data: {
753
+ voice: {
754
+ token: state.voiceState.event.token,
755
+ endpoint: state.voiceState.event.endpoint,
756
+ sessionId: state.voiceState.sessionId,
757
+ channelId: state.voiceState.event.channel_id,
758
+ },
759
+ },
757
760
  });
758
761
  player.connect();
759
762
  // Rest of the player state restoration code (tracks, filters, etc.)
@@ -774,7 +777,7 @@ class Manager extends events_1.EventEmitter {
774
777
  if (lavaPlayer.track) {
775
778
  await player.queue.clear();
776
779
  if (currentTrack) {
777
- await player.queue.add(Utils_1.TrackUtils.build(lavaPlayer.track, currentTrack.requester));
780
+ await player.queue.add(Utils_1.TrackUtils.build(lavaPlayer.track, currentTrack.requester, currentTrack.isAutoplay));
778
781
  }
779
782
  const remainingQueue = queueTracks.filter((t) => t.uri !== lavaPlayer.track.info.uri);
780
783
  if (remainingQueue.length > 0) {
@@ -916,11 +919,7 @@ class Manager extends events_1.EventEmitter {
916
919
  * @returns {Node} The node to use.
917
920
  */
918
921
  get useableNode() {
919
- return this.options.enablePriorityMode
920
- ? this.priorityNode
921
- : this.options.useNode === Enums_1.UseNodeOptions.LeastLoad
922
- ? this.leastLoadNode.first()
923
- : this.leastPlayersNode.first();
922
+ return this.options.enablePriorityMode ? this.priorityNode : this.options.useNode === Enums_1.UseNodeOptions.LeastLoad ? this.leastLoadNode.first() : this.leastPlayersNode.first();
924
923
  }
925
924
  /**
926
925
  * Handles the shutdown of the process by saving all active players' states and optionally cleaning up inactive players.
@@ -1079,13 +1078,17 @@ class Manager extends events_1.EventEmitter {
1079
1078
  */
1080
1079
  async handleVoiceServerUpdate(player, update) {
1081
1080
  player.voiceState.event = update;
1082
- const { sessionId, event: { token, endpoint }, } = player.voiceState;
1081
+ const sessionId = player.voiceState.sessionId;
1082
+ const channelId = player.voiceState.channelId;
1083
+ const token = update.token;
1084
+ const endpoint = update.endpoint;
1085
+ this.emit(Enums_1.ManagerEventTypes.Debug, `Updated voice server for player ${player.guildId} with token ${token} | endpoint ${endpoint} | sessionId ${sessionId} | channelId ${channelId}`);
1086
+ if (!sessionId || !channelId)
1087
+ return;
1083
1088
  await player.node.rest.updatePlayer({
1084
1089
  guildId: player.guildId,
1085
- data: { voice: { token, endpoint, sessionId } },
1090
+ data: { voice: { token, endpoint, sessionId, channelId } },
1086
1091
  });
1087
- this.emit(Enums_1.ManagerEventTypes.Debug, `Updated voice server for player ${player.guildId} with token ${token} and endpoint ${endpoint} and sessionId ${sessionId}`);
1088
- return;
1089
1092
  }
1090
1093
  /**
1091
1094
  * Handles a voice state update by updating the player's voice channel and session ID if provided, or by disconnecting and destroying the player if the channel ID is null.
@@ -1096,22 +1099,30 @@ class Manager extends events_1.EventEmitter {
1096
1099
  */
1097
1100
  async handleVoiceStateUpdate(player, update) {
1098
1101
  this.emit(Enums_1.ManagerEventTypes.Debug, `Updated voice state for player ${player.guildId} with channel id ${update.channel_id} and session id ${update.session_id}`);
1099
- if (update.channel_id) {
1100
- if (player.voiceChannelId !== update.channel_id) {
1101
- this.emit(Enums_1.ManagerEventTypes.PlayerMove, player, player.voiceChannelId, update.channel_id);
1102
- }
1103
- player.voiceState.sessionId = update.session_id;
1104
- player.voiceChannelId = update.channel_id;
1105
- player.options.voiceChannelId = update.channel_id;
1102
+ if (!update.channel_id) {
1103
+ this.emit(Enums_1.ManagerEventTypes.PlayerDisconnect, player, player.voiceChannelId);
1104
+ player.voiceChannelId = null;
1105
+ player.state = Enums_1.StateTypes.Disconnected;
1106
+ player.voiceState = Object.assign({});
1107
+ if (player.options.pauseOnDisconnect)
1108
+ await player.pause(true);
1106
1109
  return;
1107
1110
  }
1108
- this.emit(Enums_1.ManagerEventTypes.PlayerDisconnect, player, player.voiceChannelId);
1109
- player.voiceChannelId = null;
1110
- player.voiceState = Object.assign({});
1111
- if (player.options.pauseOnDisconnect) {
1112
- await player.pause(true);
1111
+ if (player.voiceChannelId !== update.channel_id) {
1112
+ this.emit(Enums_1.ManagerEventTypes.PlayerMove, player, player.voiceChannelId, update.channel_id);
1113
1113
  }
1114
- return;
1114
+ player.voiceState.sessionId = update.session_id;
1115
+ player.voiceState.channelId = update.channel_id;
1116
+ player.voiceChannelId = update.channel_id;
1117
+ player.options.voiceChannelId = update.channel_id;
1118
+ const token = player.voiceState.event?.token;
1119
+ const endpoint = player.voiceState.event?.endpoint;
1120
+ if (!token || !endpoint)
1121
+ return;
1122
+ await player.node.rest.updatePlayer({
1123
+ guildId: player.guildId,
1124
+ data: { voice: { token, endpoint, sessionId: update.session_id, channelId: update.channel_id } },
1125
+ });
1115
1126
  }
1116
1127
  /**
1117
1128
  * Cleans up inactive players by removing their state files from the file system.
@@ -1154,9 +1165,7 @@ class Manager extends events_1.EventEmitter {
1154
1165
  break;
1155
1166
  case Enums_1.StateStorageType.Redis:
1156
1167
  {
1157
- const prefix = this.options.stateStorage.redisConfig.prefix?.endsWith(":")
1158
- ? this.options.stateStorage.redisConfig.prefix
1159
- : this.options.stateStorage.redisConfig.prefix ?? "magmastream:";
1168
+ const prefix = Utils_1.PlayerUtils.getRedisKey();
1160
1169
  const pattern = `${prefix}queue:*:current`;
1161
1170
  try {
1162
1171
  const stream = this.redis.scanStream({
@@ -1229,9 +1238,7 @@ class Manager extends events_1.EventEmitter {
1229
1238
  {
1230
1239
  try {
1231
1240
  if (!player) {
1232
- const prefix = this.options.stateStorage.redisConfig.prefix?.endsWith(":")
1233
- ? this.options.stateStorage.redisConfig.prefix
1234
- : `${this.options.stateStorage.redisConfig.prefix ?? "magmastream"}:`;
1241
+ const prefix = Utils_1.PlayerUtils.getRedisKey();
1235
1242
  const keysToDelete = [
1236
1243
  `${prefix}playerstore:${guildId}`,
1237
1244
  `${prefix}queue:${guildId}:tracks`,
@@ -1339,9 +1346,7 @@ class Manager extends events_1.EventEmitter {
1339
1346
  break;
1340
1347
  }
1341
1348
  case Enums_1.StateStorageType.Redis: {
1342
- const prefix = this.options.stateStorage.redisConfig.prefix?.endsWith(":")
1343
- ? this.options.stateStorage.redisConfig.prefix
1344
- : this.options.stateStorage.redisConfig.prefix ?? "magmastream:";
1349
+ const prefix = Utils_1.PlayerUtils.getRedisKey();
1345
1350
  const patterns = [`${prefix}playerstore:*`, `${prefix}queue:*`];
1346
1351
  try {
1347
1352
  for (const pattern of patterns) {
@@ -1440,6 +1445,9 @@ class Manager extends events_1.EventEmitter {
1440
1445
  }
1441
1446
  return this._send(packet);
1442
1447
  }
1448
+ getUserFromCache(id) {
1449
+ return this._getUser?.(id);
1450
+ }
1443
1451
  sendPacket(packet) {
1444
1452
  return this.send(packet);
1445
1453
  }
@@ -1454,5 +1462,14 @@ class Manager extends events_1.EventEmitter {
1454
1462
  return { id: user }; // fallback by ID only
1455
1463
  return user; // default: just return the portable user
1456
1464
  }
1465
+ /**
1466
+ * Resolves a Guild ID to a real guild object.
1467
+ * Can be overridden by wrapper managers to return wrapper-specific Guild classes.
1468
+ */
1469
+ resolveGuild(guildId) {
1470
+ if (!guildId)
1471
+ return null;
1472
+ return this._getGuild?.(guildId);
1473
+ }
1457
1474
  }
1458
1475
  exports.Manager = Manager;
@@ -31,7 +31,6 @@ class Node {
31
31
  reconnectTimeout;
32
32
  reconnectAttempts = 1;
33
33
  redisPrefix;
34
- sessionIdsFilePath;
35
34
  sessionIdsMap = new Map();
36
35
  /**
37
36
  * Creates an instance of Node.
@@ -97,18 +96,12 @@ class Node {
97
96
  switch (this.manager.options.stateStorage.type) {
98
97
  case Enums_1.StateStorageType.Memory:
99
98
  case Enums_1.StateStorageType.JSON:
100
- this.sessionIdsFilePath = path_1.default.join(process.cwd(), "magmastream", "sessionData", "sessionIds.json");
101
- const configDir = path_1.default.dirname(this.sessionIdsFilePath);
102
- if (!fs_1.default.existsSync(configDir)) {
103
- fs_1.default.mkdirSync(configDir, { recursive: true });
104
- }
105
- this.createSessionIdsFile();
106
99
  this.createReadmeFile();
107
100
  break;
108
101
  case Enums_1.StateStorageType.Redis:
109
102
  this.redisPrefix = this.manager.options.stateStorage.redisConfig.prefix?.endsWith(":")
110
103
  ? this.manager.options.stateStorage.redisConfig.prefix
111
- : this.manager.options.stateStorage.redisConfig.prefix ?? "magmastream:";
104
+ : (this.manager.options.stateStorage.redisConfig.prefix ?? "magmastream:");
112
105
  break;
113
106
  }
114
107
  }
@@ -125,16 +118,18 @@ class Node {
125
118
  get address() {
126
119
  return `${this.options.host}:${this.options.port}`;
127
120
  }
128
- /**
129
- * Creates the sessionIds.json file if it doesn't exist. This file is used to
130
- * store the session IDs for each node. The session IDs are used to identify
131
- * the node when resuming a session.
132
- */
133
- createSessionIdsFile() {
134
- if (!fs_1.default.existsSync(this.sessionIdsFilePath)) {
135
- this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Creating sessionId file at: ${this.sessionIdsFilePath}`);
136
- fs_1.default.writeFileSync(this.sessionIdsFilePath, Utils_1.JSONUtils.safe({}), "utf-8");
137
- }
121
+ getCompositeKey() {
122
+ return `${this.options.identifier}::${this.manager.options.clusterId}`;
123
+ }
124
+ getRedisSessionIdsKey() {
125
+ return `${this.redisPrefix}node:sessionIds`;
126
+ }
127
+ getNodeSessionsDir() {
128
+ return path_1.default.join(process.cwd(), "magmastream", "sessionData", "cluster", String(this.manager.options.clusterId), "nodeSessions");
129
+ }
130
+ getNodeSessionPath() {
131
+ const safeId = String(this.options.identifier).replace(/[^a-zA-Z0-9._-]/g, "_");
132
+ return path_1.default.join(this.getNodeSessionsDir(), `${safeId}.txt`);
138
133
  }
139
134
  /**
140
135
  * Loads session IDs from the sessionIds.json file if it exists.
@@ -148,49 +143,43 @@ class Node {
148
143
  switch (this.manager.options.stateStorage.type) {
149
144
  case Enums_1.StateStorageType.Memory:
150
145
  case Enums_1.StateStorageType.JSON: {
151
- if (fs_1.default.existsSync(this.sessionIdsFilePath)) {
152
- this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Loading sessionIds from file: ${this.sessionIdsFilePath}`);
153
- const sessionIdsData = fs_1.default.readFileSync(this.sessionIdsFilePath, "utf-8");
154
- this.sessionIdsMap = new Map(Object.entries(JSON.parse(sessionIdsData)));
155
- const compositeKey = `${this.options.identifier}::${this.manager.options.clusterId}`;
156
- if (this.sessionIdsMap.has(compositeKey)) {
157
- this.sessionId = this.sessionIdsMap.get(compositeKey);
158
- }
146
+ const dir = this.getNodeSessionsDir();
147
+ const filePath = this.getNodeSessionPath();
148
+ if (!fs_1.default.existsSync(dir))
149
+ fs_1.default.mkdirSync(dir, { recursive: true });
150
+ if (!fs_1.default.existsSync(filePath)) {
151
+ this.sessionId = null;
152
+ return;
153
+ }
154
+ try {
155
+ const raw = fs_1.default.readFileSync(filePath, "utf-8").trim();
156
+ this.sessionId = raw.length ? raw : null;
157
+ if (this.sessionId)
158
+ this.sessionIdsMap.set(this.getCompositeKey(), this.sessionId);
159
+ }
160
+ catch {
161
+ this.sessionId = null;
159
162
  }
160
163
  break;
161
164
  }
162
- case Enums_1.StateStorageType.Redis:
163
- const key = `${this.redisPrefix}node:sessionIds`;
164
- this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Loading sessionIds from Redis key: ${key}`);
165
- const currentRaw = await this.manager.redis.get(key);
166
- if (currentRaw) {
167
- try {
168
- const sessionIds = JSON.parse(currentRaw);
169
- if (typeof sessionIds !== "object" || Array.isArray(sessionIds)) {
170
- throw new MagmastreamError_1.MagmaStreamError({
171
- code: Enums_1.MagmaStreamErrorCode.NODE_SESSION_IDS_LOAD_FAILED,
172
- message: "Invalid sessionIds data type from Redis.",
173
- context: { sessionIds },
174
- });
175
- }
176
- this.sessionIdsMap = new Map(Object.entries(sessionIds));
177
- const compositeKey = `${this.options.identifier}::${this.manager.options.clusterId}`;
178
- if (this.sessionIdsMap.has(compositeKey)) {
179
- this.sessionId = this.sessionIdsMap.get(compositeKey) || null;
180
- this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Restored sessionId for ${compositeKey}: ${this.sessionId}`);
181
- }
182
- }
183
- catch (err) {
184
- this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Failed to parse Redis sessionIds: ${err.message}`);
185
- this.sessionIdsMap = new Map();
165
+ case Enums_1.StateStorageType.Redis: {
166
+ const key = this.getRedisSessionIdsKey();
167
+ const compositeKey = this.getCompositeKey();
168
+ this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Loading sessionId from Redis hash: ${key} field: ${compositeKey}`);
169
+ try {
170
+ const sid = await this.manager.redis.hget(key, compositeKey);
171
+ this.sessionId = sid ?? null;
172
+ if (this.sessionId) {
173
+ this.sessionIdsMap.set(compositeKey, this.sessionId);
174
+ this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Restored sessionId for ${compositeKey}: ${this.sessionId}`);
186
175
  }
187
176
  }
188
- else {
189
- this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] No sessionIds found in Redis creating new key.`);
190
- await this.manager.redis.set(key, Utils_1.JSONUtils.safe({}));
191
- this.sessionIdsMap = new Map();
177
+ catch (err) {
178
+ this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Failed to load sessionId from Redis hash: ${err.message}`);
179
+ this.sessionId = null;
192
180
  }
193
181
  break;
182
+ }
194
183
  }
195
184
  }
196
185
  /**
@@ -207,84 +196,53 @@ class Node {
207
196
  async updateSessionId() {
208
197
  switch (this.manager.options.stateStorage.type) {
209
198
  case Enums_1.StateStorageType.Memory:
210
- case Enums_1.StateStorageType.JSON: {
211
- const compositeKey = `${this.options.identifier}::${this.manager.options.clusterId}`;
212
- const filePath = this.sessionIdsFilePath;
213
- this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Updating sessionIds to file: ${filePath}`);
214
- let updated = false;
215
- let retries = 3;
216
- while (!updated && retries > 0) {
217
- try {
218
- let fileData = {};
219
- if (fs_1.default.existsSync(filePath)) {
220
- try {
221
- const raw = fs_1.default.readFileSync(filePath, "utf-8");
222
- fileData = raw.trim() ? JSON.parse(raw) : {};
223
- }
224
- catch (err) {
225
- this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Failed to read/parse sessionIds.json: ${err.message}`);
226
- fileData = {};
227
- }
228
- }
229
- fileData[compositeKey] = this.sessionId;
230
- const tmpPath = `${filePath}.tmp`;
231
- fs_1.default.writeFileSync(tmpPath, Utils_1.JSONUtils.safe(fileData, 2), "utf-8");
232
- fs_1.default.renameSync(tmpPath, filePath);
233
- this.sessionIdsMap = new Map(Object.entries(fileData));
234
- updated = true;
235
- }
236
- catch (err) {
237
- retries--;
238
- if (retries === 0) {
239
- throw new MagmastreamError_1.MagmaStreamError({
240
- code: Enums_1.MagmaStreamErrorCode.NODE_SESSION_IDS_UPDATE_FAILED,
241
- message: `Failed to update sessionIds after retries.`,
242
- cause: err instanceof Error ? err : undefined,
243
- context: { filePath, compositeKey, storage: "file" },
244
- });
245
- }
246
- await new Promise((r) => setTimeout(r, 50));
247
- }
248
- }
249
- break;
199
+ case Enums_1.StateStorageType.JSON:
200
+ return this.updateSessionIdFile();
201
+ case Enums_1.StateStorageType.Redis:
202
+ return this.updateSessionIdRedis();
203
+ }
204
+ }
205
+ async updateSessionIdFile() {
206
+ const dir = this.getNodeSessionsDir();
207
+ const filePath = this.getNodeSessionPath();
208
+ const tmpPath = `${filePath}.tmp`;
209
+ if (!fs_1.default.existsSync(dir))
210
+ fs_1.default.mkdirSync(dir, { recursive: true });
211
+ if (this.sessionId) {
212
+ fs_1.default.writeFileSync(tmpPath, this.sessionId, "utf-8");
213
+ fs_1.default.renameSync(tmpPath, filePath);
214
+ this.sessionIdsMap.set(this.getCompositeKey(), this.sessionId);
215
+ }
216
+ else {
217
+ try {
218
+ if (fs_1.default.existsSync(filePath))
219
+ fs_1.default.unlinkSync(filePath);
250
220
  }
251
- case Enums_1.StateStorageType.Redis: {
252
- const key = `${this.redisPrefix}node:sessionIds`;
253
- const compositeKey = `${this.options.identifier}::${this.manager.options.clusterId}`;
254
- this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Updating sessionIds in Redis key: ${key}`);
255
- let sessionIds = {};
256
- try {
257
- const currentRaw = await this.manager.redis.get(key);
258
- if (currentRaw) {
259
- sessionIds = JSON.parse(currentRaw);
260
- if (typeof sessionIds !== "object" || Array.isArray(sessionIds)) {
261
- throw new Error("Invalid data type in Redis");
262
- }
263
- }
264
- else {
265
- this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Redis key not found — creating new sessionIds key.`);
266
- sessionIds = {};
267
- }
268
- }
269
- catch (err) {
270
- this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Corrupted Redis sessionIds, reinitializing: ${err.message}`);
271
- sessionIds = {};
272
- }
273
- try {
274
- sessionIds[compositeKey] = this.sessionId;
275
- this.sessionIdsMap = new Map(Object.entries(sessionIds));
276
- await this.manager.redis.set(key, Utils_1.JSONUtils.safe(sessionIds));
277
- }
278
- catch (err) {
279
- throw new MagmastreamError_1.MagmaStreamError({
280
- code: Enums_1.MagmaStreamErrorCode.NODE_SESSION_IDS_UPDATE_FAILED,
281
- message: `Failed to update sessionIds in Redis.`,
282
- cause: err instanceof Error ? err : undefined,
283
- context: { key, compositeKey, storage: "redis" },
284
- });
285
- }
286
- break;
221
+ catch { }
222
+ this.sessionIdsMap.delete(this.getCompositeKey());
223
+ }
224
+ }
225
+ async updateSessionIdRedis() {
226
+ const key = this.getRedisSessionIdsKey();
227
+ const compositeKey = this.getCompositeKey();
228
+ this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Updating sessionId in Redis hash: ${key} field: ${compositeKey}`);
229
+ try {
230
+ if (this.sessionId) {
231
+ await this.manager.redis.hset(key, compositeKey, this.sessionId);
232
+ this.sessionIdsMap.set(compositeKey, this.sessionId);
287
233
  }
234
+ else {
235
+ await this.manager.redis.hdel(key, compositeKey);
236
+ this.sessionIdsMap.delete(compositeKey);
237
+ }
238
+ }
239
+ catch (err) {
240
+ throw new MagmastreamError_1.MagmaStreamError({
241
+ code: Enums_1.MagmaStreamErrorCode.NODE_SESSION_IDS_UPDATE_FAILED,
242
+ message: "Failed to update sessionId in Redis hash.",
243
+ cause: err instanceof Error ? err : undefined,
244
+ context: { key, compositeKey, storage: "redis-hash" },
245
+ });
288
246
  }
289
247
  }
290
248
  /**
@@ -305,12 +263,7 @@ class Node {
305
263
  "User-Id": this.manager.options.clientId,
306
264
  "Client-Name": this.manager.options.clientName,
307
265
  };
308
- const compositeKey = `${this.options.identifier}::${this.manager.options.clusterId}`;
309
- if (this.sessionId) {
310
- headers["Session-Id"] = this.sessionId;
311
- }
312
- else if (this.options.enableSessionResumeOption && this.sessionIdsMap.has(compositeKey)) {
313
- this.sessionId = this.sessionIdsMap.get(compositeKey) || null;
266
+ if (typeof this.sessionId === "string" && this.sessionId.length > 0) {
314
267
  headers["Session-Id"] = this.sessionId;
315
268
  }
316
269
  this.socket = new ws_1.default(`ws${this.options.useSSL ? "s" : ""}://${this.address}/v4/websocket`, { headers });
@@ -356,9 +309,7 @@ class Node {
356
309
  // Automove all players connected to that node
357
310
  const players = this.manager.players.filter((p) => p.node == this);
358
311
  if (players.size) {
359
- for (const player of players.values()) {
360
- await player.autoMoveNode();
361
- }
312
+ await Promise.all(Array.from(players.values(), (player) => player.autoMoveNode()));
362
313
  }
363
314
  this.socket.close(1000, "destroy");
364
315
  this.socket.removeAllListeners();
@@ -632,12 +583,7 @@ class Node {
632
583
  player.playing = true;
633
584
  player.paused = false;
634
585
  this.manager.emit(Enums_1.ManagerEventTypes.TrackStart, player, track, payload);
635
- const AutoplayUser = player.get("Internal_AutoplayUser");
636
- if (!track.requester || !track.requester.id) {
637
- console.log(track);
638
- console.warn(`[WARN] Track requester missing for guild ${player.guildId}`, track);
639
- }
640
- if (AutoplayUser && track.requester && AutoplayUser.id === track.requester.id) {
586
+ if (track.isAutoplay) {
641
587
  this.manager.emit(Enums_1.ManagerEventTypes.PlayerStateUpdate, oldPlayer, player, {
642
588
  changeType: Enums_1.PlayerStateEventTypes.TrackChange,
643
589
  details: {