aqualink 1.3.0 → 1.4.0

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/README.md CHANGED
@@ -25,16 +25,19 @@ This code is based in riffy, but its an 100% Rewrite made from scratch...
25
25
 
26
26
  - Example bot: https://github.com/ToddyTheNoobDud/Thorium-Music
27
27
 
28
- # Yay, Version 1.3.0 is released ! aqualink so cool
29
-
30
- + Updated NODE with more methods (lavalinkLoadPercentage, freePercentage, usedPercentage) + Optimizations
31
- + Rewrited Aqua.js fully (rewrite events, options, and resolve)
32
- + Rewrite FetchImage (way more faster, less recourse intensive, better looking)
33
- + Convert REST to Convert to undici request (experimental, around 221.29 % faster, 12436.39 by difference (requests per min))
34
- ^^ Also is faster and more memory efficient...
35
- + Updated Player with one more method (added player.skip())
36
- + Remade all the cache system (players joined to vc shall don't use ram, improved cache saving and speed almost)
37
- + Remade the TRACK handler (improves track handling, speed, recourses usage and cleaning)
28
+ # Yay, Version 1.4.0 is released ! aqualink so cool
29
+
30
+ - Misc changes to TRACK handler (fixed some bugs, improved overall speed and caching)
31
+ - Reworked some stuff from FetchImage (improves speed, made the requests dynamic, remove useless caching)
32
+ - Rewrited Player.js (Detailed: Rewrite Events (use switch, new methods), Rewrite disconnect, stop and destroy, improve cleanUp system)
33
+
34
+ ^^ Also fixes a lot of bugs (example: create a connection first, needs an connection, and others i forgot)
35
+
36
+ - Rewrite the REST system again (uses async, dynamic requests, and re-utilizes some stuff for better speed)
37
+ - Remade NODE manager (this brings better speed, dynamic requests, better caching system, and faster connecting speeds)
38
+ - Rewrited the AQUA manager (this makes the resolve 2x faster, uses better functions, auto clean up system, better overall manager and faster)
39
+ - Misc Fixes: fixed the queue looping system, improved the finished handler, better error handler, more automations
40
+
38
41
 
39
42
  # How to install
40
43
 
@@ -1,39 +1,49 @@
1
- const undici = require("undici");
1
+ const { request } = require("undici");
2
+
3
+ const YOUTUBE_URLS = [
4
+ (id) => `https://img.youtube.com/vi/${id}/maxresdefault.jpg`,
5
+ (id) => `https://img.youtube.com/vi/${id}/hqdefault.jpg`,
6
+ (id) => `https://img.youtube.com/vi/${id}/mqdefault.jpg`,
7
+ (id) => `https://img.youtube.com/vi/${id}/default.jpg`,
8
+ ];
2
9
 
3
10
  async function getImageUrl(info) {
4
11
  if (!info || !info.sourceName || !info.uri) return null;
5
12
 
6
13
  switch (info.sourceName.toLowerCase()) {
7
14
  case "spotify":
8
- return (await fetchFromUrl(`https://open.spotify.com/oembed?url=${info.uri}`))?.json().then(json => json.thumbnail_url || null);
9
-
10
- case "soundcloud":
11
- return (await fetchFromUrl(`https://soundcloud.com/oembed?format=json&url=${info.uri}`))?.json().then(json => json.thumbnail_url || null);
12
-
15
+ return await fetchThumbnail(`https://open.spotify.com/oembed?url=${info.uri}`);
13
16
  case "youtube":
14
- const urls = [
15
- `https://img.youtube.com/vi/${info.identifier}/maxresdefault.jpg`,
16
- `https://img.youtube.com/vi/${info.identifier}/hqdefault.jpg`,
17
- `https://img.youtube.com/vi/${info.identifier}/mqdefault.jpg`,
18
- `https://img.youtube.com/vi/${info.identifier}/default.jpg`,
19
- ];
20
-
21
- const firstValidUrl = await Promise.any(urls.map(url => fetchFromUrl(url)));
22
- return firstValidUrl ? firstValidUrl.url : null;
23
-
17
+ return await fetchYouTubeThumbnail(info.identifier);
24
18
  default:
25
19
  return null;
26
20
  }
27
21
  }
28
22
 
29
- async function fetchFromUrl(url) {
23
+ async function fetchThumbnail(url) {
30
24
  try {
31
- const response = await undici.fetch(url, { cache: "force-cache" });
32
- return response.ok ? response : null;
25
+ const response = await request(url, { method: "GET" });
26
+ if (response.ok) {
27
+ const json = await response.json();
28
+ return json.thumbnail_url || null;
29
+ }
30
+ return null;
33
31
  } catch (error) {
34
32
  console.error(`Error fetching ${url}:`, error);
35
33
  return null;
36
34
  }
37
35
  }
38
36
 
39
- module.exports = { getImageUrl };
37
+ async function fetchYouTubeThumbnail(identifier) {
38
+ const fetchPromises = YOUTUBE_URLS.map(urlFunc => fetchThumbnail(urlFunc(identifier)));
39
+ const results = await Promise.allSettled(fetchPromises);
40
+
41
+ for (const result of results) {
42
+ if (result.status === "fulfilled" && result.value) {
43
+ return result.value;
44
+ }
45
+ }
46
+ return null;
47
+ }
48
+
49
+ module.exports = { getImageUrl };
@@ -5,57 +5,36 @@ const { Track } = require("./Track");
5
5
  const { version: pkgVersion } = require("../../package.json");
6
6
 
7
7
  class Aqua extends EventEmitter {
8
- /**
9
- * @param {Object} client - The client instance.
10
- * @param {Array<Object>} nodes - An array of node configurations.
11
- * @param {Object} options - Configuration options for Aqua.
12
- * @param {Function} options.send - Function to send data.
13
- * @param {string} [options.defaultSearchPlatform="ytsearch"] - Default search platform.
14
- * @param {string} [options.restVersion="v4"] - Version of the REST API.
15
- * @param {Array<Object>} [options.plugins=[]] - Plugins to load.
16
- * @param {string} [options.shouldDeleteMessage='none'] - Should delete your message? (true, false)
17
- */
18
8
  constructor(client, nodes, options) {
19
9
  super();
20
- if (!client) throw new Error("Client is required to initialize Aqua");
21
- if (!Array.isArray(nodes) || nodes.length === 0) throw new Error(`Nodes must be a non-empty Array (Received ${typeof nodes})`);
22
- if (typeof options.send !== "function") throw new Error("Send function is required to initialize Aqua");
23
-
10
+ this.validateInputs(client, nodes, options);
24
11
  this.client = client;
25
12
  this.nodes = nodes;
26
13
  this.nodeMap = new Map();
27
14
  this.players = new Map();
28
15
  this.clientId = null;
29
16
  this.initiated = false;
30
- this.sessionId = null;
31
- this.shouldDeleteMessage = options.shouldDeleteMessage || "false";
32
- this.defaultSearchPlatform = options.defaultSearchPlatform || "ytmsearch";
17
+ this.shouldDeleteMessage = options.shouldDeleteMessage || false;
18
+ this.defaultSearchPlatform = options.defaultSearchPlatform || "ytsearch";
33
19
  this.restVersion = options.restVersion || "v3";
34
20
  this.plugins = options.plugins || [];
35
21
  this.version = pkgVersion;
36
- this.loadType = null;
37
- this.tracks = [];
38
- this.playlistInfo = null;
39
- this.pluginInfo = null;
40
22
  this.options = options;
41
- this.send = options.send || null;
23
+ this.send = options.send;
24
+ }
25
+
26
+ validateInputs(client, nodes, options) {
27
+ if (!client) throw new Error("Client is required to initialize Aqua");
28
+ if (!Array.isArray(nodes) || nodes.length === 0) throw new Error(`Nodes must be a non-empty Array (Received ${typeof nodes})`);
29
+ if (typeof options.send !== "function") throw new Error("Send function is required to initialize Aqua");
42
30
  }
43
31
 
44
- /**
45
- * Gets the least used nodes based on call count.
46
- * @returns {Array<Node>} Array of least used nodes.
47
- */
48
32
  get leastUsedNodes() {
49
33
  return [...this.nodeMap.values()]
50
34
  .filter(node => node.connected)
51
35
  .sort((a, b) => a.rest.calls - b.rest.calls);
52
36
  }
53
37
 
54
- /**
55
- * Initializes Aqua with the provided client ID.
56
- * @param {string} clientId - The client ID
57
- * @returns {Aqua} The Aqua instance.
58
- */
59
38
  init(clientId) {
60
39
  if (this.initiated) return this;
61
40
  this.clientId = clientId;
@@ -65,11 +44,6 @@ class Aqua extends EventEmitter {
65
44
  return this;
66
45
  }
67
46
 
68
- /**
69
- * Creates a new node with the specified options.
70
- * @param {Object} options - The configuration for the node.
71
- * @returns {Node} The created node instance.
72
- */
73
47
  createNode(options) {
74
48
  const node = new Node(this, options, this.options);
75
49
  this.nodeMap.set(options.name || options.host, node);
@@ -78,215 +52,159 @@ class Aqua extends EventEmitter {
78
52
  return node;
79
53
  }
80
54
 
81
- /**
82
- * Destroys a node identified by the given identifier.
83
- * @param {string} identifier - The identifier of the node to destroy.
84
- */
85
55
  destroyNode(identifier) {
86
56
  const node = this.nodeMap.get(identifier);
87
- if (!node) return;
88
- node.disconnect();
89
- this.nodeMap.delete(identifier);
90
- this.emit("nodeDestroy", node);
57
+ if (node) {
58
+ node.disconnect();
59
+ this.nodeMap.delete(identifier);
60
+ this.emit("nodeDestroy", node);
61
+ }
91
62
  }
92
63
 
93
- /**
94
- * Updates the voice state based on the received packet.
95
- * @param {Object} packet - The packet containing voice state information.
96
- */
97
64
  updateVoiceState(packet) {
98
65
  const player = this.players.get(packet.d.guild_id);
99
- if (!player) return;
100
- if (packet.t === "VOICE_SERVER_UPDATE") player.connection.setServerUpdate(packet.d);
101
- else if (packet.t === "VOICE_STATE_UPDATE" && packet.d.user_id === this.clientId) player.connection.setStateUpdate(packet.d);
66
+ if (player) {
67
+ if (packet.t === "VOICE_SERVER_UPDATE") {
68
+ player.connection.setServerUpdate(packet.d);
69
+ } else if (packet.t === "VOICE_STATE_UPDATE" && packet.d.user_id === this.clientId) {
70
+ player.connection.setStateUpdate(packet.d);
71
+ if (packet.d.status === "disconnected") {
72
+ this.cleanupPlayer(player); // Cleanup when disconnected
73
+ }
74
+ }
75
+ }
102
76
  }
103
77
 
104
- /**
105
- * Fetches nodes by the specified region.
106
- * @param {string} region - The region to filter nodes by.
107
- * @returns {Array<Node>} Array of nodes in the specified region.
108
- */
109
78
  fetchRegion(region) {
110
- const nodesByRegion = [...this.nodeMap.values()]
111
- .filter(node => node.connected && node.regions?.includes(region?.toLowerCase()))
112
- .sort((a, b) => {
113
- const aLoad = a.stats.cpu ? (a.stats.cpu.systemLoad / a.stats.cpu.cores) * 100 : 0;
114
- const bLoad = b.stats.cpu ? (b.stats.cpu.systemLoad / b.stats.cpu.cores) * 100 : 0;
115
- return aLoad - bLoad;
116
- });
117
- return nodesByRegion;
79
+ const lowerRegion = region?.toLowerCase();
80
+ return [...this.nodeMap.values()]
81
+ .filter(node => node.connected && node.regions?.includes(lowerRegion))
82
+ .sort((a, b) => this.calculateLoad(a) - this.calculateLoad(b));
118
83
  }
119
84
 
120
- /**
121
- * Creates a connection for a player.
122
- * @param {Object} options - Connection options.
123
- * @param {string} options.guildId - The ID of the guild.
124
- * @param {string} [options.region] - The region to connect to.
125
- * @returns {Player} The created player instance.
126
- */
127
- createConnection(options) {
128
- if (!this.initiated) throw new Error("BRO! Get aqua on before !!!");
129
- if (!this.leastUsedNodes.length) throw new Error("No nodes are available");
85
+ calculateLoad(node) {
86
+ return node.stats.cpu ? (node.stats.cpu.systemLoad / node.stats.cpu.cores) * 100 : 0;
87
+ }
130
88
 
89
+ createConnection(options) {
90
+ this.ensureInitialized();
131
91
  const player = this.players.get(options.guildId);
132
92
  if (player && player.voiceChannel) return player;
133
-
134
- const node = (options.region ? this.fetchRegion(options.region) : this.leastUsedNodes)[0];
93
+ const node = options.region ? this.fetchRegion(options.region)[0] : this.leastUsedNodes[0];
135
94
  if (!node) throw new Error("No nodes are available");
136
-
137
95
  return this.createPlayer(node, options);
138
96
  }
139
97
 
140
- /**
141
- * Creates a player using the specified node.
142
- * @param {Node} node - The node to create the player with.
143
- * @param {Object} options - The player options.
144
- * @returns {Player} The created player instance.
145
- */
146
98
  createPlayer(node, options) {
147
99
  const player = new Player(this, node, options);
148
100
  this.players.set(options.guildId, player);
149
101
  player.connect(options);
102
+ player.on("destroy", () => this.cleanupPlayer(player)); // Listen for player destruction
150
103
  this.emit("playerCreate", player);
151
104
  return player;
152
105
  }
153
106
 
154
- /**
155
- * Destroys the player associated with the given guild ID.
156
- * @param {string} guildId - The ID of the guild.
157
- */
158
107
  destroyPlayer(guildId) {
159
108
  const player = this.players.get(guildId);
160
- player.clearData();
161
- if (!player) return;
162
- player.destroy();
163
- this.players.delete(guildId);
164
- this.emit("playerDestroy", player);
165
- }
166
-
167
- /**
168
- * Removes the connection for the specified guild ID.
169
- * @param {string} guildId - The ID of the guild.
170
- */
171
- removeConnection(guildId) {
172
- const player = this.players.get(guildId);
173
- player.clearData();
174
109
  if (player) {
110
+ player.clearData();
175
111
  player.destroy();
176
112
  this.players.delete(guildId);
113
+ this.emit("playerDestroy", player);
177
114
  }
178
115
  }
179
116
 
180
117
  /**
181
- * Resolves a query to tracks using the available nodes.
182
- * @param {Object} options - The options for resolving tracks.
183
- * @param {string} options.query - The query string to resolve.
184
- * @param {string} [options.source] - The source of the query.
185
- * @param {Object} [options.requester] - The requester of the query.
186
- * @param {string|Node} [options.nodes] - Specific nodes to use for the request.
187
- * @returns {Promise<Object>} The resolved tracks and related information.
118
+ * Resolve a track into an array of {@link Track} objects.
119
+ *
120
+ * @param {Object} options - The options for the resolve operation.
121
+ * @param {string} options.query - The query to search for.
122
+ * @param {string} [options.source] - The source to use for the search. Defaults to the bot's default search platform.
123
+ * @param {GuildMember} options.requester - The member who requested the track.
124
+ * @param {Node[]} [options.nodes] - The nodes to prioritize for the search. Defaults to all nodes.
125
+ * @returns {Promise<Track[]>} The resolved tracks.
188
126
  */
189
127
  async resolve({ query, source, requester, nodes }) {
190
- if (!this.initiated) throw new Error("Aqua must be initialized before resolving");
191
- if (nodes && (typeof nodes !== "string" && !(nodes instanceof Node))) {
192
- throw new Error(`'nodes' must be a string or Node instance, but received: ${typeof nodes}`);
128
+ this.ensureInitialized();
129
+ const requestNode = this.getRequestNode(nodes);
130
+ const formattedQuery = this.formatQuery(query, source || this.defaultSearchPlatform);
131
+ try {
132
+ let response = await requestNode.rest.makeRequest("GET", `/v4/loadtracks?identifier=${encodeURIComponent(formattedQuery)}`);
133
+ if (["empty", "NO_MATCHES"].includes(response.loadType)) {
134
+ response = await this.handleNoMatches(requestNode.rest, query);
135
+ }
136
+ return this.constructorResponse(response, requester, requestNode);
137
+ } catch (error) {
138
+ console.error("Error resolving track:", error);
139
+ throw new Error("Failed to resolve track");
193
140
  }
141
+ }
194
142
 
195
- const searchSources = source || this.defaultSearchPlatform;
196
- const requestNode = (typeof nodes === 'string' ? this.nodeMap.get(nodes) : nodes) || this.leastUsedNodes[0];
197
- if (!requestNode) throw new Error("No nodes are available.");
198
-
199
- const formattedQuery = /^https?:\/\//.test(query) ? query : `${searchSources}:${query}`;
200
- let response = await requestNode.rest.makeRequest("GET", `/v4/loadtracks?identifier=${encodeURIComponent(formattedQuery)}`);
143
+ ensureInitialized() {
144
+ if (!this.initiated) throw new Error("Aqua must be initialized before this operation");
145
+ }
201
146
 
202
- // Fallback attempts if response loadType indicates no matches
203
- if (["empty", "NO_MATCHES"].includes(response.loadType)) {
204
- response = await this.handleNoMatches(requestNode.rest, query);
147
+ getRequestNode(nodes) {
148
+ if (nodes && (typeof nodes !== "string" && !(nodes instanceof Node))) {
149
+ throw new Error(`'nodes' must be a string or Node instance, but received: ${typeof nodes}`);
205
150
  }
151
+ return (typeof nodes === 'string' ? this.nodeMap.get(nodes) : nodes) || this.leastUsedNodes[0];
152
+ }
206
153
 
207
- return this.constructorResponse(response, requester, requestNode);
154
+ formatQuery(query, source) {
155
+ return /^https?:\/\//.test(query) ? query : `${source}:${query}`;
208
156
  }
209
157
 
210
- /**
211
- * Handles cases where no matches were found for a query.
212
- * @param {Object} rest - The REST client for making requests.
213
- * @param {string} query - The original query string.
214
- * @returns {Promise<Object>} The response object from the request.
215
- */
216
158
  async handleNoMatches(rest, query) {
217
- let response = await rest.makeRequest("GET", `/v4/loadtracks?identifier=https://open.spotify.com/track/${query}`);
218
- if (["empty", "NO_MATCHES"].includes(response.loadType)) {
219
- response = await rest.makeRequest("GET", `/v4/loadtracks?identifier=https://www.youtube.com/watch?v=${query}`);
159
+ try {
160
+ const spotifyResponse = await rest.makeRequest("GET", `/v4/loadtracks?identifier=https://open.spotify.com/track/${query}`);
161
+ if (["empty", "NO_MATCHES"].includes(spotifyResponse.loadType)) {
162
+ return await rest.makeRequest("GET", `/v4/loadtracks?identifier=https://www.youtube.com/watch?v=${query}`);
163
+ }
164
+ return spotifyResponse;
165
+ } catch (error) {
166
+ console.error("Error handling no matches:", error);
167
+ throw new Error("Failed to handle no matches");
220
168
  }
221
- return response;
222
169
  }
223
170
 
224
- /**
225
- * Loads tracks from the resolved response.
226
- * @param {Object} response - The response from the track resolution.
227
- * @param {Object} requester - The requester of the tracks.
228
- * @param {Node} requestNode - The node that handled the request.
229
- * @returns {Object} The constructed response.
230
- */
231
171
  constructorResponse(response, requester, requestNode) {
172
+ const baseResponse = {
173
+ loadType: response.loadType,
174
+ exception: null,
175
+ playlistInfo: null,
176
+ pluginInfo: response.pluginInfo || {},
177
+ tracks: [],
178
+ };
232
179
  switch (response.loadType) {
233
180
  case "track":
234
181
  if (response.data) {
235
- return {
236
- loadType: response.loadType,
237
- exception: null,
238
- playlistInfo: null,
239
- pluginInfo: response.pluginInfo || {},
240
- tracks: [new Track(response.data, requester, requestNode)],
241
- };
182
+ baseResponse.tracks.push(new Track(response.data, requester, requestNode));
242
183
  }
243
184
  break;
244
185
  case "playlist":
245
- return {
246
- loadType: response.loadType,
247
- exception: null,
248
- playlistInfo: {
249
- name: response.data?.info?.name || response.data?.info?.title,
250
- ...response.data?.info,
251
- },
252
- pluginInfo: response.pluginInfo || {},
253
- tracks: response.data?.tracks?.map(track => new Track(track, requester, requestNode)) || [],
186
+ baseResponse.playlistInfo = {
187
+ name: response.data?.info?.name || response.data?.info?.title,
188
+ ...response.data?.info,
254
189
  };
190
+ baseResponse.tracks = response.data?.tracks?.map(track => new Track(track, requester, requestNode)) || [];
191
+ break;
255
192
  case "search":
256
- return {
257
- loadType: response.loadType,
258
- exception: null,
259
- playlistInfo: null,
260
- pluginInfo: response.pluginInfo || {},
261
- tracks: response.data?.map(track => new Track(track, requester, requestNode)),
262
- };
193
+ baseResponse.tracks = response.data?.map(track => new Track(track, requester, requestNode)) || [];
194
+ break;
263
195
  }
264
-
265
- return {
266
- loadType: response.loadType,
267
- exception: response.loadType === "error" ? response.loadType.data : (response.loadType === "LOAD_FAILED" ? response.loadType.exception : null),
268
- playlistInfo: null,
269
- pluginInfo: response.pluginInfo || {},
270
- tracks: [],
271
- };
196
+ if (response.loadType === "error" || response.loadType === "LOAD_FAILED") {
197
+ baseResponse.exception = response.loadType.data || response.loadType.exception;
198
+ }
199
+ return baseResponse;
272
200
  }
273
201
 
274
-
275
- /**
276
- * Gets the player associated with the specified guild ID.
277
- * @param {string} guildId - The ID of the guild.
278
- * @returns {Player} The player instance.
279
- * @throws {Error} If the player is not found.
280
- */
281
202
  get(guildId) {
282
203
  const player = this.players.get(guildId);
283
204
  if (!player) throw new Error(`Player not found for guild ID: ${guildId}`);
284
205
  return player;
285
206
  }
286
207
 
287
- /**
288
- * Cleans up idle players and nodes to free up resources.
289
- */
290
208
  cleanupIdle() {
291
209
  for (const [guildId, player] of this.players) {
292
210
  if (!player.playing && !player.paused && player.queue.isEmpty()) {
@@ -296,6 +214,15 @@ class Aqua extends EventEmitter {
296
214
  }
297
215
  }
298
216
  }
217
+
218
+ cleanupPlayer(player) {
219
+ if (player) {
220
+ player.clearData();
221
+ player.destroy();
222
+ this.players.delete(player.guildId);
223
+ this.emit("playerDestroy", player);
224
+ }
225
+ }
299
226
  }
300
227
 
301
- module.exports = { Aqua };
228
+ module.exports = { Aqua };
@@ -26,7 +26,7 @@ class Node {
26
26
  this.resumeKey = options.resumeKey || null;
27
27
  this.resumeTimeout = options.resumeTimeout || 60;
28
28
  this.autoResume = options.autoResume || false;
29
- this.reconnectTimeout = options.reconnectTimeout || 5000;
29
+ this.reconnectTimeout = options.reconnectTimeout || 2000;
30
30
  this.reconnectTries = options.reconnectTries || 3;
31
31
  this.reconnectAttempted = 0;
32
32
  this.lastStatsRequest = 0; // Track the last time stats were requested
@@ -42,17 +42,21 @@ class Node {
42
42
  used: 0,
43
43
  allocated: 0,
44
44
  reservable: 0,
45
+ freePercentage: 0,
46
+ usedPercentage: 0,
45
47
  },
46
48
  cpu: {
47
49
  cores: 0,
48
50
  systemLoad: 0,
49
51
  lavalinkLoad: 0,
52
+ lavalinkLoadPercentage: 0,
50
53
  },
51
54
  frameStats: {
52
55
  sent: 0,
53
56
  nulled: 0,
54
57
  deficit: 0,
55
58
  },
59
+ ping: 0,
56
60
  };
57
61
  }
58
62
 
@@ -61,7 +65,9 @@ class Node {
61
65
  }
62
66
 
63
67
  async connect() {
64
- if (this.ws) this.ws.close();
68
+ if (this.ws) {
69
+ this.ws.close();
70
+ }
65
71
  this.aqua.emit('debug', this.name, `Attempting to connect...`);
66
72
  this.ws = new WebSocket(this.wsUrl, { headers: this.constructHeaders() });
67
73
  this.setupWebSocketListeners();
@@ -93,15 +99,13 @@ class Node {
93
99
  this.aqua.emit('debug', `Failed to fetch info: ${err.message}`);
94
100
  this.info = null;
95
101
  }
96
-
97
102
  if (!this.info && !this.aqua.bypassChecks.nodeFetchInfo) {
98
103
  throw new Error(`Failed to fetch node info.`);
99
104
  }
100
-
101
105
  if (this.autoResume) {
102
106
  this.resumePlayers();
103
107
  }
104
- this.lastStats = 0;
108
+ this.lastStatsRequest = Date.now();
105
109
  }
106
110
 
107
111
  async getStats() {
@@ -109,7 +113,6 @@ class Node {
109
113
  if (now - this.lastStatsRequest < 5000) {
110
114
  return this.stats; // Return cached stats if requested too soon
111
115
  }
112
-
113
116
  try {
114
117
  const response = await this.rest.makeRequest("GET", `/v4/stats`);
115
118
  const stats = await response.json();
@@ -137,7 +140,13 @@ class Node {
137
140
  onMessage(msg) {
138
141
  if (Array.isArray(msg)) msg = Buffer.concat(msg);
139
142
  if (msg instanceof ArrayBuffer) msg = Buffer.from(msg);
140
- const payload = JSON.parse(msg.toString());
143
+ let payload;
144
+ try {
145
+ payload = JSON.parse(msg.toString());
146
+ } catch (err) {
147
+ this.aqua.emit('debug', `Failed to parse message: ${err.message}`);
148
+ return;
149
+ }
141
150
  if (!payload.op) return;
142
151
  this.aqua.emit("raw", "Node", payload);
143
152
  this.aqua.emit("debug", this.name, `Received update: ${JSON.stringify(payload)}`);
@@ -147,8 +156,7 @@ class Node {
147
156
  handlePayload(payload) {
148
157
  switch (payload.op) {
149
158
  case "stats":
150
- this.stats = { ...this.stats, ...payload };
151
- this.lastStats = Date.now();
159
+ this.updateStats(payload);
152
160
  break;
153
161
  case "ready":
154
162
  this.initializeSessionId(payload.sessionId);
@@ -161,6 +169,35 @@ class Node {
161
169
  }
162
170
  }
163
171
 
172
+ updateStats(payload) {
173
+ this.stats = {
174
+ ...this.stats,
175
+ players: payload.players || 0,
176
+ playingPlayers: payload.playingPlayers || 0,
177
+ uptime: payload.uptime || 0,
178
+ memory: {
179
+ free: payload.memory?.free || 0,
180
+ used: payload.memory?.used || 0,
181
+ allocated: payload.memory?.allocated || 0,
182
+ reservable: payload.memory?.reservable || 0,
183
+ freePercentage: payload.memory ? (payload.memory.free / payload.memory.allocated) * 100 : 0,
184
+ usedPercentage: payload.memory ? (payload.memory.used / payload.memory.allocated) * 100 : 0,
185
+ },
186
+ cpu: {
187
+ cores: payload.cpu?.cores || 0,
188
+ systemLoad: payload.cpu?.systemLoad || 0,
189
+ lavalinkLoad: payload.cpu?.lavalinkLoad || 0,
190
+ lavalinkLoadPercentage: payload.cpu ? (payload.cpu.lavalinkLoad / payload.cpu.cores) * 100 : 0,
191
+ },
192
+ frameStats: {
193
+ sent: payload.frameStats?.sent || 0,
194
+ nulled: payload.frameStats?.nulled || 0,
195
+ deficit: payload.frameStats?.deficit || 0,
196
+ },
197
+ ping: payload.ping || 0,
198
+ };
199
+ }
200
+
164
201
  initializeSessionId(sessionId) {
165
202
  if (this.sessionId !== sessionId) {
166
203
  this.rest.setSessionId(sessionId);
@@ -179,7 +216,6 @@ class Node {
179
216
  this.aqua.emit("nodeError", this, new Error(`Unable to connect after ${this.reconnectTries} attempts.`));
180
217
  return this.destroy();
181
218
  }
182
-
183
219
  setTimeout(() => {
184
220
  this.aqua.emit("nodeReconnect", this);
185
221
  this.connect();
@@ -188,8 +224,6 @@ class Node {
188
224
 
189
225
  destroy(clean = false) {
190
226
  if (clean) {
191
- this.ws?.removeAllListeners();
192
- this.ws = null;
193
227
  this.aqua.emit("nodeDestroy", this);
194
228
  this.aqua.nodes.delete(this.name);
195
229
  return;
@@ -198,9 +232,6 @@ class Node {
198
232
  this.aqua.players.forEach((player) => {
199
233
  if (player.node === this) player.destroy();
200
234
  });
201
- this.ws?.close(1000, "destroy");
202
- this.ws?.removeAllListeners();
203
- this.ws = null;
204
235
  this.aqua.emit("nodeDestroy", this);
205
236
  this.aqua.nodeMap.delete(this.name);
206
237
  this.connected = false;
@@ -208,8 +239,7 @@ class Node {
208
239
 
209
240
  disconnect() {
210
241
  if (!this.connected) return;
211
- this.aqua.players.forEach((player) => { if (player.node === this) { player.move() } });
212
- this.ws.close(1000, "disconnect");
242
+ this.aqua.players.forEach((player) => { if (player.node === this) { player.move(); } });
213
243
  this.aqua.emit("nodeDisconnect", this);
214
244
  this.connected = false;
215
245
  }
@@ -229,4 +259,4 @@ class Node {
229
259
  }
230
260
  }
231
261
 
232
- module.exports = { Node };
262
+ module.exports = { Node };