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