Spaces:
Runtime error
Runtime error
import http from 'node:http' | |
import https from 'node:https' | |
import http2 from 'node:http2' | |
import zlib from 'node:zlib' | |
import process from 'node:process' | |
import { Buffer } from 'node:buffer' | |
import { URL } from 'node:url' | |
import { PassThrough } from 'node:stream' | |
import config from '../config.js' | |
import constants from '../constants.js' | |
export function randomLetters(size) { | |
let result = '' | |
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' | |
let counter = 0 | |
while (counter < size) { | |
result += characters.charAt(Math.floor(Math.random() * characters.length)) | |
counter++ | |
} | |
return result | |
} | |
function _http1Events(request, headers, statusCode) { | |
return new Promise((resolve) => { | |
let data = '' | |
request.setEncoding('utf8') | |
request.on('data', (chunk) => data += chunk) | |
request.on('end', () => { | |
resolve({ | |
statusCode: statusCode, | |
headers: headers, | |
body: (headers && headers['content-type'] && headers['content-type'].startsWith('application/json')) ? JSON.parse(data) : data | |
}) | |
}) | |
}) | |
} | |
export function http1makeRequest(url, options) { | |
return new Promise(async (resolve, reject) => { | |
let compression = null | |
let req = (url.startsWith('https') ? https : http).request(url, { | |
method: options.method, | |
headers: { | |
'Accept-Encoding': 'br, gzip, deflate', | |
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/111.0', | |
'DNT': '1', | |
...(options.headers || {}), | |
...(options.body ? { 'Content-Type': 'application/json' } : {}) | |
} | |
}, async (res) => { | |
const statusCode = res.statusCode | |
const headers = res.headers | |
if (headers.location) { | |
resolve(http1makeRequest(headers.location, options)) | |
return res.destroy() | |
} | |
switch (res.headers['content-encoding']) { | |
case 'deflate': { | |
compression = zlib.createInflate() | |
break | |
} | |
case 'br': { | |
compression = zlib.createBrotliDecompress() | |
break | |
} | |
case 'gzip': { | |
compression = zlib.createGunzip() | |
break | |
} | |
} | |
if (compression) { | |
res.pipe(compression) | |
if (options.streamOnly) { | |
return resolve({ | |
statusCode, | |
headers, | |
stream: compression | |
}) | |
} | |
resolve(await _http1Events(compression, headers, statusCode)) | |
} else { | |
if (options.streamOnly) { | |
return resolve({ | |
statusCode, | |
headers, | |
stream: res | |
}) | |
} | |
resolve(await _http1Events(res, headers, statusCode)) | |
} | |
}) | |
if (options.body) { | |
if (options.disableBodyCompression || process.versions.deno) | |
req.end(JSON.stringify(options.body)) | |
else zlib.gzip(JSON.stringify(options.body), (error, data) => { | |
if (error) throw new Error(`\u001b[31mhttp1makeRequest\u001b[37m]: Failed gziping body: ${error}`) | |
req.end(data) | |
}) | |
} else req.end() | |
req.on('error', (error) => { | |
console.error(`[\u001b[31mhttp1makeRequest\u001b[37m]: Failed sending HTTP request to ${url}: \u001b[31m${error}\u001b[37m`) | |
reject(error) | |
}) | |
}) | |
} | |
function _http2Events(request, headers) { | |
return new Promise((resolve) => { | |
let data = '' | |
request.setEncoding('utf8') | |
request.on('data', (chunk) => data += chunk) | |
request.on('end', () => { | |
resolve({ | |
statusCode: headers[':status'], | |
headers: headers, | |
body: (headers && headers['content-type'] && headers['content-type'].startsWith('application/json')) ? JSON.parse(data) : data | |
}) | |
}) | |
}) | |
} | |
export function makeRequest(url, options) { | |
if (process.versions.deno) return http1makeRequest(url, options) | |
return new Promise(async (resolve) => { | |
const parsedUrl = new URL(url) | |
let compression = null | |
const client = http2.connect(parsedUrl.origin) | |
let reqOptions = { | |
':method': options.method, | |
':path': parsedUrl.pathname + parsedUrl.search, | |
'Accept-Encoding': 'br, gzip, deflate', | |
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/111.0', | |
'DNT': '1', | |
...(options.headers || {}) | |
} | |
if (options.body) { | |
if (!options.disableBodyCompression) reqOptions['Content-Encoding'] = 'gzip' | |
reqOptions['Content-Type'] = 'application/json' | |
} | |
let req = client.request(reqOptions) | |
client.on('error', () => { /* Add listener or else will crash */ }) | |
req.on('error', (error) => { | |
console.error(`[\u001b[31mmakeRequest\u001b[37m]: Failed sending HTTP request to ${url}: \u001b[31m${error}\u001b[37m`) | |
resolve({ error }) | |
}) | |
req.on('response', async (headers) => { | |
if (headers.location) { | |
client.close() | |
req.destroy() | |
return resolve(makeRequest(headers.location, options)) | |
} | |
switch (headers['content-encoding']) { | |
case 'deflate': { | |
compression = zlib.createInflate() | |
break | |
} | |
case 'br': { | |
compression = zlib.createBrotliDecompress() | |
break | |
} | |
case 'gzip': { | |
compression = zlib.createGunzip() | |
break | |
} | |
} | |
if (compression) { | |
req.pipe(compression) | |
if (options.streamOnly) { | |
req.on('end', () => client.close()) | |
return resolve({ | |
statusCode: headers[':status'], | |
headers: headers, | |
stream: compression | |
}) | |
} | |
compression.on('error', (error) => { | |
console.error(`[\u001b[31mmakeRequest\u001b[37m]: Failed decompressing HTTP response: \u001b[31m${error}\u001b[37m`) | |
resolve({ error }) | |
}) | |
resolve(await _http2Events(compression, headers)) | |
client.close() | |
} else { | |
if (options.streamOnly) { | |
req.on('end', () => client.close()) | |
return resolve({ | |
statusCode: headers[':status'], | |
headers: headers, | |
stream: req | |
}) | |
} | |
resolve(await _http2Events(req, headers)) | |
client.close() | |
} | |
}) | |
if (options.body) { | |
if (options.disableBodyCompression) | |
req.end(JSON.stringify(options.body)) | |
else zlib.gzip(JSON.stringify(options.body), (error, data) => { | |
if (error) throw new Error(`\u001b[31mmakeRequest\u001b[37m]: Failed gziping body: ${error}`) | |
req.end(data) | |
}) | |
} else req.end() | |
}) | |
} | |
class EncodeClass { | |
constructor() { | |
this.position = 0 | |
this.buffer = Buffer.alloc(512) | |
} | |
changeBytes(bytes) { | |
if (this.position + bytes > this.buffer.length) { | |
const newBuffer = Buffer.alloc(Math.max(this.buffer.length * 2, this.position + bytes)) | |
this.buffer.copy(newBuffer) | |
this.buffer = newBuffer | |
} | |
this.position += bytes | |
return this.position - bytes | |
} | |
write(type, value) { | |
switch (type) { | |
case 'byte': { | |
this.buffer[this.changeBytes(1)] = value | |
break | |
} | |
case 'unsignedShort': { | |
this.buffer.writeUInt16BE(value, this.changeBytes(2)) | |
break | |
} | |
case 'int': { | |
this.buffer.writeInt32BE(value, this.changeBytes(4)) | |
break | |
} | |
case 'long': { | |
const msb = value / BigInt(2 ** 32) | |
const lsb = value % BigInt(2 ** 32) | |
this.write('int', Number(msb)) | |
this.write('int', Number(lsb)) | |
break | |
} | |
case 'utf': { | |
const len = Buffer.byteLength(value, 'utf8') | |
this.write('unsignedShort', len) | |
const start = this.changeBytes(len) | |
this.buffer.write(value, start, len, 'utf8') | |
break | |
} | |
} | |
} | |
result() { | |
return this.buffer.subarray(0, this.position) | |
} | |
} | |
export function encodeTrack(obj) { | |
try { | |
const buf = new EncodeClass() | |
buf.write('byte', 3) | |
buf.write('utf', obj.title) | |
buf.write('utf', obj.author) | |
buf.write('long', BigInt(obj.length)) | |
buf.write('utf', obj.identifier) | |
buf.write('byte', obj.isStream ? 1 : 0) | |
buf.write('byte', obj.uri ? 1 : 0) | |
if (obj.uri) buf.write('utf', obj.uri) | |
buf.write('byte', obj.artworkUrl ? 1 : 0) | |
if (obj.artworkUrl) buf.write('utf', obj.artworkUrl) | |
buf.write('byte', obj.isrc ? 1 : 0) | |
if (obj.isrc) buf.write('utf', obj.isrc) | |
buf.write('utf', obj.sourceName) | |
buf.write('long', BigInt(obj.position)) | |
const buffer = buf.result() | |
const result = Buffer.alloc(buffer.length + 4) | |
result.writeInt32BE(buffer.length | (1 << 30)) | |
buffer.copy(result, 4) | |
return result.toString('base64') | |
} catch { | |
return null | |
} | |
} | |
class DecodeClass { | |
constructor(buffer) { | |
this.position = 0 | |
this.buffer = buffer | |
} | |
changeBytes(bytes) { | |
this.position += bytes | |
return this.position - bytes | |
} | |
read(type) { | |
switch (type) { | |
case 'byte': { | |
return this.buffer[this.changeBytes(1)] | |
} | |
case 'unsignedShort': { | |
const result = this.buffer.readUInt16BE(this.changeBytes(2)) | |
return result | |
} | |
case 'int': { | |
const result = this.buffer.readInt32BE(this.changeBytes(4)) | |
return result | |
} | |
case 'long': { | |
const msb = BigInt(this.read('int')) | |
const lsb = BigInt(this.read('int')) | |
return msb * BigInt(2 ** 32) + lsb | |
} | |
case 'utf': { | |
const len = this.read('unsignedShort') | |
const start = this.changeBytes(len) | |
const result = this.buffer.toString('utf8', start, start + len) | |
return result | |
} | |
} | |
} | |
} | |
export function decodeTrack(track) { | |
try { | |
const buf = new DecodeClass(Buffer.from(track, 'base64')) | |
const version = ((buf.read('int') & 0xC0000000) >> 30 & 1) !== 0 ? buf.read('byte') : 1 | |
switch (version) { | |
case 1: { | |
return { | |
title: buf.read('utf'), | |
author: buf.read('utf'), | |
length: Number(buf.read('long')), | |
identifier: buf.read('utf'), | |
isStream: buf.read('byte') === 1, | |
uri: null, | |
source: buf.read('utf'), | |
position: Number(buf.read('long')) | |
} | |
} | |
case 2: { | |
return { | |
title: buf.read('utf'), | |
author: buf.read('utf'), | |
length: Number(buf.read('long')), | |
identifier: buf.read('utf'), | |
isStream: buf.read('byte') === 1, | |
uri: buf.read('byte') === 1 ? buf.read('utf') : null, | |
source: buf.read('utf'), | |
position: Number(buf.read('long')) | |
} | |
} | |
case 3: { | |
return { | |
title: buf.read('utf'), | |
author: buf.read('utf'), | |
length: Number(buf.read('long')), | |
identifier: buf.read('utf'), | |
isSeekable: true, | |
isStream: buf.read('byte') === 1, | |
uri: buf.read('byte') === 1 ? buf.read('utf') : null, | |
artworkUrl: buf.read('byte') === 1 ? buf.read('utf') : null, | |
isrc: buf.read('byte') === 1 ? buf.read('utf') : null, | |
sourceName: buf.read('utf'), | |
position: Number(buf.read('long')) | |
} | |
} | |
} | |
} catch { | |
return null | |
} | |
} | |
export function debugLog(name, type, options) { | |
switch (type) { | |
case 1: { | |
if (!config.debug.request.enabled) return; | |
if (options.headers) { | |
options.headers.authorization = 'REDACTED' | |
options.headers.host = 'REDACTED' | |
} | |
if (options.error) | |
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)}` : ''}`) | |
else | |
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)}` : ''}`) | |
break | |
} | |
case 2: { | |
switch (name) { | |
case 'trackStart': { | |
if (!config.debug.track.start) return; | |
console.log(`[\u001b[32mtrackStart\u001b[37m]: \u001b[94m${options.track.title}\u001b[37m by \u001b[94m${options.track.author}\u001b[37m.`) | |
break | |
} | |
case 'trackEnd': { | |
if (!config.debug.track.end) return; | |
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.`) | |
break | |
} | |
case 'trackException': { | |
if (!config.debug.track.exception) return; | |
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`) | |
break | |
} | |
case 'trackStuck': { | |
if (!config.debug.track.stuck) return; | |
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`) | |
break | |
} | |
} | |
break | |
} | |
case 3: { | |
switch (name) { | |
case 'connect': { | |
if (!config.debug.websocket.connect) return; | |
if (options.error) | |
return console.error(`[\u001b[31mwebsocket\u001b[37m]: \u001b[31m${options.error}\u001b[37m\n Name: \u001b[94m${options.name}\u001b[37m`) | |
console.log(`[\u001b[32mwebsocket\u001b[37m]: \u001b[94m${options.name}\u001b[37m@\u001b[94m${options.version}\u001b[37m client connected to NodeLink.`) | |
break | |
} | |
case 'disconnect': { | |
if (!config.debug.websocket.disconnect) return; | |
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`) | |
break | |
} | |
case 'error': { | |
if (!config.debug.websocket.error) return; | |
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`) | |
break | |
} | |
case 'connectCD': { | |
if (!config.debug.websocket.connectCD) return; | |
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`) | |
break | |
} | |
case 'disconnectCD': { | |
if (!config.debug.websocket.disconnectCD) return; | |
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`) | |
break | |
} | |
case 'sentDataCD': { | |
if (!config.debug.websocket.sentDataCD) return; | |
console.log(`[\u001b[32msentData\u001b[37m]: Sent data to \u001b[94m${options.clientsAmount}\u001b[37m clients.\n Guild: \u001b[94m${options.guildId}\u001b[37m`) | |
break | |
} | |
default: { | |
if (!config.debug.request.error) return; | |
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)}` : ''}`) | |
break | |
} | |
} | |
break | |
} | |
case 4: { | |
switch (name) { | |
case 'loadtracks': { | |
if (options.type === 1 && config.debug.sources.loadtrack.request) | |
console.log(`[\u001b[32mloadTracks\u001b[37m]: Loading \u001b[94m${options.loadType}\u001b[37m from ${options.sourceName}: ${options.query}`) | |
if (options.type === 2 && config.debug.sources.loadtrack.results) { | |
if (options.loadType !== 'search' && options.loadType !== 'track') | |
console.log(`[\u001b[32mloadTracks\u001b[37m]: Loaded \u001b[94m${options.playlistName}\u001b[37m from \u001b[94m${options.sourceName}\u001b[37m.`) | |
else | |
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}`) | |
} | |
if (options.type === 3 && config.debug.sources.loadtrack.exception) | |
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`) | |
break | |
} | |
case 'search': { | |
if (options.type === 1 && config.debug.sources.search.request) | |
console.log(`[\u001b[32msearch\u001b[37m]: Searching for \u001b[94m${options.query}\u001b[37m on \u001b[94m${options.sourceName}\u001b[37m`) | |
if (options.type === 2 && config.debug.sources.search.results) | |
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`) | |
if (options.type === 3 && config.debug.sources.search.exception) | |
console.error(`[\u001b[31msearch\u001b[37m]: Exception from ${options.sourceName} for query \u001b[94m${options.query}\u001b[37m: \u001b[31m${options.message}\u001b[37m`) | |
break | |
} | |
case 'retrieveStream': { | |
if (!config.debug.sources.retrieveStream) return; | |
if (options.type === 1) | |
console.log(`[\u001b[32mretrieveStream\u001b[37m]: Retrieved from \u001b[94m${options.sourceName}\u001b[37m for query \u001b[94m${options.query}\u001b[37m`) | |
if (options.type === 2) | |
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`) | |
break | |
} | |
case 'loadlyrics': { | |
if (options.type === 1 && config.debug.sources.loadlyrics.request) | |
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`) | |
if (options.type === 2 && config.debug.sources.loadlyrics.results) | |
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`) | |
if (options.type === 3 && config.debug.sources.loadlyrics.exception) | |
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`) | |
break | |
} | |
} | |
break | |
} | |
case 5: { | |
switch (name) { | |
case 'youtube': { | |
if (options.type === 1 && config.debug.youtube.success) | |
console.log(`[\u001b[32myoutube\u001b[37m]: ${options.message}`) | |
if (options.type === 2 && config.debug.youtube.error) | |
console.error(`[\u001b[31myoutube\u001b[37m]: ${options.message}`) | |
break | |
} | |
case 'pandora': { | |
if (options.type === 1 && config.debug.pandora.success) | |
console.log(`[\u001b[32mpandora\u001b[37m]: ${options.message}`) | |
if (options.type === 2 && config.debug.pandora.error) | |
console.error(`[\u001b[31mpandora\u001b[37m]: ${options.message}`) | |
break | |
} | |
case 'deezer': { | |
if (options.type === 1 && config.debug.deezer.success) | |
console.log(`[\u001b[32mdeezer\u001b[37m]: ${options.message}`) | |
if (options.type === 2 && config.debug.deezer.error) | |
console.error(`[\u001b[31mdeezer\u001b[37m]: ${options.message}`) | |
break | |
} | |
case 'spotify': { | |
if (options.type === 1 && config.debug.spotify.success) | |
console.log(`[\u001b[32mspotify\u001b[37m]: ${options.message}`) | |
if (options.type === 2 && config.debug.spotify.error) | |
console.error(`[\u001b[31mspotify\u001b[37m]: ${options.message}`) | |
break | |
} | |
case 'soundcloud': { | |
if (options.type === 1 && config.debug.soundcloud.success) | |
console.log(`[\u001b[32msoundcloud\u001b[37m]: ${options.message}`) | |
if (options.type === 2 && config.debug.soundcloud.error) | |
console.error(`[\u001b[31msoundcloud\u001b[37m]: ${options.message}`) | |
break | |
} | |
case 'musixmatch': { | |
console.log(`[\u001b[32mmusixmatch\u001b[37m]: ${options.message}`) | |
break | |
} | |
} | |
break | |
} | |
case 6: { | |
if (!config.debug.request.all) return; | |
if (options.headers) { | |
options.headers.authorization = 'REDACTED' | |
options.headers.host = 'REDACTED' | |
} | |
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)}` : ''}`) | |
break | |
} | |
} | |
} | |
export function sendResponse(req, res, data, status) { | |
if (!data) { | |
res.writeHead(status) | |
res.end() | |
return true | |
} | |
if (!req.headers || !req.headers['accept-encoding']) { | |
res.setHeader('Connection', 'close') | |
res.writeHead(status, { 'Content-Type': 'application/json' }) | |
res.end(JSON.stringify(data)) | |
} | |
if (req.headers && req.headers['accept-encoding']) { | |
if (req.headers['accept-encoding'].includes('br')) { | |
res.setHeader('Content-Encoding', 'br') | |
res.writeHead(status, { 'Content-Type': 'application/json', 'Content-Encoding': 'br' }) | |
zlib.brotliCompress(JSON.stringify(data), (err, result) => { | |
if (err) { | |
res.writeHead(500) | |
res.end() | |
return; | |
} | |
res.end(result) | |
}) | |
} | |
else if (req.headers['accept-encoding'].includes('gzip')) { | |
res.setHeader('Content-Encoding', 'gzip') | |
res.writeHead(status, { 'Content-Type': 'application/json', 'Content-Encoding': 'gzip' }) | |
zlib.gzip(JSON.stringify(data), (err, result) => { | |
if (err) { | |
res.writeHead(500) | |
res.end() | |
return; | |
} | |
res.end(result) | |
}) | |
} | |
else if (req.headers['accept-encoding'].includes('deflate')) { | |
res.setHeader('Content-Encoding', 'deflate') | |
res.writeHead(status, { 'Content-Type': 'application/json', 'Content-Encoding': 'deflate' }) | |
zlib.deflate(JSON.stringify(data), (err, result) => { | |
if (err) { | |
res.writeHead(500) | |
res.end() | |
return; | |
} | |
res.end(result) | |
}) | |
} | |
} | |
return true | |
} | |
export function tryParseBody(req, res) { | |
return new Promise((resolve) => { | |
let buffer = '' | |
req.on('data', (chunk) => buffer += chunk) | |
req.on('end', () => { | |
try { | |
resolve(JSON.parse(buffer)) | |
} catch { | |
sendResponse(req, res, { | |
timestamp: Date.now(), | |
status: 400, | |
trace: new Error().stack, | |
error: 'Bad Request', | |
message: 'Invalid JSON body', | |
path: req.url | |
}, 400) | |
resolve(null) | |
} | |
}) | |
}) | |
} | |
export function sendResponseNonNull(req, res, data) { | |
if (data === null) return; | |
sendResponse(req, res, data, 200) | |
return true | |
} | |
export function verifyMethod(parsedUrl, req, res, expected) { | |
if (req.method !== expected) { | |
sendResponse(req, res, { | |
timestamp: Date.now(), | |
status: 405, | |
error: 'Method Not Allowed', | |
message: `Request method must be ${expected}`, | |
path: parsedUrl.pathname | |
}, 405) | |
return 1 | |
} | |
return 0 | |
} | |
Array.prototype.nForEach = async function(callback) { | |
return new Promise(async (resolve) => { | |
for (let i = 0; i < this.length - 1; i++) { | |
const res = await callback(this[i], i) | |
if (res) return resolve() | |
} | |
resolve() | |
}) | |
} | |
export function waitForEvent(emitter, eventName, func, timeoutMs) { | |
return new Promise((resolve) => { | |
const timeout = timeoutMs ? setTimeout(() => { | |
throw new Error(`Event ${eventName} timed out after ${timeoutMs}ms`) | |
}, timeoutMs) : null | |
const listener = (param, param2) => { | |
if (func(param, param2) === true) { | |
emitter.removeListener(eventName, listener) | |
timeoutMs ? clearTimeout(timeout) : null | |
resolve() | |
} | |
} | |
emitter.on(eventName, listener) | |
}) | |
} | |
export function clamp16Bit(sample) { | |
return Math.max(constants.pcm.minimumRate, Math.min(sample, constants.pcm.maximumRate)) | |
} | |
export function parseClientName(clientName) { | |
if (!clientName) | |
return null | |
let clientInfo = clientName.split('(') | |
if (clientInfo.length > 1) clientInfo = clientInfo[0].slice(0, clientInfo[0].length - 1) | |
else clientInfo = clientInfo[0] | |
const split = clientInfo.split('/') | |
const name = split[0] | |
const version = split[1] | |
if (!name || !version || split.length != 2) return null | |
return { name, version } | |
} | |
export function isEmpty(value) { | |
return value === undefined || value === null || false | |
} | |
export function loadHLS(url, stream, onceEnded) { | |
return new Promise(async (resolve) => { | |
const response = await http1makeRequest(url, { method: 'GET' }) | |
const body = response.body.split('\n') | |
let segmentMetadata = { | |
duration: 0 | |
} | |
body.nForEach(async (line, i) => { | |
return new Promise(async (resolveSegment) => { | |
if (stream.ended) { | |
resolveSegment(true) | |
return resolve(false) | |
} | |
if (line.startsWith('#')) { | |
const tag = line.split(':')[0] | |
let value = line.split(':')[1] | |
if (value) value = value.split(',')[0] | |
if (tag === '#EXTINF') { | |
segmentMetadata.duration = parseFloat(value) * 1000 | |
} else if (tag === '#EXT-X-ENDLIST') { | |
stream.end() | |
return resolveSegment(true) | |
} | |
return resolveSegment(false) | |
} | |
const now = Date.now() | |
const segment = await http1makeRequest(line, { method: 'GET', streamOnly: true }) | |
segment.stream.on('data', (chunk) => stream.write(chunk)) | |
segment.stream.once('readable', () => { | |
if (segmentMetadata.duration) { | |
setTimeout(() => { | |
resolveSegment(false) | |
}, segmentMetadata.duration - (Date.now() - now) * 2) | |
segmentMetadata.duration = 0 | |
} else { | |
segment.stream.on('end', () => { | |
resolveSegment(false) | |
segment.stream.destroy() | |
}) | |
} | |
}) | |
if (onceEnded && i === body.length - 2) { | |
segment.stream.on('end', () => { | |
resolve(true) | |
segment.stream.destroy() | |
}) | |
} | |
}) | |
}) | |
if (!onceEnded) resolve(true) | |
}) | |
} | |
export function loadHLSPlaylist(url, stream) { | |
return new Promise(async (resolve) => { | |
const response = await http1makeRequest(url, { method: 'GET' }) | |
const body = response.body.split('\n') | |
body.nForEach(async (line, i) => { | |
return new Promise(async (resolvePlaylist) => { | |
if (line.startsWith('#')) { | |
const tag = line.split(':')[0] | |
let value = line.split(':')[1] | |
if (value) value = value.split(',')[0] | |
if (tag === '#EXT-X-ENDLIST') { | |
stream.end() | |
resolvePlaylist(true) | |
return resolve(stream) | |
} | |
resolvePlaylist(false) | |
if (i === body.length - 1) { | |
loadHLSPlaylist(value, stream) | |
resolve(stream) | |
} | |
return; | |
} | |
if (await loadHLS(line, stream, true) === false) | |
return resolve(stream) | |
resolvePlaylist(false) | |
if (i === body.length - 2) { | |
loadHLSPlaylist(url, stream) | |
return resolve(stream) | |
} | |
}) | |
}) | |
resolve(stream) | |
}) | |
} |