aqualink 2.6.1-fix3 → 2.6.2

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.
@@ -4,16 +4,17 @@ const Player = require("./Player");
4
4
  const Track = require("./Track");
5
5
  const { version: pkgVersion } = require("../../package.json");
6
6
  const { EventEmitter } = require('tseep');
7
+ const fs = require('fs-extra');
7
8
 
8
9
  const URL_REGEX = /^https?:\/\//;
9
10
  const DEFAULT_OPTIONS = Object.freeze({
10
- shouldDeleteMessage: false,
11
- defaultSearchPlatform: 'ytsearch',
12
- leaveOnEnd: true,
13
- restVersion: 'v4',
14
- plugins: [],
15
- autoResume: false,
16
- infiniteReconnects: false
11
+ shouldDeleteMessage: false,
12
+ defaultSearchPlatform: 'ytsearch',
13
+ leaveOnEnd: true,
14
+ restVersion: 'v4',
15
+ plugins: [],
16
+ autoResume: false,
17
+ infiniteReconnects: false
17
18
  });
18
19
  const LEAST_USED_CACHE_TTL = 50;
19
20
 
@@ -32,9 +33,9 @@ class Aqua extends EventEmitter {
32
33
  this.clientId = null;
33
34
  this.initiated = false;
34
35
  this.version = pkgVersion;
35
-
36
+
36
37
  this.options = Object.assign({}, DEFAULT_OPTIONS, options);
37
-
38
+
38
39
  const {
39
40
  shouldDeleteMessage,
40
41
  defaultSearchPlatform,
@@ -45,7 +46,7 @@ class Aqua extends EventEmitter {
45
46
  infiniteReconnects,
46
47
  send
47
48
  } = this.options;
48
-
49
+
49
50
  this.shouldDeleteMessage = shouldDeleteMessage;
50
51
  this.defaultSearchPlatform = defaultSearchPlatform;
51
52
  this.leaveOnEnd = leaveOnEnd;
@@ -53,9 +54,9 @@ class Aqua extends EventEmitter {
53
54
  this.plugins = plugins;
54
55
  this.autoResume = autoResume;
55
56
  this.infiniteReconnects = infiniteReconnects;
56
-
57
+
57
58
  this.send = send || this.defaultSendFunction.bind(this);
58
-
59
+
59
60
  this._leastUsedCache = { nodes: [], timestamp: 0 };
60
61
  }
61
62
 
@@ -69,14 +70,14 @@ class Aqua extends EventEmitter {
69
70
  if (now - this._leastUsedCache.timestamp < LEAST_USED_CACHE_TTL) {
70
71
  return this._leastUsedCache.nodes;
71
72
  }
72
-
73
+
73
74
  const connectedNodes = [];
74
75
  for (const node of this.nodeMap.values()) {
75
76
  if (node.connected) connectedNodes.push(node);
76
77
  }
77
-
78
+
78
79
  connectedNodes.sort((a, b) => a.rest.calls - b.rest.calls);
79
-
80
+
80
81
  this._leastUsedCache = { nodes: connectedNodes, timestamp: now };
81
82
  return connectedNodes;
82
83
  }
@@ -84,35 +85,35 @@ class Aqua extends EventEmitter {
84
85
  async init(clientId) {
85
86
  if (this.initiated) return this;
86
87
  this.clientId = clientId;
87
-
88
+
88
89
  try {
89
90
  const nodePromises = [];
90
91
  for (const node of this.nodes) {
91
92
  nodePromises.push(this.createNode(node));
92
93
  }
93
94
  await Promise.all(nodePromises);
94
-
95
+
95
96
  for (const plugin of this.plugins) {
96
97
  plugin.load(this);
97
98
  }
98
-
99
+
99
100
  this.initiated = true;
100
101
  } catch (error) {
101
102
  this.initiated = false;
102
103
  throw error;
103
104
  }
104
-
105
+
105
106
  return this;
106
107
  }
107
108
 
108
109
  async createNode(options) {
109
110
  const nodeId = options.name || options.host;
110
111
  this.destroyNode(nodeId);
111
-
112
+
112
113
  const node = new Node(this, options, this.options);
113
114
  this.nodeMap.set(nodeId, node);
114
115
  this._leastUsedCache.timestamp = 0;
115
-
116
+
116
117
  try {
117
118
  await node.connect();
118
119
  this.emit("nodeCreate", node);
@@ -127,7 +128,7 @@ class Aqua extends EventEmitter {
127
128
  destroyNode(identifier) {
128
129
  const node = this.nodeMap.get(identifier);
129
130
  if (!node) return;
130
-
131
+
131
132
  node.destroy();
132
133
  this.nodeMap.delete(identifier);
133
134
  this._leastUsedCache.timestamp = 0;
@@ -137,14 +138,14 @@ class Aqua extends EventEmitter {
137
138
  updateVoiceState({ d, t }) {
138
139
  const player = this.players.get(d.guild_id);
139
140
  if (!player) return;
140
-
141
+
141
142
  if (t === "VOICE_SERVER_UPDATE" || (t === "VOICE_STATE_UPDATE" && d.user_id === this.clientId)) {
142
143
  if (t === "VOICE_SERVER_UPDATE") {
143
144
  player.connection?.setServerUpdate?.(d);
144
145
  } else {
145
146
  player.connection?.setStateUpdate?.(d);
146
147
  }
147
-
148
+
148
149
  if (d.channel_id === null) {
149
150
  this.cleanupPlayer(player);
150
151
  }
@@ -153,23 +154,23 @@ class Aqua extends EventEmitter {
153
154
 
154
155
  fetchRegion(region) {
155
156
  if (!region) return this.leastUsedNodes;
156
-
157
+
157
158
  const lowerRegion = region.toLowerCase();
158
159
  const regionNodes = [];
159
-
160
+
160
161
  for (const node of this.nodeMap.values()) {
161
162
  if (node.connected && node.regions?.includes(lowerRegion)) {
162
163
  regionNodes.push(node);
163
164
  }
164
165
  }
165
-
166
+
166
167
  const loadCache = new Map();
167
168
  regionNodes.sort((a, b) => {
168
169
  if (!loadCache.has(a)) loadCache.set(a, this.calculateLoad(a));
169
170
  if (!loadCache.has(b)) loadCache.set(b, this.calculateLoad(b));
170
171
  return loadCache.get(a) - loadCache.get(b);
171
172
  });
172
-
173
+
173
174
  return regionNodes;
174
175
  }
175
176
 
@@ -182,27 +183,28 @@ class Aqua extends EventEmitter {
182
183
 
183
184
  createConnection(options) {
184
185
  if (!this.initiated) throw new Error("Aqua must be initialized before this operation");
185
-
186
+
186
187
  const existingPlayer = this.players.get(options.guildId);
187
188
  if (existingPlayer && existingPlayer.voiceChannel) return existingPlayer;
188
-
189
+
189
190
  const availableNodes = options.region ? this.fetchRegion(options.region) : this.leastUsedNodes;
190
191
  const node = availableNodes[0];
191
192
  if (!node) throw new Error("No nodes are available");
192
-
193
+
193
194
  return this.createPlayer(node, options);
194
195
  }
195
196
 
196
197
  createPlayer(node, options) {
197
198
  this.destroyPlayer(options.guildId);
198
-
199
+
199
200
  const player = new Player(this, node, options);
200
201
  this.players.set(options.guildId, player);
201
-
202
+
202
203
  player.on("destroy", () => {
203
204
  this.players.delete(options.guildId);
205
+ this.emit("playerDestroy", player);
204
206
  });
205
-
207
+
206
208
  player.connect(options);
207
209
  this.emit("playerCreate", player);
208
210
  return player;
@@ -211,7 +213,7 @@ class Aqua extends EventEmitter {
211
213
  async destroyPlayer(guildId) {
212
214
  const player = this.players.get(guildId);
213
215
  if (!player) return;
214
-
216
+
215
217
  try {
216
218
  await player.clearData();
217
219
  player.removeAllListeners();
@@ -224,18 +226,18 @@ class Aqua extends EventEmitter {
224
226
 
225
227
  async resolve({ query, source = this.defaultSearchPlatform, requester, nodes }) {
226
228
  if (!this.initiated) throw new Error("Aqua must be initialized before this operation");
227
-
229
+
228
230
  const requestNode = this.getRequestNode(nodes);
229
231
  const formattedQuery = URL_REGEX.test(query) ? query : `${source}:${query}`;
230
-
232
+
231
233
  try {
232
234
  const endpoint = `/v4/loadtracks?identifier=${encodeURIComponent(formattedQuery)}`;
233
235
  const response = await requestNode.rest.makeRequest("GET", endpoint);
234
-
236
+
235
237
  if (["empty", "NO_MATCHES"].includes(response.loadType)) {
236
238
  return await this.handleNoMatches(query);
237
239
  }
238
-
240
+
239
241
  return this.constructResponse(response, requester, requestNode);
240
242
  } catch (error) {
241
243
  if (error.name === "AbortError") {
@@ -244,19 +246,19 @@ class Aqua extends EventEmitter {
244
246
  throw new Error(`Failed to resolve track: ${error.message}`);
245
247
  }
246
248
  }
247
-
249
+
248
250
  getRequestNode(nodes) {
249
251
  if (!nodes) return this.leastUsedNodes[0];
250
-
252
+
251
253
  if (nodes instanceof Node) return nodes;
252
254
  if (typeof nodes === "string") {
253
255
  const mappedNode = this.nodeMap.get(nodes);
254
256
  return mappedNode || this.leastUsedNodes[0];
255
257
  }
256
-
258
+
257
259
  throw new TypeError(`'nodes' must be a string or Node instance, received: ${typeof nodes}`);
258
260
  }
259
-
261
+
260
262
  async handleNoMatches(query) {
261
263
  return {
262
264
  loadType: "empty",
@@ -266,8 +268,8 @@ class Aqua extends EventEmitter {
266
268
  tracks: []
267
269
  };
268
270
  }
269
-
270
- constructResponse(response, requester, requestNode) {
271
+
272
+ async constructResponse(response, requester, requestNode) {
271
273
  const baseResponse = {
272
274
  loadType: response.loadType,
273
275
  exception: null,
@@ -275,28 +277,29 @@ class Aqua extends EventEmitter {
275
277
  pluginInfo: response.pluginInfo ?? {},
276
278
  tracks: []
277
279
  };
278
-
280
+
279
281
  if (response.loadType === "error" || response.loadType === "LOAD_FAILED") {
280
282
  baseResponse.exception = response.data ?? response.exception;
281
283
  return baseResponse;
282
284
  }
283
-
285
+
284
286
  const trackFactory = (trackData) => new Track(trackData, requester, requestNode);
285
-
287
+
286
288
  switch (response.loadType) {
287
289
  case "track":
288
290
  if (response.data) {
289
291
  baseResponse.tracks.push(trackFactory(response.data));
290
292
  }
291
293
  break;
292
-
293
294
  case "playlist": {
294
295
  const info = response.data?.info;
295
296
  if (info) {
296
- baseResponse.playlistInfo = {
297
+ const playlistInfo = {
297
298
  name: info.name ?? info.title,
299
+ thumbnail: response.data.pluginInfo?.artworkUrl ?? (response.data.tracks?.[0]?.info?.artworkUrl || null),
298
300
  ...info
299
301
  };
302
+ baseResponse.playlistInfo = playlistInfo;
300
303
  }
301
304
 
302
305
  const tracks = response.data?.tracks;
@@ -322,7 +325,7 @@ class Aqua extends EventEmitter {
322
325
  break;
323
326
  }
324
327
  }
325
-
328
+
326
329
  return baseResponse;
327
330
  }
328
331
 
@@ -334,7 +337,7 @@ class Aqua extends EventEmitter {
334
337
 
335
338
  async search(query, requester, source = this.defaultSearchPlatform) {
336
339
  if (!query || !requester) return null;
337
-
340
+
338
341
  try {
339
342
  const { tracks } = await this.resolve({ query, source, requester });
340
343
  return tracks || null;
@@ -344,21 +347,94 @@ class Aqua extends EventEmitter {
344
347
  }
345
348
  }
346
349
 
347
- async cleanupPlayer(player) {
348
- if (!player) return;
349
-
350
+ async savePlayer(filePath = "./AquaPlayers.json") {
351
+ const data = Array.from(this.players.values()).map(player => ({
352
+ guildId: player.guildId,
353
+ textChannel: player.textChannel,
354
+ voiceChannel: player.voiceChannel,
355
+ track: player.current ? {
356
+ identifier: player.current.identifier,
357
+ author: player.current.author,
358
+ title: player.current.title,
359
+ uri: player.current.uri,
360
+ sourceName: player.current.sourceName,
361
+ artworkUrl: player.current.artworkUrl,
362
+ duration: player.current.duration,
363
+ position: player.position,
364
+ } : null,
365
+ requester: player.requester || player.current?.requester,
366
+ volume: player.volume,
367
+ paused: player.paused
368
+ }));
369
+ console.log(`Saving ${data.length} players to ${filePath}`);
370
+ await fs.writeJSON(filePath, data, { spaces: 2 });
371
+ this.emit("debug", "Aqua", `Saved players to ${filePath}`);
372
+ }
373
+
374
+ async waitForFirstNode() {
375
+ if (this.leastUsedNodes.length > 0) return;
376
+ return new Promise(resolve => {
377
+ const check = () => {
378
+ if (this.leastUsedNodes.length > 0) {
379
+ resolve();
380
+ } else {
381
+ setTimeout(check, 100);
382
+ }
383
+ };
384
+ check();
385
+ });
386
+ }
387
+
388
+ async loadPlayers(filePath = "./AquaPlayers.json") {
389
+ if (!fs.existsSync(filePath)) {
390
+ this.emit("debug", "Aqua", `No player data found at ${filePath}`);
391
+ return;
392
+ }
350
393
  try {
351
- if (player.connection) {
352
- try {
353
- await player.destroy()
354
- player.connection = null;
355
- } catch (error) {
356
- console.error(`Error disconnecting player connection: ${error.message}`);
394
+ await this.waitForFirstNode();
395
+ const data = await fs.readJSON(filePath);
396
+ for (const playerData of data) {
397
+ const { guildId, textChannel, voiceChannel, track, volume, paused, requester } = playerData;
398
+ let player = this.players.get(guildId);
399
+
400
+ if (!player) {
401
+ player = await this.createConnection({
402
+ guildId: guildId,
403
+ textChannel: textChannel,
404
+ voiceChannel: voiceChannel,
405
+ defaultVolume: volume || 65,
406
+ deaf: true
407
+ });
408
+ }
409
+
410
+ if (track && player) {
411
+ const resolved = await this.resolve({ query: track.uri, requester });
412
+ if (resolved.tracks && resolved.tracks.length > 0) {
413
+ player.queue.add(resolved.tracks[0]);
414
+ player.position = track.position || 0;
415
+ } else {
416
+ this.emit("debug", "Aqua", `Could not resolve track for guild ${guildId}: ${track.uri}`);
417
+ }
418
+ }
419
+
420
+ if (player) {
421
+ player.paused = paused || false;
422
+ if (!player.playing && !player.paused && player.queue.size > 0) {
423
+ player.play();
424
+ }
357
425
  }
358
426
  }
359
-
360
- this.players.delete(player.guildId);
361
- this.emit("playerCleanup", player.guildId);
427
+ this.emit("debug", "Aqua", `Loaded players from ${filePath}`);
428
+ } catch (error) {
429
+ console.error(`Failed to load players from ${filePath}:`, error);
430
+ }
431
+ }
432
+
433
+ async cleanupPlayer(player) {
434
+ if (!player) return;
435
+
436
+ try {
437
+ await player.destroy();
362
438
  } catch (error) {
363
439
  console.error(`Error during player cleanup: ${error.message}`);
364
440
  }
@@ -96,7 +96,7 @@ class Node {
96
96
  this.aqua.emit("nodeConnected", this);
97
97
 
98
98
  if (this.autoResume && this.sessionId) {
99
- await this.resumePlayers();
99
+ await this.aqua.loadPlayers();
100
100
  }
101
101
  } catch (err) {
102
102
  this.info = null;
@@ -245,13 +245,33 @@ class Node {
245
245
  }
246
246
 
247
247
  async getStats() {
248
- if (this.connected) {
248
+
249
+ if (this.connected && this.stats) {
249
250
  return this.stats;
250
251
  }
251
252
 
252
253
  try {
253
254
  const newStats = await this.rest.getStats();
254
- Object.assign(this.stats, newStats);
255
+ if (newStats && this.stats) {
256
+ this.stats.players = newStats.players ?? this.stats.players;
257
+ this.stats.playingPlayers = newStats.playingPlayers ?? this.stats.playingPlayers;
258
+ this.stats.uptime = newStats.uptime ?? this.stats.uptime;
259
+ this.stats.ping = newStats.ping ?? this.stats.ping;
260
+
261
+ if (newStats.memory) {
262
+ Object.assign(this.stats.memory, newStats.memory);
263
+ this._calculateMemoryPercentages();
264
+ }
265
+
266
+ if (newStats.cpu) {
267
+ Object.assign(this.stats.cpu, newStats.cpu);
268
+ this._calculateCpuPercentages();
269
+ }
270
+
271
+ if (newStats.frameStats) {
272
+ Object.assign(this.stats.frameStats, newStats.frameStats);
273
+ }
274
+ }
255
275
  return this.stats;
256
276
  } catch (err) {
257
277
  this.emitError(`Failed to fetch node stats: ${err.message}`);
@@ -269,13 +289,12 @@ class Node {
269
289
 
270
290
  if (payload.memory) {
271
291
  Object.assign(this.stats.memory, payload.memory);
272
- this.stats.memory.freePercentage = (this.stats.memory.free / this.stats.memory.allocated) * 100;
273
- this.stats.memory.usedPercentage = (this.stats.memory.used / this.stats.memory.allocated) * 100;
292
+ this._calculateMemoryPercentages();
274
293
  }
275
294
 
276
295
  if (payload.cpu) {
277
296
  Object.assign(this.stats.cpu, payload.cpu);
278
- this.stats.cpu.lavalinkLoadPercentage = (this.stats.cpu.lavalinkLoad / this.stats.cpu.cores) * 100;
297
+ this._calculateCpuPercentages();
279
298
  }
280
299
 
281
300
  if (payload.frameStats) {
@@ -283,6 +302,21 @@ class Node {
283
302
  }
284
303
  }
285
304
 
305
+ _calculateMemoryPercentages() {
306
+ const { memory } = this.stats;
307
+ if (memory.allocated > 0) {
308
+ memory.freePercentage = (memory.free / memory.allocated) * 100;
309
+ memory.usedPercentage = (memory.used / memory.allocated) * 100;
310
+ }
311
+ }
312
+
313
+ _calculateCpuPercentages() {
314
+ const { cpu } = this.stats;
315
+ if (cpu.cores > 0) {
316
+ cpu.lavalinkLoadPercentage = (cpu.lavalinkLoad / cpu.cores) * 100;
317
+ }
318
+ }
319
+
286
320
  _handleReadyOp(payload) {
287
321
  if (!payload.sessionId) {
288
322
  this.emitError("Ready payload missing sessionId");
@@ -297,10 +331,7 @@ class Node {
297
331
 
298
332
  async resumePlayers() {
299
333
  try {
300
- await this.rest.makeRequest("PATCH", `/v4/sessions/${this.sessionId}`, {
301
- resuming: true,
302
- timeout: this.resumeTimeout
303
- });
334
+ await this.aqua.loadPlayers();
304
335
 
305
336
  this.aqua.emit("debug", this.name, "Session resumed successfully");
306
337
  } catch (err) {
@@ -62,6 +62,7 @@ class Player extends EventEmitter {
62
62
  this.position = packet.state.position;
63
63
  this.connected = packet.state.connected;
64
64
  this.ping = packet.state.ping;
65
+ this.timestamp = packet.state.timestamp;
65
66
 
66
67
  this.aqua.emit("playerUpdate", this, packet);
67
68
  });
@@ -184,18 +185,18 @@ class Player extends EventEmitter {
184
185
  return this.batchUpdatePlayer({ track: { encoded: this.current.track } }, true);
185
186
  }
186
187
 
187
- connect({ deaf = true, mute = false } = {}) {
188
+ connect(options = this) {
189
+ const { guildId, voiceChannel, deaf = true, mute = false } = options;
188
190
  this.deaf = deaf;
189
191
  this.mute = mute;
190
- const payload = {
191
- guild_id: this.guildId,
192
- channel_id: this.voiceChannel,
192
+ this.send({
193
+ guild_id: guildId,
194
+ channel_id: voiceChannel,
193
195
  self_deaf: deaf,
194
- self_mute: mute
195
- };
196
- this.send(payload);
196
+ self_mute: mute,
197
+ });
197
198
  this.connected = true;
198
- this.aqua.emit("debug", this.guildId, `Player connected to voice channel: ${this.voiceChannel}.`);
199
+ this.aqua.emit("debug", guildId, `Player connected to voice channel: ${voiceChannel}.`);
199
200
  return this;
200
201
  }
201
202
 
@@ -288,7 +289,13 @@ class Player extends EventEmitter {
288
289
  if (!channel?.length) throw new TypeError("Channel must be a non-empty string.");
289
290
  if (this.connected && channel === this.voiceChannel) throw new ReferenceError(`Player already connected to ${channel}.`);
290
291
  this.voiceChannel = channel;
291
- this.connect({ deaf: this.deaf, mute: this.mute });
292
+ this.connect({
293
+ deaf: this.deaf,
294
+ guildId: this.guildId,
295
+ voiceChannel: this.voiceChannel,
296
+ textChannel: this.textChannel,
297
+ mute: this.mute,
298
+ });
292
299
  return this;
293
300
  }
294
301
 
@@ -10,6 +10,7 @@ class Track {
10
10
  this.identifier = info.identifier || '';
11
11
  this.isSeekable = Boolean(info.isSeekable);
12
12
  this.author = info.author || '';
13
+ this.position = info.position || 0;
13
14
  this.length = info.length || 0;
14
15
  this.duration = info.length || 0;
15
16
  this.isStream = Boolean(info.isStream);
@@ -29,6 +30,7 @@ class Track {
29
30
  return {
30
31
  identifier: this.identifier,
31
32
  isSeekable: this.isSeekable,
33
+ position: this.position,
32
34
  author: this.author,
33
35
  length: this.length,
34
36
  isStream: this.isStream,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aqualink",
3
- "version": "2.6.1-fix3",
3
+ "version": "2.6.2",
4
4
  "description": "An Lavalink client, focused in pure performance and features",
5
5
  "main": "build/index.js",
6
6
  "types": "index.d.ts",
@@ -42,6 +42,7 @@
42
42
  },
43
43
  "dependencies": {
44
44
  "ws": "^8.18.2",
45
+ "fs-extra": "^11.3.0",
45
46
  "tseep": "^1.3.1"
46
47
  },
47
48
  "maintainers": [