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
|
@@ -2,6 +2,9 @@ const { EventEmitter } = require('node:events')
|
|
|
2
2
|
const { AqualinkEvents } = require('./AqualinkEvents')
|
|
3
3
|
const Connection = require('./Connection')
|
|
4
4
|
const Filters = require('./Filters')
|
|
5
|
+
const PlayerLifecycle = require('./PlayerLifecycle')
|
|
6
|
+
const { attachPlayerLifecycleState } = require('./PlayerLifecycleState')
|
|
7
|
+
const { reportSuppressedError } = require('./Reporting')
|
|
5
8
|
const { spAutoPlay, scAutoPlay } = require('../handlers/autoplay')
|
|
6
9
|
const Queue = require('./Queue')
|
|
7
10
|
|
|
@@ -43,6 +46,8 @@ const MUTE_TOGGLE_DELAY = 300
|
|
|
43
46
|
const SEEK_DELAY = 800
|
|
44
47
|
const PAUSE_DELAY = 1200
|
|
45
48
|
const VOICE_TRACE_INTERVAL = 15000
|
|
49
|
+
const PLAYER_UPDATE_SILENCE_THRESHOLD = 45000
|
|
50
|
+
const VOICE_FORCE_DESTROY_MS = 15 * 60 * 1000
|
|
46
51
|
const RETRY_BACKOFF_BASE = 1500
|
|
47
52
|
const RETRY_BACKOFF_MAX = 5000
|
|
48
53
|
const PREVIOUS_TRACKS_SIZE = 50
|
|
@@ -75,6 +80,20 @@ const _functions = {
|
|
|
75
80
|
for (const t of set) clearTimeout(t)
|
|
76
81
|
set.clear()
|
|
77
82
|
},
|
|
83
|
+
safeCall(fn) {
|
|
84
|
+
try {
|
|
85
|
+
return fn?.()
|
|
86
|
+
} catch {}
|
|
87
|
+
return null
|
|
88
|
+
},
|
|
89
|
+
emitAquaError(aqua, error) {
|
|
90
|
+
if (!aqua?.listenerCount) return
|
|
91
|
+
try {
|
|
92
|
+
if (aqua.listenerCount(AqualinkEvents.Error) > 0) {
|
|
93
|
+
aqua.emit(AqualinkEvents.Error, error)
|
|
94
|
+
}
|
|
95
|
+
} catch {}
|
|
96
|
+
},
|
|
78
97
|
emitIfActive(player, event, ...args) {
|
|
79
98
|
if (!player.destroyed) player.aqua.emit(event, player, ...args)
|
|
80
99
|
}
|
|
@@ -103,13 +122,13 @@ class MicrotaskUpdateBatcher {
|
|
|
103
122
|
const { player: p, updates: u } = this
|
|
104
123
|
this.updates = null
|
|
105
124
|
this.scheduled = false
|
|
106
|
-
if (!u || !p
|
|
125
|
+
if (!u || !p || p.destroyed || p.state === PLAYER_STATE.DISCONNECTING)
|
|
126
|
+
return Promise.resolve()
|
|
107
127
|
return p.updatePlayer(u).catch((err) => {
|
|
108
|
-
|
|
109
|
-
|
|
128
|
+
_functions.emitAquaError(
|
|
129
|
+
p.aqua,
|
|
110
130
|
new Error(`Update error: ${err.message}`)
|
|
111
131
|
)
|
|
112
|
-
throw err
|
|
113
132
|
})
|
|
114
133
|
}
|
|
115
134
|
|
|
@@ -190,11 +209,11 @@ class Player extends EventEmitter {
|
|
|
190
209
|
this.mute = !!options.mute
|
|
191
210
|
this.autoplayRetries = this.reconnectionRetries = 0
|
|
192
211
|
this._voiceDownSince = 0
|
|
193
|
-
this
|
|
194
|
-
this._resuming = !!options.resuming
|
|
212
|
+
attachPlayerLifecycleState(this, { resuming: !!options.resuming })
|
|
195
213
|
this._voiceWatchdogTimer = null
|
|
196
214
|
this._pendingTimers = new Set()
|
|
197
215
|
this._reconnectTimers = null
|
|
216
|
+
this._reconnectNonce = 0
|
|
198
217
|
this._dataStore = null
|
|
199
218
|
|
|
200
219
|
this.volume = _functions.clamp(options.defaultVolume || 100)
|
|
@@ -210,12 +229,30 @@ class Player extends EventEmitter {
|
|
|
210
229
|
this.previousIdentifiers = new Set()
|
|
211
230
|
this.previousTracks = new CircularBuffer(PREVIOUS_TRACKS_SIZE)
|
|
212
231
|
this._updateBatcher = batcherPool.acquire(this)
|
|
232
|
+
this._lifecycleController = new PlayerLifecycle(this, {
|
|
233
|
+
_functions,
|
|
234
|
+
PLAYER_STATE,
|
|
235
|
+
VOICE_TRACE_INTERVAL,
|
|
236
|
+
PLAYER_UPDATE_SILENCE_THRESHOLD,
|
|
237
|
+
VOICE_DOWN_THRESHOLD,
|
|
238
|
+
VOICE_ABANDON_MULTIPLIER,
|
|
239
|
+
VOICE_FORCE_DESTROY_MS,
|
|
240
|
+
RECONNECT_MAX,
|
|
241
|
+
MUTE_TOGGLE_DELAY,
|
|
242
|
+
SEEK_DELAY,
|
|
243
|
+
PAUSE_DELAY,
|
|
244
|
+
RETRY_BACKOFF_BASE,
|
|
245
|
+
RETRY_BACKOFF_MAX
|
|
246
|
+
})
|
|
213
247
|
|
|
214
248
|
this._voiceRequestAt = 0
|
|
215
249
|
this._voiceRequestChannel = null
|
|
216
250
|
this._suppressResumeUntil = 0
|
|
217
|
-
this._deferredStart = false
|
|
218
251
|
this._lastVoiceUpTraceAt = 0
|
|
252
|
+
this._lastPlayerUpdateAt = Date.now()
|
|
253
|
+
this._voiceRecoverySeq = 0
|
|
254
|
+
this._activeVoiceRecoveryToken = 0
|
|
255
|
+
this._voiceRecoveryReason = null
|
|
219
256
|
this._bindEvents()
|
|
220
257
|
this._startWatchdog()
|
|
221
258
|
}
|
|
@@ -252,58 +289,28 @@ class Player extends EventEmitter {
|
|
|
252
289
|
return new Promise((r) => this._createTimer(r, ms))
|
|
253
290
|
}
|
|
254
291
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
guildId: this.guildId,
|
|
268
|
-
reconnecting: !!this._reconnecting,
|
|
269
|
-
recovering: !!this._voiceRecovering
|
|
270
|
-
})
|
|
271
|
-
}
|
|
272
|
-
if (
|
|
273
|
-
!this._voiceDownSince &&
|
|
274
|
-
!this._reconnecting &&
|
|
275
|
-
!this._voiceRecovering
|
|
276
|
-
) {
|
|
277
|
-
this._voiceDownSince = Date.now()
|
|
278
|
-
this._createTimer(() => {
|
|
279
|
-
if (
|
|
280
|
-
this.connected ||
|
|
281
|
-
this.destroyed ||
|
|
282
|
-
this._reconnecting ||
|
|
283
|
-
this._voiceRecovering ||
|
|
284
|
-
this.nodes?.info?.isNodelink
|
|
285
|
-
)
|
|
286
|
-
return
|
|
287
|
-
this.connection.attemptResume()
|
|
288
|
-
}, 1000)
|
|
289
|
-
}
|
|
290
|
-
} else {
|
|
291
|
-
this._voiceDownSince = 0
|
|
292
|
-
this.state = PLAYER_STATE.READY
|
|
293
|
-
const now = Date.now()
|
|
294
|
-
if (
|
|
295
|
-
!wasConnected ||
|
|
296
|
-
now - this._lastVoiceUpTraceAt >= VOICE_TRACE_INTERVAL
|
|
297
|
-
) {
|
|
298
|
-
this._lastVoiceUpTraceAt = now
|
|
299
|
-
this.aqua?._trace?.('player.voice.up', {
|
|
300
|
-
guildId: this.guildId,
|
|
301
|
-
ping: this.ping
|
|
302
|
-
})
|
|
303
|
-
}
|
|
304
|
-
}
|
|
292
|
+
_claimVoiceRecovery(reason = 'unknown') {
|
|
293
|
+
const token = ++this._voiceRecoverySeq
|
|
294
|
+
this._activeVoiceRecoveryToken = token
|
|
295
|
+
this._voiceRecoveryReason = reason
|
|
296
|
+
return token
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
_isVoiceRecoveryActive(token) {
|
|
300
|
+
return (
|
|
301
|
+
!!token && !this.destroyed && this._activeVoiceRecoveryToken === token
|
|
302
|
+
)
|
|
303
|
+
}
|
|
305
304
|
|
|
306
|
-
|
|
305
|
+
_clearVoiceRecovery(token = this._activeVoiceRecoveryToken, reason = null) {
|
|
306
|
+
if (!token || this._activeVoiceRecoveryToken !== token) return false
|
|
307
|
+
this._activeVoiceRecoveryToken = 0
|
|
308
|
+
this._voiceRecoveryReason = reason
|
|
309
|
+
return true
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
_handlePlayerUpdate(packet) {
|
|
313
|
+
return this._lifecycleController.handlePlayerUpdate(packet)
|
|
307
314
|
}
|
|
308
315
|
|
|
309
316
|
async _handleEvent(payload) {
|
|
@@ -318,13 +325,9 @@ class Player extends EventEmitter {
|
|
|
318
325
|
return
|
|
319
326
|
}
|
|
320
327
|
try {
|
|
321
|
-
|
|
322
|
-
payload.type === 'TrackStartEvent'
|
|
323
|
-
? payload.track || this.current
|
|
324
|
-
: this.current
|
|
325
|
-
await this[handler](this, trackArg, payload)
|
|
328
|
+
await this[handler](this, this.current, payload)
|
|
326
329
|
} catch (error) {
|
|
327
|
-
this.aqua
|
|
330
|
+
_functions.emitAquaError(this.aqua, error)
|
|
328
331
|
}
|
|
329
332
|
}
|
|
330
333
|
|
|
@@ -352,6 +355,11 @@ class Player extends EventEmitter {
|
|
|
352
355
|
|
|
353
356
|
async play(track, options = {}) {
|
|
354
357
|
if (this.destroyed || !this.queue) return this
|
|
358
|
+
if (options != null && typeof options !== 'object') {
|
|
359
|
+
throw new TypeError(
|
|
360
|
+
`Player.play(): options must be an object, got ${typeof options}`
|
|
361
|
+
)
|
|
362
|
+
}
|
|
355
363
|
|
|
356
364
|
let item = track
|
|
357
365
|
if (!item) {
|
|
@@ -370,30 +378,85 @@ class Player extends EventEmitter {
|
|
|
370
378
|
}
|
|
371
379
|
this.current = resolvedItem
|
|
372
380
|
if (this.destroyed) return this
|
|
373
|
-
if (!this.current?.track)
|
|
381
|
+
if (!this.current?.track) {
|
|
382
|
+
this.current = null
|
|
383
|
+
this.playing = false
|
|
384
|
+
if (this.aqua?.debugTrace) {
|
|
385
|
+
this.aqua._trace('player.play.unresolved', {
|
|
386
|
+
guildId: this.guildId,
|
|
387
|
+
reconnecting: !!this._reconnecting,
|
|
388
|
+
resuming: !!this._resuming,
|
|
389
|
+
voiceRecovering: !!this._voiceRecovering
|
|
390
|
+
})
|
|
391
|
+
}
|
|
392
|
+
if (this._reconnecting || this._resuming || this._voiceRecovering)
|
|
393
|
+
return this
|
|
394
|
+
throw new Error('Failed to resolve track')
|
|
395
|
+
}
|
|
374
396
|
|
|
375
397
|
this.playing = true
|
|
376
398
|
this.paused = !!options.paused
|
|
377
399
|
this.position = options.startTime || 0
|
|
378
|
-
this.aqua?.
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
400
|
+
if (this.aqua?.debugTrace) {
|
|
401
|
+
this.aqua._trace('player.play', {
|
|
402
|
+
guildId: this.guildId,
|
|
403
|
+
paused: this.paused,
|
|
404
|
+
startTime: this.position,
|
|
405
|
+
hasTrack: !!this.current?.track
|
|
406
|
+
})
|
|
407
|
+
}
|
|
384
408
|
|
|
385
409
|
if (this.destroyed || !this._updateBatcher) return this
|
|
386
410
|
|
|
411
|
+
if (
|
|
412
|
+
this.voiceChannel &&
|
|
413
|
+
!this.connected &&
|
|
414
|
+
!this._reconnecting &&
|
|
415
|
+
!this._voiceRecovering
|
|
416
|
+
) {
|
|
417
|
+
this._deferredStart = true
|
|
418
|
+
const recoveryToken = this._claimVoiceRecovery('play_deferred')
|
|
419
|
+
if (this.aqua?.debugTrace) {
|
|
420
|
+
this.aqua._trace('player.play.deferred', {
|
|
421
|
+
guildId: this.guildId,
|
|
422
|
+
reason: 'voice_not_connected'
|
|
423
|
+
})
|
|
424
|
+
}
|
|
425
|
+
const now = Date.now()
|
|
426
|
+
if (
|
|
427
|
+
now - (this._voiceRequestAt || 0) >= 1200 &&
|
|
428
|
+
this._isVoiceRecoveryActive(recoveryToken)
|
|
429
|
+
) {
|
|
430
|
+
this._voiceRequestAt = now
|
|
431
|
+
if (this._isVoiceRecoveryActive(recoveryToken))
|
|
432
|
+
this.connection?._requestVoiceState?.()
|
|
433
|
+
if (this._isVoiceRecoveryActive(recoveryToken))
|
|
434
|
+
this.connection?.resendVoiceUpdate?.(true)
|
|
435
|
+
if (this._isVoiceRecoveryActive(recoveryToken))
|
|
436
|
+
_functions.safeCall(() =>
|
|
437
|
+
this.connect({
|
|
438
|
+
guildId: this.guildId,
|
|
439
|
+
voiceChannel: this.voiceChannel,
|
|
440
|
+
deaf: this.deaf,
|
|
441
|
+
mute: this.mute
|
|
442
|
+
})
|
|
443
|
+
)
|
|
444
|
+
}
|
|
445
|
+
return this
|
|
446
|
+
}
|
|
447
|
+
|
|
387
448
|
if (
|
|
388
449
|
this.aqua?.autoRegionMigrate &&
|
|
389
450
|
!this._resuming &&
|
|
390
451
|
!this.connection?.endpoint
|
|
391
452
|
) {
|
|
392
453
|
this._deferredStart = true
|
|
393
|
-
this.aqua?.
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
454
|
+
if (this.aqua?.debugTrace) {
|
|
455
|
+
this.aqua._trace('player.play.deferred', {
|
|
456
|
+
guildId: this.guildId,
|
|
457
|
+
reason: 'awaiting_voice_server_update'
|
|
458
|
+
})
|
|
459
|
+
}
|
|
397
460
|
return this
|
|
398
461
|
}
|
|
399
462
|
|
|
@@ -404,16 +467,33 @@ class Player extends EventEmitter {
|
|
|
404
467
|
if (this.position > 0) updateData.position = this.position
|
|
405
468
|
|
|
406
469
|
this._deferredStart = false
|
|
407
|
-
await this.batchUpdatePlayer(updateData, true)
|
|
470
|
+
await this.batchUpdatePlayer(updateData, true).catch((err) => {
|
|
471
|
+
if (!this.destroyed) _functions.emitAquaError(this.aqua, err)
|
|
472
|
+
})
|
|
408
473
|
} catch (error) {
|
|
409
|
-
if (
|
|
410
|
-
|
|
474
|
+
if (
|
|
475
|
+
!this.destroyed &&
|
|
476
|
+
!this._reconnecting &&
|
|
477
|
+
!this._resuming &&
|
|
478
|
+
!this._voiceRecovering
|
|
479
|
+
) {
|
|
480
|
+
_functions.emitAquaError(this.aqua, error)
|
|
481
|
+
}
|
|
482
|
+
if (
|
|
483
|
+
this.queue?.size &&
|
|
484
|
+
!track &&
|
|
485
|
+
!this._reconnecting &&
|
|
486
|
+
!this._resuming &&
|
|
487
|
+
!this._voiceRecovering
|
|
488
|
+
)
|
|
489
|
+
return this.play()
|
|
411
490
|
}
|
|
412
491
|
return this
|
|
413
492
|
}
|
|
414
493
|
|
|
415
494
|
connect(options = {}) {
|
|
416
|
-
if (this.destroyed)
|
|
495
|
+
if (this.destroyed)
|
|
496
|
+
throw new Error(`Cannot connect destroyed player (guild=${this.guildId})`)
|
|
417
497
|
|
|
418
498
|
const voiceChannel = _functions.toId(
|
|
419
499
|
options.voiceChannel || this.voiceChannel
|
|
@@ -429,19 +509,22 @@ class Player extends EventEmitter {
|
|
|
429
509
|
this._voiceRequestChannel = voiceChannel
|
|
430
510
|
|
|
431
511
|
this.voiceChannel = voiceChannel
|
|
512
|
+
this._voiceDownSince = 0
|
|
432
513
|
this.send({
|
|
433
514
|
guild_id: this.guildId,
|
|
434
515
|
channel_id: voiceChannel,
|
|
435
516
|
self_deaf: this.deaf,
|
|
436
517
|
self_mute: this.mute
|
|
437
518
|
})
|
|
438
|
-
this.aqua?.
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
519
|
+
if (this.aqua?.debugTrace) {
|
|
520
|
+
this.aqua._trace('player.connect.request', {
|
|
521
|
+
guildId: this.guildId,
|
|
522
|
+
txId: this.txId,
|
|
523
|
+
voiceChannel,
|
|
524
|
+
deaf: this.deaf,
|
|
525
|
+
mute: this.mute
|
|
526
|
+
})
|
|
527
|
+
}
|
|
445
528
|
return this
|
|
446
529
|
}
|
|
447
530
|
|
|
@@ -464,72 +547,33 @@ class Player extends EventEmitter {
|
|
|
464
547
|
}
|
|
465
548
|
|
|
466
549
|
async _voiceWatchdog() {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
const hasVoiceData =
|
|
470
|
-
this.connection?.sessionId &&
|
|
471
|
-
this.connection?.endpoint &&
|
|
472
|
-
this.connection?.token
|
|
473
|
-
if (!hasVoiceData) {
|
|
474
|
-
if (
|
|
475
|
-
Date.now() - this._voiceDownSince >
|
|
476
|
-
VOICE_DOWN_THRESHOLD * VOICE_ABANDON_MULTIPLIER
|
|
477
|
-
)
|
|
478
|
-
this.destroy()
|
|
479
|
-
return
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
this._voiceRecovering = true
|
|
483
|
-
try {
|
|
484
|
-
if (await this.connection.attemptResume()) {
|
|
485
|
-
this.reconnectionRetries = this._voiceDownSince = 0
|
|
486
|
-
return
|
|
487
|
-
}
|
|
488
|
-
const originalMute = this.mute
|
|
489
|
-
this.send({
|
|
490
|
-
guild_id: this.guildId,
|
|
491
|
-
channel_id: this.voiceChannel,
|
|
492
|
-
self_deaf: this.deaf,
|
|
493
|
-
self_mute: !originalMute
|
|
494
|
-
})
|
|
495
|
-
await this._delay(MUTE_TOGGLE_DELAY)
|
|
496
|
-
if (!this.destroyed) {
|
|
497
|
-
this.send({
|
|
498
|
-
guild_id: this.guildId,
|
|
499
|
-
channel_id: this.voiceChannel,
|
|
500
|
-
self_deaf: this.deaf,
|
|
501
|
-
self_mute: originalMute
|
|
502
|
-
})
|
|
503
|
-
}
|
|
504
|
-
this.connection.resendVoiceUpdate()
|
|
505
|
-
this.reconnectionRetries++
|
|
506
|
-
} catch {
|
|
507
|
-
if (++this.reconnectionRetries >= RECONNECT_MAX) this.destroy()
|
|
508
|
-
} finally {
|
|
509
|
-
this._voiceRecovering = false
|
|
510
|
-
}
|
|
550
|
+
return this._lifecycleController.voiceWatchdog()
|
|
511
551
|
}
|
|
512
552
|
|
|
513
553
|
destroy(options = {}) {
|
|
554
|
+
if (this.destroyed) return this
|
|
555
|
+
|
|
514
556
|
const {
|
|
515
557
|
preserveClient = true,
|
|
516
558
|
skipRemote = false,
|
|
517
559
|
preserveMessage = false,
|
|
518
560
|
preserveReconnecting = false,
|
|
519
|
-
preserveTracks = false
|
|
561
|
+
preserveTracks = false,
|
|
562
|
+
abortSignal = null
|
|
520
563
|
} = options
|
|
521
|
-
if (this.destroyed && !this.queue) return this
|
|
522
564
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
565
|
+
this._reconnectNonce++
|
|
566
|
+
this.destroyed = true
|
|
567
|
+
this._clearVoiceRecovery(undefined, 'destroyed')
|
|
568
|
+
if (this.aqua?.debugTrace) {
|
|
569
|
+
this.aqua._trace('player.destroy', {
|
|
526
570
|
guildId: this.guildId,
|
|
527
571
|
skipRemote: !!skipRemote,
|
|
528
572
|
preserveTracks: !!preserveTracks,
|
|
529
573
|
preserveReconnecting: !!preserveReconnecting
|
|
530
574
|
})
|
|
531
|
-
this.emit('destroy')
|
|
532
575
|
}
|
|
576
|
+
this.emit('destroy')
|
|
533
577
|
|
|
534
578
|
if (this._voiceWatchdogTimer) {
|
|
535
579
|
clearInterval(this._voiceWatchdogTimer)
|
|
@@ -539,7 +583,6 @@ class Player extends EventEmitter {
|
|
|
539
583
|
_functions.clearTimers(this._pendingTimers)
|
|
540
584
|
this._pendingTimers = null
|
|
541
585
|
|
|
542
|
-
// Clear reconnection timers to prevent memory leaks when destroyed externally
|
|
543
586
|
if (this._reconnectTimers) {
|
|
544
587
|
_functions.clearTimers(this._reconnectTimers)
|
|
545
588
|
this._reconnectTimers = null
|
|
@@ -553,6 +596,7 @@ class Player extends EventEmitter {
|
|
|
553
596
|
this._lastVoiceChannel = this.voiceChannel
|
|
554
597
|
this._lastTextChannel = this.textChannel
|
|
555
598
|
this.voiceChannel = null
|
|
599
|
+
this._isActivelyReconnecting = false
|
|
556
600
|
|
|
557
601
|
if (
|
|
558
602
|
this.shouldDeleteMessage &&
|
|
@@ -574,12 +618,37 @@ class Player extends EventEmitter {
|
|
|
574
618
|
this._updateBatcher = null
|
|
575
619
|
}
|
|
576
620
|
|
|
621
|
+
if (this.filters) {
|
|
622
|
+
try {
|
|
623
|
+
this.filters.destroy()
|
|
624
|
+
} catch (error) {
|
|
625
|
+
reportSuppressedError(this, 'player.destroy.filters', error, {
|
|
626
|
+
guildId: this.guildId
|
|
627
|
+
})
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
577
631
|
this.previousTracks?.clear()
|
|
578
632
|
this.previousTracks = null
|
|
579
633
|
this.previousIdentifiers?.clear()
|
|
580
634
|
this.previousIdentifiers = null
|
|
581
|
-
this.queue
|
|
635
|
+
if (this.queue) {
|
|
636
|
+
for (
|
|
637
|
+
let i = this.queue._head || 0;
|
|
638
|
+
i < (this.queue._items?.length || 0);
|
|
639
|
+
i++
|
|
640
|
+
) {
|
|
641
|
+
const t = this.queue._items[i]
|
|
642
|
+
if (t && typeof t.dispose === 'function') {
|
|
643
|
+
try {
|
|
644
|
+
t.dispose()
|
|
645
|
+
} catch {}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
this.queue.clear()
|
|
649
|
+
}
|
|
582
650
|
this.queue = null
|
|
651
|
+
// ML-1: Clear _dataStore to prevent unbounded growth
|
|
583
652
|
this._dataStore?.clear()
|
|
584
653
|
this._dataStore = null
|
|
585
654
|
|
|
@@ -592,39 +661,92 @@ class Player extends EventEmitter {
|
|
|
592
661
|
if (this.connection) {
|
|
593
662
|
try {
|
|
594
663
|
this.connection.destroy()
|
|
595
|
-
} catch {
|
|
664
|
+
} catch (error) {
|
|
665
|
+
reportSuppressedError(this, 'player.destroy.connection', error, {
|
|
666
|
+
guildId: this.guildId
|
|
667
|
+
})
|
|
668
|
+
}
|
|
596
669
|
}
|
|
597
|
-
this.connection =
|
|
670
|
+
this.connection =
|
|
671
|
+
this.filters =
|
|
672
|
+
this.current =
|
|
673
|
+
this.autoplaySeed =
|
|
674
|
+
this._lifecycleController =
|
|
675
|
+
null
|
|
598
676
|
|
|
599
677
|
if (!skipRemote) {
|
|
600
678
|
try {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
679
|
+
if (abortSignal?.aborted) {
|
|
680
|
+
if (this.aqua?.debugTrace) {
|
|
681
|
+
this.aqua._trace('player.destroy.aborted', {
|
|
682
|
+
guildId: this.guildId,
|
|
683
|
+
reason: 'abort_signal_already_set'
|
|
684
|
+
})
|
|
685
|
+
}
|
|
686
|
+
} else {
|
|
687
|
+
this.send({ guild_id: this.guildId, channel_id: null })
|
|
688
|
+
this.aqua?.destroyPlayer?.(this.guildId)
|
|
689
|
+
if (this.nodes?.connected)
|
|
690
|
+
this.nodes.rest
|
|
691
|
+
?.destroyPlayer(this.guildId, abortSignal)
|
|
692
|
+
.catch((error) => {
|
|
693
|
+
reportSuppressedError(this, 'player.destroy.remote', error, {
|
|
694
|
+
guildId: this.guildId
|
|
695
|
+
})
|
|
696
|
+
})
|
|
697
|
+
}
|
|
698
|
+
} catch (error) {
|
|
699
|
+
reportSuppressedError(this, 'player.destroy.gateway', error, {
|
|
700
|
+
guildId: this.guildId
|
|
701
|
+
})
|
|
702
|
+
}
|
|
606
703
|
}
|
|
607
704
|
|
|
608
|
-
if (!preserveClient)
|
|
705
|
+
if (!preserveClient) {
|
|
706
|
+
try {
|
|
707
|
+
this.aqua?.removeListener?.('playerUpdate', this._boundPlayerUpdate)
|
|
708
|
+
} catch {}
|
|
709
|
+
this.aqua = this.nodes = null
|
|
710
|
+
}
|
|
609
711
|
return this
|
|
610
712
|
}
|
|
611
713
|
|
|
612
714
|
pause(paused) {
|
|
613
|
-
if (this.destroyed
|
|
715
|
+
if (this.destroyed) return this
|
|
716
|
+
if (paused != null && typeof paused !== 'boolean') {
|
|
717
|
+
throw new TypeError(
|
|
718
|
+
`Player.pause(): paused must be a boolean, got ${typeof paused}`
|
|
719
|
+
)
|
|
720
|
+
}
|
|
721
|
+
if (this.paused === !!paused) return this
|
|
614
722
|
this.paused = !!paused
|
|
615
|
-
this.batchUpdatePlayer({ paused: this.paused }, true).catch(() =>
|
|
723
|
+
this.batchUpdatePlayer({ paused: this.paused }, true).catch((error) =>
|
|
724
|
+
reportSuppressedError(this, 'player.pause', error, {
|
|
725
|
+
guildId: this.guildId,
|
|
726
|
+
paused: this.paused
|
|
727
|
+
})
|
|
728
|
+
)
|
|
616
729
|
return this
|
|
617
730
|
}
|
|
618
731
|
|
|
619
732
|
seek(position) {
|
|
620
|
-
if (
|
|
621
|
-
|
|
733
|
+
if (position == null || !_functions.isNum(position)) {
|
|
734
|
+
throw new TypeError(
|
|
735
|
+
`Player.seek(): position must be a non-negative number, got ${typeof position}`
|
|
736
|
+
)
|
|
737
|
+
}
|
|
738
|
+
if (this.destroyed || !this.playing) return this
|
|
622
739
|
const len = this.current?.info?.length || 0
|
|
623
740
|
const clamped = len
|
|
624
741
|
? Math.min(Math.max(position, 0), len)
|
|
625
742
|
: Math.max(position, 0)
|
|
626
743
|
this.position = clamped
|
|
627
|
-
this.batchUpdatePlayer({ position: clamped }, true).catch(() =>
|
|
744
|
+
this.batchUpdatePlayer({ position: clamped }, true).catch((error) =>
|
|
745
|
+
reportSuppressedError(this, 'player.seek', error, {
|
|
746
|
+
guildId: this.guildId,
|
|
747
|
+
position: clamped
|
|
748
|
+
})
|
|
749
|
+
)
|
|
628
750
|
return this
|
|
629
751
|
}
|
|
630
752
|
|
|
@@ -678,15 +800,32 @@ class Player extends EventEmitter {
|
|
|
678
800
|
this.batchUpdatePlayer(
|
|
679
801
|
{ track: { encoded: null }, paused: this.paused },
|
|
680
802
|
true
|
|
681
|
-
).catch(() =>
|
|
803
|
+
).catch((error) =>
|
|
804
|
+
reportSuppressedError(this, 'player.stop', error, {
|
|
805
|
+
guildId: this.guildId
|
|
806
|
+
})
|
|
807
|
+
)
|
|
682
808
|
return this
|
|
683
809
|
}
|
|
684
810
|
|
|
685
811
|
setVolume(volume) {
|
|
812
|
+
if (
|
|
813
|
+
volume == null ||
|
|
814
|
+
(typeof volume !== 'number' && typeof volume !== 'string')
|
|
815
|
+
) {
|
|
816
|
+
throw new TypeError(
|
|
817
|
+
`Player.setVolume(): volume must be a number, got ${typeof volume}`
|
|
818
|
+
)
|
|
819
|
+
}
|
|
686
820
|
const vol = _functions.clamp(volume)
|
|
687
821
|
if (this.destroyed || this.volume === vol) return this
|
|
688
822
|
this.volume = vol
|
|
689
|
-
this.batchUpdatePlayer({ volume: vol }).catch(() =>
|
|
823
|
+
this.batchUpdatePlayer({ volume: vol }).catch((error) =>
|
|
824
|
+
reportSuppressedError(this, 'player.setVolume', error, {
|
|
825
|
+
guildId: this.guildId,
|
|
826
|
+
volume: vol
|
|
827
|
+
})
|
|
828
|
+
)
|
|
690
829
|
return this
|
|
691
830
|
}
|
|
692
831
|
|
|
@@ -703,7 +842,6 @@ class Player extends EventEmitter {
|
|
|
703
842
|
const id = _functions.toId(channel)
|
|
704
843
|
if (!id) throw new TypeError('Invalid text channel')
|
|
705
844
|
this.textChannel = id
|
|
706
|
-
this.batchUpdatePlayer({ text_channel: id }).catch(() => {})
|
|
707
845
|
return this
|
|
708
846
|
}
|
|
709
847
|
|
|
@@ -739,7 +877,31 @@ class Player extends EventEmitter {
|
|
|
739
877
|
replay() {
|
|
740
878
|
return this.seek(0)
|
|
741
879
|
}
|
|
742
|
-
skip() {
|
|
880
|
+
skip(target) {
|
|
881
|
+
if (this.destroyed || !this.playing) return this
|
|
882
|
+
|
|
883
|
+
if (target === undefined || target === null) return this.stop()
|
|
884
|
+
|
|
885
|
+
if (typeof target === 'number') {
|
|
886
|
+
const idx = target | 0
|
|
887
|
+
if (idx <= 0) return this.stop()
|
|
888
|
+
if (!this.queue?.size || idx >= this.queue.size) return this.stop()
|
|
889
|
+
for (let i = 0; i < idx; i++) this.queue.dequeue()
|
|
890
|
+
return this.stop()
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
const targetId = _functions.toId(target)
|
|
894
|
+
if (targetId && this.queue?.size) {
|
|
895
|
+
const arr = this.queue.toArray()
|
|
896
|
+
const idx = arr.findIndex(
|
|
897
|
+
(t) =>
|
|
898
|
+
_functions.toId(t) === targetId ||
|
|
899
|
+
_functions.toId(t?.info?.identifier) === targetId
|
|
900
|
+
)
|
|
901
|
+
if (idx > 0) {
|
|
902
|
+
for (let i = 0; i < idx; i++) this.queue.dequeue()
|
|
903
|
+
}
|
|
904
|
+
}
|
|
743
905
|
return this.stop()
|
|
744
906
|
}
|
|
745
907
|
|
|
@@ -835,8 +997,8 @@ class Player extends EventEmitter {
|
|
|
835
997
|
}
|
|
836
998
|
} catch (err) {
|
|
837
999
|
if (this.destroyed) return this
|
|
838
|
-
|
|
839
|
-
|
|
1000
|
+
_functions.emitAquaError(
|
|
1001
|
+
this.aqua,
|
|
840
1002
|
new Error(`Autoplay ${i + 1} fail: ${err.message}`)
|
|
841
1003
|
)
|
|
842
1004
|
}
|
|
@@ -858,7 +1020,7 @@ class Player extends EventEmitter {
|
|
|
858
1020
|
}
|
|
859
1021
|
|
|
860
1022
|
async _getAutoplayTrack(sourceName, identifier, uri, requester) {
|
|
861
|
-
if (sourceName === 'youtube') {
|
|
1023
|
+
if (sourceName === 'youtube' || sourceName === 'ytmusic') {
|
|
862
1024
|
const res = await this.aqua.resolve({
|
|
863
1025
|
query: `https://www.youtube.com/watch?v=${identifier}&list=RD${identifier}`,
|
|
864
1026
|
source: 'ytmsearch',
|
|
@@ -915,6 +1077,7 @@ class Player extends EventEmitter {
|
|
|
915
1077
|
const isReplaced = reason === 'replaced'
|
|
916
1078
|
|
|
917
1079
|
if (track) this.previousTracks.push(track)
|
|
1080
|
+
if (isReplaced) return
|
|
918
1081
|
if (this.shouldDeleteMessage && !this._reconnecting && !this._resuming)
|
|
919
1082
|
_functions.safeDel(this.nowPlayingMessage)
|
|
920
1083
|
if (!isReplaced) this.current = null
|
|
@@ -930,12 +1093,15 @@ class Player extends EventEmitter {
|
|
|
930
1093
|
return
|
|
931
1094
|
}
|
|
932
1095
|
|
|
933
|
-
if (
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
1096
|
+
if (track && reason === 'finished') {
|
|
1097
|
+
if (this.loop === LOOP_MODES.TRACK) {
|
|
1098
|
+
this.aqua.emit(AqualinkEvents.TrackEnd, this, track, reason)
|
|
1099
|
+
await this.play(track)
|
|
1100
|
+
return
|
|
1101
|
+
}
|
|
1102
|
+
if (this.loop === LOOP_MODES.QUEUE) {
|
|
1103
|
+
this.queue.add(track)
|
|
1104
|
+
}
|
|
939
1105
|
}
|
|
940
1106
|
|
|
941
1107
|
if (this.queue.size) {
|
|
@@ -1006,171 +1172,28 @@ class Player extends EventEmitter {
|
|
|
1006
1172
|
_functions.emitIfActive(this, AqualinkEvents.MixEnded, t, payload)
|
|
1007
1173
|
}
|
|
1008
1174
|
|
|
1009
|
-
async _attemptVoiceResume() {
|
|
1010
|
-
|
|
1011
|
-
if (!(await this.connection.attemptResume()))
|
|
1012
|
-
throw new Error('Resume failed')
|
|
1175
|
+
async _attemptVoiceResume(abortSignal) {
|
|
1176
|
+
return this._lifecycleController.attemptVoiceResume(abortSignal)
|
|
1013
1177
|
}
|
|
1014
1178
|
|
|
1015
1179
|
async socketClosed(_player, _track, payload) {
|
|
1016
|
-
|
|
1017
|
-
this.aqua?._trace?.('player.socketClosed', {
|
|
1018
|
-
guildId: this.guildId,
|
|
1019
|
-
code: payload?.code
|
|
1020
|
-
})
|
|
1021
|
-
|
|
1022
|
-
const code = payload?.code
|
|
1023
|
-
if (code === 4006 && this._resuming) {
|
|
1024
|
-
this.aqua?._trace?.('player.socketClosed.ignored', {
|
|
1025
|
-
guildId: this.guildId,
|
|
1026
|
-
code,
|
|
1027
|
-
reason: 'transient_while_resuming'
|
|
1028
|
-
})
|
|
1029
|
-
return
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
let isRecoverable = [4015, 4009, 4006, 4014, 4022].includes(code)
|
|
1033
|
-
if (code === 4014 && this.connection?.isWaitingForDisconnect)
|
|
1034
|
-
isRecoverable = false
|
|
1035
|
-
|
|
1036
|
-
if (code === 4015 && !this.nodes?.info?.isNodelink) {
|
|
1037
|
-
this._reconnecting = true
|
|
1038
|
-
try {
|
|
1039
|
-
await this._attemptVoiceResume()
|
|
1040
|
-
this._reconnecting = false
|
|
1041
|
-
return
|
|
1042
|
-
} catch {
|
|
1043
|
-
this._reconnecting = false
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
if (!isRecoverable) {
|
|
1048
|
-
this.aqua.emit(AqualinkEvents.SocketClosed, this, payload)
|
|
1049
|
-
this.destroy()
|
|
1050
|
-
return
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
if (code === 4014 || code === 4022) {
|
|
1054
|
-
this.connected = false
|
|
1055
|
-
if (!this._voiceDownSince) this._voiceDownSince = Date.now()
|
|
1056
|
-
if (code === 4022) this._suppressResumeUntil = Date.now() + 3000
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
const aqua = this.aqua
|
|
1060
|
-
const vcId = _functions.toId(this.voiceChannel)
|
|
1061
|
-
const tcId = _functions.toId(this.textChannel)
|
|
1062
|
-
const { guildId, deaf, mute } = this
|
|
1063
|
-
|
|
1064
|
-
if (!vcId) {
|
|
1065
|
-
aqua?.emit?.(AqualinkEvents.SocketClosed, this, payload)
|
|
1066
|
-
return
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
const state = {
|
|
1070
|
-
volume: this.volume,
|
|
1071
|
-
position: this.position,
|
|
1072
|
-
paused: this.paused,
|
|
1073
|
-
loop: this.loop,
|
|
1074
|
-
isAutoplayEnabled: this.isAutoplayEnabled,
|
|
1075
|
-
currentTrack: this.current,
|
|
1076
|
-
queue: this.queue?.toArray() || [],
|
|
1077
|
-
previousIdentifiers: Array.from(this.previousIdentifiers),
|
|
1078
|
-
autoplaySeed: this.autoplaySeed,
|
|
1079
|
-
nowPlayingMessage: this.nowPlayingMessage
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
this._reconnecting = true
|
|
1083
|
-
this.destroy({
|
|
1084
|
-
preserveClient: true,
|
|
1085
|
-
skipRemote: true,
|
|
1086
|
-
preserveMessage: true,
|
|
1087
|
-
preserveReconnecting: true,
|
|
1088
|
-
preserveTracks: true
|
|
1089
|
-
})
|
|
1090
|
-
|
|
1091
|
-
// Store reconnect timers on instance for cleanup in destroy()
|
|
1092
|
-
this._reconnectTimers = new Set()
|
|
1093
|
-
const reconnectTimers = this._reconnectTimers
|
|
1094
|
-
const tryReconnect = async (attempt) => {
|
|
1095
|
-
if (aqua?.destroyed) {
|
|
1096
|
-
_functions.clearTimers(reconnectTimers)
|
|
1097
|
-
return
|
|
1098
|
-
}
|
|
1099
|
-
try {
|
|
1100
|
-
const np = await aqua.createConnection({
|
|
1101
|
-
guildId,
|
|
1102
|
-
voiceChannel: vcId,
|
|
1103
|
-
textChannel: tcId,
|
|
1104
|
-
deaf,
|
|
1105
|
-
mute,
|
|
1106
|
-
defaultVolume: state.volume,
|
|
1107
|
-
preserveMessage: true,
|
|
1108
|
-
resuming: true
|
|
1109
|
-
})
|
|
1110
|
-
if (!np) throw new Error('Failed to create player')
|
|
1111
|
-
|
|
1112
|
-
np.reconnectionRetries = 0
|
|
1113
|
-
np.loop = state.loop
|
|
1114
|
-
np.isAutoplayEnabled = state.isAutoplayEnabled
|
|
1115
|
-
np.autoplaySeed = state.autoplaySeed
|
|
1116
|
-
np.previousIdentifiers = new Set(state.previousIdentifiers)
|
|
1117
|
-
np.nowPlayingMessage = state.nowPlayingMessage
|
|
1118
|
-
|
|
1119
|
-
const ct = state.currentTrack
|
|
1120
|
-
if (ct) np.queue.add(ct)
|
|
1121
|
-
for (const q of state.queue) if (q !== ct) np.queue.add(q)
|
|
1122
|
-
|
|
1123
|
-
if (ct) {
|
|
1124
|
-
await np.play()
|
|
1125
|
-
if (state.position > 5000)
|
|
1126
|
-
np._createTimer(
|
|
1127
|
-
() => !np.destroyed && np.seek(state.position),
|
|
1128
|
-
SEEK_DELAY
|
|
1129
|
-
)
|
|
1130
|
-
if (state.paused)
|
|
1131
|
-
np._createTimer(() => !np.destroyed && np.pause(true), PAUSE_DELAY)
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
_functions.clearTimers(reconnectTimers)
|
|
1135
|
-
this._reconnecting = false
|
|
1136
|
-
aqua.emit(AqualinkEvents.PlayerReconnected, np, {
|
|
1137
|
-
oldPlayer: this,
|
|
1138
|
-
restoredState: state
|
|
1139
|
-
})
|
|
1140
|
-
} catch (error) {
|
|
1141
|
-
const retriesLeft = RECONNECT_MAX - attempt
|
|
1142
|
-
aqua.emit(AqualinkEvents.ReconnectionFailed, this, {
|
|
1143
|
-
error,
|
|
1144
|
-
code,
|
|
1145
|
-
payload,
|
|
1146
|
-
retriesLeft
|
|
1147
|
-
})
|
|
1148
|
-
|
|
1149
|
-
if (retriesLeft > 0) {
|
|
1150
|
-
_functions.createTimer(
|
|
1151
|
-
() => tryReconnect(attempt + 1),
|
|
1152
|
-
Math.min(RETRY_BACKOFF_BASE * attempt, RETRY_BACKOFF_MAX),
|
|
1153
|
-
reconnectTimers
|
|
1154
|
-
)
|
|
1155
|
-
} else {
|
|
1156
|
-
_functions.clearTimers(reconnectTimers)
|
|
1157
|
-
this._reconnecting = false
|
|
1158
|
-
aqua.emit(AqualinkEvents.SocketClosed, this, payload)
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
tryReconnect(1)
|
|
1180
|
+
return this._lifecycleController.socketClosed(_player, _track, payload)
|
|
1164
1181
|
}
|
|
1165
1182
|
|
|
1166
1183
|
send(data) {
|
|
1167
1184
|
try {
|
|
1168
|
-
this.aqua
|
|
1185
|
+
if (this.aqua?.queueVoiceStateUpdate) {
|
|
1186
|
+
return this.aqua.queueVoiceStateUpdate(data)
|
|
1187
|
+
} else {
|
|
1188
|
+
this.aqua.send({ op: 4, d: data })
|
|
1189
|
+
return true
|
|
1190
|
+
}
|
|
1169
1191
|
} catch (err) {
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
new Error(`Send fail: ${err.message}`)
|
|
1192
|
+
_functions.emitAquaError(
|
|
1193
|
+
this.aqua,
|
|
1194
|
+
new Error(`Send fail (guild=${this.guildId}): ${err.message}`)
|
|
1173
1195
|
)
|
|
1196
|
+
return false
|
|
1174
1197
|
}
|
|
1175
1198
|
}
|
|
1176
1199
|
|
|
@@ -1203,24 +1226,7 @@ class Player extends EventEmitter {
|
|
|
1203
1226
|
}
|
|
1204
1227
|
|
|
1205
1228
|
_flushDeferredPlay() {
|
|
1206
|
-
|
|
1207
|
-
!this._deferredStart ||
|
|
1208
|
-
this.destroyed ||
|
|
1209
|
-
!this.current?.track ||
|
|
1210
|
-
!this._updateBatcher
|
|
1211
|
-
)
|
|
1212
|
-
return
|
|
1213
|
-
this._deferredStart = false
|
|
1214
|
-
const updateData = {
|
|
1215
|
-
track: { encoded: this.current.track },
|
|
1216
|
-
paused: this.paused
|
|
1217
|
-
}
|
|
1218
|
-
if (this.position > 0) updateData.position = this.position
|
|
1219
|
-
this.aqua?._trace?.('player.play.deferred.flush', {
|
|
1220
|
-
guildId: this.guildId,
|
|
1221
|
-
hasEndpoint: !!this.connection?.endpoint
|
|
1222
|
-
})
|
|
1223
|
-
this.batchUpdatePlayer(updateData, true).catch(() => {})
|
|
1229
|
+
return this._lifecycleController.flushDeferredPlay()
|
|
1224
1230
|
}
|
|
1225
1231
|
|
|
1226
1232
|
cleanup() {
|