aqualink 2.17.0 → 2.17.2

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: {
@@ -529,7 +529,7 @@ class Aqua extends EventEmitter {
529
529
  _handlePlayerDestroy(player) {
530
530
  player.nodes?.players?.delete?.(player)
531
531
  if (this.players.get(player.guildId) === player) this.players.delete(player.guildId)
532
- this.emit(AqualinkEvents.PlayerDestroy, player)
532
+ this.emit(AqualinkEvents.PlayerDestroyed, player)
533
533
  }
534
534
 
535
535
  async destroyPlayer(guildId) {
@@ -29,16 +29,17 @@ const AqualinkEvents = {
29
29
  Debug: 'debug',
30
30
  Error: 'error',
31
31
  PlayerCreate: 'playerCreate',
32
- PlayerDestroy: 'playerDestroy',
33
32
  PlayersRebuilt: 'playersRebuilt',
34
33
  VolumeChanged: 'volumeChanged',
35
34
  FiltersChanged: 'filtersChanged',
36
35
  Seek: 'seek',
37
36
  PlayerCreated: 'playerCreated',
38
37
  PlayerConnected: 'playerConnected',
39
- PlayerDestroyed: 'playerDestroyed',
38
+ PlayerDestroyed: 'playerDestroy',
40
39
  PlayerMigrated: 'playerMigrated',
41
- PauseEvent: 'pauseEvent'
40
+ PauseEvent: 'pauseEvent',
41
+ MixStarted: 'mixStarted',
42
+ MixEnded: 'mixEnded'
42
43
  };
43
44
 
44
- module.exports = { AqualinkEvents };
45
+ module.exports = { AqualinkEvents };
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const {AqualinkEvents} = require('./AqualinkEvents')
3
+ const { AqualinkEvents } = require('./AqualinkEvents')
4
4
 
5
5
  const POOL_SIZE = 12
6
6
  const UPDATE_TIMEOUT = 4000
@@ -17,15 +17,46 @@ const STATE = {
17
17
  VOICE_DATA_STALE: 512
18
18
  }
19
19
 
20
- const ENDPOINT_REGION_REGEX = /^([a-z-]+)\d*/i
21
-
22
20
  const _functions = {
23
- safeUnref: t => t?.unref?.(),
21
+ safeUnref: t => (typeof t?.unref === 'function' ? t.unref() : undefined),
24
22
  isValidNumber: n => typeof n === 'number' && n >= 0 && Number.isFinite(n),
25
- isNetworkError: e => e && (e.code === 'ECONNREFUSED' || e.code === 'ENOTFOUND' || e.code === 'ETIMEDOUT'),
23
+ isNetworkError: e => !!e && (e.code === 'ECONNREFUSED' || e.code === 'ENOTFOUND' || e.code === 'ETIMEDOUT'),
26
24
  extractRegion: endpoint => {
27
- const m = ENDPOINT_REGION_REGEX.exec(endpoint)
28
- return m ? m[1] : 'unknown'
25
+ if (typeof endpoint !== 'string') return 'unknown'
26
+ endpoint = endpoint.trim()
27
+ if (!endpoint) return 'unknown'
28
+
29
+ const proto = endpoint.indexOf('://')
30
+ if (proto !== -1) endpoint = endpoint.slice(proto + 3)
31
+
32
+ const slash = endpoint.indexOf('/')
33
+ if (slash !== -1) endpoint = endpoint.slice(0, slash)
34
+
35
+ const colon = endpoint.indexOf(':')
36
+ if (colon !== -1) endpoint = endpoint.slice(0, colon)
37
+
38
+ const dot = endpoint.indexOf('.')
39
+ const label = (dot === -1 ? endpoint : endpoint.slice(0, dot)).toLowerCase()
40
+ if (!label) return 'unknown'
41
+
42
+ let i = label.length - 1
43
+ while (i >= 0) {
44
+ const c = label.charCodeAt(i)
45
+ if (c >= 48 && c <= 57) i--
46
+ else break
47
+ }
48
+ return label.slice(0, i + 1) || 'unknown'
49
+ },
50
+ fillVoicePayload: (payload, guildId, conn, player, resume) => {
51
+ payload.guildId = guildId
52
+ const v = payload.data.voice
53
+ v.token = conn.token
54
+ v.endpoint = conn.endpoint
55
+ v.sessionId = conn.sessionId
56
+ v.resume = resume ? true : undefined
57
+ v.sequence = resume ? conn.sequence : undefined
58
+ payload.data.volume = player?.volume ?? 100
59
+ return payload
29
60
  }
30
61
  }
31
62
 
@@ -39,7 +70,7 @@ class PayloadPool {
39
70
  return {
40
71
  guildId: null,
41
72
  data: {
42
- voice: {token: null, endpoint: null, sessionId: null, resume: undefined, sequence: undefined},
73
+ voice: { token: null, endpoint: null, sessionId: null, resume: undefined, sequence: undefined },
43
74
  volume: null
44
75
  }
45
76
  }
@@ -69,21 +100,21 @@ const sharedPool = new PayloadPool()
69
100
 
70
101
  class Connection {
71
102
  constructor(player) {
72
- if (!player?.aqua?.clientId || !player.nodes?.rest) {
73
- throw new TypeError('Invalid player configuration')
74
- }
103
+ if (!player?.aqua?.clientId || !player.nodes?.rest) throw new TypeError('Invalid player configuration')
75
104
 
76
105
  this._player = player
77
106
  this._aqua = player.aqua
78
107
  this._rest = player.nodes.rest
79
108
  this._guildId = player.guildId
80
109
  this._clientId = player.aqua.clientId
110
+
81
111
  this.voiceChannel = player.voiceChannel
82
112
  this.sessionId = null
83
113
  this.endpoint = null
84
114
  this.token = null
85
115
  this.region = null
86
116
  this.sequence = 0
117
+
87
118
  this._lastEndpoint = null
88
119
  this._pendingUpdate = null
89
120
  this._stateFlags = 0
@@ -109,21 +140,29 @@ class Connection {
109
140
  return this._hasValidVoiceData() || !!this._player?._resuming
110
141
  }
111
142
 
143
+ _setReconnectTimer(delay) {
144
+ if (this._destroyed) return
145
+ this._clearReconnectTimer()
146
+ this._reconnectTimer = setTimeout(() => this._handleReconnect(), delay)
147
+ _functions.safeUnref(this._reconnectTimer)
148
+ }
149
+
112
150
  setServerUpdate(data) {
113
151
  if (this._destroyed || !data?.endpoint || !data.token) return
114
- const endpoint = typeof data.endpoint === 'string' && data.endpoint.trim()
152
+
153
+ const endpoint = typeof data.endpoint === 'string' ? data.endpoint.trim() : ''
115
154
  if (!endpoint || typeof data.token !== 'string' || !data.token) return
116
155
  if (this._lastEndpoint === endpoint && this.token === data.token) return
117
156
 
118
- const newRegion = _functions.extractRegion(endpoint)
119
157
  if (this._lastEndpoint !== endpoint) {
120
158
  this.sequence = 0
121
159
  this._lastEndpoint = endpoint
122
160
  this._reconnectAttempts = 0
123
161
  this._consecutiveFailures = 0
124
162
  }
163
+
125
164
  this.endpoint = endpoint
126
- this.region = newRegion
165
+ this.region = _functions.extractRegion(endpoint)
127
166
  this.token = data.token
128
167
  this._lastVoiceDataUpdate = Date.now()
129
168
  this._stateFlags &= ~STATE.VOICE_DATA_STALE
@@ -141,37 +180,35 @@ class Connection {
141
180
  setStateUpdate(data) {
142
181
  if (this._destroyed || !data || data.user_id !== this._clientId) return
143
182
 
144
- const {session_id: sessionId, channel_id: channelId, self_deaf: selfDeaf, self_mute: selfMute} = data
183
+ const { session_id: sessionId, channel_id: channelId, self_deaf: selfDeaf, self_mute: selfMute } = data
145
184
 
146
- if (channelId) {
147
- let needsUpdate = false
185
+ if (!channelId) return this._handleDisconnect()
148
186
 
149
- if (this.voiceChannel !== channelId) {
150
- this._aqua.emit(AqualinkEvents.PlayerMove, this.voiceChannel, channelId)
151
- this.voiceChannel = channelId
152
- this._player.voiceChannel = channelId
153
- needsUpdate = true
154
- }
187
+ let needsUpdate = false
155
188
 
156
- if (this.sessionId !== sessionId) {
157
- this.sessionId = sessionId
158
- this._lastVoiceDataUpdate = Date.now()
159
- this._stateFlags &= ~STATE.VOICE_DATA_STALE
160
- this._reconnectAttempts = 0
161
- this._consecutiveFailures = 0
162
- needsUpdate = true
163
- }
164
-
165
- this._player.connection.sessionId = sessionId || this._player.connection.sessionId
166
- this._player.self_deaf = this._player.selfDeaf = !!selfDeaf
167
- this._player.self_mute = this._player.selfMute = !!selfMute
168
- this._player.connected = true
169
- this._stateFlags |= STATE.CONNECTED
189
+ if (this.voiceChannel !== channelId) {
190
+ this._aqua.emit(AqualinkEvents.PlayerMove, this.voiceChannel, channelId)
191
+ this.voiceChannel = channelId
192
+ this._player.voiceChannel = channelId
193
+ needsUpdate = true
194
+ }
170
195
 
171
- if (needsUpdate) this._scheduleVoiceUpdate()
172
- } else {
173
- this._handleDisconnect()
196
+ if (this.sessionId !== sessionId) {
197
+ this.sessionId = sessionId
198
+ this._lastVoiceDataUpdate = Date.now()
199
+ this._stateFlags &= ~STATE.VOICE_DATA_STALE
200
+ this._reconnectAttempts = 0
201
+ this._consecutiveFailures = 0
202
+ needsUpdate = true
174
203
  }
204
+
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
209
+ this._stateFlags |= STATE.CONNECTED
210
+
211
+ if (needsUpdate) this._scheduleVoiceUpdate()
175
212
  }
176
213
 
177
214
  _handleDisconnect() {
@@ -183,7 +220,8 @@ class Connection {
183
220
  this._clearReconnectTimer()
184
221
 
185
222
  this.voiceChannel = this.sessionId = null
186
- this.sequence = this._lastVoiceDataUpdate = 0
223
+ this.sequence = 0
224
+ this._lastVoiceDataUpdate = 0
187
225
  this._stateFlags |= STATE.VOICE_DATA_STALE
188
226
 
189
227
  try {
@@ -197,19 +235,18 @@ class Connection {
197
235
 
198
236
  _requestVoiceState() {
199
237
  try {
200
- if (typeof this._player?.send === 'function' && this._player.voiceChannel) {
201
- this._player.send({
202
- guild_id: this._guildId,
203
- channel_id: this._player.voiceChannel,
204
- self_deaf: this._player.deaf,
205
- self_mute: this._player.mute
206
- })
207
- this._reconnectTimer = setTimeout(() => this._handleReconnect(), 1500)
208
- _functions.safeUnref(this._reconnectTimer)
209
- return true
210
- }
211
- } catch {}
212
- return false
238
+ if (typeof this._player?.send !== 'function' || !this._player.voiceChannel) return false
239
+ this._player.send({
240
+ guild_id: this._guildId,
241
+ channel_id: this._player.voiceChannel,
242
+ self_deaf: this._player.deaf,
243
+ self_mute: this._player.mute
244
+ })
245
+ this._setReconnectTimer(1500)
246
+ return true
247
+ } catch {
248
+ return false
249
+ }
213
250
  }
214
251
 
215
252
  async attemptResume() {
@@ -240,21 +277,18 @@ class Connection {
240
277
 
241
278
  this._stateFlags |= STATE.ATTEMPTING_RESUME
242
279
  this._reconnectAttempts++
243
- this._aqua.emit(AqualinkEvents.Debug, `Attempt resume: guild=${this._guildId} endpoint=${this.endpoint} session=${this.sessionId}`)
280
+ this._aqua.emit(
281
+ AqualinkEvents.Debug,
282
+ `Attempt resume: guild=${this._guildId} endpoint=${this.endpoint} session=${this.sessionId}`
283
+ )
244
284
 
245
285
  const payload = sharedPool.acquire()
246
286
  try {
247
- payload.guildId = this._guildId
248
- const v = payload.data.voice
249
- v.token = this.token
250
- v.endpoint = this.endpoint
251
- v.sessionId = this.sessionId
252
- v.resume = true
253
- v.sequence = this.sequence
254
- payload.data.volume = this._player?.volume ?? 100
255
-
287
+ _functions.fillVoicePayload(payload, this._guildId, this, this._player, true)
256
288
  await this._sendUpdate(payload)
257
- this._reconnectAttempts = this._consecutiveFailures = 0
289
+
290
+ this._reconnectAttempts = 0
291
+ this._consecutiveFailures = 0
258
292
  if (this._player) this._player._resuming = false
259
293
  this._aqua.emit(AqualinkEvents.Debug, `Resume successful for guild ${this._guildId}`)
260
294
  return true
@@ -264,8 +298,7 @@ class Connection {
264
298
 
265
299
  if (this._reconnectAttempts < MAX_RECONNECT_ATTEMPTS && !this._destroyed && this._consecutiveFailures < 5) {
266
300
  const delay = Math.min(RECONNECT_DELAY * (1 << (this._reconnectAttempts - 1)), RESUME_BACKOFF_MAX)
267
- this._reconnectTimer = setTimeout(() => this._handleReconnect(), delay)
268
- _functions.safeUnref(this._reconnectTimer)
301
+ this._setReconnectTimer(delay)
269
302
  } else {
270
303
  this._aqua.emit(AqualinkEvents.Debug, `Max reconnect attempts or failures reached for guild ${this._guildId}`)
271
304
  if (this._player) this._player._resuming = false
@@ -288,10 +321,9 @@ class Connection {
288
321
  }
289
322
 
290
323
  _clearReconnectTimer() {
291
- if (this._reconnectTimer) {
292
- clearTimeout(this._reconnectTimer)
293
- this._reconnectTimer = null
294
- }
324
+ if (!this._reconnectTimer) return
325
+ clearTimeout(this._reconnectTimer)
326
+ this._reconnectTimer = null
295
327
  }
296
328
 
297
329
  _clearPendingUpdate() {
@@ -304,16 +336,11 @@ class Connection {
304
336
  if (this._destroyed || !this._hasValidVoiceData() || (this._stateFlags & STATE.UPDATE_SCHEDULED)) return
305
337
 
306
338
  this._clearPendingUpdate()
339
+
307
340
  const payload = sharedPool.acquire()
308
- payload.guildId = this._guildId
309
- const v = payload.data.voice
310
- v.token = this.token
311
- v.endpoint = this.endpoint
312
- v.sessionId = this.sessionId
313
- v.resume = v.sequence = undefined
314
- payload.data.volume = this._player.volume
341
+ _functions.fillVoicePayload(payload, this._guildId, this, this._player, false)
315
342
 
316
- this._pendingUpdate = {payload, timestamp: Date.now()}
343
+ this._pendingUpdate = { payload, timestamp: Date.now() }
317
344
  this._stateFlags |= STATE.UPDATE_SCHEDULED
318
345
  queueMicrotask(() => this._executeVoiceUpdate())
319
346
  }
@@ -358,7 +385,11 @@ class Connection {
358
385
 
359
386
  this._player = this._aqua = this._rest = null
360
387
  this.voiceChannel = this.sessionId = this.endpoint = this.token = this.region = this._lastEndpoint = null
361
- this._stateFlags = this.sequence = this._reconnectAttempts = this._consecutiveFailures = this._lastVoiceDataUpdate = 0
388
+ this._stateFlags = 0
389
+ this.sequence = 0
390
+ this._reconnectAttempts = 0
391
+ this._consecutiveFailures = 0
392
+ this._lastVoiceDataUpdate = 0
362
393
  }
363
394
  }
364
395
 
@@ -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
@@ -285,11 +287,37 @@ class Player extends EventEmitter {
285
287
  return this
286
288
  }
287
289
 
290
+ async _waitForConnection(timeout = RESUME_TIMEOUT) {
291
+ if (this.destroyed) return
292
+ if (this.connected) return
293
+ return new Promise((resolve, reject) => {
294
+ let timer
295
+ const cleanup = () => {
296
+ if (timer) { this._pendingTimers?.delete(timer); clearTimeout(timer) }
297
+ this.off('playerUpdate', onUpdate)
298
+ }
299
+ const onUpdate = payload => {
300
+ if (this.destroyed) { cleanup(); return reject(new Error('Player destroyed')) }
301
+ if (payload?.state?.connected || _functions.isNum(payload?.state?.time)) {
302
+ cleanup()
303
+ return resolve()
304
+ }
305
+ }
306
+ this.on('playerUpdate', onUpdate)
307
+ timer = this._createTimer(() => { cleanup(); reject(new Error('No connection confirmation')) }, timeout)
308
+ })
309
+ }
310
+
288
311
  async play() {
289
312
  if (this.destroyed || !this.queue.size) return this
290
- // most lazy fix i ever did lol
291
- if (this.nodes.isNodelink && !this.connected) { await this._delay(1000); if (!this.connected || this.destroyed) return this }
292
- if (!this.connected) return this
313
+ if (!this.connected) {
314
+ try {
315
+ await this._waitForConnection(RESUME_TIMEOUT)
316
+ if (!this.connected || this.destroyed) return this
317
+ } catch {
318
+ return this
319
+ }
320
+ }
293
321
 
294
322
  const item = this.queue.dequeue()
295
323
  if (!item) return this
@@ -313,7 +341,6 @@ class Player extends EventEmitter {
313
341
  if (!voiceChannel) throw new TypeError('Voice channel required')
314
342
  this.deaf = options.deaf !== undefined ? !!options.deaf : true
315
343
  this.mute = !!options.mute
316
- this.connected = true
317
344
  this.destroyed = false
318
345
  this.voiceChannel = voiceChannel
319
346
  this.send({ guild_id: this.guildId, channel_id: voiceChannel, self_deaf: this.deaf, self_mute: this.mute })
@@ -435,11 +462,54 @@ class Player extends EventEmitter {
435
462
  return this
436
463
  }
437
464
 
465
+ async getActiveMixer(guildId) {
466
+ if (this.destroyed) return null
467
+ return await this.nodes.rest.getActiveMixer(guildId)
468
+ }
469
+
470
+ async updateMixerVolume(guildId, mix, volume) {
471
+ if (this.destroyed) return null
472
+ return await this.nodes.rest.updateMixerVolume(guildId, mix, volume)
473
+ }
474
+
475
+ async removeMixer(guildId, mix) {
476
+ if (this.destroyed) return null
477
+ return await this.nodes.rest.removeMixer(guildId, mix)
478
+ }
479
+
480
+ async addMixer(guildId, options) {
481
+ if (this.destroyed) return null
482
+
483
+ if (options.identifier && !options.encoded) {
484
+ try {
485
+ const resolved = await this.aqua.resolve({
486
+ query: options.identifier,
487
+ requester: options.requester || this.current?.requester
488
+ })
489
+
490
+ if (resolved?.tracks?.[0]) {
491
+ const track = resolved.tracks[0]
492
+ options = {
493
+ ...options,
494
+ encoded: track.track || track.encoded,
495
+ userData: options.userData
496
+ }
497
+ } else {
498
+ throw new Error('Failed to resolve track identifier')
499
+ }
500
+ } catch (error) {
501
+ throw new Error(`Failed to resolve track: ${error.message}`)
502
+ }
503
+ }
504
+
505
+ return await this.nodes.rest.addMixer(guildId, options)
506
+ }
507
+
438
508
  stop() {
439
509
  if (this.destroyed || !this.playing) return this
440
510
  this.playing = this.paused = false
441
511
  this.position = 0
442
- this.batchUpdatePlayer({ guildId: this.guildId, track: { encoded: null } }, true).catch(() => { })
512
+ this.batchUpdatePlayer({ guildId: this.guildId, track: { encoded: null, paused: this.paused } }, true).catch(() => { })
443
513
  return this
444
514
  }
445
515
 
@@ -659,7 +729,9 @@ class Player extends EventEmitter {
659
729
  playerCreated(p, t, payload) { _functions.emitIfActive(this, AqualinkEvents.PlayerCreated, payload) }
660
730
  playerConnected(p, t, payload) { _functions.emitIfActive(this, AqualinkEvents.PlayerConnected, payload) }
661
731
  playerDestroyed(p, t, payload) { _functions.emitIfActive(this, AqualinkEvents.PlayerDestroyed, payload) }
662
- PauseEvent(p, t, payload) { _functions.emitIfActive(this, AqualinkEvents.PauseEvent, payload) }
732
+ pauseEvent(p, t, payload) { _functions.emitIfActive(this, AqualinkEvents.PauseEvent, payload) }
733
+ mixStarted(p, t, payload) { _functions.emitIfActive(this, AqualinkEvents.MixStarted, t, payload) }
734
+ mixEnded(p, t, payload) { _functions.emitIfActive(this, AqualinkEvents.MixEnded, t, payload) }
663
735
 
664
736
  async _attemptVoiceResume() {
665
737
  if (!this.connection?.sessionId) throw new Error('No session')
@@ -820,4 +892,4 @@ class Player extends EventEmitter {
820
892
  }
821
893
  }
822
894
 
823
- module.exports = Player
895
+ 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()
@@ -532,4 +570,4 @@ class Rest {
532
570
  }
533
571
  }
534
572
 
535
- module.exports = Rest
573
+ module.exports = Rest
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aqualink",
3
- "version": "2.17.0",
3
+ "version": "2.17.2",
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",