import { Readable } from 'stream'; interface ProxyHeaders { referer?: string; origin?: string; userAgent?: string; } interface ProxyResponse { response: Response; contentType: string | null; isM3U8: boolean; contentLength: string | null; } export async function proxyRequest(url: string, { referer, origin, userAgent }: ProxyHeaders): Promise { const headers: Record = { 'User-Agent': userAgent || "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", 'Accept': '*/*', 'Accept-Language': 'en-US,en;q=0.9', 'Accept-Encoding': 'identity', 'Connection': 'keep-alive' }; if (referer) headers['Referer'] = referer; if (origin) headers['Origin'] = origin; let lastError: Error | null = null; const maxRetries = 2; for (let attempt = 0; attempt < maxRetries; attempt++) { try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 60000); const response = await fetch(url, { headers, redirect: 'follow', signal: controller.signal }); clearTimeout(timeoutId); if (!response.ok) { if (response.status === 404 || response.status === 403) { throw new Error(`Proxy Error: ${response.status} ${response.statusText}`); } if (attempt < maxRetries - 1) { await new Promise(resolve => setTimeout(resolve, 500)); continue; } throw new Error(`Proxy Error: ${response.status} ${response.statusText}`); } const contentType = response.headers.get('content-type'); const contentLength = response.headers.get('content-length'); const isM3U8 = (contentType && contentType.includes('mpegurl')) || url.includes('.m3u8'); return { response, contentType, isM3U8, contentLength }; } catch (error) { lastError = error as Error; if (attempt === maxRetries - 1) { throw lastError; } await new Promise(resolve => setTimeout(resolve, 500)); } } throw lastError || new Error('Unknown error in proxyRequest'); } export function processM3U8Content(text: string, baseUrl: URL, { referer, origin, userAgent }: ProxyHeaders): string { return text.replace(/^(?!#)(?!\s*$).+/gm, (line) => { line = line.trim(); let absoluteUrl: string; try { absoluteUrl = new URL(line, baseUrl).href; } catch (e) { return line; } const proxyParams = new URLSearchParams(); proxyParams.set('url', absoluteUrl); if (referer) proxyParams.set('referer', referer); if (origin) proxyParams.set('origin', origin); if (userAgent) proxyParams.set('userAgent', userAgent); return `/api/proxy?${proxyParams.toString()}`; }); } export function streamToReadable(webStream: ReadableStream): Readable { const reader = webStream.getReader(); let readTimeout: NodeJS.Timeout; return new Readable({ async read() { try { const timeoutPromise = new Promise((_, reject) => { readTimeout = setTimeout(() => reject(new Error('Stream read timeout')), 10000); }); const readPromise = reader.read(); const { done, value } = await Promise.race([readPromise, timeoutPromise]) as any; clearTimeout(readTimeout); if (done) { this.push(null); } else { this.push(Buffer.from(value)); } } catch (error) { clearTimeout(readTimeout); this.destroy(error as Error); } }, destroy(error, callback) { clearTimeout(readTimeout); reader.cancel().then(() => callback(error)).catch(callback); } }); }