Shadowfilebot/nodelink
0
1import http from 'node:http'2import https from 'node:https'3import http2 from 'node:http2'4import zlib from 'node:zlib'5import process from 'node:process'6import { Buffer } from 'node:buffer'7import { URL } from 'node:url'8import { PassThrough } from 'node:stream'9 10import config from '../config.js'11import constants from '../constants.js'12 13export function randomLetters(size) {14 let result = ''15 const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'16 17 let counter = 018 while (counter < size) {19 result += characters.charAt(Math.floor(Math.random() * characters.length))20 counter++21 }22 23 return result24}25 26function _http1Events(request, headers, statusCode) {27 return new Promise((resolve) => {28 let data = ''29 30 request.setEncoding('utf8')31 request.on('data', (chunk) => data += chunk)32 request.on('end', () => {33 resolve({34 statusCode: statusCode,35 headers: headers,36 body: (headers && headers['content-type'] && headers['content-type'].startsWith('application/json')) ? JSON.parse(data) : data37 })38 })39 })40}41 42export function http1makeRequest(url, options) { 43 return new Promise(async (resolve, reject) => {44 let compression = null45 46 let req = (url.startsWith('https') ? https : http).request(url, {47 method: options.method,48 headers: {49 'Accept-Encoding': 'br, gzip, deflate',50 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/111.0',51 'DNT': '1',52 ...(options.headers || {}),53 ...(options.body ? { 'Content-Type': 'application/json' } : {})54 }55 }, async (res) => {56 const statusCode = res.statusCode57 const headers = res.headers58 59 if (headers.location) {60 resolve(http1makeRequest(headers.location, options))61 62 return res.destroy()63 }64 65 switch (res.headers['content-encoding']) {66 case 'deflate': {67 compression = zlib.createInflate()68 break69 }70 case 'br': {71 compression = zlib.createBrotliDecompress()72 break73 }74 case 'gzip': {75 compression = zlib.createGunzip()76 break77 }78 }79 80 if (compression) {81 res.pipe(compression)82 83 if (options.streamOnly) {84 return resolve({85 statusCode,86 headers,87 stream: compression88 })89 }90 91 resolve(await _http1Events(compression, headers, statusCode))92 } else {93 if (options.streamOnly) {94 return resolve({95 statusCode,96 headers,97 stream: res98 })99 }100 101 resolve(await _http1Events(res, headers, statusCode))102 }103 })104 105 if (options.body) {106 if (options.disableBodyCompression || process.versions.deno)107 req.end(JSON.stringify(options.body))108 else zlib.gzip(JSON.stringify(options.body), (error, data) => {109 if (error) throw new Error(`\u001b[31mhttp1makeRequest\u001b[37m]: Failed gziping body: ${error}`)110 req.end(data)111 })112 } else req.end()113 114 req.on('error', (error) => {115 console.error(`[\u001b[31mhttp1makeRequest\u001b[37m]: Failed sending HTTP request to ${url}: \u001b[31m${error}\u001b[37m`)116 117 reject(error)118 })119 })120}121 122function _http2Events(request, headers) {123 return new Promise((resolve) => {124 let data = ''125 126 request.setEncoding('utf8')127 request.on('data', (chunk) => data += chunk)128 request.on('end', () => {129 resolve({130 statusCode: headers[':status'],131 headers: headers,132 body: (headers && headers['content-type'] && headers['content-type'].startsWith('application/json')) ? JSON.parse(data) : data133 })134 })135 })136}137 138export function makeRequest(url, options) {139 if (process.versions.deno) return http1makeRequest(url, options)140 141 return new Promise(async (resolve) => {142 const parsedUrl = new URL(url)143 let compression = null144 145 const client = http2.connect(parsedUrl.origin)146 147 let reqOptions = {148 ':method': options.method,149 ':path': parsedUrl.pathname + parsedUrl.search,150 'Accept-Encoding': 'br, gzip, deflate',151 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/111.0',152 'DNT': '1',153 ...(options.headers || {})154 }155 156 if (options.body) {157 if (!options.disableBodyCompression) reqOptions['Content-Encoding'] = 'gzip'158 159 reqOptions['Content-Type'] = 'application/json'160 }161 162 let req = client.request(reqOptions)163 164 client.on('error', () => { /* Add listener or else will crash */ })165 166 req.on('error', (error) => {167 console.error(`[\u001b[31mmakeRequest\u001b[37m]: Failed sending HTTP request to ${url}: \u001b[31m${error}\u001b[37m`)168 169 resolve({ error })170 })171 172 req.on('response', async (headers) => {173 if (headers.location) {174 client.close()175 req.destroy()176 177 return resolve(makeRequest(headers.location, options))178 }179 180 switch (headers['content-encoding']) {181 case 'deflate': {182 compression = zlib.createInflate()183 break184 }185 case 'br': {186 compression = zlib.createBrotliDecompress()187 break188 }189 case 'gzip': {190 compression = zlib.createGunzip()191 break192 }193 }194 195 if (compression) {196 req.pipe(compression)197 198 if (options.streamOnly) {199 req.on('end', () => client.close())200 201 return resolve({202 statusCode: headers[':status'],203 headers: headers,204 stream: compression205 })206 }207 208 compression.on('error', (error) => {209 console.error(`[\u001b[31mmakeRequest\u001b[37m]: Failed decompressing HTTP response: \u001b[31m${error}\u001b[37m`)210 211 resolve({ error })212 })213 214 resolve(await _http2Events(compression, headers))215 216 client.close()217 } else {218 if (options.streamOnly) {219 req.on('end', () => client.close())220 221 return resolve({222 statusCode: headers[':status'],223 headers: headers,224 stream: req225 })226 }227 228 resolve(await _http2Events(req, headers))229 230 client.close()231 }232 })233 234 if (options.body) {235 if (options.disableBodyCompression)236 req.end(JSON.stringify(options.body))237 else zlib.gzip(JSON.stringify(options.body), (error, data) => {238 if (error) throw new Error(`\u001b[31mmakeRequest\u001b[37m]: Failed gziping body: ${error}`)239 req.end(data)240 })241 } else req.end()242 })243}244 245class EncodeClass {246 constructor() {247 this.position = 0248 this.buffer = Buffer.alloc(512)249 }250 251 changeBytes(bytes) {252 if (this.position + bytes > this.buffer.length) {253 const newBuffer = Buffer.alloc(Math.max(this.buffer.length * 2, this.position + bytes))254 this.buffer.copy(newBuffer)255 this.buffer = newBuffer256 }257 this.position += bytes258 return this.position - bytes259 }260 261 write(type, value) {262 switch (type) {263 case 'byte': {264 this.buffer[this.changeBytes(1)] = value265 break266 }267 case 'unsignedShort': {268 this.buffer.writeUInt16BE(value, this.changeBytes(2))269 break270 }271 case 'int': {272 this.buffer.writeInt32BE(value, this.changeBytes(4))273 break274 }275 case 'long': {276 const msb = value / BigInt(2 ** 32)277 const lsb = value % BigInt(2 ** 32)278 279 this.write('int', Number(msb))280 this.write('int', Number(lsb))281 break282 }283 case 'utf': {284 const len = Buffer.byteLength(value, 'utf8')285 this.write('unsignedShort', len)286 const start = this.changeBytes(len)287 this.buffer.write(value, start, len, 'utf8')288 break289 }290 }291 }292 293 result() {294 return this.buffer.subarray(0, this.position)295 }296}297 298export function encodeTrack(obj) {299 try {300 const buf = new EncodeClass()301 302 buf.write('byte', 3)303 buf.write('utf', obj.title)304 buf.write('utf', obj.author)305 buf.write('long', BigInt(obj.length))306 buf.write('utf', obj.identifier)307 buf.write('byte', obj.isStream ? 1 : 0)308 buf.write('byte', obj.uri ? 1 : 0)309 if (obj.uri) buf.write('utf', obj.uri)310 buf.write('byte', obj.artworkUrl ? 1 : 0)311 if (obj.artworkUrl) buf.write('utf', obj.artworkUrl)312 buf.write('byte', obj.isrc ? 1 : 0)313 if (obj.isrc) buf.write('utf', obj.isrc)314 buf.write('utf', obj.sourceName)315 buf.write('long', BigInt(obj.position))316 317 const buffer = buf.result()318 const result = Buffer.alloc(buffer.length + 4)319 320 result.writeInt32BE(buffer.length | (1 << 30))321 buffer.copy(result, 4)322 323 return result.toString('base64')324 } catch {325 return null326 }327}328 329class DecodeClass {330 constructor(buffer) {331 this.position = 0332 this.buffer = buffer333 }334 335 changeBytes(bytes) {336 this.position += bytes337 return this.position - bytes338 }339 340 read(type) {341 switch (type) {342 case 'byte': {343 return this.buffer[this.changeBytes(1)]344 }345 case 'unsignedShort': {346 const result = this.buffer.readUInt16BE(this.changeBytes(2))347 return result348 }349 case 'int': {350 const result = this.buffer.readInt32BE(this.changeBytes(4))351 return result352 }353 case 'long': {354 const msb = BigInt(this.read('int'))355 const lsb = BigInt(this.read('int'))356 357 return msb * BigInt(2 ** 32) + lsb358 }359 case 'utf': {360 const len = this.read('unsignedShort')361 const start = this.changeBytes(len)362 const result = this.buffer.toString('utf8', start, start + len)363 return result364 }365 }366 }367}368 369export function decodeTrack(track) {370 try {371 const buf = new DecodeClass(Buffer.from(track, 'base64'))372 373 const version = ((buf.read('int') & 0xC0000000) >> 30 & 1) !== 0 ? buf.read('byte') : 1374 375 switch (version) {376 case 1: {377 return {378 title: buf.read('utf'),379 author: buf.read('utf'),380 length: Number(buf.read('long')),381 identifier: buf.read('utf'),382 isStream: buf.read('byte') === 1,383 uri: null,384 source: buf.read('utf'),385 position: Number(buf.read('long'))386 }387 }388 case 2: {389 return {390 title: buf.read('utf'),391 author: buf.read('utf'),392 length: Number(buf.read('long')),393 identifier: buf.read('utf'),394 isStream: buf.read('byte') === 1,395 uri: buf.read('byte') === 1 ? buf.read('utf') : null,396 source: buf.read('utf'),397 position: Number(buf.read('long'))398 }399 }400 case 3: {401 return {402 title: buf.read('utf'),403 author: buf.read('utf'),404 length: Number(buf.read('long')),405 identifier: buf.read('utf'),406 isSeekable: true,407 isStream: buf.read('byte') === 1,408 uri: buf.read('byte') === 1 ? buf.read('utf') : null,409 artworkUrl: buf.read('byte') === 1 ? buf.read('utf') : null,410 isrc: buf.read('byte') === 1 ? buf.read('utf') : null,411 sourceName: buf.read('utf'),412 position: Number(buf.read('long'))413 }414 }415 }416 } catch {417 return null418 }419}420 421export function debugLog(name, type, options) {422 switch (type) {423 case 1: {424 if (!config.debug.request.enabled) return;425 426 if (options.headers) {427 options.headers.authorization = 'REDACTED'428 options.headers.host = 'REDACTED'429 }430 431 if (options.error)432 console.error(`[\u001b[32m${name}\u001b[37m]: Detected an error in a request: \u001b[31m${options.error}\u001b[37m${config.debug.request.showParams && options.params ? `\n Params: ${JSON.stringify(options.params)}` : ''}${config.debug.request.showHeaders && options.headers ? `\n Headers: ${JSON.stringify(options.headers)}` : ''}${config.debug.request.showBody && options.body ? `\n Body: ${JSON.stringify(options.body)}` : ''}`)433 else434 console.log(`[\u001b[32m${name}\u001b[37m]: Received a request from client.${config.debug.request.showParams && options.params ? `\n Params: ${JSON.stringify(options.params)}` : ''}${config.debug.request.showHeaders && options.headers ? `\n Headers: ${JSON.stringify(options.headers)}` : ''}${config.debug.request.showBody && options.body ? `\n Body: ${JSON.stringify(options.body)}` : ''}`)435 436 break437 }438 case 2: {439 switch (name) {440 case 'trackStart': {441 if (!config.debug.track.start) return;442 443 console.log(`[\u001b[32mtrackStart\u001b[37m]: \u001b[94m${options.track.title}\u001b[37m by \u001b[94m${options.track.author}\u001b[37m.`)444 445 break446 }447 case 'trackEnd': {448 if (!config.debug.track.end) return;449 450 console.log(`[\u001b[32mtrackEnd\u001b[37m]: \u001b[94m${options.track.title}\u001b[37m by \u001b[94m${options.track.author}\u001b[37m because was \u001b[94m${options.reason}\u001b[37m.`)451 452 break453 }454 case 'trackException': {455 if (!config.debug.track.exception) return;456 457 console.error(`[\u001b[31mtrackException\u001b[37m]: \u001b[94m${options.track?.title || 'None'}\u001b[37m by \u001b[94m${options.track?.author || 'none'}\u001b[37m: \u001b[31m${options.exception}\u001b[37m`)458 459 break460 }461 case 'trackStuck': {462 if (!config.debug.track.stuck) return;463 464 console.warn(`[\u001b[33mtrackStuck\u001b[37m]: \u001b[94m${options.track.title}\u001b[37m by \u001b[94m${options.track.author}\u001b[37m: \u001b[33m${config.options.threshold}ms have passed.\u001b[37m`)465 466 break467 }468 }469 470 break471 }472 case 3: {473 switch (name) {474 case 'connect': {475 if (!config.debug.websocket.connect) return;476 477 if (options.error)478 return console.error(`[\u001b[31mwebsocket\u001b[37m]: \u001b[31m${options.error}\u001b[37m\n Name: \u001b[94m${options.name}\u001b[37m`)479 480 console.log(`[\u001b[32mwebsocket\u001b[37m]: \u001b[94m${options.name}\u001b[37m@\u001b[94m${options.version}\u001b[37m client connected to NodeLink.`)481 482 break483 }484 case 'disconnect': {485 if (!config.debug.websocket.disconnect) return;486 487 console.error(`[\u001b[33mwebsocket\u001b[37m]: A connection was closed with a client.\n Code: \u001b[33m${options.code}\u001b[37m\n Reason: \u001b[33m${options.reason === '' ? 'No reason provided' : options.reason}\u001b[37m`)488 489 break490 }491 case 'error': {492 if (!config.debug.websocket.error) return;493 494 console.error(`[\u001b[31mwebsocketError\u001b[37m]: \u001b[94m${options.name}\u001b[37m@\u001b[94m${options.version}\u001b[37m ran into an error: \u001b[31m${options.error}\u001b[37m`)495 496 break497 }498 case 'connectCD': {499 if (!config.debug.websocket.connectCD) return;500 501 console.log(`[\u001b[32mwebsocketCD\u001b[37m]: \u001b[94m${options.name}\u001b[37m@\u001b[94m${options.version}\u001b[37m client connected to NodeLink.\n Guild: \u001b[94m${options.guildId}\u001b[37m`)502 503 break504 }505 case 'disconnectCD': {506 if (!config.debug.websocket.disconnectCD) return;507 508 console.error(`[\u001b[32mwebsocketCD\u001b[37m]: Connection with \u001b[94m${options.name}\u001b[37m@\u001b[94m${options.version}\u001b[37m was closed.\n Guild: \u001b[94m${options.guildId}\u001b[37m\n Code: \u001b[33m${options.code}\u001b[37m\n Reason: \u001b[33m${options.reason === '' ? 'No reason provided' : options.reason}\u001b[37m`)509 510 break511 }512 case 'sentDataCD': {513 if (!config.debug.websocket.sentDataCD) return;514 515 console.log(`[\u001b[32msentData\u001b[37m]: Sent data to \u001b[94m${options.clientsAmount}\u001b[37m clients.\n Guild: \u001b[94m${options.guildId}\u001b[37m`)516 517 break518 }519 default: {520 if (!config.debug.request.error) return;521 522 console.error(`[\u001b[31m${name}\u001b[37m]: \u001b[31m${options.error}\u001b[37m${config.debug.request.showParams && options.params ? `\n Params: ${JSON.stringify(options.params)}` : ''}${config.debug.request.showHeaders && options.headers ? `\n Headers: ${JSON.stringify(options.headers)}` : ''}${config.debug.request.showBody && options.body ? `\n Body: ${JSON.stringify(options.body)}` : ''}`)523 524 break525 }526 }527 528 break529 }530 case 4: {531 switch (name) {532 case 'loadtracks': {533 if (options.type === 1 && config.debug.sources.loadtrack.request)534 console.log(`[\u001b[32mloadTracks\u001b[37m]: Loading \u001b[94m${options.loadType}\u001b[37m from ${options.sourceName}: ${options.query}`)535 536 if (options.type === 2 && config.debug.sources.loadtrack.results) {537 if (options.loadType !== 'search' && options.loadType !== 'track')538 console.log(`[\u001b[32mloadTracks\u001b[37m]: Loaded \u001b[94m${options.playlistName}\u001b[37m from \u001b[94m${options.sourceName}\u001b[37m.`)539 else540 console.log(`[\u001b[32mloadTracks\u001b[37m]: Loaded \u001b[94m${options.track.title}\u001b[37m by \u001b[94m${options.track.author}\u001b[37m from \u001b[94m${options.sourceName}\u001b[37m: ${options.query}`)541 }542 543 if (options.type === 3 && config.debug.sources.loadtrack.exception)544 console.error(`[\u001b[31mloadTracks\u001b[37m]: Exception loading \u001b[94m${options.loadType}\u001b[37m from \u001b[94m${options.sourceName}\u001b[37m: \u001b[31m${options.message}\u001b[37m`)545 546 break547 }548 case 'search': {549 if (options.type === 1 && config.debug.sources.search.request)550 console.log(`[\u001b[32msearch\u001b[37m]: Searching for \u001b[94m${options.query}\u001b[37m on \u001b[94m${options.sourceName}\u001b[37m`)551 552 if (options.type === 2 && config.debug.sources.search.results)553 console.log(`[\u001b[32msearch\u001b[37m]: Found \u001b[94m${options.tracksLen}\u001b[37m tracks on \u001b[94m${options.sourceName}\u001b[37m for query \u001b[94m${options.query}\u001b[37m`)554 555 if (options.type === 3 && config.debug.sources.search.exception)556 console.error(`[\u001b[31msearch\u001b[37m]: Exception from ${options.sourceName} for query \u001b[94m${options.query}\u001b[37m: \u001b[31m${options.message}\u001b[37m`)557 558 break559 }560 case 'retrieveStream': {561 if (!config.debug.sources.retrieveStream) return;562 563 if (options.type === 1)564 console.log(`[\u001b[32mretrieveStream\u001b[37m]: Retrieved from \u001b[94m${options.sourceName}\u001b[37m for query \u001b[94m${options.query}\u001b[37m`)565 566 if (options.type === 2)567 console.error(`[\u001b[31mretrieveStream\u001b[37m]: Exception from \u001b[94m${options.sourceName}\u001b[37m for query \u001b[94m${options.query}\u001b[37m: \u001b[31m${options.message}\u001b[37m`)568 569 break570 }571 case 'loadlyrics': {572 if (options.type === 1 && config.debug.sources.loadlyrics.request)573 console.log(`[\u001b[32mloadCaptions\u001b[37m]: Loading captions for \u001b[94m${options.track.title}\u001b[37m by \u001b[94m${options.track.author}\u001b[37m from \u001b[94m${options.sourceName}\u001b[37m`)574 575 if (options.type === 2 && config.debug.sources.loadlyrics.results)576 console.log(`[\u001b[32mloadCaptions\u001b[37m]: Loaded captions for \u001b[94m${options.track.title}\u001b[37m by \u001b[94m${options.track.author}\u001b[37m from \u001b[94m${options.sourceName}\u001b[37m`)577 578 if (options.type === 3 && config.debug.sources.loadlyrics.exception)579 console.error(`[\u001b[31mloadCaptions\u001b[37m]: Exception loading captions for \u001b[94m${options.track.title}\u001b[37m by \u001b[94m${options.track.author}\u001b[37m from \u001b[94m${options.sourceName}\u001b[37m: \u001b[31m${options.message}\u001b[37m`)580 581 break582 }583 }584 585 break586 }587 case 5: {588 switch (name) {589 case 'youtube': {590 if (options.type === 1 && config.debug.youtube.success)591 console.log(`[\u001b[32myoutube\u001b[37m]: ${options.message}`)592 593 if (options.type === 2 && config.debug.youtube.error)594 console.error(`[\u001b[31myoutube\u001b[37m]: ${options.message}`)595 596 break597 }598 599 case 'pandora': {600 if (options.type === 1 && config.debug.pandora.success)601 console.log(`[\u001b[32mpandora\u001b[37m]: ${options.message}`)602 603 if (options.type === 2 && config.debug.pandora.error)604 console.error(`[\u001b[31mpandora\u001b[37m]: ${options.message}`)605 606 break607 }608 case 'deezer': {609 if (options.type === 1 && config.debug.deezer.success)610 console.log(`[\u001b[32mdeezer\u001b[37m]: ${options.message}`)611 612 if (options.type === 2 && config.debug.deezer.error)613 console.error(`[\u001b[31mdeezer\u001b[37m]: ${options.message}`)614 615 break616 }617 case 'spotify': {618 if (options.type === 1 && config.debug.spotify.success)619 console.log(`[\u001b[32mspotify\u001b[37m]: ${options.message}`)620 621 if (options.type === 2 && config.debug.spotify.error)622 console.error(`[\u001b[31mspotify\u001b[37m]: ${options.message}`)623 624 break625 }626 case 'soundcloud': {627 if (options.type === 1 && config.debug.soundcloud.success)628 console.log(`[\u001b[32msoundcloud\u001b[37m]: ${options.message}`)629 630 if (options.type === 2 && config.debug.soundcloud.error)631 console.error(`[\u001b[31msoundcloud\u001b[37m]: ${options.message}`)632 633 break634 }635 case 'musixmatch': {636 console.log(`[\u001b[32mmusixmatch\u001b[37m]: ${options.message}`)637 638 break639 }640 }641 642 break643 }644 case 6: {645 if (!config.debug.request.all) return;646 647 if (options.headers) {648 options.headers.authorization = 'REDACTED'649 options.headers.host = 'REDACTED'650 }651 652 console.log(`[\u001b[32mALL\u001b[37m]: Received a request from client.\n Path: ${options.path}${options.params ? `\n Params: ${JSON.stringify(options.params)}` : ''}${options.headers ? `\n Headers: ${JSON.stringify(options.headers)}` : ''}${options.body ? `\n Body: ${JSON.stringify(options.body)}` : ''}`)653 654 break655 }656 }657}658 659export function sendResponse(req, res, data, status) {660 if (!data) {661 res.writeHead(status)662 res.end()663 664 return true665 }666 667 if (!req.headers || !req.headers['accept-encoding']) {668 res.setHeader('Connection', 'close')669 res.writeHead(status, { 'Content-Type': 'application/json' })670 671 res.end(JSON.stringify(data))672 }673 674 if (req.headers && req.headers['accept-encoding']) {675 if (req.headers['accept-encoding'].includes('br')) {676 res.setHeader('Content-Encoding', 'br')677 res.writeHead(status, { 'Content-Type': 'application/json', 'Content-Encoding': 'br' })678 679 zlib.brotliCompress(JSON.stringify(data), (err, result) => {680 if (err) {681 res.writeHead(500)682 res.end()683 684 return;685 }686 687 res.end(result)688 })689 }690 691 else if (req.headers['accept-encoding'].includes('gzip')) {692 res.setHeader('Content-Encoding', 'gzip')693 res.writeHead(status, { 'Content-Type': 'application/json', 'Content-Encoding': 'gzip' })694 695 zlib.gzip(JSON.stringify(data), (err, result) => {696 if (err) {697 res.writeHead(500)698 res.end()699 700 return;701 }702 703 res.end(result)704 })705 }706 707 else if (req.headers['accept-encoding'].includes('deflate')) {708 res.setHeader('Content-Encoding', 'deflate')709 res.writeHead(status, { 'Content-Type': 'application/json', 'Content-Encoding': 'deflate' })710 711 zlib.deflate(JSON.stringify(data), (err, result) => {712 if (err) {713 res.writeHead(500)714 res.end()715 716 return;717 }718 719 res.end(result)720 })721 }722 }723 724 return true725}726 727export function tryParseBody(req, res) {728 return new Promise((resolve) => {729 let buffer = ''730 731 req.on('data', (chunk) => buffer += chunk)732 req.on('end', () => {733 try {734 resolve(JSON.parse(buffer))735 } catch {736 sendResponse(req, res, {737 timestamp: Date.now(),738 status: 400,739 trace: new Error().stack,740 error: 'Bad Request',741 message: 'Invalid JSON body',742 path: req.url743 }, 400)744 745 resolve(null)746 }747 })748 })749}750 751export function sendResponseNonNull(req, res, data) {752 if (data === null) return;753 754 sendResponse(req, res, data, 200)755 756 return true757}758 759export function verifyMethod(parsedUrl, req, res, expected) {760 if (req.method !== expected) {761 sendResponse(req, res, {762 timestamp: Date.now(),763 status: 405,764 error: 'Method Not Allowed',765 message: `Request method must be ${expected}`,766 path: parsedUrl.pathname767 }, 405)768 769 return 1770 }771 772 return 0773}774 775Array.prototype.nForEach = async function(callback) {776 return new Promise(async (resolve) => {777 for (let i = 0; i < this.length - 1; i++) {778 const res = await callback(this[i], i)779 780 if (res) return resolve()781 }782 783 resolve()784 })785}786 787export function waitForEvent(emitter, eventName, func, timeoutMs) {788 return new Promise((resolve) => {789 const timeout = timeoutMs ? setTimeout(() => {790 throw new Error(`Event ${eventName} timed out after ${timeoutMs}ms`)791 }, timeoutMs) : null792 793 const listener = (param, param2) => {794 if (func(param, param2) === true) {795 emitter.removeListener(eventName, listener)796 timeoutMs ? clearTimeout(timeout) : null797 resolve()798 }799 }800 emitter.on(eventName, listener)801 })802}803 804export function clamp16Bit(sample) {805 return Math.max(constants.pcm.minimumRate, Math.min(sample, constants.pcm.maximumRate))806}807 808export function parseClientName(clientName) {809 if (!clientName)810 return null811 812 let clientInfo = clientName.split('(')813 if (clientInfo.length > 1) clientInfo = clientInfo[0].slice(0, clientInfo[0].length - 1)814 else clientInfo = clientInfo[0]815 816 const split = clientInfo.split('/')817 const name = split[0]818 const version = split[1]819 820 if (!name || !version || split.length != 2) return null821 822 return { name, version }823}824 825export function isEmpty(value) {826 return value === undefined || value === null || false827}828 829export function loadHLS(url, stream, onceEnded) {830 return new Promise(async (resolve) => {831 const response = await http1makeRequest(url, { method: 'GET' })832 const body = response.body.split('\n')833 834 let segmentMetadata = {835 duration: 0836 }837 838 body.nForEach(async (line, i) => {839 return new Promise(async (resolveSegment) => {840 if (stream.ended) {841 resolveSegment(true)842 843 return resolve(false)844 }845 846 if (line.startsWith('#')) {847 const tag = line.split(':')[0]848 let value = line.split(':')[1]849 if (value) value = value.split(',')[0]850 851 if (tag === '#EXTINF') {852 segmentMetadata.duration = parseFloat(value) * 1000853 } else if (tag === '#EXT-X-ENDLIST') {854 stream.end()855 856 return resolveSegment(true)857 }858 859 return resolveSegment(false)860 }861 862 const now = Date.now()863 864 const segment = await http1makeRequest(line, { method: 'GET', streamOnly: true })865 866 segment.stream.on('data', (chunk) => stream.write(chunk))867 segment.stream.once('readable', () => {868 if (segmentMetadata.duration) {869 setTimeout(() => {870 resolveSegment(false)871 }, segmentMetadata.duration - (Date.now() - now) * 2)872 873 segmentMetadata.duration = 0874 } else {875 segment.stream.on('end', () => {876 resolveSegment(false)877 878 segment.stream.destroy()879 })880 }881 })882 883 if (onceEnded && i === body.length - 2) {884 segment.stream.on('end', () => {885 resolve(true)886 887 segment.stream.destroy()888 })889 }890 })891 })892 893 if (!onceEnded) resolve(true)894 })895}896 897export function loadHLSPlaylist(url, stream) {898 return new Promise(async (resolve) => {899 const response = await http1makeRequest(url, { method: 'GET' })900 const body = response.body.split('\n')901 902 body.nForEach(async (line, i) => {903 return new Promise(async (resolvePlaylist) => {904 if (line.startsWith('#')) {905 const tag = line.split(':')[0]906 let value = line.split(':')[1]907 if (value) value = value.split(',')[0]908 909 if (tag === '#EXT-X-ENDLIST') {910 stream.end()911 912 resolvePlaylist(true)913 914 return resolve(stream)915 }916 917 resolvePlaylist(false)918 919 if (i === body.length - 1) {920 loadHLSPlaylist(value, stream)921 922 resolve(stream)923 }924 925 return;926 }927 928 if (await loadHLS(line, stream, true) === false)929 return resolve(stream)930 931 resolvePlaylist(false)932 933 if (i === body.length - 2) {934 loadHLSPlaylist(url, stream)935 936 return resolve(stream)937 }938 })939 })940 941 resolve(stream)942 })943}