better proxy handling for streams
This commit is contained in:
@@ -1,6 +1,16 @@
|
|||||||
import {FastifyReply} from 'fastify';
|
import {FastifyReply, FastifyRequest} from 'fastify';
|
||||||
import {processM3U8Content, proxyRequest, streamToReadable} from './proxy.service';
|
import {processM3U8Content, proxyRequest, streamToReadable} from './proxy.service';
|
||||||
import {ProxyRequest} from '../types';
|
|
||||||
|
interface ProxyQuerystring {
|
||||||
|
url: string;
|
||||||
|
referer?: string;
|
||||||
|
origin?: string;
|
||||||
|
userAgent?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProxyRequest = FastifyRequest<{
|
||||||
|
Querystring: ProxyQuerystring
|
||||||
|
}>;
|
||||||
|
|
||||||
export async function handleProxy(req: ProxyRequest, reply: FastifyReply) {
|
export async function handleProxy(req: ProxyRequest, reply: FastifyReply) {
|
||||||
const { url, referer, origin, userAgent } = req.query;
|
const { url, referer, origin, userAgent } = req.query;
|
||||||
@@ -10,32 +20,23 @@ export async function handleProxy(req: ProxyRequest, reply: FastifyReply) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const rangeHeader = req.headers.range;
|
||||||
|
|
||||||
const { response, contentType, isM3U8, contentLength } = await proxyRequest(url, {
|
const { response, contentType, isM3U8, contentLength } = await proxyRequest(url, {
|
||||||
referer,
|
referer,
|
||||||
origin,
|
origin,
|
||||||
userAgent
|
userAgent
|
||||||
});
|
}, rangeHeader);
|
||||||
|
|
||||||
|
|
||||||
reply.header('Access-Control-Allow-Origin', '*');
|
reply.header('Access-Control-Allow-Origin', '*');
|
||||||
reply.header('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
reply.header('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
||||||
reply.header('Access-Control-Allow-Headers', 'Content-Type, Range');
|
reply.header('Access-Control-Allow-Headers', 'Content-Type, Range');
|
||||||
reply.header('Access-Control-Expose-Headers', 'Content-Length, Content-Range, Accept-Ranges');
|
reply.header('Access-Control-Expose-Headers', 'Content-Length, Content-Range, Accept-Ranges');
|
||||||
|
|
||||||
if (contentType) {
|
|
||||||
reply.header('Content-Type', contentType);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (contentLength) {
|
|
||||||
reply.header('Content-Length', contentLength);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (contentType?.startsWith('image/') || contentType?.startsWith('video/')) {
|
|
||||||
reply.header('Cache-Control', 'public, max-age=31536000, immutable');
|
|
||||||
}
|
|
||||||
|
|
||||||
reply.header('Accept-Ranges', 'bytes');
|
|
||||||
|
|
||||||
if (isM3U8) {
|
if (isM3U8) {
|
||||||
|
reply.header('Content-Type', 'application/vnd.apple.mpegurl');
|
||||||
|
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
const baseUrl = new URL(response.url);
|
const baseUrl = new URL(response.url);
|
||||||
|
|
||||||
@@ -48,13 +49,67 @@ export async function handleProxy(req: ProxyRequest, reply: FastifyReply) {
|
|||||||
return reply.send(processedContent);
|
return reply.send(processedContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isSubtitle = url.includes('.vtt') || url.includes('.srt') || url.includes('.ass') ||
|
||||||
|
contentType?.includes('text/vtt') || contentType?.includes('text/srt');
|
||||||
|
|
||||||
|
if (isSubtitle) {
|
||||||
|
const text = await response.text();
|
||||||
|
|
||||||
|
let mimeType = 'text/vtt';
|
||||||
|
if (url.includes('.srt') || contentType?.includes('srt')) {
|
||||||
|
mimeType = 'text/plain';
|
||||||
|
} else if (url.includes('.ass')) {
|
||||||
|
mimeType = 'text/plain';
|
||||||
|
}
|
||||||
|
|
||||||
|
reply.header('Content-Type', mimeType);
|
||||||
|
reply.header('Cache-Control', 'public, max-age=3600');
|
||||||
|
return reply.send(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentType) {
|
||||||
|
reply.header('Content-Type', contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status === 206) {
|
||||||
|
const contentRange = response.headers.get('content-range');
|
||||||
|
if (contentRange) {
|
||||||
|
reply.header('Content-Range', contentRange);
|
||||||
|
}
|
||||||
|
reply.code(206);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentLength) {
|
||||||
|
reply.header('Content-Length', contentLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentType?.startsWith('image/') || contentType?.startsWith('video/')) {
|
||||||
|
reply.header('Cache-Control', 'public, max-age=31536000, immutable');
|
||||||
|
}
|
||||||
|
|
||||||
|
reply.header('Accept-Ranges', 'bytes');
|
||||||
|
|
||||||
return reply.send(streamToReadable(response.body!));
|
return reply.send(streamToReadable(response.body!));
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
req.server.log.error(err);
|
req.server.log.error(err);
|
||||||
|
console.error('=== PROXY ERROR ===');
|
||||||
|
console.error('URL:', url);
|
||||||
|
console.error('Error:', err);
|
||||||
|
console.error('===================');
|
||||||
|
|
||||||
if (!reply.sent) {
|
if (!reply.sent) {
|
||||||
return reply.code(500).send({ error: "Internal Server Error" });
|
return reply.code(500).send({
|
||||||
|
error: "Internal Server Error",
|
||||||
|
details: err instanceof Error ? err.message : String(err)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleProxyOptions(req: FastifyRequest, reply: FastifyReply) {
|
||||||
|
reply.header('Access-Control-Allow-Origin', '*');
|
||||||
|
reply.header('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
||||||
|
reply.header('Access-Control-Allow-Headers', 'Content-Type, Range');
|
||||||
|
return reply.code(204).send();
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { FastifyInstance } from 'fastify';
|
import { FastifyInstance } from 'fastify';
|
||||||
import { handleProxy } from './proxy.controller';
|
import {handleProxy, handleProxyOptions} from './proxy.controller';
|
||||||
|
|
||||||
async function proxyRoutes(fastify: FastifyInstance) {
|
async function proxyRoutes(fastify: FastifyInstance) {
|
||||||
fastify.get('/proxy', handleProxy);
|
fastify.get('/proxy', handleProxy);
|
||||||
|
fastify.options('/proxy', handleProxyOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default proxyRoutes;
|
export default proxyRoutes;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Readable } from 'stream';
|
import {Readable} from 'stream';
|
||||||
|
|
||||||
interface ProxyHeaders {
|
interface ProxyHeaders {
|
||||||
referer?: string;
|
referer?: string;
|
||||||
@@ -13,25 +13,24 @@ interface ProxyResponse {
|
|||||||
contentLength: string | null;
|
contentLength: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function proxyRequest(url: string, { referer, origin, userAgent }: ProxyHeaders): Promise<ProxyResponse> {
|
export async function proxyRequest(url: string, { referer, origin, userAgent }: ProxyHeaders, rangeHeader?: string): Promise<ProxyResponse> {
|
||||||
const headers: Record<string, string> = {
|
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",
|
'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': '*/*',
|
||||||
'Accept-Language': 'en-US,en;q=0.9',
|
'Accept-Language': 'en-US,en;q=0.9',
|
||||||
'Accept-Encoding': 'identity',
|
'Accept-Encoding': 'identity',
|
||||||
|
|
||||||
'Connection': 'keep-alive'
|
'Connection': 'keep-alive'
|
||||||
};
|
};
|
||||||
|
|
||||||
if (referer) headers['Referer'] = referer;
|
if (referer) headers['Referer'] = referer;
|
||||||
if (origin) headers['Origin'] = origin;
|
if (origin) headers['Origin'] = origin;
|
||||||
|
if (rangeHeader) headers['Range'] = rangeHeader;
|
||||||
|
|
||||||
let lastError: Error | null = null;
|
let lastError: Error | null = null;
|
||||||
const maxRetries = 2;
|
const maxRetries = 2;
|
||||||
|
|
||||||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeoutId = setTimeout(() => controller.abort(), 60000);
|
const timeoutId = setTimeout(() => controller.abort(), 60000);
|
||||||
|
|
||||||
@@ -43,8 +42,7 @@ export async function proxyRequest(url: string, { referer, origin, userAgent }:
|
|||||||
|
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok && response.status !== 206) {
|
||||||
|
|
||||||
if (response.status === 404 || response.status === 403) {
|
if (response.status === 404 || response.status === 403) {
|
||||||
throw new Error(`Proxy Error: ${response.status} ${response.statusText}`);
|
throw new Error(`Proxy Error: ${response.status} ${response.statusText}`);
|
||||||
}
|
}
|
||||||
@@ -57,9 +55,16 @@ export async function proxyRequest(url: string, { referer, origin, userAgent }:
|
|||||||
throw new Error(`Proxy Error: ${response.status} ${response.statusText}`);
|
throw new Error(`Proxy Error: ${response.status} ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const contentType = response.headers.get('content-type');
|
|
||||||
const contentLength = response.headers.get('content-length');
|
const contentLength = response.headers.get('content-length');
|
||||||
const isM3U8 = (contentType && contentType.includes('mpegurl')) || url.includes('.m3u8');
|
const contentType = response.headers.get('content-type');
|
||||||
|
const ct = contentType?.toLowerCase();
|
||||||
|
|
||||||
|
const isM3U8 =
|
||||||
|
!!ct && (
|
||||||
|
ct.includes('mpegurl') ||
|
||||||
|
ct.includes('m3u8')
|
||||||
|
) ||
|
||||||
|
url.toLowerCase().includes('.m3u8');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
response,
|
response,
|
||||||
@@ -83,24 +88,57 @@ export async function proxyRequest(url: string, { referer, origin, userAgent }:
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function processM3U8Content(text: string, baseUrl: URL, { referer, origin, userAgent }: ProxyHeaders): string {
|
export function processM3U8Content(text: string, baseUrl: URL, { referer, origin, userAgent }: ProxyHeaders): string {
|
||||||
return text.replace(/^(?!#)(?!\s*$).+/gm, (line) => {
|
const buildProxy = (url: string) => {
|
||||||
line = line.trim();
|
|
||||||
let absoluteUrl: string;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
absoluteUrl = new URL(line, baseUrl).href;
|
const params = new URLSearchParams();
|
||||||
} catch (e) {
|
params.set('url', new URL(url, baseUrl).href);
|
||||||
return line;
|
if (referer) params.set('referer', referer);
|
||||||
|
if (origin) params.set('origin', origin);
|
||||||
|
if (userAgent) params.set('userAgent', userAgent);
|
||||||
|
return `/api/proxy?${params.toString()}`;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error building proxy URL for:', url, error);
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isMasterPlaylist = text.includes('#EXT-X-STREAM-INF');
|
||||||
|
|
||||||
|
|
||||||
|
let lines = text.split('\n');
|
||||||
|
let result = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
const trimmed = line.trim();
|
||||||
|
|
||||||
|
if (!trimmed) {
|
||||||
|
result.push(line);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const proxyParams = new URLSearchParams();
|
if (trimmed.startsWith('#')) {
|
||||||
proxyParams.set('url', absoluteUrl);
|
if (line.includes('URI=')) {
|
||||||
if (referer) proxyParams.set('referer', referer);
|
const processedLine = line.replace(/URI="([^"]+)"/g, (match, uri) => {
|
||||||
if (origin) proxyParams.set('origin', origin);
|
return `URI="${buildProxy(uri)}"`;
|
||||||
if (userAgent) proxyParams.set('userAgent', userAgent);
|
});
|
||||||
|
result.push(processedLine);
|
||||||
|
} else {
|
||||||
|
result.push(line);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
return `/api/proxy?${proxyParams.toString()}`;
|
try {
|
||||||
});
|
const proxiedUrl = buildProxy(trimmed);
|
||||||
|
result.push(proxiedUrl);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error processing M3U8 URL line:', trimmed, error);
|
||||||
|
result.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function streamToReadable(webStream: ReadableStream): Readable {
|
export function streamToReadable(webStream: ReadableStream): Readable {
|
||||||
@@ -110,7 +148,6 @@ export function streamToReadable(webStream: ReadableStream): Readable {
|
|||||||
return new Readable({
|
return new Readable({
|
||||||
async read() {
|
async read() {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const timeoutPromise = new Promise((_, reject) => {
|
const timeoutPromise = new Promise((_, reject) => {
|
||||||
readTimeout = setTimeout(() => reject(new Error('Stream read timeout')), 10000);
|
readTimeout = setTimeout(() => reject(new Error('Stream read timeout')), 10000);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
import {FastifyReply} from 'fastify';
|
import {FastifyReply, FastifyRequest} from 'fastify';
|
||||||
import {processM3U8Content, proxyRequest, streamToReadable} from './proxy.service';
|
import {processM3U8Content, proxyRequest, streamToReadable} from './proxy.service';
|
||||||
import {ProxyRequest} from '../types';
|
|
||||||
|
interface ProxyQuerystring {
|
||||||
|
url: string;
|
||||||
|
referer?: string;
|
||||||
|
origin?: string;
|
||||||
|
userAgent?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProxyRequest = FastifyRequest<{
|
||||||
|
Querystring: ProxyQuerystring
|
||||||
|
}>;
|
||||||
|
|
||||||
export async function handleProxy(req: ProxyRequest, reply: FastifyReply) {
|
export async function handleProxy(req: ProxyRequest, reply: FastifyReply) {
|
||||||
const { url, referer, origin, userAgent } = req.query;
|
const { url, referer, origin, userAgent } = req.query;
|
||||||
@@ -10,32 +20,23 @@ export async function handleProxy(req: ProxyRequest, reply: FastifyReply) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const rangeHeader = req.headers.range;
|
||||||
|
|
||||||
const { response, contentType, isM3U8, contentLength } = await proxyRequest(url, {
|
const { response, contentType, isM3U8, contentLength } = await proxyRequest(url, {
|
||||||
referer,
|
referer,
|
||||||
origin,
|
origin,
|
||||||
userAgent
|
userAgent
|
||||||
});
|
}, rangeHeader);
|
||||||
|
|
||||||
|
|
||||||
reply.header('Access-Control-Allow-Origin', '*');
|
reply.header('Access-Control-Allow-Origin', '*');
|
||||||
reply.header('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
reply.header('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
||||||
reply.header('Access-Control-Allow-Headers', 'Content-Type, Range');
|
reply.header('Access-Control-Allow-Headers', 'Content-Type, Range');
|
||||||
reply.header('Access-Control-Expose-Headers', 'Content-Length, Content-Range, Accept-Ranges');
|
reply.header('Access-Control-Expose-Headers', 'Content-Length, Content-Range, Accept-Ranges');
|
||||||
|
|
||||||
if (contentType) {
|
|
||||||
reply.header('Content-Type', contentType);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (contentLength) {
|
|
||||||
reply.header('Content-Length', contentLength);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (contentType?.startsWith('image/') || contentType?.startsWith('video/')) {
|
|
||||||
reply.header('Cache-Control', 'public, max-age=31536000, immutable');
|
|
||||||
}
|
|
||||||
|
|
||||||
reply.header('Accept-Ranges', 'bytes');
|
|
||||||
|
|
||||||
if (isM3U8) {
|
if (isM3U8) {
|
||||||
|
reply.header('Content-Type', 'application/vnd.apple.mpegurl');
|
||||||
|
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
const baseUrl = new URL(response.url);
|
const baseUrl = new URL(response.url);
|
||||||
|
|
||||||
@@ -48,13 +49,67 @@ export async function handleProxy(req: ProxyRequest, reply: FastifyReply) {
|
|||||||
return reply.send(processedContent);
|
return reply.send(processedContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isSubtitle = url.includes('.vtt') || url.includes('.srt') || url.includes('.ass') ||
|
||||||
|
contentType?.includes('text/vtt') || contentType?.includes('text/srt');
|
||||||
|
|
||||||
|
if (isSubtitle) {
|
||||||
|
const text = await response.text();
|
||||||
|
|
||||||
|
let mimeType = 'text/vtt';
|
||||||
|
if (url.includes('.srt') || contentType?.includes('srt')) {
|
||||||
|
mimeType = 'text/plain';
|
||||||
|
} else if (url.includes('.ass')) {
|
||||||
|
mimeType = 'text/plain';
|
||||||
|
}
|
||||||
|
|
||||||
|
reply.header('Content-Type', mimeType);
|
||||||
|
reply.header('Cache-Control', 'public, max-age=3600');
|
||||||
|
return reply.send(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentType) {
|
||||||
|
reply.header('Content-Type', contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status === 206) {
|
||||||
|
const contentRange = response.headers.get('content-range');
|
||||||
|
if (contentRange) {
|
||||||
|
reply.header('Content-Range', contentRange);
|
||||||
|
}
|
||||||
|
reply.code(206);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentLength) {
|
||||||
|
reply.header('Content-Length', contentLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentType?.startsWith('image/') || contentType?.startsWith('video/')) {
|
||||||
|
reply.header('Cache-Control', 'public, max-age=31536000, immutable');
|
||||||
|
}
|
||||||
|
|
||||||
|
reply.header('Accept-Ranges', 'bytes');
|
||||||
|
|
||||||
return reply.send(streamToReadable(response.body!));
|
return reply.send(streamToReadable(response.body!));
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
req.server.log.error(err);
|
req.server.log.error(err);
|
||||||
|
console.error('=== PROXY ERROR ===');
|
||||||
|
console.error('URL:', url);
|
||||||
|
console.error('Error:', err);
|
||||||
|
console.error('===================');
|
||||||
|
|
||||||
if (!reply.sent) {
|
if (!reply.sent) {
|
||||||
return reply.code(500).send({ error: "Internal Server Error" });
|
return reply.code(500).send({
|
||||||
|
error: "Internal Server Error",
|
||||||
|
details: err instanceof Error ? err.message : String(err)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleProxyOptions(req: FastifyRequest, reply: FastifyReply) {
|
||||||
|
reply.header('Access-Control-Allow-Origin', '*');
|
||||||
|
reply.header('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
||||||
|
reply.header('Access-Control-Allow-Headers', 'Content-Type, Range');
|
||||||
|
return reply.code(204).send();
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { FastifyInstance } from 'fastify';
|
import { FastifyInstance } from 'fastify';
|
||||||
import { handleProxy } from './proxy.controller';
|
import {handleProxy, handleProxyOptions} from './proxy.controller';
|
||||||
|
|
||||||
async function proxyRoutes(fastify: FastifyInstance) {
|
async function proxyRoutes(fastify: FastifyInstance) {
|
||||||
fastify.get('/proxy', handleProxy);
|
fastify.get('/proxy', handleProxy);
|
||||||
|
fastify.options('/proxy', handleProxyOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default proxyRoutes;
|
export default proxyRoutes;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Readable } from 'stream';
|
import {Readable} from 'stream';
|
||||||
|
|
||||||
interface ProxyHeaders {
|
interface ProxyHeaders {
|
||||||
referer?: string;
|
referer?: string;
|
||||||
@@ -13,25 +13,24 @@ interface ProxyResponse {
|
|||||||
contentLength: string | null;
|
contentLength: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function proxyRequest(url: string, { referer, origin, userAgent }: ProxyHeaders): Promise<ProxyResponse> {
|
export async function proxyRequest(url: string, { referer, origin, userAgent }: ProxyHeaders, rangeHeader?: string): Promise<ProxyResponse> {
|
||||||
const headers: Record<string, string> = {
|
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",
|
'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': '*/*',
|
||||||
'Accept-Language': 'en-US,en;q=0.9',
|
'Accept-Language': 'en-US,en;q=0.9',
|
||||||
'Accept-Encoding': 'identity',
|
'Accept-Encoding': 'identity',
|
||||||
|
|
||||||
'Connection': 'keep-alive'
|
'Connection': 'keep-alive'
|
||||||
};
|
};
|
||||||
|
|
||||||
if (referer) headers['Referer'] = referer;
|
if (referer) headers['Referer'] = referer;
|
||||||
if (origin) headers['Origin'] = origin;
|
if (origin) headers['Origin'] = origin;
|
||||||
|
if (rangeHeader) headers['Range'] = rangeHeader;
|
||||||
|
|
||||||
let lastError: Error | null = null;
|
let lastError: Error | null = null;
|
||||||
const maxRetries = 2;
|
const maxRetries = 2;
|
||||||
|
|
||||||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeoutId = setTimeout(() => controller.abort(), 60000);
|
const timeoutId = setTimeout(() => controller.abort(), 60000);
|
||||||
|
|
||||||
@@ -43,8 +42,7 @@ export async function proxyRequest(url: string, { referer, origin, userAgent }:
|
|||||||
|
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok && response.status !== 206) {
|
||||||
|
|
||||||
if (response.status === 404 || response.status === 403) {
|
if (response.status === 404 || response.status === 403) {
|
||||||
throw new Error(`Proxy Error: ${response.status} ${response.statusText}`);
|
throw new Error(`Proxy Error: ${response.status} ${response.statusText}`);
|
||||||
}
|
}
|
||||||
@@ -57,9 +55,16 @@ export async function proxyRequest(url: string, { referer, origin, userAgent }:
|
|||||||
throw new Error(`Proxy Error: ${response.status} ${response.statusText}`);
|
throw new Error(`Proxy Error: ${response.status} ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const contentType = response.headers.get('content-type');
|
|
||||||
const contentLength = response.headers.get('content-length');
|
const contentLength = response.headers.get('content-length');
|
||||||
const isM3U8 = (contentType && contentType.includes('mpegurl')) || url.includes('.m3u8');
|
const contentType = response.headers.get('content-type');
|
||||||
|
const ct = contentType?.toLowerCase();
|
||||||
|
|
||||||
|
const isM3U8 =
|
||||||
|
!!ct && (
|
||||||
|
ct.includes('mpegurl') ||
|
||||||
|
ct.includes('m3u8')
|
||||||
|
) ||
|
||||||
|
url.toLowerCase().includes('.m3u8');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
response,
|
response,
|
||||||
@@ -83,24 +88,57 @@ export async function proxyRequest(url: string, { referer, origin, userAgent }:
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function processM3U8Content(text: string, baseUrl: URL, { referer, origin, userAgent }: ProxyHeaders): string {
|
export function processM3U8Content(text: string, baseUrl: URL, { referer, origin, userAgent }: ProxyHeaders): string {
|
||||||
return text.replace(/^(?!#)(?!\s*$).+/gm, (line) => {
|
const buildProxy = (url: string) => {
|
||||||
line = line.trim();
|
|
||||||
let absoluteUrl: string;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
absoluteUrl = new URL(line, baseUrl).href;
|
const params = new URLSearchParams();
|
||||||
} catch (e) {
|
params.set('url', new URL(url, baseUrl).href);
|
||||||
return line;
|
if (referer) params.set('referer', referer);
|
||||||
|
if (origin) params.set('origin', origin);
|
||||||
|
if (userAgent) params.set('userAgent', userAgent);
|
||||||
|
return `/api/proxy?${params.toString()}`;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error building proxy URL for:', url, error);
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isMasterPlaylist = text.includes('#EXT-X-STREAM-INF');
|
||||||
|
|
||||||
|
|
||||||
|
let lines = text.split('\n');
|
||||||
|
let result = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
const trimmed = line.trim();
|
||||||
|
|
||||||
|
if (!trimmed) {
|
||||||
|
result.push(line);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const proxyParams = new URLSearchParams();
|
if (trimmed.startsWith('#')) {
|
||||||
proxyParams.set('url', absoluteUrl);
|
if (line.includes('URI=')) {
|
||||||
if (referer) proxyParams.set('referer', referer);
|
const processedLine = line.replace(/URI="([^"]+)"/g, (match, uri) => {
|
||||||
if (origin) proxyParams.set('origin', origin);
|
return `URI="${buildProxy(uri)}"`;
|
||||||
if (userAgent) proxyParams.set('userAgent', userAgent);
|
});
|
||||||
|
result.push(processedLine);
|
||||||
|
} else {
|
||||||
|
result.push(line);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
return `/api/proxy?${proxyParams.toString()}`;
|
try {
|
||||||
});
|
const proxiedUrl = buildProxy(trimmed);
|
||||||
|
result.push(proxiedUrl);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error processing M3U8 URL line:', trimmed, error);
|
||||||
|
result.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function streamToReadable(webStream: ReadableStream): Readable {
|
export function streamToReadable(webStream: ReadableStream): Readable {
|
||||||
@@ -110,7 +148,6 @@ export function streamToReadable(webStream: ReadableStream): Readable {
|
|||||||
return new Readable({
|
return new Readable({
|
||||||
async read() {
|
async read() {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const timeoutPromise = new Promise((_, reject) => {
|
const timeoutPromise = new Promise((_, reject) => {
|
||||||
readTimeout = setTimeout(() => reject(new Error('Stream read timeout')), 10000);
|
readTimeout = setTimeout(() => reject(new Error('Stream read timeout')), 10000);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user