We are launching a docker version (server version) today so we want to just organize the repo so its easier to navigate.
138 lines
4.2 KiB
TypeScript
138 lines
4.2 KiB
TypeScript
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<ProxyResponse> {
|
|
const headers: Record<string, string> = {
|
|
'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);
|
|
}
|
|
});
|
|
} |