aqualink 2.17.1 → 2.17.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -180,16 +180,18 @@ const aqua = new Aqua(client, nodes, {
180
180
 
181
181
  client.aqua = aqua;
182
182
 
183
- client.once(Events.Ready, () => {
183
+ client.once(Events.ClientReady, () => {
184
184
  client.aqua.init(client.user.id);
185
185
  console.log(`Logged in as ${client.user.tag}`);
186
186
  });
187
187
 
188
- client.on(Events.Raw, (d) => {
189
- if (![Events.VoiceStateUpdate, Events.VoiceServerUpdate].includes(d.t)) return;
190
- client.aqua.updateVoiceState(d);
188
+ client.on(Events.Raw, (d, t) => {
189
+ if (d.t === "VOICE_SERVER_UPDATE" || d.t === "VOICE_STATE_UPDATE") {
190
+ return client.aqua.updateVoiceState(d, t);
191
+ }
191
192
  });
192
193
 
194
+
193
195
  client.on(Events.MessageCreate, async (message) => {
194
196
  if (message.author.bot || !message.content.startsWith("!play")) return;
195
197
 
@@ -446,4 +448,4 @@ Join our thriving community of developers and bot creators!
446
448
 
447
449
  <sub>Built with 💙 by the Aqualink Team</sub>
448
450
 
449
- </div>
451
+ </div>
package/build/index.d.ts CHANGED
@@ -38,8 +38,7 @@ declare module "aqualink" {
38
38
  _rebuildLocks: Set<string>;
39
39
  _leastUsedNodesCache: Node[] | null;
40
40
  _leastUsedNodesCacheTime: number;
41
- _nodeLoadCache: Map<string, number>;
42
- _nodeLoadCacheTime: Map<string, number>;
41
+ _nodeLoadCache: Map<string, {load: number; time: number}>;
43
42
  _cleanupTimer: NodeJS.Timer | null;
44
43
  _onNodeConnect?: (node: Node) => void;
45
44
  _onNodeDisconnect?: (node: Node) => void;
@@ -126,6 +125,7 @@ declare module "aqualink" {
126
125
  infiniteReconnects: boolean;
127
126
  connected: boolean;
128
127
  info: NodeInfo | null;
128
+ isNodelink: boolean;
129
129
  ws: any | null; // WebSocket
130
130
  reconnectAttempted: number;
131
131
  reconnectTimeoutId: NodeJS.Timeout | null;
@@ -198,6 +198,8 @@ declare module "aqualink" {
198
198
  mute: boolean;
199
199
  autoplayRetries: number;
200
200
  reconnectionRetries: number;
201
+ _resuming: boolean;
202
+ _reconnecting: boolean;
201
203
  previousIdentifiers: Set<string>;
202
204
  self_deaf: boolean;
203
205
  self_mute: boolean;
@@ -242,6 +244,11 @@ declare module "aqualink" {
242
244
  setAutoplay(enabled: boolean): Player;
243
245
  updatePlayer(data: any): Promise<any>;
244
246
  cleanup(): Promise<void>;
247
+ getActiveMixer(guildId: string): Promise<any[]>;
248
+ updateMixerVolume(guildId: string, mix: string, volume: number): Promise<any>;
249
+ removeMixer(guildId: string, mix: string): Promise<any>;
250
+ addMixer(guildId: string, options: MixerOptions): Promise<any>;
251
+ getLoadLyrics(encodedTrack: string): Promise<LyricsResponse | null>;
245
252
 
246
253
  // Data Methods
247
254
  set(key: string, value: any): void;
@@ -276,7 +283,7 @@ declare module "aqualink" {
276
283
  }
277
284
 
278
285
  export class Track {
279
- constructor(data?: TrackData, requester?: any, nodes?: Node);
286
+ constructor(data?: TrackData, requester?: any, node?: Node);
280
287
 
281
288
  // Properties
282
289
  identifier: string;
@@ -326,6 +333,7 @@ declare module "aqualink" {
326
333
  baseUrl: string;
327
334
  defaultHeaders: Record<string, string>;
328
335
  agent: any; // HTTP/HTTPS Agent
336
+ useHttp2: boolean;
329
337
 
330
338
  // Core Methods
331
339
  setSessionId(sessionId: string): void;
@@ -347,6 +355,11 @@ declare module "aqualink" {
347
355
  getRoutePlannerStatus(): Promise<any>;
348
356
  freeRoutePlannerAddress(address: string): Promise<any>;
349
357
  freeAllRoutePlannerAddresses(): Promise<any>;
358
+ addMixer(guildId: string, options: MixerOptions): Promise<any>;
359
+ getActiveMixer(guildId: string): Promise<any[]>;
360
+ updateMixerVolume(guildId: string, mix: string, volume: number): Promise<any>;
361
+ removeMixer(guildId: string, mix: string): Promise<any>;
362
+ getLoadLyrics(encodedTrack: string): Promise<LyricsResponse>;
350
363
  destroy(): void;
351
364
  }
352
365
 
@@ -452,7 +465,7 @@ declare module "aqualink" {
452
465
  updateSequence(seq: number): void;
453
466
  destroy(): void;
454
467
  attemptResume(): Promise<boolean>;
455
- resendVoiceUpdate(options?: { resume?: boolean }): void;
468
+ resendVoiceUpdate(): boolean;
456
469
 
457
470
  // Internal Methods
458
471
  _extractRegion(endpoint: string): string | null;
@@ -736,6 +749,13 @@ declare module "aqualink" {
736
749
  smoothing?: number;
737
750
  }
738
751
 
752
+ export interface MixerOptions {
753
+ identifier?: string;
754
+ encoded?: string;
755
+ userData?: any;
756
+ volume?: number;
757
+ }
758
+
739
759
  // Voice Update Interfaces
740
760
  export interface VoiceStateUpdate {
741
761
  d: {
@@ -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,
@@ -56,11 +55,17 @@ const DEFAULT_OPTIONS = Object.freeze({
56
55
  })
57
56
  })
58
57
 
59
- // Shared helper functions
60
58
  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),
59
+ delay: ms => new Promise(r => {
60
+ const t = setTimeout(r, ms)
61
+ t.unref?.()
62
+ }),
63
+ noop: () => { },
64
+ isUrl: query => {
65
+ if (typeof query !== 'string' || query.length <= 8) return false
66
+ const q = query.trimStart()
67
+ return q.startsWith('http://') || q.startsWith('https://')
68
+ },
64
69
  formatQuery(query, source) {
65
70
  return this.isUrl(query) ? query : `${source}${SEARCH_PREFIX}${query}`
66
71
  },
@@ -69,12 +74,17 @@ const _functions = {
69
74
  try {
70
75
  const result = fn()
71
76
  return result?.then ? result.catch(this.noop) : result
72
- } catch {}
77
+ } catch { }
73
78
  },
74
79
  parseRequester(str) {
75
80
  if (!str || typeof str !== 'string') return null
76
81
  const i = str.indexOf(':')
77
- return i > 0 ? {id: str.substring(0, i), username: str.substring(i + 1)} : null
82
+ return i > 0 ? { id: str.substring(0, i), username: str.substring(i + 1) } : null
83
+ },
84
+ unrefTimeout: (fn, ms) => {
85
+ const t = setTimeout(fn, ms)
86
+ t.unref?.()
87
+ return t
78
88
  }
79
89
  }
80
90
 
@@ -92,9 +102,9 @@ class Aqua extends EventEmitter {
92
102
  this.initiated = false
93
103
  this.version = pkgVersion
94
104
 
95
- const merged = {...DEFAULT_OPTIONS, ...options}
105
+ const merged = { ...DEFAULT_OPTIONS, ...options }
96
106
  this.options = merged
97
- this.failoverOptions = {...DEFAULT_OPTIONS.failoverOptions, ...options.failoverOptions}
107
+ this.failoverOptions = { ...DEFAULT_OPTIONS.failoverOptions, ...options.failoverOptions }
98
108
 
99
109
  this.shouldDeleteMessage = merged.shouldDeleteMessage
100
110
  this.defaultSearchPlatform = merged.defaultSearchPlatform
@@ -149,13 +159,13 @@ class Aqua extends EventEmitter {
149
159
  this._performCleanup()
150
160
  })
151
161
  },
152
- onNodeReady: (node, {resumed}) => {
162
+ onNodeReady: (node, { resumed }) => {
153
163
  if (!resumed) return
154
164
  const batch = []
155
165
  for (const player of this.players.values()) {
156
166
  if (player.nodes === node && player.connection) batch.push(player)
157
167
  }
158
- if (batch.length) queueMicrotask(() => batch.forEach(p => p.connection.resendVoiceUpdate({resume: true})))
168
+ if (batch.length) queueMicrotask(() => batch.forEach(p => p.connection.resendVoiceUpdate()))
159
169
  }
160
170
  }
161
171
  this.on(AqualinkEvents.NodeConnect, this._eventHandlers.onNodeConnect)
@@ -171,8 +181,10 @@ class Aqua extends EventEmitter {
171
181
  this._eventHandlers = null
172
182
  }
173
183
  this.removeAllListeners()
174
- this.nodeMap.forEach(node => this._destroyNode(node.name || node.host))
175
- this.players.forEach(player => _functions.safeCall(() => player.destroy()))
184
+
185
+ for (const id of Array.from(this.nodeMap.keys())) this._destroyNode(id)
186
+ for (const player of Array.from(this.players.values())) _functions.safeCall(() => player.destroy())
187
+
176
188
  this.players.clear()
177
189
  this._nodeStates.clear()
178
190
  this._failoverQueue.clear()
@@ -220,7 +232,7 @@ class Aqua extends EventEmitter {
220
232
  (stats.playingPlayers || 0) * 0.75 +
221
233
  (stats.memory ? stats.memory.used / reservable : 0) * 40 +
222
234
  (node.rest?.calls || 0) * 0.001
223
- this._nodeLoadCache.set(id, {load, time: now})
235
+ this._nodeLoadCache.set(id, { load, time: now })
224
236
  if (this._nodeLoadCache.size > MAX_CACHE_SIZE) {
225
237
  const first = this._nodeLoadCache.keys().next().value
226
238
  this._nodeLoadCache.delete(first)
@@ -233,7 +245,7 @@ class Aqua extends EventEmitter {
233
245
  this.clientId = clientId
234
246
  if (!this.clientId) return this
235
247
  const results = await Promise.allSettled(
236
- this.nodes.map(n => Promise.race([this._createNode(n), _functions.delay(NODE_TIMEOUT).then(() => {throw new Error('Timeout')})]))
248
+ this.nodes.map(n => Promise.race([this._createNode(n), _functions.delay(NODE_TIMEOUT).then(() => { throw new Error('Timeout') })]))
237
249
  )
238
250
  if (!results.some(r => r.status === 'fulfilled')) throw new Error('No nodes connected')
239
251
  if (this.plugins?.length) {
@@ -249,10 +261,10 @@ class Aqua extends EventEmitter {
249
261
  const node = new Node(this, options, this.options)
250
262
  node.players = new Set()
251
263
  this.nodeMap.set(id, node)
252
- this._nodeStates.set(id, {connected: false, failoverInProgress: false})
264
+ this._nodeStates.set(id, { connected: false, failoverInProgress: false })
253
265
  try {
254
266
  await node.connect()
255
- this._nodeStates.set(id, {connected: true, failoverInProgress: false})
267
+ this._nodeStates.set(id, { connected: true, failoverInProgress: false })
256
268
  this._invalidateCache()
257
269
  this.emit(AqualinkEvents.NodeCreate, node)
258
270
  return node
@@ -304,7 +316,7 @@ class Aqua extends EventEmitter {
304
316
  const now = Date.now()
305
317
  for (const [guildId, state] of this._brokenPlayers) {
306
318
  if (state.originalNodeId === id && (now - state.brokenAt) < BROKEN_PLAYER_TTL) {
307
- rebuilds.push({guildId, state})
319
+ rebuilds.push({ guildId, state })
308
320
  }
309
321
  }
310
322
  if (!rebuilds.length) return
@@ -312,7 +324,7 @@ class Aqua extends EventEmitter {
312
324
  for (let i = 0; i < rebuilds.length; i += MAX_CONCURRENT_OPS) {
313
325
  const batch = rebuilds.slice(i, i + MAX_CONCURRENT_OPS)
314
326
  const results = await Promise.allSettled(
315
- batch.map(({guildId, state}) => this._rebuildPlayer(state, node).then(() => guildId))
327
+ batch.map(({ guildId, state }) => this._rebuildPlayer(state, node).then(() => guildId))
316
328
  )
317
329
  for (const r of results) {
318
330
  if (r.status === 'fulfilled') successes.push(r.value)
@@ -324,7 +336,7 @@ class Aqua extends EventEmitter {
324
336
  }
325
337
 
326
338
  async _rebuildPlayer(state, targetNode) {
327
- const {guildId, textChannel, voiceChannel, current, volume = 65, deaf = true} = state
339
+ const { guildId, textChannel, voiceChannel, current, volume = 65, deaf = true } = state
328
340
  const lockKey = `rebuild_${guildId}`
329
341
  if (this._rebuildLocks.has(lockKey)) return
330
342
  this._rebuildLocks.add(lockKey)
@@ -333,11 +345,11 @@ class Aqua extends EventEmitter {
333
345
  await this.destroyPlayer(guildId)
334
346
  await _functions.delay(RECONNECT_DELAY)
335
347
  }
336
- const player = this.createPlayer(targetNode, {guildId, textChannel, voiceChannel, defaultVolume: volume, deaf})
348
+ const player = this.createPlayer(targetNode, { guildId, textChannel, voiceChannel, defaultVolume: volume, deaf })
337
349
  if (current && player?.queue?.add) {
338
350
  player.queue.add(current)
339
351
  await player.play()
340
- if (state.position > 0) setTimeout(() => player.seek?.(state.position), SEEK_DELAY)
352
+ if (state.position > 0) _functions.unrefTimeout(() => player.seek?.(state.position), SEEK_DELAY)
341
353
  if (state.paused) player.pause(true)
342
354
  }
343
355
  return player
@@ -357,7 +369,7 @@ class Aqua extends EventEmitter {
357
369
  const attempts = this._failoverQueue.get(id) || 0
358
370
  if (attempts >= this.failoverOptions.maxFailoverAttempts) return
359
371
 
360
- this._nodeStates.set(id, {connected: false, failoverInProgress: true})
372
+ this._nodeStates.set(id, { connected: false, failoverInProgress: true })
361
373
  this._lastFailoverAttempt.set(id, now)
362
374
  this._failoverQueue.set(id, attempts + 1)
363
375
 
@@ -379,7 +391,7 @@ class Aqua extends EventEmitter {
379
391
  } catch (error) {
380
392
  this.emit(AqualinkEvents.Error, null, error)
381
393
  } finally {
382
- this._nodeStates.set(id, {connected: false, failoverInProgress: false})
394
+ this._nodeStates.set(id, { connected: false, failoverInProgress: false })
383
395
  }
384
396
  }
385
397
 
@@ -403,7 +415,7 @@ class Aqua extends EventEmitter {
403
415
  for (let i = 0; i < players.length; i += MAX_CONCURRENT_OPS) {
404
416
  const batch = players.slice(i, i + MAX_CONCURRENT_OPS)
405
417
  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})
418
+ for (const r of batchResults) results.push({ success: r.status === 'fulfilled', error: r.reason })
407
419
  }
408
420
  return results
409
421
  }
@@ -411,7 +423,7 @@ class Aqua extends EventEmitter {
411
423
  async _migratePlayer(player, pickNode) {
412
424
  const state = this._capturePlayerState(player)
413
425
  if (!state) throw new Error('Failed to capture state')
414
- const {maxRetries, retryDelay} = this.failoverOptions
426
+ const { maxRetries, retryDelay } = this.failoverOptions
415
427
  for (let retry = 0; retry < maxRetries; retry++) {
416
428
  try {
417
429
  const targetNode = pickNode()
@@ -462,10 +474,10 @@ class Aqua extends EventEmitter {
462
474
  }
463
475
  if (state.queue?.length && newPlayer.queue?.add) newPlayer.queue.add(...state.queue)
464
476
  if (state.current && this.failoverOptions.preservePosition) {
465
- newPlayer.queue?.add?.(state.current, {toFront: true})
477
+ newPlayer.queue?.add?.(state.current, { toFront: true })
466
478
  if (this.failoverOptions.resumePlayback) {
467
479
  ops.push(newPlayer.play())
468
- if (state.position > 0) setTimeout(() => newPlayer.seek?.(state.position), SEEK_DELAY)
480
+ if (state.position > 0) _functions.unrefTimeout(() => newPlayer.seek?.(state.position), SEEK_DELAY)
469
481
  if (state.paused) ops.push(newPlayer.pause(true))
470
482
  }
471
483
  }
@@ -474,7 +486,7 @@ class Aqua extends EventEmitter {
474
486
  await Promise.allSettled(ops)
475
487
  }
476
488
 
477
- updateVoiceState({d, t}) {
489
+ updateVoiceState({ d, t }) {
478
490
  if (!d?.guild_id || (t !== 'VOICE_STATE_UPDATE' && t !== 'VOICE_SERVER_UPDATE')) return
479
491
  const player = this.players.get(d.guild_id)
480
492
  if (!player || !player.nodes?.connected) return
@@ -540,7 +552,7 @@ class Aqua extends EventEmitter {
540
552
  await _functions.safeCall(() => player.destroy())
541
553
  }
542
554
 
543
- async resolve({query, source, requester, nodes}) {
555
+ async resolve({ query, source, requester, nodes }) {
544
556
  if (!this.initiated) throw new Error('Aqua not initialized')
545
557
  const node = this._getRequestNode(nodes)
546
558
  if (!node) throw new Error('No nodes available')
@@ -581,8 +593,8 @@ class Aqua extends EventEmitter {
581
593
  }
582
594
 
583
595
  _constructResponse(response, requester, node) {
584
- const {loadType, data, pluginInfo: rootPlugin} = response || {}
585
- const base = {loadType, exception: null, playlistInfo: null, pluginInfo: rootPlugin || {}, tracks: []}
596
+ const { loadType, data, pluginInfo: rootPlugin } = response || {}
597
+ const base = { loadType, exception: null, playlistInfo: null, pluginInfo: rootPlugin || {}, tracks: [] }
586
598
  if (loadType === 'error' || loadType === 'LOAD_FAILED') {
587
599
  base.exception = data || response.exception || null
588
600
  return base
@@ -599,7 +611,7 @@ class Aqua extends EventEmitter {
599
611
  ...info
600
612
  }
601
613
  }
602
- base.pluginInfo = data.pluginInfo || base.pluginInfo
614
+ base.pluginInfo = data.pluginInfo || rootPlugin || base.pluginInfo
603
615
  base.tracks = Array.isArray(data.tracks) ? data.tracks.map(t => _functions.makeTrack(t, requester, node)) : []
604
616
  } else if (loadType === 'search') {
605
617
  base.tracks = Array.isArray(data) ? data.map(t => _functions.makeTrack(t, requester, node)) : []
@@ -616,7 +628,7 @@ class Aqua extends EventEmitter {
616
628
  async search(query, requester, source) {
617
629
  if (!query || !requester) return null
618
630
  try {
619
- const {tracks} = await this.resolve({query, source: source || this.defaultSearchPlatform, requester})
631
+ const { tracks } = await this.resolve({ query, source: source || this.defaultSearchPlatform, requester })
620
632
  return tracks || null
621
633
  } catch {
622
634
  return null
@@ -628,8 +640,8 @@ class Aqua extends EventEmitter {
628
640
  const tempFile = `${filePath}.tmp`
629
641
  let ws = null
630
642
  try {
631
- await fs.promises.writeFile(lockFile, String(process.pid), {flag: 'wx'})
632
- ws = fs.createWriteStream(tempFile, {encoding: 'utf8', flags: 'w'})
643
+ await fs.promises.writeFile(lockFile, String(process.pid), { flag: 'wx' })
644
+ ws = fs.createWriteStream(tempFile, { encoding: 'utf8', flags: 'w' })
633
645
  const buffer = []
634
646
  let drainPromise = Promise.resolve()
635
647
 
@@ -680,11 +692,11 @@ class Aqua extends EventEmitter {
680
692
  let stream = null, rl = null
681
693
  try {
682
694
  await fs.promises.access(filePath)
683
- await fs.promises.writeFile(lockFile, String(process.pid), {flag: 'wx'})
695
+ await fs.promises.writeFile(lockFile, String(process.pid), { flag: 'wx' })
684
696
  await this._waitForFirstNode()
685
697
 
686
- stream = fs.createReadStream(filePath, {encoding: 'utf8'})
687
- rl = readline.createInterface({input: stream, crlfDelay: Infinity})
698
+ stream = fs.createReadStream(filePath, { encoding: 'utf8' })
699
+ rl = readline.createInterface({ input: stream, crlfDelay: Infinity })
688
700
 
689
701
  const batch = []
690
702
  for await (const line of rl) {
@@ -719,7 +731,7 @@ class Aqua extends EventEmitter {
719
731
  player._resuming = !!p.resuming
720
732
  const requester = _functions.parseRequester(p.r)
721
733
  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)))
734
+ const resolved = await Promise.all(tracksToResolve.map(uri => this.resolve({ query: uri, requester }).catch(() => null)))
723
735
  const validTracks = resolved.flatMap(r => r?.tracks || [])
724
736
  if (validTracks.length && player.queue?.add) {
725
737
  if (player.queue.length <= 2) player.queue.length = 0
@@ -731,14 +743,14 @@ class Aqua extends EventEmitter {
731
743
  else player.volume = p.vol
732
744
  }
733
745
  await player.play()
734
- if (p.p > 0) setTimeout(() => player.seek?.(p.p), SEEK_DELAY)
746
+ if (p.p > 0) _functions.unrefTimeout(() => player.seek?.(p.p), SEEK_DELAY)
735
747
  if (p.pa) await player.pause(true)
736
748
  }
737
749
  if (p.nw && p.t) {
738
750
  const channel = this.client.channels?.cache?.get(p.t)
739
751
  if (channel?.messages) player.nowPlayingMessage = await channel.messages.fetch(p.nw).catch(() => null)
740
752
  }
741
- } catch {}
753
+ } catch { }
742
754
  }
743
755
 
744
756
  async _waitForFirstNode(timeout = NODE_TIMEOUT) {
@@ -756,6 +768,7 @@ class Aqua extends EventEmitter {
756
768
  if (this.leastUsedNodes.length) { cleanup(); resolve() }
757
769
  }
758
770
  const timer = setTimeout(() => { cleanup(); reject(new Error('Timeout waiting for first node')) }, timeout)
771
+ timer.unref?.()
759
772
  this.on(AqualinkEvents.NodeConnect, onReady)
760
773
  this.on(AqualinkEvents.NodeCreate, onReady)
761
774
  onReady()
@@ -37,7 +37,9 @@ const AqualinkEvents = {
37
37
  PlayerConnected: 'playerConnected',
38
38
  PlayerDestroyed: 'playerDestroy',
39
39
  PlayerMigrated: 'playerMigrated',
40
- PauseEvent: 'pauseEvent'
40
+ PauseEvent: 'pauseEvent',
41
+ MixStarted: 'mixStarted',
42
+ MixEnded: 'mixEnded'
41
43
  };
42
44
 
43
- module.exports = { AqualinkEvents };
45
+ module.exports = { AqualinkEvents };
@@ -4,11 +4,17 @@ const { AqualinkEvents } = require('./AqualinkEvents')
4
4
 
5
5
  const POOL_SIZE = 12
6
6
  const UPDATE_TIMEOUT = 4000
7
+
7
8
  const RECONNECT_DELAY = 1000
8
9
  const MAX_RECONNECT_ATTEMPTS = 3
9
10
  const RESUME_BACKOFF_MAX = 8000
11
+
10
12
  const VOICE_DATA_TIMEOUT = 30000
11
13
 
14
+ const VOICE_FLUSH_DELAY = 50
15
+
16
+ const NULL_CHANNEL_GRACE_MS = 1500
17
+
12
18
  const STATE = {
13
19
  CONNECTED: 1,
14
20
  UPDATE_SCHEDULED: 64,
@@ -53,6 +59,7 @@ const _functions = {
53
59
  v.token = conn.token
54
60
  v.endpoint = conn.endpoint
55
61
  v.sessionId = conn.sessionId
62
+ v.channelId = player.voiceChannel
56
63
  v.resume = resume ? true : undefined
57
64
  v.sequence = resume ? conn.sequence : undefined
58
65
  payload.data.volume = player?.volume ?? 100
@@ -110,19 +117,28 @@ class Connection {
110
117
 
111
118
  this.voiceChannel = player.voiceChannel
112
119
  this.sessionId = null
120
+ this.channelId = null
113
121
  this.endpoint = null
114
122
  this.token = null
115
123
  this.region = null
116
124
  this.sequence = 0
117
125
 
118
126
  this._lastEndpoint = null
119
- this._pendingUpdate = null
120
127
  this._stateFlags = 0
121
128
  this._reconnectAttempts = 0
122
129
  this._destroyed = false
123
130
  this._reconnectTimer = null
124
131
  this._lastVoiceDataUpdate = 0
125
132
  this._consecutiveFailures = 0
133
+
134
+ this._voiceFlushTimer = null
135
+ this._pendingUpdate = null
136
+ this._lastSentVoiceKey = ''
137
+
138
+ this._nullChannelTimer = null
139
+
140
+ this._lastStateReqAt = 0
141
+ this._stateGeneration = 0
126
142
  }
127
143
 
128
144
  _hasValidVoiceData() {
@@ -134,10 +150,17 @@ class Connection {
134
150
  return true
135
151
  }
136
152
 
137
- _canAttemptResume() {
138
- if (this._destroyed || this._reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) return false
153
+ _clearNullChannelTimer() {
154
+ if (!this._nullChannelTimer) return
155
+ clearTimeout(this._nullChannelTimer)
156
+ this._nullChannelTimer = null
157
+ }
158
+
159
+ _canAttemptResumeCore() {
160
+ if (this._destroyed) return false
161
+ if (this._reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) return false
139
162
  if (this._stateFlags & (STATE.ATTEMPTING_RESUME | STATE.DISCONNECTING)) return false
140
- return this._hasValidVoiceData() || !!this._player?._resuming
163
+ return true
141
164
  }
142
165
 
143
166
  _setReconnectTimer(delay) {
@@ -148,12 +171,15 @@ class Connection {
148
171
  }
149
172
 
150
173
  setServerUpdate(data) {
151
- if (this._destroyed || !data?.endpoint || !data.token) return
174
+ if (this._destroyed || !data?.token) return
152
175
 
153
176
  const endpoint = typeof data.endpoint === 'string' ? data.endpoint.trim() : ''
154
- if (!endpoint || typeof data.token !== 'string' || !data.token) return
177
+ if (!endpoint) return
178
+
155
179
  if (this._lastEndpoint === endpoint && this.token === data.token) return
156
180
 
181
+ this._stateGeneration++
182
+
157
183
  if (this._lastEndpoint !== endpoint) {
158
184
  this.sequence = 0
159
185
  this._lastEndpoint = endpoint
@@ -164,10 +190,11 @@ class Connection {
164
190
  this.endpoint = endpoint
165
191
  this.region = _functions.extractRegion(endpoint)
166
192
  this.token = data.token
193
+ this.channelId = data.channel_id || this.channelId || this.voiceChannel
167
194
  this._lastVoiceDataUpdate = Date.now()
168
195
  this._stateFlags &= ~STATE.VOICE_DATA_STALE
169
196
 
170
- if (this._player.paused) this._player.pause(false)
197
+ if (this._player?.paused) this._player.pause(false)
171
198
  this._scheduleVoiceUpdate()
172
199
  }
173
200
 
@@ -181,15 +208,38 @@ class Connection {
181
208
  if (this._destroyed || !data || data.user_id !== this._clientId) return
182
209
 
183
210
  const { session_id: sessionId, channel_id: channelId, self_deaf: selfDeaf, self_mute: selfMute } = data
211
+ const p = this._player
184
212
 
185
- if (!channelId) return this._handleDisconnect()
213
+ if (channelId) this._clearNullChannelTimer()
214
+
215
+ const reqCh = p?._voiceRequestChannel
216
+ const reqFresh = !!(reqCh && (Date.now() - (p._voiceRequestAt || 0)) < 5000)
217
+
218
+ if (!channelId) {
219
+ if (reqFresh) return
220
+
221
+ if (!this._nullChannelTimer) {
222
+ this._nullChannelTimer = setTimeout(() => {
223
+ this._nullChannelTimer = null
224
+ this._handleDisconnect()
225
+ }, NULL_CHANNEL_GRACE_MS)
226
+ _functions.safeUnref(this._nullChannelTimer)
227
+ }
228
+ return
229
+ }
230
+
231
+ if (reqFresh && channelId !== reqCh) return
232
+
233
+ if (reqCh && channelId === reqCh) {
234
+ p._voiceRequestChannel = null
235
+ }
186
236
 
187
237
  let needsUpdate = false
188
238
 
189
239
  if (this.voiceChannel !== channelId) {
190
240
  this._aqua.emit(AqualinkEvents.PlayerMove, this.voiceChannel, channelId)
191
241
  this.voiceChannel = channelId
192
- this._player.voiceChannel = channelId
242
+ p.voiceChannel = channelId
193
243
  needsUpdate = true
194
244
  }
195
245
 
@@ -202,30 +252,29 @@ class Connection {
202
252
  needsUpdate = true
203
253
  }
204
254
 
205
- this._player.connection.sessionId = sessionId || this._player.connection.sessionId
206
- this._player.self_deaf = this._player.selfDeaf = !!selfDeaf
207
- this._player.self_mute = this._player.selfMute = !!selfMute
208
- this._player.connected = true
255
+ p.self_deaf = p.selfDeaf = !!selfDeaf
256
+ p.self_mute = p.selfMute = !!selfMute
209
257
  this._stateFlags |= STATE.CONNECTED
210
258
 
211
259
  if (needsUpdate) this._scheduleVoiceUpdate()
212
260
  }
213
261
 
214
262
  _handleDisconnect() {
215
- if (this._destroyed || !(this._stateFlags & STATE.CONNECTED)) return
263
+ if (this._destroyed) return
216
264
 
217
265
  this._stateFlags = (this._stateFlags | STATE.DISCONNECTING) & ~STATE.CONNECTED
218
- this._player.connected = false
266
+ this._clearNullChannelTimer()
219
267
  this._clearPendingUpdate()
220
268
  this._clearReconnectTimer()
221
269
 
222
- this.voiceChannel = this.sessionId = null
270
+ this.voiceChannel = null
271
+ this.sessionId = null
223
272
  this.sequence = 0
224
273
  this._lastVoiceDataUpdate = 0
225
274
  this._stateFlags |= STATE.VOICE_DATA_STALE
226
275
 
227
276
  try {
228
- this._player.destroy?.()
277
+ this._player?.destroy?.()
229
278
  } catch (e) {
230
279
  this._aqua.emit(AqualinkEvents.Debug, new Error(`Player destroy failed: ${e?.message || e}`))
231
280
  } finally {
@@ -235,6 +284,10 @@ class Connection {
235
284
 
236
285
  _requestVoiceState() {
237
286
  try {
287
+ const now = Date.now()
288
+ if (now - (this._lastStateReqAt || 0) < 1500) return false
289
+ this._lastStateReqAt = now
290
+
238
291
  if (typeof this._player?.send !== 'function' || !this._player.voiceChannel) return false
239
292
  this._player.send({
240
293
  guild_id: this._guildId,
@@ -242,7 +295,6 @@ class Connection {
242
295
  self_deaf: this._player.deaf,
243
296
  self_mute: this._player.mute
244
297
  })
245
- this._setReconnectTimer(1500)
246
298
  return true
247
299
  } catch {
248
300
  return false
@@ -250,47 +302,38 @@ class Connection {
250
302
  }
251
303
 
252
304
  async attemptResume() {
253
- if (!this._canAttemptResume()) {
254
- this._aqua.emit(
255
- AqualinkEvents.Debug,
256
- `Resume blocked: destroyed=${this._destroyed}, hasValidData=${this._hasValidVoiceData()}, attempts=${this._reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS}`
257
- )
258
-
259
- const isResuming = this._player?._resuming
260
- const isStale = this._stateFlags & STATE.VOICE_DATA_STALE
261
- const needsVoiceData = !this.sessionId || !this.endpoint || !this.token
262
-
263
- if ((isStale || needsVoiceData) && isResuming) {
264
- this._aqua.emit(AqualinkEvents.Debug, `Requesting fresh voice state for guild ${this._guildId}`)
265
- this._requestVoiceState()
266
- } else if (this._reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
267
- this._handleDisconnect()
268
- }
269
- return false
270
- }
305
+ if (!this._canAttemptResumeCore()) return false
306
+
307
+ const currentGen = this._stateGeneration
271
308
 
272
- if ((!this.sessionId || !this.endpoint || !this.token) && this._player?._resuming) {
273
- this._aqua.emit(AqualinkEvents.Debug, `Resuming player but voice data missing for guild ${this._guildId}`)
309
+
310
+ if (!this.sessionId || !this.endpoint || !this.token || (this._stateFlags & STATE.VOICE_DATA_STALE)) {
311
+ this._aqua.emit(AqualinkEvents.Debug, `Resume blocked: missing voice data for guild ${this._guildId}, requesting voice state`)
274
312
  this._requestVoiceState()
275
313
  return false
276
314
  }
277
315
 
278
316
  this._stateFlags |= STATE.ATTEMPTING_RESUME
279
317
  this._reconnectAttempts++
280
- this._aqua.emit(
281
- AqualinkEvents.Debug,
282
- `Attempt resume: guild=${this._guildId} endpoint=${this.endpoint} session=${this.sessionId}`
283
- )
318
+ this._aqua.emit(AqualinkEvents.Debug, `Attempt resume: guild=${this._guildId} endpoint=${this.endpoint} session=${this.sessionId}`)
284
319
 
285
320
  const payload = sharedPool.acquire()
286
321
  try {
287
322
  _functions.fillVoicePayload(payload, this._guildId, this, this._player, true)
323
+
324
+ if (this._stateGeneration !== currentGen) {
325
+ this._aqua.emit(AqualinkEvents.Debug, `Resume aborted: State changed during attempt for guild ${this._guildId}`)
326
+ return false
327
+ }
328
+
329
+
288
330
  await this._sendUpdate(payload)
289
331
 
290
332
  this._reconnectAttempts = 0
291
333
  this._consecutiveFailures = 0
292
334
  if (this._player) this._player._resuming = false
293
- this._aqua.emit(AqualinkEvents.Debug, `Resume successful for guild ${this._guildId}`)
335
+
336
+ this._aqua.emit(AqualinkEvents.Debug, `Resume PATCH sent for guild ${this._guildId}`)
294
337
  return true
295
338
  } catch (e) {
296
339
  this._consecutiveFailures++
@@ -300,7 +343,7 @@ class Connection {
300
343
  const delay = Math.min(RECONNECT_DELAY * (1 << (this._reconnectAttempts - 1)), RESUME_BACKOFF_MAX)
301
344
  this._setReconnectTimer(delay)
302
345
  } else {
303
- this._aqua.emit(AqualinkEvents.Debug, `Max reconnect attempts or failures reached for guild ${this._guildId}`)
346
+ this._aqua.emit(AqualinkEvents.Debug, `Max reconnect attempts/failures reached for guild ${this._guildId}`)
304
347
  if (this._player) this._player._resuming = false
305
348
  this._handleDisconnect()
306
349
  }
@@ -330,36 +373,64 @@ class Connection {
330
373
  this._stateFlags &= ~STATE.UPDATE_SCHEDULED
331
374
  if (this._pendingUpdate?.payload) sharedPool.release(this._pendingUpdate.payload)
332
375
  this._pendingUpdate = null
376
+ if (this._voiceFlushTimer) {
377
+ clearTimeout(this._voiceFlushTimer)
378
+ this._voiceFlushTimer = null
379
+ }
333
380
  }
334
381
 
335
- _scheduleVoiceUpdate() {
336
- if (this._destroyed || !this._hasValidVoiceData() || (this._stateFlags & STATE.UPDATE_SCHEDULED)) return
337
-
338
- this._clearPendingUpdate()
382
+ _makeVoiceKey() {
383
+ const p = this._player
384
+ const vol = p?.volume ?? 100
385
+ return (this.sessionId || '') + '|' +
386
+ (this.token || '') + '|' +
387
+ (this.endpoint || '') + '|' +
388
+ (p?.voiceChannel || '') + '|' +
389
+ vol
390
+ }
339
391
 
340
- const payload = sharedPool.acquire()
341
- _functions.fillVoicePayload(payload, this._guildId, this, this._player, false)
392
+ _scheduleVoiceUpdate() {
393
+ if (this._destroyed) return
394
+ if (!this._hasValidVoiceData()) return
395
+
396
+ if (!this._pendingUpdate) {
397
+ const payload = sharedPool.acquire()
398
+ _functions.fillVoicePayload(payload, this._guildId, this, this._player, false)
399
+ this._pendingUpdate = { payload, timestamp: Date.now() }
400
+ } else {
401
+ this._pendingUpdate.timestamp = Date.now()
402
+ _functions.fillVoicePayload(this._pendingUpdate.payload, this._guildId, this, this._player, false)
403
+ }
342
404
 
343
- this._pendingUpdate = { payload, timestamp: Date.now() }
405
+ if (this._stateFlags & STATE.UPDATE_SCHEDULED) return
344
406
  this._stateFlags |= STATE.UPDATE_SCHEDULED
345
- queueMicrotask(() => this._executeVoiceUpdate())
407
+
408
+ this._voiceFlushTimer = setTimeout(() => this._executeVoiceUpdate(), VOICE_FLUSH_DELAY)
409
+ _functions.safeUnref(this._voiceFlushTimer)
346
410
  }
347
411
 
348
412
  _executeVoiceUpdate() {
349
413
  if (this._destroyed) return
350
414
  this._stateFlags &= ~STATE.UPDATE_SCHEDULED
415
+ this._voiceFlushTimer = null
351
416
 
352
417
  const pending = this._pendingUpdate
353
- if (!pending) return
354
418
  this._pendingUpdate = null
355
419
 
420
+ if (!pending) return
356
421
  if (Date.now() - pending.timestamp > UPDATE_TIMEOUT) {
357
422
  sharedPool.release(pending.payload)
358
423
  return
359
424
  }
360
425
 
361
- const payload = pending.payload
362
- this._sendUpdate(payload).finally(() => sharedPool.release(payload))
426
+ const key = this._makeVoiceKey()
427
+ if (key === this._lastSentVoiceKey) {
428
+ sharedPool.release(pending.payload)
429
+ return
430
+ }
431
+ this._lastSentVoiceKey = key
432
+
433
+ this._sendUpdate(pending.payload).finally(() => sharedPool.release(pending.payload))
363
434
  }
364
435
 
365
436
  async _sendUpdate(payload) {
@@ -380,6 +451,7 @@ class Connection {
380
451
  if (this._destroyed) return
381
452
  this._destroyed = true
382
453
 
454
+ this._clearNullChannelTimer()
383
455
  this._clearPendingUpdate()
384
456
  this._clearReconnectTimer()
385
457
 
@@ -456,4 +456,4 @@ class Node {
456
456
  }
457
457
  }
458
458
 
459
- module.exports = Node
459
+ module.exports = Node
@@ -22,10 +22,12 @@ const EVENT_HANDLERS = Object.freeze({
22
22
  FiltersChangedEvent: 'filtersChanged',
23
23
  SeekEvent: 'seekEvent',
24
24
  PlayerCreatedEvent: 'playerCreated',
25
- pauseEvent: 'PauseEvent',
25
+ PauseEvent: 'pauseEvent',
26
26
  PlayerConnectedEvent: 'playerConnected',
27
27
  PlayerDestroyedEvent: 'playerDestroyed',
28
- LyricsNotFoundEvent: 'lyricsNotFound'
28
+ LyricsNotFoundEvent: 'lyricsNotFound',
29
+ MixStartedEvent: 'mixStarted',
30
+ MixEndedEvent: 'mixEnded'
29
31
  })
30
32
 
31
33
  const WATCHDOG_INTERVAL = 15000
@@ -191,6 +193,9 @@ class Player extends EventEmitter {
191
193
  this.previousTracks = new CircularBuffer(PREVIOUS_TRACKS_SIZE)
192
194
  this._updateBatcher = batcherPool.acquire(this)
193
195
 
196
+ this._voiceRequestAt = 0
197
+ this._voiceRequestChannel = null
198
+ this._suppressResumeUntil = 0
194
199
  this._bindEvents()
195
200
  this._startWatchdog()
196
201
  }
@@ -239,6 +244,7 @@ class Player extends EventEmitter {
239
244
  this._voiceDownSince = Date.now()
240
245
  this._createTimer(() => {
241
246
  if (this.connected || this.destroyed || this.nodes?.info?.isNodelink) return
247
+ if (Date.now() < (this._suppressResumeUntil || 0)) return
242
248
  this.connection.attemptResume()
243
249
  }, 1000)
244
250
  }
@@ -335,13 +341,24 @@ class Player extends EventEmitter {
335
341
 
336
342
  connect(options = {}) {
337
343
  if (this.destroyed) throw new Error('Cannot connect destroyed player')
344
+
338
345
  const voiceChannel = _functions.toId(options.voiceChannel || this.voiceChannel)
339
346
  if (!voiceChannel) throw new TypeError('Voice channel required')
347
+
340
348
  this.deaf = options.deaf !== undefined ? !!options.deaf : true
341
349
  this.mute = !!options.mute
342
350
  this.destroyed = false
351
+
352
+ this._voiceRequestAt = Date.now()
353
+ this._voiceRequestChannel = voiceChannel
354
+
343
355
  this.voiceChannel = voiceChannel
344
- this.send({ guild_id: this.guildId, channel_id: voiceChannel, self_deaf: this.deaf, self_mute: this.mute })
356
+ this.send({
357
+ guild_id: this.guildId,
358
+ channel_id: voiceChannel,
359
+ self_deaf: this.deaf,
360
+ self_mute: this.mute
361
+ })
345
362
  return this
346
363
  }
347
364
 
@@ -430,6 +447,9 @@ class Player extends EventEmitter {
430
447
  this._dataStore = null
431
448
 
432
449
  if (this.current?.dispose && !this.aqua?.options?.autoResume) this.current.dispose()
450
+ if (this.connection) {
451
+ try { this.connection.destroy() } catch { }
452
+ }
433
453
  this.connection = this.filters = this.current = this.autoplaySeed = null
434
454
 
435
455
  if (!skipRemote) {
@@ -460,11 +480,54 @@ class Player extends EventEmitter {
460
480
  return this
461
481
  }
462
482
 
483
+ async getActiveMixer(guildId) {
484
+ if (this.destroyed) return null
485
+ return await this.nodes.rest.getActiveMixer(guildId)
486
+ }
487
+
488
+ async updateMixerVolume(guildId, mix, volume) {
489
+ if (this.destroyed) return null
490
+ return await this.nodes.rest.updateMixerVolume(guildId, mix, volume)
491
+ }
492
+
493
+ async removeMixer(guildId, mix) {
494
+ if (this.destroyed) return null
495
+ return await this.nodes.rest.removeMixer(guildId, mix)
496
+ }
497
+
498
+ async addMixer(guildId, options) {
499
+ if (this.destroyed) return null
500
+
501
+ if (options.identifier && !options.encoded) {
502
+ try {
503
+ const resolved = await this.aqua.resolve({
504
+ query: options.identifier,
505
+ requester: options.requester || this.current?.requester
506
+ })
507
+
508
+ if (resolved?.tracks?.[0]) {
509
+ const track = resolved.tracks[0]
510
+ options = {
511
+ ...options,
512
+ encoded: track.track || track.encoded,
513
+ userData: options.userData
514
+ }
515
+ } else {
516
+ throw new Error('Failed to resolve track identifier')
517
+ }
518
+ } catch (error) {
519
+ throw new Error(`Failed to resolve track: ${error.message}`)
520
+ }
521
+ }
522
+
523
+ return await this.nodes.rest.addMixer(guildId, options)
524
+ }
525
+
463
526
  stop() {
464
527
  if (this.destroyed || !this.playing) return this
465
528
  this.playing = this.paused = false
466
529
  this.position = 0
467
- this.batchUpdatePlayer({ guildId: this.guildId, track: { encoded: null } }, true).catch(() => { })
530
+ this.batchUpdatePlayer({ guildId: this.guildId, track: { encoded: null, paused: this.paused } }, true).catch(() => { })
468
531
  return this
469
532
  }
470
533
 
@@ -684,7 +747,9 @@ class Player extends EventEmitter {
684
747
  playerCreated(p, t, payload) { _functions.emitIfActive(this, AqualinkEvents.PlayerCreated, payload) }
685
748
  playerConnected(p, t, payload) { _functions.emitIfActive(this, AqualinkEvents.PlayerConnected, payload) }
686
749
  playerDestroyed(p, t, payload) { _functions.emitIfActive(this, AqualinkEvents.PlayerDestroyed, payload) }
687
- PauseEvent(p, t, payload) { _functions.emitIfActive(this, AqualinkEvents.PauseEvent, payload) }
750
+ pauseEvent(p, t, payload) { _functions.emitIfActive(this, AqualinkEvents.PauseEvent, payload) }
751
+ mixStarted(p, t, payload) { _functions.emitIfActive(this, AqualinkEvents.MixStarted, t, payload) }
752
+ mixEnded(p, t, payload) { _functions.emitIfActive(this, AqualinkEvents.MixEnded, t, payload) }
688
753
 
689
754
  async _attemptVoiceResume() {
690
755
  if (!this.connection?.sessionId) throw new Error('No session')
@@ -710,14 +775,20 @@ class Player extends EventEmitter {
710
775
  if (this.destroyed) return
711
776
  const code = payload?.code
712
777
 
713
- if (code === 4022) {
778
+ if (code === 4014 || code === 4022) {
714
779
  this.aqua.emit(AqualinkEvents.SocketClosed, this, payload)
715
- this.destroy()
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
+ }
716
787
  return
717
788
  }
718
789
 
719
790
  if (code === 4015 && !this.nodes?.info?.isNodelink) {
720
- try { await this._attemptVoiceResume(); return } catch { }
791
+ try { await this._attemptVoiceResume(); return } catch { /* ignore */ }
721
792
  }
722
793
 
723
794
  if (![4015, 4009, 4006].includes(code)) {
@@ -804,8 +875,6 @@ class Player extends EventEmitter {
804
875
  _handleAquaPlayerMove(oldChannel, newChannel) {
805
876
  if (_functions.toId(oldChannel) !== _functions.toId(this.voiceChannel)) return
806
877
  this.voiceChannel = _functions.toId(newChannel)
807
- this.connected = !!newChannel
808
- this.send({ guild_id: this.guildId, channel_id: this.voiceChannel, self_deaf: this.deaf, self_mute: this.mute })
809
878
  }
810
879
 
811
880
  send(data) {
@@ -845,4 +914,4 @@ class Player extends EventEmitter {
845
914
  }
846
915
  }
847
916
 
848
- module.exports = Player
917
+ module.exports = Player
@@ -317,7 +317,7 @@ class Rest {
317
317
 
318
318
  _closeH2() {
319
319
  if (this._h2Timer) { clearTimeout(this._h2Timer); this._h2Timer = null }
320
- if (this._h2) { try { this._h2.close() } catch {} this._h2 = null }
320
+ if (this._h2) { try { this._h2.close() } catch { } this._h2 = null }
321
321
  }
322
322
 
323
323
  _h2Request(method, path, headers, payload) {
@@ -480,14 +480,14 @@ class Rest {
480
480
  try {
481
481
  const lyrics = await this.makeRequest('GET', `${this._getSessionPath()}/players/${guildId}/track/lyrics?skipTrackSource=${skip}`)
482
482
  if (this._validLyrics(lyrics)) return lyrics
483
- } catch {}
483
+ } catch { }
484
484
  }
485
485
 
486
486
  if (hasEncoded) {
487
487
  try {
488
488
  const lyrics = await this.makeRequest('GET', `${this._endpoints.lyrics}?track=${encodeURIComponent(encoded)}&skipTrackSource=${skip}`)
489
489
  if (this._validLyrics(lyrics)) return lyrics
490
- } catch {}
490
+ } catch { }
491
491
  }
492
492
 
493
493
  if (title) {
@@ -495,7 +495,7 @@ class Rest {
495
495
  try {
496
496
  const lyrics = await this.makeRequest('GET', `${this._endpoints.lyrics}/search?query=${encodeURIComponent(query)}`)
497
497
  if (this._validLyrics(lyrics)) return lyrics
498
- } catch {}
498
+ } catch { }
499
499
  }
500
500
 
501
501
  return null
@@ -524,6 +524,44 @@ class Rest {
524
524
  }
525
525
  }
526
526
 
527
+ async addMixer(guildId, options) {
528
+ if (!this.node.isNodelink) throw new Error('Mixer endpoints are only available on Nodelink nodes')
529
+ if (!options?.encoded && !options?.identifier) throw new Error('You must provide either encoded or identifier')
530
+
531
+ const track = {}
532
+ if (options.encoded) track.encoded = options.encoded
533
+ if (options.identifier) track.identifier = options.identifier
534
+ if (options.userData) track.userData = options.userData
535
+
536
+ const payload = {
537
+ track,
538
+ volume: options.volume !== undefined ? options.volume : 0.8
539
+ }
540
+
541
+ return await this.makeRequest("POST", `/v4/sessions/${this.sessionId}/players/${guildId}/mix`, payload)
542
+ }
543
+
544
+ async getActiveMixer(guildId) {
545
+ 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`)
547
+ return response?.mixes || []
548
+ }
549
+
550
+ async updateMixerVolume(guildId, mix, volume) {
551
+ if (!this.node.isNodelink) throw new Error('Mixer endpoints are only available on Nodelink nodes')
552
+ if (!guildId || !mix || typeof volume !== 'number') throw new Error('You forget to set the guild_id, mix or volume options')
553
+
554
+ return await this.makeRequest("PATCH", `/v4/sessions/${this.sessionId}/players/${guildId}/mix/${mix}`, { volume })
555
+ }
556
+
557
+ async removeMixer(guildId, mix) {
558
+ if (!this.node.isNodelink) throw new Error('Mixer endpoints are only available on Nodelink nodes')
559
+ if (!guildId || !mix) throw new Error('You forget to set the guild_id and/or mix options')
560
+
561
+ return await this.makeRequest("DELETE", `/v4/sessions/${this.sessionId}/players/${guildId}/mix/${mix}`)
562
+ }
563
+
564
+
527
565
  destroy() {
528
566
  if (this.agent) { this.agent.destroy(); this.agent = null }
529
567
  this._closeH2()
@@ -22,6 +22,7 @@ class Track {
22
22
  this.uri = _h.str(info.uri)
23
23
  this.sourceName = _h.str(info.sourceName)
24
24
  this.artworkUrl = _h.str(info.artworkUrl)
25
+ this.pluginInfo = info.pluginInfo || data.pluginInfo || {}
25
26
 
26
27
  this.playlist = data.playlist || null
27
28
  this.node = node || data.node || null
@@ -92,6 +93,7 @@ class Track {
92
93
  this.uri = fi.uri ?? this.uri
93
94
  this.sourceName = fi.sourceName ?? this.sourceName
94
95
  this.artworkUrl = fi.artworkUrl ?? this.artworkUrl
96
+ this.pluginInfo = fi.pluginInfo ?? found.pluginInfo ?? this.pluginInfo
95
97
  this.isSeekable = fi.isSeekable ?? this.isSeekable
96
98
  this.isStream = fi.isStream ?? this.isStream
97
99
  this.position = _h.num(fi.position, this.position)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aqualink",
3
- "version": "2.17.1",
3
+ "version": "2.17.3",
4
4
  "description": "An Lavalink client, focused in pure performance and features",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
@@ -45,7 +45,7 @@
45
45
  },
46
46
  "homepage": "https://aqualink-6006388d.mintlify.app/",
47
47
  "dependencies": {
48
- "ws": "^8.18.3",
48
+ "ws": "^8.19.0",
49
49
  "tseep": "^1.3.1"
50
50
  },
51
51
  "contributors": [