Add Swoole alternative backend

This commit is contained in:
Jonathan Sykes
2026-10-14 00:43:38 +08:00
parent f465cc86d0
commit 1130fabc94
11 changed files with 756 additions and 0 deletions

43
alt/compare.mjs Normal file
View File

@@ -0,0 +1,43 @@
#!/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;