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.
- package/README.md +92 -58
- package/build/handlers/autoplay.js +39 -44
- package/build/structures/Aqua.js +164 -377
- package/build/structures/Connection.js +57 -64
- package/build/structures/Node.js +54 -78
- package/build/structures/Player.js +212 -361
- package/build/structures/Rest.js +73 -45
- package/build/structures/Track.js +55 -126
- package/package.json +17 -2
package/build/structures/Aqua.js
CHANGED
|
@@ -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
|
|
12
|
-
const
|
|
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
|
|
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
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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 =
|
|
83
|
+
_defaultSend = packet => {
|
|
77
84
|
const guildId = packet.d.guild_id
|
|
78
|
-
const guild = this.client
|
|
79
|
-
|
|
80
|
-
|
|
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.
|
|
91
|
-
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
|
-
|
|
122
|
-
|
|
123
|
-
const
|
|
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
|
-
|
|
129
|
-
|
|
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
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
.
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
.
|
|
211
|
-
|
|
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 (
|
|
247
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
323
|
-
const
|
|
324
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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 (!
|
|
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
|
-
|
|
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.
|
|
444
|
-
|
|
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
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
487
|
+
async loadPlayers(filePath = './AquaPlayers.jsonl') {
|
|
625
488
|
try {
|
|
626
489
|
await fs.access(filePath)
|
|
627
490
|
await this._waitForFirstNode()
|
|
628
|
-
|
|
629
|
-
const
|
|
630
|
-
|
|
631
|
-
|
|
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
|
-
|
|
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.
|
|
644
|
-
const
|
|
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)
|
|
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
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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,
|
|
672
|
-
this.emit('debug', 'Aqua', `Saved ${
|
|
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 =
|
|
680
|
-
|
|
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.
|
|
695
|
-
if (p.
|
|
696
|
-
if (p.
|
|
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
|
-
|
|
738
|
-
if (
|
|
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
|
|
583
|
+
if (this.leastUsedNodes.length) {
|
|
743
584
|
clearInterval(checkInterval)
|
|
744
585
|
resolve()
|
|
745
586
|
}
|
|
746
|
-
},
|
|
587
|
+
}, NODE_CHECK_INTERVAL)
|
|
747
588
|
})
|
|
748
589
|
}
|
|
749
590
|
|
|
750
591
|
_performCleanup() {
|
|
751
592
|
const now = Date.now()
|
|
752
|
-
|
|
753
|
-
|
|
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
|
-
|
|
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
|
|