m3u8-proxy / app.js
vickydmt's picture
Update app.js
628a1d2 verified
const express = require('express');
const fetch = require('node-fetch');
const cors = require('cors');
const { URL } = require('url');
const app = express();
const PORT = process.env.PORT || 7860;
const ALLOWED_ORIGINS = [
'https://hianimez.xyz',
'https://hianimez.qzz.io',
'https://kaori.qzz.io',
'https://anime-player-f0n.pages.dev',
'https://aniwatchtv.to',
'https://aniwatch.to',
'http://localhost:3000',
'http://localhost:5173',
'http://localhost:3005',
'http://localhost:3004'
];
app.use(cors({
origin: (origin, callback) => {
if (!origin) return callback(null, true);
const isAllowed = ALLOWED_ORIGINS.includes(origin) ||
['hianimez.xyz', 'qzz.io', 'pages.dev', 'localhost', 'aniwatchtv.to', 'aniwatch.to'].some(domain => origin.includes(domain));
if (isAllowed) {
callback(null, true);
} else {
callback(null, true);
}
},
credentials: true,
exposedHeaders: ['Content-Length', 'Content-Range', 'Accept-Ranges', 'Content-Type']
}));
function generateHeadersForDomain(targetUrl) {
const url = new URL(targetUrl);
const hostname = url.hostname.toLowerCase();
const path = url.pathname.toLowerCase();
const headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36",
"Accept": "*/*",
"Accept-Language": "en-US,en;q=0.9",
"Sec-Ch-Ua": '"Not A(Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"',
"Sec-Ch-Ua-Mobile": "?0",
"Sec-Ch-Ua-Platform": '"Windows"',
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "cross-site",
"Pragma": "no-cache",
"Cache-Control": "no-cache",
"DNT": "1",
"Upgrade-Insecure-Requests": "1",
"Connection": "keep-alive"
};
if (path.endsWith(".vtt")) {
headers["Accept"] = "text/vtt, */*;q=0.1";
headers["Sec-Fetch-Dest"] = "track";
}
const mirrorDomains = ["watching.onl", "pixvideo.skin", "trycloud.pro", "sugevideo.xyz", "cloudbuzz.lol", "cloudvideo.lat", "dnscd.onl", "vidlink.pro", "vidsrc.me", "vidsrc.to", "rabbitstream.net", "freewave.lol", "livedns.my", "freeproxy.io", "megacz.io"];
let isMirror = false;
if (mirrorDomains.some(d => hostname.includes(d))) {
headers["Referer"] = "https://vidwish.live/";
headers["Origin"] = "https://vidwish.live";
isMirror = true;
} else if (hostname.includes("megaplay.buzz")) {
headers["Referer"] = "https://megaplay.buzz/";
headers["Origin"] = "https://megaplay.buzz";
} else if (hostname.includes("vidstreaming") || hostname.includes("vizcloud") || hostname.includes("vidcloud")) {
headers["Referer"] = "https://aniwatchtv.to/";
headers["Origin"] = "https://aniwatchtv.to";
} else if (hostname.includes("megacloud")) {
headers["Referer"] = "https://megacloud.tv/";
headers["Origin"] = "https://megacloud.tv";
} else if (hostname.includes("vidwish.live") || hostname.includes("vidlink.pro")) {
headers["Referer"] = "https://vidwish.live/";
headers["Origin"] = "https://vidwish.live";
} else if (hostname.includes("hianimez.xyz")) {
headers["Referer"] = "https://hianimez.xyz/";
headers["Origin"] = "https://hianimez.xyz";
}
const isSensitive = hostname.includes("megacloud") || hostname.includes("vizcloud") || hostname.includes("vidstreaming") || hostname.includes("sugevideo.xyz") || isMirror;
if (isSensitive) {
const randomIP = `${100 + Math.floor(Math.random() * 150)}.${10 + Math.floor(Math.random() * 200)}.${1 + Math.floor(Math.random() * 250)}.${1 + Math.floor(Math.random() * 250)}`;
headers["X-Forwarded-For"] = randomIP;
headers["X-Real-IP"] = randomIP;
}
return { headers, isMirror };
}
function resolveURL(href, base) {
try {
return new URL(href, base).toString();
} catch {
return href;
}
}
function isVideoSegment(rawURL) {
const lower = rawURL.toLowerCase();
// Manifests are NOT segments
if (lower.split('?')[0].endsWith('.m3u8')) return false;
if (lower.split('?')[0].match(/\.(ts|m4s|mp4|m4v|m2ts)$/)) return true;
const segmentPatterns = ['seg-', 'segment-', 'chunk-', 'piece-', 'variant', 'v1-', 'v2-', 'v3-', 'v-a1', 'f1-', 'master'];
// Removed 'index' from patterns as it often appears in playlist names
return segmentPatterns.some(p => lower.includes(p));
}
function isM3U8URL(rawURL, needsForce = false) {
const lower = rawURL.toLowerCase();
const path = lower.split('?')[0];
if (path.endsWith('.m3u8') || path.endsWith('.m3u') || lower.includes('m3u8')) return true;
if (needsForce && !isVideoSegment(rawURL)) {
const segOrAsset = ['.ts', '.m4s', '.mp4', '.jpg', '.jpeg', '.png', '.webp', '.js', '.css'];
if (!segOrAsset.some(ext => path.endsWith(ext))) return true;
}
return false;
}
const handleProxy = async (req, res) => {
const targetUrl = req.query.url;
const path = req.path;
// CORS is handled by the middleware, removing manual headers to avoid duplicates
if (!targetUrl) return res.status(400).send('Missing URL');
const headersParam = req.query.headers;
let additionalHeaders = {};
if (headersParam) {
try { additionalHeaders = JSON.parse(decodeURIComponent(headersParam)); } catch (e) { }
}
const range = req.headers['range'];
if (range) additionalHeaders['Range'] = range;
const { headers: domainHeaders, isMirror } = generateHeadersForDomain(targetUrl);
// Header Precedence: Additional headers from the client override defaults,
// but mirrors need stealth headers to avoid 403, so we only override if client didn't provide them.
const requestHeaders = { ...domainHeaders, ...additionalHeaders };
if (isMirror) {
requestHeaders["Referer"] = additionalHeaders["Referer"] || domainHeaders["Referer"];
requestHeaders["Origin"] = additionalHeaders["Origin"] || domainHeaders["Origin"];
}
const sensitiveDomains = ["megacloud", "vizcloud", "vidstreaming", "sugevideo.xyz"];
if (sensitiveDomains.some(d => targetUrl.includes(d)) && !isMirror) {
delete requestHeaders["X-Forwarded-For"];
delete requestHeaders["X-Real-IP"];
}
try {
let response = await fetch(targetUrl, { headers: requestHeaders });
// Robust 403 Retry Logic for segments and manifests
if (response.status === 403) {
console.warn(`[403 Retry] Forbidden for ${targetUrl}, attempting failover referers...`);
const retryReferers = [
"", // No referer
"https://vidwish.live/",
"https://megaplay.buzz/",
"https://hianime.to/",
"https://aniwatchtv.to/"
];
for (const ref of retryReferers) {
const retryHeaders = { ...requestHeaders };
if (ref) {
retryHeaders["Referer"] = ref;
try { retryHeaders["Origin"] = new URL(ref).origin; } catch (e) { }
} else {
delete requestHeaders["Referer"];
delete requestHeaders["Origin"];
}
// Some origins block random IPs, try removing them on retry
delete retryHeaders["X-Forwarded-For"];
delete retryHeaders["X-Real-IP"];
const retryResp = await fetch(targetUrl, { headers: retryHeaders });
if (retryResp.ok) {
console.log(`[403 Retry Success] Found working referer: "${ref}" for ${targetUrl}`);
response = retryResp;
break;
}
}
}
if (!response.ok) {
console.error(`[Upstream Error] Status ${response.status} | URL: ${targetUrl} | Referer: ${requestHeaders["Referer"] || 'None'}`);
return res.status(response.status).send('Upstream Error');
}
const ignoredHeaders = ['access-control-allow-origin', 'access-control-allow-credentials', 'access-control-allow-methods', 'access-control-allow-headers', 'vary', 'set-cookie', 'server', 'content-length'];
response.headers.forEach((value, key) => {
if (!ignoredHeaders.includes(key.toLowerCase())) {
res.setHeader(key, value);
}
});
if (path === '/ts-proxy' || isVideoSegment(targetUrl)) {
res.setHeader('Content-Type', 'video/mp2t');
}
if (path === '/proxy') {
const text = await response.text();
if (!text.trim().startsWith('#EXTM3U')) {
return res.send(text);
}
const protocol = req.headers['x-forwarded-proto'] || req.protocol;
const host = req.headers['x-forwarded-host'] || req.get('host');
const origin = `${protocol}://${host}`;
const headersSuffix = headersParam ? `&headers=${encodeURIComponent(headersParam)}` : '';
const lines = text.split('\n');
const newLines = [];
const forceDomains = ['pixvideo.skin', 'watching.onl', 'trycloud.pro', 'sugevideo.xyz', 'vidlink.pro', 'vidsrc.me', 'vidsrc.to', 'sugevideo', 'vidwish.live', 'qzz.io', 'rabbitstream', 'megaplay', 'vizcloud', 'vidstreaming', 'cloudbuzz.lol', 'cloudvideo.lat', 'dnscd.onl', 'freewave.lol', 'livedns.my', 'freeproxy.io', 'megacz.io'];
const realAssets = ['.js', '.css', '.json', '.png', '.jpg', '.jpeg', '.webp', '.ico', '.svg', '.woff2'];
const isTargetForceProxd = forceDomains.some(d => targetUrl.includes(d));
for (let line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) {
if (trimmed.includes('URI=')) {
line = line.replace(/URI="([^"]+)"/g, (match, originalURI) => {
const resolved = resolveURL(originalURI, targetUrl);
const isRealAsset = realAssets.some(ext => resolved.toLowerCase().endsWith(ext));
const needsForce = isTargetForceProxd || forceDomains.some(d => resolved.includes(d));
const isPlaylist = isM3U8URL(resolved, needsForce);
const isSegment = isVideoSegment(resolved);
if (isPlaylist) {
return `URI="${origin}/proxy?url=${encodeURIComponent(resolved)}${headersSuffix}"`;
}
if (isSegment) {
return `URI="${origin}/ts-proxy?url=${encodeURIComponent(resolved)}${headersSuffix}"`;
}
if (needsForce) {
if (isRealAsset) return `URI="${resolved}"`;
return `URI="${origin}/fetch?url=${encodeURIComponent(resolved)}${headersSuffix}"`;
}
return match;
});
}
newLines.push(line);
continue;
}
const resolved = resolveURL(trimmed, targetUrl);
const isRealAsset = realAssets.some(ext => resolved.toLowerCase().endsWith(ext));
const needsForce = isTargetForceProxd || forceDomains.some(d => resolved.includes(d));
const isPlaylist = isM3U8URL(resolved, needsForce);
const isSegment = isVideoSegment(resolved);
if (isPlaylist) {
newLines.push(`${origin}/proxy?url=${encodeURIComponent(resolved)}${headersSuffix}`);
} else if (isSegment) {
newLines.push(`${origin}/ts-proxy?url=${encodeURIComponent(resolved)}${headersSuffix}`);
} else if (needsForce) {
if (isRealAsset) {
newLines.push(resolved);
} else {
newLines.push(`${origin}/fetch?url=${encodeURIComponent(resolved)}${headersSuffix}`);
}
} else {
newLines.push(resolved);
}
}
res.setHeader('Content-Type', 'application/vnd.apple.mpegurl');
res.setHeader('Cache-Control', 'public, max-age=60');
return res.send(newLines.join('\n'));
}
const isSegment = path === '/ts-proxy' || isVideoSegment(targetUrl);
if (isSegment) {
res.setHeader('Content-Type', 'video/mp2t');
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
} else {
res.setHeader('Cache-Control', 'public, max-age=7200');
}
response.body.pipe(res);
} catch (e) {
res.status(500).send('Proxy Error: ' + e.message);
}
};
app.get('/proxy', handleProxy);
app.get('/ts-proxy', handleProxy);
app.get('/fetch', handleProxy);
app.get('/', (req, res) => {
res.send('M3U8 Proxy for Hugging Face is running.');
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});