aqualink 2.7.0 → 2.7.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.
- package/build/structures/Aqua.js +238 -93
- package/build/structures/Node.js +31 -31
- package/build/structures/Player.js +1 -1
- package/package.json +1 -2
package/build/structures/Aqua.js
CHANGED
|
@@ -4,7 +4,7 @@ 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
|
|
7
|
+
const fs = require('fs/promises');
|
|
8
8
|
|
|
9
9
|
const URL_REGEX = /^https?:\/\//;
|
|
10
10
|
const DEFAULT_OPTIONS = Object.freeze({
|
|
@@ -57,13 +57,21 @@ class Aqua extends EventEmitter {
|
|
|
57
57
|
this.send = this.options.send || this.defaultSendFunction.bind(this);
|
|
58
58
|
|
|
59
59
|
this._leastUsedCache = { nodes: [], timestamp: 0 };
|
|
60
|
-
|
|
60
|
+
|
|
61
61
|
this._nodeStates = new Map();
|
|
62
62
|
this._failoverQueue = new Map();
|
|
63
63
|
this._lastFailoverAttempt = new Map();
|
|
64
|
-
|
|
64
|
+
|
|
65
|
+
this._brokenPlayers = new Map();
|
|
66
|
+
this._nodeReconnectHandlers = new Map();
|
|
67
|
+
|
|
65
68
|
this._boundCleanupPlayer = this.cleanupPlayer.bind(this);
|
|
66
69
|
this._boundHandlePlayerDestroy = this._handlePlayerDestroy.bind(this);
|
|
70
|
+
this._boundHandleNodeReconnect = this._handleNodeReconnect.bind(this);
|
|
71
|
+
this._boundHandleSocketClosed = this._handleSocketClosed.bind(this);
|
|
72
|
+
|
|
73
|
+
this.on('nodeConnect', (node) => this._handleNodeReconnect(node));
|
|
74
|
+
this.on('nodeDisconnect', (node) => this._handleSocketClosed(node));
|
|
67
75
|
}
|
|
68
76
|
|
|
69
77
|
defaultSendFunction(packet) {
|
|
@@ -100,16 +108,16 @@ class Aqua extends EventEmitter {
|
|
|
100
108
|
console.error(`Failed to create node ${node.name || node.host}:`, err);
|
|
101
109
|
return null;
|
|
102
110
|
}));
|
|
103
|
-
|
|
111
|
+
|
|
104
112
|
const results = await Promise.allSettled(nodePromises);
|
|
105
113
|
const successfulNodes = results.filter(r => r.status === 'fulfilled' && r.value).length;
|
|
106
|
-
|
|
114
|
+
|
|
107
115
|
if (successfulNodes === 0) {
|
|
108
116
|
throw new Error("No nodes could be connected");
|
|
109
117
|
}
|
|
110
118
|
|
|
111
|
-
await Promise.all(this.plugins.map(plugin =>
|
|
112
|
-
Promise.resolve(plugin.load(this)).catch(err =>
|
|
119
|
+
await Promise.all(this.plugins.map(plugin =>
|
|
120
|
+
Promise.resolve(plugin.load(this)).catch(err =>
|
|
113
121
|
console.error("Plugin load error:", err)
|
|
114
122
|
)
|
|
115
123
|
));
|
|
@@ -128,6 +136,7 @@ class Aqua extends EventEmitter {
|
|
|
128
136
|
this.destroyNode(nodeId);
|
|
129
137
|
|
|
130
138
|
const node = new Node(this, options, this.options);
|
|
139
|
+
node.players = new Set();
|
|
131
140
|
this.nodeMap.set(nodeId, node);
|
|
132
141
|
this._invalidateCache();
|
|
133
142
|
|
|
@@ -144,6 +153,116 @@ class Aqua extends EventEmitter {
|
|
|
144
153
|
}
|
|
145
154
|
}
|
|
146
155
|
|
|
156
|
+
async _handleNodeReconnect(node) {
|
|
157
|
+
if (!this.autoResume) return;
|
|
158
|
+
|
|
159
|
+
const nodeId = node.name || node.host;
|
|
160
|
+
this.emit("debug", "Aqua", `Node ${nodeId} reconnected, attempting to rebuild broken players`);
|
|
161
|
+
|
|
162
|
+
this._nodeStates.set(nodeId, { connected: true, failoverInProgress: false });
|
|
163
|
+
|
|
164
|
+
await this._rebuildBrokenPlayers(node);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async _handleSocketClosed(node) {
|
|
168
|
+
if (!this.autoResume) return;
|
|
169
|
+
|
|
170
|
+
const nodeId = node.name || node.host;
|
|
171
|
+
this.emit("debug", "Aqua", `Socket closed for node ${nodeId}, storing broken players`);
|
|
172
|
+
|
|
173
|
+
this._nodeStates.set(nodeId, { connected: false, failoverInProgress: false });
|
|
174
|
+
|
|
175
|
+
await this._storeBrokenPlayersForNode(node);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async _storeBrokenPlayersForNode(node) {
|
|
179
|
+
const nodeId = node.name || node.host;
|
|
180
|
+
const affectedPlayers = await this._getPlayersForNode(node);
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
for (const player of affectedPlayers) {
|
|
184
|
+
try {
|
|
185
|
+
const playerState = this._capturePlayerState(player);
|
|
186
|
+
if (playerState) {
|
|
187
|
+
playerState.originalNodeId = nodeId;
|
|
188
|
+
playerState.brokenAt = Date.now();
|
|
189
|
+
this._brokenPlayers.set(player.guildId, playerState);
|
|
190
|
+
|
|
191
|
+
this.emit("debug", "Aqua", `Stored broken player state for guild ${player.guildId}`);
|
|
192
|
+
}
|
|
193
|
+
} catch (error) {
|
|
194
|
+
this.emit("error", null, new Error(`Failed to store broken player state: ${error.message}`));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async _getPlayersForNode(node) {
|
|
200
|
+
const affectedPlayers = [];
|
|
201
|
+
for (const player of this.players.values()) {
|
|
202
|
+
if (player.nodes === node || player.nodes?.name === node.name) {
|
|
203
|
+
affectedPlayers.push(player);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return affectedPlayers;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async _rebuildBrokenPlayers(node) {
|
|
210
|
+
const nodeId = node.name || node.host;
|
|
211
|
+
const rebuiltCount = 0;
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
for (const [guildId, brokenState] of this._brokenPlayers.entries()) {
|
|
216
|
+
if (brokenState.originalNodeId !== nodeId) continue;
|
|
217
|
+
try {
|
|
218
|
+
await this._rebuildPlayer(brokenState, node);
|
|
219
|
+
this._brokenPlayers.delete(guildId);
|
|
220
|
+
rebuiltCount++;
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
this.emit("debug", "Aqua", `Successfully rebuilt player for guild ${guildId}`);
|
|
224
|
+
} catch (error) {
|
|
225
|
+
this.emit("error", null, new Error(`Failed to rebuild player for guild ${guildId}: ${error.message}`));
|
|
226
|
+
|
|
227
|
+
if (Date.now() - brokenState.brokenAt > 300000) {
|
|
228
|
+
this._brokenPlayers.delete(guildId);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (rebuiltCount > 0) {
|
|
234
|
+
this.emit("playersRebuilt", node, rebuiltCount);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async _rebuildPlayer(brokenState, targetNode) {
|
|
239
|
+
const { guildId, textChannel, voiceChannel, current, volume = 65, deaf = true } = brokenState;
|
|
240
|
+
|
|
241
|
+
const connectionOptions = { guildId, textChannel, voiceChannel, defaultVolume: volume, deaf };
|
|
242
|
+
const existingPlayer = this.players.get(guildId);
|
|
243
|
+
|
|
244
|
+
if (existingPlayer) {
|
|
245
|
+
await existingPlayer.destroy();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
setTimeout(async () => {
|
|
249
|
+
const newestPlayer = await this.createConnection(connectionOptions);
|
|
250
|
+
this.players.set(guildId, newestPlayer);
|
|
251
|
+
|
|
252
|
+
if (current) {
|
|
253
|
+
try {
|
|
254
|
+
await newestPlayer.queue.add(current);
|
|
255
|
+
await newestPlayer.play();
|
|
256
|
+
} catch (error) {
|
|
257
|
+
console.error("Error while playing track:", error);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
this.emit("trackStart", newestPlayer, brokenState.current);
|
|
262
|
+
return newestPlayer;
|
|
263
|
+
}, 2000);
|
|
264
|
+
}
|
|
265
|
+
|
|
147
266
|
destroyNode(identifier) {
|
|
148
267
|
const node = this.nodeMap.get(identifier);
|
|
149
268
|
if (!node) return;
|
|
@@ -153,6 +272,11 @@ class Aqua extends EventEmitter {
|
|
|
153
272
|
}
|
|
154
273
|
|
|
155
274
|
_cleanupNode(nodeId) {
|
|
275
|
+
const node = this.nodeMap.get(nodeId);
|
|
276
|
+
if (node) {
|
|
277
|
+
node.removeAllListeners();
|
|
278
|
+
}
|
|
279
|
+
|
|
156
280
|
this.nodeMap.delete(nodeId);
|
|
157
281
|
this._nodeStates.delete(nodeId);
|
|
158
282
|
this._failoverQueue.delete(nodeId);
|
|
@@ -166,10 +290,10 @@ class Aqua extends EventEmitter {
|
|
|
166
290
|
|
|
167
291
|
async handleNodeFailover(failedNode) {
|
|
168
292
|
if (!this.failoverOptions.enabled) return;
|
|
169
|
-
|
|
293
|
+
|
|
170
294
|
const nodeId = failedNode.name || failedNode.host;
|
|
171
295
|
const now = Date.now();
|
|
172
|
-
|
|
296
|
+
|
|
173
297
|
const nodeState = this._nodeStates.get(nodeId);
|
|
174
298
|
if (nodeState?.failoverInProgress) return;
|
|
175
299
|
|
|
@@ -182,11 +306,11 @@ class Aqua extends EventEmitter {
|
|
|
182
306
|
this._nodeStates.set(nodeId, { connected: false, failoverInProgress: true });
|
|
183
307
|
this._lastFailoverAttempt.set(nodeId, now);
|
|
184
308
|
this._failoverQueue.set(nodeId, currentAttempts + 1);
|
|
185
|
-
|
|
309
|
+
|
|
186
310
|
try {
|
|
187
311
|
this.emit("nodeFailover", failedNode);
|
|
188
|
-
|
|
189
|
-
const affectedPlayers =
|
|
312
|
+
|
|
313
|
+
const affectedPlayers = Array.from(failedNode.players);
|
|
190
314
|
if (affectedPlayers.length === 0) {
|
|
191
315
|
this._nodeStates.set(nodeId, { connected: false, failoverInProgress: false });
|
|
192
316
|
return;
|
|
@@ -200,14 +324,14 @@ class Aqua extends EventEmitter {
|
|
|
200
324
|
}
|
|
201
325
|
|
|
202
326
|
const failoverResults = await this._migratePlayersWithRetry(affectedPlayers, availableNodes);
|
|
203
|
-
|
|
327
|
+
|
|
204
328
|
const successful = failoverResults.filter(r => r.success).length;
|
|
205
329
|
const failed = failoverResults.length - successful;
|
|
206
|
-
|
|
330
|
+
|
|
207
331
|
if (successful > 0) {
|
|
208
332
|
this.emit("nodeFailoverComplete", failedNode, successful, failed);
|
|
209
333
|
}
|
|
210
|
-
|
|
334
|
+
|
|
211
335
|
} catch (error) {
|
|
212
336
|
this.emit("error", null, new Error(`Failover failed for node ${nodeId}: ${error.message}`));
|
|
213
337
|
} finally {
|
|
@@ -215,25 +339,9 @@ class Aqua extends EventEmitter {
|
|
|
215
339
|
}
|
|
216
340
|
}
|
|
217
341
|
|
|
218
|
-
_getPlayersForNode(node) {
|
|
219
|
-
const affectedPlayers = [];
|
|
220
|
-
for (const player of this.players.values()) {
|
|
221
|
-
if (player.nodes === node || player.nodes?.name === node.name) {
|
|
222
|
-
affectedPlayers.push(player);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
return affectedPlayers;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
_getAvailableNodesForFailover(failedNode) {
|
|
229
|
-
return this.leastUsedNodes.filter(node =>
|
|
230
|
-
node !== failedNode && node.name !== failedNode.name
|
|
231
|
-
);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
342
|
async _migratePlayersWithRetry(players, availableNodes) {
|
|
235
343
|
const results = [];
|
|
236
|
-
|
|
344
|
+
|
|
237
345
|
const concurrency = 3;
|
|
238
346
|
for (let i = 0; i < players.length; i += concurrency) {
|
|
239
347
|
const batch = players.slice(i, i + concurrency);
|
|
@@ -246,11 +354,11 @@ class Aqua extends EventEmitter {
|
|
|
246
354
|
return { player, success: false, error };
|
|
247
355
|
}
|
|
248
356
|
});
|
|
249
|
-
|
|
357
|
+
|
|
250
358
|
const batchResults = await Promise.allSettled(batchPromises);
|
|
251
359
|
results.push(...batchResults.map(r => r.value || r.reason));
|
|
252
360
|
}
|
|
253
|
-
|
|
361
|
+
|
|
254
362
|
return results;
|
|
255
363
|
}
|
|
256
364
|
|
|
@@ -261,7 +369,7 @@ class Aqua extends EventEmitter {
|
|
|
261
369
|
|
|
262
370
|
const guildId = player.guildId;
|
|
263
371
|
let retryCount = 0;
|
|
264
|
-
|
|
372
|
+
|
|
265
373
|
while (retryCount < this.failoverOptions.maxRetries) {
|
|
266
374
|
try {
|
|
267
375
|
const targetNode = this._selectBestNode(availableNodes, player);
|
|
@@ -274,15 +382,14 @@ class Aqua extends EventEmitter {
|
|
|
274
382
|
if (!newPlayer) throw new Error("Failed to create player on target node");
|
|
275
383
|
|
|
276
384
|
await this._restorePlayerState(newPlayer, playerState);
|
|
277
|
-
|
|
278
|
-
newPlayer.destroy();
|
|
385
|
+
|
|
279
386
|
if (playerState.current) {
|
|
280
387
|
newPlayer.queue.add(playerState.current);
|
|
281
388
|
}
|
|
282
|
-
|
|
389
|
+
|
|
283
390
|
this.emit("playerMigrated", player, newPlayer, targetNode);
|
|
284
391
|
return newPlayer;
|
|
285
|
-
|
|
392
|
+
|
|
286
393
|
} catch (error) {
|
|
287
394
|
retryCount++;
|
|
288
395
|
if (retryCount < this.failoverOptions.maxRetries) {
|
|
@@ -296,12 +403,12 @@ class Aqua extends EventEmitter {
|
|
|
296
403
|
|
|
297
404
|
_selectBestNode(availableNodes, player) {
|
|
298
405
|
if (player.region) {
|
|
299
|
-
const regionNode = availableNodes.find(node =>
|
|
406
|
+
const regionNode = availableNodes.find(node =>
|
|
300
407
|
node.regions?.includes(player.region.toLowerCase())
|
|
301
408
|
);
|
|
302
409
|
if (regionNode) return regionNode;
|
|
303
410
|
}
|
|
304
|
-
|
|
411
|
+
|
|
305
412
|
return availableNodes[0];
|
|
306
413
|
}
|
|
307
414
|
|
|
@@ -316,13 +423,12 @@ class Aqua extends EventEmitter {
|
|
|
316
423
|
position: player.position || 0,
|
|
317
424
|
current: player.current ? { ...player.current } : null,
|
|
318
425
|
queue: player.queue?.tracks ? [...player.queue.tracks] : [],
|
|
319
|
-
repeat: player.
|
|
426
|
+
repeat: player.loop,
|
|
320
427
|
shuffle: player.shuffle,
|
|
321
428
|
deaf: player.deaf || false,
|
|
322
429
|
mute: player.mute || false,
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
timestamp: Date.now()
|
|
430
|
+
timestamp: Date.now(),
|
|
431
|
+
connected: player.connected || false,
|
|
326
432
|
};
|
|
327
433
|
} catch (error) {
|
|
328
434
|
return null;
|
|
@@ -347,40 +453,35 @@ class Aqua extends EventEmitter {
|
|
|
347
453
|
if (!newPlayer || !playerState) return;
|
|
348
454
|
|
|
349
455
|
try {
|
|
350
|
-
// Batch operations where possible
|
|
351
456
|
const operations = [];
|
|
352
|
-
|
|
457
|
+
|
|
353
458
|
if (playerState.volume !== undefined) {
|
|
354
459
|
operations.push(newPlayer.setVolume(playerState.volume));
|
|
355
460
|
}
|
|
356
461
|
|
|
357
|
-
// Restore queue efficiently
|
|
358
462
|
if (playerState.queue?.length > 0) {
|
|
359
463
|
newPlayer.queue.add(...playerState.queue);
|
|
360
464
|
}
|
|
361
465
|
|
|
362
|
-
// Wait for all operations
|
|
363
466
|
await Promise.all(operations);
|
|
364
467
|
|
|
365
|
-
// Handle current track restoration
|
|
366
468
|
if (playerState.current && this.failoverOptions.preservePosition) {
|
|
367
469
|
newPlayer.queue.unshift(playerState.current);
|
|
368
|
-
|
|
470
|
+
|
|
369
471
|
if (this.failoverOptions.resumePlayback) {
|
|
370
472
|
await newPlayer.play();
|
|
371
|
-
|
|
473
|
+
|
|
372
474
|
if (playerState.position > 0) {
|
|
373
|
-
await this._delay(300);
|
|
475
|
+
await this._delay(300);
|
|
374
476
|
await newPlayer.seek(playerState.position);
|
|
375
477
|
}
|
|
376
|
-
|
|
478
|
+
|
|
377
479
|
if (playerState.paused) {
|
|
378
480
|
await newPlayer.pause();
|
|
379
481
|
}
|
|
380
482
|
}
|
|
381
483
|
}
|
|
382
484
|
|
|
383
|
-
// Restore other properties
|
|
384
485
|
Object.assign(newPlayer, {
|
|
385
486
|
repeat: playerState.repeat,
|
|
386
487
|
shuffle: playerState.shuffle
|
|
@@ -395,7 +496,6 @@ class Aqua extends EventEmitter {
|
|
|
395
496
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
396
497
|
}
|
|
397
498
|
|
|
398
|
-
// Optimized cleanup
|
|
399
499
|
async cleanupPlayer(player) {
|
|
400
500
|
if (!player) return;
|
|
401
501
|
try {
|
|
@@ -434,7 +534,6 @@ class Aqua extends EventEmitter {
|
|
|
434
534
|
}
|
|
435
535
|
}
|
|
436
536
|
|
|
437
|
-
// Optimized sorting with caching
|
|
438
537
|
const loadCache = new Map();
|
|
439
538
|
regionNodes.sort((a, b) => {
|
|
440
539
|
const loadA = loadCache.get(a) ?? (loadCache.set(a, this._calculateLoad(a)), loadCache.get(a));
|
|
@@ -469,7 +568,6 @@ class Aqua extends EventEmitter {
|
|
|
469
568
|
const player = new Player(this, node, options);
|
|
470
569
|
this.players.set(options.guildId, player);
|
|
471
570
|
|
|
472
|
-
// Use pre-bound method for better performance
|
|
473
571
|
player.once("destroy", this._boundHandlePlayerDestroy);
|
|
474
572
|
player.connect(options);
|
|
475
573
|
this.emit("playerCreate", player);
|
|
@@ -477,6 +575,10 @@ class Aqua extends EventEmitter {
|
|
|
477
575
|
}
|
|
478
576
|
|
|
479
577
|
_handlePlayerDestroy(player) {
|
|
578
|
+
const node = player.nodes;
|
|
579
|
+
if (node && node.players) {
|
|
580
|
+
node.players.delete(player);
|
|
581
|
+
}
|
|
480
582
|
this.players.delete(player.guildId);
|
|
481
583
|
this.emit("playerDestroy", player);
|
|
482
584
|
}
|
|
@@ -557,7 +659,7 @@ class Aqua extends EventEmitter {
|
|
|
557
659
|
baseResponse.tracks.push(new Track(response.data, requester, requestNode));
|
|
558
660
|
}
|
|
559
661
|
break;
|
|
560
|
-
|
|
662
|
+
|
|
561
663
|
case "playlist": {
|
|
562
664
|
const info = response.data?.info;
|
|
563
665
|
if (info) {
|
|
@@ -604,42 +706,50 @@ class Aqua extends EventEmitter {
|
|
|
604
706
|
}
|
|
605
707
|
}
|
|
606
708
|
|
|
607
|
-
// Optimized save/load methods
|
|
608
709
|
async savePlayer(filePath = "./AquaPlayers.json") {
|
|
609
|
-
const data = Array.from(this.players.values()
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
710
|
+
const data = Array.from(this.players.values()).map(player => {
|
|
711
|
+
const requester = player.requester || player.current?.requester;
|
|
712
|
+
|
|
713
|
+
return {
|
|
714
|
+
g: player.guildId,
|
|
715
|
+
t: player.textChannel,
|
|
716
|
+
v: player.voiceChannel,
|
|
717
|
+
u: player.current?.uri || null,
|
|
718
|
+
p: player.position || 0,
|
|
719
|
+
ts: player.timestamp || 0,
|
|
720
|
+
q: player.queue?.tracks?.map(tr => tr.uri).slice(0, 5) || [],
|
|
721
|
+
r: requester ? {
|
|
722
|
+
id: requester.id,
|
|
723
|
+
username: requester.username,
|
|
724
|
+
globalName: requester.globalName,
|
|
725
|
+
discriminator: requester.discriminator,
|
|
726
|
+
avatar: requester.avatar
|
|
727
|
+
} : null,
|
|
728
|
+
vol: player.volume,
|
|
729
|
+
pa: player.paused,
|
|
730
|
+
isPlaying: !!player.current && !player.paused
|
|
731
|
+
};
|
|
732
|
+
});
|
|
733
|
+
|
|
623
734
|
await fs.writeFile(filePath, JSON.stringify(data), "utf8");
|
|
735
|
+
this.emit("debug", "Aqua", `Saved ${data.length} players to ${filePath}`);
|
|
624
736
|
}
|
|
625
737
|
|
|
626
738
|
async loadPlayers(filePath = "./AquaPlayers.json") {
|
|
627
739
|
try {
|
|
628
740
|
await fs.access(filePath);
|
|
629
741
|
await this._waitForFirstNode();
|
|
630
|
-
|
|
742
|
+
|
|
631
743
|
const data = JSON.parse(await fs.readFile(filePath, "utf8"));
|
|
632
|
-
|
|
633
|
-
// Process in batches to avoid overwhelming
|
|
744
|
+
|
|
634
745
|
const batchSize = 5;
|
|
635
746
|
for (let i = 0; i < data.length; i += batchSize) {
|
|
636
747
|
const batch = data.slice(i, i + batchSize);
|
|
637
748
|
await Promise.all(batch.map(p => this._restorePlayer(p)));
|
|
638
749
|
}
|
|
639
|
-
|
|
750
|
+
|
|
640
751
|
await fs.writeFile(filePath, "[]", "utf8");
|
|
641
752
|
} catch (error) {
|
|
642
|
-
// Silent fail if file doesn't exist
|
|
643
753
|
}
|
|
644
754
|
}
|
|
645
755
|
|
|
@@ -647,11 +757,11 @@ class Aqua extends EventEmitter {
|
|
|
647
757
|
try {
|
|
648
758
|
let player = this.players.get(p.g);
|
|
649
759
|
if (!player) {
|
|
650
|
-
const targetNode = (p.n && this.nodeMap.get(p.n)?.connected) ?
|
|
760
|
+
const targetNode = (p.n && this.nodeMap.get(p.n)?.connected) ?
|
|
651
761
|
this.nodeMap.get(p.n) : this.leastUsedNodes[0];
|
|
652
|
-
|
|
762
|
+
|
|
653
763
|
if (!targetNode) return;
|
|
654
|
-
|
|
764
|
+
|
|
655
765
|
player = await this.createConnection({
|
|
656
766
|
guildId: p.g,
|
|
657
767
|
textChannel: p.t,
|
|
@@ -661,7 +771,6 @@ class Aqua extends EventEmitter {
|
|
|
661
771
|
});
|
|
662
772
|
}
|
|
663
773
|
|
|
664
|
-
// Restore current track
|
|
665
774
|
if (p.u && player) {
|
|
666
775
|
const resolved = await this.resolve({ query: p.u, requester: p.r });
|
|
667
776
|
if (resolved.tracks?.[0]) {
|
|
@@ -670,13 +779,12 @@ class Aqua extends EventEmitter {
|
|
|
670
779
|
if (typeof p.ts === "number") player.timestamp = p.ts;
|
|
671
780
|
}
|
|
672
781
|
}
|
|
673
|
-
|
|
674
|
-
// Restore queue
|
|
782
|
+
|
|
675
783
|
if (p.q?.length && player) {
|
|
676
784
|
const queuePromises = p.q
|
|
677
785
|
.filter(uri => uri !== p.u)
|
|
678
786
|
.map(uri => this.resolve({ query: uri, requester: p.r }));
|
|
679
|
-
|
|
787
|
+
|
|
680
788
|
const queueResults = await Promise.allSettled(queuePromises);
|
|
681
789
|
queueResults.forEach(result => {
|
|
682
790
|
if (result.status === 'fulfilled' && result.value.tracks?.[0]) {
|
|
@@ -684,21 +792,20 @@ class Aqua extends EventEmitter {
|
|
|
684
792
|
}
|
|
685
793
|
});
|
|
686
794
|
}
|
|
687
|
-
|
|
795
|
+
|
|
688
796
|
if (player) {
|
|
689
797
|
player.paused = !!p.pa;
|
|
690
|
-
if (
|
|
798
|
+
if ((p.isPlaying || (p.pa && p.u)) && player.queue.size > 0) {
|
|
691
799
|
player.play();
|
|
692
800
|
}
|
|
693
801
|
}
|
|
694
802
|
} catch (error) {
|
|
695
|
-
// Silent fail for individual player restoration
|
|
696
803
|
}
|
|
697
804
|
}
|
|
698
805
|
|
|
699
806
|
async _waitForFirstNode() {
|
|
700
807
|
if (this.leastUsedNodes.length > 0) return;
|
|
701
|
-
|
|
808
|
+
|
|
702
809
|
return new Promise(resolve => {
|
|
703
810
|
const checkInterval = setInterval(() => {
|
|
704
811
|
if (this.leastUsedNodes.length > 0) {
|
|
@@ -709,6 +816,44 @@ class Aqua extends EventEmitter {
|
|
|
709
816
|
});
|
|
710
817
|
}
|
|
711
818
|
|
|
819
|
+
// Auto-resume utility methods
|
|
820
|
+
getBrokenPlayersCount() {
|
|
821
|
+
return this._brokenPlayers.size;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
getBrokenPlayers() {
|
|
825
|
+
return Array.from(this._brokenPlayers.entries()).map(([guildId, state]) => ({
|
|
826
|
+
guildId,
|
|
827
|
+
originalNodeId: state.originalNodeId,
|
|
828
|
+
brokenAt: state.brokenAt,
|
|
829
|
+
hasCurrentTrack: !!state.current,
|
|
830
|
+
queueSize: state.queue?.length || 0
|
|
831
|
+
}));
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
clearBrokenPlayers() {
|
|
835
|
+
this._brokenPlayers.clear();
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
async forceBrokenPlayerRebuild(guildId) {
|
|
839
|
+
const brokenState = this._brokenPlayers.get(guildId);
|
|
840
|
+
if (!brokenState) return false;
|
|
841
|
+
|
|
842
|
+
try {
|
|
843
|
+
const targetNode = this.nodeMap.get(brokenState.originalNodeId) || this.leastUsedNodes[0];
|
|
844
|
+
if (!targetNode || !targetNode.connected) {
|
|
845
|
+
throw new Error("No available nodes for rebuild");
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
await this._rebuildPlayer(brokenState, targetNode);
|
|
849
|
+
this._brokenPlayers.delete(guildId);
|
|
850
|
+
return true;
|
|
851
|
+
} catch (error) {
|
|
852
|
+
this.emit("error", null, new Error(`Failed to force rebuild player for guild ${guildId}: ${error.message}`));
|
|
853
|
+
return false;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
712
857
|
// Utility methods
|
|
713
858
|
resetFailoverAttempts(nodeId) {
|
|
714
859
|
this._failoverQueue.delete(nodeId);
|
|
@@ -747,15 +892,15 @@ class Aqua extends EventEmitter {
|
|
|
747
892
|
}
|
|
748
893
|
return stats;
|
|
749
894
|
}
|
|
750
|
-
|
|
895
|
+
|
|
751
896
|
async forceFailover(nodeIdentifier) {
|
|
752
897
|
const node = this.nodeMap.get(nodeIdentifier);
|
|
753
898
|
if (!node) return;
|
|
754
|
-
|
|
899
|
+
|
|
755
900
|
if (node.connected) {
|
|
756
901
|
await node.destroy();
|
|
757
902
|
}
|
|
758
|
-
|
|
903
|
+
|
|
759
904
|
this._cleanupNode(nodeIdentifier);
|
|
760
905
|
}
|
|
761
906
|
}
|
package/build/structures/Node.js
CHANGED
|
@@ -52,7 +52,11 @@ class Node {
|
|
|
52
52
|
this.reconnectAttempted = 0;
|
|
53
53
|
this.reconnectTimeoutId = null;
|
|
54
54
|
this.isDestroyed = false;
|
|
55
|
-
|
|
55
|
+
|
|
56
|
+
this._onOpen = this._onOpen.bind(this);
|
|
57
|
+
this._onError = this._onError.bind(this);
|
|
58
|
+
this._onMessage = this._onMessage.bind(this);
|
|
59
|
+
this._onClose = this._onClose.bind(this);
|
|
56
60
|
|
|
57
61
|
this._headers = this._constructHeaders();
|
|
58
62
|
this.initializeStats();
|
|
@@ -87,7 +91,6 @@ class Node {
|
|
|
87
91
|
async _onOpen() {
|
|
88
92
|
this.connected = true;
|
|
89
93
|
this.reconnectAttempted = 0;
|
|
90
|
-
this.lastHealthCheck = Date.now();
|
|
91
94
|
this.aqua.emit("debug", this.name, "WebSocket connection established");
|
|
92
95
|
|
|
93
96
|
if (this.aqua.bypassChecks?.nodeFetchInfo) return;
|
|
@@ -118,30 +121,26 @@ class Node {
|
|
|
118
121
|
return;
|
|
119
122
|
}
|
|
120
123
|
|
|
121
|
-
const op = payload
|
|
124
|
+
const { op, guildId } = payload;
|
|
122
125
|
if (!op) return;
|
|
123
126
|
|
|
124
|
-
this.lastHealthCheck = Date.now();
|
|
125
|
-
|
|
126
|
-
if (op === "stats") {
|
|
127
|
-
this._updateStats(payload);
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
if (op === "ready") {
|
|
131
|
-
this._handleReadyOp(payload);
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
127
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
128
|
+
switch (op) {
|
|
129
|
+
case "stats":
|
|
130
|
+
this._updateStats(payload);
|
|
131
|
+
break;
|
|
132
|
+
case "ready":
|
|
133
|
+
this._handleReadyOp(payload);
|
|
134
|
+
break;
|
|
135
|
+
default:
|
|
136
|
+
if (op.startsWith("Lyrics")) {
|
|
137
|
+
const player = guildId ? this.aqua.players.get(guildId) : null;
|
|
138
|
+
this.aqua.emit(op, player, payload.track || null, payload);
|
|
139
|
+
} else if (guildId) {
|
|
140
|
+
const player = this.aqua.players.get(guildId);
|
|
141
|
+
player?.emit(op, payload);
|
|
142
|
+
}
|
|
143
|
+
break;
|
|
145
144
|
}
|
|
146
145
|
}
|
|
147
146
|
|
|
@@ -202,7 +201,7 @@ class Node {
|
|
|
202
201
|
|
|
203
202
|
async connect() {
|
|
204
203
|
if (this.isDestroyed) return;
|
|
205
|
-
|
|
204
|
+
|
|
206
205
|
if (this.ws && this.ws.readyState === Node.WS_OPEN) {
|
|
207
206
|
this.aqua.emit("debug", this.name, "WebSocket already connected");
|
|
208
207
|
return;
|
|
@@ -215,10 +214,11 @@ class Node {
|
|
|
215
214
|
perMessageDeflate: false
|
|
216
215
|
});
|
|
217
216
|
|
|
218
|
-
this.ws.once("open", this._onOpen
|
|
219
|
-
this.ws.once("error", this._onError
|
|
220
|
-
this.ws.on("message", this._onMessage
|
|
221
|
-
this.ws.once("close", this._onClose
|
|
217
|
+
this.ws.once("open", this._onOpen);
|
|
218
|
+
this.ws.once("error", this._onError);
|
|
219
|
+
this.ws.on("message", this._onMessage);
|
|
220
|
+
this.ws.once("close", this._onClose);
|
|
221
|
+
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
cleanupExistingConnection() {
|
|
@@ -264,17 +264,17 @@ class Node {
|
|
|
264
264
|
this.stats.playingPlayers = newStats.playingPlayers ?? this.stats.playingPlayers;
|
|
265
265
|
this.stats.uptime = newStats.uptime ?? this.stats.uptime;
|
|
266
266
|
this.stats.ping = newStats.ping ?? this.stats.ping;
|
|
267
|
-
|
|
267
|
+
|
|
268
268
|
if (newStats.memory) {
|
|
269
269
|
Object.assign(this.stats.memory, newStats.memory);
|
|
270
270
|
this._calculateMemoryPercentages();
|
|
271
271
|
}
|
|
272
|
-
|
|
272
|
+
|
|
273
273
|
if (newStats.cpu) {
|
|
274
274
|
Object.assign(this.stats.cpu, newStats.cpu);
|
|
275
275
|
this._calculateCpuPercentages();
|
|
276
276
|
}
|
|
277
|
-
|
|
277
|
+
|
|
278
278
|
if (newStats.frameStats) {
|
|
279
279
|
Object.assign(this.stats.frameStats, newStats.frameStats);
|
|
280
280
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aqualink",
|
|
3
|
-
"version": "2.7.
|
|
3
|
+
"version": "2.7.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,7 +42,6 @@
|
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"ws": "^8.18.3",
|
|
45
|
-
"fs-extra": "^11.3.0",
|
|
46
45
|
"tseep": "^1.3.1"
|
|
47
46
|
},
|
|
48
47
|
"maintainers": [
|