aqualink 2.20.0 → 3.0.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/structures/Aqua.js +226 -539
- package/build/structures/AquaRecovery.js +901 -0
- package/build/structures/Connection.js +72 -261
- package/build/structures/ConnectionRecovery.js +398 -0
- package/build/structures/Filters.js +93 -12
- package/build/structures/Node.js +93 -54
- package/build/structures/Player.js +284 -337
- package/build/structures/PlayerLifecycle.js +575 -0
- package/build/structures/PlayerLifecycleState.js +42 -0
- package/build/structures/Reporting.js +32 -0
- package/build/structures/Rest.js +25 -2
- package/build/structures/Track.js +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,398 @@
|
|
|
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._stateGeneration !== currentGen) {
|
|
167
|
+
conn._aqua.emit(
|
|
168
|
+
AqualinkEvents.Debug,
|
|
169
|
+
`Resume aborted: State changed during attempt for guild ${conn._guildId}`
|
|
170
|
+
)
|
|
171
|
+
return false
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
await this.sendUpdate(payload)
|
|
175
|
+
if (conn._aqua?.debugTrace) {
|
|
176
|
+
conn._aqua._trace('connection.resume.success', {
|
|
177
|
+
guildId: conn._guildId
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
conn._reconnectAttempts = 0
|
|
182
|
+
conn._consecutiveFailures = 0
|
|
183
|
+
if (conn._player) conn._player._resuming = false
|
|
184
|
+
|
|
185
|
+
conn._aqua.emit(
|
|
186
|
+
AqualinkEvents.Debug,
|
|
187
|
+
`Resume PATCH sent for guild ${conn._guildId}`
|
|
188
|
+
)
|
|
189
|
+
return true
|
|
190
|
+
} catch (error) {
|
|
191
|
+
if (conn._destroyed || !conn._aqua) throw error
|
|
192
|
+
conn._consecutiveFailures++
|
|
193
|
+
conn._aqua.emit(
|
|
194
|
+
AqualinkEvents.Debug,
|
|
195
|
+
`Resume failed for guild ${conn._guildId}: ${error?.message || error}`
|
|
196
|
+
)
|
|
197
|
+
if (conn._aqua?.debugTrace) {
|
|
198
|
+
conn._aqua._trace('connection.resume.error', {
|
|
199
|
+
guildId: conn._guildId,
|
|
200
|
+
error: error?.message || String(error)
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (
|
|
205
|
+
conn._reconnectAttempts < this.MAX_RECONNECT_ATTEMPTS &&
|
|
206
|
+
!conn._destroyed &&
|
|
207
|
+
conn._consecutiveFailures < 5
|
|
208
|
+
) {
|
|
209
|
+
const delay = Math.min(
|
|
210
|
+
this.RECONNECT_DELAY * (1 << (conn._reconnectAttempts - 1)),
|
|
211
|
+
this.RESUME_BACKOFF_MAX
|
|
212
|
+
)
|
|
213
|
+
conn._setReconnectTimer(delay)
|
|
214
|
+
} else {
|
|
215
|
+
conn._aqua.emit(
|
|
216
|
+
AqualinkEvents.Debug,
|
|
217
|
+
`Max reconnect attempts/failures reached for guild ${conn._guildId}`
|
|
218
|
+
)
|
|
219
|
+
if (conn._player) conn._player._resuming = false
|
|
220
|
+
conn._handleDisconnect()
|
|
221
|
+
}
|
|
222
|
+
return false
|
|
223
|
+
} finally {
|
|
224
|
+
conn._stateFlags &= ~this.STATE.ATTEMPTING_RESUME
|
|
225
|
+
sharedPool.release(payload)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async recoverMissingPlayer(isSessionError) {
|
|
230
|
+
const conn = this.connection
|
|
231
|
+
if (conn._destroyed || !conn._player || conn._missingPlayerRecovering)
|
|
232
|
+
return false
|
|
233
|
+
|
|
234
|
+
const now = Date.now()
|
|
235
|
+
if (now - conn._lastMissingPlayerRecoverAt < 5000) return false
|
|
236
|
+
|
|
237
|
+
conn._missingPlayerRecovering = true
|
|
238
|
+
conn._lastMissingPlayerRecoverAt = now
|
|
239
|
+
if (conn._aqua?.debugTrace) {
|
|
240
|
+
conn._aqua._trace('connection.playerMissing.recover.start', {
|
|
241
|
+
guildId: conn._guildId,
|
|
242
|
+
isSessionError: !!isSessionError
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
const recoveryToken = conn._player._claimVoiceRecovery?.(
|
|
248
|
+
'missing_player_recover'
|
|
249
|
+
)
|
|
250
|
+
if (isSessionError && conn._player?.nodes?._clearSession) {
|
|
251
|
+
conn._player.nodes._clearSession()
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (conn._player?._isVoiceRecoveryActive?.(recoveryToken))
|
|
255
|
+
conn._requestVoiceState()
|
|
256
|
+
const resumed = await this.attemptResume().catch((error) => {
|
|
257
|
+
reportSuppressedError(
|
|
258
|
+
conn._aqua,
|
|
259
|
+
'connection.playerMissing.resume',
|
|
260
|
+
error,
|
|
261
|
+
{
|
|
262
|
+
guildId: conn._guildId
|
|
263
|
+
}
|
|
264
|
+
)
|
|
265
|
+
return false
|
|
266
|
+
})
|
|
267
|
+
if (resumed) {
|
|
268
|
+
conn._player?._clearVoiceRecovery?.(recoveryToken, 'missing_player_resumed')
|
|
269
|
+
} else if (conn._player?._isVoiceRecoveryActive?.(recoveryToken)) {
|
|
270
|
+
conn.resendVoiceUpdate(true)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (conn._player.playing && conn._player.current?.track) {
|
|
274
|
+
const data = {
|
|
275
|
+
track: { encoded: conn._player.current.track },
|
|
276
|
+
paused: !!conn._player.paused
|
|
277
|
+
}
|
|
278
|
+
if (conn._player.position > 0) data.position = conn._player.position
|
|
279
|
+
await conn._rest.updatePlayer({
|
|
280
|
+
guildId: conn._guildId,
|
|
281
|
+
data,
|
|
282
|
+
noReplace: false
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (conn._aqua?.debugTrace) {
|
|
287
|
+
conn._aqua._trace('connection.playerMissing.recover.ok', {
|
|
288
|
+
guildId: conn._guildId,
|
|
289
|
+
resumed: !!resumed,
|
|
290
|
+
playing: !!conn._player.playing
|
|
291
|
+
})
|
|
292
|
+
}
|
|
293
|
+
return true
|
|
294
|
+
} catch (error) {
|
|
295
|
+
if (conn._aqua?.debugTrace) {
|
|
296
|
+
conn._aqua._trace('connection.playerMissing.recover.error', {
|
|
297
|
+
guildId: conn._guildId,
|
|
298
|
+
error: error?.message || String(error)
|
|
299
|
+
})
|
|
300
|
+
}
|
|
301
|
+
return false
|
|
302
|
+
} finally {
|
|
303
|
+
conn._missingPlayerRecovering = false
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async sendUpdate(payload) {
|
|
308
|
+
const conn = this.connection
|
|
309
|
+
if (conn._destroyed) throw new Error('Connection destroyed')
|
|
310
|
+
if (!conn._rest) throw new Error('REST interface unavailable')
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
if (conn._aqua?.debugTrace) {
|
|
314
|
+
conn._aqua._trace('connection.update.send', {
|
|
315
|
+
guildId: conn._guildId,
|
|
316
|
+
hasSessionId: !!conn._rest?.sessionId,
|
|
317
|
+
hasVoice:
|
|
318
|
+
!!payload?.data?.voice?.sessionId &&
|
|
319
|
+
!!payload?.data?.voice?.endpoint
|
|
320
|
+
})
|
|
321
|
+
}
|
|
322
|
+
await conn._rest.updatePlayer(payload)
|
|
323
|
+
if (conn._aqua?.debugTrace) {
|
|
324
|
+
conn._aqua._trace('connection.update.ok', {
|
|
325
|
+
guildId: conn._guildId
|
|
326
|
+
})
|
|
327
|
+
}
|
|
328
|
+
} catch (error) {
|
|
329
|
+
if (conn._aqua?.debugTrace) {
|
|
330
|
+
conn._aqua._trace('connection.update.error', {
|
|
331
|
+
guildId: conn._guildId,
|
|
332
|
+
statusCode: error?.statusCode || error?.response?.statusCode || null,
|
|
333
|
+
error: error?.message || String(error)
|
|
334
|
+
})
|
|
335
|
+
}
|
|
336
|
+
if (error.statusCode === 404 || error.response?.statusCode === 404) {
|
|
337
|
+
const isSessionError =
|
|
338
|
+
error.body?.message?.includes('sessionId') || false
|
|
339
|
+
const recovered = await this.recoverMissingPlayer(isSessionError)
|
|
340
|
+
if (recovered) return
|
|
341
|
+
|
|
342
|
+
if (conn._aqua) {
|
|
343
|
+
conn._aqua.emit(
|
|
344
|
+
AqualinkEvents.Debug,
|
|
345
|
+
`[Aqua/Connection] Player ${conn._guildId} not found (404)${isSessionError ? ' - Session invalid' : ''}. Recovery failed, destroying.`
|
|
346
|
+
)
|
|
347
|
+
await conn._aqua.destroyPlayer(conn._guildId)
|
|
348
|
+
}
|
|
349
|
+
throw error
|
|
350
|
+
}
|
|
351
|
+
if (!this._functions.isNetworkError(error)) {
|
|
352
|
+
conn._aqua.emit(
|
|
353
|
+
AqualinkEvents.Debug,
|
|
354
|
+
new Error(`Voice update failed: ${error?.message || error}`)
|
|
355
|
+
)
|
|
356
|
+
}
|
|
357
|
+
throw error
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
class PayloadPool {
|
|
363
|
+
constructor() {
|
|
364
|
+
this._pool = []
|
|
365
|
+
this._size = 0
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
_create() {
|
|
369
|
+
return {
|
|
370
|
+
guildId: null,
|
|
371
|
+
data: {
|
|
372
|
+
voice: {
|
|
373
|
+
token: null,
|
|
374
|
+
endpoint: null,
|
|
375
|
+
sessionId: null
|
|
376
|
+
},
|
|
377
|
+
volume: null
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
acquire() {
|
|
383
|
+
return this._size > 0 ? this._pool[--this._size] : this._create()
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
release(payload) {
|
|
387
|
+
if (!payload || this._size >= 12) return
|
|
388
|
+
payload.guildId = null
|
|
389
|
+
const v = payload.data.voice
|
|
390
|
+
v.token = v.endpoint = v.sessionId = null
|
|
391
|
+
payload.data.volume = null
|
|
392
|
+
this._pool[this._size++] = payload
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const sharedPool = new PayloadPool()
|
|
397
|
+
|
|
398
|
+
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
|
}
|
|
@@ -182,7 +207,11 @@ class Filters {
|
|
|
182
207
|
queueMicrotask(() => {
|
|
183
208
|
this._pendingUpdate = false
|
|
184
209
|
if (this.player) {
|
|
185
|
-
this.updateFilters().catch(() =>
|
|
210
|
+
this.updateFilters().catch((error) =>
|
|
211
|
+
reportSuppressedError(this.player, 'filters.update', error, {
|
|
212
|
+
guildId: this.player.guildId
|
|
213
|
+
})
|
|
214
|
+
)
|
|
186
215
|
}
|
|
187
216
|
})
|
|
188
217
|
return this
|
|
@@ -221,6 +250,37 @@ class Filters {
|
|
|
221
250
|
return this._setFilter('lowPass', enabled, options)
|
|
222
251
|
}
|
|
223
252
|
|
|
253
|
+
setPluginFilters(filters) {
|
|
254
|
+
const next = _utils.normalizePluginFilters(filters)
|
|
255
|
+
if (_utils.objectEqual(this.filters.pluginFilters, next)) return this
|
|
256
|
+
this.filters.pluginFilters = next
|
|
257
|
+
this._dirty.add('pluginFilters')
|
|
258
|
+
return this._scheduleUpdate()
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
setPluginFilter(name, config) {
|
|
262
|
+
if (!name || typeof name !== 'string')
|
|
263
|
+
throw new TypeError('Plugin filter name is required')
|
|
264
|
+
|
|
265
|
+
if (!config || typeof config !== 'object') {
|
|
266
|
+
if (!this.filters.pluginFilters?.[name]) return this
|
|
267
|
+
const next = { ...this.filters.pluginFilters }
|
|
268
|
+
delete next[name]
|
|
269
|
+
return this.setPluginFilters(next)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const current = this.filters.pluginFilters || EMPTY_OBJECT
|
|
273
|
+
if (current[name] === config) return this
|
|
274
|
+
return this.setPluginFilters({ ...current, [name]: config })
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
clearPluginFilters() {
|
|
278
|
+
if (!this.filters.pluginFilters) return this
|
|
279
|
+
this.filters.pluginFilters = null
|
|
280
|
+
this._dirty.add('pluginFilters')
|
|
281
|
+
return this._scheduleUpdate()
|
|
282
|
+
}
|
|
283
|
+
|
|
224
284
|
setBassboost(enabled, options = {}) {
|
|
225
285
|
if (!enabled) {
|
|
226
286
|
if (
|
|
@@ -317,12 +377,19 @@ class Filters {
|
|
|
317
377
|
for (let i = 0; i < filterNames.length; i++) {
|
|
318
378
|
const key = filterNames[i]
|
|
319
379
|
if (f[key] !== null) {
|
|
380
|
+
if (key !== 'pluginFilters') filterPool.release(key, f[key])
|
|
320
381
|
f[key] = null
|
|
321
382
|
this._dirty.add(key)
|
|
322
383
|
changed = true
|
|
323
384
|
}
|
|
324
385
|
}
|
|
325
386
|
|
|
387
|
+
if (f.pluginFilters !== null) {
|
|
388
|
+
f.pluginFilters = null
|
|
389
|
+
this._dirty.add('pluginFilters')
|
|
390
|
+
changed = true
|
|
391
|
+
}
|
|
392
|
+
|
|
326
393
|
for (const key in this.presets) {
|
|
327
394
|
if (this.presets[key] !== null) this.presets[key] = null
|
|
328
395
|
}
|
|
@@ -333,17 +400,31 @@ class Filters {
|
|
|
333
400
|
async updateFilters() {
|
|
334
401
|
if (!this.player || !this._dirty.size) return this
|
|
335
402
|
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
403
|
+
const dirtyKeys = [...this._dirty]
|
|
404
|
+
const dirtySet = new Set(dirtyKeys)
|
|
405
|
+
const payload = {
|
|
406
|
+
volume: this.filters.volume,
|
|
407
|
+
equalizer: this.filters.equalizer
|
|
408
|
+
}
|
|
409
|
+
const filterNames = Object.keys(FILTER_DEFAULTS)
|
|
410
|
+
for (let i = 0; i < filterNames.length; i++) {
|
|
411
|
+
const key = filterNames[i]
|
|
412
|
+
if (this.filters[key] !== null) payload[key] = this.filters[key]
|
|
413
|
+
else if (dirtySet.has(key)) payload[key] = null
|
|
414
|
+
}
|
|
415
|
+
payload.pluginFilters = this.filters.pluginFilters || {}
|
|
416
|
+
|
|
417
|
+
for (const key of dirtyKeys) this._dirty.delete(key)
|
|
418
|
+
|
|
419
|
+
try {
|
|
420
|
+
await this.player.nodes.rest.updatePlayer({
|
|
421
|
+
guildId: this.player.guildId,
|
|
422
|
+
data: { filters: payload }
|
|
423
|
+
})
|
|
424
|
+
} catch (error) {
|
|
425
|
+
for (const key of dirtyKeys) this._dirty.add(key)
|
|
426
|
+
throw error
|
|
339
427
|
}
|
|
340
|
-
|
|
341
|
-
this._dirty.clear()
|
|
342
|
-
|
|
343
|
-
await this.player.nodes.rest.updatePlayer({
|
|
344
|
-
guildId: this.player.guildId,
|
|
345
|
-
data: { filters: payload }
|
|
346
|
-
})
|
|
347
428
|
return this
|
|
348
429
|
}
|
|
349
430
|
}
|