CoolFace
Apppublic

Shadowfilebot/nodelink

sourceHugging Faceupdated 2y agoView on Hugging Face
0likes
utils.js943 linesDownload Raw Back to src
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}