function withTimeout(promise, ms, label) { return new Promise((resolve, reject) => { const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms); promise.then( (v) => { clearTimeout(timer); resolve(v); }, (e) => { clearTimeout(timer); reject(e); } ); }); } function toStreamArray(raw, providerName) { let arr = Array.isArray(raw) ? raw : raw && typeof raw === 'object' && raw.url ? [raw] : []; return arr .filter((s) => s && typeof s === 'object' && typeof s.url === 'string' && s.url.length > 0) .map((s) => ({ ...s, _provider: s._provider || providerName })); } async function scrapeAll(providers, media, timeoutMs) { const settled = await Promise.allSettled( providers.map(async (p) => { const raw = await withTimeout( Promise.resolve().then(() => p.getStreams(String(media.id), media.type, media.seasonId ?? null, media.episodeId ?? null) ), timeoutMs, p.name ); return toStreamArray(raw, p.name); }) ); const streams = []; const errors = {}; settled.forEach((r, i) => { if (r.status === 'fulfilled') streams.push(...r.value); else errors[providers[i].name] = (r.reason && r.reason.message) || String(r.reason); }); const seen = new Set(); const unique = []; for (const s of streams) { const key = `${s._provider}|${s.url}|${s.quality || ''}|${s.name || s.title || ''}`; if (seen.has(key)) continue; seen.add(key); unique.push(s); } return { streams: unique, errors }; } module.exports = { scrapeAll, withTimeout };