aqualink 2.8.0 → 2.9.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.
@@ -1,831 +1,822 @@
1
- "use strict";
2
- const Node = require("./Node");
3
- const Player = require("./Player");
4
- const Track = require("./Track");
5
- const { version: pkgVersion } = require("../../package.json");
6
- const { EventEmitter } = require('tseep');
7
- const fs = require('fs/promises');
1
+ 'use strict'
8
2
 
9
- const URL_REGEX = /^https?:\/\//;
10
- const GUILD_ID_REGEX = /^\d+$/;
3
+ const { promises: fs } = require('node:fs')
4
+ const { EventEmitter } = require('tseep')
5
+
6
+ const Node = require('./Node')
7
+ const Player = require('./Player')
8
+ const Track = require('./Track')
9
+ const { version: pkgVersion } = require('../../package.json')
10
+
11
+ const URL_REGEX = /^https?:\/\//
12
+ const GUILD_ID_REGEX = /^\d+$/
11
13
 
12
14
  const DEFAULT_OPTIONS = Object.freeze({
13
- shouldDeleteMessage: false,
14
- defaultSearchPlatform: 'ytsearch',
15
- leaveOnEnd: true,
16
- restVersion: 'v4',
17
- plugins: [],
18
- autoResume: false,
19
- infiniteReconnects: false,
20
- failoverOptions: {
21
- enabled: true,
22
- maxRetries: 3,
23
- retryDelay: 1000,
24
- preservePosition: true,
25
- resumePlayback: true,
26
- cooldownTime: 5000,
27
- maxFailoverAttempts: 5
28
- }
29
- });
30
-
31
- const LEAST_USED_TTL = 15000;
32
- const CLEANUP_INTERVAL = 60000;
33
- const MAX_CONCURRENT_OPERATIONS = 2;
15
+ shouldDeleteMessage: false,
16
+ defaultSearchPlatform: 'ytsearch',
17
+ leaveOnEnd: true,
18
+ restVersion: 'v4',
19
+ plugins: [],
20
+ autoResume: false,
21
+ infiniteReconnects: false,
22
+ failoverOptions: {
23
+ enabled: true,
24
+ maxRetries: 3,
25
+ retryDelay: 1000,
26
+ preservePosition: true,
27
+ resumePlayback: true,
28
+ cooldownTime: 5000,
29
+ maxFailoverAttempts: 5
30
+ }
31
+ })
32
+
33
+ const CLEANUP_INTERVAL = 60000
34
+ const MAX_CONCURRENT_OPERATIONS = 3
35
+ const BROKEN_PLAYER_TTL = 300000
36
+ const FAILOVER_CLEANUP_TTL = 600000
34
37
 
35
38
  class Aqua extends EventEmitter {
36
- constructor(client, nodes, options = {}) {
37
- super();
38
- if (!client) throw new Error("Client is required");
39
- if (!Array.isArray(nodes) || !nodes.length) {
40
- throw new TypeError(`Nodes must be non-empty Array (got ${typeof nodes})`);
41
- }
39
+ constructor(client, nodes, options = {}) {
40
+ super()
42
41
 
43
- this.client = client;
44
- this.nodes = nodes;
45
- this.nodeMap = new Map();
46
- this.players = new Map();
47
- this.clientId = null;
48
- this.initiated = false;
49
- this.version = pkgVersion;
50
-
51
- this.options = Object.assign({}, DEFAULT_OPTIONS, options);
52
- this.failoverOptions = Object.assign({}, DEFAULT_OPTIONS.failoverOptions, options.failoverOptions);
53
-
54
- this.shouldDeleteMessage = this.options.shouldDeleteMessage;
55
- this.defaultSearchPlatform = this.options.defaultSearchPlatform;
56
- this.leaveOnEnd = this.options.leaveOnEnd;
57
- this.restVersion = this.options.restVersion;
58
- this.plugins = this.options.plugins;
59
- this.autoResume = this.options.autoResume;
60
- this.infiniteReconnects = this.options.infiniteReconnects;
61
- this.send = this.options.send || this._defaultSend;
62
-
63
- this._leastUsedCache = null;
64
- this._cacheTimestamp = 0;
65
- this._nodeStates = new Map();
66
- this._failoverQueue = new Map();
67
- this._lastFailoverAttempt = new Map();
68
- this._brokenPlayers = new Map();
69
-
70
- this._boundCleanupPlayer = this._cleanupPlayer.bind(this);
71
- this._boundHandleDestroy = this._handlePlayerDestroy.bind(this);
72
- this._boundNodeConnect = this._handleNodeConnect.bind(this);
73
- this._boundNodeDisconnect = this._handleNodeDisconnect.bind(this);
74
-
75
- this.on('nodeConnect', this._boundNodeConnect);
76
- this.on('nodeDisconnect', this._boundNodeDisconnect);
77
- this._cleanupTimer = setInterval(() => this._performCleanup(), CLEANUP_INTERVAL);
78
- }
79
-
80
- _defaultSend = (packet) => {
81
- const guildId = packet.d.guild_id;
82
- const guild = this.client?.cache?.guilds.get(guildId) ?? this.client.guilds?.cache?.get(guildId);
83
-
84
- if (guild) {
85
- if (this.client.gateway) {
86
- this.client.gateway.send(this.client.gateway.calculateShardId(guildId), packet);
87
- } else {
88
- guild.shard?.send(packet);
89
- }
90
- }
42
+ if (!client) throw new Error('Client is required')
43
+ if (!Array.isArray(nodes) || !nodes.length) {
44
+ throw new TypeError(`Nodes must be non-empty Array (got ${typeof nodes})`)
91
45
  }
92
46
 
93
- get leastUsedNodes() {
94
- const now = Date.now();
95
- if (this._leastUsedCache && (now - this._cacheTimestamp) < LEAST_USED_TTL) {
96
- return this._leastUsedCache;
97
- }
47
+ this.client = client
48
+ this.nodes = nodes
49
+ this.nodeMap = new Map()
50
+ this.players = new Map()
51
+ this.clientId = null
52
+ this.initiated = false
53
+ this.version = pkgVersion
98
54
 
99
- const connectedNodes = [];
100
- for (const node of this.nodeMap.values()) {
101
- if (node.connected) {
102
- connectedNodes.push(node);
103
- }
104
- }
55
+ this.options = { ...DEFAULT_OPTIONS, ...options }
56
+ this.failoverOptions = { ...DEFAULT_OPTIONS.failoverOptions, ...options.failoverOptions }
105
57
 
106
- connectedNodes.sort((a, b) => (a.rest?.calls || 0) - (b.rest?.calls || 0));
58
+ this.shouldDeleteMessage = this.options.shouldDeleteMessage
59
+ this.defaultSearchPlatform = this.options.defaultSearchPlatform
60
+ this.leaveOnEnd = this.options.leaveOnEnd
61
+ this.restVersion = this.options.restVersion
62
+ this.plugins = this.options.plugins
63
+ this.autoResume = this.options.autoResume
64
+ this.infiniteReconnects = this.options.infiniteReconnects
65
+ this.send = this.options.send || this._defaultSend
107
66
 
108
- this._leastUsedCache = connectedNodes;
109
- this._cacheTimestamp = now;
110
- return connectedNodes;
111
- }
112
-
113
- async init(clientId) {
114
- if (this.initiated) return this;
67
+ this._nodeStates = new Map()
68
+ this._failoverQueue = new Map()
69
+ this._lastFailoverAttempt = new Map()
70
+ this._brokenPlayers = new Map()
115
71
 
116
- this.clientId = clientId;
117
- let successCount = 0;
72
+ this._bindEventHandlers()
73
+ this._startCleanupTimer()
74
+ }
118
75
 
119
- const batchSize = 2;
120
- for (let i = 0; i < this.nodes.length; i += batchSize) {
121
- const batch = this.nodes.slice(i, i + batchSize);
122
- const results = await Promise.allSettled(
123
- batch.map(node => this._createNode(node))
124
- );
125
-
126
- successCount += results.filter(r => r.status === 'fulfilled').length;
127
- }
76
+ _defaultSend = (packet) => {
77
+ const guildId = packet.d.guild_id
78
+ const guild = this.client?.cache?.guilds.get(guildId) ?? this.client.guilds?.cache?.get(guildId)
128
79
 
129
- if (successCount === 0) {
130
- throw new Error("No nodes connected");
131
- }
132
-
133
- if (this.plugins.length > 0) {
134
- this.plugins.forEach(plugin => {
135
- Promise.resolve(plugin.load(this)).catch(err =>
136
- this.emit('error', null, new Error(`Plugin error: ${err.message}`))
137
- );
138
- });
139
- }
80
+ if (!guild) return;
140
81
 
141
- this.initiated = true;
142
- return this;
82
+ if (this.client.gateway) {
83
+ this.client.gateway.send(this.client.gateway.calculateShardId(guildId), packet)
84
+ } else {
85
+ guild.shard?.send(packet)
143
86
  }
87
+ }
144
88
 
145
- async _createNode(options) {
146
- const nodeId = options.name || options.host;
147
- this._destroyNode(nodeId);
89
+ _bindEventHandlers() {
90
+ this._boundCleanupPlayer = this._cleanupPlayer.bind(this)
91
+ this._boundHandleDestroy = this._handlePlayerDestroy.bind(this)
92
+ this._boundNodeConnect = this._handleNodeConnect.bind(this)
93
+ this._boundNodeDisconnect = this._handleNodeDisconnect.bind(this)
148
94
 
149
- const node = new Node(this, options, this.options);
150
- node.players = new Set();
151
- this.nodeMap.set(nodeId, node);
152
- this._invalidateCache();
95
+ this.on('nodeConnect', this._boundNodeConnect)
96
+ this.on('nodeDisconnect', this._boundNodeDisconnect)
97
+ }
153
98
 
154
- this._nodeStates.set(nodeId, { connected: false, failoverInProgress: false });
99
+ _startCleanupTimer() {
100
+ this._cleanupTimer = setInterval(() => this._performCleanup(), CLEANUP_INTERVAL)
101
+ }
155
102
 
156
- try {
157
- await node.connect();
158
- this._nodeStates.set(nodeId, { connected: true, failoverInProgress: false });
159
- this.emit("nodeCreate", node);
160
- return node;
161
- } catch (error) {
162
- this._cleanupNode(nodeId);
163
- throw error;
164
- }
165
- }
166
-
167
- _handleNodeConnect(node) {
168
- if (!this.autoResume) return;
169
-
170
- const nodeId = node.name || node.host;
171
- this._nodeStates.set(nodeId, { connected: true, failoverInProgress: false });
103
+ get leastUsedNodes() {
104
+ const connectedNodes = []
172
105
 
173
- process.nextTick(() => this._rebuildBrokenPlayers(node));
106
+ for (const node of this.nodeMap.values()) {
107
+ if (node.connected) {
108
+ connectedNodes.push(node)
109
+ }
174
110
  }
175
111
 
176
- _handleNodeDisconnect(node) {
177
- if (!this.autoResume) return;
112
+ return connectedNodes.sort((a, b) => (a.rest?.calls || 0) - (b.rest?.calls || 0))
113
+ }
178
114
 
179
- const nodeId = node.name || node.host;
180
- this._nodeStates.set(nodeId, { connected: false, failoverInProgress: false });
115
+ async init(clientId) {
116
+ if (this.initiated) return this
181
117
 
182
- process.nextTick(() => this._storeBrokenPlayers(node));
183
- }
184
-
185
- _storeBrokenPlayers(node) {
186
- const nodeId = node.name || node.host;
187
- const now = Date.now();
118
+ this.clientId = clientId
119
+ let successCount = 0
188
120
 
189
- for (const player of this.players.values()) {
190
- if (player.nodes === node) {
191
- try {
192
- const state = this._capturePlayerState(player);
193
- if (state) {
194
- state.originalNodeId = nodeId;
195
- state.brokenAt = now;
196
- this._brokenPlayers.set(player.guildId, state);
197
- }
198
- } catch (error) {
199
- }
200
- }
201
- }
121
+ const batchSize = 2
122
+ for (let i = 0; i < this.nodes.length; i += batchSize) {
123
+ const batch = this.nodes.slice(i, i + batchSize)
124
+ const results = await Promise.allSettled(batch.map(node => this._createNode(node)))
125
+ successCount += results.filter(r => r.status === 'fulfilled').length
202
126
  }
203
127
 
204
- _rebuildBrokenPlayers(node) {
205
- const nodeId = node.name || node.host;
206
- let rebuiltCount = 0;
207
-
208
- for (const [guildId, brokenState] of this._brokenPlayers.entries()) {
209
- if (brokenState.originalNodeId === nodeId) {
210
- this._rebuildPlayer(brokenState, node)
211
- .then(() => {
212
- this._brokenPlayers.delete(guildId);
213
- rebuiltCount++;
214
- })
215
- .catch(() => {
216
- if (Date.now() - brokenState.brokenAt > 300000) {
217
- this._brokenPlayers.delete(guildId);
218
- }
219
- });
220
- }
221
- }
128
+ if (successCount === 0) {
129
+ throw new Error('No nodes connected')
130
+ }
222
131
 
223
- if (rebuiltCount > 0) {
224
- this.emit("playersRebuilt", node, rebuiltCount);
225
- }
132
+ if (this.plugins.length > 0) {
133
+ this.plugins.forEach(plugin => {
134
+ Promise.resolve(plugin.load(this)).catch(err =>
135
+ this.emit('error', null, new Error(`Plugin error: ${err.message}`))
136
+ )
137
+ })
226
138
  }
227
139
 
228
- async _rebuildPlayer(brokenState, targetNode) {
229
- const { guildId, textChannel, voiceChannel, current, volume = 65, deaf = true } = brokenState;
140
+ this.initiated = true
141
+ return this
142
+ }
230
143
 
231
- const existingPlayer = this.players.get(guildId);
232
- if (existingPlayer) {
233
- await existingPlayer.destroy();
234
- }
144
+ async _createNode(options) {
145
+ const nodeId = options.name || options.host
146
+ this._destroyNode(nodeId)
235
147
 
236
- setTimeout(async () => {
237
- try {
238
- const player = await this.createConnection({
239
- guildId, textChannel, voiceChannel,
240
- defaultVolume: volume, deaf
241
- });
242
-
243
- if (current) {
244
- await player.queue.add(current);
245
- await player.play();
246
- this.emit("trackStart", player, current);
247
- }
248
- } catch (error) {
249
- this._brokenPlayers.delete(guildId);
250
- }
251
- }, 1000);
252
- }
148
+ const node = new Node(this, options, this.options)
149
+ node.players = new Set()
150
+ this.nodeMap.set(nodeId, node)
253
151
 
254
- _destroyNode(identifier) {
255
- const node = this.nodeMap.get(identifier);
256
- if (node) {
257
- this._cleanupNode(identifier);
258
- this.emit("nodeDestroy", node);
259
- }
260
- }
152
+ this._nodeStates.set(nodeId, { connected: false, failoverInProgress: false })
261
153
 
262
- _cleanupNode(nodeId) {
263
- const node = this.nodeMap.get(nodeId);
264
- if (node) {
265
- node.removeAllListeners();
266
- }
267
-
268
- this.nodeMap.delete(nodeId);
269
- this._nodeStates.delete(nodeId);
270
- this._failoverQueue.delete(nodeId);
271
- this._lastFailoverAttempt.delete(nodeId);
272
- this._invalidateCache();
154
+ try {
155
+ await node.connect()
156
+ this._nodeStates.set(nodeId, { connected: true, failoverInProgress: false })
157
+ this.emit('nodeCreate', node)
158
+ return node
159
+ } catch (error) {
160
+ this._cleanupNode(nodeId)
161
+ throw error
273
162
  }
163
+ }
274
164
 
275
- _invalidateCache() {
276
- this._leastUsedCache = null;
277
- this._cacheTimestamp = 0;
278
- }
165
+ _handleNodeConnect(node) {
166
+ if (!this.autoResume) return;
279
167
 
280
- async handleNodeFailover(failedNode) {
281
- if (!this.failoverOptions.enabled) return;
168
+ const nodeId = node.name || node.host
169
+ this._nodeStates.set(nodeId, { connected: true, failoverInProgress: false })
282
170
 
283
- const nodeId = failedNode.name || failedNode.host;
284
- const now = Date.now();
171
+ process.nextTick(() => this._rebuildBrokenPlayers(node))
172
+ }
285
173
 
286
- const nodeState = this._nodeStates.get(nodeId);
287
- if (nodeState?.failoverInProgress) return;
174
+ _handleNodeDisconnect(node) {
175
+ if (!this.autoResume) return;
288
176
 
289
- const lastAttempt = this._lastFailoverAttempt.get(nodeId);
290
- if (lastAttempt && (now - lastAttempt) < this.failoverOptions.cooldownTime) return;
177
+ const nodeId = node.name || node.host
178
+ this._nodeStates.set(nodeId, { connected: false, failoverInProgress: false })
291
179
 
292
- const attempts = this._failoverQueue.get(nodeId) || 0;
293
- if (attempts >= this.failoverOptions.maxFailoverAttempts) return;
180
+ process.nextTick(() => this._storeBrokenPlayers(node))
181
+ }
294
182
 
295
- this._nodeStates.set(nodeId, { connected: false, failoverInProgress: true });
296
- this._lastFailoverAttempt.set(nodeId, now);
297
- this._failoverQueue.set(nodeId, attempts + 1);
183
+ _storeBrokenPlayers(node) {
184
+ const nodeId = node.name || node.host
185
+ const now = Date.now()
298
186
 
299
- try {
300
- this.emit("nodeFailover", failedNode);
187
+ for (const player of this.players.values()) {
188
+ if (player.nodes === node) {
189
+ const state = this._capturePlayerState(player)
190
+ if (state) {
191
+ state.originalNodeId = nodeId
192
+ state.brokenAt = now
193
+ this._brokenPlayers.set(player.guildId, state)
194
+ }
195
+ }
196
+ }
197
+ }
301
198
 
302
- const affectedPlayers = Array.from(failedNode.players);
303
- if (affectedPlayers.length === 0) {
304
- this._nodeStates.set(nodeId, { connected: false, failoverInProgress: false });
305
- return;
306
- }
199
+ _rebuildBrokenPlayers(node) {
200
+ const nodeId = node.name || node.host
201
+ let rebuiltCount = 0
307
202
 
308
- const availableNodes = this._getAvailableNodes(failedNode);
309
- if (availableNodes.length === 0) {
310
- this.emit("error", null, new Error("No failover nodes available"));
311
- return;
203
+ for (const [guildId, brokenState] of this._brokenPlayers.entries()) {
204
+ if (brokenState.originalNodeId === nodeId) {
205
+ this._rebuildPlayer(brokenState, node)
206
+ .then(() => {
207
+ this._brokenPlayers.delete(guildId)
208
+ rebuiltCount++
209
+ })
210
+ .catch(() => {
211
+ if (Date.now() - brokenState.brokenAt > BROKEN_PLAYER_TTL) {
212
+ this._brokenPlayers.delete(guildId)
312
213
  }
214
+ })
215
+ }
216
+ }
313
217
 
314
- const results = await this._migratePlayersOptimized(affectedPlayers, availableNodes);
315
- const successful = results.filter(r => r.success).length;
218
+ if (rebuiltCount > 0) {
219
+ this.emit('playersRebuilt', node, rebuiltCount)
220
+ }
221
+ }
316
222
 
317
- if (successful > 0) {
318
- this.emit("nodeFailoverComplete", failedNode, successful, results.length - successful);
319
- }
223
+ async _rebuildPlayer(brokenState, targetNode) {
224
+ const { guildId, textChannel, voiceChannel, current, volume = 65, deaf = true } = brokenState
320
225
 
321
- } catch (error) {
322
- this.emit("error", null, new Error(`Failover failed: ${error.message}`));
323
- } finally {
324
- this._nodeStates.set(nodeId, { connected: false, failoverInProgress: false });
325
- }
226
+ const existingPlayer = this.players.get(guildId)
227
+ if (existingPlayer) {
228
+ await existingPlayer.destroy()
326
229
  }
327
230
 
328
- async _migratePlayersOptimized(players, availableNodes) {
329
- const results = [];
330
-
331
- // Process in smaller batches
332
- for (let i = 0; i < players.length; i += MAX_CONCURRENT_OPERATIONS) {
333
- const batch = players.slice(i, i + MAX_CONCURRENT_OPERATIONS);
334
- const batchResults = await Promise.allSettled(
335
- batch.map(player => this._migratePlayer(player, availableNodes))
336
- );
231
+ setTimeout(async () => {
232
+ try {
233
+ const player = await this.createConnection({
234
+ guildId,
235
+ textChannel,
236
+ voiceChannel,
237
+ defaultVolume: volume,
238
+ deaf
239
+ })
337
240
 
338
- results.push(...batchResults.map(r => ({
339
- success: r.status === 'fulfilled',
340
- error: r.reason
341
- })));
241
+ if (current) {
242
+ await player.queue.add(current)
243
+ await player.play()
244
+ this.emit('trackStart', player, current)
342
245
  }
246
+ } catch (error) {
247
+ this._brokenPlayers.delete(guildId)
248
+ }
249
+ }, 1000)
250
+ }
343
251
 
344
- return results;
252
+ _destroyNode(identifier) {
253
+ const node = this.nodeMap.get(identifier)
254
+ if (node) {
255
+ this._cleanupNode(identifier)
256
+ this.emit('nodeDestroy', node)
345
257
  }
258
+ }
346
259
 
347
- async _migratePlayer(player, availableNodes) {
348
- let retryCount = 0;
349
-
350
- while (retryCount < this.failoverOptions.maxRetries) {
351
- try {
352
- const targetNode = availableNodes[0]; // Simple selection
353
- const playerState = this._capturePlayerState(player);
354
-
355
- if (!playerState) throw new Error("Failed to capture state");
356
-
357
- const newPlayer = await this._createPlayerOnNode(targetNode, playerState);
358
- await this._restorePlayerState(newPlayer, playerState);
359
-
360
- this.emit("playerMigrated", player, newPlayer, targetNode);
361
- return newPlayer;
362
-
363
- } catch (error) {
364
- retryCount++;
365
- if (retryCount >= this.failoverOptions.maxRetries) throw error;
366
- await this._delay(this.failoverOptions.retryDelay);
367
- }
368
- }
260
+ _cleanupNode(nodeId) {
261
+ const node = this.nodeMap.get(nodeId)
262
+ if (node) {
263
+ node.removeAllListeners()
369
264
  }
370
265
 
371
- _capturePlayerState(player) {
372
- try {
373
- return {
374
- guildId: player.guildId,
375
- textChannel: player.textChannel,
376
- voiceChannel: player.voiceChannel,
377
- volume: player.volume || 100,
378
- paused: player.paused || false,
379
- position: player.position || 0,
380
- current: player.current || null,
381
- queue: player.queue?.tracks?.slice(0, 10) || [], // Limit queue size
382
- repeat: player.loop,
383
- shuffle: player.shuffle,
384
- deaf: player.deaf || false,
385
- connected: player.connected || false,
386
- };
387
- } catch {
388
- return null;
389
- }
390
- }
266
+ this.nodeMap.delete(nodeId)
267
+ this._nodeStates.delete(nodeId)
268
+ this._failoverQueue.delete(nodeId)
269
+ this._lastFailoverAttempt.delete(nodeId)
270
+ }
271
+
272
+ async handleNodeFailover(failedNode) {
273
+ if (!this.failoverOptions.enabled) return;
274
+
275
+ const nodeId = failedNode.name || failedNode.host
276
+ const now = Date.now()
277
+
278
+ const nodeState = this._nodeStates.get(nodeId)
279
+ if (nodeState?.failoverInProgress) return;
280
+
281
+ const lastAttempt = this._lastFailoverAttempt.get(nodeId)
282
+ if (lastAttempt && (now - lastAttempt) < this.failoverOptions.cooldownTime) return;
283
+
284
+ const attempts = this._failoverQueue.get(nodeId) || 0
285
+ if (attempts >= this.failoverOptions.maxFailoverAttempts) return;
286
+
287
+ this._nodeStates.set(nodeId, { connected: false, failoverInProgress: true })
288
+ this._lastFailoverAttempt.set(nodeId, now)
289
+ this._failoverQueue.set(nodeId, attempts + 1)
290
+
291
+ try {
292
+ this.emit('nodeFailover', failedNode)
293
+
294
+ const affectedPlayers = Array.from(failedNode.players)
295
+ if (affectedPlayers.length === 0) {
296
+ this._nodeStates.set(nodeId, { connected: false, failoverInProgress: false })
297
+ return;
298
+ }
299
+
300
+ const availableNodes = this._getAvailableNodes(failedNode)
301
+ if (availableNodes.length === 0) {
302
+ this.emit('error', null, new Error('No failover nodes available'))
303
+ return;
304
+ }
305
+
306
+ const results = await this._migratePlayersOptimized(affectedPlayers, availableNodes)
307
+ const successful = results.filter(r => r.success).length
391
308
 
392
- async _createPlayerOnNode(targetNode, playerState) {
393
- return this.createPlayer(targetNode, {
394
- guildId: playerState.guildId,
395
- textChannel: playerState.textChannel,
396
- voiceChannel: playerState.voiceChannel,
397
- defaultVolume: playerState.volume || 100,
398
- deaf: playerState.deaf || false
399
- });
309
+ if (successful > 0) {
310
+ this.emit('nodeFailoverComplete', failedNode, successful, results.length - successful)
311
+ }
312
+ } catch (error) {
313
+ this.emit('error', null, new Error(`Failover failed: ${error.message}`))
314
+ } finally {
315
+ this._nodeStates.set(nodeId, { connected: false, failoverInProgress: false })
400
316
  }
317
+ }
401
318
 
402
- async _restorePlayerState(newPlayer, playerState) {
403
- try {
404
- if (playerState.volume !== undefined) {
405
- newPlayer.setVolume(playerState.volume);
406
- }
319
+ async _migratePlayersOptimized(players, availableNodes) {
320
+ const results = []
407
321
 
408
- if (playerState.queue?.length > 0) {
409
- newPlayer.queue.add(...playerState.queue);
410
- }
322
+ for (let i = 0; i < players.length; i += MAX_CONCURRENT_OPERATIONS) {
323
+ const batch = players.slice(i, i + MAX_CONCURRENT_OPERATIONS)
324
+ const batchResults = await Promise.allSettled(
325
+ batch.map(player => this._migratePlayer(player, availableNodes))
326
+ )
411
327
 
412
- if (playerState.current && this.failoverOptions.preservePosition) {
413
- newPlayer.queue.unshift(playerState.current);
328
+ results.push(...batchResults.map(r => ({
329
+ success: r.status === 'fulfilled',
330
+ error: r.reason
331
+ })))
332
+ }
414
333
 
415
- if (this.failoverOptions.resumePlayback) {
416
- await newPlayer.play();
334
+ return results
335
+ }
417
336
 
418
- if (playerState.position > 0) {
419
- setTimeout(() => newPlayer.seek(playerState.position), 200);
420
- }
337
+ async _migratePlayer(player, availableNodes) {
338
+ let retryCount = 0
421
339
 
422
- if (playerState.paused) {
423
- newPlayer.pause();
424
- }
425
- }
426
- }
340
+ while (retryCount < this.failoverOptions.maxRetries) {
341
+ try {
342
+ const targetNode = availableNodes[0]
343
+ const playerState = this._capturePlayerState(player)
427
344
 
428
- // Direct assignment instead of Object.assign
429
- newPlayer.repeat = playerState.repeat;
430
- newPlayer.shuffle = playerState.shuffle;
345
+ if (!playerState) throw new Error('Failed to capture state')
431
346
 
432
- } catch (error) {
433
- throw error;
434
- }
435
- }
347
+ const newPlayer = await this._createPlayerOnNode(targetNode, playerState)
348
+ await this._restorePlayerState(newPlayer, playerState)
436
349
 
437
- _delay(ms) {
438
- return new Promise(resolve => setTimeout(resolve, ms));
350
+ this.emit('playerMigrated', player, newPlayer, targetNode)
351
+ return newPlayer
352
+ } catch (error) {
353
+ retryCount++
354
+ if (retryCount >= this.failoverOptions.maxRetries) throw error
355
+ await this._delay(this.failoverOptions.retryDelay)
356
+ }
439
357
  }
358
+ }
440
359
 
441
- _cleanupPlayer(player) {
442
- if (player) {
443
- player.destroy()
444
- }
360
+ _capturePlayerState(player) {
361
+ try {
362
+ return {
363
+ guildId: player.guildId,
364
+ textChannel: player.textChannel,
365
+ voiceChannel: player.voiceChannel,
366
+ volume: player.volume || 100,
367
+ paused: player.paused || false,
368
+ position: player.position || 0,
369
+ current: player.current || null,
370
+ queue: player.queue?.tracks?.slice(0, 10) || [],
371
+ repeat: player.loop,
372
+ shuffle: player.shuffle,
373
+ deaf: player.deaf || false,
374
+ connected: player.connected || false
375
+ }
376
+ } catch {
377
+ return null
445
378
  }
379
+ }
446
380
 
447
- updateVoiceState({ d, t }) {
448
- if (!GUILD_ID_REGEX.test(d.guild_id)) return;
381
+ async _createPlayerOnNode(targetNode, playerState) {
382
+ return this.createPlayer(targetNode, {
383
+ guildId: playerState.guildId,
384
+ textChannel: playerState.textChannel,
385
+ voiceChannel: playerState.voiceChannel,
386
+ defaultVolume: playerState.volume || 100,
387
+ deaf: playerState.deaf || false
388
+ })
389
+ }
449
390
 
450
- const player = this.players.get(d.guild_id);
451
- if (!player) return;
452
-
453
- if (t === "VOICE_SERVER_UPDATE" || (t === "VOICE_STATE_UPDATE" && d.user_id === this.clientId)) {
454
- if (t === "VOICE_SERVER_UPDATE") {
455
- player.connection?.setServerUpdate?.(d);
456
- } else {
457
- player.connection?.setStateUpdate?.(d);
458
- }
391
+ async _restorePlayerState(newPlayer, playerState) {
392
+ if (playerState.volume !== undefined) {
393
+ newPlayer.setVolume(playerState.volume)
394
+ }
459
395
 
460
- if (d.channel_id === null) {
461
- this._boundCleanupPlayer(player);
462
- }
463
- }
396
+ if (playerState.queue?.length > 0) {
397
+ newPlayer.queue.add(...playerState.queue)
464
398
  }
465
399
 
466
- fetchRegion(region) {
467
- if (!region) return this.leastUsedNodes;
400
+ if (playerState.current && this.failoverOptions.preservePosition) {
401
+ newPlayer.queue.unshift(playerState.current)
468
402
 
469
- const lowerRegion = region.toLowerCase();
470
- const regionNodes = [];
403
+ if (this.failoverOptions.resumePlayback) {
404
+ await newPlayer.play()
471
405
 
472
- for (const node of this.nodeMap.values()) {
473
- if (node.connected && node.regions?.includes(lowerRegion)) {
474
- regionNodes.push(node);
475
- }
406
+ if (playerState.position > 0) {
407
+ setTimeout(() => newPlayer.seek(playerState.position), 200)
476
408
  }
477
409
 
478
- // Simple load calculation
479
- regionNodes.sort((a, b) => {
480
- const loadA = this._getNodeLoad(a);
481
- const loadB = this._getNodeLoad(b);
482
- return loadA - loadB;
483
- });
484
-
485
- return regionNodes;
486
- }
487
-
488
- _getNodeLoad(node) {
489
- const stats = node?.stats?.cpu;
490
- return stats ? (stats.systemLoad / stats.cores) * 100 : 0;
410
+ if (playerState.paused) {
411
+ newPlayer.pause()
412
+ }
413
+ }
491
414
  }
492
415
 
493
- createConnection(options) {
494
- if (!this.initiated) throw new Error("Aqua not initialized");
416
+ newPlayer.repeat = playerState.repeat
417
+ newPlayer.shuffle = playerState.shuffle
418
+ }
495
419
 
496
- const existingPlayer = this.players.get(options.guildId);
497
- if (existingPlayer?.voiceChannel) return existingPlayer;
420
+ _delay(ms) {
421
+ return new Promise(resolve => setTimeout(resolve, ms))
422
+ }
498
423
 
499
- const availableNodes = options.region ?
500
- this.fetchRegion(options.region) : this.leastUsedNodes;
501
-
502
- if (!availableNodes.length) throw new Error("No nodes available");
503
-
504
- return this.createPlayer(availableNodes[0], options);
424
+ _cleanupPlayer(player) {
425
+ if (player) {
426
+ player.destroy()
505
427
  }
428
+ }
506
429
 
507
- createPlayer(node, options) {
508
- this.destroyPlayer(options.guildId);
430
+ updateVoiceState({ d, t }) {
431
+ if (!GUILD_ID_REGEX.test(d.guild_id)) return;
509
432
 
510
- const player = new Player(this, node, options);
511
- this.players.set(options.guildId, player);
433
+ const player = this.players.get(d.guild_id)
434
+ if (!player) return;
512
435
 
513
- player.once("destroy", this._boundHandleDestroy);
514
- player.connect(options);
515
- this.emit("playerCreate", player);
516
- return player;
517
- }
436
+ if (t === 'VOICE_SERVER_UPDATE' || (t === 'VOICE_STATE_UPDATE' && d.user_id === this.clientId)) {
437
+ if (t === 'VOICE_SERVER_UPDATE') {
438
+ player.connection?.setServerUpdate?.(d)
439
+ } else {
440
+ player.connection?.setStateUpdate?.(d)
441
+ }
518
442
 
519
- _handlePlayerDestroy(player) {
520
- const node = player.nodes;
521
- if (node?.players) {
522
- node.players.delete(player);
523
- }
524
- this.players.delete(player.guildId);
525
- this.emit("playerDestroy", player);
443
+ if (d.channel_id === null) {
444
+ this._boundCleanupPlayer(player)
445
+ }
526
446
  }
447
+ }
527
448
 
528
- async destroyPlayer(guildId) {
529
- const player = this.players.get(guildId);
530
- if (!player) return;
531
-
532
- try {
533
- await player.clearData();
534
- player.removeAllListeners();
535
- this.players.delete(guildId);
536
- this.emit("playerDestroy", player);
537
- } catch {
538
- // Silent cleanup
539
- }
540
- }
541
-
542
- async resolve({ query, source = this.defaultSearchPlatform, requester, nodes }) {
543
- if (!this.initiated) throw new Error("Aqua not initialized");
544
-
545
- const requestNode = this._getRequestNode(nodes);
546
- const formattedQuery = URL_REGEX.test(query) ? query : `${source}:${query}`;
547
-
548
- try {
549
- const endpoint = `/v4/loadtracks?identifier=${encodeURIComponent(formattedQuery)}`;
550
- const response = await requestNode.rest.makeRequest("GET", endpoint);
449
+ fetchRegion(region) {
450
+ if (!region) return this.leastUsedNodes
551
451
 
552
- if (["empty", "NO_MATCHES"].includes(response.loadType)) {
553
- return this._createEmptyResponse();
554
- }
452
+ const lowerRegion = region.toLowerCase()
453
+ const regionNodes = []
555
454
 
556
- return this._constructResponse(response, requester, requestNode);
557
- } catch (error) {
558
- throw new Error(error.name === "AbortError" ? "Request timeout" : `Resolve failed: ${error.message}`);
559
- }
455
+ for (const node of this.nodeMap.values()) {
456
+ if (node.connected && node.regions?.includes(lowerRegion)) {
457
+ regionNodes.push(node)
458
+ }
560
459
  }
561
460
 
562
- _getRequestNode(nodes) {
563
- if (!nodes) return this.leastUsedNodes[0];
564
- if (nodes instanceof Node) return nodes;
565
- if (typeof nodes === "string") {
566
- return this.nodeMap.get(nodes) || this.leastUsedNodes[0];
567
- }
568
- throw new TypeError(`Invalid nodes parameter: ${typeof nodes}`);
569
- }
570
-
571
- _createEmptyResponse() {
572
- return {
573
- loadType: "empty",
574
- exception: null,
575
- playlistInfo: null,
576
- pluginInfo: {},
577
- tracks: []
578
- };
579
- }
580
-
581
- _constructResponse(response, requester, requestNode) {
582
- const baseResponse = {
583
- loadType: response.loadType,
584
- exception: null,
585
- playlistInfo: null,
586
- pluginInfo: response.pluginInfo || {},
587
- tracks: []
588
- };
589
-
590
- if (response.loadType === "error" || response.loadType === "LOAD_FAILED") {
591
- baseResponse.exception = response.data || response.exception;
592
- return baseResponse;
593
- }
461
+ return regionNodes.sort((a, b) => this._getNodeLoad(a) - this._getNodeLoad(b))
462
+ }
594
463
 
595
- switch (response.loadType) {
596
- case "track":
597
- if (response.data) {
598
- baseResponse.tracks.push(new Track(response.data, requester, requestNode));
599
- }
600
- break;
601
-
602
- case "playlist": {
603
- const info = response.data?.info;
604
- if (info) {
605
- baseResponse.playlistInfo = {
606
- name: info.name || info.title,
607
- thumbnail: response.data.pluginInfo?.artworkUrl ||
608
- response.data.tracks?.[0]?.info?.artworkUrl || null,
609
- ...info
610
- };
611
- }
612
-
613
- const tracks = response.data?.tracks;
614
- if (tracks?.length) {
615
- baseResponse.tracks = tracks.map(track => new Track(track, requester, requestNode));
616
- }
617
- break;
618
- }
464
+ _getNodeLoad(node) {
465
+ const stats = node?.stats?.cpu
466
+ return stats ? (stats.systemLoad / stats.cores) * 100 : 0
467
+ }
619
468
 
620
- case "search": {
621
- const searchData = response.data || [];
622
- if (searchData.length) {
623
- baseResponse.tracks = searchData.map(track => new Track(track, requester, requestNode));
624
- }
625
- break;
626
- }
627
- }
469
+ createConnection(options) {
470
+ if (!this.initiated) throw new Error('Aqua not initialized')
628
471
 
629
- return baseResponse;
630
- }
472
+ const existingPlayer = this.players.get(options.guildId)
473
+ if (existingPlayer?.voiceChannel) return existingPlayer
631
474
 
632
- get(guildId) {
633
- const player = this.players.get(guildId);
634
- if (!player) throw new Error(`Player not found: ${guildId}`);
635
- return player;
636
- }
475
+ const availableNodes = options.region ? this.fetchRegion(options.region) : this.leastUsedNodes
637
476
 
638
- async search(query, requester, source = this.defaultSearchPlatform) {
639
- if (!query || !requester) return null;
477
+ if (!availableNodes.length) throw new Error('No nodes available')
640
478
 
641
- try {
642
- const { tracks } = await this.resolve({ query, source, requester });
643
- return tracks || null;
644
- } catch {
645
- return null;
646
- }
647
- }
479
+ return this.createPlayer(availableNodes[0], options)
480
+ }
648
481
 
649
- async loadPlayers(filePath = "./AquaPlayers.json") {
650
- try {
651
- await fs.access(filePath);
652
- await this._waitForFirstNode();
482
+ createPlayer(node, options) {
483
+ this.destroyPlayer(options.guildId)
653
484
 
654
- const data = JSON.parse(await fs.readFile(filePath, "utf8"));
485
+ const player = new Player(this, node, options)
486
+ this.players.set(options.guildId, player)
655
487
 
656
- const batchSize = 5;
657
- for (let i = 0; i < data.length; i += batchSize) {
658
- const batch = data.slice(i, i + batchSize);
659
- await Promise.all(batch.map(p => this._restorePlayer(p)));
660
- }
488
+ player.once('destroy', this._boundHandleDestroy)
489
+ player.connect(options)
490
+ this.emit('playerCreate', player)
491
+ return player
492
+ }
661
493
 
662
- await fs.writeFile(filePath, "[]", "utf8");
663
- } catch (error) {
664
- }
494
+ _handlePlayerDestroy(player) {
495
+ const node = player.nodes
496
+ if (node?.players) {
497
+ node.players.delete(player)
665
498
  }
499
+ this.players.delete(player.guildId)
500
+ this.emit('playerDestroy', player)
501
+ }
666
502
 
667
- async savePlayer(filePath = "./AquaPlayers.json") {
668
- const data = Array.from(this.players.values()).map(player => {
669
- const requester = player.requester || player.current?.requester;
670
-
671
- return {
672
- g: player.guildId,
673
- t: player.textChannel,
674
- v: player.voiceChannel,
675
- u: player.current?.uri || null,
676
- p: player.position || 0,
677
- ts: player.timestamp || 0,
678
- q: player.queue?.tracks?.map(tr => tr.uri).slice(0, 5) || [],
679
- r: requester ? {
680
- id: requester.id,
681
- username: requester.username,
682
- globalName: requester.globalName,
683
- discriminator: requester.discriminator,
684
- avatar: requester.avatar
685
- } : null,
686
- vol: player.volume,
687
- pa: player.paused,
688
- isPlaying: !!player.current && !player.paused
689
- };
690
- });
691
-
692
- await fs.writeFile(filePath, JSON.stringify(data), "utf8");
693
- this.emit("debug", "Aqua", `Saved ${data.length} players to ${filePath}`);
694
- }
695
-
696
- async _restorePlayer(p) {
697
- try {
698
- let player = this.players.get(p.g);
699
- if (!player) {
700
- const targetNode = (p.n && this.nodeMap.get(p.n)?.connected) ?
701
- this.nodeMap.get(p.n) : this.leastUsedNodes[0];
702
-
703
- if (!targetNode) return;
704
-
705
- player = await this.createConnection({
706
- guildId: p.g,
707
- textChannel: p.t,
708
- voiceChannel: p.v,
709
- defaultVolume: p.vol || 65,
710
- deaf: true
711
- });
712
- }
713
-
714
- if (p.u && player) {
715
- const resolved = await this.resolve({ query: p.u, requester: p.r });
716
- if (resolved.tracks?.[0]) {
717
- player.queue.add(resolved.tracks[0]);
718
- player.position = p.p || 0;
719
- if (typeof p.ts === "number") player.timestamp = p.ts;
720
- }
721
- }
722
-
723
- if (p.q?.length && player) {
724
- const queuePromises = p.q
725
- .filter(uri => uri !== p.u)
726
- .map(uri => this.resolve({ query: uri, requester: p.r }));
727
-
728
- const queueResults = await Promise.allSettled(queuePromises);
729
- queueResults.forEach(result => {
730
- if (result.status === 'fulfilled' && result.value.tracks?.[0]) {
731
- player.queue.add(result.value.tracks[0]);
732
- }
733
- });
734
- }
503
+ async destroyPlayer(guildId) {
504
+ const player = this.players.get(guildId)
505
+ if (!player) return;
735
506
 
736
- if (player) {
737
- player.paused = !!p.pa;
738
- if ((p.isPlaying || (p.pa && p.u)) && player.queue.size > 0) {
739
- player.play();
740
- }
741
- }
742
- } catch (error) {
743
- }
507
+ try {
508
+ await player.clearData()
509
+ player.removeAllListeners()
510
+ this.players.delete(guildId)
511
+ this.emit('playerDestroy', player)
512
+ } catch {
513
+ // Silent cleanup
744
514
  }
515
+ }
745
516
 
746
- async _waitForFirstNode() {
747
- if (this.leastUsedNodes.length > 0) return;
517
+ async resolve({ query, source = this.defaultSearchPlatform, requester, nodes }) {
518
+ if (!this.initiated) throw new Error('Aqua not initialized')
748
519
 
749
- return new Promise(resolve => {
750
- const checkInterval = setInterval(() => {
751
- if (this.leastUsedNodes.length > 0) {
752
- clearInterval(checkInterval);
753
- resolve();
754
- }
755
- }, 100);
756
- });
757
- }
520
+ const requestNode = this._getRequestNode(nodes)
521
+ const formattedQuery = URL_REGEX.test(query) ? query : `${source}:${query}`
758
522
 
759
- _performCleanup() {
760
- const now = Date.now();
523
+ try {
524
+ const endpoint = `/v4/loadtracks?identifier=${encodeURIComponent(formattedQuery)}`
525
+ const response = await requestNode.rest.makeRequest('GET', endpoint)
761
526
 
762
- for (const [guildId, state] of this._brokenPlayers.entries()) {
763
- if (now - state.brokenAt > 300000) {
764
- this._brokenPlayers.delete(guildId);
765
- }
766
- }
527
+ if (['empty', 'NO_MATCHES'].includes(response.loadType)) {
528
+ return this._createEmptyResponse()
529
+ }
767
530
 
768
- for (const [nodeId, timestamp] of this._lastFailoverAttempt.entries()) {
769
- if (now - timestamp > 600000) {
770
- this._lastFailoverAttempt.delete(nodeId);
771
- this._failoverQueue.delete(nodeId);
772
- }
773
- }
531
+ return this._constructResponse(response, requester, requestNode)
532
+ } catch (error) {
533
+ throw new Error(error.name === 'AbortError' ? 'Request timeout' : `Resolve failed: ${error.message}`)
774
534
  }
535
+ }
775
536
 
776
- getBrokenPlayersCount() {
777
- return this._brokenPlayers.size;
537
+ _getRequestNode(nodes) {
538
+ if (!nodes) return this.leastUsedNodes[0]
539
+ if (nodes instanceof Node) return nodes
540
+ if (typeof nodes === 'string') {
541
+ return this.nodeMap.get(nodes) || this.leastUsedNodes[0]
778
542
  }
543
+ throw new TypeError(`Invalid nodes parameter: ${typeof nodes}`)
544
+ }
779
545
 
780
- getNodeStats() {
781
- const stats = {};
782
- for (const [name, node] of this.nodeMap) {
783
- const nodeStats = node.stats || {};
784
- stats[name] = {
785
- connected: node.connected,
786
- players: nodeStats.players || 0,
787
- playingPlayers: nodeStats.playingPlayers || 0,
788
- uptime: nodeStats.uptime || 0,
789
- cpu: nodeStats.cpu || {},
790
- memory: nodeStats.memory || {},
791
- ping: nodeStats.ping || 0
792
- };
793
- }
794
- return stats;
546
+ _createEmptyResponse() {
547
+ return {
548
+ loadType: 'empty',
549
+ exception: null,
550
+ playlistInfo: null,
551
+ pluginInfo: {},
552
+ tracks: []
795
553
  }
554
+ }
796
555
 
797
- destroy() {
798
- if (this._cleanupTimer) {
799
- clearInterval(this._cleanupTimer);
800
- }
801
-
802
- this.removeAllListeners();
556
+ _constructResponse(response, requester, requestNode) {
557
+ const baseResponse = {
558
+ loadType: response.loadType,
559
+ exception: null,
560
+ playlistInfo: null,
561
+ pluginInfo: response.pluginInfo || {},
562
+ tracks: []
563
+ }
564
+
565
+ if (response.loadType === 'error' || response.loadType === 'LOAD_FAILED') {
566
+ baseResponse.exception = response.data || response.exception
567
+ return baseResponse
568
+ }
569
+
570
+ switch (response.loadType) {
571
+ case 'track':
572
+ if (response.data) {
573
+ baseResponse.tracks.push(new Track(response.data, requester, requestNode))
574
+ }
575
+ break
576
+
577
+ case 'playlist': {
578
+ const info = response.data?.info
579
+ if (info) {
580
+ baseResponse.playlistInfo = {
581
+ name: info.name || info.title,
582
+ thumbnail: response.data.pluginInfo?.artworkUrl ||
583
+ response.data.tracks?.[0]?.info?.artworkUrl || null,
584
+ ...info
585
+ }
586
+ }
587
+
588
+ const tracks = response.data?.tracks
589
+ if (tracks?.length) {
590
+ baseResponse.tracks = tracks.map(track => new Track(track, requester, requestNode))
591
+ }
592
+ break
593
+ }
594
+
595
+ case 'search': {
596
+ const searchData = response.data || []
597
+ if (searchData.length) {
598
+ baseResponse.tracks = searchData.map(track => new Track(track, requester, requestNode))
599
+ }
600
+ break
601
+ }
602
+ }
603
+
604
+ return baseResponse
605
+ }
606
+
607
+ get(guildId) {
608
+ const player = this.players.get(guildId)
609
+ if (!player) throw new Error(`Player not found: ${guildId}`)
610
+ return player
611
+ }
612
+
613
+ async search(query, requester, source = this.defaultSearchPlatform) {
614
+ if (!query || !requester) return null
615
+
616
+ try {
617
+ const { tracks } = await this.resolve({ query, source, requester })
618
+ return tracks || null
619
+ } catch {
620
+ return null
621
+ }
622
+ }
623
+
624
+ async loadPlayers(filePath = './AquaPlayers.json') {
625
+ try {
626
+ await fs.access(filePath)
627
+ await this._waitForFirstNode()
628
+
629
+ const data = JSON.parse(await fs.readFile(filePath, 'utf8'))
630
+
631
+ const batchSize = 5
632
+ for (let i = 0; i < data.length; i += batchSize) {
633
+ const batch = data.slice(i, i + batchSize)
634
+ await Promise.all(batch.map(p => this._restorePlayer(p)))
635
+ }
636
+
637
+ await fs.writeFile(filePath, '[]', 'utf8')
638
+ } catch (error) {
639
+ // Silent error handling
640
+ }
641
+ }
642
+
643
+ async savePlayer(filePath = './AquaPlayers.json') {
644
+ const data = Array.from(this.players.values()).map(player => {
645
+ const requester = player.requester || player.current?.requester
646
+
647
+ return {
648
+ g: player.guildId,
649
+ t: player.textChannel,
650
+ v: player.voiceChannel,
651
+ u: player.current?.uri || null,
652
+ p: player.position || 0,
653
+ ts: player.timestamp || 0,
654
+ q: player.queue?.tracks?.map(tr => tr.uri).slice(0, 5) || [],
655
+ r: requester ? {
656
+ id: requester.id,
657
+ username: requester.username,
658
+ globalName: requester.globalName,
659
+ discriminator: requester.discriminator,
660
+ avatar: requester.avatar
661
+ } : null,
662
+ vol: player.volume,
663
+ pa: player.paused,
664
+ isPlaying: player.playing,
665
+ sessionId: player.connection.sessionId,
666
+ endpoint: player.connection.endpoint,
667
+ token: player.connection.token
668
+ }
669
+ })
670
+
671
+ await fs.writeFile(filePath, JSON.stringify(data), 'utf8')
672
+ this.emit('debug', 'Aqua', `Saved ${data.length} players to ${filePath}`)
673
+ }
674
+
675
+ async _restorePlayer(p) {
676
+ try {
677
+ let player = this.players.get(p.g)
678
+ if (!player) {
679
+ const targetNode = (p.n && this.nodeMap.get(p.n)?.connected) ?
680
+ this.nodeMap.get(p.n) : this.leastUsedNodes[0]
681
+
682
+ if (!targetNode) return;
683
+
684
+ player = await this.createConnection({
685
+ guildId: p.g,
686
+ textChannel: p.t,
687
+ voiceChannel: p.v,
688
+ defaultVolume: p.vol || 65,
689
+ deaf: true
690
+ })
691
+ }
692
+
693
+ if (player?.connection) {
694
+ if (p.sessionId) player.connection.sessionId = p.sessionId
695
+ if (p.endpoint) player.connection.endpoint = p.endpoint
696
+ if (p.token) player.connection.token = p.token
697
+ }
698
+
699
+ if (p.u && player) {
700
+ const resolved = await this.resolve({ query: p.u, requester: p.r })
701
+ if (resolved.tracks?.[0]) {
702
+ player.queue.add(resolved.tracks[0])
703
+ player.position = p.p || 0
704
+ if (typeof p.ts === 'number') player.timestamp = p.ts
705
+ }
706
+ }
707
+
708
+ if (p.q?.length && player) {
709
+ const queuePromises = p.q
710
+ .filter(uri => uri !== p.u)
711
+ .map(uri => this.resolve({ query: uri, requester: p.r }))
712
+
713
+ const queueResults = await Promise.allSettled(queuePromises)
714
+ queueResults.forEach(result => {
715
+ if (result.status === 'fulfilled' && result.value.tracks?.[0]) {
716
+ player.queue.add(result.value.tracks[0])
717
+ }
718
+ })
719
+ }
720
+
721
+ if (player) {
722
+ if (typeof p.vol === 'number') {
723
+ player.volume = p.vol
724
+ }
725
+
726
+ player.paused = !!p.pa
727
+
728
+ if ((p.isPlaying || (p.pa && p.u)) && player.queue.size > 0) {
729
+ player.play()
730
+ }
731
+ }
732
+ } catch (error) {
733
+ this.emit('debug', 'Aqua', `Error restoring player for guild ${p.g}: ${error.message}`)
734
+ }
735
+ }
736
+
737
+ async _waitForFirstNode() {
738
+ if (this.leastUsedNodes.length > 0) return;
739
+
740
+ return new Promise(resolve => {
741
+ const checkInterval = setInterval(() => {
742
+ if (this.leastUsedNodes.length > 0) {
743
+ clearInterval(checkInterval)
744
+ resolve()
745
+ }
746
+ }, 100)
747
+ })
748
+ }
749
+
750
+ _performCleanup() {
751
+ const now = Date.now()
752
+
753
+ for (const [guildId, state] of this._brokenPlayers.entries()) {
754
+ if (now - state.brokenAt > BROKEN_PLAYER_TTL) {
755
+ this._brokenPlayers.delete(guildId)
756
+ }
757
+ }
758
+
759
+ for (const [nodeId, timestamp] of this._lastFailoverAttempt.entries()) {
760
+ if (now - timestamp > FAILOVER_CLEANUP_TTL) {
761
+ this._lastFailoverAttempt.delete(nodeId)
762
+ this._failoverQueue.delete(nodeId)
763
+ }
764
+ }
765
+ }
766
+
767
+ getBrokenPlayersCount() {
768
+ return this._brokenPlayers.size
769
+ }
770
+
771
+ getNodeStats() {
772
+ const stats = {}
773
+ for (const [name, node] of this.nodeMap) {
774
+ const nodeStats = node.stats || {}
775
+ stats[name] = {
776
+ connected: node.connected,
777
+ players: nodeStats.players || 0,
778
+ playingPlayers: nodeStats.playingPlayers || 0,
779
+ uptime: nodeStats.uptime || 0,
780
+ cpu: nodeStats.cpu || {},
781
+ memory: nodeStats.memory || {},
782
+ ping: nodeStats.ping || 0
783
+ }
784
+ }
785
+ return stats
786
+ }
787
+
788
+ destroy() {
789
+ if (this._cleanupTimer) {
790
+ clearInterval(this._cleanupTimer)
791
+ }
803
792
 
804
- for (const player of this.players.values()) {
805
- this._cleanupPlayer(player);
806
- }
793
+ this.removeAllListeners()
807
794
 
808
- for (const node of this.nodeMap.values()) {
809
- node.removeAllListeners();
810
- }
795
+ for (const player of this.players.values()) {
796
+ this._cleanupPlayer(player)
797
+ }
811
798
 
812
- this.players.clear();
813
- this.nodeMap.clear();
814
- this._brokenPlayers.clear();
815
- this._nodeStates.clear();
816
- this._failoverQueue.clear();
817
- this._lastFailoverAttempt.clear();
799
+ for (const node of this.nodeMap.values()) {
800
+ node.removeAllListeners()
818
801
  }
819
802
 
820
- _getAvailableNodes(excludeNode) {
821
- const available = [];
822
- for (const node of this.nodeMap.values()) {
823
- if (node !== excludeNode && node.connected) {
824
- available.push(node);
825
- }
826
- }
827
- return available;
803
+ this.players.clear()
804
+ this.nodeMap.clear()
805
+ this._brokenPlayers.clear()
806
+ this._nodeStates.clear()
807
+ this._failoverQueue.clear()
808
+ this._lastFailoverAttempt.clear()
809
+ }
810
+
811
+ _getAvailableNodes(excludeNode) {
812
+ const available = []
813
+ for (const node of this.nodeMap.values()) {
814
+ if (node !== excludeNode && node.connected) {
815
+ available.push(node)
816
+ }
828
817
  }
818
+ return available
819
+ }
829
820
  }
830
821
 
831
- module.exports = Aqua;
822
+ module.exports = Aqua