aqualink 2.18.1 → 2.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,646 +1,857 @@
1
- 'use strict'
2
-
3
- const { Buffer } = require('buffer')
4
- const { Agent: HttpsAgent, request: httpsRequest } = require('https')
5
- const { Agent: HttpAgent, request: httpRequest } = require('http')
6
- const http2 = require('http2')
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'
19
-
20
- const BASE64_LOOKUP = new Uint8Array(256)
21
- for (let i = 65; i <= 90; i++) BASE64_LOOKUP[i] = 1
22
- for (let i = 97; i <= 122; i++) BASE64_LOOKUP[i] = 1
23
- for (let i = 48; i <= 57; i++) BASE64_LOOKUP[i] = 1
24
- BASE64_LOOKUP[43] = BASE64_LOOKUP[47] = BASE64_LOOKUP[61] = BASE64_LOOKUP[95] = BASE64_LOOKUP[45] = 1
25
-
26
- const ENCODING_NONE = 0, ENCODING_BR = 1, ENCODING_GZIP = 2, ENCODING_DEFLATE = 3, ENCODING_ZSTD = 4
27
- const MAX_RESPONSE_SIZE = 10485760
28
- const COMPRESSION_MIN_SIZE = 1024
29
- const API_VERSION = 'v4'
30
- const UTF8 = 'utf8'
31
- const JSON_CT = 'application/json'
32
- const HTTP2_THRESHOLD = 1024
33
- const MAX_HEADER_POOL = 10
34
- const H2_TIMEOUT = 60000
35
-
36
- const ERRORS = Object.freeze({
37
- NO_SESSION: new Error('Session ID required'),
38
- INVALID_TRACK: new Error('Invalid encoded track format'),
39
- INVALID_TRACKS: new Error('One or more tracks have invalid format'),
40
- RESPONSE_TOO_LARGE: new Error('Response too large'),
41
- RESPONSE_ABORTED: new Error('Response aborted')
42
- })
43
-
44
- const _functions = {
45
- isValidBase64(str) {
46
- if (typeof str !== 'string' || !str) return false
47
- const len = str.length
48
- if (len % 4 === 1) return false
49
- for (let i = 0; i < len; i++) {
50
- if (!BASE64_LOOKUP[str.charCodeAt(i)]) return false
51
- }
52
- return true
53
- },
54
-
55
- getEncodingType(header) {
56
- if (!header) return ENCODING_NONE
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
-
62
- if (c === 98 && header.startsWith('br')) return ENCODING_BR
63
- if (c === 103 && header.startsWith('gzip')) return ENCODING_GZIP
64
- if (c === 100 && header.startsWith('deflate')) return ENCODING_DEFLATE
65
- return ENCODING_NONE
66
- },
67
-
68
- isJsonContent(ct) {
69
- return ct && ct.charCodeAt(0) === 97 && ct.includes(JSON_CT)
70
- },
71
-
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)
79
- },
80
-
81
- createHttpError(status, method, url, headers, body, statusMessage) {
82
- const err = new Error(`HTTP ${status} ${method} ${url}`)
83
- err.statusCode = status
84
- err.headers = headers
85
- err.body = body
86
- err.url = url
87
- if (statusMessage !== undefined) err.statusMessage = statusMessage
88
- return err
89
- },
90
-
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
- }
96
- return type === ENCODING_BR ? createBrotliDecompress() : createUnzip()
97
- },
98
-
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
- }
104
- return type === ENCODING_BR ? brotliDecompressSync(buf) : unzipSync(buf)
105
- }
106
- }
107
-
108
- class Rest {
109
- constructor(aqua, node) {
110
- this.aqua = aqua
111
- this.node = node
112
- this.sessionId = node.sessionId
113
- this.timeout = node.timeout || 30000
114
-
115
- const protocol = node.ssl ? 'https:' : 'http:'
116
- const host = node.host.includes(':') && !node.host.startsWith('[') ? `[${node.host}]` : node.host
117
- this.baseUrl = `${protocol}//${host}:${node.port}`
118
- this._apiBase = `/${API_VERSION}`
119
-
120
- this._endpoints = Object.freeze({
121
- loadtracks: `${this._apiBase}/loadtracks?identifier=`,
122
- decodetrack: `${this._apiBase}/decodetrack?encodedTrack=`,
123
- decodetracks: `${this._apiBase}/decodetracks`,
124
- stats: `${this._apiBase}/stats`,
125
- info: `${this._apiBase}/info`,
126
- version: `${this._apiBase}/version`,
127
- routeplanner: Object.freeze({
128
- status: `${this._apiBase}/routeplanner/status`,
129
- freeAddress: `${this._apiBase}/routeplanner/free/address`,
130
- freeAll: `${this._apiBase}/routeplanner/free/all`
131
- }),
132
- lyrics: `${this._apiBase}/lyrics`
133
- })
134
-
135
- const acceptEncoding = HAS_ZSTD ? 'zstd, br, gzip, deflate' : 'br, gzip, deflate'
136
-
137
- this.defaultHeaders = Object.freeze({
138
- Authorization: String(node.auth || node.password || ''),
139
- Accept: 'application/json, */*;q=0.5',
140
- 'Accept-Encoding': acceptEncoding,
141
- 'User-Agent': `Aqualink/${aqua?.version || '1.0'} (Node.js ${process.version})`
142
- })
143
-
144
- this._headerPool = []
145
- this._tlsOptions = null
146
- this._setupAgent(node)
147
- this.useHttp2 = !!(aqua?.options?.useHttp2)
148
- this._h2 = null
149
- this._h2Timer = null
150
- }
151
-
152
- _setupAgent(node) {
153
- const opts = {
154
- keepAlive: true,
155
- maxSockets: node.maxSockets || 128,
156
- maxFreeSockets: node.maxFreeSockets || 64,
157
- freeSocketTimeout: node.freeSocketTimeout || 15000,
158
- keepAliveMsecs: node.keepAliveMsecs || 500,
159
- scheduling: 'lifo',
160
- timeout: this.timeout
161
- }
162
-
163
- if (node.ssl) {
164
- opts.maxCachedSessions = node.maxCachedSessions || 200
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
172
- }
173
-
174
- this.agent = new (node.ssl ? HttpsAgent : HttpAgent)(opts)
175
- this.request = node.ssl ? httpsRequest : httpRequest
176
-
177
- const origCreate = this.agent.createConnection.bind(this.agent)
178
- this.agent.createConnection = (options, cb) => {
179
- const socket = origCreate(options, cb)
180
- socket.setNoDelay(true)
181
- socket.setKeepAlive(true, 500)
182
- return socket
183
- }
184
- }
185
-
186
- setSessionId(sessionId) {
187
- this.sessionId = sessionId
188
- }
189
-
190
- _getSessionPath() {
191
- if (!this.sessionId) throw ERRORS.NO_SESSION
192
- return `${this._apiBase}/sessions/${this.sessionId}`
193
- }
194
-
195
- _buildHeaders(hasPayload, payloadLength) {
196
- if (!hasPayload) return this.defaultHeaders
197
- const h = this._headerPool.pop() || Object.create(null)
198
- h.Authorization = this.defaultHeaders.Authorization
199
- h.Accept = this.defaultHeaders.Accept
200
- h['Accept-Encoding'] = this.defaultHeaders['Accept-Encoding']
201
- h['User-Agent'] = this.defaultHeaders['User-Agent']
202
- h['Content-Type'] = JSON_CT
203
- h['Content-Length'] = payloadLength
204
- return h
205
- }
206
-
207
- _returnHeaders(h) {
208
- if (h !== this.defaultHeaders && this._headerPool.length < MAX_HEADER_POOL) {
209
- h.Authorization = h.Accept = h['Accept-Encoding'] = h['User-Agent'] = h['Content-Type'] = h['Content-Length'] = null
210
- this._headerPool.push(h)
211
- }
212
- }
213
-
214
- async makeRequest(method, endpoint, body) {
215
- const url = `${this.baseUrl}${endpoint}`
216
- const payload = body === undefined ? undefined : (typeof body === 'string' ? body : JSON.stringify(body))
217
- const payloadLen = payload ? Buffer.byteLength(payload, UTF8) : 0
218
- const headers = this._buildHeaders(!!payload, payloadLen)
219
-
220
- try {
221
- const resp = this.useHttp2 && payloadLen >= HTTP2_THRESHOLD
222
- ? await this._h2Request(method, endpoint, headers, payload)
223
- : await this._h1Request(method, url, headers, payload)
224
- return resp
225
- } finally {
226
- this._returnHeaders(headers)
227
- }
228
- }
229
-
230
- _h1Request(method, url, headers, payload) {
231
- return new Promise((resolve, reject) => {
232
- let req, timer, done = false
233
- let chunks = null, size = 0, prealloc = null
234
-
235
- const finish = (ok, val) => {
236
- if (done) return
237
- done = true
238
- if (timer) { clearTimeout(timer); timer = null }
239
- chunks = prealloc = null
240
- if (req && !ok) req.destroy()
241
- ok ? resolve(val) : reject(val)
242
- }
243
-
244
- req = this.request(url, { method, headers, agent: this.agent, timeout: this.timeout }, (res) => {
245
- if (timer) { clearTimeout(timer); timer = null }
246
-
247
- const status = res.statusCode || 0
248
- const cl = res.headers['content-length']
249
- const contentType = res.headers['content-type'] || ''
250
-
251
- if (status === 204 || cl === '0') {
252
- res.resume()
253
- return finish(true, null)
254
- }
255
-
256
- const clInt = cl ? parseInt(cl, 10) : 0
257
- if (clInt > MAX_RESPONSE_SIZE) {
258
- res.resume()
259
- return finish(false, ERRORS.RESPONSE_TOO_LARGE)
260
- }
261
-
262
- const encoding = _functions.getEncodingType(res.headers['content-encoding'])
263
-
264
- const handleResponse = (buffer) => {
265
- if (buffer.length > MAX_RESPONSE_SIZE) return finish(false, ERRORS.RESPONSE_TOO_LARGE)
266
-
267
- try {
268
- const result = _functions.parseBody(buffer, contentType, false)
269
- if (status >= 400) {
270
- finish(false, _functions.createHttpError(status, method, url, res.headers, result, res.statusMessage))
271
- } else {
272
- finish(true, result)
273
- }
274
- } catch (e) {
275
- finish(false, new Error(`JSON parse error: ${e.message}`))
276
- }
277
- }
278
-
279
- if (encoding !== ENCODING_NONE && clInt > 0 && clInt < COMPRESSION_MIN_SIZE) {
280
- const compressed = []
281
- let csize = 0
282
-
283
- res.on('data', (c) => {
284
- csize += c.length
285
- if (csize > MAX_RESPONSE_SIZE) return finish(false, ERRORS.RESPONSE_TOO_LARGE)
286
- compressed.push(c)
287
- })
288
- res.once('end', () => {
289
- try {
290
- handleResponse(_functions.decompressSync(Buffer.concat(compressed), encoding))
291
- } catch (e) {
292
- finish(false, e)
293
- }
294
- })
295
- res.once('aborted', () => finish(false, ERRORS.RESPONSE_ABORTED))
296
- res.once('error', (e) => finish(false, e))
297
- return
298
- }
299
-
300
- if (encoding === ENCODING_NONE && clInt > 0 && clInt <= MAX_RESPONSE_SIZE) {
301
- prealloc = Buffer.allocUnsafe(clInt)
302
- } else {
303
- chunks = []
304
- }
305
-
306
- let stream = res
307
- if (encoding !== ENCODING_NONE) {
308
- const decomp = _functions.createDecompressor(encoding)
309
- decomp.once('error', (e) => finish(false, e))
310
- res.pipe(decomp)
311
- stream = decomp
312
- }
313
-
314
- res.once('aborted', () => finish(false, ERRORS.RESPONSE_ABORTED))
315
- res.once('error', (e) => finish(false, e))
316
-
317
- stream.on('data', (chunk) => {
318
- if (prealloc) {
319
- chunk.copy(prealloc, size)
320
- size += chunk.length
321
- } else {
322
- size += chunk.length
323
- if (size > MAX_RESPONSE_SIZE) return finish(false, ERRORS.RESPONSE_TOO_LARGE)
324
- chunks.push(chunk)
325
- }
326
- })
327
-
328
- stream.once('end', () => {
329
- if (size === 0) return finish(true, null)
330
- handleResponse(
331
- prealloc
332
- ? prealloc.slice(0, size)
333
- : (chunks.length === 1 ? chunks[0] : Buffer.concat(chunks, size))
334
- )
335
- })
336
- })
337
-
338
- req.once('error', (e) => finish(false, e))
339
- timer = setTimeout(() => finish(false, new Error(`Request timeout: ${this.timeout}ms`)), this.timeout)
340
- unrefTimer(timer)
341
- payload ? req.end(payload) : req.end()
342
- })
343
- }
344
-
345
- _getH2Session() {
346
- if (!this._h2 || this._h2.closed || this._h2.destroyed) {
347
- this._clearH2()
348
- this._h2 = http2.connect(this.baseUrl, this._tlsOptions || undefined)
349
- this._resetH2Timer()
350
-
351
- const onEnd = () => this._clearH2()
352
- this._h2.once('error', onEnd)
353
- this._h2.once('close', onEnd)
354
- this._h2.socket?.unref?.()
355
- }
356
- return this._h2
357
- }
358
-
359
- _resetH2Timer() {
360
- if (this._h2Timer) { clearTimeout(this._h2Timer); this._h2Timer = null }
361
- if (this._h2 && !this._h2.closed && !this._h2.destroyed) {
362
- this._h2Timer = setTimeout(() => this._closeH2(), H2_TIMEOUT)
363
- unrefTimer(this._h2Timer)
364
- }
365
- }
366
-
367
- _clearH2() {
368
- if (this._h2Timer) { clearTimeout(this._h2Timer); this._h2Timer = null }
369
- this._h2 = null
370
- }
371
-
372
- _closeH2() {
373
- if (this._h2Timer) { clearTimeout(this._h2Timer); this._h2Timer = null }
374
- if (this._h2) {
375
- try { this._h2.close() } catch {}
376
- this._h2 = null
377
- }
378
- }
379
-
380
- _h2Request(method, path, headers, payload) {
381
- const session = this._getH2Session()
382
-
383
- return new Promise((resolve, reject) => {
384
- let req, timer, done = false
385
- let chunks = null, size = 0, prealloc = null
386
-
387
- const finish = (ok, val) => {
388
- if (done) return
389
- done = true
390
- if (timer) { clearTimeout(timer); timer = null }
391
- chunks = prealloc = null
392
- if (req && !ok) req.close(http2.constants.NGHTTP2_CANCEL)
393
- ok ? resolve(val) : reject(val)
394
- }
395
-
396
- const h2h = {
397
- ':method': method,
398
- ':path': path,
399
- Authorization: headers.Authorization,
400
- Accept: headers.Accept,
401
- 'Accept-Encoding': headers['Accept-Encoding'],
402
- 'User-Agent': headers['User-Agent']
403
- }
404
- if (headers['Content-Type']) h2h['Content-Type'] = headers['Content-Type']
405
- if (headers['Content-Length']) h2h['Content-Length'] = headers['Content-Length']
406
-
407
- req = session.request(h2h)
408
- this._resetH2Timer()
409
-
410
- req.once('response', (rh) => {
411
- if (timer) { clearTimeout(timer); timer = null }
412
-
413
- const status = rh[':status'] || 0
414
- const cl = rh['content-length']
415
- const contentType = rh['content-type'] || ''
416
-
417
- if (status === 204 || cl === '0') {
418
- req.resume()
419
- return finish(true, null)
420
- }
421
-
422
- const clInt = cl ? parseInt(cl, 10) : 0
423
- if (clInt > MAX_RESPONSE_SIZE) {
424
- req.resume()
425
- return finish(false, ERRORS.RESPONSE_TOO_LARGE)
426
- }
427
-
428
- const encoding = _functions.getEncodingType(rh['content-encoding'])
429
-
430
- if (encoding === ENCODING_NONE && clInt > 0 && clInt <= MAX_RESPONSE_SIZE) {
431
- prealloc = Buffer.allocUnsafe(clInt)
432
- } else {
433
- chunks = []
434
- }
435
-
436
- const decomp = encoding !== ENCODING_NONE ? _functions.createDecompressor(encoding) : null
437
- const stream = decomp ? req.pipe(decomp) : req
438
-
439
- if (decomp) decomp.once('error', (e) => finish(false, e))
440
- req.once('error', (e) => finish(false, e))
441
-
442
- stream.on('data', (chunk) => {
443
- if (prealloc) {
444
- chunk.copy(prealloc, size)
445
- size += chunk.length
446
- } else {
447
- size += chunk.length
448
- if (size > MAX_RESPONSE_SIZE) return finish(false, ERRORS.RESPONSE_TOO_LARGE)
449
- chunks.push(chunk)
450
- }
451
- })
452
-
453
- stream.once('end', () => {
454
- if (size === 0) return finish(true, null)
455
-
456
- const buffer = prealloc
457
- ? prealloc.slice(0, size)
458
- : (chunks.length === 1 ? chunks[0] : Buffer.concat(chunks, size))
459
-
460
- try {
461
- const result = _functions.parseBody(buffer, contentType, false)
462
- if (status >= 400) {
463
- finish(false, _functions.createHttpError(status, method, this.baseUrl + path, rh, result))
464
- } else {
465
- finish(true, result)
466
- }
467
- } catch (e) {
468
- finish(false, new Error(`JSON parse error: ${e.message}`))
469
- }
470
- })
471
- })
472
-
473
- timer = setTimeout(() => finish(false, new Error(`Request timeout: ${this.timeout}ms`)), this.timeout)
474
- unrefTimer(timer)
475
- payload ? req.end(payload) : req.end()
476
- })
477
- }
478
-
479
- async updatePlayer({ guildId, data, noReplace = false }) {
480
- return this.makeRequest('PATCH', `${this._getSessionPath()}/players/${guildId}?noReplace=${noReplace}`, data)
481
- }
482
-
483
- async getPlayer(guildId) {
484
- return this.makeRequest('GET', `${this._getSessionPath()}/players/${guildId}`)
485
- }
486
-
487
- async getPlayers() {
488
- return this.makeRequest('GET', `${this._getSessionPath()}/players`)
489
- }
490
-
491
- async destroyPlayer(guildId) {
492
- return this.makeRequest('DELETE', `${this._getSessionPath()}/players/${guildId}`)
493
- }
494
-
495
- async loadTracks(identifier) {
496
- return this.makeRequest('GET', `${this._endpoints.loadtracks}${encodeURIComponent(identifier)}`)
497
- }
498
-
499
- async decodeTrack(encodedTrack) {
500
- if (!_functions.isValidBase64(encodedTrack)) throw ERRORS.INVALID_TRACK
501
- return this.makeRequest('GET', `${this._endpoints.decodetrack}${encodeURIComponent(encodedTrack)}`)
502
- }
503
-
504
- async decodeTracks(encodedTracks) {
505
- if (!Array.isArray(encodedTracks) || !encodedTracks.length) throw ERRORS.INVALID_TRACKS
506
- for (let i = 0; i < encodedTracks.length; i++) {
507
- if (!_functions.isValidBase64(encodedTracks[i])) throw ERRORS.INVALID_TRACKS
508
- }
509
- return this.makeRequest('POST', this._endpoints.decodetracks, encodedTracks)
510
- }
511
-
512
- async getStats() {
513
- return this.makeRequest('GET', this._endpoints.stats)
514
- }
515
-
516
- async getInfo() {
517
- return this.makeRequest('GET', this._endpoints.info)
518
- }
519
-
520
- async getVersion() {
521
- return this.makeRequest('GET', this._endpoints.version)
522
- }
523
-
524
- async getRoutePlannerStatus() {
525
- return this.makeRequest('GET', this._endpoints.routeplanner.status)
526
- }
527
-
528
- async freeRoutePlannerAddress(address) {
529
- return this.makeRequest('POST', this._endpoints.routeplanner.freeAddress, { address })
530
- }
531
-
532
- async freeAllRoutePlannerAddresses() {
533
- return this.makeRequest('POST', this._endpoints.routeplanner.freeAll)
534
- }
535
-
536
- async getLyrics({ track, skipTrackSource = false }) {
537
- const guildId = track?.guild_id ?? track?.guildId
538
- const encoded = track?.encoded
539
- const hasEncoded = typeof encoded === 'string' && encoded.length > 0 && _functions.isValidBase64(encoded)
540
- const title = track?.info?.title
541
-
542
- if (!track || (!guildId && !hasEncoded && !title)) {
543
- this.aqua?.emit?.('error', '[Aqua/Lyrics] Invalid track object')
544
- return null
545
- }
546
-
547
- const skip = skipTrackSource ? 'true' : 'false'
548
-
549
- if (guildId) {
550
- try {
551
- const lyrics = await this.makeRequest('GET', `${this._getSessionPath()}/players/${guildId}/track/lyrics?skipTrackSource=${skip}`)
552
- if (this._validLyrics(lyrics)) return lyrics
553
- } catch {}
554
- }
555
-
556
- if (hasEncoded) {
557
- try {
558
- const lyrics = await this.makeRequest('GET', `${this._endpoints.lyrics}?track=${encodeURIComponent(encoded)}&skipTrackSource=${skip}`)
559
- if (this._validLyrics(lyrics)) return lyrics
560
- } catch {}
561
- }
562
-
563
- if (title) {
564
- const info = track.info || {}
565
- const query = info.author ? `${title} ${info.author}` : title
566
- try {
567
- const lyrics = await this.makeRequest('GET', `${this._endpoints.lyrics}/search?query=${encodeURIComponent(query)}`)
568
- if (this._validLyrics(lyrics)) return lyrics
569
- } catch {}
570
- }
571
-
572
- return null
573
- }
574
-
575
- _validLyrics(r) {
576
- if (!r) return false
577
- if (typeof r === 'string') return r.length > 0
578
- if (typeof r === 'object') return Array.isArray(r) ? r.length > 0 : Object.keys(r).length > 0
579
- return false
580
- }
581
-
582
- async subscribeLiveLyrics(guildId, skipTrackSource = false) {
583
- try {
584
- return await this.makeRequest(
585
- 'POST',
586
- `${this._getSessionPath()}/players/${guildId}/lyrics/subscribe?skipTrackSource=${skipTrackSource ? 'true' : 'false'}`
587
- ) === null
588
- } catch {
589
- return false
590
- }
591
- }
592
-
593
- async unsubscribeLiveLyrics(guildId) {
594
- try {
595
- return await this.makeRequest('DELETE', `${this._getSessionPath()}/players/${guildId}/lyrics/subscribe`) === null
596
- } catch {
597
- return false
598
- }
599
- }
600
-
601
- async addMixer(guildId, options) {
602
- if (!this.node.isNodelink) throw new Error('Mixer endpoints are only available on Nodelink nodes')
603
- if (!options?.encoded && !options?.identifier) throw new Error('You must provide either encoded or identifier')
604
-
605
- const track = {}
606
- if (options.encoded) track.encoded = options.encoded
607
- if (options.identifier) track.identifier = options.identifier
608
- if (options.userData) track.userData = options.userData
609
-
610
- const payload = {
611
- track,
612
- volume: options.volume !== undefined ? options.volume : 0.8
613
- }
614
-
615
- return this.makeRequest('POST', `/v4/sessions/${this.sessionId}/players/${guildId}/mix`, payload)
616
- }
617
-
618
- async getActiveMixer(guildId) {
619
- if (!this.node.isNodelink) throw new Error('Mixer endpoints are only available on Nodelink nodes')
620
- const response = await this.makeRequest('GET', `/v4/sessions/${this.sessionId}/players/${guildId}/mix`)
621
- return response?.mixes || []
622
- }
623
-
624
- async updateMixerVolume(guildId, mix, volume) {
625
- if (!this.node.isNodelink) throw new Error('Mixer endpoints are only available on Nodelink nodes')
626
- if (!guildId || !mix || typeof volume !== 'number') throw new Error('You forget to set the guild_id, mix or volume options')
627
-
628
- return this.makeRequest('PATCH', `/v4/sessions/${this.sessionId}/players/${guildId}/mix/${mix}`, { volume })
629
- }
630
-
631
- async removeMixer(guildId, mix) {
632
- if (!this.node.isNodelink) throw new Error('Mixer endpoints are only available on Nodelink nodes')
633
- if (!guildId || !mix) throw new Error('You forget to set the guild_id and/or mix options')
634
-
635
- return this.makeRequest('DELETE', `/v4/sessions/${this.sessionId}/players/${guildId}/mix/${mix}`)
636
- }
637
-
638
- destroy() {
639
- if (this.agent) { this.agent.destroy(); this.agent = null }
640
- this._closeH2()
641
- if (this._headerPool) { this._headerPool.length = 0; this._headerPool = null }
642
- this.aqua = this.node = this.request = this.defaultHeaders = this._endpoints = null
643
- }
644
- }
645
-
1
+ const { Buffer } = require('node:buffer')
2
+ const { Agent: HttpsAgent, request: httpsRequest } = require('node:https')
3
+ const { Agent: HttpAgent, request: httpRequest } = require('node:http')
4
+ const http2 = require('node:http2')
5
+ const {
6
+ createBrotliDecompress,
7
+ createUnzip,
8
+ brotliDecompressSync,
9
+ unzipSync,
10
+ createZstdDecompress,
11
+ zstdDecompressSync
12
+ } = require('node:zlib')
13
+
14
+ let autoplayModule = null
15
+ try {
16
+ autoplayModule = require('../handlers/autoplay')
17
+ } catch {}
18
+
19
+ const unrefTimer = (t) => {
20
+ try {
21
+ t?.unref?.()
22
+ } catch {}
23
+ }
24
+
25
+ const HAS_ZSTD =
26
+ typeof createZstdDecompress === 'function' &&
27
+ typeof zstdDecompressSync === 'function'
28
+
29
+ const BASE64_LOOKUP = new Uint8Array(256)
30
+ for (let i = 65; i <= 90; i++) BASE64_LOOKUP[i] = 1
31
+ for (let i = 97; i <= 122; i++) BASE64_LOOKUP[i] = 1
32
+ for (let i = 48; i <= 57; i++) BASE64_LOOKUP[i] = 1
33
+ BASE64_LOOKUP[43] =
34
+ BASE64_LOOKUP[47] =
35
+ BASE64_LOOKUP[61] =
36
+ BASE64_LOOKUP[95] =
37
+ BASE64_LOOKUP[45] =
38
+ 1
39
+
40
+ const ENCODING_NONE = 0,
41
+ ENCODING_BR = 1,
42
+ ENCODING_GZIP = 2,
43
+ ENCODING_DEFLATE = 3,
44
+ ENCODING_ZSTD = 4
45
+ const MAX_RESPONSE_SIZE = 10485760
46
+ const COMPRESSION_MIN_SIZE = 1024
47
+ const API_VERSION = 'v4'
48
+ const UTF8 = 'utf8'
49
+ const JSON_CT = 'application/json'
50
+ const HTTP2_THRESHOLD = 1024
51
+ const MAX_HEADER_POOL = 10
52
+ const H2_TIMEOUT = 60000
53
+
54
+ const ERRORS = Object.freeze({
55
+ NO_SESSION: new Error('Session ID required'),
56
+ INVALID_TRACK: new Error('Invalid encoded track format'),
57
+ INVALID_TRACKS: new Error('One or more tracks have invalid format'),
58
+ RESPONSE_TOO_LARGE: new Error('Response too large'),
59
+ RESPONSE_ABORTED: new Error('Response aborted')
60
+ })
61
+
62
+ const _functions = {
63
+ isValidBase64(str) {
64
+ if (typeof str !== 'string' || !str) return false
65
+ const len = str.length
66
+ if (len % 4 === 1) return false
67
+ for (let i = 0; i < len; i++) {
68
+ if (!BASE64_LOOKUP[str.charCodeAt(i)]) return false
69
+ }
70
+ return true
71
+ },
72
+
73
+ getEncodingType(header) {
74
+ if (!header) return ENCODING_NONE
75
+ const c = header.charCodeAt(0)
76
+
77
+ if (c === 122 && header.startsWith('zstd')) return ENCODING_ZSTD
78
+ if (c === 120 && header.startsWith('x-zstd')) return ENCODING_ZSTD
79
+
80
+ if (c === 98 && header.startsWith('br')) return ENCODING_BR
81
+ if (c === 103 && header.startsWith('gzip')) return ENCODING_GZIP
82
+ if (c === 100 && header.startsWith('deflate')) return ENCODING_DEFLATE
83
+ return ENCODING_NONE
84
+ },
85
+
86
+ isJsonContent(ct) {
87
+ return ct && ct.charCodeAt(0) === 97 && ct.includes(JSON_CT)
88
+ },
89
+
90
+ parseBody(data, contentType, forceJson) {
91
+ const isJson = forceJson || this.isJsonContent(contentType)
92
+ if (isJson) {
93
+ if (typeof data === 'string') return JSON.parse(data)
94
+ return JSON.parse(data)
95
+ }
96
+ return typeof data === 'string' ? data : data.toString(UTF8)
97
+ },
98
+
99
+ createHttpError(status, method, url, headers, body, statusMessage) {
100
+ const err = new Error(`HTTP ${status} ${method} ${url}`)
101
+ err.statusCode = status
102
+ err.headers = headers
103
+ err.body = body
104
+ err.url = url
105
+ if (statusMessage !== undefined) err.statusMessage = statusMessage
106
+ return err
107
+ },
108
+
109
+ createDecompressor(type) {
110
+ if (type === ENCODING_ZSTD) {
111
+ if (!HAS_ZSTD)
112
+ throw new Error(
113
+ 'Unsupported content-encoding: zstd (zlib zstd APIs not available in this Node runtime)'
114
+ )
115
+ return createZstdDecompress()
116
+ }
117
+ return type === ENCODING_BR ? createBrotliDecompress() : createUnzip()
118
+ },
119
+
120
+ decompressSync(buf, type) {
121
+ if (type === ENCODING_ZSTD) {
122
+ if (!HAS_ZSTD)
123
+ throw new Error(
124
+ 'Unsupported content-encoding: zstd (zlib zstd APIs not available in this Node runtime)'
125
+ )
126
+ return zstdDecompressSync(buf)
127
+ }
128
+ return type === ENCODING_BR ? brotliDecompressSync(buf) : unzipSync(buf)
129
+ }
130
+ }
131
+
132
+ class Rest {
133
+ constructor(aqua, node) {
134
+ this.aqua = aqua
135
+ this.node = node
136
+ this.sessionId = node.sessionId
137
+ this.timeout = node.timeout || 30000
138
+
139
+ const protocol = node.ssl ? 'https:' : 'http:'
140
+ const host =
141
+ node.host.includes(':') && !node.host.startsWith('[')
142
+ ? `[${node.host}]`
143
+ : node.host
144
+ this.baseUrl = `${protocol}//${host}:${node.port}`
145
+ this._apiBase = `/${API_VERSION}`
146
+
147
+ this._endpoints = Object.freeze({
148
+ loadtracks: `${this._apiBase}/loadtracks?identifier=`,
149
+ decodetrack: `${this._apiBase}/decodetrack?encodedTrack=`,
150
+ decodetracks: `${this._apiBase}/decodetracks`,
151
+ stats: `${this._apiBase}/stats`,
152
+ info: `${this._apiBase}/info`,
153
+ version: `${this._apiBase}/version`,
154
+ routeplanner: Object.freeze({
155
+ status: `${this._apiBase}/routeplanner/status`,
156
+ freeAddress: `${this._apiBase}/routeplanner/free/address`,
157
+ freeAll: `${this._apiBase}/routeplanner/free/all`
158
+ }),
159
+ lyrics: `${this._apiBase}/lyrics`
160
+ })
161
+
162
+ const acceptEncoding = HAS_ZSTD
163
+ ? 'zstd, br, gzip, deflate'
164
+ : 'br, gzip, deflate'
165
+
166
+ this.defaultHeaders = Object.freeze({
167
+ Authorization: String(node.auth || node.password || ''),
168
+ Accept: 'application/json, */*;q=0.5',
169
+ 'Accept-Encoding': acceptEncoding,
170
+ 'User-Agent': `Aqualink/${aqua?.version || '1.0'} (Node.js ${process.version})`
171
+ })
172
+
173
+ this._headerPool = []
174
+ this._tlsOptions = null
175
+ this._setupAgent(node)
176
+ this.useHttp2 = !!aqua?.options?.useHttp2
177
+ this._h2 = null
178
+ this._h2Timer = null
179
+ }
180
+
181
+ _setupAgent(node) {
182
+ const opts = {
183
+ keepAlive: true,
184
+ maxSockets: node.maxSockets || 128,
185
+ maxFreeSockets: node.maxFreeSockets || 64,
186
+ freeSocketTimeout: node.freeSocketTimeout || 15000,
187
+ keepAliveMsecs: node.keepAliveMsecs || 500,
188
+ scheduling: 'lifo',
189
+ timeout: this.timeout
190
+ }
191
+
192
+ if (node.ssl) {
193
+ opts.maxCachedSessions = node.maxCachedSessions || 200
194
+ this._tlsOptions = Object.create(null)
195
+ if (node.rejectUnauthorized !== undefined)
196
+ this._tlsOptions.rejectUnauthorized = opts.rejectUnauthorized =
197
+ node.rejectUnauthorized
198
+ if (node.ca) this._tlsOptions.ca = opts.ca = node.ca
199
+ if (node.cert) this._tlsOptions.cert = opts.cert = node.cert
200
+ if (node.key) this._tlsOptions.key = opts.key = node.key
201
+ if (node.passphrase)
202
+ this._tlsOptions.passphrase = opts.passphrase = node.passphrase
203
+ if (node.servername) this._tlsOptions.servername = node.servername
204
+ }
205
+
206
+ this.agent = new (node.ssl ? HttpsAgent : HttpAgent)(opts)
207
+ this.request = node.ssl ? httpsRequest : httpRequest
208
+
209
+ if (node.ssl && autoplayModule?.setSharedAgent) {
210
+ autoplayModule.setSharedAgent(this.agent)
211
+ }
212
+
213
+ const origCreate = this.agent.createConnection.bind(this.agent)
214
+ this.agent.createConnection = (options, cb) => {
215
+ const socket = origCreate(options, cb)
216
+ socket.setNoDelay(true)
217
+ socket.setKeepAlive(true, 500)
218
+ return socket
219
+ }
220
+ }
221
+
222
+ setSessionId(sessionId) {
223
+ this.sessionId = sessionId
224
+ }
225
+
226
+ _getSessionPath() {
227
+ if (!this.sessionId) throw ERRORS.NO_SESSION
228
+ return `${this._apiBase}/sessions/${this.sessionId}`
229
+ }
230
+
231
+ _buildHeaders(hasPayload, payloadLength) {
232
+ if (!hasPayload) return this.defaultHeaders
233
+ const h = this._headerPool.pop() || Object.create(null)
234
+ h.Authorization = this.defaultHeaders.Authorization
235
+ h.Accept = this.defaultHeaders.Accept
236
+ h['Accept-Encoding'] = this.defaultHeaders['Accept-Encoding']
237
+ h['User-Agent'] = this.defaultHeaders['User-Agent']
238
+ h['Content-Type'] = JSON_CT
239
+ h['Content-Length'] = payloadLength
240
+ return h
241
+ }
242
+
243
+ _returnHeaders(h) {
244
+ if (
245
+ h !== this.defaultHeaders &&
246
+ this._headerPool.length < MAX_HEADER_POOL
247
+ ) {
248
+ h.Authorization =
249
+ h.Accept =
250
+ h['Accept-Encoding'] =
251
+ h['User-Agent'] =
252
+ h['Content-Type'] =
253
+ h['Content-Length'] =
254
+ null
255
+ this._headerPool.push(h)
256
+ }
257
+ }
258
+
259
+ async makeRequest(method, endpoint, body) {
260
+ const url = `${this.baseUrl}${endpoint}`
261
+ const payload =
262
+ body === undefined
263
+ ? undefined
264
+ : typeof body === 'string'
265
+ ? body
266
+ : JSON.stringify(body)
267
+ const payloadLen = payload ? Buffer.byteLength(payload, UTF8) : 0
268
+ const headers = this._buildHeaders(!!payload, payloadLen)
269
+
270
+ try {
271
+ const resp =
272
+ this.useHttp2 && payloadLen >= HTTP2_THRESHOLD
273
+ ? await this._h2Request(method, endpoint, headers, payload)
274
+ : await this._h1Request(method, url, headers, payload)
275
+ return resp
276
+ } finally {
277
+ this._returnHeaders(headers)
278
+ }
279
+ }
280
+
281
+ _h1Request(method, url, headers, payload) {
282
+ return new Promise((resolve, reject) => {
283
+ let req,
284
+ timer,
285
+ done = false
286
+ let chunks = null,
287
+ size = 0,
288
+ prealloc = null
289
+
290
+ const finish = (ok, val) => {
291
+ if (done) return
292
+ done = true
293
+ if (timer) {
294
+ clearTimeout(timer)
295
+ timer = null
296
+ }
297
+ chunks = prealloc = null
298
+ if (req && !ok) req.destroy()
299
+ ok ? resolve(val) : reject(val)
300
+ }
301
+
302
+ req = this.request(
303
+ url,
304
+ { method, headers, agent: this.agent, timeout: this.timeout },
305
+ (res) => {
306
+ if (timer) {
307
+ clearTimeout(timer)
308
+ timer = null
309
+ }
310
+
311
+ const status = res.statusCode || 0
312
+ const cl = res.headers['content-length']
313
+ const contentType = res.headers['content-type'] || ''
314
+
315
+ if (status === 204 || cl === '0') {
316
+ res.resume()
317
+ return finish(true, null)
318
+ }
319
+
320
+ const clInt = cl ? parseInt(cl, 10) : 0
321
+ if (clInt > MAX_RESPONSE_SIZE) {
322
+ res.resume()
323
+ return finish(false, ERRORS.RESPONSE_TOO_LARGE)
324
+ }
325
+
326
+ const encoding = _functions.getEncodingType(
327
+ res.headers['content-encoding']
328
+ )
329
+
330
+ const handleResponse = (buffer) => {
331
+ if (buffer.length > MAX_RESPONSE_SIZE)
332
+ return finish(false, ERRORS.RESPONSE_TOO_LARGE)
333
+
334
+ try {
335
+ const result = _functions.parseBody(buffer, contentType, false)
336
+ if (status >= 400) {
337
+ finish(
338
+ false,
339
+ _functions.createHttpError(
340
+ status,
341
+ method,
342
+ url,
343
+ res.headers,
344
+ result,
345
+ res.statusMessage
346
+ )
347
+ )
348
+ } else {
349
+ finish(true, result)
350
+ }
351
+ } catch (e) {
352
+ finish(false, new Error(`JSON parse error: ${e.message}`))
353
+ }
354
+ }
355
+
356
+ if (
357
+ encoding !== ENCODING_NONE &&
358
+ clInt > 0 &&
359
+ clInt < COMPRESSION_MIN_SIZE
360
+ ) {
361
+ const compressed = []
362
+ let csize = 0
363
+
364
+ res.on('data', (c) => {
365
+ csize += c.length
366
+ if (csize > MAX_RESPONSE_SIZE)
367
+ return finish(false, ERRORS.RESPONSE_TOO_LARGE)
368
+ compressed.push(c)
369
+ })
370
+ res.once('end', () => {
371
+ try {
372
+ handleResponse(
373
+ _functions.decompressSync(Buffer.concat(compressed), encoding)
374
+ )
375
+ } catch (e) {
376
+ finish(false, e)
377
+ }
378
+ })
379
+ res.once('aborted', () => finish(false, ERRORS.RESPONSE_ABORTED))
380
+ res.once('error', (e) => finish(false, e))
381
+ return
382
+ }
383
+
384
+ if (
385
+ encoding === ENCODING_NONE &&
386
+ clInt > 0 &&
387
+ clInt <= MAX_RESPONSE_SIZE
388
+ ) {
389
+ prealloc = Buffer.allocUnsafe(clInt)
390
+ } else {
391
+ chunks = []
392
+ }
393
+
394
+ let stream = res
395
+ if (encoding !== ENCODING_NONE) {
396
+ const decomp = _functions.createDecompressor(encoding)
397
+ decomp.once('error', (e) => finish(false, e))
398
+ res.pipe(decomp)
399
+ stream = decomp
400
+ }
401
+
402
+ res.once('aborted', () => finish(false, ERRORS.RESPONSE_ABORTED))
403
+ res.once('error', (e) => finish(false, e))
404
+
405
+ stream.on('data', (chunk) => {
406
+ if (prealloc) {
407
+ chunk.copy(prealloc, size)
408
+ size += chunk.length
409
+ } else {
410
+ size += chunk.length
411
+ if (size > MAX_RESPONSE_SIZE)
412
+ return finish(false, ERRORS.RESPONSE_TOO_LARGE)
413
+ chunks.push(chunk)
414
+ }
415
+ })
416
+
417
+ stream.once('end', () => {
418
+ if (size === 0) return finish(true, null)
419
+ handleResponse(
420
+ prealloc
421
+ ? prealloc.slice(0, size)
422
+ : chunks.length === 1
423
+ ? chunks[0]
424
+ : Buffer.concat(chunks, size)
425
+ )
426
+ })
427
+ }
428
+ )
429
+
430
+ req.once('error', (e) => finish(false, e))
431
+ timer = setTimeout(
432
+ () => finish(false, new Error(`Request timeout: ${this.timeout}ms`)),
433
+ this.timeout
434
+ )
435
+ unrefTimer(timer)
436
+ payload ? req.end(payload) : req.end()
437
+ })
438
+ }
439
+
440
+ _getH2Session() {
441
+ if (!this._h2 || this._h2.closed || this._h2.destroyed) {
442
+ this._clearH2()
443
+ this._h2 = http2.connect(this.baseUrl, this._tlsOptions || undefined)
444
+ this._resetH2Timer()
445
+
446
+ const onEnd = () => this._clearH2()
447
+ this._h2.once('error', onEnd)
448
+ this._h2.once('close', onEnd)
449
+ this._h2.socket?.unref?.()
450
+ }
451
+ return this._h2
452
+ }
453
+
454
+ _resetH2Timer() {
455
+ if (this._h2Timer) {
456
+ clearTimeout(this._h2Timer)
457
+ this._h2Timer = null
458
+ }
459
+ if (this._h2 && !this._h2.closed && !this._h2.destroyed) {
460
+ this._h2Timer = setTimeout(() => this._closeH2(), H2_TIMEOUT)
461
+ unrefTimer(this._h2Timer)
462
+ }
463
+ }
464
+
465
+ _clearH2() {
466
+ if (this._h2Timer) {
467
+ clearTimeout(this._h2Timer)
468
+ this._h2Timer = null
469
+ }
470
+ this._h2 = null
471
+ }
472
+
473
+ _closeH2() {
474
+ if (this._h2Timer) {
475
+ clearTimeout(this._h2Timer)
476
+ this._h2Timer = null
477
+ }
478
+ if (this._h2) {
479
+ try {
480
+ this._h2.close()
481
+ } catch {}
482
+ this._h2 = null
483
+ }
484
+ }
485
+
486
+ _h2Request(method, path, headers, payload) {
487
+ const session = this._getH2Session()
488
+
489
+ return new Promise((resolve, reject) => {
490
+ let req,
491
+ timer,
492
+ done = false
493
+ let chunks = null,
494
+ size = 0,
495
+ prealloc = null
496
+
497
+ const finish = (ok, val) => {
498
+ if (done) return
499
+ done = true
500
+ if (timer) {
501
+ clearTimeout(timer)
502
+ timer = null
503
+ }
504
+ chunks = prealloc = null
505
+ if (req && !ok) req.close(http2.constants.NGHTTP2_CANCEL)
506
+ ok ? resolve(val) : reject(val)
507
+ }
508
+
509
+ const h2h = {
510
+ ':method': method,
511
+ ':path': path,
512
+ Authorization: headers.Authorization,
513
+ Accept: headers.Accept,
514
+ 'Accept-Encoding': headers['Accept-Encoding'],
515
+ 'User-Agent': headers['User-Agent']
516
+ }
517
+ if (headers['Content-Type']) h2h['Content-Type'] = headers['Content-Type']
518
+ if (headers['Content-Length'])
519
+ h2h['Content-Length'] = headers['Content-Length']
520
+
521
+ req = session.request(h2h)
522
+ this._resetH2Timer()
523
+
524
+ req.once('response', (rh) => {
525
+ if (timer) {
526
+ clearTimeout(timer)
527
+ timer = null
528
+ }
529
+
530
+ const status = rh[':status'] || 0
531
+ const cl = rh['content-length']
532
+ const contentType = rh['content-type'] || ''
533
+
534
+ if (status === 204 || cl === '0') {
535
+ req.resume()
536
+ return finish(true, null)
537
+ }
538
+
539
+ const clInt = cl ? parseInt(cl, 10) : 0
540
+ if (clInt > MAX_RESPONSE_SIZE) {
541
+ req.resume()
542
+ return finish(false, ERRORS.RESPONSE_TOO_LARGE)
543
+ }
544
+
545
+ const encoding = _functions.getEncodingType(rh['content-encoding'])
546
+
547
+ if (
548
+ encoding === ENCODING_NONE &&
549
+ clInt > 0 &&
550
+ clInt <= MAX_RESPONSE_SIZE
551
+ ) {
552
+ prealloc = Buffer.allocUnsafe(clInt)
553
+ } else {
554
+ chunks = []
555
+ }
556
+
557
+ const decomp =
558
+ encoding !== ENCODING_NONE
559
+ ? _functions.createDecompressor(encoding)
560
+ : null
561
+ const stream = decomp ? req.pipe(decomp) : req
562
+
563
+ if (decomp) decomp.once('error', (e) => finish(false, e))
564
+ req.once('error', (e) => finish(false, e))
565
+
566
+ stream.on('data', (chunk) => {
567
+ if (prealloc) {
568
+ chunk.copy(prealloc, size)
569
+ size += chunk.length
570
+ } else {
571
+ size += chunk.length
572
+ if (size > MAX_RESPONSE_SIZE)
573
+ return finish(false, ERRORS.RESPONSE_TOO_LARGE)
574
+ chunks.push(chunk)
575
+ }
576
+ })
577
+
578
+ stream.once('end', () => {
579
+ if (size === 0) return finish(true, null)
580
+
581
+ const buffer = prealloc
582
+ ? prealloc.slice(0, size)
583
+ : chunks.length === 1
584
+ ? chunks[0]
585
+ : Buffer.concat(chunks, size)
586
+
587
+ try {
588
+ const result = _functions.parseBody(buffer, contentType, false)
589
+ if (status >= 400) {
590
+ finish(
591
+ false,
592
+ _functions.createHttpError(
593
+ status,
594
+ method,
595
+ this.baseUrl + path,
596
+ rh,
597
+ result
598
+ )
599
+ )
600
+ } else {
601
+ finish(true, result)
602
+ }
603
+ } catch (e) {
604
+ finish(false, new Error(`JSON parse error: ${e.message}`))
605
+ }
606
+ })
607
+ })
608
+
609
+ timer = setTimeout(
610
+ () => finish(false, new Error(`Request timeout: ${this.timeout}ms`)),
611
+ this.timeout
612
+ )
613
+ unrefTimer(timer)
614
+ payload ? req.end(payload) : req.end()
615
+ })
616
+ }
617
+
618
+ async updatePlayer({ guildId, data, noReplace = false }) {
619
+ return this.makeRequest(
620
+ 'PATCH',
621
+ `${this._getSessionPath()}/players/${guildId}?noReplace=${noReplace}`,
622
+ data
623
+ )
624
+ }
625
+
626
+ async getPlayer(guildId) {
627
+ return this.makeRequest(
628
+ 'GET',
629
+ `${this._getSessionPath()}/players/${guildId}`
630
+ )
631
+ }
632
+
633
+ async getPlayers() {
634
+ return this.makeRequest('GET', `${this._getSessionPath()}/players`)
635
+ }
636
+
637
+ async destroyPlayer(guildId) {
638
+ return this.makeRequest(
639
+ 'DELETE',
640
+ `${this._getSessionPath()}/players/${guildId}`
641
+ )
642
+ }
643
+
644
+ async loadTracks(identifier) {
645
+ return this.makeRequest(
646
+ 'GET',
647
+ `${this._endpoints.loadtracks}${encodeURIComponent(identifier)}`
648
+ )
649
+ }
650
+
651
+ async decodeTrack(encodedTrack) {
652
+ if (!_functions.isValidBase64(encodedTrack)) throw ERRORS.INVALID_TRACK
653
+ return this.makeRequest(
654
+ 'GET',
655
+ `${this._endpoints.decodetrack}${encodeURIComponent(encodedTrack)}`
656
+ )
657
+ }
658
+
659
+ async decodeTracks(encodedTracks) {
660
+ if (!Array.isArray(encodedTracks) || !encodedTracks.length)
661
+ throw ERRORS.INVALID_TRACKS
662
+ for (let i = 0; i < encodedTracks.length; i++) {
663
+ if (!_functions.isValidBase64(encodedTracks[i]))
664
+ throw ERRORS.INVALID_TRACKS
665
+ }
666
+ return this.makeRequest('POST', this._endpoints.decodetracks, encodedTracks)
667
+ }
668
+
669
+ async getStats() {
670
+ return this.makeRequest('GET', this._endpoints.stats)
671
+ }
672
+
673
+ async getInfo() {
674
+ return this.makeRequest('GET', this._endpoints.info)
675
+ }
676
+
677
+ async getVersion() {
678
+ return this.makeRequest('GET', this._endpoints.version)
679
+ }
680
+
681
+ async getRoutePlannerStatus() {
682
+ return this.makeRequest('GET', this._endpoints.routeplanner.status)
683
+ }
684
+
685
+ async freeRoutePlannerAddress(address) {
686
+ return this.makeRequest('POST', this._endpoints.routeplanner.freeAddress, {
687
+ address
688
+ })
689
+ }
690
+
691
+ async freeAllRoutePlannerAddresses() {
692
+ return this.makeRequest('POST', this._endpoints.routeplanner.freeAll)
693
+ }
694
+
695
+ async getLyrics({ track, skipTrackSource = false }) {
696
+ const guildId = track?.guild_id ?? track?.guildId
697
+ const encoded = track?.encoded
698
+ const hasEncoded =
699
+ typeof encoded === 'string' &&
700
+ encoded.length > 0 &&
701
+ _functions.isValidBase64(encoded)
702
+ const title = track?.info?.title
703
+
704
+ if (!track || (!guildId && !hasEncoded && !title)) {
705
+ this.aqua?.emit?.('error', '[Aqua/Lyrics] Invalid track object')
706
+ return null
707
+ }
708
+
709
+ const skip = skipTrackSource ? 'true' : 'false'
710
+
711
+ if (guildId) {
712
+ try {
713
+ const lyrics = await this.makeRequest(
714
+ 'GET',
715
+ `${this._getSessionPath()}/players/${guildId}/track/lyrics?skipTrackSource=${skip}`
716
+ )
717
+ if (this._validLyrics(lyrics)) return lyrics
718
+ } catch {}
719
+ }
720
+
721
+ if (hasEncoded) {
722
+ try {
723
+ const lyrics = await this.makeRequest(
724
+ 'GET',
725
+ `${this._endpoints.lyrics}?track=${encodeURIComponent(encoded)}&skipTrackSource=${skip}`
726
+ )
727
+ if (this._validLyrics(lyrics)) return lyrics
728
+ } catch {}
729
+ }
730
+
731
+ if (title) {
732
+ const info = track.info || {}
733
+ const query = info.author ? `${title} ${info.author}` : title
734
+ try {
735
+ const lyrics = await this.makeRequest(
736
+ 'GET',
737
+ `${this._endpoints.lyrics}/search?query=${encodeURIComponent(query)}`
738
+ )
739
+ if (this._validLyrics(lyrics)) return lyrics
740
+ } catch {}
741
+ }
742
+
743
+ return null
744
+ }
745
+
746
+ _validLyrics(r) {
747
+ if (!r) return false
748
+ if (typeof r === 'string') return r.length > 0
749
+ if (typeof r === 'object')
750
+ return Array.isArray(r) ? r.length > 0 : Object.keys(r).length > 0
751
+ return false
752
+ }
753
+
754
+ async subscribeLiveLyrics(guildId, skipTrackSource = false) {
755
+ try {
756
+ return (
757
+ (await this.makeRequest(
758
+ 'POST',
759
+ `${this._getSessionPath()}/players/${guildId}/lyrics/subscribe?skipTrackSource=${skipTrackSource ? 'true' : 'false'}`
760
+ )) === null
761
+ )
762
+ } catch {
763
+ return false
764
+ }
765
+ }
766
+
767
+ async unsubscribeLiveLyrics(guildId) {
768
+ try {
769
+ return (
770
+ (await this.makeRequest(
771
+ 'DELETE',
772
+ `${this._getSessionPath()}/players/${guildId}/lyrics/subscribe`
773
+ )) === null
774
+ )
775
+ } catch {
776
+ return false
777
+ }
778
+ }
779
+
780
+ async addMixer(guildId, options) {
781
+ if (!this.node.isNodelink)
782
+ throw new Error('Mixer endpoints are only available on Nodelink nodes')
783
+ if (!options?.encoded && !options?.identifier)
784
+ throw new Error('You must provide either encoded or identifier')
785
+
786
+ const track = {}
787
+ if (options.encoded) track.encoded = options.encoded
788
+ if (options.identifier) track.identifier = options.identifier
789
+ if (options.userData) track.userData = options.userData
790
+
791
+ const payload = {
792
+ track,
793
+ volume: options.volume !== undefined ? options.volume : 0.8
794
+ }
795
+
796
+ return this.makeRequest(
797
+ 'POST',
798
+ `/v4/sessions/${this.sessionId}/players/${guildId}/mix`,
799
+ payload
800
+ )
801
+ }
802
+
803
+ async getActiveMixer(guildId) {
804
+ if (!this.node.isNodelink)
805
+ throw new Error('Mixer endpoints are only available on Nodelink nodes')
806
+ const response = await this.makeRequest(
807
+ 'GET',
808
+ `/v4/sessions/${this.sessionId}/players/${guildId}/mix`
809
+ )
810
+ return response?.mixes || []
811
+ }
812
+
813
+ async updateMixerVolume(guildId, mix, volume) {
814
+ if (!this.node.isNodelink)
815
+ throw new Error('Mixer endpoints are only available on Nodelink nodes')
816
+ if (!guildId || !mix || typeof volume !== 'number')
817
+ throw new Error('You forget to set the guild_id, mix or volume options')
818
+
819
+ return this.makeRequest(
820
+ 'PATCH',
821
+ `/v4/sessions/${this.sessionId}/players/${guildId}/mix/${mix}`,
822
+ { volume }
823
+ )
824
+ }
825
+
826
+ async removeMixer(guildId, mix) {
827
+ if (!this.node.isNodelink)
828
+ throw new Error('Mixer endpoints are only available on Nodelink nodes')
829
+ if (!guildId || !mix)
830
+ throw new Error('You forget to set the guild_id and/or mix options')
831
+
832
+ return this.makeRequest(
833
+ 'DELETE',
834
+ `/v4/sessions/${this.sessionId}/players/${guildId}/mix/${mix}`
835
+ )
836
+ }
837
+
838
+ destroy() {
839
+ if (this.agent) {
840
+ this.agent.destroy()
841
+ this.agent = null
842
+ }
843
+ this._closeH2()
844
+ if (this._headerPool) {
845
+ this._headerPool.length = 0
846
+ this._headerPool = null
847
+ }
848
+ this.aqua =
849
+ this.node =
850
+ this.request =
851
+ this.defaultHeaders =
852
+ this._endpoints =
853
+ null
854
+ }
855
+ }
856
+
646
857
  module.exports = Rest