aqualink 2.20.1 → 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.
@@ -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
  }
@@ -105,11 +124,10 @@ class MicrotaskUpdateBatcher {
105
124
  this.scheduled = false
106
125
  if (!u || !p) return Promise.resolve()
107
126
  return p.updatePlayer(u).catch((err) => {
108
- p.aqua?.emit?.(
109
- AqualinkEvents.Error,
127
+ _functions.emitAquaError(
128
+ p.aqua,
110
129
  new Error(`Update error: ${err.message}`)
111
130
  )
112
- throw err
113
131
  })
114
132
  }
115
133
 
@@ -190,11 +208,11 @@ class Player extends EventEmitter {
190
208
  this.mute = !!options.mute
191
209
  this.autoplayRetries = this.reconnectionRetries = 0
192
210
  this._voiceDownSince = 0
193
- this._voiceRecovering = this._reconnecting = false
194
- this._resuming = !!options.resuming
211
+ attachPlayerLifecycleState(this, { resuming: !!options.resuming })
195
212
  this._voiceWatchdogTimer = null
196
213
  this._pendingTimers = new Set()
197
214
  this._reconnectTimers = null
215
+ this._reconnectNonce = 0
198
216
  this._dataStore = null
199
217
 
200
218
  this.volume = _functions.clamp(options.defaultVolume || 100)
@@ -210,12 +228,30 @@ class Player extends EventEmitter {
210
228
  this.previousIdentifiers = new Set()
211
229
  this.previousTracks = new CircularBuffer(PREVIOUS_TRACKS_SIZE)
212
230
  this._updateBatcher = batcherPool.acquire(this)
231
+ this._lifecycleController = new PlayerLifecycle(this, {
232
+ _functions,
233
+ PLAYER_STATE,
234
+ VOICE_TRACE_INTERVAL,
235
+ PLAYER_UPDATE_SILENCE_THRESHOLD,
236
+ VOICE_DOWN_THRESHOLD,
237
+ VOICE_ABANDON_MULTIPLIER,
238
+ VOICE_FORCE_DESTROY_MS,
239
+ RECONNECT_MAX,
240
+ MUTE_TOGGLE_DELAY,
241
+ SEEK_DELAY,
242
+ PAUSE_DELAY,
243
+ RETRY_BACKOFF_BASE,
244
+ RETRY_BACKOFF_MAX
245
+ })
213
246
 
214
247
  this._voiceRequestAt = 0
215
248
  this._voiceRequestChannel = null
216
249
  this._suppressResumeUntil = 0
217
- this._deferredStart = false
218
250
  this._lastVoiceUpTraceAt = 0
251
+ this._lastPlayerUpdateAt = Date.now()
252
+ this._voiceRecoverySeq = 0
253
+ this._activeVoiceRecoveryToken = 0
254
+ this._voiceRecoveryReason = null
219
255
  this._bindEvents()
220
256
  this._startWatchdog()
221
257
  }
@@ -252,58 +288,26 @@ class Player extends EventEmitter {
252
288
  return new Promise((r) => this._createTimer(r, ms))
253
289
  }
254
290
 
255
- _handlePlayerUpdate(packet) {
256
- if (this.destroyed || !packet?.state) return
257
- const s = packet.state
258
- const wasConnected = this.connected
259
- this.position = _functions.isNum(s.position) ? s.position : 0
260
- this.connected = !!s.connected
261
- this.ping = _functions.isNum(s.ping) ? s.ping : 0
262
- this.timestamp = _functions.isNum(s.time) ? s.time : Date.now()
263
-
264
- if (!this.connected) {
265
- if (wasConnected || !this._voiceDownSince) {
266
- this.aqua?._trace?.('player.voice.down', {
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
- }
291
+ _claimVoiceRecovery(reason = 'unknown') {
292
+ const token = ++this._voiceRecoverySeq
293
+ this._activeVoiceRecoveryToken = token
294
+ this._voiceRecoveryReason = reason
295
+ return token
296
+ }
297
+
298
+ _isVoiceRecoveryActive(token) {
299
+ return !!token && !this.destroyed && this._activeVoiceRecoveryToken === token
300
+ }
301
+
302
+ _clearVoiceRecovery(token = this._activeVoiceRecoveryToken, reason = null) {
303
+ if (!token || this._activeVoiceRecoveryToken !== token) return false
304
+ this._activeVoiceRecoveryToken = 0
305
+ this._voiceRecoveryReason = reason
306
+ return true
307
+ }
305
308
 
306
- this.aqua.emit(AqualinkEvents.PlayerUpdate, this, packet)
309
+ _handlePlayerUpdate(packet) {
310
+ return this._lifecycleController.handlePlayerUpdate(packet)
307
311
  }
308
312
 
309
313
  async _handleEvent(payload) {
@@ -318,13 +322,9 @@ class Player extends EventEmitter {
318
322
  return
319
323
  }
320
324
  try {
321
- const trackArg =
322
- payload.type === 'TrackStartEvent'
323
- ? payload.track || this.current
324
- : this.current
325
- await this[handler](this, trackArg, payload)
325
+ await this[handler](this, this.current, payload)
326
326
  } catch (error) {
327
- this.aqua.emit(AqualinkEvents.Error, error)
327
+ _functions.emitAquaError(this.aqua, error)
328
328
  }
329
329
  }
330
330
 
@@ -370,30 +370,85 @@ class Player extends EventEmitter {
370
370
  }
371
371
  this.current = resolvedItem
372
372
  if (this.destroyed) return this
373
- if (!this.current?.track) throw new Error('Failed to resolve track')
373
+ if (!this.current?.track) {
374
+ this.current = null
375
+ this.playing = false
376
+ if (this.aqua?.debugTrace) {
377
+ this.aqua._trace('player.play.unresolved', {
378
+ guildId: this.guildId,
379
+ reconnecting: !!this._reconnecting,
380
+ resuming: !!this._resuming,
381
+ voiceRecovering: !!this._voiceRecovering
382
+ })
383
+ }
384
+ if (this._reconnecting || this._resuming || this._voiceRecovering)
385
+ return this
386
+ throw new Error('Failed to resolve track')
387
+ }
374
388
 
375
389
  this.playing = true
376
390
  this.paused = !!options.paused
377
391
  this.position = options.startTime || 0
378
- this.aqua?._trace?.('player.play', {
379
- guildId: this.guildId,
380
- paused: this.paused,
381
- startTime: this.position,
382
- hasTrack: !!this.current?.track
383
- })
392
+ if (this.aqua?.debugTrace) {
393
+ this.aqua._trace('player.play', {
394
+ guildId: this.guildId,
395
+ paused: this.paused,
396
+ startTime: this.position,
397
+ hasTrack: !!this.current?.track
398
+ })
399
+ }
384
400
 
385
401
  if (this.destroyed || !this._updateBatcher) return this
386
402
 
403
+ if (
404
+ this.voiceChannel &&
405
+ !this.connected &&
406
+ !this._reconnecting &&
407
+ !this._voiceRecovering
408
+ ) {
409
+ this._deferredStart = true
410
+ const recoveryToken = this._claimVoiceRecovery('play_deferred')
411
+ if (this.aqua?.debugTrace) {
412
+ this.aqua._trace('player.play.deferred', {
413
+ guildId: this.guildId,
414
+ reason: 'voice_not_connected'
415
+ })
416
+ }
417
+ const now = Date.now()
418
+ if (
419
+ now - (this._voiceRequestAt || 0) >= 1200 &&
420
+ this._isVoiceRecoveryActive(recoveryToken)
421
+ ) {
422
+ this._voiceRequestAt = now
423
+ if (this._isVoiceRecoveryActive(recoveryToken))
424
+ this.connection?._requestVoiceState?.()
425
+ if (this._isVoiceRecoveryActive(recoveryToken))
426
+ this.connection?.resendVoiceUpdate?.(true)
427
+ if (this._isVoiceRecoveryActive(recoveryToken))
428
+ _functions.safeCall(() =>
429
+ this.connect({
430
+ guildId: this.guildId,
431
+ voiceChannel: this.voiceChannel,
432
+ deaf: this.deaf,
433
+ mute: this.mute
434
+ })
435
+ )
436
+ }
437
+ return this
438
+ }
439
+
387
440
  if (
388
441
  this.aqua?.autoRegionMigrate &&
389
442
  !this._resuming &&
390
443
  !this.connection?.endpoint
391
444
  ) {
392
445
  this._deferredStart = true
393
- this.aqua?._trace?.('player.play.deferred', {
394
- guildId: this.guildId,
395
- reason: 'awaiting_voice_server_update'
396
- })
446
+ if (this.aqua?.debugTrace) {
447
+ this.aqua._trace('player.play.deferred', {
448
+ guildId: this.guildId,
449
+ reason: 'awaiting_voice_server_update'
450
+ })
451
+ }
397
452
  return this
398
453
  }
399
454
 
@@ -404,10 +459,26 @@ class Player extends EventEmitter {
404
459
  if (this.position > 0) updateData.position = this.position
405
460
 
406
461
  this._deferredStart = false
407
- await this.batchUpdatePlayer(updateData, true)
462
+ await this.batchUpdatePlayer(updateData, true).catch((err) => {
463
+ if (!this.destroyed) _functions.emitAquaError(this.aqua, err)
464
+ })
408
465
  } catch (error) {
409
- if (!this.destroyed) this.aqua?.emit(AqualinkEvents.Error, error)
410
- if (this.queue?.size && !track) return this.play()
466
+ if (
467
+ !this.destroyed &&
468
+ !this._reconnecting &&
469
+ !this._resuming &&
470
+ !this._voiceRecovering
471
+ ) {
472
+ _functions.emitAquaError(this.aqua, error)
473
+ }
474
+ if (
475
+ this.queue?.size &&
476
+ !track &&
477
+ !this._reconnecting &&
478
+ !this._resuming &&
479
+ !this._voiceRecovering
480
+ )
481
+ return this.play()
411
482
  }
412
483
  return this
413
484
  }
@@ -429,19 +500,22 @@ class Player extends EventEmitter {
429
500
  this._voiceRequestChannel = voiceChannel
430
501
 
431
502
  this.voiceChannel = voiceChannel
503
+ this._voiceDownSince = 0
432
504
  this.send({
433
505
  guild_id: this.guildId,
434
506
  channel_id: voiceChannel,
435
507
  self_deaf: this.deaf,
436
508
  self_mute: this.mute
437
509
  })
438
- this.aqua?._trace?.('player.connect.request', {
439
- guildId: this.guildId,
440
- txId: this.txId,
441
- voiceChannel,
442
- deaf: this.deaf,
443
- mute: this.mute
444
- })
510
+ if (this.aqua?.debugTrace) {
511
+ this.aqua._trace('player.connect.request', {
512
+ guildId: this.guildId,
513
+ txId: this.txId,
514
+ voiceChannel,
515
+ deaf: this.deaf,
516
+ mute: this.mute
517
+ })
518
+ }
445
519
  return this
446
520
  }
447
521
 
@@ -464,50 +538,7 @@ class Player extends EventEmitter {
464
538
  }
465
539
 
466
540
  async _voiceWatchdog() {
467
- if (!this._shouldAttemptVoiceRecovery()) return
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
- }
541
+ return this._lifecycleController.voiceWatchdog()
511
542
  }
512
543
 
513
544
  destroy(options = {}) {
@@ -518,16 +549,29 @@ class Player extends EventEmitter {
518
549
  preserveReconnecting = false,
519
550
  preserveTracks = false
520
551
  } = options
521
- if (this.destroyed && !this.queue) return this
552
+ if (this.destroyed && !this.queue) {
553
+ this._reconnectNonce++
554
+ if (this._reconnectTimers) {
555
+ _functions.clearTimers(this._reconnectTimers)
556
+ this._reconnectTimers = null
557
+ }
558
+ this._reconnecting = false
559
+ this._isActivelyReconnecting = false
560
+ return this
561
+ }
522
562
 
523
563
  if (!this.destroyed) {
564
+ this._reconnectNonce++
524
565
  this.destroyed = true
525
- this.aqua?._trace?.('player.destroy', {
526
- guildId: this.guildId,
527
- skipRemote: !!skipRemote,
528
- preserveTracks: !!preserveTracks,
529
- preserveReconnecting: !!preserveReconnecting
530
- })
566
+ this._clearVoiceRecovery(undefined, 'destroyed')
567
+ if (this.aqua?.debugTrace) {
568
+ this.aqua._trace('player.destroy', {
569
+ guildId: this.guildId,
570
+ skipRemote: !!skipRemote,
571
+ preserveTracks: !!preserveTracks,
572
+ preserveReconnecting: !!preserveReconnecting
573
+ })
574
+ }
531
575
  this.emit('destroy')
532
576
  }
533
577
 
@@ -553,6 +597,7 @@ class Player extends EventEmitter {
553
597
  this._lastVoiceChannel = this.voiceChannel
554
598
  this._lastTextChannel = this.textChannel
555
599
  this.voiceChannel = null
600
+ this._isActivelyReconnecting = false
556
601
 
557
602
  if (
558
603
  this.shouldDeleteMessage &&
@@ -574,6 +619,16 @@ class Player extends EventEmitter {
574
619
  this._updateBatcher = null
575
620
  }
576
621
 
622
+ if (this.filters) {
623
+ try {
624
+ this.filters.destroy()
625
+ } catch (error) {
626
+ reportSuppressedError(this, 'player.destroy.filters', error, {
627
+ guildId: this.guildId
628
+ })
629
+ }
630
+ }
631
+
577
632
  this.previousTracks?.clear()
578
633
  this.previousTracks = null
579
634
  this.previousIdentifiers?.clear()
@@ -592,17 +647,34 @@ class Player extends EventEmitter {
592
647
  if (this.connection) {
593
648
  try {
594
649
  this.connection.destroy()
595
- } catch {}
650
+ } catch (error) {
651
+ reportSuppressedError(this, 'player.destroy.connection', error, {
652
+ guildId: this.guildId
653
+ })
654
+ }
596
655
  }
597
- this.connection = this.filters = this.current = this.autoplaySeed = null
656
+ this.connection =
657
+ this.filters =
658
+ this.current =
659
+ this.autoplaySeed =
660
+ this._lifecycleController =
661
+ null
598
662
 
599
663
  if (!skipRemote) {
600
664
  try {
601
665
  this.send({ guild_id: this.guildId, channel_id: null })
602
666
  this.aqua?.destroyPlayer?.(this.guildId)
603
667
  if (this.nodes?.connected)
604
- this.nodes.rest?.destroyPlayer(this.guildId).catch(() => {})
605
- } catch {}
668
+ this.nodes.rest?.destroyPlayer(this.guildId).catch((error) =>
669
+ reportSuppressedError(this, 'player.destroy.remote', error, {
670
+ guildId: this.guildId
671
+ })
672
+ )
673
+ } catch (error) {
674
+ reportSuppressedError(this, 'player.destroy.gateway', error, {
675
+ guildId: this.guildId
676
+ })
677
+ }
606
678
  }
607
679
 
608
680
  if (!preserveClient) this.aqua = this.nodes = null
@@ -612,7 +684,12 @@ class Player extends EventEmitter {
612
684
  pause(paused) {
613
685
  if (this.destroyed || this.paused === !!paused) return this
614
686
  this.paused = !!paused
615
- this.batchUpdatePlayer({ paused: this.paused }, true).catch(() => {})
687
+ this.batchUpdatePlayer({ paused: this.paused }, true).catch((error) =>
688
+ reportSuppressedError(this, 'player.pause', error, {
689
+ guildId: this.guildId,
690
+ paused: this.paused
691
+ })
692
+ )
616
693
  return this
617
694
  }
618
695
 
@@ -624,7 +701,12 @@ class Player extends EventEmitter {
624
701
  ? Math.min(Math.max(position, 0), len)
625
702
  : Math.max(position, 0)
626
703
  this.position = clamped
627
- this.batchUpdatePlayer({ position: clamped }, true).catch(() => {})
704
+ this.batchUpdatePlayer({ position: clamped }, true).catch((error) =>
705
+ reportSuppressedError(this, 'player.seek', error, {
706
+ guildId: this.guildId,
707
+ position: clamped
708
+ })
709
+ )
628
710
  return this
629
711
  }
630
712
 
@@ -678,7 +760,11 @@ class Player extends EventEmitter {
678
760
  this.batchUpdatePlayer(
679
761
  { track: { encoded: null }, paused: this.paused },
680
762
  true
681
- ).catch(() => {})
763
+ ).catch((error) =>
764
+ reportSuppressedError(this, 'player.stop', error, {
765
+ guildId: this.guildId
766
+ })
767
+ )
682
768
  return this
683
769
  }
684
770
 
@@ -686,7 +772,12 @@ class Player extends EventEmitter {
686
772
  const vol = _functions.clamp(volume)
687
773
  if (this.destroyed || this.volume === vol) return this
688
774
  this.volume = vol
689
- this.batchUpdatePlayer({ volume: vol }).catch(() => {})
775
+ this.batchUpdatePlayer({ volume: vol }).catch((error) =>
776
+ reportSuppressedError(this, 'player.setVolume', error, {
777
+ guildId: this.guildId,
778
+ volume: vol
779
+ })
780
+ )
690
781
  return this
691
782
  }
692
783
 
@@ -703,7 +794,6 @@ class Player extends EventEmitter {
703
794
  const id = _functions.toId(channel)
704
795
  if (!id) throw new TypeError('Invalid text channel')
705
796
  this.textChannel = id
706
- this.batchUpdatePlayer({ text_channel: id }).catch(() => {})
707
797
  return this
708
798
  }
709
799
 
@@ -739,7 +829,31 @@ class Player extends EventEmitter {
739
829
  replay() {
740
830
  return this.seek(0)
741
831
  }
742
- skip() {
832
+ skip(target) {
833
+ if (this.destroyed || !this.playing) return this
834
+
835
+ if (target === undefined || target === null) return this.stop()
836
+
837
+ if (typeof target === 'number') {
838
+ const idx = target | 0
839
+ if (idx <= 0) return this.stop()
840
+ if (!this.queue?.size || idx >= this.queue.size) return this.stop()
841
+ for (let i = 0; i < idx; i++) this.queue.dequeue()
842
+ return this.stop()
843
+ }
844
+
845
+ const targetId = _functions.toId(target)
846
+ if (targetId && this.queue?.size) {
847
+ const arr = this.queue.toArray()
848
+ const idx = arr.findIndex(
849
+ (t) =>
850
+ _functions.toId(t) === targetId ||
851
+ _functions.toId(t?.info?.identifier) === targetId
852
+ )
853
+ if (idx > 0) {
854
+ for (let i = 0; i < idx; i++) this.queue.dequeue()
855
+ }
856
+ }
743
857
  return this.stop()
744
858
  }
745
859
 
@@ -835,8 +949,8 @@ class Player extends EventEmitter {
835
949
  }
836
950
  } catch (err) {
837
951
  if (this.destroyed) return this
838
- this.aqua?.emit(
839
- AqualinkEvents.Error,
952
+ _functions.emitAquaError(
953
+ this.aqua,
840
954
  new Error(`Autoplay ${i + 1} fail: ${err.message}`)
841
955
  )
842
956
  }
@@ -858,7 +972,7 @@ class Player extends EventEmitter {
858
972
  }
859
973
 
860
974
  async _getAutoplayTrack(sourceName, identifier, uri, requester) {
861
- if (sourceName === 'youtube') {
975
+ if (sourceName === 'youtube' || sourceName === 'ytmusic') {
862
976
  const res = await this.aqua.resolve({
863
977
  query: `https://www.youtube.com/watch?v=${identifier}&list=RD${identifier}`,
864
978
  source: 'ytmsearch',
@@ -930,12 +1044,15 @@ class Player extends EventEmitter {
930
1044
  return
931
1045
  }
932
1046
 
933
- if (
934
- track &&
935
- reason === 'finished' &&
936
- (this.loop === LOOP_MODES.TRACK || this.loop === LOOP_MODES.QUEUE)
937
- ) {
938
- this.queue.add(track)
1047
+ if (track && reason === 'finished') {
1048
+ if (this.loop === LOOP_MODES.TRACK) {
1049
+ this.aqua.emit(AqualinkEvents.TrackEnd, this, track, reason)
1050
+ await this.play(track)
1051
+ return
1052
+ }
1053
+ if (this.loop === LOOP_MODES.QUEUE) {
1054
+ this.queue.add(track)
1055
+ }
939
1056
  }
940
1057
 
941
1058
  if (this.queue.size) {
@@ -1007,170 +1124,27 @@ class Player extends EventEmitter {
1007
1124
  }
1008
1125
 
1009
1126
  async _attemptVoiceResume() {
1010
- if (!this.connection?.sessionId) throw new Error('No session')
1011
- if (!(await this.connection.attemptResume()))
1012
- throw new Error('Resume failed')
1127
+ return this._lifecycleController.attemptVoiceResume()
1013
1128
  }
1014
1129
 
1015
1130
  async socketClosed(_player, _track, payload) {
1016
- if (this.destroyed || this._reconnecting) return
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)
1131
+ return this._lifecycleController.socketClosed(_player, _track, payload)
1164
1132
  }
1165
1133
 
1166
1134
  send(data) {
1167
1135
  try {
1168
- this.aqua.send({ op: 4, d: data })
1136
+ if (this.aqua?.queueVoiceStateUpdate) {
1137
+ return this.aqua.queueVoiceStateUpdate(data)
1138
+ } else {
1139
+ this.aqua.send({ op: 4, d: data })
1140
+ return true
1141
+ }
1169
1142
  } catch (err) {
1170
- this.aqua.emit(
1171
- AqualinkEvents.Error,
1143
+ _functions.emitAquaError(
1144
+ this.aqua,
1172
1145
  new Error(`Send fail: ${err.message}`)
1173
1146
  )
1147
+ return false
1174
1148
  }
1175
1149
  }
1176
1150
 
@@ -1203,24 +1177,7 @@ class Player extends EventEmitter {
1203
1177
  }
1204
1178
 
1205
1179
  _flushDeferredPlay() {
1206
- if (
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(() => {})
1180
+ return this._lifecycleController.flushDeferredPlay()
1224
1181
  }
1225
1182
 
1226
1183
  cleanup() {