aqualink 2.20.1 → 3.1.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/README.md +174 -184
- package/build/handlers/autoplay.js +5 -1
- package/build/index.d.ts +1 -1
- package/build/structures/Aqua.js +235 -540
- package/build/structures/AquaRecovery.js +905 -0
- package/build/structures/Connection.js +84 -262
- package/build/structures/ConnectionRecovery.js +425 -0
- package/build/structures/Filters.js +96 -13
- package/build/structures/Node.js +175 -72
- package/build/structures/Player.js +344 -338
- package/build/structures/PlayerLifecycle.js +584 -0
- package/build/structures/PlayerLifecycleState.js +42 -0
- package/build/structures/Queue.js +5 -1
- package/build/structures/Reporting.js +32 -0
- package/build/structures/Rest.js +51 -11
- package/build/structures/Track.js +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
const { AqualinkEvents } = require('./AqualinkEvents')
|
|
2
|
+
const { reportSuppressedError } = require('./Reporting')
|
|
3
|
+
|
|
4
|
+
class ConnectionRecovery {
|
|
5
|
+
constructor(connection, deps) {
|
|
6
|
+
this.connection = connection
|
|
7
|
+
this._functions = deps._functions
|
|
8
|
+
this.STATE = deps.STATE
|
|
9
|
+
this.RECONNECT_DELAY = deps.RECONNECT_DELAY
|
|
10
|
+
this.MAX_RECONNECT_ATTEMPTS = deps.MAX_RECONNECT_ATTEMPTS
|
|
11
|
+
this.RESUME_BACKOFF_MAX = deps.RESUME_BACKOFF_MAX
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
setServerUpdate(data) {
|
|
15
|
+
const conn = this.connection
|
|
16
|
+
if (conn._destroyed || !data?.token) return
|
|
17
|
+
|
|
18
|
+
const endpoint =
|
|
19
|
+
typeof data.endpoint === 'string' ? data.endpoint.trim() : ''
|
|
20
|
+
if (!endpoint) return
|
|
21
|
+
|
|
22
|
+
if (conn._lastEndpoint === endpoint && conn.token === data.token) return
|
|
23
|
+
if (data.txId && data.txId < conn.txId) return
|
|
24
|
+
|
|
25
|
+
conn._stateGeneration++
|
|
26
|
+
|
|
27
|
+
if (conn._lastEndpoint !== endpoint) {
|
|
28
|
+
conn.sequence = 0
|
|
29
|
+
conn._lastEndpoint = endpoint
|
|
30
|
+
conn._reconnectAttempts = 0
|
|
31
|
+
conn._consecutiveFailures = 0
|
|
32
|
+
conn._regionMigrationAttempted = false
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
conn.endpoint = endpoint
|
|
36
|
+
conn.region = this._functions.extractRegion(endpoint)
|
|
37
|
+
conn.token = data.token
|
|
38
|
+
conn.channelId = data.channel_id || conn.channelId || conn.voiceChannel
|
|
39
|
+
conn._lastVoiceDataUpdate = Date.now()
|
|
40
|
+
if (conn._aqua?.debugTrace) {
|
|
41
|
+
conn._aqua._trace('connection.serverUpdate', {
|
|
42
|
+
guildId: conn._guildId,
|
|
43
|
+
endpoint: conn.endpoint,
|
|
44
|
+
region: conn.region,
|
|
45
|
+
txId: data.txId || null
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
conn._stateFlags &= ~this.STATE.VOICE_DATA_STALE
|
|
49
|
+
|
|
50
|
+
const migrated = this.checkRegionMigration()
|
|
51
|
+
if (migrated) return
|
|
52
|
+
conn._scheduleVoiceUpdate()
|
|
53
|
+
conn._player?._flushDeferredPlay?.()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
checkRegionMigration() {
|
|
57
|
+
const conn = this.connection
|
|
58
|
+
if (conn._destroyed || conn._regionMigrationAttempted) return false
|
|
59
|
+
if (
|
|
60
|
+
!conn._aqua?.autoRegionMigrate ||
|
|
61
|
+
!conn.region ||
|
|
62
|
+
conn.region === 'unknown'
|
|
63
|
+
)
|
|
64
|
+
return false
|
|
65
|
+
|
|
66
|
+
const player = conn._player
|
|
67
|
+
if (!player || player.destroyed || player._resuming || player._reconnecting)
|
|
68
|
+
return false
|
|
69
|
+
|
|
70
|
+
const currentNode = player.nodes
|
|
71
|
+
if (!currentNode) return false
|
|
72
|
+
|
|
73
|
+
const currentRegions = Array.isArray(currentNode.regions)
|
|
74
|
+
? currentNode.regions
|
|
75
|
+
: []
|
|
76
|
+
const alreadyMatching = currentRegions.some((r) =>
|
|
77
|
+
conn._aqua._regionMatches?.(r, conn.region)
|
|
78
|
+
)
|
|
79
|
+
if (alreadyMatching) {
|
|
80
|
+
conn._regionMigrationAttempted = true
|
|
81
|
+
return false
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const targetNode = conn._aqua._findBestNodeForRegion?.(conn.region)
|
|
85
|
+
if (!targetNode || targetNode === currentNode) return false
|
|
86
|
+
|
|
87
|
+
conn._regionMigrationAttempted = true
|
|
88
|
+
if (conn._aqua?.debugTrace) {
|
|
89
|
+
conn._aqua._trace('connection.region.migrate', {
|
|
90
|
+
guildId: conn._guildId,
|
|
91
|
+
region: conn.region,
|
|
92
|
+
from: currentNode?.name || currentNode?.host,
|
|
93
|
+
to: targetNode?.name || targetNode?.host
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
queueMicrotask(() => {
|
|
98
|
+
conn._aqua
|
|
99
|
+
.movePlayerToNode?.(conn._guildId, targetNode, 'region')
|
|
100
|
+
.catch((error) => {
|
|
101
|
+
conn._regionMigrationAttempted = false
|
|
102
|
+
reportSuppressedError(
|
|
103
|
+
conn._aqua,
|
|
104
|
+
'connection.region.migrate',
|
|
105
|
+
error,
|
|
106
|
+
{
|
|
107
|
+
guildId: conn._guildId,
|
|
108
|
+
region: conn.region
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
return true
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async attemptResume() {
|
|
117
|
+
const conn = this.connection
|
|
118
|
+
if (!conn._canAttemptResumeCore()) return false
|
|
119
|
+
if (conn._aqua?.debugTrace) {
|
|
120
|
+
conn._aqua._trace('connection.resume.attempt', {
|
|
121
|
+
guildId: conn._guildId,
|
|
122
|
+
reconnectAttempts: conn._reconnectAttempts,
|
|
123
|
+
hasSessionId: !!conn.sessionId,
|
|
124
|
+
hasEndpoint: !!conn.endpoint,
|
|
125
|
+
hasToken: !!conn.token
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const currentGen = conn._stateGeneration
|
|
130
|
+
if (
|
|
131
|
+
!conn.sessionId ||
|
|
132
|
+
!conn.endpoint ||
|
|
133
|
+
!conn.token ||
|
|
134
|
+
conn._stateFlags & this.STATE.VOICE_DATA_STALE
|
|
135
|
+
) {
|
|
136
|
+
const now = Date.now()
|
|
137
|
+
if (now - (conn._lastResumeBlockedLogAt || 0) >= 5000) {
|
|
138
|
+
conn._lastResumeBlockedLogAt = now
|
|
139
|
+
conn._aqua.emit(
|
|
140
|
+
AqualinkEvents.Debug,
|
|
141
|
+
`Resume blocked: missing voice data for guild ${conn._guildId}, requesting voice state`
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
conn._requestVoiceState()
|
|
145
|
+
return false
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
conn.txId = conn._player.txId || conn.txId
|
|
149
|
+
conn._stateFlags |= this.STATE.ATTEMPTING_RESUME
|
|
150
|
+
conn._reconnectAttempts++
|
|
151
|
+
conn._aqua.emit(
|
|
152
|
+
AqualinkEvents.Debug,
|
|
153
|
+
`Attempt resume: guild=${conn._guildId} endpoint=${conn.endpoint} session=${conn.sessionId}`
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
const payload = sharedPool.acquire()
|
|
157
|
+
try {
|
|
158
|
+
this._functions.fillVoicePayload(
|
|
159
|
+
payload,
|
|
160
|
+
conn._guildId,
|
|
161
|
+
conn,
|
|
162
|
+
conn._player,
|
|
163
|
+
true
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
if (conn._destroyed || !conn._player || conn._player.destroyed) {
|
|
167
|
+
conn._aqua.emit(
|
|
168
|
+
AqualinkEvents.Debug,
|
|
169
|
+
`Resume aborted: player destroyed during attempt for guild ${conn._guildId}`
|
|
170
|
+
)
|
|
171
|
+
return false
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (conn._stateGeneration !== currentGen) {
|
|
175
|
+
conn._aqua.emit(
|
|
176
|
+
AqualinkEvents.Debug,
|
|
177
|
+
`Resume aborted: State changed during attempt for guild ${conn._guildId}`
|
|
178
|
+
)
|
|
179
|
+
return false
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
await this.sendUpdate(payload)
|
|
183
|
+
if (conn._aqua?.debugTrace) {
|
|
184
|
+
conn._aqua._trace('connection.resume.success', {
|
|
185
|
+
guildId: conn._guildId
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (conn._destroyed || conn._player?.destroyed) {
|
|
190
|
+
return false
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
conn._reconnectAttempts = 0
|
|
194
|
+
conn._consecutiveFailures = 0
|
|
195
|
+
if (conn._player) conn._player._resuming = false
|
|
196
|
+
|
|
197
|
+
conn._aqua.emit(
|
|
198
|
+
AqualinkEvents.Debug,
|
|
199
|
+
`Resume PATCH sent for guild ${conn._guildId}`
|
|
200
|
+
)
|
|
201
|
+
return true
|
|
202
|
+
} catch (error) {
|
|
203
|
+
if (conn._destroyed || !conn._aqua) throw error
|
|
204
|
+
if (conn._player?.destroyed) {
|
|
205
|
+
conn._aqua.emit(
|
|
206
|
+
AqualinkEvents.Debug,
|
|
207
|
+
`Resume aborted: player destroyed during retry for guild ${conn._guildId}`
|
|
208
|
+
)
|
|
209
|
+
return false
|
|
210
|
+
}
|
|
211
|
+
conn._consecutiveFailures++
|
|
212
|
+
conn._aqua.emit(
|
|
213
|
+
AqualinkEvents.Debug,
|
|
214
|
+
`Resume failed for guild ${conn._guildId} (sessionId=${conn.sessionId || 'none'}, endpoint=${conn.endpoint || 'none'}): ${error?.message || error}`
|
|
215
|
+
)
|
|
216
|
+
if (conn._aqua?.debugTrace) {
|
|
217
|
+
conn._aqua._trace('connection.resume.error', {
|
|
218
|
+
guildId: conn._guildId,
|
|
219
|
+
error: error?.message || String(error)
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (
|
|
224
|
+
conn._reconnectAttempts < this.MAX_RECONNECT_ATTEMPTS &&
|
|
225
|
+
!conn._destroyed &&
|
|
226
|
+
conn._consecutiveFailures < 5 &&
|
|
227
|
+
!conn._player?.destroyed
|
|
228
|
+
) {
|
|
229
|
+
const delay = Math.min(
|
|
230
|
+
this.RECONNECT_DELAY * (1 << (conn._reconnectAttempts - 1)),
|
|
231
|
+
this.RESUME_BACKOFF_MAX
|
|
232
|
+
)
|
|
233
|
+
conn._setReconnectTimer(delay)
|
|
234
|
+
} else {
|
|
235
|
+
conn._aqua.emit(
|
|
236
|
+
AqualinkEvents.Debug,
|
|
237
|
+
`Max reconnect attempts/failures reached for guild ${conn._guildId}`
|
|
238
|
+
)
|
|
239
|
+
if (conn._player) conn._player._resuming = false
|
|
240
|
+
conn._handleDisconnect()
|
|
241
|
+
}
|
|
242
|
+
return false
|
|
243
|
+
} finally {
|
|
244
|
+
conn._stateFlags &= ~this.STATE.ATTEMPTING_RESUME
|
|
245
|
+
sharedPool.release(payload)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async recoverMissingPlayer(isSessionError) {
|
|
250
|
+
const conn = this.connection
|
|
251
|
+
if (conn._destroyed || !conn._player || conn._missingPlayerRecovering)
|
|
252
|
+
return false
|
|
253
|
+
|
|
254
|
+
const now = Date.now()
|
|
255
|
+
if (now - conn._lastMissingPlayerRecoverAt < 5000) return false
|
|
256
|
+
|
|
257
|
+
conn._missingPlayerRecovering = true
|
|
258
|
+
conn._lastMissingPlayerRecoverAt = now
|
|
259
|
+
if (conn._aqua?.debugTrace) {
|
|
260
|
+
conn._aqua._trace('connection.playerMissing.recover.start', {
|
|
261
|
+
guildId: conn._guildId,
|
|
262
|
+
isSessionError: !!isSessionError
|
|
263
|
+
})
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
const recoveryToken = conn._player._claimVoiceRecovery?.(
|
|
268
|
+
'missing_player_recover'
|
|
269
|
+
)
|
|
270
|
+
if (isSessionError && conn._player?.nodes?._clearSession) {
|
|
271
|
+
conn._player.nodes._clearSession()
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (conn._player?._isVoiceRecoveryActive?.(recoveryToken))
|
|
275
|
+
conn._requestVoiceState()
|
|
276
|
+
const resumed = await this.attemptResume().catch((error) => {
|
|
277
|
+
reportSuppressedError(
|
|
278
|
+
conn._aqua,
|
|
279
|
+
'connection.playerMissing.resume',
|
|
280
|
+
error,
|
|
281
|
+
{
|
|
282
|
+
guildId: conn._guildId
|
|
283
|
+
}
|
|
284
|
+
)
|
|
285
|
+
return false
|
|
286
|
+
})
|
|
287
|
+
if (resumed) {
|
|
288
|
+
conn._player?._clearVoiceRecovery?.(
|
|
289
|
+
recoveryToken,
|
|
290
|
+
'missing_player_resumed'
|
|
291
|
+
)
|
|
292
|
+
} else if (conn._player?._isVoiceRecoveryActive?.(recoveryToken)) {
|
|
293
|
+
conn.resendVoiceUpdate(true)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (conn._player.playing && conn._player.current?.track) {
|
|
297
|
+
const data = {
|
|
298
|
+
track: { encoded: conn._player.current.track },
|
|
299
|
+
paused: !!conn._player.paused
|
|
300
|
+
}
|
|
301
|
+
if (conn._player.position > 0) data.position = conn._player.position
|
|
302
|
+
await conn._rest.updatePlayer({
|
|
303
|
+
guildId: conn._guildId,
|
|
304
|
+
data,
|
|
305
|
+
noReplace: false
|
|
306
|
+
})
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (conn._aqua?.debugTrace) {
|
|
310
|
+
conn._aqua._trace('connection.playerMissing.recover.ok', {
|
|
311
|
+
guildId: conn._guildId,
|
|
312
|
+
resumed: !!resumed,
|
|
313
|
+
playing: !!conn._player.playing
|
|
314
|
+
})
|
|
315
|
+
}
|
|
316
|
+
return true
|
|
317
|
+
} catch (error) {
|
|
318
|
+
if (conn._aqua?.debugTrace) {
|
|
319
|
+
conn._aqua._trace('connection.playerMissing.recover.error', {
|
|
320
|
+
guildId: conn._guildId,
|
|
321
|
+
error: error?.message || String(error)
|
|
322
|
+
})
|
|
323
|
+
}
|
|
324
|
+
return false
|
|
325
|
+
} finally {
|
|
326
|
+
conn._missingPlayerRecovering = false
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async sendUpdate(payload) {
|
|
331
|
+
const conn = this.connection
|
|
332
|
+
if (conn._destroyed)
|
|
333
|
+
throw new Error(`Connection destroyed (guild=${conn._guildId})`)
|
|
334
|
+
if (!conn._rest)
|
|
335
|
+
throw new Error(
|
|
336
|
+
`REST interface unavailable (guild=${conn._guildId}, sessionId=${conn.sessionId || 'none'})`
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
try {
|
|
340
|
+
if (conn._aqua?.debugTrace) {
|
|
341
|
+
conn._aqua._trace('connection.update.send', {
|
|
342
|
+
guildId: conn._guildId,
|
|
343
|
+
hasSessionId: !!conn._rest?.sessionId,
|
|
344
|
+
hasVoice:
|
|
345
|
+
!!payload?.data?.voice?.sessionId &&
|
|
346
|
+
!!payload?.data?.voice?.endpoint
|
|
347
|
+
})
|
|
348
|
+
}
|
|
349
|
+
await conn._rest.updatePlayer(payload)
|
|
350
|
+
if (conn._aqua?.debugTrace) {
|
|
351
|
+
conn._aqua._trace('connection.update.ok', {
|
|
352
|
+
guildId: conn._guildId
|
|
353
|
+
})
|
|
354
|
+
}
|
|
355
|
+
} catch (error) {
|
|
356
|
+
if (conn._aqua?.debugTrace) {
|
|
357
|
+
conn._aqua._trace('connection.update.error', {
|
|
358
|
+
guildId: conn._guildId,
|
|
359
|
+
statusCode: error?.statusCode || error?.response?.statusCode || null,
|
|
360
|
+
error: error?.message || String(error)
|
|
361
|
+
})
|
|
362
|
+
}
|
|
363
|
+
if (error.statusCode === 404 || error.response?.statusCode === 404) {
|
|
364
|
+
const isSessionError =
|
|
365
|
+
error.body?.message?.includes('sessionId') || false
|
|
366
|
+
const recovered = await this.recoverMissingPlayer(isSessionError)
|
|
367
|
+
if (recovered) return
|
|
368
|
+
|
|
369
|
+
if (conn._aqua) {
|
|
370
|
+
conn._aqua.emit(
|
|
371
|
+
AqualinkEvents.Debug,
|
|
372
|
+
`[Aqua/Connection] Player ${conn._guildId} not found (404, sessionId=${conn.sessionId || 'none'}, endpoint=${conn.endpoint || 'none'})${isSessionError ? ' - Session invalid' : ''}. Recovery failed, destroying.`
|
|
373
|
+
)
|
|
374
|
+
await conn._aqua.destroyPlayer(conn._guildId)
|
|
375
|
+
}
|
|
376
|
+
throw error
|
|
377
|
+
}
|
|
378
|
+
if (!this._functions.isNetworkError(error)) {
|
|
379
|
+
conn._aqua.emit(
|
|
380
|
+
AqualinkEvents.Debug,
|
|
381
|
+
new Error(`Voice update failed: ${error?.message || error}`)
|
|
382
|
+
)
|
|
383
|
+
}
|
|
384
|
+
throw error
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
class PayloadPool {
|
|
390
|
+
constructor() {
|
|
391
|
+
this._pool = []
|
|
392
|
+
this._size = 0
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
_create() {
|
|
396
|
+
return {
|
|
397
|
+
guildId: null,
|
|
398
|
+
data: {
|
|
399
|
+
voice: {
|
|
400
|
+
token: null,
|
|
401
|
+
endpoint: null,
|
|
402
|
+
sessionId: null
|
|
403
|
+
},
|
|
404
|
+
volume: null
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
acquire() {
|
|
410
|
+
return this._size > 0 ? this._pool[--this._size] : this._create()
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
release(payload) {
|
|
414
|
+
if (!payload || this._size >= 12) return
|
|
415
|
+
payload.guildId = null
|
|
416
|
+
const v = payload.data.voice
|
|
417
|
+
v.token = v.endpoint = v.sessionId = null
|
|
418
|
+
payload.data.volume = null
|
|
419
|
+
this._pool[this._size++] = payload
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const sharedPool = new PayloadPool()
|
|
424
|
+
|
|
425
|
+
module.exports = ConnectionRecovery
|
|
@@ -27,6 +27,7 @@ const FILTER_DEFAULTS = Object.freeze({
|
|
|
27
27
|
}),
|
|
28
28
|
lowPass: Object.freeze({ smoothing: 20 })
|
|
29
29
|
})
|
|
30
|
+
const { reportSuppressedError } = require('./Reporting')
|
|
30
31
|
|
|
31
32
|
const FILTER_KEYS = Object.freeze(
|
|
32
33
|
Object.fromEntries(
|
|
@@ -38,6 +39,7 @@ const FILTER_KEYS = Object.freeze(
|
|
|
38
39
|
)
|
|
39
40
|
|
|
40
41
|
const EMPTY_ARRAY = Object.freeze([])
|
|
42
|
+
const EMPTY_OBJECT = Object.freeze({})
|
|
41
43
|
|
|
42
44
|
const FILTER_POOL_SIZE = 16
|
|
43
45
|
const filterPool = {
|
|
@@ -88,6 +90,27 @@ const _utils = Object.freeze({
|
|
|
88
90
|
return !arr || arr.length === 0
|
|
89
91
|
},
|
|
90
92
|
|
|
93
|
+
objectEqual(a, b) {
|
|
94
|
+
if (a === b) return true
|
|
95
|
+
const keysA = a ? Object.keys(a) : EMPTY_ARRAY
|
|
96
|
+
const keysB = b ? Object.keys(b) : EMPTY_ARRAY
|
|
97
|
+
if (keysA.length !== keysB.length) return false
|
|
98
|
+
for (let i = 0; i < keysA.length; i++) {
|
|
99
|
+
const key = keysA[i]
|
|
100
|
+
if (!(key in b) || a[key] !== b[key]) return false
|
|
101
|
+
}
|
|
102
|
+
return true
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
normalizePluginFilters(filters) {
|
|
106
|
+
if (!filters || typeof filters !== 'object') return null
|
|
107
|
+
const entries = Object.entries(filters).filter(
|
|
108
|
+
([key, value]) => key && value && typeof value === 'object'
|
|
109
|
+
)
|
|
110
|
+
if (!entries.length) return null
|
|
111
|
+
return Object.fromEntries(entries)
|
|
112
|
+
},
|
|
113
|
+
|
|
91
114
|
makeEqArray(len, gain) {
|
|
92
115
|
const out = new Array(len)
|
|
93
116
|
for (let i = 0; i < len; i++) out[i] = { band: i, gain }
|
|
@@ -125,7 +148,8 @@ class Filters {
|
|
|
125
148
|
rotation: options.rotation ?? null,
|
|
126
149
|
distortion: options.distortion ?? null,
|
|
127
150
|
channelMix: options.channelMix ?? null,
|
|
128
|
-
lowPass: options.lowPass ?? null
|
|
151
|
+
lowPass: options.lowPass ?? null,
|
|
152
|
+
pluginFilters: _utils.normalizePluginFilters(options.pluginFilters)
|
|
129
153
|
}
|
|
130
154
|
|
|
131
155
|
this.presets = {
|
|
@@ -140,6 +164,7 @@ class Filters {
|
|
|
140
164
|
destroy() {
|
|
141
165
|
for (const [key, value] of Object.entries(this.filters)) {
|
|
142
166
|
if (value && typeof value === 'object' && key !== 'equalizer') {
|
|
167
|
+
if (key === 'pluginFilters') continue
|
|
143
168
|
filterPool.release(key, value)
|
|
144
169
|
}
|
|
145
170
|
}
|
|
@@ -177,12 +202,17 @@ class Filters {
|
|
|
177
202
|
}
|
|
178
203
|
|
|
179
204
|
_scheduleUpdate() {
|
|
180
|
-
if (this._pendingUpdate || !this.player
|
|
205
|
+
if (this._pendingUpdate || !this.player || this.player.destroyed)
|
|
206
|
+
return this
|
|
181
207
|
this._pendingUpdate = true
|
|
182
208
|
queueMicrotask(() => {
|
|
183
209
|
this._pendingUpdate = false
|
|
184
210
|
if (this.player) {
|
|
185
|
-
this.updateFilters().catch(() =>
|
|
211
|
+
this.updateFilters().catch((error) =>
|
|
212
|
+
reportSuppressedError(this.player, 'filters.update', error, {
|
|
213
|
+
guildId: this.player.guildId
|
|
214
|
+
})
|
|
215
|
+
)
|
|
186
216
|
}
|
|
187
217
|
})
|
|
188
218
|
return this
|
|
@@ -221,6 +251,37 @@ class Filters {
|
|
|
221
251
|
return this._setFilter('lowPass', enabled, options)
|
|
222
252
|
}
|
|
223
253
|
|
|
254
|
+
setPluginFilters(filters) {
|
|
255
|
+
const next = _utils.normalizePluginFilters(filters)
|
|
256
|
+
if (_utils.objectEqual(this.filters.pluginFilters, next)) return this
|
|
257
|
+
this.filters.pluginFilters = next
|
|
258
|
+
this._dirty.add('pluginFilters')
|
|
259
|
+
return this._scheduleUpdate()
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
setPluginFilter(name, config) {
|
|
263
|
+
if (!name || typeof name !== 'string')
|
|
264
|
+
throw new TypeError('Plugin filter name is required')
|
|
265
|
+
|
|
266
|
+
if (!config || typeof config !== 'object') {
|
|
267
|
+
if (!this.filters.pluginFilters?.[name]) return this
|
|
268
|
+
const next = { ...this.filters.pluginFilters }
|
|
269
|
+
delete next[name]
|
|
270
|
+
return this.setPluginFilters(next)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const current = this.filters.pluginFilters || EMPTY_OBJECT
|
|
274
|
+
if (current[name] === config) return this
|
|
275
|
+
return this.setPluginFilters({ ...current, [name]: config })
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
clearPluginFilters() {
|
|
279
|
+
if (!this.filters.pluginFilters) return this
|
|
280
|
+
this.filters.pluginFilters = null
|
|
281
|
+
this._dirty.add('pluginFilters')
|
|
282
|
+
return this._scheduleUpdate()
|
|
283
|
+
}
|
|
284
|
+
|
|
224
285
|
setBassboost(enabled, options = {}) {
|
|
225
286
|
if (!enabled) {
|
|
226
287
|
if (
|
|
@@ -317,12 +378,19 @@ class Filters {
|
|
|
317
378
|
for (let i = 0; i < filterNames.length; i++) {
|
|
318
379
|
const key = filterNames[i]
|
|
319
380
|
if (f[key] !== null) {
|
|
381
|
+
if (key !== 'pluginFilters') filterPool.release(key, f[key])
|
|
320
382
|
f[key] = null
|
|
321
383
|
this._dirty.add(key)
|
|
322
384
|
changed = true
|
|
323
385
|
}
|
|
324
386
|
}
|
|
325
387
|
|
|
388
|
+
if (f.pluginFilters !== null) {
|
|
389
|
+
f.pluginFilters = null
|
|
390
|
+
this._dirty.add('pluginFilters')
|
|
391
|
+
changed = true
|
|
392
|
+
}
|
|
393
|
+
|
|
326
394
|
for (const key in this.presets) {
|
|
327
395
|
if (this.presets[key] !== null) this.presets[key] = null
|
|
328
396
|
}
|
|
@@ -331,19 +399,34 @@ class Filters {
|
|
|
331
399
|
}
|
|
332
400
|
|
|
333
401
|
async updateFilters() {
|
|
402
|
+
this._pendingUpdate = false
|
|
334
403
|
if (!this.player || !this._dirty.size) return this
|
|
335
404
|
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
405
|
+
const dirtyKeys = [...this._dirty]
|
|
406
|
+
const dirtySet = new Set(dirtyKeys)
|
|
407
|
+
const payload = {
|
|
408
|
+
volume: this.filters.volume,
|
|
409
|
+
equalizer: this.filters.equalizer
|
|
410
|
+
}
|
|
411
|
+
const filterNames = Object.keys(FILTER_DEFAULTS)
|
|
412
|
+
for (let i = 0; i < filterNames.length; i++) {
|
|
413
|
+
const key = filterNames[i]
|
|
414
|
+
if (this.filters[key] !== null) payload[key] = this.filters[key]
|
|
415
|
+
else if (dirtySet.has(key)) payload[key] = null
|
|
416
|
+
}
|
|
417
|
+
payload.pluginFilters = this.filters.pluginFilters || {}
|
|
418
|
+
|
|
419
|
+
for (const key of dirtyKeys) this._dirty.delete(key)
|
|
420
|
+
|
|
421
|
+
try {
|
|
422
|
+
await this.player.nodes.rest.updatePlayer({
|
|
423
|
+
guildId: this.player.guildId,
|
|
424
|
+
data: { filters: payload }
|
|
425
|
+
})
|
|
426
|
+
} catch (error) {
|
|
427
|
+
for (const key of dirtyKeys) this._dirty.add(key)
|
|
428
|
+
throw error
|
|
339
429
|
}
|
|
340
|
-
|
|
341
|
-
this._dirty.clear()
|
|
342
|
-
|
|
343
|
-
await this.player.nodes.rest.updatePlayer({
|
|
344
|
-
guildId: this.player.guildId,
|
|
345
|
-
data: { filters: payload }
|
|
346
|
-
})
|
|
347
430
|
return this
|
|
348
431
|
}
|
|
349
432
|
}
|