aqualink 2.18.1 → 2.19.1

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.
@@ -1,904 +1,1143 @@
1
- 'use strict'
2
-
3
- const { EventEmitter } = require('tseep')
4
- const { AqualinkEvents } = require('./AqualinkEvents')
5
- const Connection = require('./Connection')
6
- const Filters = require('./Filters')
7
- const { spAutoPlay, scAutoPlay } = require('../handlers/autoplay')
8
- const Queue = require('./Queue')
9
-
10
- const PLAYER_STATE = Object.freeze({ IDLE: 0, CONNECTING: 1, READY: 2, DISCONNECTING: 3, DESTROYED: 4 })
11
- const LOOP_MODES = Object.freeze({ NONE: 0, TRACK: 1, QUEUE: 2 })
12
- const LOOP_MODE_NAMES = Object.freeze(['none', 'track', 'queue'])
13
- const EVENT_HANDLERS = Object.freeze({
14
- TrackStartEvent: 'trackStart',
15
- TrackEndEvent: 'trackEnd',
16
- TrackExceptionEvent: 'trackError',
17
- TrackStuckEvent: 'trackStuck',
18
- TrackChangeEvent: 'trackChange',
19
- WebSocketClosedEvent: 'socketClosed',
20
- LyricsLineEvent: 'lyricsLine',
21
- LyricsFoundEvent: 'lyricsFound',
22
- VolumeChangedEvent: 'volumeChanged',
23
- FiltersChangedEvent: 'filtersChanged',
24
- SeekEvent: 'seekEvent',
25
- PlayerCreatedEvent: 'playerCreated',
26
- PauseEvent: 'pauseEvent',
27
- PlayerConnectedEvent: 'playerConnected',
28
- PlayerDestroyedEvent: 'playerDestroyed',
29
- LyricsNotFoundEvent: 'lyricsNotFound',
30
- MixStartedEvent: 'mixStarted',
31
- MixEndedEvent: 'mixEnded'
32
- })
33
-
34
- const WATCHDOG_INTERVAL = 15000
35
- const VOICE_DOWN_THRESHOLD = 10000
36
- const VOICE_ABANDON_MULTIPLIER = 12
37
- const RECONNECT_MAX = 15
38
- const RESUME_TIMEOUT = 5000
39
- const MUTE_TOGGLE_DELAY = 300
40
- const SEEK_DELAY = 800
41
- const PAUSE_DELAY = 1200
42
- const RETRY_BACKOFF_BASE = 1500
43
- const RETRY_BACKOFF_MAX = 5000
44
- const PREVIOUS_TRACKS_SIZE = 50
45
- const PREVIOUS_IDS_MAX = 20
46
- const AUTOPLAY_MAX = 3
47
- const BATCHER_POOL_SIZE = 2
48
- const INVALID_LOADS = new Set(['error', 'empty', 'LOAD_FAILED', 'NO_MATCHES'])
49
-
50
- const _functions = {
51
- clamp(v) {
52
- const n = +v
53
- return Number.isNaN(n) ? 100 : n < 0 ? 0 : n > 200 ? 200 : n
54
- },
55
- randIdx: len => Math.random() * len | 0,
56
- toId: v => v?.id || v || null,
57
- isNum: v => typeof v === 'number' && !Number.isNaN(v),
58
- isInvalidLoad: r => !r?.tracks?.length || INVALID_LOADS.has(r.loadType),
59
- safeDel: msg => msg?.delete?.().catch(() => { }),
60
- createTimer(fn, delay, timerSet, unref = true) {
61
- const t = setTimeout(() => {
62
- timerSet?.delete(t)
63
- fn()
64
- }, delay)
65
- if (unref) t.unref?.()
66
- timerSet?.add(t)
67
- return t
68
- },
69
- clearTimers(set) {
70
- if (!set) return
71
- for (const t of set) clearTimeout(t)
72
- set.clear()
73
- },
74
- emitIfActive(player, event, ...args) {
75
- if (!player.destroyed) player.aqua.emit(event, player, ...args)
76
- }
77
- }
78
-
79
- class MicrotaskUpdateBatcher {
80
- constructor(player) {
81
- this.player = player
82
- this.updates = null
83
- this.scheduled = false
84
- }
85
-
86
- batch(data, immediate) {
87
- if (!this.player) return Promise.reject(new Error('Player destroyed'))
88
- this.updates = Object.assign(this.updates || {}, data)
89
- if (immediate || 'track' in data || 'paused' in data || 'position' in data) return this._flush()
90
- if (!this.scheduled) {
91
- this.scheduled = true
92
- queueMicrotask(() => this._flush())
93
- }
94
- return Promise.resolve()
95
- }
96
-
97
- _flush() {
98
- const { player: p, updates: u } = this
99
- this.updates = null
100
- this.scheduled = false
101
- if (!u || !p) return Promise.resolve()
102
- return p.updatePlayer(u).catch(err => {
103
- p.aqua?.emit?.(AqualinkEvents.Error, new Error(`Update error: ${err.message}`))
104
- throw err
105
- })
106
- }
107
-
108
- reset() {
109
- this.updates = null
110
- this.scheduled = false
111
- this.player = null
112
- }
113
- }
114
-
115
- const batcherPool = {
116
- pool: [],
117
- acquire(player) {
118
- const b = this.pool.pop()
119
- if (b) { b.player = player; return b }
120
- return new MicrotaskUpdateBatcher(player)
121
- },
122
- release(batcher) {
123
- if (this.pool.length < BATCHER_POOL_SIZE && batcher) {
124
- batcher.reset()
125
- this.pool.push(batcher)
126
- }
127
- }
128
- }
129
-
130
- class CircularBuffer {
131
- constructor(size) {
132
- this.buffer = new Array(size)
133
- this.size = size
134
- this.index = 0
135
- this.count = 0
136
- }
137
-
138
- push(item) {
139
- if (!item) return
140
- this.buffer[this.index] = item
141
- this.index = (this.index + 1) % this.size
142
- if (this.count < this.size) this.count++
143
- }
144
-
145
- getLast() {
146
- return this.count ? this.buffer[(this.index - 1 + this.size) % this.size] : null
147
- }
148
-
149
- clear() {
150
- if (!this.count) return
151
- this.buffer.fill(undefined)
152
- this.count = this.index = 0
153
- }
154
- }
155
-
156
- class Player extends EventEmitter {
157
- static LOOP_MODES = LOOP_MODES
158
- static EVENT_HANDLERS = EVENT_HANDLERS
159
-
160
- constructor(aqua, nodes, options) {
161
- super()
162
- if (!aqua || !nodes || !options.guildId) throw new TypeError('Missing required parameters')
163
-
164
- this.aqua = aqua
165
- this.nodes = nodes
166
- this.guildId = String(options.guildId)
167
- this.textChannel = options.textChannel
168
- this.voiceChannel = options.voiceChannel
169
- this.playing = this.paused = this.connected = this.destroyed = false
170
- this.state = PLAYER_STATE.IDLE
171
- this.txId = 0
172
- this.isAutoplayEnabled = this.isAutoplay = false
173
- this.autoplaySeed = this.current = this.nowPlayingMessage = null
174
- this.position = this.timestamp = this.ping = 0
175
- this.deaf = options.deaf !== false
176
- this.mute = !!options.mute
177
- this.autoplayRetries = this.reconnectionRetries = 0
178
- this._voiceDownSince = 0
179
- this._voiceRecovering = this._reconnecting = false
180
- this._resuming = !!options.resuming
181
- this._voiceWatchdogTimer = null
182
- this._pendingTimers = new Set()
183
- this._reconnectTimers = null
184
- this._dataStore = null
185
-
186
- this.volume = _functions.clamp(options.defaultVolume || 100)
187
- this.loop = this._parseLoop(options.loop)
188
-
189
- const aquaOpts = aqua.options || {}
190
- this.shouldDeleteMessage = !!aquaOpts.shouldDeleteMessage
191
- this.leaveOnEnd = !!aquaOpts.leaveOnEnd
192
-
193
- this.connection = new Connection(this)
194
- this.filters = new Filters(this)
195
- this.queue = new Queue()
196
- this.previousIdentifiers = new Set()
197
- this.previousTracks = new CircularBuffer(PREVIOUS_TRACKS_SIZE)
198
- this._updateBatcher = batcherPool.acquire(this)
199
-
200
- this._voiceRequestAt = 0
201
- this._voiceRequestChannel = null
202
- this._suppressResumeUntil = 0
203
- this._bindEvents()
204
- this._startWatchdog()
205
- }
206
-
207
- _parseLoop(loop) {
208
- if (typeof loop === 'string') {
209
- const idx = LOOP_MODE_NAMES.indexOf(loop)
210
- return idx >= 0 && idx <= 2 ? idx : 0
211
- }
212
- return loop >= 0 && loop <= 2 ? loop : 0
213
- }
214
-
215
- _bindEvents() {
216
- this._boundPlayerUpdate = this._handlePlayerUpdate.bind(this)
217
- this._boundEvent = this._handleEvent.bind(this)
218
- this._boundPlayerMove = this._handleAquaPlayerMove.bind(this)
219
-
220
- this.on('playerUpdate', this._boundPlayerUpdate)
221
- this.on('event', this._boundEvent)
222
- this.aqua.on('playerMove', this._boundPlayerMove)
223
- }
224
-
225
- _startWatchdog() {
226
- this._voiceWatchdogTimer = setInterval(() => this._voiceWatchdog(), WATCHDOG_INTERVAL)
227
- this._voiceWatchdogTimer.unref?.()
228
- }
229
-
230
- _createTimer(fn, delay, unref = true) {
231
- return _functions.createTimer(fn, delay, this._pendingTimers, unref)
232
- }
233
-
234
- _delay(ms) {
235
- return new Promise(r => this._createTimer(r, ms))
236
- }
237
-
238
- _handlePlayerUpdate(packet) {
239
- if (this.destroyed || !packet?.state) return
240
- const s = packet.state
241
- this.position = _functions.isNum(s.position) ? s.position : 0
242
- this.connected = !!s.connected
243
- this.ping = _functions.isNum(s.ping) ? s.ping : 0
244
- this.timestamp = _functions.isNum(s.time) ? s.time : Date.now()
245
-
246
- if (!this.connected) {
247
- if (!this._voiceDownSince) {
248
- this._voiceDownSince = Date.now()
249
- this._createTimer(() => {
250
- if (this.connected || this.destroyed || this.nodes?.info?.isNodelink) return
251
- this.connection.attemptResume()
252
- }, 1000)
253
- }
254
- } else {
255
- this._voiceDownSince = 0
256
- this.state = PLAYER_STATE.READY
257
- }
258
-
259
- this.aqua.emit(AqualinkEvents.PlayerUpdate, this, packet)
260
- }
261
-
262
- async _handleEvent(payload) {
263
- if (this.destroyed || !payload?.type) return
264
- const handler = EVENT_HANDLERS[payload.type]
265
- if (typeof this[handler] !== 'function') {
266
- this.aqua.emit(AqualinkEvents.NodeError, this, new Error(`Unknown event: ${payload.type}`))
267
- return
268
- }
269
- try {
270
- await this[handler](this, this.current, payload)
271
- } catch (error) {
272
- this.aqua.emit(AqualinkEvents.Error, error)
273
- }
274
- }
275
-
276
- get previous() {
277
- return this.previousTracks?.getLast() || null
278
- }
279
-
280
- get currenttrack() {
281
- return this.current
282
- }
283
-
284
- getQueue() {
285
- return this.queue
286
- }
287
-
288
- batchUpdatePlayer(data, immediate) {
289
- return this._updateBatcher.batch(data, immediate)
290
- }
291
-
292
- setAutoplay(enabled) {
293
- this.isAutoplayEnabled = !!enabled
294
- this.autoplayRetries = 0
295
- return this
296
- }
297
-
298
-
299
- async play() {
300
- if (this.destroyed || !this.queue) return this
301
- if (!this.queue.size) return this
302
-
303
- const item = this.queue.dequeue()
304
- if (!item) return this
305
-
306
- try {
307
- this.current = item.track ? item : await item.resolve(this.aqua)
308
- if (this.destroyed) return this
309
- if (!this.current?.track) throw new Error('Failed to resolve track')
310
-
311
- this.playing = true
312
- this.paused = false
313
- this.position = 0
314
- if (this.destroyed || !this._updateBatcher) return this
315
- await this.batchUpdatePlayer({ guildId: this.guildId, track: { encoded: this.current.track } }, true)
316
- } catch (error) {
317
- if (!this.destroyed) this.aqua?.emit(AqualinkEvents.Error, error)
318
- if (this.queue?.size) return this.play()
319
- }
320
- return this
321
- }
322
-
323
- connect(options = {}) {
324
- if (this.destroyed) throw new Error('Cannot connect destroyed player')
325
-
326
- const voiceChannel = _functions.toId(options.voiceChannel || this.voiceChannel)
327
- if (!voiceChannel) throw new TypeError('Voice channel required')
328
-
329
- this.deaf = options.deaf !== undefined ? !!options.deaf : true
330
- this.mute = !!options.mute
331
- this.destroyed = false
332
- this.state = PLAYER_STATE.CONNECTING
333
-
334
- this.txId++
335
- this._voiceRequestChannel = voiceChannel
336
-
337
- this.voiceChannel = voiceChannel
338
- this.send({
339
- guild_id: this.guildId,
340
- channel_id: voiceChannel,
341
- self_deaf: this.deaf,
342
- self_mute: this.mute
343
- })
344
- return this
345
- }
346
-
347
- _shouldAttemptVoiceRecovery() {
348
- if (this.nodes?.info?.isNodelink || this.destroyed || !this.voiceChannel || this.connected) return false
349
- if (!this._voiceDownSince || Date.now() - this._voiceDownSince < VOICE_DOWN_THRESHOLD) return false
350
- return !this._voiceRecovering && this.reconnectionRetries < RECONNECT_MAX
351
- }
352
-
353
- async _voiceWatchdog() {
354
- if (!this._shouldAttemptVoiceRecovery()) return
355
-
356
- const hasVoiceData = this.connection?.sessionId && this.connection?.endpoint && this.connection?.token
357
- if (!hasVoiceData) {
358
- if ((Date.now() - this._voiceDownSince) > (VOICE_DOWN_THRESHOLD * VOICE_ABANDON_MULTIPLIER)) this.destroy()
359
- return
360
- }
361
-
362
- this._voiceRecovering = true
363
- try {
364
- if (await this.connection.attemptResume()) {
365
- this.reconnectionRetries = this._voiceDownSince = 0
366
- return
367
- }
368
- const originalMute = this.mute
369
- this.send({ guild_id: this.guildId, channel_id: this.voiceChannel, self_deaf: this.deaf, self_mute: !originalMute })
370
- await this._delay(MUTE_TOGGLE_DELAY)
371
- if (!this.destroyed) {
372
- this.send({ guild_id: this.guildId, channel_id: this.voiceChannel, self_deaf: this.deaf, self_mute: originalMute })
373
- }
374
- this.connection.resendVoiceUpdate()
375
- this.reconnectionRetries++
376
- } catch {
377
- if (++this.reconnectionRetries >= RECONNECT_MAX) this.destroy()
378
- } finally {
379
- this._voiceRecovering = false
380
- }
381
- }
382
-
383
- destroy(options = {}) {
384
- const {
385
- preserveClient = true, skipRemote = false,
386
- preserveMessage = false, preserveReconnecting = false,
387
- preserveTracks = false
388
- } = options
389
- if (this.destroyed && !this.queue) return this
390
-
391
- if (!this.destroyed) {
392
- this.destroyed = true
393
- this.emit('destroy')
394
- }
395
-
396
- if (this._voiceWatchdogTimer) {
397
- clearInterval(this._voiceWatchdogTimer)
398
- this._voiceWatchdogTimer = null
399
- }
400
-
401
- _functions.clearTimers(this._pendingTimers)
402
- this._pendingTimers = null
403
-
404
- // Clear reconnection timers to prevent memory leaks when destroyed externally
405
- if (this._reconnectTimers) {
406
- _functions.clearTimers(this._reconnectTimers)
407
- this._reconnectTimers = null
408
- }
409
-
410
- this.connected = this.playing = this.paused = this.isAutoplay = false
411
- this.state = PLAYER_STATE.DESTROYED
412
- this.autoplayRetries = this.reconnectionRetries = 0
413
- if (!preserveReconnecting) this._reconnecting = false
414
- this._lastVoiceChannel = this.voiceChannel
415
- this.voiceChannel = null
416
-
417
- if (this.shouldDeleteMessage && this.nowPlayingMessage && !preserveMessage) {
418
- _functions.safeDel(this.nowPlayingMessage)
419
- this.nowPlayingMessage = null
420
- }
421
-
422
- if (this._boundPlayerUpdate) this.removeListener('playerUpdate', this._boundPlayerUpdate)
423
- if (this._boundEvent) this.removeListener('event', this._boundEvent)
424
- if (this.aqua && this._boundPlayerMove) this.aqua.removeListener('playerMove', this._boundPlayerMove)
425
- this._boundPlayerUpdate = this._boundEvent = this._boundPlayerMove = null
426
- this.removeAllListeners()
427
-
428
- if (this._updateBatcher) {
429
- batcherPool.release(this._updateBatcher)
430
- this._updateBatcher = null
431
- }
432
-
433
- this.previousTracks?.clear()
434
- this.previousTracks = null
435
- this.previousIdentifiers?.clear()
436
- this.previousIdentifiers = null
437
- this.queue?.clear()
438
- this.queue = null
439
- this._dataStore?.clear()
440
- this._dataStore = null
441
-
442
- if (this.current?.dispose && !this.aqua?.options?.autoResume && !preserveTracks) this.current.dispose()
443
- if (this.connection) {
444
- try { this.connection.destroy() } catch { }
445
- }
446
- this.connection = this.filters = this.current = this.autoplaySeed = null
447
-
448
- if (!skipRemote) {
449
- try {
450
- this.send({ guild_id: this.guildId, channel_id: null })
451
- this.aqua?.destroyPlayer?.(this.guildId)
452
- if (this.nodes?.connected) this.nodes.rest?.destroyPlayer(this.guildId).catch(() => { })
453
- } catch { }
454
- }
455
-
456
- if (!preserveClient) this.aqua = this.nodes = null
457
- return this
458
- }
459
-
460
- pause(paused) {
461
- if (this.destroyed || this.paused === !!paused) return this
462
- this.paused = !!paused
463
- this.batchUpdatePlayer({ guildId: this.guildId, paused: this.paused }, true).catch(() => { })
464
- return this
465
- }
466
-
467
- seek(position) {
468
- if (this.destroyed || !this.playing || !_functions.isNum(position)) return this
469
- const len = this.current?.info?.length || 0
470
- const clamped = len ? Math.min(Math.max(position, 0), len) : Math.max(position, 0)
471
- this.position = clamped
472
- this.batchUpdatePlayer({ guildId: this.guildId, position: clamped }, true).catch(() => { })
473
- return this
474
- }
475
-
476
- async getActiveMixer(guildId) {
477
- if (this.destroyed) return null
478
- return await this.nodes.rest.getActiveMixer(guildId)
479
- }
480
-
481
- async updateMixerVolume(guildId, mix, volume) {
482
- if (this.destroyed) return null
483
- return await this.nodes.rest.updateMixerVolume(guildId, mix, volume)
484
- }
485
-
486
- async removeMixer(guildId, mix) {
487
- if (this.destroyed) return null
488
- return await this.nodes.rest.removeMixer(guildId, mix)
489
- }
490
-
491
- async addMixer(guildId, options) {
492
- if (this.destroyed) return null
493
-
494
- if (options.identifier && !options.encoded) {
495
- try {
496
- const resolved = await this.aqua.resolve({
497
- query: options.identifier,
498
- requester: options.requester || this.current?.requester
499
- })
500
-
501
- if (resolved?.tracks?.[0]) {
502
- const track = resolved.tracks[0]
503
- options = {
504
- ...options,
505
- encoded: track.track || track.encoded,
506
- userData: options.userData
507
- }
508
- } else {
509
- throw new Error('Failed to resolve track identifier')
510
- }
511
- } catch (error) {
512
- throw new Error(`Failed to resolve track: ${error.message}`)
513
- }
514
- }
515
-
516
- return await this.nodes.rest.addMixer(guildId, options)
517
- }
518
-
519
- stop() {
520
- if (this.destroyed || !this.playing) return this
521
- this.playing = this.paused = false
522
- this.position = 0
523
- this.batchUpdatePlayer({ guildId: this.guildId, track: { encoded: null, paused: this.paused } }, true).catch(() => { })
524
- return this
525
- }
526
-
527
- setVolume(volume) {
528
- const vol = _functions.clamp(volume)
529
- if (this.destroyed || this.volume === vol) return this
530
- this.volume = vol
531
- this.batchUpdatePlayer({ guildId: this.guildId, volume: vol }).catch(() => { })
532
- return this
533
- }
534
-
535
- setLoop(mode) {
536
- if (this.destroyed) return this
537
- const idx = typeof mode === 'string' ? LOOP_MODE_NAMES.indexOf(mode) : mode
538
- if (idx < 0 || idx > 2) throw new Error('Invalid loop mode')
539
- this.loop = idx
540
- return this
541
- }
542
-
543
- setTextChannel(channel) {
544
- if (this.destroyed) return this
545
- const id = _functions.toId(channel)
546
- if (!id) throw new TypeError('Invalid text channel')
547
- this.textChannel = id
548
- this.batchUpdatePlayer({ guildId: this.guildId, text_channel: id }).catch(() => { })
549
- return this
550
- }
551
-
552
- setVoiceChannel(channel) {
553
- if (this.destroyed) return this
554
- const id = _functions.toId(channel)
555
- if (!id) throw new TypeError('Voice channel required')
556
- if (this.connected && id === _functions.toId(this.voiceChannel)) return this
557
- this.voiceChannel = id
558
- this.connect({ deaf: this.deaf, guildId: this.guildId, voiceChannel: id, mute: this.mute })
559
- return this
560
- }
561
-
562
- disconnect() {
563
- if (this.destroyed || !this.connected) return this
564
- this.connected = false
565
- this.voiceChannel = null
566
- this.send({ guild_id: this.guildId, channel_id: null })
567
- return this
568
- }
569
-
570
- shuffle() {
571
- if (this.destroyed || !this.queue?.size) return this
572
- this.queue.shuffle()
573
- return this
574
- }
575
-
576
- replay() { return this.seek(0) }
577
- skip() { return this.stop() }
578
-
579
- async getLyrics(options = {}) {
580
- if (this.destroyed || !this.nodes?.rest) return null
581
- const { query, useCurrentTrack = true, skipTrackSource = false } = options
582
- if (query) return this.nodes.rest.getLyrics({ track: { info: { title: query } }, skipTrackSource })
583
- if (useCurrentTrack && this.playing && this.current) {
584
- const info = this.current.info
585
- return this.nodes.rest.getLyrics({
586
- track: { info, encoded: this.current.track, identifier: info.identifier, guild_id: this.guildId },
587
- skipTrackSource
588
- })
589
- }
590
- return null
591
- }
592
-
593
- getLoadLyrics(encodedTrack) {
594
- return (this.destroyed || !this.nodes?.rest) ? null : this.nodes.rest.getLoadLyrics(encodedTrack)
595
- }
596
-
597
- subscribeLiveLyrics() {
598
- return this.destroyed ? Promise.reject(new Error('Player destroyed')) : this.nodes?.rest?.subscribeLiveLyrics(this.guildId, false)
599
- }
600
-
601
- unsubscribeLiveLyrics() {
602
- return this.destroyed ? Promise.reject(new Error('Player destroyed')) : this.nodes?.rest?.unsubscribeLiveLyrics(this.guildId)
603
- }
604
-
605
- async autoplay() {
606
- if (this.destroyed || !this.isAutoplayEnabled || !this.previous || (this.queue && this.queue.size)) return this
607
- const prev = this.previous
608
- const info = prev?.info
609
- if (!info?.sourceName || !info.identifier) return this
610
- const { sourceName, identifier, uri, author } = info
611
- this.isAutoplay = true
612
-
613
- if (sourceName === 'spotify' && info.identifier) {
614
- this.previousIdentifiers.add(info.identifier)
615
- if (this.previousIdentifiers.size > PREVIOUS_IDS_MAX) {
616
- this.previousIdentifiers.delete(this.previousIdentifiers.values().next().value)
617
- }
618
- if (!this.autoplaySeed) {
619
- this.autoplaySeed = { trackId: identifier, artistIds: Array.isArray(author) ? author.join(',') : author }
620
- }
621
- }
622
-
623
- for (let i = 0; !this.destroyed && i < AUTOPLAY_MAX && (this.queue && !this.queue.size); i++) {
624
- try {
625
- const track = await this._getAutoplayTrack(sourceName, identifier, uri, prev.requester)
626
- if (this.destroyed || !this.queue) return this
627
- if (track?.info?.title) {
628
- this.autoplayRetries = 0
629
- track.requester = prev.requester || { id: 'Unknown' }
630
- this.queue.add(track)
631
- await this.play()
632
- return this
633
- }
634
- } catch (err) {
635
- if (this.destroyed) return this
636
- this.aqua?.emit(AqualinkEvents.Error, new Error(`Autoplay ${i + 1} fail: ${err.message}`))
637
- }
638
- }
639
-
640
- if (this.destroyed) return this
641
- this.aqua?.emit(AqualinkEvents.AutoplayFailed, this, new Error('Max retries'))
642
- this.stop()
643
- return this
644
- }
645
-
646
- async liveLyrics(guildId, state) {
647
- if (state) return await this.nodes.rest.subscribeLiveLyrics(guildId)
648
- else return await this.nodes.rest.unsubscribeLiveLyrics(guildId)
649
- }
650
-
651
- async _getAutoplayTrack(sourceName, identifier, uri, requester) {
652
- if (sourceName === 'youtube') {
653
- const res = await this.aqua.resolve({
654
- query: `https://www.youtube.com/watch?v=${identifier}&list=RD${identifier}`,
655
- source: 'ytmsearch',
656
- requester
657
- })
658
- return _functions.isInvalidLoad(res) ? null : res.tracks[_functions.randIdx(res.tracks.length)]
659
- }
660
- if (sourceName === 'soundcloud') {
661
- const scRes = await scAutoPlay(uri)
662
- if (!scRes?.length) return null
663
- const res = await this.aqua.resolve({ query: scRes[0], source: 'scsearch', requester })
664
- return _functions.isInvalidLoad(res) ? null : res.tracks[_functions.randIdx(res.tracks.length)]
665
- }
666
- if (sourceName === 'spotify') {
667
- const res = await spAutoPlay(this.autoplaySeed, this, requester, Array.from(this.previousIdentifiers))
668
- return res?.length ? res[_functions.randIdx(res.length)] : null
669
- }
670
- return null
671
- }
672
-
673
- trackStart(player, track, payload = {}) {
674
- if (this.destroyed) return
675
- this.playing = true
676
- this.paused = false
677
- this.aqua.emit(AqualinkEvents.TrackStart, this, this.current, { ...payload, resumed: this._resuming })
678
- this._resuming = false
679
- }
680
-
681
- async trackEnd(player, track, payload) {
682
- if (this.destroyed) return
683
-
684
- const reason = payload?.reason
685
- const isFailure = reason === 'loadFailed' || reason === 'cleanup'
686
- const isReplaced = reason === 'replaced'
687
-
688
- if (track) this.previousTracks.push(track)
689
- if (this.shouldDeleteMessage && !this._reconnecting && !this._resuming) _functions.safeDel(this.nowPlayingMessage)
690
- if (!isReplaced) this.current = null
691
-
692
- if (isFailure) {
693
- if (!this.queue.size) {
694
- this.clearData({ preserveTracks: this._reconnecting || this._resuming })
695
- this.aqua.emit(AqualinkEvents.QueueEnd, this)
696
- } else {
697
- this.aqua.emit(AqualinkEvents.TrackEnd, this, track, reason)
698
- await this.play()
699
- }
700
- return
701
- }
702
-
703
- if (track && reason === 'finished' && (this.loop === LOOP_MODES.TRACK || this.loop === LOOP_MODES.QUEUE)) {
704
- this.queue.add(track)
705
- }
706
-
707
- if (this.queue.size) {
708
- this.aqua.emit(AqualinkEvents.TrackEnd, this, track, reason)
709
- await this.play()
710
- } else if (this.isAutoplayEnabled && !isReplaced) {
711
- await this.autoplay()
712
- } else {
713
- this.playing = false
714
- if (this.leaveOnEnd && !this.destroyed) {
715
- this.clearData({ preserveTracks: this._reconnecting || this._resuming })
716
- this.destroy()
717
- }
718
- this.aqua.emit(AqualinkEvents.QueueEnd, this)
719
- }
720
- }
721
-
722
- trackError(player, track, payload) {
723
- if (this.destroyed) return
724
- this.aqua.emit(AqualinkEvents.TrackError, this, track, payload)
725
- this.stop()
726
- }
727
-
728
- trackStuck(player, track, payload) {
729
- if (this.destroyed) return
730
- this.aqua.emit(AqualinkEvents.TrackStuck, this, track, payload)
731
- this.stop()
732
- }
733
-
734
- trackChange(p, t, payload) { _functions.emitIfActive(this, AqualinkEvents.TrackChange, t, payload) }
735
- lyricsLine(p, t, payload) { _functions.emitIfActive(this, AqualinkEvents.LyricsLine, t, payload) }
736
- volumeChanged(p, t, payload) { _functions.emitIfActive(this, AqualinkEvents.VolumeChanged, t, payload) }
737
- filtersChanged(p, t, payload) { _functions.emitIfActive(this, AqualinkEvents.FiltersChanged, t, payload) }
738
- seekEvent(p, t, payload) { _functions.emitIfActive(this, AqualinkEvents.Seek, t, payload) }
739
- lyricsFound(p, t, payload) { _functions.emitIfActive(this, AqualinkEvents.LyricsFound, t, payload) }
740
- lyricsNotFound(p, t, payload) { _functions.emitIfActive(this, AqualinkEvents.LyricsNotFound, t, payload) }
741
- playerCreated(p, t, payload) { _functions.emitIfActive(this, AqualinkEvents.PlayerCreated, payload) }
742
- playerConnected(p, t, payload) { _functions.emitIfActive(this, AqualinkEvents.PlayerConnected, payload) }
743
- playerDestroyed(p, t, payload) { _functions.emitIfActive(this, AqualinkEvents.PlayerDestroyed, payload) }
744
- pauseEvent(p, t, payload) { _functions.emitIfActive(this, AqualinkEvents.PauseEvent, payload) }
745
- mixStarted(p, t, payload) { _functions.emitIfActive(this, AqualinkEvents.MixStarted, t, payload) }
746
- mixEnded(p, t, payload) { _functions.emitIfActive(this, AqualinkEvents.MixEnded, t, payload) }
747
-
748
-
749
- async _attemptVoiceResume() {
750
- if (!this.connection?.sessionId) throw new Error('No session')
751
- if (!await this.connection.attemptResume()) throw new Error('Resume failed')
752
- }
753
-
754
- async socketClosed(player, track, payload) {
755
- if (this.destroyed) return
756
- const code = payload?.code
757
- let isRecoverable = [4015, 4009, 4006, 4014, 4022].includes(code)
758
- if (code === 4014 && this.connection?.isWaitingForDisconnect) isRecoverable = false
759
-
760
- if (code === 4015 && !this.nodes?.info?.isNodelink) {
761
- try { await this._attemptVoiceResume(); return } catch { /* ignore */ }
762
- }
763
-
764
- if (!isRecoverable) {
765
- this.aqua.emit(AqualinkEvents.SocketClosed, this, payload)
766
- return
767
- }
768
-
769
- if (code === 4014 || code === 4022) {
770
- this.connected = false
771
- if (!this._voiceDownSince) this._voiceDownSince = Date.now()
772
- if (code === 4022) this._suppressResumeUntil = Date.now() + 3000
773
- }
774
-
775
- if (this._reconnecting) return
776
-
777
- const aqua = this.aqua
778
- const vcId = _functions.toId(this.voiceChannel)
779
- const tcId = _functions.toId(this.textChannel)
780
- const { guildId, deaf, mute } = this
781
-
782
- if (!vcId) {
783
- aqua?.emit?.(AqualinkEvents.SocketClosed, this, payload)
784
- return
785
- }
786
-
787
- const state = {
788
- volume: this.volume,
789
- position: this.position,
790
- paused: this.paused,
791
- loop: this.loop,
792
- isAutoplayEnabled: this.isAutoplayEnabled,
793
- currentTrack: this.current,
794
- queue: this.queue?.toArray() || [],
795
- previousIdentifiers: Array.from(this.previousIdentifiers),
796
- autoplaySeed: this.autoplaySeed,
797
- nowPlayingMessage: this.nowPlayingMessage
798
- }
799
-
800
- this._reconnecting = true
801
- this.destroy({
802
- preserveClient: true, skipRemote: true,
803
- preserveMessage: true, preserveReconnecting: true,
804
- preserveTracks: true
805
- })
806
-
807
- // Store reconnect timers on instance for cleanup in destroy()
808
- this._reconnectTimers = new Set()
809
- const reconnectTimers = this._reconnectTimers
810
- const tryReconnect = async attempt => {
811
- if (aqua?.destroyed) { _functions.clearTimers(reconnectTimers); return }
812
- try {
813
- const np = await aqua.createConnection({
814
- guildId, voiceChannel: vcId, textChannel: tcId, deaf, mute, defaultVolume: state.volume,
815
- preserveMessage: true,
816
- resuming: true
817
- })
818
- if (!np) throw new Error('Failed to create player')
819
-
820
- np.reconnectionRetries = 0
821
- np.loop = state.loop
822
- np.isAutoplayEnabled = state.isAutoplayEnabled
823
- np.autoplaySeed = state.autoplaySeed
824
- np.previousIdentifiers = new Set(state.previousIdentifiers)
825
- np.nowPlayingMessage = state.nowPlayingMessage
826
-
827
- const ct = state.currentTrack
828
- if (ct) np.queue.add(ct)
829
- for (const q of state.queue) if (q !== ct) np.queue.add(q)
830
-
831
- if (ct) {
832
- await np.play()
833
- if (state.position > 5000) np._createTimer(() => !np.destroyed && np.seek(state.position), SEEK_DELAY)
834
- if (state.paused) np._createTimer(() => !np.destroyed && np.pause(true), PAUSE_DELAY)
835
- }
836
-
837
- _functions.clearTimers(reconnectTimers)
838
- this._reconnecting = false
839
- aqua.emit(AqualinkEvents.PlayerReconnected, np, { oldPlayer: this, restoredState: state })
840
- } catch (error) {
841
- const retriesLeft = RECONNECT_MAX - attempt
842
- aqua.emit(AqualinkEvents.ReconnectionFailed, this, { error, code, payload, retriesLeft })
843
-
844
- if (retriesLeft > 0) {
845
- _functions.createTimer(
846
- () => tryReconnect(attempt + 1),
847
- Math.min(RETRY_BACKOFF_BASE * attempt, RETRY_BACKOFF_MAX),
848
- reconnectTimers
849
- )
850
- } else {
851
- _functions.clearTimers(reconnectTimers)
852
- this._reconnecting = false
853
- aqua.emit(AqualinkEvents.SocketClosed, this, payload)
854
- }
855
- }
856
- }
857
-
858
- tryReconnect(1)
859
- }
860
-
861
- _handleAquaPlayerMove(oldChannel, newChannel) {
862
- if (_functions.toId(oldChannel) !== _functions.toId(this.voiceChannel)) return
863
- this.voiceChannel = _functions.toId(newChannel)
864
- }
865
-
866
- send(data) {
867
- try {
868
- this.aqua.send({ op: 4, d: data })
869
- } catch (err) {
870
- this.aqua.emit(AqualinkEvents.Error, new Error(`Send fail: ${err.message}`))
871
- }
872
- }
873
-
874
- set(key, value) {
875
- if (this.destroyed) return
876
- (this._dataStore || (this._dataStore = new Map())).set(key, value)
877
- }
878
-
879
- get(key) {
880
- return this._dataStore?.get(key)
881
- }
882
-
883
- clearData(options = {}) {
884
- const { preserveTracks = false } = options
885
- this.previousTracks?.clear()
886
- this._dataStore?.clear()
887
- this.previousIdentifiers?.clear()
888
- if (this.current?.dispose && !preserveTracks) this.current.dispose()
889
- this.current = null
890
- this.position = this.timestamp = 0
891
- this.queue?.clear()
892
- return this
893
- }
894
-
895
- updatePlayer(data) {
896
- return this.nodes.rest.updatePlayer({ guildId: this.guildId, data })
897
- }
898
-
899
- cleanup() {
900
- if (!this.playing && !this.paused && !this.queue?.size) this.destroy()
901
- }
902
- }
903
-
904
- module.exports = Player
1
+ const { EventEmitter } = require('tseep')
2
+ const { AqualinkEvents } = require('./AqualinkEvents')
3
+ const Connection = require('./Connection')
4
+ const Filters = require('./Filters')
5
+ const { spAutoPlay, scAutoPlay } = require('../handlers/autoplay')
6
+ const Queue = require('./Queue')
7
+
8
+ const PLAYER_STATE = Object.freeze({
9
+ IDLE: 0,
10
+ CONNECTING: 1,
11
+ READY: 2,
12
+ DISCONNECTING: 3,
13
+ DESTROYED: 4
14
+ })
15
+ const LOOP_MODES = Object.freeze({ NONE: 0, TRACK: 1, QUEUE: 2 })
16
+ const LOOP_MODE_NAMES = Object.freeze(['none', 'track', 'queue'])
17
+ const EVENT_HANDLERS = Object.freeze({
18
+ TrackStartEvent: 'trackStart',
19
+ TrackEndEvent: 'trackEnd',
20
+ TrackExceptionEvent: 'trackError',
21
+ TrackStuckEvent: 'trackStuck',
22
+ TrackChangeEvent: 'trackChange',
23
+ WebSocketClosedEvent: 'socketClosed',
24
+ LyricsLineEvent: 'lyricsLine',
25
+ LyricsFoundEvent: 'lyricsFound',
26
+ VolumeChangedEvent: 'volumeChanged',
27
+ FiltersChangedEvent: 'filtersChanged',
28
+ SeekEvent: 'seekEvent',
29
+ PlayerCreatedEvent: 'playerCreated',
30
+ PauseEvent: 'pauseEvent',
31
+ PlayerConnectedEvent: 'playerConnected',
32
+ PlayerDestroyedEvent: 'playerDestroyed',
33
+ LyricsNotFoundEvent: 'lyricsNotFound',
34
+ MixStartedEvent: 'mixStarted',
35
+ MixEndedEvent: 'mixEnded'
36
+ })
37
+
38
+ const WATCHDOG_INTERVAL = 15000
39
+ const VOICE_DOWN_THRESHOLD = 10000
40
+ const VOICE_ABANDON_MULTIPLIER = 12
41
+ const RECONNECT_MAX = 15
42
+ const MUTE_TOGGLE_DELAY = 300
43
+ const SEEK_DELAY = 800
44
+ const PAUSE_DELAY = 1200
45
+ const RETRY_BACKOFF_BASE = 1500
46
+ const RETRY_BACKOFF_MAX = 5000
47
+ const PREVIOUS_TRACKS_SIZE = 50
48
+ const PREVIOUS_IDS_MAX = 20
49
+ const AUTOPLAY_MAX = 3
50
+ const BATCHER_POOL_SIZE = 2
51
+ const INVALID_LOADS = new Set(['error', 'empty', 'LOAD_FAILED', 'NO_MATCHES'])
52
+
53
+ const _functions = {
54
+ clamp(v) {
55
+ const n = +v
56
+ return Number.isNaN(n) ? 100 : n < 0 ? 0 : n > 200 ? 200 : n
57
+ },
58
+ randIdx: (len) => (Math.random() * len) | 0,
59
+ toId: (v) => v?.id || v || null,
60
+ isNum: (v) => typeof v === 'number' && !Number.isNaN(v),
61
+ isInvalidLoad: (r) => !r?.tracks?.length || INVALID_LOADS.has(r.loadType),
62
+ safeDel: (msg) => msg?.delete?.().catch(() => {}),
63
+ createTimer(fn, delay, timerSet, unref = true) {
64
+ const t = setTimeout(() => {
65
+ timerSet?.delete(t)
66
+ fn()
67
+ }, delay)
68
+ if (unref) t.unref?.()
69
+ timerSet?.add(t)
70
+ return t
71
+ },
72
+ clearTimers(set) {
73
+ if (!set) return
74
+ for (const t of set) clearTimeout(t)
75
+ set.clear()
76
+ },
77
+ emitIfActive(player, event, ...args) {
78
+ if (!player.destroyed) player.aqua.emit(event, player, ...args)
79
+ }
80
+ }
81
+
82
+ class MicrotaskUpdateBatcher {
83
+ constructor(player) {
84
+ this.player = player
85
+ this.updates = null
86
+ this.scheduled = false
87
+ }
88
+
89
+ batch(data, immediate) {
90
+ if (!this.player) return Promise.reject(new Error('Player destroyed'))
91
+ this.updates = Object.assign(this.updates || {}, data)
92
+ if (immediate || 'track' in data || 'paused' in data || 'position' in data)
93
+ return this._flush()
94
+ if (!this.scheduled) {
95
+ this.scheduled = true
96
+ queueMicrotask(() => this._flush())
97
+ }
98
+ return Promise.resolve()
99
+ }
100
+
101
+ _flush() {
102
+ const { player: p, updates: u } = this
103
+ this.updates = null
104
+ this.scheduled = false
105
+ if (!u || !p) return Promise.resolve()
106
+ return p.updatePlayer(u).catch((err) => {
107
+ p.aqua?.emit?.(
108
+ AqualinkEvents.Error,
109
+ new Error(`Update error: ${err.message}`)
110
+ )
111
+ throw err
112
+ })
113
+ }
114
+
115
+ reset() {
116
+ this.updates = null
117
+ this.scheduled = false
118
+ this.player = null
119
+ }
120
+ }
121
+
122
+ const batcherPool = {
123
+ pool: [],
124
+ acquire(player) {
125
+ const b = this.pool.pop()
126
+ if (b) {
127
+ b.player = player
128
+ return b
129
+ }
130
+ return new MicrotaskUpdateBatcher(player)
131
+ },
132
+ release(batcher) {
133
+ if (this.pool.length < BATCHER_POOL_SIZE && batcher) {
134
+ batcher.reset()
135
+ this.pool.push(batcher)
136
+ }
137
+ }
138
+ }
139
+
140
+ class CircularBuffer {
141
+ constructor(size) {
142
+ this.buffer = new Array(size)
143
+ this.size = size
144
+ this.index = 0
145
+ this.count = 0
146
+ }
147
+
148
+ push(item) {
149
+ if (!item) return
150
+ this.buffer[this.index] = item
151
+ this.index = (this.index + 1) % this.size
152
+ if (this.count < this.size) this.count++
153
+ }
154
+
155
+ getLast() {
156
+ return this.count
157
+ ? this.buffer[(this.index - 1 + this.size) % this.size]
158
+ : null
159
+ }
160
+
161
+ clear() {
162
+ if (!this.count) return
163
+ this.buffer.fill(undefined)
164
+ this.count = this.index = 0
165
+ }
166
+ }
167
+
168
+ class Player extends EventEmitter {
169
+ static LOOP_MODES = LOOP_MODES
170
+ static EVENT_HANDLERS = EVENT_HANDLERS
171
+
172
+ constructor(aqua, nodes, options) {
173
+ super()
174
+ if (!aqua || !nodes || !options.guildId)
175
+ throw new TypeError('Missing required parameters')
176
+
177
+ this.aqua = aqua
178
+ this.nodes = nodes
179
+ this.guildId = String(options.guildId)
180
+ this.textChannel = options.textChannel
181
+ this.voiceChannel = options.voiceChannel
182
+ this.playing = this.paused = this.connected = this.destroyed = false
183
+ this.state = PLAYER_STATE.IDLE
184
+ this.txId = 0
185
+ this.isAutoplayEnabled = this.isAutoplay = false
186
+ this.autoplaySeed = this.current = this.nowPlayingMessage = null
187
+ this.position = this.timestamp = this.ping = 0
188
+ this.deaf = options.deaf !== false
189
+ this.mute = !!options.mute
190
+ this.autoplayRetries = this.reconnectionRetries = 0
191
+ this._voiceDownSince = 0
192
+ this._voiceRecovering = this._reconnecting = false
193
+ this._resuming = !!options.resuming
194
+ this._voiceWatchdogTimer = null
195
+ this._pendingTimers = new Set()
196
+ this._reconnectTimers = null
197
+ this._dataStore = null
198
+
199
+ this.volume = _functions.clamp(options.defaultVolume || 100)
200
+ this.loop = this._parseLoop(options.loop)
201
+
202
+ const aquaOpts = aqua.options || {}
203
+ this.shouldDeleteMessage = !!aquaOpts.shouldDeleteMessage
204
+ this.leaveOnEnd = !!aquaOpts.leaveOnEnd
205
+
206
+ this.connection = new Connection(this)
207
+ this.filters = new Filters(this)
208
+ this.queue = new Queue()
209
+ this.previousIdentifiers = new Set()
210
+ this.previousTracks = new CircularBuffer(PREVIOUS_TRACKS_SIZE)
211
+ this._updateBatcher = batcherPool.acquire(this)
212
+
213
+ this._voiceRequestAt = 0
214
+ this._voiceRequestChannel = null
215
+ this._suppressResumeUntil = 0
216
+ this._bindEvents()
217
+ this._startWatchdog()
218
+ }
219
+
220
+ _parseLoop(loop) {
221
+ if (typeof loop === 'string') {
222
+ const idx = LOOP_MODE_NAMES.indexOf(loop)
223
+ return idx >= 0 && idx <= 2 ? idx : 0
224
+ }
225
+ return loop >= 0 && loop <= 2 ? loop : 0
226
+ }
227
+
228
+ _bindEvents() {
229
+ this._boundPlayerUpdate = this._handlePlayerUpdate.bind(this)
230
+ this._boundEvent = this._handleEvent.bind(this)
231
+ this._boundPlayerMove = this._handleAquaPlayerMove.bind(this)
232
+
233
+ this.on('playerUpdate', this._boundPlayerUpdate)
234
+ this.on('event', this._boundEvent)
235
+ this.aqua.on('playerMove', this._boundPlayerMove)
236
+ }
237
+
238
+ _startWatchdog() {
239
+ this._voiceWatchdogTimer = setInterval(
240
+ () => this._voiceWatchdog(),
241
+ WATCHDOG_INTERVAL
242
+ )
243
+ this._voiceWatchdogTimer.unref?.()
244
+ }
245
+
246
+ _createTimer(fn, delay, unref = true) {
247
+ return _functions.createTimer(fn, delay, this._pendingTimers, unref)
248
+ }
249
+
250
+ _delay(ms) {
251
+ return new Promise((r) => this._createTimer(r, ms))
252
+ }
253
+
254
+ _handlePlayerUpdate(packet) {
255
+ if (this.destroyed || !packet?.state) return
256
+ const s = packet.state
257
+ this.position = _functions.isNum(s.position) ? s.position : 0
258
+ this.connected = !!s.connected
259
+ this.ping = _functions.isNum(s.ping) ? s.ping : 0
260
+ this.timestamp = _functions.isNum(s.time) ? s.time : Date.now()
261
+
262
+ if (!this.connected) {
263
+ if (!this._voiceDownSince && !this._reconnecting && !this._voiceRecovering) {
264
+ this._voiceDownSince = Date.now()
265
+ this._createTimer(() => {
266
+ if (
267
+ this.connected ||
268
+ this.destroyed ||
269
+ this._reconnecting ||
270
+ this._voiceRecovering ||
271
+ this.nodes?.info?.isNodelink
272
+ )
273
+ return
274
+ this.connection.attemptResume()
275
+ }, 1000)
276
+ }
277
+ } else {
278
+ this._voiceDownSince = 0
279
+ this.state = PLAYER_STATE.READY
280
+ }
281
+
282
+ this.aqua.emit(AqualinkEvents.PlayerUpdate, this, packet)
283
+ }
284
+
285
+ async _handleEvent(payload) {
286
+ if (this.destroyed || !payload?.type) return
287
+ const handler = EVENT_HANDLERS[payload.type]
288
+ if (typeof this[handler] !== 'function') {
289
+ this.aqua.emit(
290
+ AqualinkEvents.NodeError,
291
+ this,
292
+ new Error(`Unknown event: ${payload.type}`)
293
+ )
294
+ return
295
+ }
296
+ try {
297
+ await this[handler](this, this.current, payload)
298
+ } catch (error) {
299
+ this.aqua.emit(AqualinkEvents.Error, error)
300
+ }
301
+ }
302
+
303
+ get previous() {
304
+ return this.previousTracks?.getLast() || null
305
+ }
306
+
307
+ get currenttrack() {
308
+ return this.current
309
+ }
310
+
311
+ getQueue() {
312
+ return this.queue
313
+ }
314
+
315
+ batchUpdatePlayer(data, immediate) {
316
+ return this._updateBatcher.batch(data, immediate)
317
+ }
318
+
319
+ setAutoplay(enabled) {
320
+ this.isAutoplayEnabled = !!enabled
321
+ this.autoplayRetries = 0
322
+ return this
323
+ }
324
+
325
+ async play(track, options = {}) {
326
+ if (this.destroyed || !this.queue) return this
327
+
328
+ let item = track
329
+ if (!item) {
330
+ if (!this.queue.size) return this
331
+ item = this.queue.dequeue()
332
+ }
333
+
334
+ if (!item) return this
335
+
336
+ try {
337
+ this.current = item.track ? item : await item.resolve(this.aqua)
338
+ if (this.destroyed) return this
339
+ if (!this.current?.track) throw new Error('Failed to resolve track')
340
+
341
+ this.playing = true
342
+ this.paused = !!options.paused
343
+ this.position = options.startTime || 0
344
+
345
+ if (this.destroyed || !this._updateBatcher) return this
346
+
347
+ const updateData = {
348
+ guildId: this.guildId,
349
+ track: { encoded: this.current.track },
350
+ paused: this.paused,
351
+ }
352
+ if (this.position > 0) updateData.position = this.position
353
+
354
+ await this.batchUpdatePlayer(updateData, true)
355
+ } catch (error) {
356
+ if (!this.destroyed) this.aqua?.emit(AqualinkEvents.Error, error)
357
+ if (this.queue?.size && !track) return this.play()
358
+ }
359
+ return this
360
+ }
361
+
362
+ connect(options = {}) {
363
+ if (this.destroyed) throw new Error('Cannot connect destroyed player')
364
+
365
+ const voiceChannel = _functions.toId(
366
+ options.voiceChannel || this.voiceChannel
367
+ )
368
+ if (!voiceChannel) throw new TypeError('Voice channel required')
369
+
370
+ this.deaf = options.deaf !== undefined ? !!options.deaf : true
371
+ this.mute = !!options.mute
372
+ this.destroyed = false
373
+ this.state = PLAYER_STATE.CONNECTING
374
+
375
+ this.txId++
376
+ this._voiceRequestChannel = voiceChannel
377
+
378
+ this.voiceChannel = voiceChannel
379
+ this.send({
380
+ guild_id: this.guildId,
381
+ channel_id: voiceChannel,
382
+ self_deaf: this.deaf,
383
+ self_mute: this.mute
384
+ })
385
+ return this
386
+ }
387
+
388
+ _shouldAttemptVoiceRecovery() {
389
+ if (
390
+ this.nodes?.info?.isNodelink ||
391
+ this.destroyed ||
392
+ !this.voiceChannel ||
393
+ this.connected ||
394
+ this._reconnecting ||
395
+ this._voiceRecovering
396
+ )
397
+ return false
398
+ if (
399
+ !this._voiceDownSince ||
400
+ Date.now() - this._voiceDownSince < VOICE_DOWN_THRESHOLD
401
+ )
402
+ return false
403
+ return this.reconnectionRetries < RECONNECT_MAX
404
+ }
405
+
406
+ async _voiceWatchdog() {
407
+ if (!this._shouldAttemptVoiceRecovery()) return
408
+
409
+ const hasVoiceData =
410
+ this.connection?.sessionId &&
411
+ this.connection?.endpoint &&
412
+ this.connection?.token
413
+ if (!hasVoiceData) {
414
+ if (
415
+ Date.now() - this._voiceDownSince >
416
+ VOICE_DOWN_THRESHOLD * VOICE_ABANDON_MULTIPLIER
417
+ )
418
+ this.destroy()
419
+ return
420
+ }
421
+
422
+ this._voiceRecovering = true
423
+ try {
424
+ if (await this.connection.attemptResume()) {
425
+ this.reconnectionRetries = this._voiceDownSince = 0
426
+ return
427
+ }
428
+ const originalMute = this.mute
429
+ this.send({
430
+ guild_id: this.guildId,
431
+ channel_id: this.voiceChannel,
432
+ self_deaf: this.deaf,
433
+ self_mute: !originalMute
434
+ })
435
+ await this._delay(MUTE_TOGGLE_DELAY)
436
+ if (!this.destroyed) {
437
+ this.send({
438
+ guild_id: this.guildId,
439
+ channel_id: this.voiceChannel,
440
+ self_deaf: this.deaf,
441
+ self_mute: originalMute
442
+ })
443
+ }
444
+ this.connection.resendVoiceUpdate()
445
+ this.reconnectionRetries++
446
+ } catch {
447
+ if (++this.reconnectionRetries >= RECONNECT_MAX) this.destroy()
448
+ } finally {
449
+ this._voiceRecovering = false
450
+ }
451
+ }
452
+
453
+ destroy(options = {}) {
454
+ const {
455
+ preserveClient = true,
456
+ skipRemote = false,
457
+ preserveMessage = false,
458
+ preserveReconnecting = false,
459
+ preserveTracks = false
460
+ } = options
461
+ if (this.destroyed && !this.queue) return this
462
+
463
+ if (!this.destroyed) {
464
+ this.destroyed = true
465
+ this.emit('destroy')
466
+ }
467
+
468
+ if (this._voiceWatchdogTimer) {
469
+ clearInterval(this._voiceWatchdogTimer)
470
+ this._voiceWatchdogTimer = null
471
+ }
472
+
473
+ _functions.clearTimers(this._pendingTimers)
474
+ this._pendingTimers = null
475
+
476
+ // Clear reconnection timers to prevent memory leaks when destroyed externally
477
+ if (this._reconnectTimers) {
478
+ _functions.clearTimers(this._reconnectTimers)
479
+ this._reconnectTimers = null
480
+ }
481
+
482
+ this.connected = this.playing = this.paused = this.isAutoplay = false
483
+ this.state = PLAYER_STATE.DESTROYED
484
+ this.autoplayRetries = this.reconnectionRetries = 0
485
+ if (!preserveReconnecting) this._reconnecting = false
486
+ this._lastVoiceChannel = this.voiceChannel
487
+ this._lastTextChannel = this.textChannel
488
+ this.voiceChannel = null
489
+
490
+ if (
491
+ this.shouldDeleteMessage &&
492
+ this.nowPlayingMessage &&
493
+ !preserveMessage
494
+ ) {
495
+ _functions.safeDel(this.nowPlayingMessage)
496
+ this.nowPlayingMessage = null
497
+ }
498
+
499
+ if (this._boundPlayerUpdate)
500
+ this.removeListener('playerUpdate', this._boundPlayerUpdate)
501
+ if (this._boundEvent) this.removeListener('event', this._boundEvent)
502
+ if (this.aqua && this._boundPlayerMove)
503
+ this.aqua.removeListener('playerMove', this._boundPlayerMove)
504
+ this._boundPlayerUpdate = this._boundEvent = this._boundPlayerMove = null
505
+ this.removeAllListeners()
506
+
507
+ if (this._updateBatcher) {
508
+ batcherPool.release(this._updateBatcher)
509
+ this._updateBatcher = null
510
+ }
511
+
512
+ this.previousTracks?.clear()
513
+ this.previousTracks = null
514
+ this.previousIdentifiers?.clear()
515
+ this.previousIdentifiers = null
516
+ this.queue?.clear()
517
+ this.queue = null
518
+ this._dataStore?.clear()
519
+ this._dataStore = null
520
+
521
+ if (
522
+ this.current?.dispose &&
523
+ !this.aqua?.options?.autoResume &&
524
+ !preserveTracks
525
+ )
526
+ this.current.dispose()
527
+ if (this.connection) {
528
+ try {
529
+ this.connection.destroy()
530
+ } catch {}
531
+ }
532
+ this.connection = this.filters = this.current = this.autoplaySeed = null
533
+
534
+ if (!skipRemote) {
535
+ try {
536
+ this.send({ guild_id: this.guildId, channel_id: null })
537
+ this.aqua?.destroyPlayer?.(this.guildId)
538
+ if (this.nodes?.connected)
539
+ this.nodes.rest?.destroyPlayer(this.guildId).catch(() => {})
540
+ } catch {}
541
+ }
542
+
543
+ if (!preserveClient) this.aqua = this.nodes = null
544
+ return this
545
+ }
546
+
547
+ pause(paused) {
548
+ if (this.destroyed || this.paused === !!paused) return this
549
+ this.paused = !!paused
550
+ this.batchUpdatePlayer(
551
+ { guildId: this.guildId, paused: this.paused },
552
+ true
553
+ ).catch(() => {})
554
+ return this
555
+ }
556
+
557
+ seek(position) {
558
+ if (this.destroyed || !this.playing || !_functions.isNum(position))
559
+ return this
560
+ const len = this.current?.info?.length || 0
561
+ const clamped = len
562
+ ? Math.min(Math.max(position, 0), len)
563
+ : Math.max(position, 0)
564
+ this.position = clamped
565
+ this.batchUpdatePlayer(
566
+ { guildId: this.guildId, position: clamped },
567
+ true
568
+ ).catch(() => {})
569
+ return this
570
+ }
571
+
572
+ async getActiveMixer(guildId) {
573
+ if (this.destroyed) return null
574
+ return await this.nodes.rest.getActiveMixer(guildId)
575
+ }
576
+
577
+ async updateMixerVolume(guildId, mix, volume) {
578
+ if (this.destroyed) return null
579
+ return await this.nodes.rest.updateMixerVolume(guildId, mix, volume)
580
+ }
581
+
582
+ async removeMixer(guildId, mix) {
583
+ if (this.destroyed) return null
584
+ return await this.nodes.rest.removeMixer(guildId, mix)
585
+ }
586
+
587
+ async addMixer(guildId, options) {
588
+ if (this.destroyed) return null
589
+
590
+ if (options.identifier && !options.encoded) {
591
+ try {
592
+ const resolved = await this.aqua.resolve({
593
+ query: options.identifier,
594
+ requester: options.requester || this.current?.requester
595
+ })
596
+
597
+ if (resolved?.tracks?.[0]) {
598
+ const track = resolved.tracks[0]
599
+ options = {
600
+ ...options,
601
+ encoded: track.track || track.encoded,
602
+ userData: options.userData
603
+ }
604
+ } else {
605
+ throw new Error('Failed to resolve track identifier')
606
+ }
607
+ } catch (error) {
608
+ throw new Error(`Failed to resolve track: ${error.message}`)
609
+ }
610
+ }
611
+
612
+ return await this.nodes.rest.addMixer(guildId, options)
613
+ }
614
+
615
+ stop() {
616
+ if (this.destroyed || !this.playing) return this
617
+ this.playing = this.paused = false
618
+ this.position = 0
619
+ this.batchUpdatePlayer(
620
+ { guildId: this.guildId, track: { encoded: null, paused: this.paused } },
621
+ true
622
+ ).catch(() => {})
623
+ return this
624
+ }
625
+
626
+ setVolume(volume) {
627
+ const vol = _functions.clamp(volume)
628
+ if (this.destroyed || this.volume === vol) return this
629
+ this.volume = vol
630
+ this.batchUpdatePlayer({ guildId: this.guildId, volume: vol }).catch(
631
+ () => {}
632
+ )
633
+ return this
634
+ }
635
+
636
+ setLoop(mode) {
637
+ if (this.destroyed) return this
638
+ const idx = typeof mode === 'string' ? LOOP_MODE_NAMES.indexOf(mode) : mode
639
+ if (idx < 0 || idx > 2) throw new Error('Invalid loop mode')
640
+ this.loop = idx
641
+ return this
642
+ }
643
+
644
+ setTextChannel(channel) {
645
+ if (this.destroyed) return this
646
+ const id = _functions.toId(channel)
647
+ if (!id) throw new TypeError('Invalid text channel')
648
+ this.textChannel = id
649
+ this.batchUpdatePlayer({ guildId: this.guildId, text_channel: id }).catch(
650
+ () => {}
651
+ )
652
+ return this
653
+ }
654
+
655
+ setVoiceChannel(channel) {
656
+ if (this.destroyed) return this
657
+ const id = _functions.toId(channel)
658
+ if (!id) throw new TypeError('Voice channel required')
659
+ if (this.connected && id === _functions.toId(this.voiceChannel)) return this
660
+ this.voiceChannel = id
661
+ this.connect({
662
+ deaf: this.deaf,
663
+ guildId: this.guildId,
664
+ voiceChannel: id,
665
+ mute: this.mute
666
+ })
667
+ return this
668
+ }
669
+
670
+ disconnect() {
671
+ if (this.destroyed || !this.connected) return this
672
+ this.connected = false
673
+ this.voiceChannel = null
674
+ this.send({ guild_id: this.guildId, channel_id: null })
675
+ return this
676
+ }
677
+
678
+ shuffle() {
679
+ if (this.destroyed || !this.queue?.size) return this
680
+ this.queue.shuffle()
681
+ return this
682
+ }
683
+
684
+ replay() {
685
+ return this.seek(0)
686
+ }
687
+ skip() {
688
+ return this.stop()
689
+ }
690
+
691
+ async getLyrics(options = {}) {
692
+ if (this.destroyed || !this.nodes?.rest) return null
693
+ const { query, useCurrentTrack = true, skipTrackSource = false } = options
694
+ if (query)
695
+ return this.nodes.rest.getLyrics({
696
+ track: { info: { title: query } },
697
+ skipTrackSource
698
+ })
699
+ if (useCurrentTrack && this.playing && this.current) {
700
+ const info = this.current.info
701
+ return this.nodes.rest.getLyrics({
702
+ track: {
703
+ info,
704
+ encoded: this.current.track,
705
+ identifier: info.identifier,
706
+ guild_id: this.guildId
707
+ },
708
+ skipTrackSource
709
+ })
710
+ }
711
+ return null
712
+ }
713
+
714
+ getLoadLyrics(encodedTrack) {
715
+ return this.destroyed || !this.nodes?.rest
716
+ ? null
717
+ : this.nodes.rest.getLoadLyrics(encodedTrack)
718
+ }
719
+
720
+ subscribeLiveLyrics() {
721
+ return this.destroyed
722
+ ? Promise.reject(new Error('Player destroyed'))
723
+ : this.nodes?.rest?.subscribeLiveLyrics(this.guildId, false)
724
+ }
725
+
726
+ unsubscribeLiveLyrics() {
727
+ return this.destroyed
728
+ ? Promise.reject(new Error('Player destroyed'))
729
+ : this.nodes?.rest?.unsubscribeLiveLyrics(this.guildId)
730
+ }
731
+
732
+ async autoplay() {
733
+ if (
734
+ this.destroyed ||
735
+ !this.isAutoplayEnabled ||
736
+ !this.previous ||
737
+ (this.queue?.size)
738
+ )
739
+ return this
740
+ const prev = this.previous
741
+ const info = prev?.info
742
+ if (!info?.sourceName || !info.identifier) return this
743
+ const { sourceName, identifier, uri, author } = info
744
+ this.isAutoplay = true
745
+
746
+ if (sourceName === 'spotify' && info.identifier) {
747
+ this.previousIdentifiers.add(info.identifier)
748
+ if (this.previousIdentifiers.size > PREVIOUS_IDS_MAX) {
749
+ this.previousIdentifiers.delete(
750
+ this.previousIdentifiers.values().next().value
751
+ )
752
+ }
753
+ if (!this.autoplaySeed) {
754
+ this.autoplaySeed = {
755
+ trackId: identifier,
756
+ artistIds: Array.isArray(author) ? author.join(',') : author
757
+ }
758
+ }
759
+ }
760
+
761
+ for (
762
+ let i = 0;
763
+ !this.destroyed && i < AUTOPLAY_MAX && this.queue && !this.queue.size;
764
+ i++
765
+ ) {
766
+ try {
767
+ const track = await this._getAutoplayTrack(
768
+ sourceName,
769
+ identifier,
770
+ uri,
771
+ prev.requester
772
+ )
773
+ if (this.destroyed || !this.queue) return this
774
+ if (track?.info?.title) {
775
+ this.autoplayRetries = 0
776
+ track.requester = prev.requester || { id: 'Unknown' }
777
+ this.queue.add(track)
778
+ await this.play()
779
+ return this
780
+ }
781
+ } catch (err) {
782
+ if (this.destroyed) return this
783
+ this.aqua?.emit(
784
+ AqualinkEvents.Error,
785
+ new Error(`Autoplay ${i + 1} fail: ${err.message}`)
786
+ )
787
+ }
788
+ }
789
+
790
+ if (this.destroyed) return this
791
+ this.aqua?.emit(
792
+ AqualinkEvents.AutoplayFailed,
793
+ this,
794
+ new Error('Max retries')
795
+ )
796
+ this.stop()
797
+ return this
798
+ }
799
+
800
+ async liveLyrics(guildId, state) {
801
+ if (state) return await this.nodes.rest.subscribeLiveLyrics(guildId)
802
+ else return await this.nodes.rest.unsubscribeLiveLyrics(guildId)
803
+ }
804
+
805
+ async _getAutoplayTrack(sourceName, identifier, uri, requester) {
806
+ if (sourceName === 'youtube') {
807
+ const res = await this.aqua.resolve({
808
+ query: `https://www.youtube.com/watch?v=${identifier}&list=RD${identifier}`,
809
+ source: 'ytmsearch',
810
+ requester
811
+ })
812
+ return _functions.isInvalidLoad(res)
813
+ ? null
814
+ : res.tracks[_functions.randIdx(res.tracks.length)]
815
+ }
816
+ if (sourceName === 'soundcloud') {
817
+ const scRes = await scAutoPlay(uri)
818
+ if (!scRes?.length) return null
819
+ const res = await this.aqua.resolve({
820
+ query: scRes[0],
821
+ source: 'scsearch',
822
+ requester
823
+ })
824
+ return _functions.isInvalidLoad(res)
825
+ ? null
826
+ : res.tracks[_functions.randIdx(res.tracks.length)]
827
+ }
828
+ if (sourceName === 'spotify') {
829
+ const res = await spAutoPlay(
830
+ this.autoplaySeed,
831
+ this,
832
+ requester,
833
+ Array.from(this.previousIdentifiers)
834
+ )
835
+ return res?.length ? res[_functions.randIdx(res.length)] : null
836
+ }
837
+ return null
838
+ }
839
+
840
+ trackStart(player, track, payload = {}) {
841
+ if (this.destroyed) return
842
+ this.playing = true
843
+ this.paused = false
844
+ this.aqua.emit(AqualinkEvents.TrackStart, this, this.current, {
845
+ ...payload,
846
+ resumed: this._resuming
847
+ })
848
+ this._resuming = false
849
+ }
850
+
851
+ async trackEnd(player, track, payload) {
852
+ if (this.destroyed) return
853
+
854
+ const reason = payload?.reason
855
+ const isFailure = reason === 'loadFailed' || reason === 'cleanup'
856
+ const isReplaced = reason === 'replaced'
857
+
858
+ if (track) this.previousTracks.push(track)
859
+ if (this.shouldDeleteMessage && !this._reconnecting && !this._resuming)
860
+ _functions.safeDel(this.nowPlayingMessage)
861
+ if (!isReplaced) this.current = null
862
+
863
+ if (isFailure) {
864
+ if (!this.queue.size) {
865
+ this.clearData({ preserveTracks: this._reconnecting || this._resuming })
866
+ this.aqua.emit(AqualinkEvents.QueueEnd, this)
867
+ } else {
868
+ this.aqua.emit(AqualinkEvents.TrackEnd, this, track, reason)
869
+ await this.play()
870
+ }
871
+ return
872
+ }
873
+
874
+ if (
875
+ track &&
876
+ reason === 'finished' &&
877
+ (this.loop === LOOP_MODES.TRACK || this.loop === LOOP_MODES.QUEUE)
878
+ ) {
879
+ this.queue.add(track)
880
+ }
881
+
882
+ if (this.queue.size && !isReplaced) {
883
+ this.aqua.emit(AqualinkEvents.TrackEnd, this, track, reason)
884
+ await this.play()
885
+ } else if (this.isAutoplayEnabled && !isReplaced) {
886
+ await this.autoplay()
887
+ } else {
888
+ this.playing = false
889
+ if (this.leaveOnEnd && !this.destroyed) {
890
+ this.clearData({ preserveTracks: this._reconnecting || this._resuming })
891
+ this.destroy()
892
+ }
893
+ this.aqua.emit(AqualinkEvents.QueueEnd, this)
894
+ }
895
+ }
896
+
897
+ trackError(player, track, payload) {
898
+ if (this.destroyed) return
899
+ this.aqua.emit(AqualinkEvents.TrackError, this, track, payload)
900
+ this.stop()
901
+ }
902
+
903
+ trackStuck(player, track, payload) {
904
+ if (this.destroyed) return
905
+ this.aqua.emit(AqualinkEvents.TrackStuck, this, track, payload)
906
+ this.stop()
907
+ }
908
+
909
+ trackChange(p, t, payload) {
910
+ _functions.emitIfActive(this, AqualinkEvents.TrackChange, t, payload)
911
+ }
912
+ lyricsLine(p, t, payload) {
913
+ _functions.emitIfActive(this, AqualinkEvents.LyricsLine, t, payload)
914
+ }
915
+ volumeChanged(p, t, payload) {
916
+ _functions.emitIfActive(this, AqualinkEvents.VolumeChanged, t, payload)
917
+ }
918
+ filtersChanged(p, t, payload) {
919
+ _functions.emitIfActive(this, AqualinkEvents.FiltersChanged, t, payload)
920
+ }
921
+ seekEvent(p, t, payload) {
922
+ _functions.emitIfActive(this, AqualinkEvents.Seek, t, payload)
923
+ }
924
+ lyricsFound(p, t, payload) {
925
+ _functions.emitIfActive(this, AqualinkEvents.LyricsFound, t, payload)
926
+ }
927
+ lyricsNotFound(p, t, payload) {
928
+ _functions.emitIfActive(this, AqualinkEvents.LyricsNotFound, t, payload)
929
+ }
930
+ playerCreated(p, t, payload) {
931
+ _functions.emitIfActive(this, AqualinkEvents.PlayerCreated, payload)
932
+ }
933
+ playerConnected(p, t, payload) {
934
+ _functions.emitIfActive(this, AqualinkEvents.PlayerConnected, payload)
935
+ }
936
+ playerDestroyed(p, t, payload) {
937
+ _functions.emitIfActive(this, AqualinkEvents.PlayerDestroyed, payload)
938
+ }
939
+ pauseEvent(p, t, payload) {
940
+ _functions.emitIfActive(this, AqualinkEvents.PauseEvent, payload)
941
+ }
942
+ mixStarted(p, t, payload) {
943
+ _functions.emitIfActive(this, AqualinkEvents.MixStarted, t, payload)
944
+ }
945
+ mixEnded(p, t, payload) {
946
+ _functions.emitIfActive(this, AqualinkEvents.MixEnded, t, payload)
947
+ }
948
+
949
+ async _attemptVoiceResume() {
950
+ if (!this.connection?.sessionId) throw new Error('No session')
951
+ if (!(await this.connection.attemptResume()))
952
+ throw new Error('Resume failed')
953
+ }
954
+
955
+ async socketClosed(player, track, payload) {
956
+ if (this.destroyed || this._reconnecting) return
957
+
958
+ const code = payload?.code
959
+ let isRecoverable = [4015, 4009, 4006, 4014, 4022].includes(code)
960
+ if (code === 4014 && this.connection?.isWaitingForDisconnect)
961
+ isRecoverable = false
962
+
963
+ if (code === 4015 && !this.nodes?.info?.isNodelink) {
964
+ this._reconnecting = true
965
+ try {
966
+ await this._attemptVoiceResume()
967
+ this._reconnecting = false
968
+ return
969
+ } catch {
970
+ this._reconnecting = false
971
+ }
972
+ }
973
+
974
+ if (!isRecoverable) {
975
+ this.aqua.emit(AqualinkEvents.SocketClosed, this, payload)
976
+ this.destroy()
977
+ return
978
+ }
979
+
980
+ if (code === 4014 || code === 4022) {
981
+ this.connected = false
982
+ if (!this._voiceDownSince) this._voiceDownSince = Date.now()
983
+ if (code === 4022) this._suppressResumeUntil = Date.now() + 3000
984
+ }
985
+
986
+ const aqua = this.aqua
987
+ const vcId = _functions.toId(this.voiceChannel)
988
+ const tcId = _functions.toId(this.textChannel)
989
+ const { guildId, deaf, mute } = this
990
+
991
+ if (!vcId) {
992
+ aqua?.emit?.(AqualinkEvents.SocketClosed, this, payload)
993
+ return
994
+ }
995
+
996
+ const state = {
997
+ volume: this.volume,
998
+ position: this.position,
999
+ paused: this.paused,
1000
+ loop: this.loop,
1001
+ isAutoplayEnabled: this.isAutoplayEnabled,
1002
+ currentTrack: this.current,
1003
+ queue: this.queue?.toArray() || [],
1004
+ previousIdentifiers: Array.from(this.previousIdentifiers),
1005
+ autoplaySeed: this.autoplaySeed,
1006
+ nowPlayingMessage: this.nowPlayingMessage
1007
+ }
1008
+
1009
+ this._reconnecting = true
1010
+ this.destroy({
1011
+ preserveClient: true,
1012
+ skipRemote: true,
1013
+ preserveMessage: true,
1014
+ preserveReconnecting: true,
1015
+ preserveTracks: true
1016
+ })
1017
+
1018
+ // Store reconnect timers on instance for cleanup in destroy()
1019
+ this._reconnectTimers = new Set()
1020
+ const reconnectTimers = this._reconnectTimers
1021
+ const tryReconnect = async (attempt) => {
1022
+ if (aqua?.destroyed) {
1023
+ _functions.clearTimers(reconnectTimers)
1024
+ return
1025
+ }
1026
+ try {
1027
+ const np = await aqua.createConnection({
1028
+ guildId,
1029
+ voiceChannel: vcId,
1030
+ textChannel: tcId,
1031
+ deaf,
1032
+ mute,
1033
+ defaultVolume: state.volume,
1034
+ preserveMessage: true,
1035
+ resuming: true
1036
+ })
1037
+ if (!np) throw new Error('Failed to create player')
1038
+
1039
+ np.reconnectionRetries = 0
1040
+ np.loop = state.loop
1041
+ np.isAutoplayEnabled = state.isAutoplayEnabled
1042
+ np.autoplaySeed = state.autoplaySeed
1043
+ np.previousIdentifiers = new Set(state.previousIdentifiers)
1044
+ np.nowPlayingMessage = state.nowPlayingMessage
1045
+
1046
+ const ct = state.currentTrack
1047
+ if (ct) np.queue.add(ct)
1048
+ for (const q of state.queue) if (q !== ct) np.queue.add(q)
1049
+
1050
+ if (ct) {
1051
+ await np.play()
1052
+ if (state.position > 5000)
1053
+ np._createTimer(
1054
+ () => !np.destroyed && np.seek(state.position),
1055
+ SEEK_DELAY
1056
+ )
1057
+ if (state.paused)
1058
+ np._createTimer(() => !np.destroyed && np.pause(true), PAUSE_DELAY)
1059
+ }
1060
+
1061
+ _functions.clearTimers(reconnectTimers)
1062
+ this._reconnecting = false
1063
+ aqua.emit(AqualinkEvents.PlayerReconnected, np, {
1064
+ oldPlayer: this,
1065
+ restoredState: state
1066
+ })
1067
+ } catch (error) {
1068
+ const retriesLeft = RECONNECT_MAX - attempt
1069
+ aqua.emit(AqualinkEvents.ReconnectionFailed, this, {
1070
+ error,
1071
+ code,
1072
+ payload,
1073
+ retriesLeft
1074
+ })
1075
+
1076
+ if (retriesLeft > 0) {
1077
+ _functions.createTimer(
1078
+ () => tryReconnect(attempt + 1),
1079
+ Math.min(RETRY_BACKOFF_BASE * attempt, RETRY_BACKOFF_MAX),
1080
+ reconnectTimers
1081
+ )
1082
+ } else {
1083
+ _functions.clearTimers(reconnectTimers)
1084
+ this._reconnecting = false
1085
+ aqua.emit(AqualinkEvents.SocketClosed, this, payload)
1086
+ }
1087
+ }
1088
+ }
1089
+
1090
+ tryReconnect(1)
1091
+ }
1092
+
1093
+ _handleAquaPlayerMove(oldChannel, newChannel) {
1094
+ if (_functions.toId(oldChannel) !== _functions.toId(this.voiceChannel))
1095
+ return
1096
+ this.voiceChannel = _functions.toId(newChannel)
1097
+ }
1098
+
1099
+ send(data) {
1100
+ try {
1101
+ this.aqua.send({ op: 4, d: data })
1102
+ } catch (err) {
1103
+ this.aqua.emit(
1104
+ AqualinkEvents.Error,
1105
+ new Error(`Send fail: ${err.message}`)
1106
+ )
1107
+ }
1108
+ }
1109
+
1110
+ set(key, value) {
1111
+ if (this.destroyed) return
1112
+ if (!this._dataStore) {
1113
+ this._dataStore = new Map()
1114
+ }
1115
+ this._dataStore.set(key, value)
1116
+ }
1117
+
1118
+ get(key) {
1119
+ return this._dataStore?.get(key)
1120
+ }
1121
+
1122
+ clearData(options = {}) {
1123
+ const { preserveTracks = false } = options
1124
+ this.previousTracks?.clear()
1125
+ this._dataStore?.clear()
1126
+ this.previousIdentifiers?.clear()
1127
+ if (this.current?.dispose && !preserveTracks) this.current.dispose()
1128
+ this.current = null
1129
+ this.position = this.timestamp = 0
1130
+ this.queue?.clear()
1131
+ return this
1132
+ }
1133
+
1134
+ updatePlayer(data) {
1135
+ return this.nodes.rest.updatePlayer({ guildId: this.guildId, data })
1136
+ }
1137
+
1138
+ cleanup() {
1139
+ if (!this.playing && !this.paused && !this.queue?.size) this.destroy()
1140
+ }
1141
+ }
1142
+
1143
+ module.exports = Player