aqualink 2.6.4 → 2.7.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/build/structures/Aqua.js +472 -161
- package/build/structures/Node.js +36 -43
- package/build/structures/Player.js +50 -31
- package/package.json +2 -2
package/build/structures/Aqua.js
CHANGED
|
@@ -14,9 +14,19 @@ const DEFAULT_OPTIONS = Object.freeze({
|
|
|
14
14
|
restVersion: 'v4',
|
|
15
15
|
plugins: [],
|
|
16
16
|
autoResume: false,
|
|
17
|
-
infiniteReconnects: false
|
|
17
|
+
infiniteReconnects: false,
|
|
18
|
+
failoverOptions: {
|
|
19
|
+
enabled: true,
|
|
20
|
+
maxRetries: 3,
|
|
21
|
+
retryDelay: 1000,
|
|
22
|
+
preservePosition: true,
|
|
23
|
+
resumePlayback: true,
|
|
24
|
+
cooldownTime: 5000,
|
|
25
|
+
maxFailoverAttempts: 5
|
|
26
|
+
}
|
|
18
27
|
});
|
|
19
|
-
|
|
28
|
+
|
|
29
|
+
const LEAST_USED_CACHE_TTL = 30;
|
|
20
30
|
|
|
21
31
|
class Aqua extends EventEmitter {
|
|
22
32
|
constructor(client, nodes, options = {}) {
|
|
@@ -34,35 +44,37 @@ class Aqua extends EventEmitter {
|
|
|
34
44
|
this.initiated = false;
|
|
35
45
|
this.version = pkgVersion;
|
|
36
46
|
|
|
37
|
-
this.options =
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
} = this.options;
|
|
49
|
-
|
|
50
|
-
this.shouldDeleteMessage = shouldDeleteMessage;
|
|
51
|
-
this.defaultSearchPlatform = defaultSearchPlatform;
|
|
52
|
-
this.leaveOnEnd = leaveOnEnd;
|
|
53
|
-
this.restVersion = restVersion;
|
|
54
|
-
this.plugins = plugins;
|
|
55
|
-
this.autoResume = autoResume;
|
|
56
|
-
this.infiniteReconnects = infiniteReconnects;
|
|
57
|
-
|
|
58
|
-
this.send = send || this.defaultSendFunction.bind(this);
|
|
47
|
+
this.options = { ...DEFAULT_OPTIONS, ...options };
|
|
48
|
+
this.failoverOptions = { ...DEFAULT_OPTIONS.failoverOptions, ...options.failoverOptions };
|
|
49
|
+
|
|
50
|
+
this.shouldDeleteMessage = this.options.shouldDeleteMessage;
|
|
51
|
+
this.defaultSearchPlatform = this.options.defaultSearchPlatform;
|
|
52
|
+
this.leaveOnEnd = this.options.leaveOnEnd;
|
|
53
|
+
this.restVersion = this.options.restVersion;
|
|
54
|
+
this.plugins = this.options.plugins;
|
|
55
|
+
this.autoResume = this.options.autoResume;
|
|
56
|
+
this.infiniteReconnects = this.options.infiniteReconnects;
|
|
57
|
+
this.send = this.options.send || this.defaultSendFunction.bind(this);
|
|
59
58
|
|
|
60
59
|
this._leastUsedCache = { nodes: [], timestamp: 0 };
|
|
60
|
+
|
|
61
|
+
this._nodeStates = new Map();
|
|
62
|
+
this._failoverQueue = new Map();
|
|
63
|
+
this._lastFailoverAttempt = new Map();
|
|
64
|
+
|
|
65
|
+
this._boundCleanupPlayer = this.cleanupPlayer.bind(this);
|
|
66
|
+
this._boundHandlePlayerDestroy = this._handlePlayerDestroy.bind(this);
|
|
61
67
|
}
|
|
62
68
|
|
|
63
|
-
defaultSendFunction(
|
|
64
|
-
const guild = this.client.guilds.cache.get(
|
|
65
|
-
if (guild)
|
|
69
|
+
defaultSendFunction(packet) {
|
|
70
|
+
const guild = this.client?.cache?.guilds.get(packet.d.guild_id) ?? this.client.guilds.cache.get(packet.d.guild_id);
|
|
71
|
+
if (guild) {
|
|
72
|
+
if (this.client.gateway) {
|
|
73
|
+
this.client.gateway.send(this.client.gateway.calculateShardId(packet.d.guild_id), packet);
|
|
74
|
+
} else {
|
|
75
|
+
guild.shard.send(packet);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
66
78
|
}
|
|
67
79
|
|
|
68
80
|
get leastUsedNodes() {
|
|
@@ -71,12 +83,9 @@ class Aqua extends EventEmitter {
|
|
|
71
83
|
return this._leastUsedCache.nodes;
|
|
72
84
|
}
|
|
73
85
|
|
|
74
|
-
const connectedNodes =
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
connectedNodes.sort((a, b) => a.rest.calls - b.rest.calls);
|
|
86
|
+
const connectedNodes = Array.from(this.nodeMap.values())
|
|
87
|
+
.filter(node => node.connected)
|
|
88
|
+
.sort((a, b) => a.rest.calls - b.rest.calls);
|
|
80
89
|
|
|
81
90
|
this._leastUsedCache = { nodes: connectedNodes, timestamp: now };
|
|
82
91
|
return connectedNodes;
|
|
@@ -87,15 +96,23 @@ class Aqua extends EventEmitter {
|
|
|
87
96
|
this.clientId = clientId;
|
|
88
97
|
|
|
89
98
|
try {
|
|
90
|
-
const nodePromises =
|
|
91
|
-
|
|
92
|
-
|
|
99
|
+
const nodePromises = this.nodes.map(node => this.createNode(node).catch(err => {
|
|
100
|
+
console.error(`Failed to create node ${node.name || node.host}:`, err);
|
|
101
|
+
return null;
|
|
102
|
+
}));
|
|
103
|
+
|
|
104
|
+
const results = await Promise.allSettled(nodePromises);
|
|
105
|
+
const successfulNodes = results.filter(r => r.status === 'fulfilled' && r.value).length;
|
|
106
|
+
|
|
107
|
+
if (successfulNodes === 0) {
|
|
108
|
+
throw new Error("No nodes could be connected");
|
|
93
109
|
}
|
|
94
|
-
await Promise.all(nodePromises);
|
|
95
110
|
|
|
96
|
-
|
|
97
|
-
plugin.load(this)
|
|
98
|
-
|
|
111
|
+
await Promise.all(this.plugins.map(plugin =>
|
|
112
|
+
Promise.resolve(plugin.load(this)).catch(err =>
|
|
113
|
+
console.error("Plugin load error:", err)
|
|
114
|
+
)
|
|
115
|
+
));
|
|
99
116
|
|
|
100
117
|
this.initiated = true;
|
|
101
118
|
} catch (error) {
|
|
@@ -112,15 +129,17 @@ class Aqua extends EventEmitter {
|
|
|
112
129
|
|
|
113
130
|
const node = new Node(this, options, this.options);
|
|
114
131
|
this.nodeMap.set(nodeId, node);
|
|
115
|
-
this.
|
|
132
|
+
this._invalidateCache();
|
|
133
|
+
|
|
134
|
+
this._nodeStates.set(nodeId, { connected: false, failoverInProgress: false });
|
|
116
135
|
|
|
117
136
|
try {
|
|
118
137
|
await node.connect();
|
|
138
|
+
this._nodeStates.set(nodeId, { connected: true, failoverInProgress: false });
|
|
119
139
|
this.emit("nodeCreate", node);
|
|
120
140
|
return node;
|
|
121
141
|
} catch (error) {
|
|
122
|
-
this.
|
|
123
|
-
console.error("Failed to connect node:", error);
|
|
142
|
+
this._cleanupNode(nodeId);
|
|
124
143
|
throw error;
|
|
125
144
|
}
|
|
126
145
|
}
|
|
@@ -129,12 +148,263 @@ class Aqua extends EventEmitter {
|
|
|
129
148
|
const node = this.nodeMap.get(identifier);
|
|
130
149
|
if (!node) return;
|
|
131
150
|
|
|
132
|
-
|
|
133
|
-
this.nodeMap.delete(identifier);
|
|
134
|
-
this._leastUsedCache.timestamp = 0;
|
|
151
|
+
this._cleanupNode(identifier);
|
|
135
152
|
this.emit("nodeDestroy", node);
|
|
136
153
|
}
|
|
137
154
|
|
|
155
|
+
_cleanupNode(nodeId) {
|
|
156
|
+
this.nodeMap.delete(nodeId);
|
|
157
|
+
this._nodeStates.delete(nodeId);
|
|
158
|
+
this._failoverQueue.delete(nodeId);
|
|
159
|
+
this._lastFailoverAttempt.delete(nodeId);
|
|
160
|
+
this._invalidateCache();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
_invalidateCache() {
|
|
164
|
+
this._leastUsedCache.timestamp = 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async handleNodeFailover(failedNode) {
|
|
168
|
+
if (!this.failoverOptions.enabled) return;
|
|
169
|
+
|
|
170
|
+
const nodeId = failedNode.name || failedNode.host;
|
|
171
|
+
const now = Date.now();
|
|
172
|
+
|
|
173
|
+
const nodeState = this._nodeStates.get(nodeId);
|
|
174
|
+
if (nodeState?.failoverInProgress) return;
|
|
175
|
+
|
|
176
|
+
const lastAttempt = this._lastFailoverAttempt.get(nodeId);
|
|
177
|
+
if (lastAttempt && (now - lastAttempt) < this.failoverOptions.cooldownTime) return;
|
|
178
|
+
|
|
179
|
+
const currentAttempts = this._failoverQueue.get(nodeId) || 0;
|
|
180
|
+
if (currentAttempts >= this.failoverOptions.maxFailoverAttempts) return;
|
|
181
|
+
|
|
182
|
+
this._nodeStates.set(nodeId, { connected: false, failoverInProgress: true });
|
|
183
|
+
this._lastFailoverAttempt.set(nodeId, now);
|
|
184
|
+
this._failoverQueue.set(nodeId, currentAttempts + 1);
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
this.emit("nodeFailover", failedNode);
|
|
188
|
+
|
|
189
|
+
const affectedPlayers = this._getPlayersForNode(failedNode);
|
|
190
|
+
if (affectedPlayers.length === 0) {
|
|
191
|
+
this._nodeStates.set(nodeId, { connected: false, failoverInProgress: false });
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const availableNodes = this._getAvailableNodesForFailover(failedNode);
|
|
196
|
+
if (availableNodes.length === 0) {
|
|
197
|
+
this.emit("error", null, new Error("No available nodes for failover"));
|
|
198
|
+
this._nodeStates.set(nodeId, { connected: false, failoverInProgress: false });
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const failoverResults = await this._migratePlayersWithRetry(affectedPlayers, availableNodes);
|
|
203
|
+
|
|
204
|
+
const successful = failoverResults.filter(r => r.success).length;
|
|
205
|
+
const failed = failoverResults.length - successful;
|
|
206
|
+
|
|
207
|
+
if (successful > 0) {
|
|
208
|
+
this.emit("nodeFailoverComplete", failedNode, successful, failed);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
} catch (error) {
|
|
212
|
+
this.emit("error", null, new Error(`Failover failed for node ${nodeId}: ${error.message}`));
|
|
213
|
+
} finally {
|
|
214
|
+
this._nodeStates.set(nodeId, { connected: false, failoverInProgress: false });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
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
|
+
async _migratePlayersWithRetry(players, availableNodes) {
|
|
235
|
+
const results = [];
|
|
236
|
+
|
|
237
|
+
const concurrency = 3;
|
|
238
|
+
for (let i = 0; i < players.length; i += concurrency) {
|
|
239
|
+
const batch = players.slice(i, i + concurrency);
|
|
240
|
+
const batchPromises = batch.map(async player => {
|
|
241
|
+
try {
|
|
242
|
+
const result = await this._migratePlayer(player, availableNodes);
|
|
243
|
+
return { player, success: true, result };
|
|
244
|
+
} catch (error) {
|
|
245
|
+
await this._boundCleanupPlayer(player);
|
|
246
|
+
return { player, success: false, error };
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const batchResults = await Promise.allSettled(batchPromises);
|
|
251
|
+
results.push(...batchResults.map(r => r.value || r.reason));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return results;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async _migratePlayer(player, availableNodes) {
|
|
258
|
+
if (!player || !availableNodes.length) {
|
|
259
|
+
throw new Error("Invalid player or no available nodes");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const guildId = player.guildId;
|
|
263
|
+
let retryCount = 0;
|
|
264
|
+
|
|
265
|
+
while (retryCount < this.failoverOptions.maxRetries) {
|
|
266
|
+
try {
|
|
267
|
+
const targetNode = this._selectBestNode(availableNodes, player);
|
|
268
|
+
if (!targetNode) throw new Error("No suitable node found");
|
|
269
|
+
|
|
270
|
+
const playerState = this._capturePlayerState(player);
|
|
271
|
+
if (!playerState) throw new Error("Failed to capture player state");
|
|
272
|
+
|
|
273
|
+
const newPlayer = await this._createPlayerOnNode(targetNode, player, playerState);
|
|
274
|
+
if (!newPlayer) throw new Error("Failed to create player on target node");
|
|
275
|
+
|
|
276
|
+
await this._restorePlayerState(newPlayer, playerState);
|
|
277
|
+
|
|
278
|
+
newPlayer.destroy();
|
|
279
|
+
if (playerState.current) {
|
|
280
|
+
newPlayer.queue.add(playerState.current);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
this.emit("playerMigrated", player, newPlayer, targetNode);
|
|
284
|
+
return newPlayer;
|
|
285
|
+
|
|
286
|
+
} catch (error) {
|
|
287
|
+
retryCount++;
|
|
288
|
+
if (retryCount < this.failoverOptions.maxRetries) {
|
|
289
|
+
await this._delay(this.failoverOptions.retryDelay);
|
|
290
|
+
} else {
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
_selectBestNode(availableNodes, player) {
|
|
298
|
+
if (player.region) {
|
|
299
|
+
const regionNode = availableNodes.find(node =>
|
|
300
|
+
node.regions?.includes(player.region.toLowerCase())
|
|
301
|
+
);
|
|
302
|
+
if (regionNode) return regionNode;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return availableNodes[0];
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
_capturePlayerState(player) {
|
|
309
|
+
try {
|
|
310
|
+
return {
|
|
311
|
+
guildId: player.guildId,
|
|
312
|
+
textChannel: player.textChannel,
|
|
313
|
+
voiceChannel: player.voiceChannel,
|
|
314
|
+
volume: player.volume || 100,
|
|
315
|
+
paused: player.paused || false,
|
|
316
|
+
position: player.position || 0,
|
|
317
|
+
current: player.current ? { ...player.current } : null,
|
|
318
|
+
queue: player.queue?.tracks ? [...player.queue.tracks] : [],
|
|
319
|
+
repeat: player.repeat,
|
|
320
|
+
shuffle: player.shuffle,
|
|
321
|
+
deaf: player.deaf || false,
|
|
322
|
+
mute: player.mute || false,
|
|
323
|
+
region: player.region,
|
|
324
|
+
requester: player.requester,
|
|
325
|
+
timestamp: Date.now()
|
|
326
|
+
};
|
|
327
|
+
} catch (error) {
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async _createPlayerOnNode(targetNode, originalPlayer, playerState) {
|
|
333
|
+
const options = {
|
|
334
|
+
guildId: playerState.guildId,
|
|
335
|
+
textChannel: playerState.textChannel,
|
|
336
|
+
voiceChannel: playerState.voiceChannel,
|
|
337
|
+
defaultVolume: playerState.volume || 100,
|
|
338
|
+
deaf: playerState.deaf || false,
|
|
339
|
+
mute: playerState.mute || false,
|
|
340
|
+
region: playerState.region
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
return this.createPlayer(targetNode, options);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async _restorePlayerState(newPlayer, playerState) {
|
|
347
|
+
if (!newPlayer || !playerState) return;
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
// Batch operations where possible
|
|
351
|
+
const operations = [];
|
|
352
|
+
|
|
353
|
+
if (playerState.volume !== undefined) {
|
|
354
|
+
operations.push(newPlayer.setVolume(playerState.volume));
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Restore queue efficiently
|
|
358
|
+
if (playerState.queue?.length > 0) {
|
|
359
|
+
newPlayer.queue.add(...playerState.queue);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Wait for all operations
|
|
363
|
+
await Promise.all(operations);
|
|
364
|
+
|
|
365
|
+
// Handle current track restoration
|
|
366
|
+
if (playerState.current && this.failoverOptions.preservePosition) {
|
|
367
|
+
newPlayer.queue.unshift(playerState.current);
|
|
368
|
+
|
|
369
|
+
if (this.failoverOptions.resumePlayback) {
|
|
370
|
+
await newPlayer.play();
|
|
371
|
+
|
|
372
|
+
if (playerState.position > 0) {
|
|
373
|
+
await this._delay(300); // Reduced delay
|
|
374
|
+
await newPlayer.seek(playerState.position);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (playerState.paused) {
|
|
378
|
+
await newPlayer.pause();
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Restore other properties
|
|
384
|
+
Object.assign(newPlayer, {
|
|
385
|
+
repeat: playerState.repeat,
|
|
386
|
+
shuffle: playerState.shuffle
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
} catch (error) {
|
|
390
|
+
throw error;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
_delay(ms) {
|
|
395
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Optimized cleanup
|
|
399
|
+
async cleanupPlayer(player) {
|
|
400
|
+
if (!player) return;
|
|
401
|
+
try {
|
|
402
|
+
await player.destroy();
|
|
403
|
+
} catch (error) {
|
|
404
|
+
// Silent fail for cleanup
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
138
408
|
updateVoiceState({ d, t }) {
|
|
139
409
|
const player = this.players.get(d.guild_id);
|
|
140
410
|
if (!player) return;
|
|
@@ -147,7 +417,7 @@ class Aqua extends EventEmitter {
|
|
|
147
417
|
}
|
|
148
418
|
|
|
149
419
|
if (d.channel_id === null) {
|
|
150
|
-
this.
|
|
420
|
+
this._boundCleanupPlayer(player);
|
|
151
421
|
}
|
|
152
422
|
}
|
|
153
423
|
}
|
|
@@ -164,34 +434,33 @@ class Aqua extends EventEmitter {
|
|
|
164
434
|
}
|
|
165
435
|
}
|
|
166
436
|
|
|
437
|
+
// Optimized sorting with caching
|
|
167
438
|
const loadCache = new Map();
|
|
168
439
|
regionNodes.sort((a, b) => {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
return
|
|
440
|
+
const loadA = loadCache.get(a) ?? (loadCache.set(a, this._calculateLoad(a)), loadCache.get(a));
|
|
441
|
+
const loadB = loadCache.get(b) ?? (loadCache.set(b, this._calculateLoad(b)), loadCache.get(b));
|
|
442
|
+
return loadA - loadB;
|
|
172
443
|
});
|
|
173
444
|
|
|
174
445
|
return regionNodes;
|
|
175
446
|
}
|
|
176
447
|
|
|
177
|
-
|
|
448
|
+
_calculateLoad(node) {
|
|
178
449
|
const stats = node?.stats?.cpu;
|
|
179
450
|
if (!stats) return 0;
|
|
180
|
-
|
|
181
|
-
return (systemLoad / cores) * 100;
|
|
451
|
+
return (stats.systemLoad / stats.cores) * 100;
|
|
182
452
|
}
|
|
183
453
|
|
|
184
454
|
createConnection(options) {
|
|
185
455
|
if (!this.initiated) throw new Error("Aqua must be initialized before this operation");
|
|
186
456
|
|
|
187
457
|
const existingPlayer = this.players.get(options.guildId);
|
|
188
|
-
if (existingPlayer
|
|
458
|
+
if (existingPlayer?.voiceChannel) return existingPlayer;
|
|
189
459
|
|
|
190
460
|
const availableNodes = options.region ? this.fetchRegion(options.region) : this.leastUsedNodes;
|
|
191
|
-
|
|
192
|
-
if (!node) throw new Error("No nodes are available");
|
|
461
|
+
if (!availableNodes.length) throw new Error("No nodes are available");
|
|
193
462
|
|
|
194
|
-
return this.createPlayer(
|
|
463
|
+
return this.createPlayer(availableNodes[0], options);
|
|
195
464
|
}
|
|
196
465
|
|
|
197
466
|
createPlayer(node, options) {
|
|
@@ -200,16 +469,18 @@ class Aqua extends EventEmitter {
|
|
|
200
469
|
const player = new Player(this, node, options);
|
|
201
470
|
this.players.set(options.guildId, player);
|
|
202
471
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
this.emit("playerDestroy", player);
|
|
206
|
-
});
|
|
207
|
-
|
|
472
|
+
// Use pre-bound method for better performance
|
|
473
|
+
player.once("destroy", this._boundHandlePlayerDestroy);
|
|
208
474
|
player.connect(options);
|
|
209
475
|
this.emit("playerCreate", player);
|
|
210
476
|
return player;
|
|
211
477
|
}
|
|
212
478
|
|
|
479
|
+
_handlePlayerDestroy(player) {
|
|
480
|
+
this.players.delete(player.guildId);
|
|
481
|
+
this.emit("playerDestroy", player);
|
|
482
|
+
}
|
|
483
|
+
|
|
213
484
|
async destroyPlayer(guildId) {
|
|
214
485
|
const player = this.players.get(guildId);
|
|
215
486
|
if (!player) return;
|
|
@@ -220,14 +491,14 @@ class Aqua extends EventEmitter {
|
|
|
220
491
|
this.players.delete(guildId);
|
|
221
492
|
this.emit("playerDestroy", player);
|
|
222
493
|
} catch (error) {
|
|
223
|
-
|
|
494
|
+
// Silent fail for cleanup
|
|
224
495
|
}
|
|
225
496
|
}
|
|
226
497
|
|
|
227
498
|
async resolve({ query, source = this.defaultSearchPlatform, requester, nodes }) {
|
|
228
499
|
if (!this.initiated) throw new Error("Aqua must be initialized before this operation");
|
|
229
500
|
|
|
230
|
-
const requestNode = this.
|
|
501
|
+
const requestNode = this._getRequestNode(nodes);
|
|
231
502
|
const formattedQuery = URL_REGEX.test(query) ? query : `${source}:${query}`;
|
|
232
503
|
|
|
233
504
|
try {
|
|
@@ -235,10 +506,10 @@ class Aqua extends EventEmitter {
|
|
|
235
506
|
const response = await requestNode.rest.makeRequest("GET", endpoint);
|
|
236
507
|
|
|
237
508
|
if (["empty", "NO_MATCHES"].includes(response.loadType)) {
|
|
238
|
-
return
|
|
509
|
+
return this._createEmptyResponse();
|
|
239
510
|
}
|
|
240
511
|
|
|
241
|
-
return this.
|
|
512
|
+
return this._constructResponse(response, requester, requestNode);
|
|
242
513
|
} catch (error) {
|
|
243
514
|
if (error.name === "AbortError") {
|
|
244
515
|
throw new Error("Request timed out");
|
|
@@ -247,19 +518,16 @@ class Aqua extends EventEmitter {
|
|
|
247
518
|
}
|
|
248
519
|
}
|
|
249
520
|
|
|
250
|
-
|
|
521
|
+
_getRequestNode(nodes) {
|
|
251
522
|
if (!nodes) return this.leastUsedNodes[0];
|
|
252
|
-
|
|
253
523
|
if (nodes instanceof Node) return nodes;
|
|
254
524
|
if (typeof nodes === "string") {
|
|
255
|
-
|
|
256
|
-
return mappedNode || this.leastUsedNodes[0];
|
|
525
|
+
return this.nodeMap.get(nodes) || this.leastUsedNodes[0];
|
|
257
526
|
}
|
|
258
|
-
|
|
259
527
|
throw new TypeError(`'nodes' must be a string or Node instance, received: ${typeof nodes}`);
|
|
260
528
|
}
|
|
261
529
|
|
|
262
|
-
|
|
530
|
+
_createEmptyResponse() {
|
|
263
531
|
return {
|
|
264
532
|
loadType: "empty",
|
|
265
533
|
exception: null,
|
|
@@ -269,7 +537,7 @@ class Aqua extends EventEmitter {
|
|
|
269
537
|
};
|
|
270
538
|
}
|
|
271
539
|
|
|
272
|
-
|
|
540
|
+
_constructResponse(response, requester, requestNode) {
|
|
273
541
|
const baseResponse = {
|
|
274
542
|
loadType: response.loadType,
|
|
275
543
|
exception: null,
|
|
@@ -283,44 +551,34 @@ class Aqua extends EventEmitter {
|
|
|
283
551
|
return baseResponse;
|
|
284
552
|
}
|
|
285
553
|
|
|
286
|
-
const trackFactory = (trackData) => new Track(trackData, requester, requestNode);
|
|
287
|
-
|
|
288
554
|
switch (response.loadType) {
|
|
289
555
|
case "track":
|
|
290
556
|
if (response.data) {
|
|
291
|
-
baseResponse.tracks.push(
|
|
557
|
+
baseResponse.tracks.push(new Track(response.data, requester, requestNode));
|
|
292
558
|
}
|
|
293
559
|
break;
|
|
560
|
+
|
|
294
561
|
case "playlist": {
|
|
295
562
|
const info = response.data?.info;
|
|
296
563
|
if (info) {
|
|
297
|
-
|
|
564
|
+
baseResponse.playlistInfo = {
|
|
298
565
|
name: info.name ?? info.title,
|
|
299
566
|
thumbnail: response.data.pluginInfo?.artworkUrl ?? (response.data.tracks?.[0]?.info?.artworkUrl || null),
|
|
300
567
|
...info
|
|
301
568
|
};
|
|
302
|
-
baseResponse.playlistInfo = playlistInfo;
|
|
303
569
|
}
|
|
304
570
|
|
|
305
571
|
const tracks = response.data?.tracks;
|
|
306
572
|
if (tracks?.length) {
|
|
307
|
-
|
|
308
|
-
baseResponse.tracks = new Array(trackCount);
|
|
309
|
-
for (let i = 0; i < trackCount; i++) {
|
|
310
|
-
baseResponse.tracks[i] = trackFactory(tracks[i]);
|
|
311
|
-
}
|
|
573
|
+
baseResponse.tracks = tracks.map(track => new Track(track, requester, requestNode));
|
|
312
574
|
}
|
|
313
575
|
break;
|
|
314
576
|
}
|
|
315
577
|
|
|
316
578
|
case "search": {
|
|
317
579
|
const searchData = response.data ?? [];
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
baseResponse.tracks = new Array(dataLength);
|
|
321
|
-
for (let i = 0; i < dataLength; i++) {
|
|
322
|
-
baseResponse.tracks[i] = trackFactory(searchData[i]);
|
|
323
|
-
}
|
|
580
|
+
if (searchData.length) {
|
|
581
|
+
baseResponse.tracks = searchData.map(track => new Track(track, requester, requestNode));
|
|
324
582
|
}
|
|
325
583
|
break;
|
|
326
584
|
}
|
|
@@ -342,110 +600,163 @@ class Aqua extends EventEmitter {
|
|
|
342
600
|
const { tracks } = await this.resolve({ query, source, requester });
|
|
343
601
|
return tracks || null;
|
|
344
602
|
} catch (error) {
|
|
345
|
-
console.error("Search error:", error);
|
|
346
603
|
return null;
|
|
347
604
|
}
|
|
348
605
|
}
|
|
349
606
|
|
|
607
|
+
// Optimized save/load methods
|
|
350
608
|
async savePlayer(filePath = "./AquaPlayers.json") {
|
|
351
|
-
const data = Array.from(this.players.values()
|
|
609
|
+
const data = Array.from(this.players.values(), player => ({
|
|
352
610
|
g: player.guildId,
|
|
353
611
|
t: player.textChannel,
|
|
354
612
|
v: player.voiceChannel,
|
|
355
613
|
u: player.current?.uri || null,
|
|
356
614
|
p: player.position || 0,
|
|
357
615
|
ts: player.timestamp || 0,
|
|
358
|
-
q: player.queue?.tracks?.map(tr => tr.uri)
|
|
616
|
+
q: player.queue?.tracks?.slice(0, 5).map(tr => tr.uri) || [],
|
|
359
617
|
r: player.requester || player.current?.requester,
|
|
360
618
|
vol: player.volume,
|
|
361
|
-
pa: player.paused
|
|
619
|
+
pa: player.paused,
|
|
620
|
+
n: player.nodes?.name || null
|
|
362
621
|
}));
|
|
622
|
+
|
|
363
623
|
await fs.writeFile(filePath, JSON.stringify(data), "utf8");
|
|
364
|
-
this.emit("debug", "Aqua", `Saved ${data.length} players to ${filePath}`);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
async waitForFirstNode() {
|
|
368
|
-
if (this.leastUsedNodes.length > 0) return;
|
|
369
|
-
return new Promise(resolve => {
|
|
370
|
-
const check = () => {
|
|
371
|
-
if (this.leastUsedNodes.length > 0) {
|
|
372
|
-
resolve();
|
|
373
|
-
} else {
|
|
374
|
-
setTimeout(check, 100);
|
|
375
|
-
}
|
|
376
|
-
};
|
|
377
|
-
check();
|
|
378
|
-
});
|
|
379
624
|
}
|
|
380
625
|
|
|
381
626
|
async loadPlayers(filePath = "./AquaPlayers.json") {
|
|
382
627
|
try {
|
|
383
|
-
await fs.
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
628
|
+
await fs.access(filePath);
|
|
629
|
+
await this._waitForFirstNode();
|
|
630
|
+
|
|
631
|
+
const data = JSON.parse(await fs.readFile(filePath, "utf8"));
|
|
632
|
+
|
|
633
|
+
// Process in batches to avoid overwhelming
|
|
634
|
+
const batchSize = 5;
|
|
635
|
+
for (let i = 0; i < data.length; i += batchSize) {
|
|
636
|
+
const batch = data.slice(i, i + batchSize);
|
|
637
|
+
await Promise.all(batch.map(p => this._restorePlayer(p)));
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
await fs.writeFile(filePath, "[]", "utf8");
|
|
641
|
+
} catch (error) {
|
|
642
|
+
// Silent fail if file doesn't exist
|
|
387
643
|
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
async _restorePlayer(p) {
|
|
388
647
|
try {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
648
|
+
let player = this.players.get(p.g);
|
|
649
|
+
if (!player) {
|
|
650
|
+
const targetNode = (p.n && this.nodeMap.get(p.n)?.connected) ?
|
|
651
|
+
this.nodeMap.get(p.n) : this.leastUsedNodes[0];
|
|
652
|
+
|
|
653
|
+
if (!targetNode) return;
|
|
654
|
+
|
|
655
|
+
player = await this.createConnection({
|
|
656
|
+
guildId: p.g,
|
|
657
|
+
textChannel: p.t,
|
|
658
|
+
voiceChannel: p.v,
|
|
659
|
+
defaultVolume: p.vol || 65,
|
|
660
|
+
deaf: true
|
|
661
|
+
});
|
|
662
|
+
}
|
|
402
663
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
}
|
|
411
|
-
}
|
|
664
|
+
// Restore current track
|
|
665
|
+
if (p.u && player) {
|
|
666
|
+
const resolved = await this.resolve({ query: p.u, requester: p.r });
|
|
667
|
+
if (resolved.tracks?.[0]) {
|
|
668
|
+
player.queue.add(resolved.tracks[0]);
|
|
669
|
+
player.position = p.p || 0;
|
|
670
|
+
if (typeof p.ts === "number") player.timestamp = p.ts;
|
|
412
671
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
if (player) {
|
|
428
|
-
player.paused = !!p.pa;
|
|
429
|
-
if (!player.playing && !player.paused && player.queue.size > 0) {
|
|
430
|
-
player.play();
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Restore queue
|
|
675
|
+
if (p.q?.length && player) {
|
|
676
|
+
const queuePromises = p.q
|
|
677
|
+
.filter(uri => uri !== p.u)
|
|
678
|
+
.map(uri => this.resolve({ query: uri, requester: p.r }));
|
|
679
|
+
|
|
680
|
+
const queueResults = await Promise.allSettled(queuePromises);
|
|
681
|
+
queueResults.forEach(result => {
|
|
682
|
+
if (result.status === 'fulfilled' && result.value.tracks?.[0]) {
|
|
683
|
+
player.queue.add(result.value.tracks[0]);
|
|
431
684
|
}
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
if (player) {
|
|
689
|
+
player.paused = !!p.pa;
|
|
690
|
+
if (!player.playing && !player.paused && player.queue.size > 0) {
|
|
691
|
+
player.play();
|
|
432
692
|
}
|
|
433
693
|
}
|
|
434
|
-
await fs.writeFile(filePath, "[]", "utf8");
|
|
435
|
-
this.emit("debug", "Aqua", `Loaded players from ${filePath} and cleared its content.`);
|
|
436
694
|
} catch (error) {
|
|
437
|
-
|
|
695
|
+
// Silent fail for individual player restoration
|
|
438
696
|
}
|
|
439
697
|
}
|
|
440
698
|
|
|
441
|
-
async
|
|
442
|
-
if (
|
|
699
|
+
async _waitForFirstNode() {
|
|
700
|
+
if (this.leastUsedNodes.length > 0) return;
|
|
701
|
+
|
|
702
|
+
return new Promise(resolve => {
|
|
703
|
+
const checkInterval = setInterval(() => {
|
|
704
|
+
if (this.leastUsedNodes.length > 0) {
|
|
705
|
+
clearInterval(checkInterval);
|
|
706
|
+
resolve();
|
|
707
|
+
}
|
|
708
|
+
}, 100);
|
|
709
|
+
});
|
|
710
|
+
}
|
|
443
711
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
712
|
+
// Utility methods
|
|
713
|
+
resetFailoverAttempts(nodeId) {
|
|
714
|
+
this._failoverQueue.delete(nodeId);
|
|
715
|
+
this._lastFailoverAttempt.delete(nodeId);
|
|
716
|
+
const nodeState = this._nodeStates.get(nodeId);
|
|
717
|
+
if (nodeState) nodeState.failoverInProgress = false;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
getFailoverStatus() {
|
|
721
|
+
const status = {};
|
|
722
|
+
for (const [nodeId, attempts] of this._failoverQueue) {
|
|
723
|
+
const lastAttempt = this._lastFailoverAttempt.get(nodeId);
|
|
724
|
+
const nodeState = this._nodeStates.get(nodeId);
|
|
725
|
+
status[nodeId] = {
|
|
726
|
+
attempts,
|
|
727
|
+
lastAttempt,
|
|
728
|
+
inProgress: nodeState?.failoverInProgress || false,
|
|
729
|
+
connected: nodeState?.connected || false
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
return status;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
getNodeStats() {
|
|
736
|
+
const stats = {};
|
|
737
|
+
for (const [name, node] of this.nodeMap) {
|
|
738
|
+
stats[name] = {
|
|
739
|
+
connected: node.connected,
|
|
740
|
+
players: node.stats?.players || 0,
|
|
741
|
+
playingPlayers: node.stats?.playingPlayers || 0,
|
|
742
|
+
uptime: node.stats?.uptime || 0,
|
|
743
|
+
cpu: node.stats?.cpu || {},
|
|
744
|
+
memory: node.stats?.memory || {},
|
|
745
|
+
ping: node.stats?.ping || 0
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
return stats;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
async forceFailover(nodeIdentifier) {
|
|
752
|
+
const node = this.nodeMap.get(nodeIdentifier);
|
|
753
|
+
if (!node) return;
|
|
754
|
+
|
|
755
|
+
if (node.connected) {
|
|
756
|
+
await node.destroy();
|
|
448
757
|
}
|
|
758
|
+
|
|
759
|
+
this._cleanupNode(nodeIdentifier);
|
|
449
760
|
}
|
|
450
761
|
}
|
|
451
762
|
|
package/build/structures/Node.js
CHANGED
|
@@ -51,9 +51,10 @@ class Node {
|
|
|
51
51
|
this.ws = null;
|
|
52
52
|
this.reconnectAttempted = 0;
|
|
53
53
|
this.reconnectTimeoutId = null;
|
|
54
|
+
this.isDestroyed = false;
|
|
55
|
+
this.lastHealthCheck = Date.now();
|
|
54
56
|
|
|
55
57
|
this._headers = this._constructHeaders();
|
|
56
|
-
|
|
57
58
|
this.initializeStats();
|
|
58
59
|
}
|
|
59
60
|
|
|
@@ -73,7 +74,7 @@ class Node {
|
|
|
73
74
|
const headers = {
|
|
74
75
|
Authorization: this.password,
|
|
75
76
|
"User-Id": this.aqua.clientId,
|
|
76
|
-
"Client-Name": `Aqua/${this.aqua.version} (https://github.com/ToddyTheNoobDud/AquaLink`
|
|
77
|
+
"Client-Name": `Aqua/${this.aqua.version} (https://github.com/ToddyTheNoobDud/AquaLink)`
|
|
77
78
|
};
|
|
78
79
|
|
|
79
80
|
if (this.sessionId) {
|
|
@@ -86,13 +87,13 @@ class Node {
|
|
|
86
87
|
async _onOpen() {
|
|
87
88
|
this.connected = true;
|
|
88
89
|
this.reconnectAttempted = 0;
|
|
90
|
+
this.lastHealthCheck = Date.now();
|
|
89
91
|
this.aqua.emit("debug", this.name, "WebSocket connection established");
|
|
90
92
|
|
|
91
93
|
if (this.aqua.bypassChecks?.nodeFetchInfo) return;
|
|
92
94
|
|
|
93
95
|
try {
|
|
94
96
|
this.info = await this.rest.makeRequest("GET", "/v4/info");
|
|
95
|
-
|
|
96
97
|
this.aqua.emit("nodeConnected", this);
|
|
97
98
|
|
|
98
99
|
if (this.autoResume && this.sessionId) {
|
|
@@ -120,41 +121,37 @@ class Node {
|
|
|
120
121
|
const op = payload?.op;
|
|
121
122
|
if (!op) return;
|
|
122
123
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const player = this.aqua.players.get(payload.guildId);
|
|
145
|
-
if (player) player.emit(op, payload);
|
|
146
|
-
}
|
|
147
|
-
break;
|
|
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
|
+
|
|
135
|
+
if (op.startsWith("Lyrics")) {
|
|
136
|
+
const player = payload.guildId ? this.aqua.players.get(payload.guildId) : null;
|
|
137
|
+
const track = payload.track || null;
|
|
138
|
+
this.aqua.emit(op, player, track, payload);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (payload.guildId) {
|
|
143
|
+
const player = this.aqua.players.get(payload.guildId);
|
|
144
|
+
if (player) player.emit(op, payload);
|
|
148
145
|
}
|
|
149
146
|
}
|
|
150
147
|
|
|
151
148
|
_onClose(code, reason) {
|
|
152
149
|
this.connected = false;
|
|
150
|
+
const reasonStr = reason?.toString() || "No reason provided";
|
|
153
151
|
|
|
154
|
-
this.aqua.emit("nodeDisconnect", this, {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
});
|
|
152
|
+
this.aqua.emit("nodeDisconnect", this, { code, reason: reasonStr });
|
|
153
|
+
|
|
154
|
+
this.aqua.handleNodeFailover(this);
|
|
158
155
|
|
|
159
156
|
this.scheduleReconnect(code);
|
|
160
157
|
}
|
|
@@ -162,7 +159,7 @@ class Node {
|
|
|
162
159
|
scheduleReconnect(code) {
|
|
163
160
|
this.clearReconnectTimeout();
|
|
164
161
|
|
|
165
|
-
if (code === Node.WS_CLOSE_NORMAL) {
|
|
162
|
+
if (code === Node.WS_CLOSE_NORMAL || this.isDestroyed) {
|
|
166
163
|
this.aqua.emit("debug", this.name, "WebSocket closed normally, not reconnecting");
|
|
167
164
|
return;
|
|
168
165
|
}
|
|
@@ -204,6 +201,8 @@ class Node {
|
|
|
204
201
|
}
|
|
205
202
|
|
|
206
203
|
async connect() {
|
|
204
|
+
if (this.isDestroyed) return;
|
|
205
|
+
|
|
207
206
|
if (this.ws && this.ws.readyState === Node.WS_OPEN) {
|
|
208
207
|
this.aqua.emit("debug", this.name, "WebSocket already connected");
|
|
209
208
|
return;
|
|
@@ -231,7 +230,7 @@ class Node {
|
|
|
231
230
|
try {
|
|
232
231
|
this.ws.close();
|
|
233
232
|
} catch (err) {
|
|
234
|
-
|
|
233
|
+
this.emitError(`Failed to close WebSocket: ${err.message}`);
|
|
235
234
|
}
|
|
236
235
|
}
|
|
237
236
|
|
|
@@ -239,25 +238,21 @@ class Node {
|
|
|
239
238
|
}
|
|
240
239
|
|
|
241
240
|
destroy(clean = false) {
|
|
241
|
+
this.isDestroyed = true;
|
|
242
242
|
this.clearReconnectTimeout();
|
|
243
243
|
this.cleanupExistingConnection();
|
|
244
244
|
|
|
245
245
|
if (!clean) {
|
|
246
|
-
|
|
247
|
-
if (player.nodes === this) {
|
|
248
|
-
player.destroy();
|
|
249
|
-
}
|
|
250
|
-
}
|
|
246
|
+
this.aqua.handleNodeFailover(this);
|
|
251
247
|
}
|
|
252
248
|
|
|
253
249
|
this.connected = false;
|
|
254
|
-
this.aqua.
|
|
250
|
+
this.aqua.destroyNode(this.name);
|
|
255
251
|
this.aqua.emit("nodeDestroy", this);
|
|
256
252
|
this.info = null;
|
|
257
253
|
}
|
|
258
254
|
|
|
259
255
|
async getStats() {
|
|
260
|
-
|
|
261
256
|
if (this.connected && this.stats) {
|
|
262
257
|
return this.stats;
|
|
263
258
|
}
|
|
@@ -344,13 +339,11 @@ class Node {
|
|
|
344
339
|
async resumePlayers() {
|
|
345
340
|
try {
|
|
346
341
|
await this.aqua.loadPlayers();
|
|
347
|
-
|
|
348
342
|
this.aqua.emit("debug", this.name, "Session resumed successfully");
|
|
349
343
|
} catch (err) {
|
|
350
344
|
this.emitError(`Failed to resume session: ${err.message}`);
|
|
351
345
|
}
|
|
352
346
|
}
|
|
353
|
-
|
|
354
347
|
emitError(error) {
|
|
355
348
|
const errorObj = error instanceof Error ? error : new Error(error);
|
|
356
349
|
console.error(`[Aqua] [${this.name}] Error:`, errorObj);
|
|
@@ -18,7 +18,8 @@ const EVENT_HANDLERS = Object.freeze({
|
|
|
18
18
|
TrackChangeEvent: "trackChange",
|
|
19
19
|
WebSocketClosedEvent: "socketClosed",
|
|
20
20
|
LyricsLineEvent: "lyricsLine",
|
|
21
|
-
LyricsFoundEvent: "lyricsFound"
|
|
21
|
+
LyricsFoundEvent: "lyricsFound" ,
|
|
22
|
+
LyricsNotFoundEvent: "lyricsNotFound"
|
|
22
23
|
});
|
|
23
24
|
|
|
24
25
|
|
|
@@ -248,37 +249,47 @@ class Player extends EventEmitter {
|
|
|
248
249
|
return this;
|
|
249
250
|
}
|
|
250
251
|
|
|
251
|
-
|
|
252
|
-
|
|
252
|
+
destroy() {
|
|
253
|
+
if (!this.connected) return this;
|
|
253
254
|
|
|
254
|
-
|
|
255
|
-
|
|
255
|
+
const voiceChannelId = this.voiceChannel ? this.voiceChannel.id || this.voiceChannel : null;
|
|
256
|
+
this._updateBatcher.destroy();
|
|
256
257
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
258
|
+
this.send({ guild_id: this.guildId, channel_id: null });
|
|
259
|
+
this._lastVoiceChannel = voiceChannelId;
|
|
260
|
+
this.voiceChannel = null;
|
|
261
|
+
this.connected = false;
|
|
262
|
+
this.send({ guild_id: this.guildId, channel_id: null });
|
|
262
263
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
264
|
+
if (this.nowPlayingMessage) {
|
|
265
|
+
this.nowPlayingMessage.delete().catch(() => { });
|
|
266
|
+
this.nowPlayingMessage = null;
|
|
267
|
+
}
|
|
267
268
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
269
|
+
this.isAutoplay = false;
|
|
270
|
+
this.aqua.destroyPlayer(this.guildId);
|
|
271
|
+
|
|
272
|
+
if (this.nodes?.connected) {
|
|
273
|
+
try {
|
|
274
|
+
this.nodes.rest.destroyPlayer(this.guildId);
|
|
275
|
+
} catch (error) {
|
|
276
|
+
if (!error.message.includes('ECONNREFUSED')) {
|
|
277
|
+
console.error('Error destroying player on node:', error);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
this.previousTracksCount = 0;
|
|
283
|
+
this._dataStore.clear();
|
|
284
|
+
this.removeAllListeners();
|
|
274
285
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
286
|
+
this.queue = null;
|
|
287
|
+
this.previousTracks = null;
|
|
288
|
+
this.connection = null;
|
|
289
|
+
this.filters = null;
|
|
279
290
|
|
|
280
|
-
|
|
281
|
-
|
|
291
|
+
return this;
|
|
292
|
+
}
|
|
282
293
|
|
|
283
294
|
pause(paused) {
|
|
284
295
|
if (this.paused === paused) return this;
|
|
@@ -324,12 +335,16 @@ class Player extends EventEmitter {
|
|
|
324
335
|
return this.nodes.rest.unsubscribeLiveLyrics(this.guildId);
|
|
325
336
|
}
|
|
326
337
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
338
|
+
seek(position) {
|
|
339
|
+
if (!this.playing) return this;
|
|
340
|
+
if (position < 0) position = 0;
|
|
341
|
+
if (this.current?.info?.length && position > this.current.info.length) {
|
|
342
|
+
position = this.current.info.length;
|
|
332
343
|
}
|
|
344
|
+
this.position = position;
|
|
345
|
+
this.batchUpdatePlayer({ position: this.position });
|
|
346
|
+
return this;
|
|
347
|
+
}
|
|
333
348
|
|
|
334
349
|
stop() {
|
|
335
350
|
if (!this.playing) return this;
|
|
@@ -520,6 +535,10 @@ class Player extends EventEmitter {
|
|
|
520
535
|
this.aqua.emit("lyricsFound", player, track, payload);
|
|
521
536
|
}
|
|
522
537
|
|
|
538
|
+
async lyricsNotFound(player, track, payload) {
|
|
539
|
+
this.aqua.emit("lyricsNotFound", player, track, payload);
|
|
540
|
+
}
|
|
541
|
+
|
|
523
542
|
send(data) {
|
|
524
543
|
this.aqua.send({ op: 4, d: data });
|
|
525
544
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aqualink",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.0",
|
|
4
4
|
"description": "An Lavalink client, focused in pure performance and features",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"url": "https://github.com/ToddyTheNoobDud/AquaLink"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"ws": "^8.18.
|
|
44
|
+
"ws": "^8.18.3",
|
|
45
45
|
"fs-extra": "^11.3.0",
|
|
46
46
|
"tseep": "^1.3.1"
|
|
47
47
|
},
|