aqualink 2.17.2 → 2.18.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,12 +2,12 @@
2
2
 
3
3
  const fs = require('node:fs')
4
4
  const readline = require('node:readline')
5
- const {EventEmitter} = require('tseep')
6
- const {AqualinkEvents} = require('./AqualinkEvents')
5
+ const { EventEmitter } = require('tseep')
6
+ const { AqualinkEvents } = require('./AqualinkEvents')
7
7
  const Node = require('./Node')
8
8
  const Player = require('./Player')
9
9
  const Track = require('./Track')
10
- const {version: pkgVersion} = require('../../package.json')
10
+ const { version: pkgVersion } = require('../../package.json')
11
11
 
12
12
  const SEARCH_PREFIX = ':'
13
13
  const EMPTY_ARRAY = Object.freeze([])
@@ -33,7 +33,6 @@ const MAX_REBUILD_LOCKS = 100
33
33
  const WRITE_BUFFER_SIZE = 100
34
34
  const MAX_QUEUE_SAVE = 10
35
35
  const MAX_TRACKS_RESTORE = 20
36
- const URL_PATTERN = /^https?:\/\//i
37
36
 
38
37
  const DEFAULT_OPTIONS = Object.freeze({
39
38
  shouldDeleteMessage: false,
@@ -53,14 +52,22 @@ const DEFAULT_OPTIONS = Object.freeze({
53
52
  resumePlayback: true,
54
53
  cooldownTime: 5000,
55
54
  maxFailoverAttempts: 5
56
- })
55
+ }),
56
+ maxQueueSave: 10,
57
+ maxTracksRestore: 20
57
58
  })
58
59
 
59
- // Shared helper functions
60
60
  const _functions = {
61
- delay: ms => new Promise(r => setTimeout(r, ms)),
62
- noop: () => {},
63
- isUrl: query => typeof query === 'string' && query.length > 8 && URL_PATTERN.test(query),
61
+ delay: ms => new Promise(r => {
62
+ const t = setTimeout(r, ms)
63
+ t.unref?.()
64
+ }),
65
+ noop: () => { },
66
+ isUrl: query => {
67
+ if (typeof query !== 'string' || query.length <= 8) return false
68
+ const q = query.trimStart()
69
+ return q.startsWith('http://') || q.startsWith('https://')
70
+ },
64
71
  formatQuery(query, source) {
65
72
  return this.isUrl(query) ? query : `${source}${SEARCH_PREFIX}${query}`
66
73
  },
@@ -69,12 +76,17 @@ const _functions = {
69
76
  try {
70
77
  const result = fn()
71
78
  return result?.then ? result.catch(this.noop) : result
72
- } catch {}
79
+ } catch { }
73
80
  },
74
81
  parseRequester(str) {
75
82
  if (!str || typeof str !== 'string') return null
76
83
  const i = str.indexOf(':')
77
- return i > 0 ? {id: str.substring(0, i), username: str.substring(i + 1)} : null
84
+ return i > 0 ? { id: str.substring(0, i), username: str.substring(i + 1) } : null
85
+ },
86
+ unrefTimeout: (fn, ms) => {
87
+ const t = setTimeout(fn, ms)
88
+ t.unref?.()
89
+ return t
78
90
  }
79
91
  }
80
92
 
@@ -92,9 +104,9 @@ class Aqua extends EventEmitter {
92
104
  this.initiated = false
93
105
  this.version = pkgVersion
94
106
 
95
- const merged = {...DEFAULT_OPTIONS, ...options}
107
+ const merged = { ...DEFAULT_OPTIONS, ...options }
96
108
  this.options = merged
97
- this.failoverOptions = {...DEFAULT_OPTIONS.failoverOptions, ...options.failoverOptions}
109
+ this.failoverOptions = { ...DEFAULT_OPTIONS.failoverOptions, ...options.failoverOptions }
98
110
 
99
111
  this.shouldDeleteMessage = merged.shouldDeleteMessage
100
112
  this.defaultSearchPlatform = merged.defaultSearchPlatform
@@ -108,6 +120,8 @@ class Aqua extends EventEmitter {
108
120
  this.allowedDomains = merged.allowedDomains || []
109
121
  this.loadBalancer = merged.loadBalancer
110
122
  this.useHttp2 = merged.useHttp2
123
+ this.maxQueueSave = merged.maxQueueSave
124
+ this.maxTracksRestore = merged.maxTracksRestore
111
125
  this.send = merged.send || this._createDefaultSend()
112
126
 
113
127
  this._nodeStates = new Map()
@@ -149,13 +163,13 @@ class Aqua extends EventEmitter {
149
163
  this._performCleanup()
150
164
  })
151
165
  },
152
- onNodeReady: (node, {resumed}) => {
166
+ onNodeReady: (node, { resumed }) => {
153
167
  if (!resumed) return
154
168
  const batch = []
155
169
  for (const player of this.players.values()) {
156
170
  if (player.nodes === node && player.connection) batch.push(player)
157
171
  }
158
- if (batch.length) queueMicrotask(() => batch.forEach(p => p.connection.resendVoiceUpdate({resume: true})))
172
+ if (batch.length) queueMicrotask(() => batch.forEach(p => p.connection.resendVoiceUpdate()))
159
173
  }
160
174
  }
161
175
  this.on(AqualinkEvents.NodeConnect, this._eventHandlers.onNodeConnect)
@@ -171,8 +185,10 @@ class Aqua extends EventEmitter {
171
185
  this._eventHandlers = null
172
186
  }
173
187
  this.removeAllListeners()
174
- this.nodeMap.forEach(node => this._destroyNode(node.name || node.host))
175
- this.players.forEach(player => _functions.safeCall(() => player.destroy()))
188
+
189
+ for (const id of Array.from(this.nodeMap.keys())) this._destroyNode(id)
190
+ for (const player of Array.from(this.players.values())) _functions.safeCall(() => player.destroy())
191
+
176
192
  this.players.clear()
177
193
  this._nodeStates.clear()
178
194
  this._failoverQueue.clear()
@@ -220,7 +236,7 @@ class Aqua extends EventEmitter {
220
236
  (stats.playingPlayers || 0) * 0.75 +
221
237
  (stats.memory ? stats.memory.used / reservable : 0) * 40 +
222
238
  (node.rest?.calls || 0) * 0.001
223
- this._nodeLoadCache.set(id, {load, time: now})
239
+ this._nodeLoadCache.set(id, { load, time: now })
224
240
  if (this._nodeLoadCache.size > MAX_CACHE_SIZE) {
225
241
  const first = this._nodeLoadCache.keys().next().value
226
242
  this._nodeLoadCache.delete(first)
@@ -233,7 +249,7 @@ class Aqua extends EventEmitter {
233
249
  this.clientId = clientId
234
250
  if (!this.clientId) return this
235
251
  const results = await Promise.allSettled(
236
- this.nodes.map(n => Promise.race([this._createNode(n), _functions.delay(NODE_TIMEOUT).then(() => {throw new Error('Timeout')})]))
252
+ this.nodes.map(n => Promise.race([this._createNode(n), _functions.delay(NODE_TIMEOUT).then(() => { throw new Error('Timeout') })]))
237
253
  )
238
254
  if (!results.some(r => r.status === 'fulfilled')) throw new Error('No nodes connected')
239
255
  if (this.plugins?.length) {
@@ -249,10 +265,10 @@ class Aqua extends EventEmitter {
249
265
  const node = new Node(this, options, this.options)
250
266
  node.players = new Set()
251
267
  this.nodeMap.set(id, node)
252
- this._nodeStates.set(id, {connected: false, failoverInProgress: false})
268
+ this._nodeStates.set(id, { connected: false, failoverInProgress: false })
253
269
  try {
254
270
  await node.connect()
255
- this._nodeStates.set(id, {connected: true, failoverInProgress: false})
271
+ this._nodeStates.set(id, { connected: true, failoverInProgress: false })
256
272
  this._invalidateCache()
257
273
  this.emit(AqualinkEvents.NodeCreate, node)
258
274
  return node
@@ -304,7 +320,7 @@ class Aqua extends EventEmitter {
304
320
  const now = Date.now()
305
321
  for (const [guildId, state] of this._brokenPlayers) {
306
322
  if (state.originalNodeId === id && (now - state.brokenAt) < BROKEN_PLAYER_TTL) {
307
- rebuilds.push({guildId, state})
323
+ rebuilds.push({ guildId, state })
308
324
  }
309
325
  }
310
326
  if (!rebuilds.length) return
@@ -312,7 +328,7 @@ class Aqua extends EventEmitter {
312
328
  for (let i = 0; i < rebuilds.length; i += MAX_CONCURRENT_OPS) {
313
329
  const batch = rebuilds.slice(i, i + MAX_CONCURRENT_OPS)
314
330
  const results = await Promise.allSettled(
315
- batch.map(({guildId, state}) => this._rebuildPlayer(state, node).then(() => guildId))
331
+ batch.map(({ guildId, state }) => this._rebuildPlayer(state, node).then(() => guildId))
316
332
  )
317
333
  for (const r of results) {
318
334
  if (r.status === 'fulfilled') successes.push(r.value)
@@ -324,7 +340,7 @@ class Aqua extends EventEmitter {
324
340
  }
325
341
 
326
342
  async _rebuildPlayer(state, targetNode) {
327
- const {guildId, textChannel, voiceChannel, current, volume = 65, deaf = true} = state
343
+ const { guildId, textChannel, voiceChannel, current, volume = 65, deaf = true } = state
328
344
  const lockKey = `rebuild_${guildId}`
329
345
  if (this._rebuildLocks.has(lockKey)) return
330
346
  this._rebuildLocks.add(lockKey)
@@ -333,11 +349,11 @@ class Aqua extends EventEmitter {
333
349
  await this.destroyPlayer(guildId)
334
350
  await _functions.delay(RECONNECT_DELAY)
335
351
  }
336
- const player = this.createPlayer(targetNode, {guildId, textChannel, voiceChannel, defaultVolume: volume, deaf})
352
+ const player = this.createPlayer(targetNode, { guildId, textChannel, voiceChannel, defaultVolume: volume, deaf })
337
353
  if (current && player?.queue?.add) {
338
354
  player.queue.add(current)
339
355
  await player.play()
340
- if (state.position > 0) setTimeout(() => player.seek?.(state.position), SEEK_DELAY)
356
+ if (state.position > 0) _functions.unrefTimeout(() => player.seek?.(state.position), SEEK_DELAY)
341
357
  if (state.paused) player.pause(true)
342
358
  }
343
359
  return player
@@ -357,7 +373,7 @@ class Aqua extends EventEmitter {
357
373
  const attempts = this._failoverQueue.get(id) || 0
358
374
  if (attempts >= this.failoverOptions.maxFailoverAttempts) return
359
375
 
360
- this._nodeStates.set(id, {connected: false, failoverInProgress: true})
376
+ this._nodeStates.set(id, { connected: false, failoverInProgress: true })
361
377
  this._lastFailoverAttempt.set(id, now)
362
378
  this._failoverQueue.set(id, attempts + 1)
363
379
 
@@ -379,7 +395,7 @@ class Aqua extends EventEmitter {
379
395
  } catch (error) {
380
396
  this.emit(AqualinkEvents.Error, null, error)
381
397
  } finally {
382
- this._nodeStates.set(id, {connected: false, failoverInProgress: false})
398
+ this._nodeStates.set(id, { connected: false, failoverInProgress: false })
383
399
  }
384
400
  }
385
401
 
@@ -403,7 +419,7 @@ class Aqua extends EventEmitter {
403
419
  for (let i = 0; i < players.length; i += MAX_CONCURRENT_OPS) {
404
420
  const batch = players.slice(i, i + MAX_CONCURRENT_OPS)
405
421
  const batchResults = await Promise.allSettled(batch.map(p => this._migratePlayer(p, pickNode)))
406
- for (const r of batchResults) results.push({success: r.status === 'fulfilled', error: r.reason})
422
+ for (const r of batchResults) results.push({ success: r.status === 'fulfilled', error: r.reason })
407
423
  }
408
424
  return results
409
425
  }
@@ -411,7 +427,7 @@ class Aqua extends EventEmitter {
411
427
  async _migratePlayer(player, pickNode) {
412
428
  const state = this._capturePlayerState(player)
413
429
  if (!state) throw new Error('Failed to capture state')
414
- const {maxRetries, retryDelay} = this.failoverOptions
430
+ const { maxRetries, retryDelay } = this.failoverOptions
415
431
  for (let retry = 0; retry < maxRetries; retry++) {
416
432
  try {
417
433
  const targetNode = pickNode()
@@ -462,10 +478,10 @@ class Aqua extends EventEmitter {
462
478
  }
463
479
  if (state.queue?.length && newPlayer.queue?.add) newPlayer.queue.add(...state.queue)
464
480
  if (state.current && this.failoverOptions.preservePosition) {
465
- newPlayer.queue?.add?.(state.current, {toFront: true})
481
+ newPlayer.queue?.add?.(state.current, { toFront: true })
466
482
  if (this.failoverOptions.resumePlayback) {
467
483
  ops.push(newPlayer.play())
468
- if (state.position > 0) setTimeout(() => newPlayer.seek?.(state.position), SEEK_DELAY)
484
+ if (state.position > 0) _functions.unrefTimeout(() => newPlayer.seek?.(state.position), SEEK_DELAY)
469
485
  if (state.paused) ops.push(newPlayer.pause(true))
470
486
  }
471
487
  }
@@ -474,16 +490,21 @@ class Aqua extends EventEmitter {
474
490
  await Promise.allSettled(ops)
475
491
  }
476
492
 
477
- updateVoiceState({d, t}) {
493
+ updateVoiceState({ d, t }) {
478
494
  if (!d?.guild_id || (t !== 'VOICE_STATE_UPDATE' && t !== 'VOICE_SERVER_UPDATE')) return
479
495
  const player = this.players.get(d.guild_id)
480
- if (!player || !player.nodes?.connected) return
496
+ if (!player) return
497
+
498
+ d.txId = player.txId
481
499
  if (t === 'VOICE_STATE_UPDATE') {
482
500
  if (d.user_id !== this.clientId) return
483
- if (!d.channel_id) return void this.destroyPlayer(d.guild_id)
484
501
  if (player.connection) {
485
- player.connection.sessionId = d.session_id
486
- player.connection.setStateUpdate(d)
502
+ if (!d.channel_id && player.connection.voiceChannel) {
503
+ player.connection.setStateUpdate(d)
504
+ } else {
505
+ player.connection.sessionId = d.session_id
506
+ player.connection.setStateUpdate(d)
507
+ }
487
508
  }
488
509
  } else {
489
510
  player.connection?.setServerUpdate(d)
@@ -503,7 +524,7 @@ class Aqua extends EventEmitter {
503
524
  createConnection(options) {
504
525
  if (!this.initiated) throw new Error('Aqua not initialized')
505
526
  const existing = this.players.get(options.guildId)
506
- if (existing) {
527
+ if (existing && !existing.destroyed) {
507
528
  if (options.voiceChannel && existing.voiceChannel !== options.voiceChannel) {
508
529
  _functions.safeCall(() => existing.connect(options))
509
530
  }
@@ -516,7 +537,12 @@ class Aqua extends EventEmitter {
516
537
 
517
538
  createPlayer(node, options) {
518
539
  const existing = this.players.get(options.guildId)
519
- if (existing) _functions.safeCall(() => existing.destroy())
540
+ if (existing) {
541
+ _functions.safeCall(() => existing.destroy({
542
+ preserveMessage: options.preserveMessage || !!options.resuming || false,
543
+ preserveTracks: !!options.resuming || false
544
+ }))
545
+ }
520
546
  const player = new Player(this, node, options)
521
547
  this.players.set(options.guildId, player)
522
548
  node?.players?.add?.(player)
@@ -540,7 +566,7 @@ class Aqua extends EventEmitter {
540
566
  await _functions.safeCall(() => player.destroy())
541
567
  }
542
568
 
543
- async resolve({query, source, requester, nodes}) {
569
+ async resolve({ query, source, requester, nodes }) {
544
570
  if (!this.initiated) throw new Error('Aqua not initialized')
545
571
  const node = this._getRequestNode(nodes)
546
572
  if (!node) throw new Error('No nodes available')
@@ -581,8 +607,8 @@ class Aqua extends EventEmitter {
581
607
  }
582
608
 
583
609
  _constructResponse(response, requester, node) {
584
- const {loadType, data, pluginInfo: rootPlugin} = response || {}
585
- const base = {loadType, exception: null, playlistInfo: null, pluginInfo: rootPlugin || {}, tracks: []}
610
+ const { loadType, data, pluginInfo: rootPlugin } = response || {}
611
+ const base = { loadType, exception: null, playlistInfo: null, pluginInfo: rootPlugin || {}, tracks: [] }
586
612
  if (loadType === 'error' || loadType === 'LOAD_FAILED') {
587
613
  base.exception = data || response.exception || null
588
614
  return base
@@ -599,7 +625,7 @@ class Aqua extends EventEmitter {
599
625
  ...info
600
626
  }
601
627
  }
602
- base.pluginInfo = data.pluginInfo || base.pluginInfo
628
+ base.pluginInfo = data.pluginInfo || rootPlugin || base.pluginInfo
603
629
  base.tracks = Array.isArray(data.tracks) ? data.tracks.map(t => _functions.makeTrack(t, requester, node)) : []
604
630
  } else if (loadType === 'search') {
605
631
  base.tracks = Array.isArray(data) ? data.map(t => _functions.makeTrack(t, requester, node)) : []
@@ -616,7 +642,7 @@ class Aqua extends EventEmitter {
616
642
  async search(query, requester, source) {
617
643
  if (!query || !requester) return null
618
644
  try {
619
- const {tracks} = await this.resolve({query, source: source || this.defaultSearchPlatform, requester})
645
+ const { tracks } = await this.resolve({ query, source: source || this.defaultSearchPlatform, requester })
620
646
  return tracks || null
621
647
  } catch {
622
648
  return null
@@ -628,8 +654,8 @@ class Aqua extends EventEmitter {
628
654
  const tempFile = `${filePath}.tmp`
629
655
  let ws = null
630
656
  try {
631
- await fs.promises.writeFile(lockFile, String(process.pid), {flag: 'wx'})
632
- ws = fs.createWriteStream(tempFile, {encoding: 'utf8', flags: 'w'})
657
+ await fs.promises.writeFile(lockFile, String(process.pid), { flag: 'wx' })
658
+ ws = fs.createWriteStream(tempFile, { encoding: 'utf8', flags: 'w' })
633
659
  const buffer = []
634
660
  let drainPromise = Promise.resolve()
635
661
 
@@ -642,7 +668,7 @@ class Aqua extends EventEmitter {
642
668
  u: player.current?.uri || null,
643
669
  p: player.position || 0,
644
670
  ts: player.timestamp || 0,
645
- q: player.queue.slice(0, MAX_QUEUE_SAVE).map(tr => tr.uri),
671
+ q: player.queue.slice(0, this.maxQueueSave).map(tr => tr.uri),
646
672
  r: requester ? `${requester.id}:${requester.username}` : null,
647
673
  vol: player.volume,
648
674
  pa: player.paused,
@@ -680,11 +706,11 @@ class Aqua extends EventEmitter {
680
706
  let stream = null, rl = null
681
707
  try {
682
708
  await fs.promises.access(filePath)
683
- await fs.promises.writeFile(lockFile, String(process.pid), {flag: 'wx'})
709
+ await fs.promises.writeFile(lockFile, String(process.pid), { flag: 'wx' })
684
710
  await this._waitForFirstNode()
685
711
 
686
- stream = fs.createReadStream(filePath, {encoding: 'utf8'})
687
- rl = readline.createInterface({input: stream, crlfDelay: Infinity})
712
+ stream = fs.createReadStream(filePath, { encoding: 'utf8' })
713
+ rl = readline.createInterface({ input: stream, crlfDelay: Infinity })
688
714
 
689
715
  const batch = []
690
716
  for await (const line of rl) {
@@ -718,8 +744,8 @@ class Aqua extends EventEmitter {
718
744
  })
719
745
  player._resuming = !!p.resuming
720
746
  const requester = _functions.parseRequester(p.r)
721
- const tracksToResolve = [p.u, ...(p.q || [])].filter(Boolean).slice(0, MAX_TRACKS_RESTORE)
722
- const resolved = await Promise.all(tracksToResolve.map(uri => this.resolve({query: uri, requester}).catch(() => null)))
747
+ const tracksToResolve = [p.u, ...(p.q || [])].filter(Boolean).slice(0, this.maxTracksRestore)
748
+ const resolved = await Promise.all(tracksToResolve.map(uri => this.resolve({ query: uri, requester }).catch(() => null)))
723
749
  const validTracks = resolved.flatMap(r => r?.tracks || [])
724
750
  if (validTracks.length && player.queue?.add) {
725
751
  if (player.queue.length <= 2) player.queue.length = 0
@@ -731,14 +757,14 @@ class Aqua extends EventEmitter {
731
757
  else player.volume = p.vol
732
758
  }
733
759
  await player.play()
734
- if (p.p > 0) setTimeout(() => player.seek?.(p.p), SEEK_DELAY)
760
+ if (p.p > 0) _functions.unrefTimeout(() => player.seek?.(p.p), SEEK_DELAY)
735
761
  if (p.pa) await player.pause(true)
736
762
  }
737
763
  if (p.nw && p.t) {
738
764
  const channel = this.client.channels?.cache?.get(p.t)
739
765
  if (channel?.messages) player.nowPlayingMessage = await channel.messages.fetch(p.nw).catch(() => null)
740
766
  }
741
- } catch {}
767
+ } catch { }
742
768
  }
743
769
 
744
770
  async _waitForFirstNode(timeout = NODE_TIMEOUT) {
@@ -756,6 +782,7 @@ class Aqua extends EventEmitter {
756
782
  if (this.leastUsedNodes.length) { cleanup(); resolve() }
757
783
  }
758
784
  const timer = setTimeout(() => { cleanup(); reject(new Error('Timeout waiting for first node')) }, timeout)
785
+ timer.unref?.()
759
786
  this.on(AqualinkEvents.NodeConnect, onReady)
760
787
  this.on(AqualinkEvents.NodeCreate, onReady)
761
788
  onReady()