magmastream 2.9.0-dev.37 → 2.9.0-dev.39

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 CHANGED
@@ -1378,7 +1378,7 @@ interface ManagerEvents {
1378
1378
  [ManagerEventTypes.PlayerCreate]: [player: Player];
1379
1379
  [ManagerEventTypes.PlayerDestroy]: [player: Player];
1380
1380
  [ManagerEventTypes.PlayerDisconnect]: [player: Player, oldChannel: string];
1381
- [ManagerEventTypes.PlayerMove]: [player: Player, initChannel: string, newChannel: string];
1381
+ [ManagerEventTypes.PlayerMove]: [player: Player, oldChannel: string, newChannel: string];
1382
1382
  [ManagerEventTypes.PlayerRestored]: [player: Player, node: Node];
1383
1383
  [ManagerEventTypes.PlayerStateUpdate]: [oldPlayer: Player, newPlayer: Player, changeType: PlayerStateUpdateEvent];
1384
1384
  [ManagerEventTypes.QueueEnd]: [player: Player, track: Track, payload: TrackEndEvent];
@@ -1949,15 +1949,21 @@ declare class Node {
1949
1949
  isNodeLink: boolean;
1950
1950
  private reconnectTimeout?;
1951
1951
  private reconnectAttempts;
1952
+ private redisPrefix?;
1953
+ private sessionIdsFilePath?;
1954
+ private sessionIdsMap;
1952
1955
  /**
1953
1956
  * Creates an instance of Node.
1954
1957
  * @param manager - The manager for the node.
1955
1958
  * @param options - The options for the node.
1956
1959
  */
1957
1960
  constructor(manager: Manager, options: NodeOptions);
1958
- /** Returns if connected to the Node. */
1961
+ /**
1962
+ * Checks if the Node is currently connected.
1963
+ * This method returns true if the Node has an active WebSocket connection, indicating it is ready to receive and process commands.
1964
+ */
1959
1965
  get connected(): boolean;
1960
- /** Returns the address for this node. */
1966
+ /** Returns the full address for this node, including the host and port. */
1961
1967
  get address(): string;
1962
1968
  /**
1963
1969
  * Creates the sessionIds.json file if it doesn't exist. This file is used to
@@ -1973,7 +1979,7 @@ declare class Node {
1973
1979
  * of the node identifier and cluster ID. This allows multiple clusters to
1974
1980
  * be used with the same node identifier.
1975
1981
  */
1976
- loadSessionIds(): void;
1982
+ loadSessionIds(): Promise<void>;
1977
1983
  /**
1978
1984
  * Updates the session ID in the sessionIds.json file.
1979
1985
  *
@@ -1985,7 +1991,7 @@ declare class Node {
1985
1991
  * of the node identifier and cluster ID. This allows multiple clusters to
1986
1992
  * be used with the same node identifier.
1987
1993
  */
1988
- updateSessionId(): void;
1994
+ updateSessionId(): Promise<void>;
1989
1995
  /**
1990
1996
  * Connects to the Node.
1991
1997
  *
@@ -1995,7 +2001,7 @@ declare class Node {
1995
2001
  * If the node has no session ID but the `enableSessionResumeOption` option is true, it will use the session ID
1996
2002
  * stored in the sessionIds.json file if it exists.
1997
2003
  */
1998
- connect(): void;
2004
+ connect(): Promise<void>;
1999
2005
  /**
2000
2006
  * Destroys the node and cleans up associated resources.
2001
2007
  *
@@ -2323,7 +2329,7 @@ declare class Manager extends EventEmitter {
2323
2329
  readonly options: ManagerOptions;
2324
2330
  initiated: boolean;
2325
2331
  redis?: Redis;
2326
- private _send?;
2332
+ private _send;
2327
2333
  private loadedPlugins;
2328
2334
  /**
2329
2335
  * Initiates the Manager class.
@@ -2347,7 +2353,7 @@ declare class Manager extends EventEmitter {
2347
2353
  * @param clusterId - The cluster ID which runs the current process (required).
2348
2354
  * @returns The manager instance.
2349
2355
  */
2350
- init(options?: ManagerInitOptions): this;
2356
+ init(options?: ManagerInitOptions): Promise<this>;
2351
2357
  /**
2352
2358
  * Searches the enabled sources based off the URL or the `source` property.
2353
2359
  * @param query
@@ -2423,7 +2429,7 @@ declare class Manager extends EventEmitter {
2423
2429
  */
2424
2430
  decodeTrack(track: string): Promise<TrackData>;
2425
2431
  /**
2426
- * Saves player states to the JSON file.
2432
+ * Saves player states.
2427
2433
  * @param {string} guildId - The guild ID of the player to save
2428
2434
  */
2429
2435
  savePlayerState(guildId: string): Promise<void>;
@@ -2522,8 +2528,8 @@ declare class Manager extends EventEmitter {
2522
2528
  */
2523
2529
  cleanupInactivePlayers(): Promise<void>;
2524
2530
  /**
2525
- * Cleans up an inactive player by removing its state files from the file system.
2526
- * This is done to prevent stale state files from accumulating on the file system.
2531
+ * Cleans up an inactive player by removing its state data.
2532
+ * This is done to prevent stale state data from accumulating.
2527
2533
  * @param guildId The guild ID of the player to clean up.
2528
2534
  */
2529
2535
  cleanupInactivePlayer(guildId: string): Promise<void>;
@@ -3396,6 +3402,9 @@ declare abstract class AutoPlayUtils {
3396
3402
  * @returns A single resolved {@link Track} object if found, or `null` if the search fails or returns no results.
3397
3403
  */
3398
3404
  private static resolveFirstTrackFromQuery;
3405
+ private static isPlaylistRawData;
3406
+ private static isTrackData;
3407
+ private static isTrackDataArray;
3399
3408
  static buildTracksFromResponse<T>(recommendedResult: LavalinkResponse, requester?: T): Track[];
3400
3409
  }
3401
3410
  /** Gets or extends structures to extend the built in, or already extended, classes to add more functionality. */
@@ -87,6 +87,7 @@ class Manager extends events_1.EventEmitter {
87
87
  deleteInactivePlayers: options.stateStorage?.deleteInactivePlayers ?? true,
88
88
  },
89
89
  autoPlaySearchPlatforms: options.autoPlaySearchPlatforms ?? [Enums_1.AutoPlayPlatform.YouTube],
90
+ send: this._send,
90
91
  };
91
92
  Utils_1.AutoPlayUtils.init(this);
92
93
  if (this.options.nodes) {
@@ -127,7 +128,7 @@ class Manager extends events_1.EventEmitter {
127
128
  * @param clusterId - The cluster ID which runs the current process (required).
128
129
  * @returns The manager instance.
129
130
  */
130
- init(options = {}) {
131
+ async init(options = {}) {
131
132
  if (this.initiated) {
132
133
  return this;
133
134
  }
@@ -145,14 +146,6 @@ class Manager extends events_1.EventEmitter {
145
146
  else {
146
147
  this.options.clusterId = clusterId;
147
148
  }
148
- for (const node of this.nodes.values()) {
149
- try {
150
- node.connect();
151
- }
152
- catch (err) {
153
- this.emit(Enums_1.ManagerEventTypes.NodeError, node, err);
154
- }
155
- }
156
149
  if (this.options.stateStorage.type === Enums_1.StateStorageType.Redis) {
157
150
  const config = this.options.stateStorage.redisConfig;
158
151
  this.redis = new ioredis_1.default({
@@ -162,6 +155,14 @@ class Manager extends events_1.EventEmitter {
162
155
  db: config.db ?? 0,
163
156
  });
164
157
  }
158
+ for (const node of this.nodes.values()) {
159
+ try {
160
+ await node.connect();
161
+ }
162
+ catch (err) {
163
+ this.emit(Enums_1.ManagerEventTypes.NodeError, node, err);
164
+ }
165
+ }
165
166
  this.loadPlugins();
166
167
  this.initiated = true;
167
168
  return this;
@@ -397,7 +398,7 @@ class Manager extends events_1.EventEmitter {
397
398
  return res[0];
398
399
  }
399
400
  /**
400
- * Saves player states to the JSON file.
401
+ * Saves player states.
401
402
  * @param {string} guildId - The guild ID of the player to save
402
403
  */
403
404
  async savePlayerState(guildId) {
@@ -1193,8 +1194,8 @@ class Manager extends events_1.EventEmitter {
1193
1194
  }
1194
1195
  }
1195
1196
  /**
1196
- * Cleans up an inactive player by removing its state files from the file system.
1197
- * This is done to prevent stale state files from accumulating on the file system.
1197
+ * Cleans up an inactive player by removing its state data.
1198
+ * This is done to prevent stale state data from accumulating.
1198
1199
  * @param guildId The guild ID of the player to clean up.
1199
1200
  */
1200
1201
  async cleanupInactivePlayer(guildId) {
@@ -1302,30 +1303,32 @@ class Manager extends events_1.EventEmitter {
1302
1303
  const prefix = this.options.stateStorage.redisConfig.prefix?.endsWith(":")
1303
1304
  ? this.options.stateStorage.redisConfig.prefix
1304
1305
  : this.options.stateStorage.redisConfig.prefix ?? "magmastream:";
1305
- const pattern = `${prefix}playerstore:*`;
1306
+ const patterns = [`${prefix}playerstore:*`, `${prefix}queue:*`];
1306
1307
  try {
1307
- const stream = this.redis.scanStream({
1308
- match: pattern,
1309
- count: 100,
1310
- });
1311
- let totalDeleted = 0;
1312
- stream.on("data", async (keys) => {
1313
- if (keys.length) {
1314
- const pipeline = this.redis.pipeline();
1315
- keys.forEach((key) => pipeline.unlink(key));
1316
- await pipeline.exec();
1317
- totalDeleted += keys.length;
1318
- }
1319
- });
1320
- stream.on("end", () => {
1321
- this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Cleared ${totalDeleted} Redis player state keys (pattern: ${pattern})`);
1322
- });
1323
- stream.on("error", (err) => {
1324
- console.error("[MANAGER] Error during Redis SCAN stream:", err);
1325
- });
1308
+ for (const pattern of patterns) {
1309
+ const stream = this.redis.scanStream({
1310
+ match: pattern,
1311
+ count: 100,
1312
+ });
1313
+ let totalDeleted = 0;
1314
+ stream.on("data", async (keys) => {
1315
+ if (keys.length) {
1316
+ const pipeline = this.redis.pipeline();
1317
+ keys.forEach((key) => pipeline.unlink(key));
1318
+ await pipeline.exec();
1319
+ totalDeleted += keys.length;
1320
+ }
1321
+ });
1322
+ stream.on("end", () => {
1323
+ this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Cleared ${totalDeleted} Redis keys (pattern: ${pattern})`);
1324
+ });
1325
+ stream.on("error", (err) => {
1326
+ console.error(`[MANAGER] Error during Redis SCAN stream (${pattern}):`, err);
1327
+ });
1328
+ }
1326
1329
  }
1327
1330
  catch (err) {
1328
- console.error("[MANAGER] Failed to clear Redis player state keys:", err);
1331
+ console.error("[MANAGER] Failed to clear Redis keys:", err);
1329
1332
  }
1330
1333
  break;
1331
1334
  }
@@ -1394,6 +1397,10 @@ class Manager extends events_1.EventEmitter {
1394
1397
  return this.options.useNode === Enums_1.UseNodeOptions.LeastLoad ? this.leastLoadNode.first() : this.leastPlayersNode.first();
1395
1398
  }
1396
1399
  send(packet) {
1400
+ if (!this._send) {
1401
+ console.warn("[Manager.send] _send is not defined! Packet will not be sent.");
1402
+ return;
1403
+ }
1397
1404
  return this._send(packet);
1398
1405
  }
1399
1406
  sendPacket(packet) {
@@ -10,12 +10,6 @@ const fs_1 = tslib_1.__importDefault(require("fs"));
10
10
  const path_1 = tslib_1.__importDefault(require("path"));
11
11
  const Enums_1 = require("./Enums");
12
12
  const validSponsorBlocks = Object.values(Enums_1.SponsorBlockSegment).map((v) => v.toLowerCase());
13
- const sessionIdsFilePath = path_1.default.join(process.cwd(), "magmastream", "dist", "sessionData", "sessionIds.json");
14
- let sessionIdsMap = new Map();
15
- const configDir = path_1.default.dirname(sessionIdsFilePath);
16
- if (!fs_1.default.existsSync(configDir)) {
17
- fs_1.default.mkdirSync(configDir, { recursive: true });
18
- }
19
13
  class Node {
20
14
  manager;
21
15
  options;
@@ -35,6 +29,9 @@ class Node {
35
29
  isNodeLink = false;
36
30
  reconnectTimeout;
37
31
  reconnectAttempts = 1;
32
+ redisPrefix;
33
+ sessionIdsFilePath;
34
+ sessionIdsMap = new Map();
38
35
  /**
39
36
  * Creates an instance of Node.
40
37
  * @param manager - The manager for the node.
@@ -91,18 +88,33 @@ class Node {
91
88
  this.manager.nodes.set(this.options.identifier, this);
92
89
  this.manager.emit(Enums_1.ManagerEventTypes.NodeCreate, this);
93
90
  this.rest = new Rest_1.Rest(this, this.manager);
94
- this.createSessionIdsFile();
95
- this.loadSessionIds();
96
- // Create README file to inform the user about the magmastream folder
97
- this.createReadmeFile();
91
+ switch (this.manager.options.stateStorage.type) {
92
+ case Enums_1.StateStorageType.JSON:
93
+ this.sessionIdsFilePath = path_1.default.join(process.cwd(), "magmastream", "dist", "sessionData", "sessionIds.json");
94
+ const configDir = path_1.default.dirname(this.sessionIdsFilePath);
95
+ if (!fs_1.default.existsSync(configDir)) {
96
+ fs_1.default.mkdirSync(configDir, { recursive: true });
97
+ }
98
+ this.createSessionIdsFile();
99
+ this.createReadmeFile();
100
+ break;
101
+ case Enums_1.StateStorageType.Redis:
102
+ this.redisPrefix = this.manager.options.stateStorage.redisConfig.prefix?.endsWith(":")
103
+ ? this.manager.options.stateStorage.redisConfig.prefix
104
+ : this.manager.options.stateStorage.redisConfig.prefix ?? "magmastream:";
105
+ break;
106
+ }
98
107
  }
99
- /** Returns if connected to the Node. */
108
+ /**
109
+ * Checks if the Node is currently connected.
110
+ * This method returns true if the Node has an active WebSocket connection, indicating it is ready to receive and process commands.
111
+ */
100
112
  get connected() {
101
113
  if (!this.socket)
102
114
  return false;
103
115
  return this.socket.readyState === ws_1.default.OPEN;
104
116
  }
105
- /** Returns the address for this node. */
117
+ /** Returns the full address for this node, including the host and port. */
106
118
  get address() {
107
119
  return `${this.options.host}:${this.options.port}`;
108
120
  }
@@ -112,11 +124,9 @@ class Node {
112
124
  * the node when resuming a session.
113
125
  */
114
126
  createSessionIdsFile() {
115
- // If the sessionIds.json file does not exist, create it
116
- if (!fs_1.default.existsSync(sessionIdsFilePath)) {
117
- this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Creating sessionId file at: ${sessionIdsFilePath}`);
118
- // Create the file with an empty object as the content
119
- fs_1.default.writeFileSync(sessionIdsFilePath, JSON.stringify({}), "utf-8");
127
+ if (!fs_1.default.existsSync(this.sessionIdsFilePath)) {
128
+ this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Creating sessionId file at: ${this.sessionIdsFilePath}`);
129
+ fs_1.default.writeFileSync(this.sessionIdsFilePath, JSON.stringify({}), "utf-8");
120
130
  }
121
131
  }
122
132
  /**
@@ -127,21 +137,48 @@ class Node {
127
137
  * of the node identifier and cluster ID. This allows multiple clusters to
128
138
  * be used with the same node identifier.
129
139
  */
130
- loadSessionIds() {
131
- // Check if the sessionIds.json file exists
132
- if (fs_1.default.existsSync(sessionIdsFilePath)) {
133
- // Emit a debug event indicating that session IDs are being loaded
134
- this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Loading sessionIds from file: ${sessionIdsFilePath}`);
135
- // Read the content of the sessionIds.json file as a string
136
- const sessionIdsData = fs_1.default.readFileSync(sessionIdsFilePath, "utf-8");
137
- // Parse the JSON string into an object and convert it into a Map
138
- sessionIdsMap = new Map(Object.entries(JSON.parse(sessionIdsData)));
139
- // Check if the session IDs Map contains the session ID for this node
140
- const compositeKey = `${this.options.identifier}::${this.manager.options.clusterId}`;
141
- if (sessionIdsMap.has(compositeKey)) {
142
- // Set the session ID on this node if it exists in the session IDs Map
143
- this.sessionId = sessionIdsMap.get(compositeKey);
140
+ async loadSessionIds() {
141
+ switch (this.manager.options.stateStorage.type) {
142
+ case Enums_1.StateStorageType.JSON: {
143
+ if (fs_1.default.existsSync(this.sessionIdsFilePath)) {
144
+ this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Loading sessionIds from file: ${this.sessionIdsFilePath}`);
145
+ const sessionIdsData = fs_1.default.readFileSync(this.sessionIdsFilePath, "utf-8");
146
+ this.sessionIdsMap = new Map(Object.entries(JSON.parse(sessionIdsData)));
147
+ const compositeKey = `${this.options.identifier}::${this.manager.options.clusterId}`;
148
+ if (this.sessionIdsMap.has(compositeKey)) {
149
+ this.sessionId = this.sessionIdsMap.get(compositeKey);
150
+ }
151
+ }
152
+ break;
144
153
  }
154
+ case Enums_1.StateStorageType.Redis:
155
+ const key = `${this.redisPrefix}node:sessionIds`;
156
+ this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Loading sessionIds from Redis key: ${key}`);
157
+ const currentRaw = await this.manager.redis.get(key);
158
+ if (currentRaw) {
159
+ try {
160
+ const sessionIds = JSON.parse(currentRaw);
161
+ if (typeof sessionIds !== "object" || Array.isArray(sessionIds)) {
162
+ throw new Error("[NODE] loadSessionIds invalid data type from Redis.");
163
+ }
164
+ this.sessionIdsMap = new Map(Object.entries(sessionIds));
165
+ const compositeKey = `${this.options.identifier}::${this.manager.options.clusterId}`;
166
+ if (this.sessionIdsMap.has(compositeKey)) {
167
+ this.sessionId = this.sessionIdsMap.get(compositeKey) || null;
168
+ this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Restored sessionId for ${compositeKey}: ${this.sessionId}`);
169
+ }
170
+ }
171
+ catch (err) {
172
+ this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Failed to parse Redis sessionIds: ${err.message}`);
173
+ this.sessionIdsMap = new Map();
174
+ }
175
+ }
176
+ else {
177
+ this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] No sessionIds found in Redis — creating new key.`);
178
+ await this.manager.redis.set(key, JSON.stringify({}));
179
+ this.sessionIdsMap = new Map();
180
+ }
181
+ break;
145
182
  }
146
183
  }
147
184
  /**
@@ -155,15 +192,43 @@ class Node {
155
192
  * of the node identifier and cluster ID. This allows multiple clusters to
156
193
  * be used with the same node identifier.
157
194
  */
158
- updateSessionId() {
159
- // Emit a debug event indicating that the session IDs are being updated
160
- this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Updating sessionIds to file: ${sessionIdsFilePath}`);
161
- // Create a composite key using identifier and clusterId
162
- const compositeKey = `${this.options.identifier}::${this.manager.options.clusterId}`;
163
- // Update the session IDs Map with the new session ID
164
- sessionIdsMap.set(compositeKey, this.sessionId);
165
- // Write the updated session IDs Map to the sessionIds.json file
166
- fs_1.default.writeFileSync(sessionIdsFilePath, JSON.stringify(Object.fromEntries(sessionIdsMap)));
195
+ async updateSessionId() {
196
+ switch (this.manager.options.stateStorage.type) {
197
+ case Enums_1.StateStorageType.JSON: {
198
+ this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Updating sessionIds to file: ${this.sessionIdsFilePath}`);
199
+ const compositeKey = `${this.options.identifier}::${this.manager.options.clusterId}`;
200
+ this.sessionIdsMap.set(compositeKey, this.sessionId);
201
+ fs_1.default.writeFileSync(this.sessionIdsFilePath, JSON.stringify(Object.fromEntries(this.sessionIdsMap)));
202
+ break;
203
+ }
204
+ case Enums_1.StateStorageType.Redis: {
205
+ const key = `${this.redisPrefix}node:sessionIds`;
206
+ const compositeKey = `${this.options.identifier}::${this.manager.options.clusterId}`;
207
+ this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Updating sessionIds in Redis key: ${key}`);
208
+ const currentRaw = await this.manager.redis.get(key);
209
+ let sessionIds;
210
+ if (currentRaw) {
211
+ try {
212
+ sessionIds = JSON.parse(currentRaw);
213
+ if (typeof sessionIds !== "object" || Array.isArray(sessionIds)) {
214
+ throw new Error("Invalid data type in Redis");
215
+ }
216
+ }
217
+ catch (err) {
218
+ this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Corrupted Redis sessionIds, reinitializing: ${err.message}`);
219
+ sessionIds = {};
220
+ }
221
+ }
222
+ else {
223
+ this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Redis key not found — creating new sessionIds key.`);
224
+ sessionIds = {};
225
+ }
226
+ sessionIds[compositeKey] = this.sessionId;
227
+ this.sessionIdsMap = new Map(Object.entries(sessionIds));
228
+ await this.manager.redis.set(key, JSON.stringify(sessionIds));
229
+ break;
230
+ }
231
+ }
167
232
  }
168
233
  /**
169
234
  * Connects to the Node.
@@ -174,7 +239,8 @@ class Node {
174
239
  * If the node has no session ID but the `enableSessionResumeOption` option is true, it will use the session ID
175
240
  * stored in the sessionIds.json file if it exists.
176
241
  */
177
- connect() {
242
+ async connect() {
243
+ await this.loadSessionIds();
178
244
  if (this.connected)
179
245
  return;
180
246
  const headers = {
@@ -186,8 +252,8 @@ class Node {
186
252
  if (this.sessionId) {
187
253
  headers["Session-Id"] = this.sessionId;
188
254
  }
189
- else if (this.options.enableSessionResumeOption && sessionIdsMap.has(compositeKey)) {
190
- this.sessionId = sessionIdsMap.get(compositeKey) || null;
255
+ else if (this.options.enableSessionResumeOption && this.sessionIdsMap.has(compositeKey)) {
256
+ this.sessionId = this.sessionIdsMap.get(compositeKey) || null;
191
257
  headers["Session-Id"] = this.sessionId;
192
258
  }
193
259
  this.socket = new ws_1.default(`ws${this.options.useSSL ? "s" : ""}://${this.address}/v4/websocket`, { headers });
@@ -222,7 +288,6 @@ class Node {
222
288
  async destroy() {
223
289
  if (!this.connected)
224
290
  return;
225
- // Emit a debug event indicating that the node is being destroyed
226
291
  const debugInfo = {
227
292
  connected: this.connected,
228
293
  identifier: this.options.identifier,
@@ -238,16 +303,11 @@ class Node {
238
303
  await player.autoMoveNode();
239
304
  }
240
305
  }
241
- // Close the WebSocket connection
242
306
  this.socket.close(1000, "destroy");
243
- // Remove all event listeners on the WebSocket
244
307
  this.socket.removeAllListeners();
245
- // Clear the reconnect timeout
246
308
  this.reconnectAttempts = 1;
247
309
  clearTimeout(this.reconnectTimeout);
248
- // Emit a "nodeDestroy" event with the node as the argument
249
310
  this.manager.emit(Enums_1.ManagerEventTypes.NodeDestroy, this);
250
- // Remove the node from the manager
251
311
  this.manager.nodes.delete(this.options.identifier);
252
312
  }
253
313
  /**
@@ -267,7 +327,6 @@ class Node {
267
327
  * @emits {nodeDestroy} - Emits a nodeDestroy event if the maximum number of retry attempts is reached.
268
328
  */
269
329
  async reconnect() {
270
- // Collect debug information regarding the current state of the node
271
330
  const debugInfo = {
272
331
  identifier: this.options.identifier,
273
332
  connected: this.connected,
@@ -275,24 +334,17 @@ class Node {
275
334
  maxRetryAttempts: this.options.maxRetryAttempts,
276
335
  retryDelayMs: this.options.retryDelayMs,
277
336
  };
278
- // Emit a debug event indicating the node is attempting to reconnect
279
337
  this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Reconnecting node: ${JSON.stringify(debugInfo)}`);
280
- // Schedule the reconnection attempt after the specified retry delay
281
338
  this.reconnectTimeout = setTimeout(async () => {
282
- // Check if the maximum number of retry attempts has been reached
283
339
  if (this.reconnectAttempts >= this.options.maxRetryAttempts) {
284
- // Emit an error event and destroy the node if retries are exhausted
285
340
  const error = new Error(`Unable to connect after ${this.options.maxRetryAttempts} attempts.`);
286
341
  this.manager.emit(Enums_1.ManagerEventTypes.NodeError, this, error);
287
342
  return await this.destroy();
288
343
  }
289
- // Remove all listeners from the current WebSocket and reset it
290
344
  this.socket?.removeAllListeners();
291
345
  this.socket = null;
292
- // Emit a nodeReconnect event and attempt to connect again
293
346
  this.manager.emit(Enums_1.ManagerEventTypes.NodeReconnect, this);
294
347
  this.connect();
295
- // Increment the reconnect attempts counter
296
348
  this.reconnectAttempts++;
297
349
  }, this.options.retryDelayMs);
298
350
  }
@@ -313,17 +365,13 @@ class Node {
313
365
  * with the node as the argument.
314
366
  */
315
367
  open() {
316
- // Clear any existing reconnect timeouts
317
368
  if (this.reconnectTimeout)
318
369
  clearTimeout(this.reconnectTimeout);
319
- // Collect debug information regarding the current state of the node
320
370
  const debugInfo = {
321
371
  identifier: this.options.identifier,
322
372
  connected: this.connected,
323
373
  };
324
- // Emit a debug event indicating the node is connected
325
374
  this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Connected node: ${JSON.stringify(debugInfo)}`);
326
- // Emit a "nodeConnect" event with the node as the argument
327
375
  this.manager.emit(Enums_1.ManagerEventTypes.NodeConnect, this);
328
376
  }
329
377
  /**
@@ -345,18 +393,14 @@ class Node {
345
393
  code,
346
394
  reason,
347
395
  };
348
- // Emit a "nodeDisconnect" event with the node and the close event as arguments
349
396
  this.manager.emit(Enums_1.ManagerEventTypes.NodeDisconnect, this, { code, reason });
350
- // Emit a debug event indicating the node is disconnected
351
397
  this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Disconnected node: ${JSON.stringify(debugInfo)}`);
352
- // Try moving all players connected to that node to a useable one
353
398
  if (this.manager.useableNode) {
354
399
  const players = this.manager.players.filter((p) => p.node.options.identifier == this.options.identifier);
355
400
  if (players.size) {
356
401
  await Promise.all(Array.from(players.values(), (player) => player.autoMoveNode()));
357
402
  }
358
403
  }
359
- // If the close event was not initiated by the user, attempt to reconnect
360
404
  if (code !== 1000 || reason !== "destroy")
361
405
  await this.reconnect();
362
406
  }
@@ -371,14 +415,11 @@ class Node {
371
415
  error(error) {
372
416
  if (!error)
373
417
  return;
374
- // Collect debug information regarding the error
375
418
  const debugInfo = {
376
419
  identifier: this.options.identifier,
377
420
  error: error.message,
378
421
  };
379
- // Emit a debug event indicating the error on the node
380
422
  this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Error on node: ${JSON.stringify(debugInfo)}`);
381
- // Emit a "nodeError" event with the node and the error as arguments
382
423
  this.manager.emit(Enums_1.ManagerEventTypes.NodeError, this, error);
383
424
  }
384
425
  /**
@@ -406,7 +447,7 @@ class Node {
406
447
  this.stats = { ...payload };
407
448
  break;
408
449
  case "playerUpdate":
409
- player = await this.manager.players.get(payload.guildId);
450
+ player = this.manager.players.get(payload.guildId);
410
451
  if (player && player.node.options.identifier !== this.options.identifier) {
411
452
  return;
412
453
  }
@@ -414,7 +455,7 @@ class Node {
414
455
  player.position = payload.state.position || 0;
415
456
  break;
416
457
  case "event":
417
- player = await this.manager.players.get(payload.guildId);
458
+ player = this.manager.players.get(payload.guildId);
418
459
  if (player && player.node.options.identifier !== this.options.identifier) {
419
460
  return;
420
461
  }
@@ -425,11 +466,9 @@ class Node {
425
466
  this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Node message: ${JSON.stringify(payload)}`);
426
467
  this.rest.setSessionId(payload.sessionId);
427
468
  this.sessionId = payload.sessionId;
428
- this.updateSessionId(); // Call to update session ID
469
+ await this.updateSessionId();
429
470
  this.info = await this.fetchInfo();
430
- // Log if the session was resumed successfully
431
471
  if (payload.resumed) {
432
- // Load player states from the JSON file
433
472
  await this.manager.loadPlayerStates(this.options.identifier);
434
473
  }
435
474
  if (this.options.enableSessionResumeOption) {
@@ -453,7 +492,7 @@ class Node {
453
492
  async handleEvent(payload) {
454
493
  if (!payload.guildId)
455
494
  return;
456
- const player = await this.manager.players.get(payload.guildId);
495
+ const player = this.manager.players.get(payload.guildId);
457
496
  if (!player)
458
497
  return;
459
498
  const track = await player.queue.getCurrent();
@@ -560,18 +599,14 @@ class Node {
560
599
  }
561
600
  player.set("skipFlag", false);
562
601
  const oldPlayer = player;
563
- // Handle track end events
564
602
  switch (reason) {
565
603
  case Enums_1.TrackEndReasonTypes.LoadFailed:
566
604
  case Enums_1.TrackEndReasonTypes.Cleanup:
567
- // Handle the case when a track failed to load or was cleaned up
568
605
  await this.handleFailedTrack(player, track, payload);
569
606
  break;
570
607
  case Enums_1.TrackEndReasonTypes.Replaced:
571
- // Handle the case when a track was replaced
572
608
  break;
573
609
  case Enums_1.TrackEndReasonTypes.Stopped:
574
- // If the track was forcibly replaced
575
610
  if (await player.queue.size()) {
576
611
  await this.playNextTrack(player, track, payload);
577
612
  }
@@ -686,9 +721,7 @@ class Node {
686
721
  }
687
722
  // Move to the next track
688
723
  await queue.setCurrent(await queue.dequeue());
689
- // Emit track end event
690
724
  this.manager.emit(Enums_1.ManagerEventTypes.TrackEnd, player, track, payload);
691
- // If the track was stopped manually and no more tracks exist, end the queue
692
725
  if (payload.reason === Enums_1.TrackEndReasonTypes.Stopped) {
693
726
  const next = await queue.dequeue();
694
727
  await queue.setCurrent(next ?? null);
@@ -697,7 +730,6 @@ class Node {
697
730
  return;
698
731
  }
699
732
  }
700
- // If autoplay is enabled, play the next track
701
733
  if (playNextOnEnd)
702
734
  await player.play();
703
735
  }
@@ -715,9 +747,7 @@ class Node {
715
747
  async playNextTrack(player, track, payload) {
716
748
  // Shift the queue to set the next track as current
717
749
  await player.queue.setCurrent(await player.queue.dequeue());
718
- // Emit the track end event
719
750
  this.manager.emit(Enums_1.ManagerEventTypes.TrackEnd, player, track, payload);
720
- // If autoplay is enabled, play the next track
721
751
  if (this.manager.options.playNextOnEnd)
722
752
  await player.play();
723
753
  }
@@ -745,7 +775,6 @@ class Node {
745
775
  return;
746
776
  attempt++;
747
777
  }
748
- // If all attempts fail, reset the player state and emit queueEnd
749
778
  player.playing = false;
750
779
  this.manager.emit(Enums_1.ManagerEventTypes.QueueEnd, player, track, payload);
751
780
  }
@@ -690,6 +690,7 @@ class Player {
690
690
  // Get and remove the most recent previous track
691
691
  const lastTrack = await this.queue.popPrevious();
692
692
  if (!lastTrack) {
693
+ await this.queue.clearPrevious();
693
694
  throw new Error("No previous track available.");
694
695
  }
695
696
  // Capture the current state of the player before making changes.
@@ -263,9 +263,11 @@ class AutoPlayUtils {
263
263
  */
264
264
  static async getRecommendedTracksFromSource(track, platform) {
265
265
  const requester = track.requester;
266
+ const parsedURL = new URL(track.uri);
266
267
  switch (platform) {
267
268
  case Enums_1.AutoPlayPlatform.Spotify: {
268
- if (!track.uri.includes("spotify")) {
269
+ const allowedSpotifyHosts = ["open.spotify.com", "www.spotify.com"];
270
+ if (!allowedSpotifyHosts.includes(parsedURL.host)) {
269
271
  const resolvedTrack = await this.resolveFirstTrackFromQuery(`${track.author} - ${track.title}`, Enums_1.SearchPlatform.Spotify, requester);
270
272
  if (!resolvedTrack)
271
273
  return [];
@@ -282,7 +284,8 @@ class AutoPlayUtils {
282
284
  return tracks;
283
285
  }
284
286
  case Enums_1.AutoPlayPlatform.Deezer: {
285
- if (!track.uri.includes("deezer")) {
287
+ const allowedDeezerHosts = ["deezer.com", "www.deezer.com", "www.deezer.page.link"];
288
+ if (!allowedDeezerHosts.includes(parsedURL.host)) {
286
289
  const resolvedTrack = await this.resolveFirstTrackFromQuery(`${track.author} - ${track.title}`, Enums_1.SearchPlatform.Deezer, requester);
287
290
  if (!resolvedTrack)
288
291
  return [];
@@ -294,7 +297,8 @@ class AutoPlayUtils {
294
297
  return tracks;
295
298
  }
296
299
  case Enums_1.AutoPlayPlatform.SoundCloud: {
297
- if (!track.uri.includes("soundcloud")) {
300
+ const allowedSoundCloudHosts = ["soundcloud.com", "www.soundcloud.com"];
301
+ if (!allowedSoundCloudHosts.includes(parsedURL.host)) {
298
302
  const resolvedTrack = await this.resolveFirstTrackFromQuery(`${track.author} - ${track.title}`, Enums_1.SearchPlatform.SoundCloud, requester);
299
303
  if (!resolvedTrack)
300
304
  return [];
@@ -345,7 +349,8 @@ class AutoPlayUtils {
345
349
  }
346
350
  }
347
351
  case Enums_1.AutoPlayPlatform.YouTube: {
348
- const hasYouTubeURL = ["youtube.com", "youtu.be"].some((url) => track.uri.includes(url));
352
+ const allowedYouTubeHosts = ["youtube.com", "youtu.be"];
353
+ const hasYouTubeURL = allowedYouTubeHosts.some((url) => track.uri.includes(url));
349
354
  let videoID = null;
350
355
  if (hasYouTubeURL) {
351
356
  videoID = track.uri.split("=").pop();
@@ -370,7 +375,8 @@ class AutoPlayUtils {
370
375
  return filteredTracks;
371
376
  }
372
377
  case Enums_1.AutoPlayPlatform.Tidal: {
373
- if (!track.uri.includes("tidal")) {
378
+ const allowedTidalHosts = ["tidal.com", "www.tidal.com"];
379
+ if (!allowedTidalHosts.includes(parsedURL.host)) {
374
380
  const resolvedTrack = await this.resolveFirstTrackFromQuery(`${track.author} - ${track.title}`, Enums_1.SearchPlatform.Tidal, requester);
375
381
  if (!resolvedTrack)
376
382
  return [];
@@ -382,7 +388,8 @@ class AutoPlayUtils {
382
388
  return tracks;
383
389
  }
384
390
  case Enums_1.AutoPlayPlatform.VKMusic: {
385
- if (!track.uri.includes("vk.com") && !track.uri.includes("vk.ru")) {
391
+ const allowedVKHosts = ["vk.com", "www.vk.com", "vk.ru", "www.vk.ru"];
392
+ if (!allowedVKHosts.includes(parsedURL.host)) {
386
393
  const resolvedTrack = await this.resolveFirstTrackFromQuery(`${track.author} - ${track.title}`, Enums_1.SearchPlatform.VKMusic, requester);
387
394
  if (!resolvedTrack)
388
395
  return [];
@@ -394,7 +401,8 @@ class AutoPlayUtils {
394
401
  return tracks;
395
402
  }
396
403
  case Enums_1.AutoPlayPlatform.Qobuz: {
397
- if (!track.uri.includes("qobuz.com")) {
404
+ const allowedQobuzHosts = ["qobuz.com", "www.qobuz.com", "play.qobuz.com"];
405
+ if (!allowedQobuzHosts.includes(parsedURL.host)) {
398
406
  const resolvedTrack = await this.resolveFirstTrackFromQuery(`${track.author} - ${track.title}`, Enums_1.SearchPlatform.Qobuz, requester);
399
407
  if (!resolvedTrack)
400
408
  return [];
@@ -522,24 +530,48 @@ class AutoPlayUtils {
522
530
  // console.error("[Spotify] Failed to launch Playwright:", err);
523
531
  // }
524
532
  // }
533
+ static isPlaylistRawData(data) {
534
+ return typeof data === "object" && data !== null && Array.isArray(data.tracks);
535
+ }
536
+ static isTrackData(data) {
537
+ return typeof data === "object" && data !== null && "encoded" in data && "info" in data;
538
+ }
539
+ static isTrackDataArray(data) {
540
+ return (Array.isArray(data) &&
541
+ data.every((track) => typeof track === "object" && track !== null && "encoded" in track && "info" in track && typeof track.encoded === "string"));
542
+ }
525
543
  static buildTracksFromResponse(recommendedResult, requester) {
526
544
  if (!recommendedResult)
527
545
  return [];
528
546
  if (TrackUtils.isErrorOrEmptySearchResult(recommendedResult))
529
547
  return [];
530
548
  switch (recommendedResult.loadType) {
531
- case Enums_1.LoadTypes.Search: {
532
- const tracks = recommendedResult.data.map((t) => TrackUtils.build(t, requester));
533
- return tracks;
534
- }
535
549
  case Enums_1.LoadTypes.Track: {
536
- const track = TrackUtils.build(recommendedResult.data, requester);
537
- return [track];
550
+ const data = recommendedResult.data;
551
+ if (!this.isTrackData(data)) {
552
+ throw new Error("[TrackBuilder] Invalid TrackData object.");
553
+ }
554
+ return [TrackUtils.build(data, requester)];
555
+ }
556
+ case Enums_1.LoadTypes.Short:
557
+ case Enums_1.LoadTypes.Search: {
558
+ const data = recommendedResult.data;
559
+ if (!this.isTrackDataArray(data)) {
560
+ throw new Error("[TrackBuilder] Invalid TrackData[] array for LoadTypes.Search or Short.");
561
+ }
562
+ return data.map((d) => TrackUtils.build(d, requester));
538
563
  }
564
+ case Enums_1.LoadTypes.Album:
565
+ case Enums_1.LoadTypes.Artist:
566
+ case Enums_1.LoadTypes.Station:
567
+ case Enums_1.LoadTypes.Podcast:
568
+ case Enums_1.LoadTypes.Show:
539
569
  case Enums_1.LoadTypes.Playlist: {
540
- const playlistData = recommendedResult.data;
541
- const tracks = playlistData.tracks.map((t) => TrackUtils.build(t, requester));
542
- return tracks;
570
+ const data = recommendedResult.data;
571
+ if (this.isPlaylistRawData(data)) {
572
+ return data.tracks.map((d) => TrackUtils.build(d, requester));
573
+ }
574
+ throw new Error(`[TrackBuilder] Invalid playlist data for loadType: ${recommendedResult.loadType}`);
543
575
  }
544
576
  default:
545
577
  throw new Error(`[TrackBuilder] Unsupported loadType: ${recommendedResult.loadType}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magmastream",
3
- "version": "2.9.0-dev.37",
3
+ "version": "2.9.0-dev.39",
4
4
  "description": "A user-friendly Lavalink client designed for NodeJS.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -95,4 +95,4 @@
95
95
  "homepage": "https://docs.magmastream.com",
96
96
  "author": "Abel Purnwasy",
97
97
  "license": "Apache-2.0"
98
- }
98
+ }