aqualink 2.16.1 → 2.17.1
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 +133 -146
- package/build/structures/AqualinkEvents.js +3 -3
- package/build/structures/Connection.js +192 -220
- package/build/structures/Node.js +89 -103
- package/build/structures/Player.js +200 -270
- package/build/structures/Queue.js +6 -16
- package/build/structures/Rest.js +229 -342
- package/build/structures/Track.js +39 -58
- package/package.json +4 -4
package/build/structures/Aqua.js
CHANGED
|
@@ -9,7 +9,6 @@ const Player = require('./Player')
|
|
|
9
9
|
const Track = require('./Track')
|
|
10
10
|
const {version: pkgVersion} = require('../../package.json')
|
|
11
11
|
|
|
12
|
-
// Constants
|
|
13
12
|
const SEARCH_PREFIX = ':'
|
|
14
13
|
const EMPTY_ARRAY = Object.freeze([])
|
|
15
14
|
const EMPTY_TRACKS_RESPONSE = Object.freeze({
|
|
@@ -57,17 +56,26 @@ const DEFAULT_OPTIONS = Object.freeze({
|
|
|
57
56
|
})
|
|
58
57
|
})
|
|
59
58
|
|
|
60
|
-
//
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
59
|
+
// Shared helper functions
|
|
60
|
+
const _functions = {
|
|
61
|
+
delay: ms => new Promise(r => setTimeout(r, ms)),
|
|
62
|
+
noop: () => {},
|
|
63
|
+
isUrl: query => typeof query === 'string' && query.length > 8 && URL_PATTERN.test(query),
|
|
64
|
+
formatQuery(query, source) {
|
|
65
|
+
return this.isUrl(query) ? query : `${source}${SEARCH_PREFIX}${query}`
|
|
66
|
+
},
|
|
67
|
+
makeTrack: (t, requester, node) => new Track(t, requester, node),
|
|
68
|
+
safeCall(fn) {
|
|
69
|
+
try {
|
|
70
|
+
const result = fn()
|
|
71
|
+
return result?.then ? result.catch(this.noop) : result
|
|
72
|
+
} catch {}
|
|
73
|
+
},
|
|
74
|
+
parseRequester(str) {
|
|
75
|
+
if (!str || typeof str !== 'string') return null
|
|
76
|
+
const i = str.indexOf(':')
|
|
77
|
+
return i > 0 ? {id: str.substring(0, i), username: str.substring(i + 1)} : null
|
|
78
|
+
}
|
|
71
79
|
}
|
|
72
80
|
|
|
73
81
|
class Aqua extends EventEmitter {
|
|
@@ -128,7 +136,6 @@ class Aqua extends EventEmitter {
|
|
|
128
136
|
}
|
|
129
137
|
|
|
130
138
|
_bindEventHandlers() {
|
|
131
|
-
// Store handlers for cleanup
|
|
132
139
|
this._eventHandlers = {
|
|
133
140
|
onNodeConnect: async node => {
|
|
134
141
|
this._invalidateCache()
|
|
@@ -156,7 +163,6 @@ class Aqua extends EventEmitter {
|
|
|
156
163
|
this.on(AqualinkEvents.NodeReady, this._eventHandlers.onNodeReady)
|
|
157
164
|
}
|
|
158
165
|
|
|
159
|
-
// Cleanup event handlers to prevent memory leaks
|
|
160
166
|
destroy() {
|
|
161
167
|
if (this._eventHandlers) {
|
|
162
168
|
this.off(AqualinkEvents.NodeConnect, this._eventHandlers.onNodeConnect)
|
|
@@ -166,8 +172,15 @@ class Aqua extends EventEmitter {
|
|
|
166
172
|
}
|
|
167
173
|
this.removeAllListeners()
|
|
168
174
|
this.nodeMap.forEach(node => this._destroyNode(node.name || node.host))
|
|
169
|
-
this.players.forEach(player =>
|
|
175
|
+
this.players.forEach(player => _functions.safeCall(() => player.destroy()))
|
|
170
176
|
this.players.clear()
|
|
177
|
+
this._nodeStates.clear()
|
|
178
|
+
this._failoverQueue.clear()
|
|
179
|
+
this._lastFailoverAttempt.clear()
|
|
180
|
+
this._brokenPlayers.clear()
|
|
181
|
+
this._rebuildLocks.clear()
|
|
182
|
+
this._nodeLoadCache.clear()
|
|
183
|
+
this._invalidateCache()
|
|
171
184
|
}
|
|
172
185
|
|
|
173
186
|
get leastUsedNodes() {
|
|
@@ -175,7 +188,10 @@ class Aqua extends EventEmitter {
|
|
|
175
188
|
if (this._leastUsedNodesCache && (now - this._leastUsedNodesCacheTime) < CACHE_VALID_TIME) {
|
|
176
189
|
return this._leastUsedNodesCache
|
|
177
190
|
}
|
|
178
|
-
const connected =
|
|
191
|
+
const connected = []
|
|
192
|
+
for (const n of this.nodeMap.values()) {
|
|
193
|
+
if (n.connected) connected.push(n)
|
|
194
|
+
}
|
|
179
195
|
const sorted = this.loadBalancer === 'leastRest'
|
|
180
196
|
? connected.sort((a, b) => (a.rest?.calls || 0) - (b.rest?.calls || 0))
|
|
181
197
|
: this.loadBalancer === 'random'
|
|
@@ -193,14 +209,16 @@ class Aqua extends EventEmitter {
|
|
|
193
209
|
|
|
194
210
|
_getNodeLoad(node) {
|
|
195
211
|
const id = node.name || node.host
|
|
196
|
-
const cached = this._nodeLoadCache.get(id)
|
|
197
212
|
const now = Date.now()
|
|
213
|
+
const cached = this._nodeLoadCache.get(id)
|
|
198
214
|
if (cached && (now - cached.time) < 5000) return cached.load
|
|
199
215
|
const stats = node?.stats
|
|
200
216
|
if (!stats) return 0
|
|
201
|
-
const
|
|
217
|
+
const cores = Math.max(1, stats.cpu?.cores || 1)
|
|
218
|
+
const reservable = Math.max(1, stats.memory?.reservable || 1)
|
|
219
|
+
const load = (stats.cpu ? stats.cpu.systemLoad / cores : 0) * 100 +
|
|
202
220
|
(stats.playingPlayers || 0) * 0.75 +
|
|
203
|
-
(stats.memory ? stats.memory.used /
|
|
221
|
+
(stats.memory ? stats.memory.used / reservable : 0) * 40 +
|
|
204
222
|
(node.rest?.calls || 0) * 0.001
|
|
205
223
|
this._nodeLoadCache.set(id, {load, time: now})
|
|
206
224
|
if (this._nodeLoadCache.size > MAX_CACHE_SIZE) {
|
|
@@ -213,13 +231,13 @@ class Aqua extends EventEmitter {
|
|
|
213
231
|
async init(clientId) {
|
|
214
232
|
if (this.initiated) return this
|
|
215
233
|
this.clientId = clientId
|
|
216
|
-
if (!this.clientId) return
|
|
234
|
+
if (!this.clientId) return this
|
|
217
235
|
const results = await Promise.allSettled(
|
|
218
|
-
this.nodes.map(n => Promise.race([this._createNode(n),
|
|
236
|
+
this.nodes.map(n => Promise.race([this._createNode(n), _functions.delay(NODE_TIMEOUT).then(() => {throw new Error('Timeout')})]))
|
|
219
237
|
)
|
|
220
238
|
if (!results.some(r => r.status === 'fulfilled')) throw new Error('No nodes connected')
|
|
221
239
|
if (this.plugins?.length) {
|
|
222
|
-
await Promise.allSettled(this.plugins.map(p =>
|
|
240
|
+
await Promise.allSettled(this.plugins.map(p => _functions.safeCall(() => p.load(this))))
|
|
223
241
|
}
|
|
224
242
|
this.initiated = true
|
|
225
243
|
return this
|
|
@@ -247,7 +265,7 @@ class Aqua extends EventEmitter {
|
|
|
247
265
|
_destroyNode(id) {
|
|
248
266
|
const node = this.nodeMap.get(id)
|
|
249
267
|
if (!node) return
|
|
250
|
-
|
|
268
|
+
_functions.safeCall(() => node.destroy())
|
|
251
269
|
this._cleanupNode(id)
|
|
252
270
|
this.emit(AqualinkEvents.NodeDestroy, node)
|
|
253
271
|
}
|
|
@@ -255,8 +273,8 @@ class Aqua extends EventEmitter {
|
|
|
255
273
|
_cleanupNode(id) {
|
|
256
274
|
const node = this.nodeMap.get(id)
|
|
257
275
|
if (node) {
|
|
258
|
-
|
|
259
|
-
|
|
276
|
+
_functions.safeCall(() => node.removeAllListeners())
|
|
277
|
+
_functions.safeCall(() => node.players.clear())
|
|
260
278
|
this.nodeMap.delete(id)
|
|
261
279
|
}
|
|
262
280
|
this._nodeStates.delete(id)
|
|
@@ -296,11 +314,11 @@ class Aqua extends EventEmitter {
|
|
|
296
314
|
const results = await Promise.allSettled(
|
|
297
315
|
batch.map(({guildId, state}) => this._rebuildPlayer(state, node).then(() => guildId))
|
|
298
316
|
)
|
|
299
|
-
|
|
317
|
+
for (const r of results) {
|
|
300
318
|
if (r.status === 'fulfilled') successes.push(r.value)
|
|
301
|
-
}
|
|
319
|
+
}
|
|
302
320
|
}
|
|
303
|
-
|
|
321
|
+
for (const guildId of successes) this._brokenPlayers.delete(guildId)
|
|
304
322
|
if (successes.length) this.emit(AqualinkEvents.PlayersRebuilt, node, successes.length)
|
|
305
323
|
this._performCleanup()
|
|
306
324
|
}
|
|
@@ -311,17 +329,16 @@ class Aqua extends EventEmitter {
|
|
|
311
329
|
if (this._rebuildLocks.has(lockKey)) return
|
|
312
330
|
this._rebuildLocks.add(lockKey)
|
|
313
331
|
try {
|
|
314
|
-
|
|
315
|
-
if (existing) {
|
|
332
|
+
if (this.players.has(guildId)) {
|
|
316
333
|
await this.destroyPlayer(guildId)
|
|
317
|
-
await
|
|
334
|
+
await _functions.delay(RECONNECT_DELAY)
|
|
318
335
|
}
|
|
319
336
|
const player = this.createPlayer(targetNode, {guildId, textChannel, voiceChannel, defaultVolume: volume, deaf})
|
|
320
337
|
if (current && player?.queue?.add) {
|
|
321
338
|
player.queue.add(current)
|
|
322
339
|
await player.play()
|
|
323
340
|
if (state.position > 0) setTimeout(() => player.seek?.(state.position), SEEK_DELAY)
|
|
324
|
-
if (state.paused)
|
|
341
|
+
if (state.paused) player.pause(true)
|
|
325
342
|
}
|
|
326
343
|
return player
|
|
327
344
|
} finally {
|
|
@@ -348,7 +365,10 @@ class Aqua extends EventEmitter {
|
|
|
348
365
|
this.emit(AqualinkEvents.NodeFailover, failedNode)
|
|
349
366
|
const players = Array.from(failedNode.players || [])
|
|
350
367
|
if (!players.length) return
|
|
351
|
-
const available =
|
|
368
|
+
const available = []
|
|
369
|
+
for (const n of this.nodeMap.values()) {
|
|
370
|
+
if (n !== failedNode && n.connected) available.push(n)
|
|
371
|
+
}
|
|
352
372
|
if (!available.length) throw new Error('No failover nodes')
|
|
353
373
|
const results = await this._migratePlayersOptimized(players, available)
|
|
354
374
|
const successful = results.filter(r => r.success).length
|
|
@@ -366,24 +386,24 @@ class Aqua extends EventEmitter {
|
|
|
366
386
|
async _migratePlayersOptimized(players, nodes) {
|
|
367
387
|
const loads = new Map()
|
|
368
388
|
const counts = new Map()
|
|
369
|
-
|
|
389
|
+
for (const n of nodes) {
|
|
370
390
|
loads.set(n, this._getNodeLoad(n))
|
|
371
391
|
counts.set(n, 0)
|
|
372
|
-
}
|
|
392
|
+
}
|
|
373
393
|
const pickNode = () => {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
}
|
|
379
|
-
counts.set(
|
|
380
|
-
return
|
|
394
|
+
let best = nodes[0], bestScore = loads.get(best) + counts.get(best)
|
|
395
|
+
for (let i = 1; i < nodes.length; i++) {
|
|
396
|
+
const score = loads.get(nodes[i]) + counts.get(nodes[i])
|
|
397
|
+
if (score < bestScore) { best = nodes[i]; bestScore = score }
|
|
398
|
+
}
|
|
399
|
+
counts.set(best, counts.get(best) + 1)
|
|
400
|
+
return best
|
|
381
401
|
}
|
|
382
402
|
const results = []
|
|
383
403
|
for (let i = 0; i < players.length; i += MAX_CONCURRENT_OPS) {
|
|
384
404
|
const batch = players.slice(i, i + MAX_CONCURRENT_OPS)
|
|
385
405
|
const batchResults = await Promise.allSettled(batch.map(p => this._migratePlayer(p, pickNode)))
|
|
386
|
-
|
|
406
|
+
for (const r of batchResults) results.push({success: r.status === 'fulfilled', error: r.reason})
|
|
387
407
|
}
|
|
388
408
|
return results
|
|
389
409
|
}
|
|
@@ -391,7 +411,8 @@ class Aqua extends EventEmitter {
|
|
|
391
411
|
async _migratePlayer(player, pickNode) {
|
|
392
412
|
const state = this._capturePlayerState(player)
|
|
393
413
|
if (!state) throw new Error('Failed to capture state')
|
|
394
|
-
|
|
414
|
+
const {maxRetries, retryDelay} = this.failoverOptions
|
|
415
|
+
for (let retry = 0; retry < maxRetries; retry++) {
|
|
395
416
|
try {
|
|
396
417
|
const targetNode = pickNode()
|
|
397
418
|
const newPlayer = this._createPlayerOnNode(targetNode, state)
|
|
@@ -399,14 +420,15 @@ class Aqua extends EventEmitter {
|
|
|
399
420
|
this.emit(AqualinkEvents.PlayerMigrated, player, newPlayer, targetNode)
|
|
400
421
|
return newPlayer
|
|
401
422
|
} catch (error) {
|
|
402
|
-
if (retry ===
|
|
403
|
-
await
|
|
423
|
+
if (retry === maxRetries - 1) throw error
|
|
424
|
+
await _functions.delay(retryDelay * Math.pow(1.5, retry))
|
|
404
425
|
}
|
|
405
426
|
}
|
|
406
427
|
}
|
|
407
428
|
|
|
408
429
|
_capturePlayerState(player) {
|
|
409
|
-
|
|
430
|
+
if (!player) return null
|
|
431
|
+
return {
|
|
410
432
|
guildId: player.guildId,
|
|
411
433
|
textChannel: player.textChannel,
|
|
412
434
|
voiceChannel: player.voiceChannel,
|
|
@@ -419,7 +441,7 @@ class Aqua extends EventEmitter {
|
|
|
419
441
|
shuffle: player.shuffle,
|
|
420
442
|
deaf: player.deaf ?? false,
|
|
421
443
|
connected: !!player.connected
|
|
422
|
-
}
|
|
444
|
+
}
|
|
423
445
|
}
|
|
424
446
|
|
|
425
447
|
_createPlayerOnNode(targetNode, state) {
|
|
@@ -447,20 +469,18 @@ class Aqua extends EventEmitter {
|
|
|
447
469
|
if (state.paused) ops.push(newPlayer.pause(true))
|
|
448
470
|
}
|
|
449
471
|
}
|
|
450
|
-
|
|
472
|
+
newPlayer.loop = state.loop
|
|
473
|
+
newPlayer.shuffle = state.shuffle
|
|
451
474
|
await Promise.allSettled(ops)
|
|
452
475
|
}
|
|
453
476
|
|
|
454
477
|
updateVoiceState({d, t}) {
|
|
455
478
|
if (!d?.guild_id || (t !== 'VOICE_STATE_UPDATE' && t !== 'VOICE_SERVER_UPDATE')) return
|
|
456
479
|
const player = this.players.get(d.guild_id)
|
|
457
|
-
if (!player) return
|
|
480
|
+
if (!player || !player.nodes?.connected) return
|
|
458
481
|
if (t === 'VOICE_STATE_UPDATE') {
|
|
459
482
|
if (d.user_id !== this.clientId) return
|
|
460
|
-
if (!d.channel_id)
|
|
461
|
-
this.destroyPlayer(d.guild_id)
|
|
462
|
-
return
|
|
463
|
-
}
|
|
483
|
+
if (!d.channel_id) return void this.destroyPlayer(d.guild_id)
|
|
464
484
|
if (player.connection) {
|
|
465
485
|
player.connection.sessionId = d.session_id
|
|
466
486
|
player.connection.setStateUpdate(d)
|
|
@@ -473,7 +493,10 @@ class Aqua extends EventEmitter {
|
|
|
473
493
|
fetchRegion(region) {
|
|
474
494
|
if (!region) return this.leastUsedNodes
|
|
475
495
|
const lower = region.toLowerCase()
|
|
476
|
-
const filtered =
|
|
496
|
+
const filtered = []
|
|
497
|
+
for (const n of this.nodeMap.values()) {
|
|
498
|
+
if (n.connected && n.regions?.includes(lower)) filtered.push(n)
|
|
499
|
+
}
|
|
477
500
|
return Object.freeze(filtered.sort((a, b) => this._getNodeLoad(a) - this._getNodeLoad(b)))
|
|
478
501
|
}
|
|
479
502
|
|
|
@@ -482,7 +505,7 @@ class Aqua extends EventEmitter {
|
|
|
482
505
|
const existing = this.players.get(options.guildId)
|
|
483
506
|
if (existing) {
|
|
484
507
|
if (options.voiceChannel && existing.voiceChannel !== options.voiceChannel) {
|
|
485
|
-
|
|
508
|
+
_functions.safeCall(() => existing.connect(options))
|
|
486
509
|
}
|
|
487
510
|
return existing
|
|
488
511
|
}
|
|
@@ -493,7 +516,7 @@ class Aqua extends EventEmitter {
|
|
|
493
516
|
|
|
494
517
|
createPlayer(node, options) {
|
|
495
518
|
const existing = this.players.get(options.guildId)
|
|
496
|
-
if (existing)
|
|
519
|
+
if (existing) _functions.safeCall(() => existing.destroy())
|
|
497
520
|
const player = new Player(this, node, options)
|
|
498
521
|
this.players.set(options.guildId, player)
|
|
499
522
|
node?.players?.add?.(player)
|
|
@@ -506,29 +529,29 @@ class Aqua extends EventEmitter {
|
|
|
506
529
|
_handlePlayerDestroy(player) {
|
|
507
530
|
player.nodes?.players?.delete?.(player)
|
|
508
531
|
if (this.players.get(player.guildId) === player) this.players.delete(player.guildId)
|
|
509
|
-
this.emit(AqualinkEvents.
|
|
532
|
+
this.emit(AqualinkEvents.PlayerDestroyed, player)
|
|
510
533
|
}
|
|
511
534
|
|
|
512
535
|
async destroyPlayer(guildId) {
|
|
513
536
|
const player = this.players.get(guildId)
|
|
514
537
|
if (!player) return
|
|
515
538
|
this.players.delete(guildId)
|
|
516
|
-
|
|
517
|
-
await
|
|
539
|
+
_functions.safeCall(() => player.removeAllListeners())
|
|
540
|
+
await _functions.safeCall(() => player.destroy())
|
|
518
541
|
}
|
|
519
542
|
|
|
520
|
-
async resolve({query, source
|
|
543
|
+
async resolve({query, source, requester, nodes}) {
|
|
521
544
|
if (!this.initiated) throw new Error('Aqua not initialized')
|
|
522
545
|
const node = this._getRequestNode(nodes)
|
|
523
546
|
if (!node) throw new Error('No nodes available')
|
|
524
|
-
const formatted =
|
|
547
|
+
const formatted = _functions.formatQuery(query, source || this.defaultSearchPlatform)
|
|
525
548
|
const endpoint = `/${this.restVersion}/loadtracks?identifier=${encodeURIComponent(formatted)}`
|
|
526
549
|
try {
|
|
527
550
|
const response = await node.rest.makeRequest('GET', endpoint)
|
|
528
551
|
if (!response || response.loadType === 'empty' || response.loadType === 'NO_MATCHES') return EMPTY_TRACKS_RESPONSE
|
|
529
552
|
return this._constructResponse(response, requester, node)
|
|
530
553
|
} catch (error) {
|
|
531
|
-
throw new Error(error?.name === 'AbortError' ? 'Request timeout' : `Resolve failed: ${error?.message ||
|
|
554
|
+
throw new Error(error?.name === 'AbortError' ? 'Request timeout' : `Resolve failed: ${error?.message || error}`)
|
|
532
555
|
}
|
|
533
556
|
}
|
|
534
557
|
|
|
@@ -549,11 +572,12 @@ class Aqua extends EventEmitter {
|
|
|
549
572
|
_chooseLeastBusyNode(nodes) {
|
|
550
573
|
if (!nodes?.length) return null
|
|
551
574
|
if (nodes.length === 1) return nodes[0]
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
}
|
|
575
|
+
let best = nodes[0], bestScore = this._getNodeLoad(best)
|
|
576
|
+
for (let i = 1; i < nodes.length; i++) {
|
|
577
|
+
const score = this._getNodeLoad(nodes[i])
|
|
578
|
+
if (score < bestScore) { best = nodes[i]; bestScore = score }
|
|
579
|
+
}
|
|
580
|
+
return best
|
|
557
581
|
}
|
|
558
582
|
|
|
559
583
|
_constructResponse(response, requester, node) {
|
|
@@ -563,25 +587,22 @@ class Aqua extends EventEmitter {
|
|
|
563
587
|
base.exception = data || response.exception || null
|
|
564
588
|
return base
|
|
565
589
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
const thumbnail = data.pluginInfo?.artworkUrl || data.tracks?.[0]?.info?.artworkUrl || null
|
|
577
|
-
if (info) base.playlistInfo = {name: info.name || info.title, thumbnail, ...info}
|
|
578
|
-
base.pluginInfo = data.pluginInfo || base.pluginInfo
|
|
579
|
-
base.tracks = Array.isArray(data.tracks) ? data.tracks.map(t => _makeTrack(t, requester, node)) : []
|
|
590
|
+
if (loadType === 'track' && data) {
|
|
591
|
+
base.pluginInfo = data.info?.pluginInfo || data.pluginInfo || base.pluginInfo
|
|
592
|
+
base.tracks.push(_functions.makeTrack(data, requester, node))
|
|
593
|
+
} else if (loadType === 'playlist' && data) {
|
|
594
|
+
const info = data.info
|
|
595
|
+
if (info) {
|
|
596
|
+
base.playlistInfo = {
|
|
597
|
+
name: info.name || info.title,
|
|
598
|
+
thumbnail: data.pluginInfo?.artworkUrl || data.tracks?.[0]?.info?.artworkUrl || null,
|
|
599
|
+
...info
|
|
580
600
|
}
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
601
|
+
}
|
|
602
|
+
base.pluginInfo = data.pluginInfo || base.pluginInfo
|
|
603
|
+
base.tracks = Array.isArray(data.tracks) ? data.tracks.map(t => _functions.makeTrack(t, requester, node)) : []
|
|
604
|
+
} else if (loadType === 'search') {
|
|
605
|
+
base.tracks = Array.isArray(data) ? data.map(t => _functions.makeTrack(t, requester, node)) : []
|
|
585
606
|
}
|
|
586
607
|
return base
|
|
587
608
|
}
|
|
@@ -592,7 +613,7 @@ class Aqua extends EventEmitter {
|
|
|
592
613
|
return player
|
|
593
614
|
}
|
|
594
615
|
|
|
595
|
-
async search(query, requester, source
|
|
616
|
+
async search(query, requester, source) {
|
|
596
617
|
if (!query || !requester) return null
|
|
597
618
|
try {
|
|
598
619
|
const {tracks} = await this.resolve({query, source: source || this.defaultSearchPlatform, requester})
|
|
@@ -610,11 +631,10 @@ class Aqua extends EventEmitter {
|
|
|
610
631
|
await fs.promises.writeFile(lockFile, String(process.pid), {flag: 'wx'})
|
|
611
632
|
ws = fs.createWriteStream(tempFile, {encoding: 'utf8', flags: 'w'})
|
|
612
633
|
const buffer = []
|
|
613
|
-
let
|
|
634
|
+
let drainPromise = Promise.resolve()
|
|
614
635
|
|
|
615
636
|
for (const player of this.players.values()) {
|
|
616
637
|
const requester = player.requester || player.current?.requester
|
|
617
|
-
console.log(`Saving player for guild ${player.guildId} to ${player}`)
|
|
618
638
|
const data = {
|
|
619
639
|
g: player.guildId,
|
|
620
640
|
t: player.textChannel,
|
|
@@ -636,36 +656,28 @@ class Aqua extends EventEmitter {
|
|
|
636
656
|
const chunk = buffer.join('\n') + '\n'
|
|
637
657
|
buffer.length = 0
|
|
638
658
|
if (!ws.write(chunk)) {
|
|
639
|
-
|
|
659
|
+
drainPromise = drainPromise.then(() => new Promise(r => ws.once('drain', r)))
|
|
640
660
|
}
|
|
641
661
|
}
|
|
642
662
|
}
|
|
643
663
|
|
|
644
|
-
if (buffer.length)
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
await writePromise
|
|
650
|
-
await new Promise((resolve, reject) => {
|
|
651
|
-
ws.end(err => err ? reject(err) : resolve())
|
|
652
|
-
})
|
|
664
|
+
if (buffer.length) ws.write(buffer.join('\n') + '\n')
|
|
665
|
+
await drainPromise
|
|
666
|
+
await new Promise((resolve, reject) => ws.end(err => err ? reject(err) : resolve()))
|
|
653
667
|
ws = null
|
|
654
|
-
|
|
655
668
|
await fs.promises.rename(tempFile, filePath)
|
|
656
669
|
} catch (error) {
|
|
657
670
|
this.emit(AqualinkEvents.Error, null, error)
|
|
658
|
-
if (ws)
|
|
659
|
-
await fs.promises.unlink(tempFile).catch(
|
|
671
|
+
if (ws) _functions.safeCall(() => ws.destroy())
|
|
672
|
+
await fs.promises.unlink(tempFile).catch(_functions.noop)
|
|
660
673
|
} finally {
|
|
661
|
-
await fs.promises.unlink(lockFile).catch(
|
|
674
|
+
await fs.promises.unlink(lockFile).catch(_functions.noop)
|
|
662
675
|
}
|
|
663
676
|
}
|
|
664
677
|
|
|
665
678
|
async loadPlayers(filePath = './AquaPlayers.jsonl') {
|
|
666
679
|
const lockFile = `${filePath}.lock`
|
|
667
|
-
let stream = null
|
|
668
|
-
let rl = null
|
|
680
|
+
let stream = null, rl = null
|
|
669
681
|
try {
|
|
670
682
|
await fs.promises.access(filePath)
|
|
671
683
|
await fs.promises.writeFile(lockFile, String(process.pid), {flag: 'wx'})
|
|
@@ -677,26 +689,20 @@ class Aqua extends EventEmitter {
|
|
|
677
689
|
const batch = []
|
|
678
690
|
for await (const line of rl) {
|
|
679
691
|
if (!line.trim()) continue
|
|
680
|
-
try {
|
|
681
|
-
batch.push(JSON.parse(line))
|
|
682
|
-
} catch {
|
|
683
|
-
continue
|
|
684
|
-
}
|
|
692
|
+
try { batch.push(JSON.parse(line)) } catch { continue }
|
|
685
693
|
if (batch.length >= PLAYER_BATCH_SIZE) {
|
|
686
694
|
await Promise.allSettled(batch.map(p => this._restorePlayer(p)))
|
|
687
695
|
batch.length = 0
|
|
688
696
|
}
|
|
689
697
|
}
|
|
690
698
|
if (batch.length) await Promise.allSettled(batch.map(p => this._restorePlayer(p)))
|
|
691
|
-
|
|
692
|
-
// Clear file after successful load
|
|
693
699
|
await fs.promises.writeFile(filePath, '')
|
|
694
700
|
} catch (err) {
|
|
695
701
|
if (err.code !== 'ENOENT') this.emit(AqualinkEvents.Error, null, err)
|
|
696
702
|
} finally {
|
|
697
|
-
if (rl)
|
|
698
|
-
if (stream)
|
|
699
|
-
await fs.promises.unlink(lockFile).catch(
|
|
703
|
+
if (rl) _functions.safeCall(() => rl.close())
|
|
704
|
+
if (stream) _functions.safeCall(() => stream.destroy())
|
|
705
|
+
await fs.promises.unlink(lockFile).catch(_functions.noop)
|
|
700
706
|
}
|
|
701
707
|
}
|
|
702
708
|
|
|
@@ -708,13 +714,10 @@ class Aqua extends EventEmitter {
|
|
|
708
714
|
voiceChannel: p.v,
|
|
709
715
|
defaultVolume: p.vol || 65,
|
|
710
716
|
deaf: true,
|
|
711
|
-
// indicate this player is being restored from persisted state so Connection
|
|
712
|
-
// can behave accordingly (attempt resume even if voice data is stale)
|
|
713
717
|
resuming: !!p.resuming
|
|
714
718
|
})
|
|
715
|
-
// Ensure flag is set on existing players as well
|
|
716
719
|
player._resuming = !!p.resuming
|
|
717
|
-
const requester =
|
|
720
|
+
const requester = _functions.parseRequester(p.r)
|
|
718
721
|
const tracksToResolve = [p.u, ...(p.q || [])].filter(Boolean).slice(0, MAX_TRACKS_RESTORE)
|
|
719
722
|
const resolved = await Promise.all(tracksToResolve.map(uri => this.resolve({query: uri, requester}).catch(() => null)))
|
|
720
723
|
const validTracks = resolved.flatMap(r => r?.tracks || [])
|
|
@@ -733,35 +736,26 @@ class Aqua extends EventEmitter {
|
|
|
733
736
|
}
|
|
734
737
|
if (p.nw && p.t) {
|
|
735
738
|
const channel = this.client.channels?.cache?.get(p.t)
|
|
736
|
-
if (channel?.messages)
|
|
737
|
-
player.nowPlayingMessage = await channel.messages.fetch(p.nw).catch(() => null)
|
|
738
|
-
}
|
|
739
|
+
if (channel?.messages) player.nowPlayingMessage = await channel.messages.fetch(p.nw).catch(() => null)
|
|
739
740
|
}
|
|
740
741
|
} catch {}
|
|
741
742
|
}
|
|
742
743
|
|
|
743
|
-
_parseRequester(str) {
|
|
744
|
-
if (!str || typeof str !== 'string') return null
|
|
745
|
-
const i = str.indexOf(':')
|
|
746
|
-
return i > 0 ? {id: str.substring(0, i), username: str.substring(i + 1)} : null
|
|
747
|
-
}
|
|
748
|
-
|
|
749
744
|
async _waitForFirstNode(timeout = NODE_TIMEOUT) {
|
|
750
745
|
if (this.leastUsedNodes.length) return
|
|
751
746
|
return new Promise((resolve, reject) => {
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
resolve()
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
const timer = setTimeout(() => {
|
|
747
|
+
let resolved = false
|
|
748
|
+
const cleanup = () => {
|
|
749
|
+
if (resolved) return
|
|
750
|
+
resolved = true
|
|
751
|
+
clearTimeout(timer)
|
|
761
752
|
this.off(AqualinkEvents.NodeConnect, onReady)
|
|
762
753
|
this.off(AqualinkEvents.NodeCreate, onReady)
|
|
763
|
-
|
|
764
|
-
|
|
754
|
+
}
|
|
755
|
+
const onReady = () => {
|
|
756
|
+
if (this.leastUsedNodes.length) { cleanup(); resolve() }
|
|
757
|
+
}
|
|
758
|
+
const timer = setTimeout(() => { cleanup(); reject(new Error('Timeout waiting for first node')) }, timeout)
|
|
765
759
|
this.on(AqualinkEvents.NodeConnect, onReady)
|
|
766
760
|
this.on(AqualinkEvents.NodeCreate, onReady)
|
|
767
761
|
onReady()
|
|
@@ -770,24 +764,17 @@ class Aqua extends EventEmitter {
|
|
|
770
764
|
|
|
771
765
|
_performCleanup() {
|
|
772
766
|
const now = Date.now()
|
|
773
|
-
|
|
774
|
-
// Cleanup expired broken players
|
|
775
767
|
for (const [guildId, state] of this._brokenPlayers) {
|
|
776
768
|
if (now - state.brokenAt > BROKEN_PLAYER_TTL) this._brokenPlayers.delete(guildId)
|
|
777
769
|
}
|
|
778
|
-
|
|
779
|
-
// Cleanup old failover attempts
|
|
780
770
|
for (const [id, ts] of this._lastFailoverAttempt) {
|
|
781
771
|
if (now - ts > FAILOVER_CLEANUP_TTL) {
|
|
782
772
|
this._lastFailoverAttempt.delete(id)
|
|
783
773
|
this._failoverQueue.delete(id)
|
|
784
774
|
}
|
|
785
775
|
}
|
|
786
|
-
|
|
787
776
|
if (this._failoverQueue.size > MAX_FAILOVER_QUEUE) this._failoverQueue.clear()
|
|
788
777
|
if (this._rebuildLocks.size > MAX_REBUILD_LOCKS) this._rebuildLocks.clear()
|
|
789
|
-
|
|
790
|
-
// Cleanup orphaned node states
|
|
791
778
|
for (const id of this._nodeStates.keys()) {
|
|
792
779
|
if (!this.nodeMap.has(id)) this._nodeStates.delete(id)
|
|
793
780
|
}
|
|
@@ -29,15 +29,15 @@ const AqualinkEvents = {
|
|
|
29
29
|
Debug: 'debug',
|
|
30
30
|
Error: 'error',
|
|
31
31
|
PlayerCreate: 'playerCreate',
|
|
32
|
-
PlayerDestroy: 'playerDestroy',
|
|
33
32
|
PlayersRebuilt: 'playersRebuilt',
|
|
34
33
|
VolumeChanged: 'volumeChanged',
|
|
35
34
|
FiltersChanged: 'filtersChanged',
|
|
36
35
|
Seek: 'seek',
|
|
37
36
|
PlayerCreated: 'playerCreated',
|
|
38
37
|
PlayerConnected: 'playerConnected',
|
|
39
|
-
PlayerDestroyed: '
|
|
40
|
-
PlayerMigrated: 'playerMigrated'
|
|
38
|
+
PlayerDestroyed: 'playerDestroy',
|
|
39
|
+
PlayerMigrated: 'playerMigrated',
|
|
40
|
+
PauseEvent: 'pauseEvent'
|
|
41
41
|
};
|
|
42
42
|
|
|
43
43
|
module.exports = { AqualinkEvents };
|