aqualink 2.10.1 → 2.11.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 +695 -517
- package/build/structures/Connection.js +89 -38
- package/build/structures/Filters.js +50 -46
- package/build/structures/Node.js +167 -180
- package/build/structures/Player.js +525 -295
- package/build/structures/Rest.js +226 -271
- package/package.json +1 -1
package/build/structures/Aqua.js
CHANGED
|
@@ -1,26 +1,24 @@
|
|
|
1
|
-
'use strict'
|
|
1
|
+
'use strict'
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const
|
|
3
|
+
const fs = require('node:fs')
|
|
4
|
+
const readline = require('node:readline')
|
|
5
|
+
const { EventEmitter } = require('tseep')
|
|
5
6
|
|
|
6
|
-
const Node = require('./Node')
|
|
7
|
-
const Player = require('./Player')
|
|
8
|
-
const Track = require('./Track')
|
|
9
|
-
const { version: pkgVersion } = require('../../package.json')
|
|
7
|
+
const Node = require('./Node')
|
|
8
|
+
const Player = require('./Player')
|
|
9
|
+
const Track = require('./Track')
|
|
10
|
+
const { version: pkgVersion } = require('../../package.json')
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
const SEARCH_PREFIX = ':'
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
const EMPTY_ARRAY = Object.freeze([]);
|
|
12
|
+
// Constants
|
|
13
|
+
const SEARCH_PREFIX = ':'
|
|
14
|
+
const EMPTY_ARRAY = Object.freeze([])
|
|
16
15
|
const EMPTY_TRACKS_RESPONSE = Object.freeze({
|
|
17
16
|
loadType: 'empty',
|
|
18
17
|
exception: null,
|
|
19
18
|
playlistInfo: null,
|
|
20
19
|
pluginInfo: {},
|
|
21
20
|
tracks: EMPTY_ARRAY
|
|
22
|
-
})
|
|
23
|
-
|
|
21
|
+
})
|
|
24
22
|
|
|
25
23
|
const DEFAULT_OPTIONS = Object.freeze({
|
|
26
24
|
shouldDeleteMessage: false,
|
|
@@ -39,365 +37,454 @@ const DEFAULT_OPTIONS = Object.freeze({
|
|
|
39
37
|
cooldownTime: 5000,
|
|
40
38
|
maxFailoverAttempts: 5
|
|
41
39
|
})
|
|
42
|
-
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const CLEANUP_INTERVAL = 180000 // 3m
|
|
43
|
+
const MAX_CONCURRENT_OPS = 10
|
|
44
|
+
const BROKEN_PLAYER_TTL = 300000 // 5m
|
|
45
|
+
const FAILOVER_CLEANUP_TTL = 600000 // 10m
|
|
46
|
+
const PLAYER_BATCH_SIZE = 20
|
|
47
|
+
const SEEK_DELAY = 120
|
|
48
|
+
const RECONNECT_DELAY = 400
|
|
49
|
+
const CACHE_VALID_TIME = 12000 // 12s
|
|
50
|
+
const NODE_TIMEOUT = 30000
|
|
43
51
|
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
const BROKEN_PLAYER_TTL = 300000;
|
|
47
|
-
const FAILOVER_CLEANUP_TTL = 600000;
|
|
48
|
-
const NODE_BATCH_SIZE = 3;
|
|
49
|
-
const PLAYER_BATCH_SIZE = 8;
|
|
50
|
-
const SEEK_DELAY = 150;
|
|
51
|
-
const RECONNECT_DELAY = 800;
|
|
52
|
-
const NODE_CHECK_INTERVAL = 100;
|
|
53
|
-
const CACHE_VALID_TIME = 5000;
|
|
52
|
+
const URL_PATTERN = /^https?:\/\//i
|
|
53
|
+
const isProbablyUrl = s => typeof s === 'string' && URL_PATTERN.test(s)
|
|
54
54
|
|
|
55
|
-
const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
|
|
55
|
+
const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
|
|
56
56
|
|
|
57
57
|
class Aqua extends EventEmitter {
|
|
58
58
|
constructor(client, nodes, options = {}) {
|
|
59
|
-
super()
|
|
60
|
-
if (!client) throw new Error('Client is required')
|
|
61
|
-
if (!Array.isArray(nodes) || !nodes.length) throw new TypeError('Nodes must be non-empty Array')
|
|
62
|
-
|
|
63
|
-
this.client = client
|
|
64
|
-
this.nodes = nodes
|
|
65
|
-
this.nodeMap = new Map()
|
|
66
|
-
this.players = new Map()
|
|
67
|
-
this.clientId = null
|
|
68
|
-
this.initiated = false
|
|
69
|
-
this.version = pkgVersion
|
|
70
|
-
|
|
71
|
-
this.options = Object.assign({}, DEFAULT_OPTIONS, options)
|
|
72
|
-
this.failoverOptions = Object.assign({}, DEFAULT_OPTIONS.failoverOptions, options.failoverOptions)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
this.
|
|
86
|
-
this.
|
|
87
|
-
|
|
88
|
-
this.
|
|
89
|
-
|
|
90
|
-
this.
|
|
91
|
-
this.
|
|
92
|
-
|
|
93
|
-
this._bindEventHandlers()
|
|
94
|
-
this._startCleanupTimer()
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
59
|
+
super()
|
|
60
|
+
if (!client) throw new Error('Client is required')
|
|
61
|
+
if (!Array.isArray(nodes) || !nodes.length) throw new TypeError('Nodes must be non-empty Array')
|
|
62
|
+
|
|
63
|
+
this.client = client
|
|
64
|
+
this.nodes = nodes
|
|
65
|
+
this.nodeMap = new Map()
|
|
66
|
+
this.players = new Map()
|
|
67
|
+
this.clientId = null
|
|
68
|
+
this.initiated = false
|
|
69
|
+
this.version = pkgVersion
|
|
70
|
+
|
|
71
|
+
this.options = Object.assign({}, DEFAULT_OPTIONS, options)
|
|
72
|
+
this.failoverOptions = Object.assign({}, DEFAULT_OPTIONS.failoverOptions, options.failoverOptions)
|
|
73
|
+
this.shouldDeleteMessage = this.options.shouldDeleteMessage
|
|
74
|
+
this.defaultSearchPlatform = this.options.defaultSearchPlatform
|
|
75
|
+
this.leaveOnEnd = this.options.leaveOnEnd
|
|
76
|
+
this.restVersion = this.options.restVersion || 'v4'
|
|
77
|
+
this.plugins = this.options.plugins
|
|
78
|
+
this.autoResume = this.options.autoResume
|
|
79
|
+
this.infiniteReconnects = this.options.infiniteReconnects
|
|
80
|
+
this.send = this.options.send || this._createDefaultSend()
|
|
81
|
+
|
|
82
|
+
this._nodeStates = new Map() // nodeId -> { connected, failoverInProgress }
|
|
83
|
+
this._failoverQueue = new Map() // nodeId -> attempts
|
|
84
|
+
this._lastFailoverAttempt = new Map() // nodeId -> timestamp
|
|
85
|
+
this._brokenPlayers = new Map() // guildId -> capturedState
|
|
86
|
+
this._rebuildLocks = new Set() // guild-level lock for rebuilds
|
|
87
|
+
|
|
88
|
+
this._leastUsedNodesCache = null
|
|
89
|
+
this._leastUsedNodesCacheTime = 0
|
|
90
|
+
this._nodeLoadCache = new Map()
|
|
91
|
+
this._nodeLoadCacheTime = new Map()
|
|
92
|
+
|
|
93
|
+
this._bindEventHandlers()
|
|
94
|
+
this._startCleanupTimer()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
_createDefaultSend() {
|
|
98
|
+
return packet => {
|
|
99
|
+
const guildId = packet?.d?.guild_id
|
|
100
|
+
if (!guildId) return
|
|
101
|
+
|
|
102
|
+
const guild = this.client.cache?.guilds?.get?.(guildId) ?? this.client.guilds?.cache?.get?.(guildId)
|
|
103
|
+
if (!guild) return
|
|
104
|
+
|
|
105
|
+
const gateway = this.client.gateway
|
|
106
|
+
if (gateway?.send) {
|
|
107
|
+
gateway.send(gateway.calculateShardId(guildId), packet)
|
|
108
|
+
} else if (guild.shard?.send) {
|
|
109
|
+
guild.shard.send(packet)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
109
113
|
|
|
110
114
|
_bindEventHandlers() {
|
|
111
|
-
|
|
112
|
-
|
|
115
|
+
if (!this.autoResume) return
|
|
116
|
+
|
|
117
|
+
this._onNodeConnect = node => queueMicrotask(() => {
|
|
118
|
+
this._invalidateCache()
|
|
119
|
+
this._rebuildBrokenPlayers(node)
|
|
120
|
+
})
|
|
121
|
+
this._onNodeDisconnect = node => queueMicrotask(() => {
|
|
122
|
+
this._invalidateCache()
|
|
123
|
+
this._storeBrokenPlayers(node)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
this.on('nodeConnect', this._onNodeConnect)
|
|
127
|
+
this.on('nodeDisconnect', this._onNodeDisconnect)
|
|
113
128
|
}
|
|
114
129
|
|
|
115
130
|
_startCleanupTimer() {
|
|
116
|
-
this._cleanupTimer = setInterval(() => this._performCleanup(), CLEANUP_INTERVAL)
|
|
117
|
-
this._cleanupTimer.unref()
|
|
131
|
+
this._cleanupTimer = setInterval(() => this._performCleanup(), CLEANUP_INTERVAL)
|
|
132
|
+
this._cleanupTimer.unref?.()
|
|
118
133
|
}
|
|
119
134
|
|
|
120
135
|
get leastUsedNodes() {
|
|
121
|
-
const now = Date.now()
|
|
136
|
+
const now = Date.now()
|
|
122
137
|
if (this._leastUsedNodesCache && (now - this._leastUsedNodesCacheTime) < CACHE_VALID_TIME) {
|
|
123
|
-
return this._leastUsedNodesCache
|
|
138
|
+
return this._leastUsedNodesCache
|
|
124
139
|
}
|
|
125
140
|
|
|
126
|
-
const
|
|
141
|
+
const connected = []
|
|
127
142
|
for (const node of this.nodeMap.values()) {
|
|
128
|
-
if (node.connected)
|
|
143
|
+
if (node.connected) connected.push(node)
|
|
129
144
|
}
|
|
130
145
|
|
|
131
|
-
|
|
132
|
-
for (let i = 1; i < connectedNodes.length; i++) {
|
|
133
|
-
const current = connectedNodes[i];
|
|
134
|
-
const calls = current.rest?.calls || 0;
|
|
135
|
-
let j = i - 1;
|
|
146
|
+
connected.sort((a, b) => this._getCachedNodeLoad(a) - this._getCachedNodeLoad(b))
|
|
136
147
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
connectedNodes[j + 1] = current;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
148
|
+
this._leastUsedNodesCache = Object.freeze(connected.slice())
|
|
149
|
+
this._leastUsedNodesCacheTime = now
|
|
150
|
+
return this._leastUsedNodesCache
|
|
151
|
+
}
|
|
144
152
|
|
|
145
|
-
|
|
146
|
-
this.
|
|
147
|
-
|
|
153
|
+
_invalidateCache() {
|
|
154
|
+
this._leastUsedNodesCache = null
|
|
155
|
+
this._leastUsedNodesCacheTime = 0
|
|
148
156
|
}
|
|
149
157
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
158
|
+
_getCachedNodeLoad(node) {
|
|
159
|
+
const nodeId = node.name || node.host
|
|
160
|
+
const now = Date.now()
|
|
161
|
+
const cacheTime = this._nodeLoadCacheTime.get(nodeId)
|
|
154
162
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
successCount += await this._processNodeBatch(batch);
|
|
163
|
+
if (cacheTime && (now - cacheTime) < 5000) {
|
|
164
|
+
return this._nodeLoadCache.get(nodeId) || 0
|
|
158
165
|
}
|
|
159
166
|
|
|
160
|
-
|
|
167
|
+
const load = this._calculateNodeLoad(node)
|
|
168
|
+
this._nodeLoadCache.set(nodeId, load)
|
|
169
|
+
this._nodeLoadCacheTime.set(nodeId, now)
|
|
170
|
+
return load
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
_calculateNodeLoad(node) {
|
|
174
|
+
const stats = node?.stats
|
|
175
|
+
if (!stats) return 0
|
|
176
|
+
|
|
177
|
+
const cpu = stats.cpu
|
|
178
|
+
const cores = Math.max(1, cpu?.cores || 1)
|
|
179
|
+
const cpuLoad = cpu ? (cpu.systemLoad / cores) : 0
|
|
161
180
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
181
|
+
const playing = stats.playingPlayers || 0
|
|
182
|
+
|
|
183
|
+
const memory = stats.memory
|
|
184
|
+
const memoryUsage = memory ? (memory.used / Math.max(1, memory.reservable)) : 0
|
|
185
|
+
|
|
186
|
+
const restCalls = node?.rest?.calls || 0
|
|
187
|
+
|
|
188
|
+
return (cpuLoad * 100) + (playing * 0.75) + (memoryUsage * 40) + (restCalls * 0.001)
|
|
165
189
|
}
|
|
166
190
|
|
|
167
|
-
async
|
|
168
|
-
|
|
169
|
-
|
|
191
|
+
async init(clientId) {
|
|
192
|
+
if (this.initiated) return this
|
|
193
|
+
this.clientId = clientId
|
|
194
|
+
|
|
195
|
+
if (!this.clientId) return
|
|
196
|
+
|
|
197
|
+
const results = await Promise.allSettled(
|
|
198
|
+
this.nodes.map(n =>
|
|
199
|
+
Promise.race([
|
|
200
|
+
this._createNode(n),
|
|
201
|
+
new Promise((_, reject) =>
|
|
202
|
+
setTimeout(() => reject(new Error('Node timeout')), NODE_TIMEOUT)
|
|
203
|
+
)
|
|
204
|
+
])
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
const successCount = results.filter(r => r.status === 'fulfilled').length
|
|
209
|
+
if (!successCount) throw new Error('No nodes connected')
|
|
210
|
+
|
|
211
|
+
await this._loadPlugins()
|
|
212
|
+
this.initiated = true
|
|
213
|
+
return this
|
|
170
214
|
}
|
|
171
215
|
|
|
172
216
|
async _loadPlugins() {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
217
|
+
if (!this.plugins?.length) return
|
|
218
|
+
await Promise.allSettled(
|
|
219
|
+
this.plugins.map(async plugin => {
|
|
220
|
+
try {
|
|
221
|
+
await plugin.load(this)
|
|
222
|
+
} catch (err) {
|
|
223
|
+
this.emit('error', null, new Error(`Plugin error: ${err?.message || String(err)}`))
|
|
224
|
+
}
|
|
225
|
+
})
|
|
226
|
+
)
|
|
179
227
|
}
|
|
180
228
|
|
|
181
229
|
async _createNode(options) {
|
|
182
|
-
const nodeId = options.name || options.host
|
|
183
|
-
this._destroyNode(nodeId)
|
|
230
|
+
const nodeId = options.name || options.host
|
|
231
|
+
this._destroyNode(nodeId)
|
|
184
232
|
|
|
185
|
-
const node = new Node(this, options, this.options)
|
|
186
|
-
node.players = new Set()
|
|
233
|
+
const node = new Node(this, options, this.options)
|
|
234
|
+
if (!node.players) node.players = new Set()
|
|
187
235
|
|
|
188
|
-
this.nodeMap.set(nodeId, node)
|
|
189
|
-
this._nodeStates.set(nodeId, { connected: false, failoverInProgress: false })
|
|
236
|
+
this.nodeMap.set(nodeId, node)
|
|
237
|
+
this._nodeStates.set(nodeId, { connected: false, failoverInProgress: false })
|
|
190
238
|
|
|
191
239
|
try {
|
|
192
|
-
node.connect()
|
|
193
|
-
this._nodeStates.set(nodeId, { connected: true, failoverInProgress: false })
|
|
194
|
-
this.
|
|
195
|
-
|
|
240
|
+
await node.connect()
|
|
241
|
+
this._nodeStates.set(nodeId, { connected: true, failoverInProgress: false })
|
|
242
|
+
this._invalidateCache()
|
|
243
|
+
this.emit('nodeCreate', node)
|
|
244
|
+
return node
|
|
196
245
|
} catch (error) {
|
|
197
|
-
this._cleanupNode(nodeId)
|
|
198
|
-
throw error
|
|
246
|
+
this._cleanupNode(nodeId)
|
|
247
|
+
throw error
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
_destroyNode(identifier) {
|
|
252
|
+
const node = this.nodeMap.get(identifier)
|
|
253
|
+
if (node) {
|
|
254
|
+
try { node.destroy?.() } catch {}
|
|
255
|
+
this._cleanupNode(identifier)
|
|
256
|
+
this.emit('nodeDestroy', node)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
_cleanupNode(nodeId) {
|
|
261
|
+
const node = this.nodeMap.get(nodeId)
|
|
262
|
+
if (node) {
|
|
263
|
+
node.removeAllListeners?.()
|
|
264
|
+
node.players?.clear?.()
|
|
265
|
+
this.nodeMap.delete(nodeId)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
this._nodeStates.delete(nodeId)
|
|
269
|
+
this._failoverQueue.delete(nodeId)
|
|
270
|
+
this._lastFailoverAttempt.delete(nodeId)
|
|
271
|
+
this._nodeLoadCache.delete(nodeId)
|
|
272
|
+
this._nodeLoadCacheTime.delete(nodeId)
|
|
273
|
+
|
|
274
|
+
if (this._leastUsedNodesCache?.some?.(n => (n.name || n.host) === nodeId)) {
|
|
275
|
+
this._invalidateCache()
|
|
199
276
|
}
|
|
200
277
|
}
|
|
201
278
|
|
|
202
279
|
_storeBrokenPlayers(node) {
|
|
203
|
-
const nodeId = node.name || node.host
|
|
204
|
-
const now = Date.now()
|
|
280
|
+
const nodeId = node.name || node.host
|
|
281
|
+
const now = Date.now()
|
|
282
|
+
const brokenStates = []
|
|
205
283
|
|
|
206
284
|
for (const player of this.players.values()) {
|
|
207
|
-
if (player.nodes !== node) continue
|
|
208
|
-
|
|
209
|
-
const state = this._capturePlayerState(player);
|
|
285
|
+
if (player.nodes !== node) continue
|
|
286
|
+
const state = this._capturePlayerState(player)
|
|
210
287
|
if (state) {
|
|
211
|
-
state.originalNodeId = nodeId
|
|
212
|
-
state.brokenAt = now
|
|
213
|
-
|
|
288
|
+
state.originalNodeId = nodeId
|
|
289
|
+
state.brokenAt = now
|
|
290
|
+
brokenStates.push([player.guildId, state])
|
|
214
291
|
}
|
|
215
292
|
}
|
|
293
|
+
|
|
294
|
+
for (const [guildId, state] of brokenStates) {
|
|
295
|
+
this._brokenPlayers.set(guildId, state)
|
|
296
|
+
}
|
|
216
297
|
}
|
|
217
298
|
|
|
218
|
-
_rebuildBrokenPlayers(node) {
|
|
219
|
-
const nodeId = node.name || node.host
|
|
220
|
-
|
|
221
|
-
const toDelete = [];
|
|
299
|
+
async _rebuildBrokenPlayers(node) {
|
|
300
|
+
const nodeId = node.name || node.host
|
|
301
|
+
const rebuilds = []
|
|
222
302
|
|
|
223
303
|
for (const [guildId, brokenState] of this._brokenPlayers) {
|
|
224
|
-
if (brokenState.originalNodeId !== nodeId) continue
|
|
304
|
+
if (brokenState.originalNodeId !== nodeId) continue
|
|
305
|
+
if (Date.now() - brokenState.brokenAt > BROKEN_PLAYER_TTL) continue
|
|
306
|
+
rebuilds.push({ guildId, brokenState })
|
|
307
|
+
}
|
|
225
308
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
309
|
+
if (!rebuilds.length) return
|
|
310
|
+
|
|
311
|
+
const batchSize = Math.min(MAX_CONCURRENT_OPS, rebuilds.length)
|
|
312
|
+
const successes = []
|
|
313
|
+
|
|
314
|
+
for (let i = 0; i < rebuilds.length; i += batchSize) {
|
|
315
|
+
const batch = rebuilds.slice(i, i + batchSize)
|
|
316
|
+
const results = await Promise.allSettled(
|
|
317
|
+
batch.map(({ guildId, brokenState }) =>
|
|
318
|
+
this._rebuildPlayer(brokenState, node).then(() => guildId)
|
|
319
|
+
)
|
|
320
|
+
)
|
|
321
|
+
for (const r of results) {
|
|
322
|
+
if (r.status === 'fulfilled') successes.push(r.value)
|
|
323
|
+
}
|
|
236
324
|
}
|
|
237
325
|
|
|
238
|
-
for (const guildId of
|
|
239
|
-
this._brokenPlayers.delete(guildId)
|
|
326
|
+
for (const guildId of successes) {
|
|
327
|
+
this._brokenPlayers.delete(guildId)
|
|
240
328
|
}
|
|
241
329
|
|
|
242
|
-
if (
|
|
330
|
+
if (successes.length) this.emit('playersRebuilt', node, successes.length)
|
|
243
331
|
}
|
|
244
332
|
|
|
245
333
|
async _rebuildPlayer(brokenState, targetNode) {
|
|
246
|
-
const { guildId, textChannel, voiceChannel, current, volume = 65, deaf = true } = brokenState
|
|
247
|
-
const
|
|
248
|
-
if (
|
|
249
|
-
|
|
250
|
-
await delay(RECONNECT_DELAY);
|
|
334
|
+
const { guildId, textChannel, voiceChannel, current, volume = 65, deaf = true } = brokenState
|
|
335
|
+
const lockKey = `rebuild_${guildId}`
|
|
336
|
+
if (this._rebuildLocks.has(lockKey)) return
|
|
337
|
+
this._rebuildLocks.add(lockKey)
|
|
251
338
|
|
|
252
339
|
try {
|
|
253
|
-
const
|
|
340
|
+
const existing = this.players.get(guildId)
|
|
341
|
+
if (existing) {
|
|
342
|
+
await this.destroyPlayer(guildId)
|
|
343
|
+
await delay(RECONNECT_DELAY)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const player = this.createPlayer(targetNode, {
|
|
254
347
|
guildId,
|
|
255
348
|
textChannel,
|
|
256
349
|
voiceChannel,
|
|
257
350
|
defaultVolume: volume,
|
|
258
351
|
deaf
|
|
259
|
-
})
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
if (current && player?.queue?.add) {
|
|
355
|
+
player.queue.add(current)
|
|
356
|
+
await player.play()
|
|
260
357
|
|
|
261
|
-
if (current) {
|
|
262
|
-
await player.queue.add(current);
|
|
263
|
-
await player.play();
|
|
264
358
|
if (brokenState.position > 0) {
|
|
265
|
-
setTimeout(() => player.seek(brokenState.position), SEEK_DELAY)
|
|
359
|
+
setTimeout(() => player.seek?.(brokenState.position), SEEK_DELAY)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (brokenState.paused) {
|
|
363
|
+
await player.pause(true)
|
|
266
364
|
}
|
|
267
|
-
if (brokenState.paused) player.pause();
|
|
268
|
-
this.emit('trackStart', player, current);
|
|
269
365
|
}
|
|
270
|
-
} catch {
|
|
271
|
-
this._brokenPlayers.delete(guildId);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
366
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
this._cleanupNode(identifier);
|
|
279
|
-
this.emit('nodeDestroy', node);
|
|
367
|
+
return player
|
|
368
|
+
} finally {
|
|
369
|
+
this._rebuildLocks.delete(lockKey)
|
|
280
370
|
}
|
|
281
371
|
}
|
|
282
372
|
|
|
283
|
-
_cleanupNode(nodeId) {
|
|
284
|
-
const node = this.nodeMap.get(nodeId);
|
|
285
|
-
if (node) {
|
|
286
|
-
node.removeAllListeners();
|
|
287
|
-
this.nodeMap.delete(nodeId);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
this._nodeStates.delete(nodeId);
|
|
291
|
-
this._failoverQueue.delete(nodeId);
|
|
292
|
-
this._lastFailoverAttempt.delete(nodeId);
|
|
293
|
-
this._invalidateCache();
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
_invalidateCache() {
|
|
297
|
-
this._leastUsedNodesCache = null;
|
|
298
|
-
this._leastUsedNodesCacheTime = 0;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
373
|
async handleNodeFailover(failedNode) {
|
|
302
|
-
if (!this.failoverOptions.enabled) return
|
|
374
|
+
if (!this.failoverOptions.enabled) return
|
|
303
375
|
|
|
304
|
-
const nodeId = failedNode.name || failedNode.host
|
|
305
|
-
const now = Date.now()
|
|
306
|
-
const nodeState = this._nodeStates.get(nodeId);
|
|
376
|
+
const nodeId = failedNode.name || failedNode.host
|
|
377
|
+
const now = Date.now()
|
|
307
378
|
|
|
308
|
-
|
|
379
|
+
const nodeState = this._nodeStates.get(nodeId)
|
|
380
|
+
if (nodeState?.failoverInProgress) return
|
|
309
381
|
|
|
310
|
-
const lastAttempt = this._lastFailoverAttempt.get(nodeId)
|
|
311
|
-
if (lastAttempt && (now - lastAttempt) < this.failoverOptions.cooldownTime) return
|
|
382
|
+
const lastAttempt = this._lastFailoverAttempt.get(nodeId)
|
|
383
|
+
if (lastAttempt && (now - lastAttempt) < this.failoverOptions.cooldownTime) return
|
|
312
384
|
|
|
313
|
-
const attempts = this._failoverQueue.get(nodeId) || 0
|
|
314
|
-
if (attempts >= this.failoverOptions.maxFailoverAttempts) return
|
|
385
|
+
const attempts = this._failoverQueue.get(nodeId) || 0
|
|
386
|
+
if (attempts >= this.failoverOptions.maxFailoverAttempts) return
|
|
315
387
|
|
|
316
|
-
this._nodeStates.set(nodeId, { connected: false, failoverInProgress: true })
|
|
317
|
-
this._lastFailoverAttempt.set(nodeId, now)
|
|
318
|
-
this._failoverQueue.set(nodeId, attempts + 1)
|
|
388
|
+
this._nodeStates.set(nodeId, { connected: false, failoverInProgress: true })
|
|
389
|
+
this._lastFailoverAttempt.set(nodeId, now)
|
|
390
|
+
this._failoverQueue.set(nodeId, attempts + 1)
|
|
319
391
|
|
|
320
392
|
try {
|
|
321
|
-
this.emit('nodeFailover', failedNode)
|
|
322
|
-
const affectedPlayers = Array.from(failedNode.players);
|
|
393
|
+
this.emit('nodeFailover', failedNode)
|
|
323
394
|
|
|
395
|
+
const affectedPlayers = Array.from(failedNode.players || [])
|
|
324
396
|
if (!affectedPlayers.length) {
|
|
325
|
-
this._nodeStates.set(nodeId, { connected: false, failoverInProgress: false })
|
|
326
|
-
return
|
|
397
|
+
this._nodeStates.set(nodeId, { connected: false, failoverInProgress: false })
|
|
398
|
+
return
|
|
327
399
|
}
|
|
328
400
|
|
|
329
|
-
const availableNodes = this._getAvailableNodes(failedNode)
|
|
330
|
-
if (!availableNodes.length)
|
|
331
|
-
this.emit('error', null, new Error('No failover nodes available'));
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
401
|
+
const availableNodes = this._getAvailableNodes(failedNode)
|
|
402
|
+
if (!availableNodes.length) throw new Error('No failover nodes available')
|
|
334
403
|
|
|
335
|
-
const results = await this._migratePlayersOptimized(affectedPlayers, availableNodes)
|
|
336
|
-
const successful = results.filter(r => r.success).length
|
|
404
|
+
const results = await this._migratePlayersOptimized(affectedPlayers, availableNodes)
|
|
405
|
+
const successful = results.filter(r => r.success).length
|
|
337
406
|
|
|
338
407
|
if (successful) {
|
|
339
|
-
this.emit('nodeFailoverComplete', failedNode, successful, results.length - successful)
|
|
408
|
+
this.emit('nodeFailoverComplete', failedNode, successful, results.length - successful)
|
|
340
409
|
}
|
|
341
410
|
} catch (error) {
|
|
342
|
-
this.emit('error', null, new Error(`Failover failed: ${error
|
|
411
|
+
this.emit('error', null, new Error(`Failover failed: ${error?.message || String(error)}`))
|
|
343
412
|
} finally {
|
|
344
|
-
this._nodeStates.set(nodeId, { connected: false, failoverInProgress: false })
|
|
413
|
+
this._nodeStates.set(nodeId, { connected: false, failoverInProgress: false })
|
|
345
414
|
}
|
|
346
415
|
}
|
|
347
416
|
|
|
348
417
|
async _migratePlayersOptimized(players, availableNodes) {
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
418
|
+
const baseLoads = new Map()
|
|
419
|
+
const assignedCounts = new Map()
|
|
420
|
+
for (const n of availableNodes) {
|
|
421
|
+
baseLoads.set(n, this._getCachedNodeLoad(n))
|
|
422
|
+
assignedCounts.set(n, 0)
|
|
423
|
+
}
|
|
424
|
+
const pickNode = () => {
|
|
425
|
+
let best = null
|
|
426
|
+
let bestScore = Infinity
|
|
427
|
+
for (const n of availableNodes) {
|
|
428
|
+
const score = baseLoads.get(n) + (assignedCounts.get(n) || 0)
|
|
429
|
+
if (score < bestScore) {
|
|
430
|
+
bestScore = score
|
|
431
|
+
best = n
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
assignedCounts.set(best, (assignedCounts.get(best) || 0) + 1)
|
|
435
|
+
return best
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const batchSize = Math.min(MAX_CONCURRENT_OPS, players.length)
|
|
439
|
+
const results = []
|
|
440
|
+
|
|
441
|
+
for (let i = 0; i < players.length; i += batchSize) {
|
|
442
|
+
const batch = players.slice(i, i + batchSize)
|
|
352
443
|
const batchResults = await Promise.allSettled(
|
|
353
|
-
batch.map(p => this._migratePlayer(p,
|
|
354
|
-
)
|
|
444
|
+
batch.map(p => this._migratePlayer(p, pickNode))
|
|
445
|
+
)
|
|
355
446
|
results.push(...batchResults.map(r => ({
|
|
356
447
|
success: r.status === 'fulfilled',
|
|
357
448
|
error: r.reason
|
|
358
|
-
})))
|
|
449
|
+
})))
|
|
359
450
|
}
|
|
360
|
-
|
|
451
|
+
|
|
452
|
+
return results
|
|
361
453
|
}
|
|
362
454
|
|
|
363
|
-
async _migratePlayer(player,
|
|
364
|
-
|
|
365
|
-
|
|
455
|
+
async _migratePlayer(player, pickNode) {
|
|
456
|
+
const playerState = this._capturePlayerState(player)
|
|
457
|
+
if (!playerState) throw new Error('Failed to capture state')
|
|
458
|
+
|
|
459
|
+
for (let retry = 0; retry < this.failoverOptions.maxRetries; retry++) {
|
|
366
460
|
try {
|
|
367
|
-
const targetNode =
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
await this._restorePlayerState(newPlayer, playerState);
|
|
373
|
-
this.emit('playerMigrated', player, newPlayer, targetNode);
|
|
374
|
-
return newPlayer;
|
|
461
|
+
const targetNode = pickNode()
|
|
462
|
+
const newPlayer = await this._createPlayerOnNode(targetNode, playerState)
|
|
463
|
+
await this._restorePlayerState(newPlayer, playerState)
|
|
464
|
+
this.emit('playerMigrated', player, newPlayer, targetNode)
|
|
465
|
+
return newPlayer
|
|
375
466
|
} catch (error) {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
await delay(this.failoverOptions.retryDelay);
|
|
467
|
+
if (retry === this.failoverOptions.maxRetries - 1) throw error
|
|
468
|
+
await delay(this.failoverOptions.retryDelay * Math.pow(1.5, retry))
|
|
379
469
|
}
|
|
380
470
|
}
|
|
381
471
|
}
|
|
382
472
|
|
|
383
473
|
_capturePlayerState(player) {
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
};
|
|
399
|
-
} catch {
|
|
400
|
-
return null;
|
|
474
|
+
if (!player) return null
|
|
475
|
+
return {
|
|
476
|
+
guildId: player.guildId,
|
|
477
|
+
textChannel: player.textChannel,
|
|
478
|
+
voiceChannel: player.voiceChannel,
|
|
479
|
+
volume: player.volume ?? 100,
|
|
480
|
+
paused: !!player.paused,
|
|
481
|
+
position: player.position || 0,
|
|
482
|
+
current: player.current || null,
|
|
483
|
+
queue: player.queue?.tracks?.slice(0, 50) || EMPTY_ARRAY,
|
|
484
|
+
repeat: player.loop,
|
|
485
|
+
shuffle: player.shuffle,
|
|
486
|
+
deaf: player.deaf ?? false,
|
|
487
|
+
connected: !!player.connected
|
|
401
488
|
}
|
|
402
489
|
}
|
|
403
490
|
|
|
@@ -408,156 +495,204 @@ class Aqua extends EventEmitter {
|
|
|
408
495
|
voiceChannel: playerState.voiceChannel,
|
|
409
496
|
defaultVolume: playerState.volume || 100,
|
|
410
497
|
deaf: playerState.deaf || false
|
|
411
|
-
})
|
|
498
|
+
})
|
|
412
499
|
}
|
|
413
500
|
|
|
414
501
|
async _restorePlayerState(newPlayer, playerState) {
|
|
415
|
-
|
|
416
|
-
|
|
502
|
+
const operations = []
|
|
503
|
+
|
|
504
|
+
if (typeof playerState.volume === 'number') {
|
|
505
|
+
if (typeof newPlayer.setVolume === 'function') {
|
|
506
|
+
operations.push(newPlayer.setVolume(playerState.volume))
|
|
507
|
+
} else {
|
|
508
|
+
newPlayer.volume = playerState.volume
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (playerState.queue?.length && newPlayer.queue?.add) {
|
|
513
|
+
newPlayer.queue.add(...playerState.queue)
|
|
514
|
+
}
|
|
417
515
|
|
|
418
516
|
if (playerState.current && this.failoverOptions.preservePosition) {
|
|
419
|
-
newPlayer.queue
|
|
517
|
+
if (newPlayer.queue?.add) {
|
|
518
|
+
newPlayer.queue.add(playerState.current, { toFront: true })
|
|
519
|
+
}
|
|
420
520
|
if (this.failoverOptions.resumePlayback) {
|
|
421
|
-
|
|
521
|
+
operations.push(newPlayer.play())
|
|
422
522
|
if (playerState.position > 0) {
|
|
423
|
-
setTimeout(() => newPlayer.seek(playerState.position), SEEK_DELAY)
|
|
523
|
+
setTimeout(() => newPlayer.seek?.(playerState.position), SEEK_DELAY)
|
|
524
|
+
}
|
|
525
|
+
if (playerState.paused) {
|
|
526
|
+
operations.push(newPlayer.pause(true))
|
|
424
527
|
}
|
|
425
|
-
if (playerState.paused) newPlayer.pause();
|
|
426
528
|
}
|
|
427
529
|
}
|
|
428
530
|
|
|
429
|
-
newPlayer
|
|
430
|
-
|
|
431
|
-
|
|
531
|
+
Object.assign(newPlayer, {
|
|
532
|
+
repeat: playerState.repeat,
|
|
533
|
+
shuffle: playerState.shuffle
|
|
534
|
+
})
|
|
432
535
|
|
|
433
|
-
|
|
434
|
-
if (!player) return;
|
|
435
|
-
player.destroy();
|
|
436
|
-
player.voiceChannel = null;
|
|
437
|
-
this.emit('playerDestroy', player);
|
|
536
|
+
await Promise.allSettled(operations)
|
|
438
537
|
}
|
|
439
538
|
|
|
440
539
|
updateVoiceState({ d, t }) {
|
|
441
|
-
if (!d
|
|
540
|
+
if (!d?.guild_id) return
|
|
541
|
+
if (t !== 'VOICE_STATE_UPDATE' && t !== 'VOICE_SERVER_UPDATE') return
|
|
442
542
|
|
|
443
|
-
const player = this.players.get(d.guild_id)
|
|
444
|
-
if (!player) return
|
|
543
|
+
const player = this.players.get(d.guild_id)
|
|
544
|
+
if (!player) return
|
|
445
545
|
|
|
446
546
|
if (t === 'VOICE_STATE_UPDATE') {
|
|
447
|
-
if (d.user_id !== this.clientId) return
|
|
448
|
-
if (!d.channel_id) return this._cleanupPlayer(player);
|
|
547
|
+
if (d.user_id !== this.clientId) return
|
|
449
548
|
|
|
450
|
-
if (
|
|
451
|
-
|
|
452
|
-
return
|
|
549
|
+
if (!d.channel_id) {
|
|
550
|
+
this.destroyPlayer(d.guild_id)
|
|
551
|
+
return
|
|
453
552
|
}
|
|
454
553
|
|
|
455
|
-
if (player.connection
|
|
456
|
-
player.connection.sessionId = d.session_id
|
|
457
|
-
|
|
554
|
+
if (player.connection) {
|
|
555
|
+
player.connection.sessionId = d.session_id
|
|
556
|
+
player.connection.setStateUpdate(d)
|
|
458
557
|
}
|
|
459
|
-
|
|
460
|
-
player.connection?.setStateUpdate(d);
|
|
461
558
|
} else {
|
|
462
|
-
player.connection?.setServerUpdate(d)
|
|
559
|
+
player.connection?.setServerUpdate(d)
|
|
463
560
|
}
|
|
464
561
|
}
|
|
465
562
|
|
|
466
563
|
fetchRegion(region) {
|
|
467
|
-
if (!region) return this.leastUsedNodes
|
|
468
|
-
|
|
469
|
-
const
|
|
470
|
-
const filtered = [];
|
|
471
|
-
|
|
564
|
+
if (!region) return this.leastUsedNodes
|
|
565
|
+
const lowerRegion = region.toLowerCase()
|
|
566
|
+
const filtered = []
|
|
472
567
|
for (const node of this.nodeMap.values()) {
|
|
473
|
-
if (node.connected && node.regions?.includes(lowerRegion))
|
|
474
|
-
filtered.push(node);
|
|
475
|
-
}
|
|
568
|
+
if (node.connected && node.regions?.includes(lowerRegion)) filtered.push(node)
|
|
476
569
|
}
|
|
477
|
-
|
|
478
|
-
return
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
_getNodeLoad(node) {
|
|
482
|
-
const stats = node?.stats?.cpu;
|
|
483
|
-
return stats ? (stats.systemLoad / stats.cores) * 100 : 0;
|
|
570
|
+
filtered.sort((a, b) => this._getCachedNodeLoad(a) - this._getCachedNodeLoad(b))
|
|
571
|
+
return Object.freeze(filtered.slice())
|
|
484
572
|
}
|
|
485
573
|
|
|
486
574
|
createConnection(options) {
|
|
487
|
-
if (!this.initiated) throw new Error('Aqua not initialized')
|
|
575
|
+
if (!this.initiated) throw new Error('Aqua not initialized')
|
|
488
576
|
|
|
489
|
-
const
|
|
490
|
-
if (
|
|
577
|
+
const existing = this.players.get(options.guildId)
|
|
578
|
+
if (existing) {
|
|
579
|
+
if (options.voiceChannel && existing.voiceChannel !== options.voiceChannel) {
|
|
580
|
+
try { existing.connect(options) } catch {}
|
|
581
|
+
}
|
|
582
|
+
return existing
|
|
583
|
+
}
|
|
491
584
|
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
this.leastUsedNodes;
|
|
585
|
+
const candidateNodes = options.region ? this.fetchRegion(options.region) : this.leastUsedNodes
|
|
586
|
+
if (!candidateNodes.length) throw new Error('No nodes available')
|
|
495
587
|
|
|
496
|
-
|
|
588
|
+
const node = this._chooseLeastBusyNode(candidateNodes)
|
|
589
|
+
if (!node) throw new Error('No suitable node found')
|
|
497
590
|
|
|
498
|
-
return this.createPlayer(
|
|
591
|
+
return this.createPlayer(node, options)
|
|
499
592
|
}
|
|
500
593
|
|
|
501
594
|
createPlayer(node, options) {
|
|
502
|
-
this.
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
this
|
|
508
|
-
|
|
595
|
+
const existing = this.players.get(options.guildId)
|
|
596
|
+
if (existing) {
|
|
597
|
+
try { existing.destroy?.() } catch {}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const player = new Player(this, node, options)
|
|
601
|
+
this.players.set(options.guildId, player)
|
|
602
|
+
node?.players?.add?.(player)
|
|
603
|
+
|
|
604
|
+
player.once('destroy', () => this._handlePlayerDestroy(player))
|
|
605
|
+
player.connect(options)
|
|
606
|
+
this.emit('playerCreate', player)
|
|
607
|
+
return player
|
|
509
608
|
}
|
|
510
609
|
|
|
511
610
|
_handlePlayerDestroy(player) {
|
|
512
|
-
const node = player.nodes
|
|
513
|
-
node?.players?.delete(player)
|
|
514
|
-
|
|
515
|
-
this.
|
|
611
|
+
const node = player.nodes
|
|
612
|
+
node?.players?.delete?.(player)
|
|
613
|
+
|
|
614
|
+
if (this.players.get(player.guildId) === player) {
|
|
615
|
+
this.players.delete(player.guildId)
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
this.emit('playerDestroy', player)
|
|
516
619
|
}
|
|
517
620
|
|
|
518
621
|
async destroyPlayer(guildId) {
|
|
519
|
-
const player = this.players.get(guildId)
|
|
520
|
-
if (!player) return
|
|
521
|
-
|
|
622
|
+
const player = this.players.get(guildId)
|
|
623
|
+
if (!player) return
|
|
522
624
|
try {
|
|
523
|
-
|
|
524
|
-
player.removeAllListeners()
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
}
|
|
625
|
+
this.players.delete(guildId)
|
|
626
|
+
player.removeAllListeners?.()
|
|
627
|
+
await player.destroy?.()
|
|
628
|
+
} finally {
|
|
629
|
+
// Cleanup is performed by _handlePlayerDestroy via 'destroy' event
|
|
630
|
+
}
|
|
529
631
|
}
|
|
530
632
|
|
|
531
633
|
async resolve({ query, source = this.defaultSearchPlatform, requester, nodes }) {
|
|
532
|
-
if (!this.initiated) throw new Error('Aqua not initialized')
|
|
634
|
+
if (!this.initiated) throw new Error('Aqua not initialized')
|
|
635
|
+
|
|
636
|
+
const requestNode = this._getRequestNode(nodes)
|
|
637
|
+
if (!requestNode) throw new Error('No nodes available')
|
|
533
638
|
|
|
534
|
-
const
|
|
535
|
-
const formattedQuery = URL_PATTERN.test(query) ?
|
|
536
|
-
query :
|
|
537
|
-
`${source}${SEARCH_PREFIX}${query}`;
|
|
639
|
+
const formattedQuery = isProbablyUrl(query) ? query : `${source}${SEARCH_PREFIX}${query}`
|
|
538
640
|
|
|
539
641
|
try {
|
|
540
|
-
const endpoint =
|
|
541
|
-
const response = await requestNode.rest.makeRequest('GET', endpoint)
|
|
642
|
+
const endpoint = `/${this.restVersion}/loadtracks?identifier=${encodeURIComponent(formattedQuery)}`
|
|
643
|
+
const response = await requestNode.rest.makeRequest('GET', endpoint)
|
|
542
644
|
|
|
543
|
-
if (
|
|
544
|
-
return EMPTY_TRACKS_RESPONSE
|
|
645
|
+
if (!response || response.loadType === 'empty' || response.loadType === 'NO_MATCHES') {
|
|
646
|
+
return EMPTY_TRACKS_RESPONSE
|
|
545
647
|
}
|
|
546
648
|
|
|
547
|
-
return this._constructResponse(response, requester, requestNode)
|
|
649
|
+
return this._constructResponse(response, requester, requestNode)
|
|
548
650
|
} catch (error) {
|
|
549
|
-
throw new Error(error
|
|
550
|
-
'Request timeout' :
|
|
551
|
-
`Resolve failed: ${error.message}`
|
|
552
|
-
);
|
|
651
|
+
throw new Error(error?.name === 'AbortError' ? 'Request timeout' : `Resolve failed: ${error?.message || String(error)}`)
|
|
553
652
|
}
|
|
554
653
|
}
|
|
555
654
|
|
|
556
655
|
_getRequestNode(nodes) {
|
|
557
|
-
if (!nodes)
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
656
|
+
if (!nodes) {
|
|
657
|
+
const chosen = this._chooseLeastBusyNode(this.leastUsedNodes)
|
|
658
|
+
if (!chosen) throw new Error('No nodes available')
|
|
659
|
+
return chosen
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (nodes instanceof Node) return nodes
|
|
663
|
+
|
|
664
|
+
if (Array.isArray(nodes)) {
|
|
665
|
+
const candidates = nodes.filter(n => n?.connected)
|
|
666
|
+
const chosen = this._chooseLeastBusyNode(candidates.length ? candidates : this.leastUsedNodes)
|
|
667
|
+
if (!chosen) throw new Error('No nodes available')
|
|
668
|
+
return chosen
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
if (typeof nodes === 'string') {
|
|
672
|
+
const node = this.nodeMap.get(nodes)
|
|
673
|
+
if (node?.connected) return node
|
|
674
|
+
const chosen = this._chooseLeastBusyNode(this.leastUsedNodes)
|
|
675
|
+
if (!chosen) throw new Error('No nodes available')
|
|
676
|
+
return chosen
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
throw new TypeError(`Invalid nodes parameter: ${typeof nodes}`)
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
_chooseLeastBusyNode(nodes) {
|
|
683
|
+
if (!nodes?.length) return null
|
|
684
|
+
if (nodes.length === 1) return nodes[0]
|
|
685
|
+
|
|
686
|
+
let best = nodes[0]
|
|
687
|
+
let bestScore = this._getCachedNodeLoad(best)
|
|
688
|
+
for (let i = 1; i < nodes.length; i++) {
|
|
689
|
+
const score = this._getCachedNodeLoad(nodes[i])
|
|
690
|
+
if (score < bestScore) {
|
|
691
|
+
bestScore = score
|
|
692
|
+
best = nodes[i]
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return best
|
|
561
696
|
}
|
|
562
697
|
|
|
563
698
|
_constructResponse(response, requester, requestNode) {
|
|
@@ -567,254 +702,297 @@ class Aqua extends EventEmitter {
|
|
|
567
702
|
playlistInfo: null,
|
|
568
703
|
pluginInfo: response.pluginInfo || {},
|
|
569
704
|
tracks: []
|
|
570
|
-
}
|
|
705
|
+
}
|
|
571
706
|
|
|
572
707
|
if (response.loadType === 'error' || response.loadType === 'LOAD_FAILED') {
|
|
573
|
-
baseResponse.exception = response.data || response.exception
|
|
574
|
-
return baseResponse
|
|
708
|
+
baseResponse.exception = response.data || response.exception
|
|
709
|
+
return baseResponse
|
|
575
710
|
}
|
|
576
711
|
|
|
577
712
|
switch (response.loadType) {
|
|
578
|
-
case 'track':
|
|
579
|
-
|
|
580
|
-
|
|
713
|
+
case 'track': {
|
|
714
|
+
const data = response.data
|
|
715
|
+
if (data) {
|
|
716
|
+
baseResponse.pluginInfo = data.info?.pluginInfo ?? baseResponse.pluginInfo
|
|
717
|
+
baseResponse.tracks.push(new Track(data, requester, requestNode))
|
|
581
718
|
}
|
|
582
|
-
break
|
|
583
|
-
|
|
719
|
+
break
|
|
720
|
+
}
|
|
584
721
|
case 'playlist': {
|
|
585
|
-
const info = response.data?.info
|
|
722
|
+
const info = response.data?.info
|
|
586
723
|
if (info) {
|
|
587
724
|
baseResponse.playlistInfo = {
|
|
588
725
|
name: info.name || info.title,
|
|
589
|
-
thumbnail: response.data.pluginInfo?.artworkUrl
|
|
590
|
-
response.data.tracks?.[0]?.info?.artworkUrl
|
|
591
|
-
null,
|
|
726
|
+
thumbnail: response.data.pluginInfo?.artworkUrl
|
|
727
|
+
|| response.data.tracks?.[0]?.info?.artworkUrl
|
|
728
|
+
|| null,
|
|
592
729
|
...info
|
|
593
|
-
}
|
|
730
|
+
}
|
|
594
731
|
}
|
|
732
|
+
baseResponse.pluginInfo = response.data?.pluginInfo ?? baseResponse.pluginInfo
|
|
595
733
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
baseResponse.tracks = tracks.map(t => new Track(t, requester, requestNode));
|
|
734
|
+
if (response.data?.tracks?.length) {
|
|
735
|
+
baseResponse.tracks = response.data.tracks.map(t => new Track(t, requester, requestNode))
|
|
599
736
|
}
|
|
600
|
-
break
|
|
737
|
+
break
|
|
601
738
|
}
|
|
602
|
-
|
|
603
739
|
case 'search': {
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
baseResponse.tracks = searchData.map(t => new Track(t, requester, requestNode));
|
|
740
|
+
if (response.data?.length) {
|
|
741
|
+
baseResponse.tracks = response.data.map(t => new Track(t, requester, requestNode))
|
|
607
742
|
}
|
|
608
|
-
break
|
|
743
|
+
break
|
|
609
744
|
}
|
|
610
745
|
}
|
|
611
746
|
|
|
612
|
-
return baseResponse
|
|
747
|
+
return baseResponse
|
|
613
748
|
}
|
|
614
749
|
|
|
615
750
|
get(guildId) {
|
|
616
|
-
const player = this.players.get(guildId)
|
|
617
|
-
if (!player) throw new Error(`Player not found: ${guildId}`)
|
|
618
|
-
return player
|
|
751
|
+
const player = this.players.get(guildId)
|
|
752
|
+
if (!player) throw new Error(`Player not found: ${guildId}`)
|
|
753
|
+
return player
|
|
619
754
|
}
|
|
620
755
|
|
|
621
756
|
async search(query, requester, source = this.defaultSearchPlatform) {
|
|
622
|
-
if (!query || !requester) return null
|
|
757
|
+
if (!query || !requester) return null
|
|
623
758
|
try {
|
|
624
|
-
const { tracks } = await this.resolve({ query, source, requester })
|
|
625
|
-
return tracks || null
|
|
759
|
+
const { tracks } = await this.resolve({ query, source, requester })
|
|
760
|
+
return tracks || null
|
|
626
761
|
} catch {
|
|
627
|
-
return null
|
|
762
|
+
return null
|
|
628
763
|
}
|
|
629
764
|
}
|
|
630
765
|
|
|
631
766
|
async loadPlayers(filePath = './AquaPlayers.jsonl') {
|
|
767
|
+
const lockFile = `${filePath}.lock`
|
|
632
768
|
try {
|
|
633
|
-
await fs.access(filePath)
|
|
634
|
-
await
|
|
635
|
-
|
|
636
|
-
|
|
769
|
+
await fs.promises.access(filePath).catch(() => null)
|
|
770
|
+
await fs.promises.writeFile(lockFile, process.pid.toString(), { flag: 'wx' }).catch(() => null)
|
|
771
|
+
|
|
772
|
+
await this._waitForFirstNode()
|
|
637
773
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
.map(line => JSON.parse(line));
|
|
774
|
+
const stream = fs.createReadStream(filePath, { encoding: 'utf8' })
|
|
775
|
+
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity })
|
|
641
776
|
|
|
642
|
-
|
|
777
|
+
const batch = []
|
|
778
|
+
for await (const line of rl) {
|
|
779
|
+
if (!line.trim()) continue
|
|
780
|
+
try { batch.push(JSON.parse(line)) } catch { continue }
|
|
781
|
+
if (batch.length >= PLAYER_BATCH_SIZE) {
|
|
782
|
+
await Promise.allSettled(batch.map(p => this._restorePlayer(p)))
|
|
783
|
+
batch.length = 0
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
if (batch.length) {
|
|
787
|
+
await Promise.allSettled(batch.map(p => this._restorePlayer(p)))
|
|
643
788
|
}
|
|
644
789
|
|
|
645
|
-
await fs.writeFile(filePath, ''
|
|
646
|
-
} catch {
|
|
790
|
+
await fs.promises.writeFile(filePath, '')
|
|
791
|
+
} catch (error) {
|
|
792
|
+
this.emit('debug', 'Aqua', `Load players error: ${error?.message || String(error)}`)
|
|
793
|
+
} finally {
|
|
794
|
+
await fs.promises.unlink(lockFile).catch(() => {})
|
|
795
|
+
}
|
|
647
796
|
}
|
|
648
797
|
|
|
649
798
|
async savePlayer(filePath = './AquaPlayers.jsonl') {
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
const targetNode = this.leastUsedNodes[0];
|
|
681
|
-
if (!targetNode) return;
|
|
682
|
-
|
|
683
|
-
player = await this.createConnection({
|
|
684
|
-
guildId: p.g,
|
|
685
|
-
textChannel: p.t,
|
|
686
|
-
voiceChannel: p.v,
|
|
687
|
-
defaultVolume: p.vol || 65,
|
|
688
|
-
deaf: true
|
|
689
|
-
});
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
if (p.u && player) {
|
|
693
|
-
const resolved = await this.resolve({
|
|
694
|
-
query: p.u,
|
|
695
|
-
requester: this._parseRequester(p.r)
|
|
696
|
-
});
|
|
697
|
-
if (resolved.tracks?.[0]) {
|
|
698
|
-
player.queue.add(resolved.tracks[0]);
|
|
699
|
-
player.position = p.p || 0;
|
|
700
|
-
player.paused = !!p.pa;
|
|
799
|
+
const lockFile = `${filePath}.lock`
|
|
800
|
+
try {
|
|
801
|
+
await fs.promises.writeFile(lockFile, process.pid.toString(), { flag: 'wx' }).catch(() => null)
|
|
802
|
+
|
|
803
|
+
const ws = fs.createWriteStream(filePath, { encoding: 'utf8', flags: 'w' })
|
|
804
|
+
const buffer = []
|
|
805
|
+
let count = 0
|
|
806
|
+
|
|
807
|
+
for (const player of this.players.values()) {
|
|
808
|
+
const requester = player.requester || player.current?.requester
|
|
809
|
+
const data = {
|
|
810
|
+
g: player.guildId,
|
|
811
|
+
t: player.textChannel,
|
|
812
|
+
v: player.voiceChannel,
|
|
813
|
+
u: player.current?.uri || null,
|
|
814
|
+
p: player.position || 0,
|
|
815
|
+
ts: player.timestamp || 0,
|
|
816
|
+
q: player.queue?.tracks?.slice(0, 10).map(tr => tr.uri) || [],
|
|
817
|
+
r: requester ? JSON.stringify({ id: requester.id, username: requester.username }) : null,
|
|
818
|
+
vol: player.volume,
|
|
819
|
+
pa: player.paused,
|
|
820
|
+
pl: player.playing,
|
|
821
|
+
nw: player.nowPlayingMessage?.id || null
|
|
822
|
+
}
|
|
823
|
+
buffer.push(JSON.stringify(data))
|
|
824
|
+
count++
|
|
825
|
+
if (buffer.length >= 100) {
|
|
826
|
+
ws.write(buffer.join('\n') + '\n')
|
|
827
|
+
buffer.length = 0
|
|
828
|
+
}
|
|
701
829
|
}
|
|
830
|
+
|
|
831
|
+
if (buffer.length) ws.write(buffer.join('\n') + '\n')
|
|
832
|
+
await new Promise(resolve => ws.end(resolve))
|
|
833
|
+
this.emit('debug', 'Aqua', `Saved ${count} players to ${filePath}`)
|
|
834
|
+
} catch (error) {
|
|
835
|
+
this.emit('error', null, new Error(`Save players failed: ${error?.message || String(error)}`))
|
|
836
|
+
} finally {
|
|
837
|
+
await fs.promises.unlink(lockFile).catch(() => {})
|
|
702
838
|
}
|
|
839
|
+
}
|
|
703
840
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
}
|
|
718
|
-
}
|
|
841
|
+
async _restorePlayer(p) {
|
|
842
|
+
try {
|
|
843
|
+
let player = this.players.get(p.g)
|
|
844
|
+
if (!player) {
|
|
845
|
+
const targetNode = this._chooseLeastBusyNode(this.leastUsedNodes)
|
|
846
|
+
if (!targetNode) return
|
|
847
|
+
player = this.createPlayer(targetNode, {
|
|
848
|
+
guildId: p.g,
|
|
849
|
+
textChannel: p.t,
|
|
850
|
+
voiceChannel: p.v,
|
|
851
|
+
defaultVolume: p.vol || 65,
|
|
852
|
+
deaf: true
|
|
853
|
+
})
|
|
719
854
|
}
|
|
720
|
-
player.nowPlayingMessage = message || null;
|
|
721
|
-
}
|
|
722
855
|
|
|
723
|
-
|
|
724
|
-
const
|
|
725
|
-
|
|
726
|
-
|
|
856
|
+
const requester = this._parseRequester(p.r)
|
|
857
|
+
const tracksToResolve = [p.u, ...(p.q || [])].filter(Boolean).slice(0, 20)
|
|
858
|
+
|
|
859
|
+
const resolved = await Promise.all(tracksToResolve.map(uri =>
|
|
860
|
+
this.resolve({ query: uri, requester }).catch(() => null)
|
|
861
|
+
))
|
|
862
|
+
const validTracks = resolved.filter(r => r?.tracks?.length).flatMap(r => r.tracks)
|
|
727
863
|
|
|
728
|
-
|
|
729
|
-
player.queue.
|
|
864
|
+
if (validTracks.length && player.queue?.add) {
|
|
865
|
+
if (player.queue.tracks?.length <= 2) player.queue.tracks = []
|
|
866
|
+
player.queue.add(...validTracks)
|
|
730
867
|
}
|
|
731
|
-
}
|
|
732
868
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
869
|
+
if (p.u && validTracks[0]) {
|
|
870
|
+
if (p.vol != null) {
|
|
871
|
+
if (typeof player.setVolume === 'function') {
|
|
872
|
+
await player.setVolume(p.vol)
|
|
873
|
+
} else {
|
|
874
|
+
player.volume = p.vol
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
await player.play()
|
|
879
|
+
if (p.p > 0) setTimeout(() => player.seek?.(p.p), SEEK_DELAY)
|
|
880
|
+
if (p.pa) await player.pause(true)
|
|
739
881
|
}
|
|
740
|
-
}
|
|
741
882
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
883
|
+
if (p.nw && p.t) {
|
|
884
|
+
const channel = this.client.channels?.cache?.get(p.t)
|
|
885
|
+
if (channel?.messages) {
|
|
886
|
+
try {
|
|
887
|
+
player.nowPlayingMessage = await channel.messages.fetch(p.nw).catch(() => null)
|
|
888
|
+
} catch {}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
} catch (error) {
|
|
892
|
+
this.emit('debug', 'Aqua', `Error restoring player for guild ${p.g}: ${error?.message || String(error)}`)
|
|
893
|
+
}
|
|
745
894
|
}
|
|
746
|
-
}
|
|
747
895
|
|
|
748
896
|
_parseRequester(requesterString) {
|
|
749
|
-
if (!requesterString) return null
|
|
750
|
-
|
|
751
|
-
|
|
897
|
+
if (!requesterString || typeof requesterString !== 'string') return null
|
|
898
|
+
try {
|
|
899
|
+
return JSON.parse(requesterString)
|
|
900
|
+
} catch {
|
|
901
|
+
const i = requesterString.indexOf(':')
|
|
902
|
+
if (i <= 0) return null
|
|
903
|
+
return { id: requesterString.substring(0, i), username: requesterString.substring(i + 1) }
|
|
904
|
+
}
|
|
752
905
|
}
|
|
753
906
|
|
|
754
|
-
async _waitForFirstNode() {
|
|
755
|
-
if (this.leastUsedNodes.length) return
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
const checkInterval = setInterval(() => {
|
|
907
|
+
async _waitForFirstNode(timeout = NODE_TIMEOUT) {
|
|
908
|
+
if (this.leastUsedNodes.length) return
|
|
909
|
+
return new Promise((resolve, reject) => {
|
|
910
|
+
const onReady = () => {
|
|
759
911
|
if (this.leastUsedNodes.length) {
|
|
760
|
-
|
|
761
|
-
|
|
912
|
+
clearTimeout(timer)
|
|
913
|
+
this.off('nodeConnect', onReady)
|
|
914
|
+
this.off('nodeCreate', onReady)
|
|
915
|
+
resolve()
|
|
762
916
|
}
|
|
763
|
-
}
|
|
764
|
-
|
|
917
|
+
}
|
|
918
|
+
const timer = setTimeout(() => {
|
|
919
|
+
this.off('nodeConnect', onReady)
|
|
920
|
+
this.off('nodeCreate', onReady)
|
|
921
|
+
reject(new Error('Timeout waiting for first node'))
|
|
922
|
+
}, timeout)
|
|
923
|
+
|
|
924
|
+
this.on('nodeConnect', onReady)
|
|
925
|
+
this.on('nodeCreate', onReady)
|
|
926
|
+
onReady()
|
|
927
|
+
})
|
|
765
928
|
}
|
|
766
929
|
|
|
767
930
|
_performCleanup() {
|
|
768
|
-
const now = Date.now()
|
|
931
|
+
const now = Date.now()
|
|
932
|
+
const expiredGuilds = []
|
|
769
933
|
|
|
770
934
|
for (const [guildId, state] of this._brokenPlayers) {
|
|
771
|
-
if (now - state.brokenAt > BROKEN_PLAYER_TTL)
|
|
772
|
-
this._brokenPlayers.delete(guildId);
|
|
773
|
-
}
|
|
935
|
+
if (now - state.brokenAt > BROKEN_PLAYER_TTL) expiredGuilds.push(guildId)
|
|
774
936
|
}
|
|
937
|
+
for (const g of expiredGuilds) this._brokenPlayers.delete(g)
|
|
775
938
|
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
939
|
+
const expiredNodes = []
|
|
940
|
+
for (const [nodeId, ts] of this._lastFailoverAttempt) {
|
|
941
|
+
if (now - ts > FAILOVER_CLEANUP_TTL) expiredNodes.push(nodeId)
|
|
942
|
+
}
|
|
943
|
+
for (const n of expiredNodes) {
|
|
944
|
+
this._lastFailoverAttempt.delete(n)
|
|
945
|
+
this._failoverQueue.delete(n)
|
|
781
946
|
}
|
|
782
947
|
|
|
783
|
-
this.
|
|
948
|
+
if (this._nodeLoadCache.size > 50) {
|
|
949
|
+
this._nodeLoadCache.clear()
|
|
950
|
+
this._nodeLoadCacheTime.clear()
|
|
951
|
+
}
|
|
784
952
|
}
|
|
785
953
|
|
|
786
954
|
_getAvailableNodes(excludeNode) {
|
|
787
|
-
const
|
|
955
|
+
const out = []
|
|
788
956
|
for (const node of this.nodeMap.values()) {
|
|
789
|
-
if (node !== excludeNode && node.connected)
|
|
790
|
-
available.push(node);
|
|
791
|
-
}
|
|
957
|
+
if (node !== excludeNode && node.connected) out.push(node)
|
|
792
958
|
}
|
|
793
|
-
return
|
|
959
|
+
return out
|
|
794
960
|
}
|
|
795
961
|
|
|
796
962
|
destroy() {
|
|
797
963
|
if (this._cleanupTimer) {
|
|
798
|
-
clearInterval(this._cleanupTimer)
|
|
799
|
-
this._cleanupTimer = null
|
|
964
|
+
clearInterval(this._cleanupTimer)
|
|
965
|
+
this._cleanupTimer = null
|
|
800
966
|
}
|
|
801
967
|
|
|
802
|
-
|
|
803
|
-
|
|
968
|
+
if (this._onNodeConnect) {
|
|
969
|
+
this.off('nodeConnect', this._onNodeConnect)
|
|
970
|
+
this.off('nodeDisconnect', this._onNodeDisconnect)
|
|
804
971
|
}
|
|
805
972
|
|
|
973
|
+
const tasks = []
|
|
974
|
+
|
|
975
|
+
for (const player of this.players.values()) {
|
|
976
|
+
player.removeAllListeners?.()
|
|
977
|
+
tasks.push(Promise.resolve(player.destroy?.()).catch(() => {}))
|
|
978
|
+
}
|
|
806
979
|
for (const node of this.nodeMap.values()) {
|
|
807
|
-
node.
|
|
980
|
+
tasks.push(Promise.resolve(node.destroy?.()).catch(() => {}))
|
|
808
981
|
}
|
|
809
982
|
|
|
810
|
-
this.
|
|
811
|
-
this.
|
|
812
|
-
this.
|
|
813
|
-
this.
|
|
814
|
-
this.
|
|
815
|
-
this.
|
|
816
|
-
this.
|
|
983
|
+
this.players.clear()
|
|
984
|
+
this.nodeMap.clear()
|
|
985
|
+
this._nodeStates.clear()
|
|
986
|
+
this._failoverQueue.clear()
|
|
987
|
+
this._lastFailoverAttempt.clear()
|
|
988
|
+
this._brokenPlayers.clear()
|
|
989
|
+
this._nodeLoadCache.clear()
|
|
990
|
+
this._nodeLoadCacheTime.clear()
|
|
991
|
+
this._leastUsedNodesCache = null
|
|
992
|
+
|
|
993
|
+
this.removeAllListeners()
|
|
994
|
+
return Promise.all(tasks)
|
|
817
995
|
}
|
|
818
996
|
}
|
|
819
997
|
|
|
820
|
-
module.exports = Aqua
|
|
998
|
+
module.exports = Aqua
|