44 lines
3.1 KiB
JavaScript
44 lines
3.1 KiB
JavaScript
#!/usr/bin/env node
|
|
const base = process.argv[2];
|
|
if (!base) { console.error('usage: node alt/compare.mjs <bun-base-url>'); process.exit(2); }
|
|
const alt = (process.env.ALT_URL || 'http://127.0.0.1:3000').replace(/\/$/, '');
|
|
const video = process.env.VIDEO_ID || 'dQw4w9WgXcQ';
|
|
const generation = process.env.MEDIA_GEN || '1';
|
|
const query = process.env.SEARCH_QUERY || 'worship';
|
|
const paths = [
|
|
['search', `/api/search?q=${encodeURIComponent(query)}`],
|
|
['streams', `/api/streams?v=${encodeURIComponent(video)}`],
|
|
['download-prepare', `/api/download/${encodeURIComponent(video)}/prepare`],
|
|
['media-range', `/api/media/${encodeURIComponent(video)}?g=${encodeURIComponent(generation)}`],
|
|
];
|
|
function jsonPaths(value, prefix = '$', out = []) {
|
|
if (Array.isArray(value)) { out.push(`${prefix}[]`); if (value.length) jsonPaths(value[0], `${prefix}[]`, out); }
|
|
else if (value && typeof value === 'object') for (const key of Object.keys(value).sort()) { out.push(`${prefix}.${key}`); jsonPaths(value[key], `${prefix}.${key}`, out); }
|
|
return out.sort();
|
|
}
|
|
async function request(url, range = false) {
|
|
const headers = range ? { Range: 'bytes=0-1023' } : {};
|
|
const response = await fetch(url, { headers, signal: AbortSignal.timeout(Number(process.env.COMPARE_TIMEOUT_MS || 30000)) });
|
|
const type = response.headers.get('content-type') || '';
|
|
const body = type.includes('json') ? await response.json() : await response.arrayBuffer();
|
|
return { status: response.status, headers: Object.fromEntries(['content-type','cache-control','accept-ranges','content-range','etag'].map(k => [k,response.headers.get(k)])), body };
|
|
}
|
|
let failed = false;
|
|
for (const [name, path] of paths) {
|
|
try {
|
|
const [a,b] = await Promise.all([request(base.replace(/\/$/,'')+path,name==='media-range'),request(alt+path,name==='media-range')]);
|
|
const isJson = typeof a.body === 'object' && !ArrayBuffer.isView(a.body) && !(a.body instanceof ArrayBuffer);
|
|
const result = { name, path, bun: {status:a.status,headers:a.headers}, swoole:{status:b.status,headers:b.headers} };
|
|
if (isJson) { result.bun.shape=jsonPaths(a.body); result.swoole.shape=jsonPaths(b.body); result.shapeMatches=JSON.stringify(result.bun.shape)===JSON.stringify(result.swoole.shape); }
|
|
const headerKeys=['content-type','cache-control','accept-ranges','content-range','etag'];
|
|
const norm=(key,value)=>value===null?null:(key==='content-type'?value.toLowerCase().replace(/;\s*charset=utf-8/i,'; charset=utf-8'):value.trim());
|
|
result.headerMatches=headerKeys.filter((key)=>norm(key,a.headers[key])===norm(key,b.headers[key]));
|
|
result.headerDiffs=headerKeys.filter((key)=>norm(key,a.headers[key])!==norm(key,b.headers[key])).map((key)=>({key,bun:a.headers[key] ?? null,swoole:b.headers[key] ?? null}));
|
|
if(name==='media-range') result.rangeBytes={bun:a.body.byteLength,swoole:b.body.byteLength};
|
|
result.statusMatches=a.status===b.status;
|
|
console.log(JSON.stringify(result));
|
|
if(!result.statusMatches || (result.shapeMatches===false) || result.headerDiffs.length)failed=true;
|
|
} catch(error) { failed=true; console.error(JSON.stringify({name,path,error:String(error)})); }
|
|
}
|
|
if(failed) process.exitCode=1;
|