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