aqualink 2.17.3 → 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.
@@ -52,7 +52,9 @@ const DEFAULT_OPTIONS = Object.freeze({
52
52
  resumePlayback: true,
53
53
  cooldownTime: 5000,
54
54
  maxFailoverAttempts: 5
55
- })
55
+ }),
56
+ maxQueueSave: 10,
57
+ maxTracksRestore: 20
56
58
  })
57
59
 
58
60
  const _functions = {
@@ -118,6 +120,8 @@ class Aqua extends EventEmitter {
118
120
  this.allowedDomains = merged.allowedDomains || []
119
121
  this.loadBalancer = merged.loadBalancer
120
122
  this.useHttp2 = merged.useHttp2
123
+ this.maxQueueSave = merged.maxQueueSave
124
+ this.maxTracksRestore = merged.maxTracksRestore
121
125
  this.send = merged.send || this._createDefaultSend()
122
126
 
123
127
  this._nodeStates = new Map()
@@ -489,13 +493,18 @@ class Aqua extends EventEmitter {
489
493
  updateVoiceState({ d, t }) {
490
494
  if (!d?.guild_id || (t !== 'VOICE_STATE_UPDATE' && t !== 'VOICE_SERVER_UPDATE')) return
491
495
  const player = this.players.get(d.guild_id)
492
- if (!player || !player.nodes?.connected) return
496
+ if (!player) return
497
+
498
+ d.txId = player.txId
493
499
  if (t === 'VOICE_STATE_UPDATE') {
494
500
  if (d.user_id !== this.clientId) return
495
- if (!d.channel_id) return void this.destroyPlayer(d.guild_id)
496
501
  if (player.connection) {
497
- player.connection.sessionId = d.session_id
498
- 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
+ }
499
508
  }
500
509
  } else {
501
510
  player.connection?.setServerUpdate(d)
@@ -515,7 +524,7 @@ class Aqua extends EventEmitter {
515
524
  createConnection(options) {
516
525
  if (!this.initiated) throw new Error('Aqua not initialized')
517
526
  const existing = this.players.get(options.guildId)
518
- if (existing) {
527
+ if (existing && !existing.destroyed) {
519
528
  if (options.voiceChannel && existing.voiceChannel !== options.voiceChannel) {
520
529
  _functions.safeCall(() => existing.connect(options))
521
530
  }
@@ -528,7 +537,12 @@ class Aqua extends EventEmitter {
528
537
 
529
538
  createPlayer(node, options) {
530
539
  const existing = this.players.get(options.guildId)
531
- 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
+ }
532
546
  const player = new Player(this, node, options)
533
547
  this.players.set(options.guildId, player)
534
548
  node?.players?.add?.(player)
@@ -654,7 +668,7 @@ class Aqua extends EventEmitter {
654
668
  u: player.current?.uri || null,
655
669
  p: player.position || 0,
656
670
  ts: player.timestamp || 0,
657
- q: player.queue.slice(0, MAX_QUEUE_SAVE).map(tr => tr.uri),
671
+ q: player.queue.slice(0, this.maxQueueSave).map(tr => tr.uri),
658
672
  r: requester ? `${requester.id}:${requester.username}` : null,
659
673
  vol: player.volume,
660
674
  pa: player.paused,
@@ -730,7 +744,7 @@ class Aqua extends EventEmitter {
730
744
  })
731
745
  player._resuming = !!p.resuming
732
746
  const requester = _functions.parseRequester(p.r)
733
- const tracksToResolve = [p.u, ...(p.q || [])].filter(Boolean).slice(0, MAX_TRACKS_RESTORE)
747
+ const tracksToResolve = [p.u, ...(p.q || [])].filter(Boolean).slice(0, this.maxTracksRestore)
734
748
  const resolved = await Promise.all(tracksToResolve.map(uri => this.resolve({ query: uri, requester }).catch(() => null)))
735
749
  const validTracks = resolved.flatMap(r => r?.tracks || [])
736
750
  if (validTracks.length && player.queue?.add) {
@@ -122,6 +122,7 @@ class Connection {
122
122
  this.token = null
123
123
  this.region = null
124
124
  this.sequence = 0
125
+ this.txId = 0
125
126
 
126
127
  this._lastEndpoint = null
127
128
  this._stateFlags = 0
@@ -136,6 +137,7 @@ class Connection {
136
137
  this._lastSentVoiceKey = ''
137
138
 
138
139
  this._nullChannelTimer = null
140
+ this.isWaitingForDisconnect = false
139
141
 
140
142
  this._lastStateReqAt = 0
141
143
  this._stateGeneration = 0
@@ -178,6 +180,8 @@ class Connection {
178
180
 
179
181
  if (this._lastEndpoint === endpoint && this.token === data.token) return
180
182
 
183
+ if (data.txId && data.txId < this.txId) return
184
+
181
185
  this._stateGeneration++
182
186
 
183
187
  if (this._lastEndpoint !== endpoint) {
@@ -212,12 +216,10 @@ class Connection {
212
216
 
213
217
  if (channelId) this._clearNullChannelTimer()
214
218
 
215
- const reqCh = p?._voiceRequestChannel
216
- const reqFresh = !!(reqCh && (Date.now() - (p._voiceRequestAt || 0)) < 5000)
219
+ if (data.txId && data.txId < this.txId) return
217
220
 
218
221
  if (!channelId) {
219
- if (reqFresh) return
220
-
222
+ this.isWaitingForDisconnect = true
221
223
  if (!this._nullChannelTimer) {
222
224
  this._nullChannelTimer = setTimeout(() => {
223
225
  this._nullChannelTimer = null
@@ -228,16 +230,16 @@ class Connection {
228
230
  return
229
231
  }
230
232
 
231
- if (reqFresh && channelId !== reqCh) return
233
+ this.isWaitingForDisconnect = false
232
234
 
233
- if (reqCh && channelId === reqCh) {
234
- p._voiceRequestChannel = null
235
- }
235
+ if (p && p.txId > this.txId) this.txId = p.txId
236
236
 
237
237
  let needsUpdate = false
238
238
 
239
239
  if (this.voiceChannel !== channelId) {
240
- this._aqua.emit(AqualinkEvents.PlayerMove, this.voiceChannel, channelId)
240
+ p._reconnecting = true
241
+ p._resuming = true
242
+ this._aqua.emit(AqualinkEvents.PlayerMove, p, this.voiceChannel, channelId)
241
243
  this.voiceChannel = channelId
242
244
  p.voiceChannel = channelId
243
245
  needsUpdate = true
@@ -313,6 +315,7 @@ class Connection {
313
315
  return false
314
316
  }
315
317
 
318
+ this.txId = this._player.txId || this.txId
316
319
  this._stateFlags |= STATE.ATTEMPTING_RESUME
317
320
  this._reconnectAttempts++
318
321
  this._aqua.emit(AqualinkEvents.Debug, `Attempt resume: guild=${this._guildId} endpoint=${this.endpoint} session=${this.sessionId}`)
@@ -60,6 +60,7 @@ class Filters {
60
60
  if (!player) throw new Error('Player instance is required')
61
61
  this.player = player
62
62
  this._pendingUpdate = false
63
+ this._dirty = new Set()
63
64
 
64
65
 
65
66
  this.filters = {
@@ -102,6 +103,7 @@ class Filters {
102
103
  if (current && _utils.shallowEqual(current, defaults, options, keys)) return this
103
104
 
104
105
  this.filters[filterName] = Object.assign({}, defaults, options)
106
+ this._dirty.add(filterName)
105
107
  return this._scheduleUpdate()
106
108
  }
107
109
 
@@ -122,6 +124,7 @@ class Filters {
122
124
  const next = bands ?? EMPTY_ARRAY
123
125
  if (_utils.equalizerEqual(this.filters.equalizer, next)) return this
124
126
  this.filters.equalizer = next
127
+ this._dirty.add('equalizer')
125
128
  return this._scheduleUpdate()
126
129
  }
127
130
 
@@ -147,7 +150,17 @@ class Filters {
147
150
 
148
151
  this.presets.bassboost = value
149
152
  const gain = (value - 1) * (1.25 / 9) - 0.25
150
- return this.setEqualizer(_utils.makeEqArray(13, gain))
153
+
154
+ const current = Array.isArray(this.filters.equalizer) ? [...this.filters.equalizer] : []
155
+ const bands = _utils.makeEqArray(13, gain)
156
+
157
+ for (const b of bands) {
158
+ const idx = current.findIndex(e => e.band === b.band)
159
+ if (idx !== -1) current[idx] = b
160
+ else current.push(b)
161
+ }
162
+
163
+ return this.setEqualizer(current)
151
164
  }
152
165
 
153
166
  setSlowmode(enabled, options = {}) {
@@ -182,14 +195,23 @@ class Filters {
182
195
  const f = this.filters
183
196
  let changed = false
184
197
 
185
- if (f.volume !== 1) { f.volume = 1; changed = true }
186
- if (!_utils.eqIsEmpty(f.equalizer)) { f.equalizer = EMPTY_ARRAY; changed = true }
198
+ if (f.volume !== 1) {
199
+ f.volume = 1
200
+ this._dirty.add('volume')
201
+ changed = true
202
+ }
203
+ if (!_utils.eqIsEmpty(f.equalizer)) {
204
+ f.equalizer = EMPTY_ARRAY
205
+ this._dirty.add('equalizer')
206
+ changed = true
207
+ }
187
208
 
188
209
  const filterNames = Object.keys(FILTER_DEFAULTS)
189
210
  for (let i = 0; i < filterNames.length; i++) {
190
211
  const key = filterNames[i]
191
212
  if (f[key] !== null) {
192
213
  f[key] = null
214
+ this._dirty.add(key)
193
215
  changed = true
194
216
  }
195
217
  }
@@ -202,10 +224,18 @@ class Filters {
202
224
  }
203
225
 
204
226
  async updateFilters() {
205
- if (!this.player) return this
227
+ if (!this.player || !this._dirty.size) return this
228
+
229
+ const payload = {}
230
+ for (const key of this._dirty) {
231
+ payload[key] = this.filters[key]
232
+ }
233
+
234
+ this._dirty.clear()
235
+
206
236
  await this.player.nodes.rest.updatePlayer({
207
237
  guildId: this.player.guildId,
208
- data: { filters: this.filters }
238
+ data: { filters: payload }
209
239
  })
210
240
  return this
211
241
  }
@@ -1,11 +1,16 @@
1
1
  'use strict'
2
2
 
3
- const WebSocket = require('ws')
3
+ const IS_BUN = !!(process?.isBun || process?.versions?.bun || globalThis.Bun)
4
+ if (process && typeof process.isBun !== 'boolean') process.isBun = IS_BUN
5
+
6
+ const WebSocketImpl = process.isBun ? globalThis.WebSocket : require('ws')
7
+
4
8
  const Rest = require('./Rest')
5
9
  const { AqualinkEvents } = require('./AqualinkEvents')
6
10
 
7
11
  const privateData = new WeakMap()
8
12
 
13
+ const NODE_STATE = Object.freeze({ IDLE: 0, CONNECTING: 1, READY: 2, DISCONNECTING: 3, RECONNECTING: 4 })
9
14
  const WS_STATES = Object.freeze({ CONNECTING: 0, OPEN: 1, CLOSING: 2, CLOSED: 3 })
10
15
  const FATAL_CLOSE_CODES = Object.freeze([4003, 4004, 4010, 4011, 4012, 4015])
11
16
  const WS_PATH = '/v4/websocket'
@@ -15,11 +20,12 @@ const OPS_READY = 'ready'
15
20
  const OPS_PLAYER_UPDATE = 'playerUpdate'
16
21
  const OPS_EVENT = 'event'
17
22
 
23
+ const unrefTimer = (t) => { try { t?.unref?.() } catch {} }
24
+
18
25
  const _functions = {
19
26
  buildWsUrl(host, port, ssl) {
20
27
  const needsBrackets = host.includes(':') && !host.startsWith('[')
21
- const h = needsBrackets ? `[${host}]` : host
22
- return `ws${ssl ? 's' : ''}://${h}:${port}${WS_PATH}`
28
+ return `ws${ssl ? 's' : ''}://${needsBrackets ? `[${host}]` : host}:${port}${WS_PATH}`
23
29
  },
24
30
 
25
31
  isLyricsOp(op) {
@@ -30,8 +36,13 @@ const _functions = {
30
36
  if (!reason) return 'No reason provided'
31
37
  if (typeof reason === 'string') return reason
32
38
  if (Buffer.isBuffer(reason)) {
33
- try { return reason.toString('utf8') }
34
- catch { return String(reason) }
39
+ try { return reason.toString('utf8') } catch { return String(reason) }
40
+ }
41
+ if (reason instanceof ArrayBuffer) {
42
+ try { return Buffer.from(reason).toString('utf8') } catch { return String(reason) }
43
+ }
44
+ if (ArrayBuffer.isView(reason)) {
45
+ try { return Buffer.from(reason.buffer, reason.byteOffset, reason.byteLength).toString('utf8') } catch { return String(reason) }
35
46
  }
36
47
  if (typeof reason === 'object') return reason.message || reason.code || JSON.stringify(reason)
37
48
  return String(reason)
@@ -79,6 +90,7 @@ class Node {
79
90
  this.skipUTF8Validation = options.skipUTF8Validation ?? true
80
91
 
81
92
  this.connected = false
93
+ this.state = NODE_STATE.IDLE
82
94
  this.info = null
83
95
  this.ws = null
84
96
  this.reconnectAttempted = 0
@@ -87,6 +99,9 @@ class Node {
87
99
  this._isConnecting = false
88
100
  this.isNodelink = false
89
101
 
102
+ this._wsIsBun = !!process.isBun
103
+ this._bunCleanup = null
104
+
90
105
  this.stats = {
91
106
  players: 0,
92
107
  playingPlayers: 0,
@@ -113,13 +128,11 @@ class Node {
113
128
 
114
129
  _buildHeaders() {
115
130
  const headers = {
116
- 'Authorization': this.auth,
131
+ Authorization: this.auth,
117
132
  'User-Id': this.aqua.clientId,
118
133
  'Client-Name': this._clientName
119
134
  }
120
- if (this.sessionId) {
121
- headers['Session-Id'] = this.sessionId
122
- }
135
+ if (this.sessionId) headers['Session-Id'] = this.sessionId
123
136
  return headers
124
137
  }
125
138
 
@@ -139,6 +152,7 @@ class Node {
139
152
 
140
153
  async _handleOpen() {
141
154
  this.connected = true
155
+ this.state = NODE_STATE.READY
142
156
  this._isConnecting = false
143
157
  this.reconnectAttempted = 0
144
158
  this._emitDebug('WebSocket connection established')
@@ -147,11 +161,11 @@ class Node {
147
161
  const timeoutId = setTimeout(() => {
148
162
  if (!this.isDestroyed) this._emitError('Node info fetch timeout')
149
163
  }, Node.INFO_FETCH_TIMEOUT)
150
- timeoutId.unref?.()
164
+ unrefTimer(timeoutId)
151
165
 
152
166
  try {
153
167
  this.info = await this.rest.makeRequest('GET', '/v4/info')
154
- this.isNodelink = !!this.info?.isNodelink;
168
+ this.isNodelink = !!this.info?.isNodelink
155
169
  } catch (err) {
156
170
  this.info = null
157
171
  this._emitError(`Failed to fetch node info: ${_functions.errMsg(err)}`)
@@ -171,12 +185,9 @@ class Node {
171
185
  _handleMessage(data, isBinary) {
172
186
  if (isBinary) return
173
187
 
174
- const str = Buffer.isBuffer(data) ? data.toString('utf8') : data
175
- if (!str || typeof str !== 'string') return
176
-
177
188
  let payload
178
189
  try {
179
- payload = JSON.parse(str)
190
+ payload = JSON.parse(data)
180
191
  } catch (err) {
181
192
  this._emitDebug(() => `Invalid JSON from Lavalink: ${err.message}`)
182
193
  return
@@ -195,7 +206,6 @@ class Node {
195
206
  _emitToPlayer(eventName, payload) {
196
207
  const player = this._getPlayer(payload?.guildId)
197
208
  if (!player?.emit) return
198
-
199
209
  try {
200
210
  player.emit(eventName, payload)
201
211
  } catch (err) {
@@ -214,9 +224,14 @@ class Node {
214
224
 
215
225
  _handleClose(code, reason) {
216
226
  this.connected = false
227
+ const wasReady = this.state === NODE_STATE.READY
228
+ this.state = this.isDestroyed ? NODE_STATE.IDLE : NODE_STATE.RECONNECTING
217
229
  this._isConnecting = false
218
230
 
219
- this.aqua.emit(AqualinkEvents.NodeDisconnect, this, { code, reason: _functions.reasonToString(reason) })
231
+ this.aqua.emit(AqualinkEvents.NodeDisconnect, this, {
232
+ code,
233
+ reason: _functions.reasonToString(reason)
234
+ })
220
235
 
221
236
  if (this.isDestroyed) return
222
237
 
@@ -244,9 +259,13 @@ class Node {
244
259
  const attempt = ++this.reconnectAttempted
245
260
 
246
261
  if (this.infiniteReconnects) {
247
- this.aqua.emit(AqualinkEvents.NodeReconnect, this, { infinite: true, attempt, backoffTime: Node.INFINITE_BACKOFF })
262
+ this.aqua.emit(AqualinkEvents.NodeReconnect, this, {
263
+ infinite: true,
264
+ attempt,
265
+ backoffTime: Node.INFINITE_BACKOFF
266
+ })
248
267
  this.reconnectTimeoutId = setTimeout(this._boundHandlers.connect, Node.INFINITE_BACKOFF)
249
- this.reconnectTimeoutId.unref?.()
268
+ unrefTimer(this.reconnectTimeoutId)
250
269
  return
251
270
  }
252
271
 
@@ -259,7 +278,7 @@ class Node {
259
278
  const backoffTime = this._calcBackoff(attempt)
260
279
  this.aqua.emit(AqualinkEvents.NodeReconnect, this, { infinite: false, attempt, backoffTime })
261
280
  this.reconnectTimeoutId = setTimeout(this._boundHandlers.connect, backoffTime)
262
- this.reconnectTimeoutId.unref?.()
281
+ unrefTimer(this.reconnectTimeoutId)
263
282
  }
264
283
 
265
284
  _calcBackoff(attempt) {
@@ -269,30 +288,70 @@ class Node {
269
288
  }
270
289
 
271
290
  _clearReconnectTimeout() {
272
- if (this.reconnectTimeoutId) {
273
- clearTimeout(this.reconnectTimeoutId)
274
- this.reconnectTimeoutId = null
275
- }
291
+ if (!this.reconnectTimeoutId) return
292
+ clearTimeout(this.reconnectTimeoutId)
293
+ this.reconnectTimeoutId = null
276
294
  }
277
295
 
278
296
  connect() {
279
297
  if (this.isDestroyed || this._isConnecting) return
280
298
 
281
- const currentState = this.ws?.readyState
282
- if (currentState === WS_STATES.OPEN) {
299
+ const state = this.ws?.readyState
300
+ if (state === WS_STATES.OPEN) {
283
301
  this._emitDebug('WebSocket already connected')
284
302
  return
285
303
  }
286
- if (currentState === WS_STATES.CONNECTING || currentState === WS_STATES.CLOSING) {
304
+ if (state === WS_STATES.CONNECTING || state === WS_STATES.CLOSING) {
287
305
  this._emitDebug('WebSocket is connecting/closing; skipping new connect')
288
306
  return
289
307
  }
290
308
 
291
309
  this._isConnecting = true
310
+ this.state = NODE_STATE.CONNECTING
292
311
  this._cleanup()
293
312
 
294
313
  try {
295
- const ws = new WebSocket(this.wsUrl, {
314
+ const h = this._boundHandlers
315
+
316
+ if (this._wsIsBun) {
317
+ const ws = new WebSocketImpl(this.wsUrl, { headers: this._headers })
318
+ ws.binaryType = 'arraybuffer'
319
+
320
+ const offs = []
321
+ const add = (type, fn, once = false) => {
322
+ const wrapped = once
323
+ ? (ev) => { try { ws.removeEventListener(type, wrapped) } catch {} ; fn(ev) }
324
+ : fn
325
+ ws.addEventListener(type, wrapped)
326
+ offs.push(() => { try { ws.removeEventListener(type, wrapped) } catch {} })
327
+ }
328
+
329
+ add('open', () => h.open(), true)
330
+
331
+ add('error', (event) => {
332
+ const err = event?.error
333
+ h.error(err instanceof Error ? err : new Error('WebSocket error'))
334
+ }, true)
335
+
336
+ add('message', (event) => {
337
+ const data = event?.data
338
+ if (typeof data === 'string') h.message(data, false)
339
+ else h.message(data, true)
340
+ })
341
+
342
+ add('close', (event) => {
343
+ h.close(
344
+ typeof event?.code === 'number' ? event.code : Node.WS_CLOSE_NORMAL,
345
+ typeof event?.reason === 'string' ? event.reason : ''
346
+ )
347
+ }, true)
348
+
349
+ this._bunCleanup = () => { for (let i = 0; i < offs.length; i++) offs[i]() }
350
+ this.ws = ws
351
+ return
352
+ }
353
+
354
+ const ws = new WebSocketImpl(this.wsUrl, {
296
355
  headers: this._headers,
297
356
  perMessageDeflate: true,
298
357
  handshakeTimeout: this.timeout,
@@ -302,7 +361,6 @@ class Node {
302
361
 
303
362
  ws.binaryType = 'nodebuffer'
304
363
 
305
- const h = this._boundHandlers
306
364
  ws.once('open', h.open)
307
365
  ws.once('error', h.error)
308
366
  ws.on('message', h.message)
@@ -320,12 +378,20 @@ class Node {
320
378
  const ws = this.ws
321
379
  if (!ws) return
322
380
 
323
- ws.removeAllListeners()
381
+ if (this._wsIsBun) {
382
+ try { this._bunCleanup?.() } catch {}
383
+ this._bunCleanup = null
384
+ } else {
385
+ ws.removeAllListeners?.()
386
+ }
324
387
 
325
388
  try {
326
389
  const state = ws.readyState
327
- if (state === WS_STATES.OPEN) ws.close(Node.WS_CLOSE_NORMAL)
328
- else if (state !== WS_STATES.CLOSED) ws.terminate()
390
+ if (state === WS_STATES.OPEN || state === WS_STATES.CONNECTING) {
391
+ ws.close(Node.WS_CLOSE_NORMAL)
392
+ } else if (!this._wsIsBun && state !== WS_STATES.CLOSED) {
393
+ ws.terminate?.()
394
+ }
329
395
  } catch (err) {
330
396
  this._emitError(`WebSocket cleanup error: ${_functions.errMsg(err)}`)
331
397
  }
@@ -337,6 +403,7 @@ class Node {
337
403
  if (this.isDestroyed) return
338
404
 
339
405
  this.isDestroyed = true
406
+ this.state = NODE_STATE.IDLE
340
407
  this._isConnecting = false
341
408
  this._clearReconnectTimeout()
342
409
  this._cleanup()
@@ -415,7 +482,6 @@ class Node {
415
482
  this._headers['Session-Id'] = sessionId
416
483
 
417
484
  this.aqua.emit(AqualinkEvents.NodeReady, this, { resumed: !!payload.resumed })
418
- this.aqua.emit(AqualinkEvents.NodeConnect, this)
419
485
 
420
486
  if (this.autoResume) {
421
487
  setImmediate(() => {
@@ -427,9 +493,7 @@ class Node {
427
493
  }
428
494
 
429
495
  async _resumePlayers() {
430
- if (!this.sessionId) {
431
- return
432
- }
496
+ if (!this.sessionId) return
433
497
 
434
498
  try {
435
499
  await this.rest.makeRequest('PATCH', `/v4/sessions/${this.sessionId}`, {
@@ -452,7 +516,11 @@ class Node {
452
516
 
453
517
  _emitDebug(message) {
454
518
  if (!this.aqua?.listenerCount?.(AqualinkEvents.Debug)) return
455
- this.aqua.emit(AqualinkEvents.Debug, this.name, typeof message === 'function' ? message() : message)
519
+ this.aqua.emit(
520
+ AqualinkEvents.Debug,
521
+ this.name,
522
+ typeof message === 'function' ? message() : message
523
+ )
456
524
  }
457
525
  }
458
526
 
@@ -7,6 +7,7 @@ const Filters = require('./Filters')
7
7
  const { spAutoPlay, scAutoPlay } = require('../handlers/autoplay')
8
8
  const Queue = require('./Queue')
9
9
 
10
+ const PLAYER_STATE = Object.freeze({ IDLE: 0, CONNECTING: 1, READY: 2, DISCONNECTING: 3, DESTROYED: 4 })
10
11
  const LOOP_MODES = Object.freeze({ NONE: 0, TRACK: 1, QUEUE: 2 })
11
12
  const LOOP_MODE_NAMES = Object.freeze(['none', 'track', 'queue'])
12
13
  const EVENT_HANDLERS = Object.freeze({
@@ -166,6 +167,8 @@ class Player extends EventEmitter {
166
167
  this.textChannel = options.textChannel
167
168
  this.voiceChannel = options.voiceChannel
168
169
  this.playing = this.paused = this.connected = this.destroyed = false
170
+ this.state = PLAYER_STATE.IDLE
171
+ this.txId = 0
169
172
  this.isAutoplayEnabled = this.isAutoplay = false
170
173
  this.autoplaySeed = this.current = this.nowPlayingMessage = null
171
174
  this.position = this.timestamp = this.ping = 0
@@ -244,12 +247,12 @@ class Player extends EventEmitter {
244
247
  this._voiceDownSince = Date.now()
245
248
  this._createTimer(() => {
246
249
  if (this.connected || this.destroyed || this.nodes?.info?.isNodelink) return
247
- if (Date.now() < (this._suppressResumeUntil || 0)) return
248
250
  this.connection.attemptResume()
249
251
  }, 1000)
250
252
  }
251
253
  } else {
252
254
  this._voiceDownSince = 0
255
+ this.state = PLAYER_STATE.READY
253
256
  }
254
257
 
255
258
  this.aqua.emit(AqualinkEvents.PlayerUpdate, this, packet)
@@ -291,37 +294,9 @@ class Player extends EventEmitter {
291
294
  return this
292
295
  }
293
296
 
294
- async _waitForConnection(timeout = RESUME_TIMEOUT) {
295
- if (this.destroyed) return
296
- if (this.connected) return
297
- return new Promise((resolve, reject) => {
298
- let timer
299
- const cleanup = () => {
300
- if (timer) { this._pendingTimers?.delete(timer); clearTimeout(timer) }
301
- this.off('playerUpdate', onUpdate)
302
- }
303
- const onUpdate = payload => {
304
- if (this.destroyed) { cleanup(); return reject(new Error('Player destroyed')) }
305
- if (payload?.state?.connected || _functions.isNum(payload?.state?.time)) {
306
- cleanup()
307
- return resolve()
308
- }
309
- }
310
- this.on('playerUpdate', onUpdate)
311
- timer = this._createTimer(() => { cleanup(); reject(new Error('No connection confirmation')) }, timeout)
312
- })
313
- }
314
297
 
315
298
  async play() {
316
299
  if (this.destroyed || !this.queue.size) return this
317
- if (!this.connected) {
318
- try {
319
- await this._waitForConnection(RESUME_TIMEOUT)
320
- if (!this.connected || this.destroyed) return this
321
- } catch {
322
- return this
323
- }
324
- }
325
300
 
326
301
  const item = this.queue.dequeue()
327
302
  if (!item) return this
@@ -348,8 +323,9 @@ class Player extends EventEmitter {
348
323
  this.deaf = options.deaf !== undefined ? !!options.deaf : true
349
324
  this.mute = !!options.mute
350
325
  this.destroyed = false
326
+ this.state = PLAYER_STATE.CONNECTING
351
327
 
352
- this._voiceRequestAt = Date.now()
328
+ this.txId++
353
329
  this._voiceRequestChannel = voiceChannel
354
330
 
355
331
  this.voiceChannel = voiceChannel
@@ -399,7 +375,11 @@ class Player extends EventEmitter {
399
375
  }
400
376
 
401
377
  destroy(options = {}) {
402
- const { preserveClient = true, skipRemote = false } = options
378
+ const {
379
+ preserveClient = true, skipRemote = false,
380
+ preserveMessage = false, preserveReconnecting = false,
381
+ preserveTracks = false
382
+ } = options
403
383
  if (this.destroyed && !this.queue) return this
404
384
 
405
385
  if (!this.destroyed) {
@@ -416,12 +396,13 @@ class Player extends EventEmitter {
416
396
  this._pendingTimers = null
417
397
 
418
398
  this.connected = this.playing = this.paused = this.isAutoplay = false
399
+ this.state = PLAYER_STATE.DESTROYED
419
400
  this.autoplayRetries = this.reconnectionRetries = 0
420
- this._reconnecting = false
401
+ if (!preserveReconnecting) this._reconnecting = false
421
402
  this._lastVoiceChannel = this.voiceChannel
422
403
  this.voiceChannel = null
423
404
 
424
- if (this.shouldDeleteMessage && this.nowPlayingMessage) {
405
+ if (this.shouldDeleteMessage && this.nowPlayingMessage && !preserveMessage) {
425
406
  _functions.safeDel(this.nowPlayingMessage)
426
407
  this.nowPlayingMessage = null
427
408
  }
@@ -446,7 +427,7 @@ class Player extends EventEmitter {
446
427
  this._dataStore?.clear()
447
428
  this._dataStore = null
448
429
 
449
- if (this.current?.dispose && !this.aqua?.options?.autoResume) this.current.dispose()
430
+ if (this.current?.dispose && !this.aqua?.options?.autoResume && !preserveTracks) this.current.dispose()
450
431
  if (this.connection) {
451
432
  try { this.connection.destroy() } catch { }
452
433
  }
@@ -677,11 +658,12 @@ class Player extends EventEmitter {
677
658
  return null
678
659
  }
679
660
 
680
- trackStart() {
661
+ trackStart(player, track, payload = {}) {
681
662
  if (this.destroyed) return
682
663
  this.playing = true
683
664
  this.paused = false
684
- this.aqua.emit(AqualinkEvents.TrackStart, this, this.current)
665
+ this.aqua.emit(AqualinkEvents.TrackStart, this, this.current, { ...payload, resumed: this._resuming })
666
+ this._resuming = false
685
667
  }
686
668
 
687
669
  async trackEnd(player, track, payload) {
@@ -692,12 +674,12 @@ class Player extends EventEmitter {
692
674
  const isReplaced = reason === 'replaced'
693
675
 
694
676
  if (track) this.previousTracks.push(track)
695
- if (this.shouldDeleteMessage) _functions.safeDel(this.nowPlayingMessage)
677
+ if (this.shouldDeleteMessage && !this._reconnecting && !this._resuming) _functions.safeDel(this.nowPlayingMessage)
696
678
  this.current = null
697
679
 
698
680
  if (isFailure) {
699
681
  if (!this.queue.size) {
700
- this.clearData()
682
+ this.clearData({ preserveTracks: this._reconnecting || this._resuming })
701
683
  this.aqua.emit(AqualinkEvents.QueueEnd, this)
702
684
  } else {
703
685
  this.aqua.emit(AqualinkEvents.TrackEnd, this, track, reason)
@@ -718,7 +700,7 @@ class Player extends EventEmitter {
718
700
  } else {
719
701
  this.playing = false
720
702
  if (this.leaveOnEnd && !this.destroyed) {
721
- this.clearData()
703
+ this.clearData({ preserveTracks: this._reconnecting || this._resuming })
722
704
  this.destroy()
723
705
  }
724
706
  this.aqua.emit(AqualinkEvents.QueueEnd, this)
@@ -751,51 +733,33 @@ class Player extends EventEmitter {
751
733
  mixStarted(p, t, payload) { _functions.emitIfActive(this, AqualinkEvents.MixStarted, t, payload) }
752
734
  mixEnded(p, t, payload) { _functions.emitIfActive(this, AqualinkEvents.MixEnded, t, payload) }
753
735
 
736
+
754
737
  async _attemptVoiceResume() {
755
738
  if (!this.connection?.sessionId) throw new Error('No session')
756
739
  if (!await this.connection.attemptResume()) throw new Error('Resume failed')
757
- return new Promise((resolve, reject) => {
758
- let timeout
759
- const cleanup = () => {
760
- if (timeout) { this._pendingTimers?.delete(timeout); clearTimeout(timeout) }
761
- this.off('playerUpdate', onUpdate)
762
- }
763
- const onUpdate = payload => {
764
- if (payload?.state?.connected || _functions.isNum(payload?.state?.time)) {
765
- cleanup()
766
- resolve()
767
- }
768
- }
769
- timeout = this._createTimer(() => { cleanup(); reject(new Error('No confirmation')) }, RESUME_TIMEOUT)
770
- this.on('playerUpdate', onUpdate)
771
- })
772
740
  }
773
741
 
774
742
  async socketClosed(player, track, payload) {
775
743
  if (this.destroyed) return
776
744
  const code = payload?.code
777
-
778
- if (code === 4014 || code === 4022) {
779
- this.aqua.emit(AqualinkEvents.SocketClosed, this, payload)
780
-
781
- this.connected = false
782
- if (!this._voiceDownSince) this._voiceDownSince = Date.now()
783
-
784
- if (code !== 4014) {
785
- this._suppressResumeUntil = Date.now() + 3000
786
- }
787
- return
788
- }
745
+ let isRecoverable = [4015, 4009, 4006, 4014, 4022].includes(code)
746
+ if (code === 4014 && this.connection?.isWaitingForDisconnect) isRecoverable = false
789
747
 
790
748
  if (code === 4015 && !this.nodes?.info?.isNodelink) {
791
749
  try { await this._attemptVoiceResume(); return } catch { /* ignore */ }
792
750
  }
793
751
 
794
- if (![4015, 4009, 4006].includes(code)) {
752
+ if (!isRecoverable) {
795
753
  this.aqua.emit(AqualinkEvents.SocketClosed, this, payload)
796
754
  return
797
755
  }
798
756
 
757
+ if (code === 4014 || code === 4022) {
758
+ this.connected = false
759
+ if (!this._voiceDownSince) this._voiceDownSince = Date.now()
760
+ if (code === 4022) this._suppressResumeUntil = Date.now() + 3000
761
+ }
762
+
799
763
  if (this._reconnecting) return
800
764
 
801
765
  const aqua = this.aqua
@@ -817,18 +781,25 @@ class Player extends EventEmitter {
817
781
  currentTrack: this.current,
818
782
  queue: this.queue?.toArray() || [],
819
783
  previousIdentifiers: Array.from(this.previousIdentifiers),
820
- autoplaySeed: this.autoplaySeed
784
+ autoplaySeed: this.autoplaySeed,
785
+ nowPlayingMessage: this.nowPlayingMessage
821
786
  }
822
787
 
823
788
  this._reconnecting = true
824
- this.destroy({ preserveClient: true, skipRemote: true })
789
+ this.destroy({
790
+ preserveClient: true, skipRemote: true,
791
+ preserveMessage: true, preserveReconnecting: true,
792
+ preserveTracks: true
793
+ })
825
794
 
826
795
  const reconnectTimers = new Set()
827
796
  const tryReconnect = async attempt => {
828
797
  if (aqua?.destroyed) { _functions.clearTimers(reconnectTimers); return }
829
798
  try {
830
799
  const np = await aqua.createConnection({
831
- guildId, voiceChannel: vcId, textChannel: tcId, deaf, mute, defaultVolume: state.volume
800
+ guildId, voiceChannel: vcId, textChannel: tcId, deaf, mute, defaultVolume: state.volume,
801
+ preserveMessage: true,
802
+ resuming: true
832
803
  })
833
804
  if (!np) throw new Error('Failed to create player')
834
805
 
@@ -837,6 +808,7 @@ class Player extends EventEmitter {
837
808
  np.isAutoplayEnabled = state.isAutoplayEnabled
838
809
  np.autoplaySeed = state.autoplaySeed
839
810
  np.previousIdentifiers = new Set(state.previousIdentifiers)
811
+ np.nowPlayingMessage = state.nowPlayingMessage
840
812
 
841
813
  const ct = state.currentTrack
842
814
  if (ct) np.queue.add(ct)
@@ -894,11 +866,12 @@ class Player extends EventEmitter {
894
866
  return this._dataStore?.get(key)
895
867
  }
896
868
 
897
- clearData() {
869
+ clearData(options = {}) {
870
+ const { preserveTracks = false } = options
898
871
  this.previousTracks?.clear()
899
872
  this._dataStore?.clear()
900
873
  this.previousIdentifiers?.clear()
901
- if (this.current?.dispose) this.current.dispose()
874
+ if (this.current?.dispose && !preserveTracks) this.current.dispose()
902
875
  this.current = null
903
876
  this.position = this.timestamp = 0
904
877
  this.queue?.clear()
@@ -1,65 +1,86 @@
1
- class Queue extends Array {
1
+ 'use strict'
2
+
3
+ class Queue {
4
+ constructor() {
5
+ this._items = []
6
+ }
7
+
2
8
  get size() {
3
- return this.length
9
+ return this._items.length
4
10
  }
5
11
 
6
12
  get first() {
7
- return this[0] || null
13
+ return this._items[0] || null
8
14
  }
9
15
 
10
16
  get last() {
11
- return this[this.length - 1] || null
17
+ return this._items[this._items.length - 1] || null
12
18
  }
13
19
 
14
- add(track) {
15
- this.push(track)
20
+ add(...tracks) {
21
+ this._items.push(...tracks)
16
22
  return this
17
23
  }
18
24
 
19
25
  remove(track) {
20
- const idx = this.indexOf(track)
26
+ const idx = this._items.indexOf(track)
21
27
  if (idx === -1) return false
22
- const removed = this[idx]
23
- this.splice(idx, 1)
28
+ const removed = this._items[idx]
29
+ this._items.splice(idx, 1)
24
30
  if (removed?.dispose) removed.dispose()
25
31
  return true
26
32
  }
27
33
 
28
34
  clear() {
29
- for (let i = 0; i < this.length; i++) {
30
- if (this[i]?.dispose) this[i].dispose()
35
+ for (let i = 0; i < this._items.length; i++) {
36
+ if (this._items[i]?.dispose) this._items[i].dispose()
31
37
  }
32
- this.length = 0
38
+ this._items.length = 0
33
39
  }
34
40
 
35
41
  shuffle() {
36
- for (let i = this.length - 1; i > 0; i--) {
42
+ for (let i = this._items.length - 1; i > 0; i--) {
37
43
  const j = Math.floor(Math.random() * (i + 1))
38
- const temp = this[i]
39
- this[i] = this[j]
40
- this[j] = temp
44
+ const temp = this._items[i]
45
+ this._items[i] = this._items[j]
46
+ this._items[j] = temp
41
47
  }
42
48
  return this
43
49
  }
44
50
 
51
+ move(from, to) {
52
+ if (from < 0 || from >= this._items.length || to < 0 || to >= this._items.length) return this
53
+ const [item] = this._items.splice(from, 1)
54
+ this._items.splice(to, 0, item)
55
+ return this
56
+ }
57
+
58
+ swap(index1, index2) {
59
+ if (index1 < 0 || index1 >= this._items.length || index2 < 0 || index2 >= this._items.length) return this
60
+ const temp = this._items[index1]
61
+ this._items[index1] = this._items[index2]
62
+ this._items[index2] = temp
63
+ return this
64
+ }
65
+
45
66
  peek() {
46
67
  return this.first
47
68
  }
48
69
 
49
70
  toArray() {
50
- return this.slice()
71
+ return [...this._items]
51
72
  }
52
73
 
53
74
  at(index) {
54
- return this[index] || null
75
+ return this._items[index] || null
55
76
  }
56
77
 
57
78
  dequeue() {
58
- return this.shift()
79
+ return this._items.shift()
59
80
  }
60
81
 
61
82
  isEmpty() {
62
- return this.length === 0
83
+ return this._items.length === 0
63
84
  }
64
85
 
65
86
  enqueue(track) {
@@ -67,4 +88,4 @@ class Queue extends Array {
67
88
  }
68
89
  }
69
90
 
70
- module.exports = Queue
91
+ module.exports = Queue
@@ -1,8 +1,21 @@
1
+ 'use strict'
2
+
1
3
  const { Buffer } = require('buffer')
2
4
  const { Agent: HttpsAgent, request: httpsRequest } = require('https')
3
5
  const { Agent: HttpAgent, request: httpRequest } = require('http')
4
6
  const http2 = require('http2')
5
- const { createBrotliDecompress, createUnzip, brotliDecompressSync, unzipSync } = require('zlib')
7
+ const {
8
+ createBrotliDecompress,
9
+ createUnzip,
10
+ brotliDecompressSync,
11
+ unzipSync,
12
+ createZstdDecompress,
13
+ zstdDecompressSync
14
+ } = require('zlib')
15
+
16
+ const unrefTimer = (t) => { try { t?.unref?.() } catch {} }
17
+
18
+ const HAS_ZSTD = typeof createZstdDecompress === 'function' && typeof zstdDecompressSync === 'function'
6
19
 
7
20
  const BASE64_LOOKUP = new Uint8Array(256)
8
21
  for (let i = 65; i <= 90; i++) BASE64_LOOKUP[i] = 1
@@ -10,9 +23,8 @@ for (let i = 97; i <= 122; i++) BASE64_LOOKUP[i] = 1
10
23
  for (let i = 48; i <= 57; i++) BASE64_LOOKUP[i] = 1
11
24
  BASE64_LOOKUP[43] = BASE64_LOOKUP[47] = BASE64_LOOKUP[61] = BASE64_LOOKUP[95] = BASE64_LOOKUP[45] = 1
12
25
 
13
- const ENCODING_NONE = 0, ENCODING_BR = 1, ENCODING_GZIP = 2, ENCODING_DEFLATE = 3
26
+ const ENCODING_NONE = 0, ENCODING_BR = 1, ENCODING_GZIP = 2, ENCODING_DEFLATE = 3, ENCODING_ZSTD = 4
14
27
  const MAX_RESPONSE_SIZE = 10485760
15
- const SMALL_RESPONSE_THRESHOLD = 512
16
28
  const COMPRESSION_MIN_SIZE = 1024
17
29
  const API_VERSION = 'v4'
18
30
  const UTF8 = 'utf8'
@@ -43,6 +55,10 @@ const _functions = {
43
55
  getEncodingType(header) {
44
56
  if (!header) return ENCODING_NONE
45
57
  const c = header.charCodeAt(0)
58
+
59
+ if (c === 122 && header.startsWith('zstd')) return ENCODING_ZSTD
60
+ if (c === 120 && header.startsWith('x-zstd')) return ENCODING_ZSTD
61
+
46
62
  if (c === 98 && header.startsWith('br')) return ENCODING_BR
47
63
  if (c === 103 && header.startsWith('gzip')) return ENCODING_GZIP
48
64
  if (c === 100 && header.startsWith('deflate')) return ENCODING_DEFLATE
@@ -53,8 +69,13 @@ const _functions = {
53
69
  return ct && ct.charCodeAt(0) === 97 && ct.includes(JSON_CT)
54
70
  },
55
71
 
56
- parseBody(text, contentType, forceJson) {
57
- return (forceJson || this.isJsonContent(contentType)) ? JSON.parse(text) : text
72
+ parseBody(data, contentType, forceJson) {
73
+ const isJson = forceJson || this.isJsonContent(contentType)
74
+ if (isJson) {
75
+ if (typeof data === 'string') return JSON.parse(data)
76
+ return JSON.parse(data)
77
+ }
78
+ return typeof data === 'string' ? data : data.toString(UTF8)
58
79
  },
59
80
 
60
81
  createHttpError(status, method, url, headers, body, statusMessage) {
@@ -68,10 +89,18 @@ const _functions = {
68
89
  },
69
90
 
70
91
  createDecompressor(type) {
92
+ if (type === ENCODING_ZSTD) {
93
+ if (!HAS_ZSTD) throw new Error('Unsupported content-encoding: zstd (zlib zstd APIs not available in this Node runtime)')
94
+ return createZstdDecompress()
95
+ }
71
96
  return type === ENCODING_BR ? createBrotliDecompress() : createUnzip()
72
97
  },
73
98
 
74
99
  decompressSync(buf, type) {
100
+ if (type === ENCODING_ZSTD) {
101
+ if (!HAS_ZSTD) throw new Error('Unsupported content-encoding: zstd (zlib zstd APIs not available in this Node runtime)')
102
+ return zstdDecompressSync(buf)
103
+ }
75
104
  return type === ENCODING_BR ? brotliDecompressSync(buf) : unzipSync(buf)
76
105
  }
77
106
  }
@@ -103,14 +132,17 @@ class Rest {
103
132
  lyrics: `${this._apiBase}/lyrics`
104
133
  })
105
134
 
135
+ const acceptEncoding = HAS_ZSTD ? 'zstd, br, gzip, deflate' : 'br, gzip, deflate'
136
+
106
137
  this.defaultHeaders = Object.freeze({
107
138
  Authorization: String(node.auth || node.password || ''),
108
139
  Accept: 'application/json, */*;q=0.5',
109
- 'Accept-Encoding': 'br, gzip, deflate',
140
+ 'Accept-Encoding': acceptEncoding,
110
141
  'User-Agent': `Aqualink/${aqua?.version || '1.0'} (Node.js ${process.version})`
111
142
  })
112
143
 
113
144
  this._headerPool = []
145
+ this._tlsOptions = null
114
146
  this._setupAgent(node)
115
147
  this.useHttp2 = !!(aqua?.options?.useHttp2)
116
148
  this._h2 = null
@@ -130,11 +162,13 @@ class Rest {
130
162
 
131
163
  if (node.ssl) {
132
164
  opts.maxCachedSessions = node.maxCachedSessions || 200
133
- if (node.rejectUnauthorized !== undefined) opts.rejectUnauthorized = node.rejectUnauthorized
134
- if (node.ca) opts.ca = node.ca
135
- if (node.cert) opts.cert = node.cert
136
- if (node.key) opts.key = node.key
137
- if (node.passphrase) opts.passphrase = node.passphrase
165
+ this._tlsOptions = Object.create(null)
166
+ if (node.rejectUnauthorized !== undefined) this._tlsOptions.rejectUnauthorized = opts.rejectUnauthorized = node.rejectUnauthorized
167
+ if (node.ca) this._tlsOptions.ca = opts.ca = node.ca
168
+ if (node.cert) this._tlsOptions.cert = opts.cert = node.cert
169
+ if (node.key) this._tlsOptions.key = opts.key = node.key
170
+ if (node.passphrase) this._tlsOptions.passphrase = opts.passphrase = node.passphrase
171
+ if (node.servername) this._tlsOptions.servername = node.servername
138
172
  }
139
173
 
140
174
  this.agent = new (node.ssl ? HttpsAgent : HttpAgent)(opts)
@@ -160,7 +194,7 @@ class Rest {
160
194
 
161
195
  _buildHeaders(hasPayload, payloadLength) {
162
196
  if (!hasPayload) return this.defaultHeaders
163
- let h = this._headerPool.pop() || Object.create(null)
197
+ const h = this._headerPool.pop() || Object.create(null)
164
198
  h.Authorization = this.defaultHeaders.Authorization
165
199
  h.Accept = this.defaultHeaders.Accept
166
200
  h['Accept-Encoding'] = this.defaultHeaders['Accept-Encoding']
@@ -194,7 +228,8 @@ class Rest {
194
228
 
195
229
  _h1Request(method, url, headers, payload) {
196
230
  return new Promise((resolve, reject) => {
197
- let req, timer, done = false, chunks = [], size = 0, prealloc = null
231
+ let req, timer, done = false
232
+ let chunks = null, size = 0, prealloc = null
198
233
 
199
234
  const finish = (ok, val) => {
200
235
  if (done) return
@@ -208,7 +243,7 @@ class Rest {
208
243
  req = this.request(url, { method, headers, agent: this.agent, timeout: this.timeout }, (res) => {
209
244
  if (timer) { clearTimeout(timer); timer = null }
210
245
 
211
- const status = res.statusCode
246
+ const status = res.statusCode || 0
212
247
  const cl = res.headers['content-length']
213
248
  const contentType = res.headers['content-type'] || ''
214
249
 
@@ -226,9 +261,10 @@ class Rest {
226
261
  const encoding = _functions.getEncodingType(res.headers['content-encoding'])
227
262
 
228
263
  const handleResponse = (buffer) => {
229
- const text = buffer.toString(UTF8)
264
+ if (buffer.length > MAX_RESPONSE_SIZE) return finish(false, ERRORS.RESPONSE_TOO_LARGE)
265
+
230
266
  try {
231
- const result = _functions.parseBody(text, contentType, false)
267
+ const result = _functions.parseBody(buffer, contentType, false)
232
268
  if (status >= 400) {
233
269
  finish(false, _functions.createHttpError(status, method, url, res.headers, result, res.statusMessage))
234
270
  } else {
@@ -239,15 +275,15 @@ class Rest {
239
275
  }
240
276
  }
241
277
 
242
- if (clInt > 0 && clInt < SMALL_RESPONSE_THRESHOLD && encoding === ENCODING_NONE) {
243
- res.once('data', handleResponse)
244
- res.once('error', (e) => finish(false, e))
245
- return
246
- }
247
-
248
278
  if (encoding !== ENCODING_NONE && clInt > 0 && clInt < COMPRESSION_MIN_SIZE) {
249
279
  const compressed = []
250
- res.on('data', (c) => compressed.push(c))
280
+ let csize = 0
281
+
282
+ res.on('data', (c) => {
283
+ csize += c.length
284
+ if (csize > MAX_RESPONSE_SIZE) return finish(false, ERRORS.RESPONSE_TOO_LARGE)
285
+ compressed.push(c)
286
+ })
251
287
  res.once('end', () => {
252
288
  try {
253
289
  handleResponse(_functions.decompressSync(Buffer.concat(compressed), encoding))
@@ -255,12 +291,16 @@ class Rest {
255
291
  finish(false, e)
256
292
  }
257
293
  })
294
+ res.once('aborted', () => finish(false, ERRORS.RESPONSE_ABORTED))
258
295
  res.once('error', (e) => finish(false, e))
259
296
  return
260
297
  }
261
298
 
262
- if (clInt > 0 && clInt <= MAX_RESPONSE_SIZE) prealloc = Buffer.allocUnsafe(clInt)
263
- chunks = []
299
+ if (encoding === ENCODING_NONE && clInt > 0 && clInt <= MAX_RESPONSE_SIZE) {
300
+ prealloc = Buffer.allocUnsafe(clInt)
301
+ } else {
302
+ chunks = []
303
+ }
264
304
 
265
305
  let stream = res
266
306
  if (encoding !== ENCODING_NONE) {
@@ -286,12 +326,17 @@ class Rest {
286
326
 
287
327
  stream.once('end', () => {
288
328
  if (size === 0) return finish(true, null)
289
- handleResponse(prealloc ? prealloc.slice(0, size) : (chunks.length === 1 ? chunks[0] : Buffer.concat(chunks, size)))
329
+ handleResponse(
330
+ prealloc
331
+ ? prealloc.slice(0, size)
332
+ : (chunks.length === 1 ? chunks[0] : Buffer.concat(chunks, size))
333
+ )
290
334
  })
291
335
  })
292
336
 
293
337
  req.once('error', (e) => finish(false, e))
294
338
  timer = setTimeout(() => finish(false, new Error(`Request timeout: ${this.timeout}ms`)), this.timeout)
339
+ unrefTimer(timer)
295
340
  payload ? req.end(payload) : req.end()
296
341
  })
297
342
  }
@@ -299,9 +344,9 @@ class Rest {
299
344
  _getH2Session() {
300
345
  if (!this._h2 || this._h2.closed || this._h2.destroyed) {
301
346
  this._clearH2()
302
- this._h2 = http2.connect(this.baseUrl)
303
- this._h2Timer = setTimeout(() => this._closeH2(), H2_TIMEOUT)
304
- this._h2Timer.unref()
347
+ this._h2 = http2.connect(this.baseUrl, this._tlsOptions || undefined)
348
+ this._resetH2Timer()
349
+
305
350
  const onEnd = () => this._clearH2()
306
351
  this._h2.once('error', onEnd)
307
352
  this._h2.once('close', onEnd)
@@ -310,6 +355,14 @@ class Rest {
310
355
  return this._h2
311
356
  }
312
357
 
358
+ _resetH2Timer() {
359
+ if (this._h2Timer) { clearTimeout(this._h2Timer); this._h2Timer = null }
360
+ if (this._h2 && !this._h2.closed && !this._h2.destroyed) {
361
+ this._h2Timer = setTimeout(() => this._closeH2(), H2_TIMEOUT)
362
+ unrefTimer(this._h2Timer)
363
+ }
364
+ }
365
+
313
366
  _clearH2() {
314
367
  if (this._h2Timer) { clearTimeout(this._h2Timer); this._h2Timer = null }
315
368
  this._h2 = null
@@ -317,14 +370,18 @@ class Rest {
317
370
 
318
371
  _closeH2() {
319
372
  if (this._h2Timer) { clearTimeout(this._h2Timer); this._h2Timer = null }
320
- if (this._h2) { try { this._h2.close() } catch { } this._h2 = null }
373
+ if (this._h2) {
374
+ try { this._h2.close() } catch {}
375
+ this._h2 = null
376
+ }
321
377
  }
322
378
 
323
379
  _h2Request(method, path, headers, payload) {
324
380
  const session = this._getH2Session()
325
381
 
326
382
  return new Promise((resolve, reject) => {
327
- let req, timer, done = false, chunks = [], size = 0, prealloc = null
383
+ let req, timer, done = false
384
+ let chunks = null, size = 0, prealloc = null
328
385
 
329
386
  const finish = (ok, val) => {
330
387
  if (done) return
@@ -347,12 +404,14 @@ class Rest {
347
404
  if (headers['Content-Length']) h2h['Content-Length'] = headers['Content-Length']
348
405
 
349
406
  req = session.request(h2h)
407
+ this._resetH2Timer()
350
408
 
351
409
  req.once('response', (rh) => {
352
410
  if (timer) { clearTimeout(timer); timer = null }
353
411
 
354
412
  const status = rh[':status'] || 0
355
413
  const cl = rh['content-length']
414
+ const contentType = rh['content-type'] || ''
356
415
 
357
416
  if (status === 204 || cl === '0') {
358
417
  req.resume()
@@ -365,9 +424,14 @@ class Rest {
365
424
  return finish(false, ERRORS.RESPONSE_TOO_LARGE)
366
425
  }
367
426
 
368
- if (clInt > 0 && clInt <= MAX_RESPONSE_SIZE) prealloc = Buffer.allocUnsafe(clInt)
369
-
370
427
  const encoding = _functions.getEncodingType(rh['content-encoding'])
428
+
429
+ if (encoding === ENCODING_NONE && clInt > 0 && clInt <= MAX_RESPONSE_SIZE) {
430
+ prealloc = Buffer.allocUnsafe(clInt)
431
+ } else {
432
+ chunks = []
433
+ }
434
+
371
435
  const decomp = encoding !== ENCODING_NONE ? _functions.createDecompressor(encoding) : null
372
436
  const stream = decomp ? req.pipe(decomp) : req
373
437
 
@@ -387,9 +451,13 @@ class Rest {
387
451
 
388
452
  stream.once('end', () => {
389
453
  if (size === 0) return finish(true, null)
390
- const buffer = prealloc ? prealloc.slice(0, size) : (chunks.length === 1 ? chunks[0] : Buffer.concat(chunks, size))
454
+
455
+ const buffer = prealloc
456
+ ? prealloc.slice(0, size)
457
+ : (chunks.length === 1 ? chunks[0] : Buffer.concat(chunks, size))
458
+
391
459
  try {
392
- const result = JSON.parse(buffer.toString(UTF8))
460
+ const result = _functions.parseBody(buffer, contentType, false)
393
461
  if (status >= 400) {
394
462
  finish(false, _functions.createHttpError(status, method, this.baseUrl + path, rh, result))
395
463
  } else {
@@ -402,6 +470,7 @@ class Rest {
402
470
  })
403
471
 
404
472
  timer = setTimeout(() => finish(false, new Error(`Request timeout: ${this.timeout}ms`)), this.timeout)
473
+ unrefTimer(timer)
405
474
  payload ? req.end(payload) : req.end()
406
475
  })
407
476
  }
@@ -480,22 +549,23 @@ class Rest {
480
549
  try {
481
550
  const lyrics = await this.makeRequest('GET', `${this._getSessionPath()}/players/${guildId}/track/lyrics?skipTrackSource=${skip}`)
482
551
  if (this._validLyrics(lyrics)) return lyrics
483
- } catch { }
552
+ } catch {}
484
553
  }
485
554
 
486
555
  if (hasEncoded) {
487
556
  try {
488
557
  const lyrics = await this.makeRequest('GET', `${this._endpoints.lyrics}?track=${encodeURIComponent(encoded)}&skipTrackSource=${skip}`)
489
558
  if (this._validLyrics(lyrics)) return lyrics
490
- } catch { }
559
+ } catch {}
491
560
  }
492
561
 
493
562
  if (title) {
494
- const query = track?.info?.author ? `${title} ${track.info.author}` : title
563
+ const info = track.info || {}
564
+ const query = info.author ? `${title} ${info.author}` : title
495
565
  try {
496
566
  const lyrics = await this.makeRequest('GET', `${this._endpoints.lyrics}/search?query=${encodeURIComponent(query)}`)
497
567
  if (this._validLyrics(lyrics)) return lyrics
498
- } catch { }
568
+ } catch {}
499
569
  }
500
570
 
501
571
  return null
@@ -510,7 +580,10 @@ class Rest {
510
580
 
511
581
  async subscribeLiveLyrics(guildId, skipTrackSource = false) {
512
582
  try {
513
- return await this.makeRequest('POST', `${this._getSessionPath()}/players/${guildId}/lyrics/subscribe?skipTrackSource=${skipTrackSource ? 'true' : 'false'}`) === null
583
+ return await this.makeRequest(
584
+ 'POST',
585
+ `${this._getSessionPath()}/players/${guildId}/lyrics/subscribe?skipTrackSource=${skipTrackSource ? 'true' : 'false'}`
586
+ ) === null
514
587
  } catch {
515
588
  return false
516
589
  }
@@ -538,12 +611,12 @@ class Rest {
538
611
  volume: options.volume !== undefined ? options.volume : 0.8
539
612
  }
540
613
 
541
- return await this.makeRequest("POST", `/v4/sessions/${this.sessionId}/players/${guildId}/mix`, payload)
614
+ return this.makeRequest('POST', `/v4/sessions/${this.sessionId}/players/${guildId}/mix`, payload)
542
615
  }
543
616
 
544
617
  async getActiveMixer(guildId) {
545
618
  if (!this.node.isNodelink) throw new Error('Mixer endpoints are only available on Nodelink nodes')
546
- const response = await this.makeRequest("GET", `/v4/sessions/${this.sessionId}/players/${guildId}/mix`)
619
+ const response = await this.makeRequest('GET', `/v4/sessions/${this.sessionId}/players/${guildId}/mix`)
547
620
  return response?.mixes || []
548
621
  }
549
622
 
@@ -551,17 +624,16 @@ class Rest {
551
624
  if (!this.node.isNodelink) throw new Error('Mixer endpoints are only available on Nodelink nodes')
552
625
  if (!guildId || !mix || typeof volume !== 'number') throw new Error('You forget to set the guild_id, mix or volume options')
553
626
 
554
- return await this.makeRequest("PATCH", `/v4/sessions/${this.sessionId}/players/${guildId}/mix/${mix}`, { volume })
627
+ return this.makeRequest('PATCH', `/v4/sessions/${this.sessionId}/players/${guildId}/mix/${mix}`, { volume })
555
628
  }
556
629
 
557
630
  async removeMixer(guildId, mix) {
558
631
  if (!this.node.isNodelink) throw new Error('Mixer endpoints are only available on Nodelink nodes')
559
632
  if (!guildId || !mix) throw new Error('You forget to set the guild_id and/or mix options')
560
633
 
561
- return await this.makeRequest("DELETE", `/v4/sessions/${this.sessionId}/players/${guildId}/mix/${mix}`)
634
+ return this.makeRequest('DELETE', `/v4/sessions/${this.sessionId}/players/${guildId}/mix/${mix}`)
562
635
  }
563
636
 
564
-
565
637
  destroy() {
566
638
  if (this.agent) { this.agent.destroy(); this.agent = null }
567
639
  this._closeH2()
@@ -117,8 +117,12 @@ class Track {
117
117
  }
118
118
 
119
119
  _computeArtwork() {
120
+ if (this.artworkUrl) return this.artworkUrl
120
121
  const id = this.identifier || (this.uri && YT_ID_REGEX.exec(this.uri)?.[1])
121
- return id ? `https://i.ytimg.com/vi/${id}/hqdefault.jpg` : null
122
+ if (id && this.sourceName?.includes('youtube')) {
123
+ return `https://i.ytimg.com/vi/${id}/hqdefault.jpg`
124
+ }
125
+ return null
122
126
  }
123
127
  }
124
128
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "aqualink",
3
- "version": "2.17.3",
4
- "description": "An Lavalink client, focused in pure performance and features",
3
+ "version": "2.18.0",
4
+ "description": "An Lavalink/Nodelink client, focused in pure performance and features",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
7
7
  "scripts": {
@@ -25,7 +25,7 @@
25
25
  "discord-integration",
26
26
  "high-performance",
27
27
  "scalable",
28
- "lavalink",
28
+ "nodelink",
29
29
  "api",
30
30
  "discord",
31
31
  "lavalink.js",
@@ -68,4 +68,4 @@
68
68
  "url": "https://github.com/ToddyTheNoobDud"
69
69
  }
70
70
  ]
71
- }
71
+ }