import cluster from 'cluster' import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import dotenv from 'dotenv'; import http from 'http'; import { Readable } from 'stream'; import { CACHE_TTL } from './config.js'; import { handleSubtitleMovie, handleSubtitleTv } from './src/routes/subtitles.js'; import { handleDownloadMovie, handleDownloadTv } from './src/routes/downloads/main.js'; import { handleHealth } from './src/routes/health.js'; import { wrapUrl } from './src/utils/proxy.js'; import { handleTestRoute, handleDebugRoute } from './src/routes/test.js'; import { getUA, validateTmdbId } from './src/utils/helpers.js'; import VylaSDK from '@vyla-entertainment/sdk'; dotenv.config(); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const IS_HF = !!process.env.SPACE_ID; const ENABLE_DEBUG_ROUTE = process.env.ENABLE_DEBUG_ROUTE === 'true'; const PORT = process.env.PORT || 7860; const HF_FETCH_TIMEOUT = 12000; const EARLY_CLOSE_MS = IS_HF ? 20000 : 14000; const MAX_GLOBAL_TEST_CONCURRENCY = IS_HF ? 30 : 300; const MAX_OUTBOUND_FETCH_CONCURRENCY = IS_HF ? 60 : 400; const MAX_SHARED_INFLIGHT = 2000; const MAX_IPC_PENDING = 2000; const FALLBACK_BASE = ''; const LOGO_TEXT = (() => { try { return fs.readFileSync(path.join(__dirname, 'public/assets/title.txt'), 'utf8'); } catch { return ''; } })(); const GA_MEASUREMENT_ID = process.env.GA_MEASUREMENT_ID; const GA_API_SECRET = process.env.GA_API_SECRET; const M3U8_REGEX = /\.m3u8?(\?|$)|mpegurl|m3u8/i; const TIKTOK_REGEX = /tiktokcdn\.com|ibyteimg\.com/i; const STRIP_REGEX = /seg\.html|enproxy|letsgocdn\d+\.shop/i; const STRIP_TEST_FAST = /seg\.html|enproxy|tiktokcdn|ibyteimg/i; const URI_REPLACE = /URI="([^"]+)"/g; const CORS_HEADERS = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS, PATCH, HEAD', 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With, Origin, Accept, Access-Control-Request-Method, Access-Control-Request-Headers, X-Session-Token', 'Access-Control-Allow-Credentials': 'false', 'Access-Control-Max-Age': '86400', }; const JSON_CORS = { 'Content-Type': 'application/json', ...CORS_HEADERS }; const ROUTE_PATTERNS = { subtitleMovie: /^\/(?:api\/)?subtitles?\/movie\/([^/]+)$/, subtitleTv: /^\/(?:api\/)?subtitles?\/tv\/([^/]+)\/([^/]+)\/([^/]+)$/, debug: /^\/(?:api\/)?debug\/([^/]+)$/, test: /^\/(?:api\/)?test\/([^/]+)$/, downloadMovie: /^\/(?:api\/)?downloads?\/movie\/([^/]+)$/, downloadTv: /^\/(?:api\/)?downloads?\/tv\/([^/]+)\/([^/]+)\/([^/]+)$/, }; const TMDB_API_KEY = process.env.TMDB_API_KEY; if (!TMDB_API_KEY) console.warn('Warning: TMDB_API_KEY not set, imdb-required sources will be unavailable'); // Internal VylaSDK instance const sdk = new VylaSDK({ tmdbApiKey: TMDB_API_KEY }); if (cluster.isPrimary) { const cpus = (await import('os')).default.cpus().length; const workerCount = IS_HF ? Math.min(cpus, 2) : (process.env.WORKER_COUNT ? Math.max(1, parseInt(process.env.WORKER_COUNT, 10)) : 1); const sharedCache = new Map(); const SHARED_CACHE_MAX = 1500; const pruneCache = () => { const now = Date.now(); for (const [k, v] of sharedCache) { if (now - v.ts > v.ttl) sharedCache.delete(k); } if (sharedCache.size > SHARED_CACHE_MAX) { const overflow = sharedCache.size - SHARED_CACHE_MAX; const it = sharedCache.keys(); for (let i = 0; i < overflow; i++) { const k = it.next().value; if (k === undefined) break; sharedCache.delete(k); } } }; const pruneTimer = setInterval(pruneCache, 30000); pruneTimer.unref(); for (let i = 0; i < workerCount; i++) { const w = cluster.fork({ WORKER_ID: String(i) }); w.on('online', () => w.send({ type: 'worker:id', id: i })); } cluster.on('message', (worker, msg) => { if (!msg?.type) return; if (msg.type === 'cache:get') { const entry = sharedCache.get(msg.key); const now = Date.now(); if (entry && now - entry.ts <= entry.ttl) { worker.send({ type: 'cache:hit', id: msg.id, value: entry.value }); } else { sharedCache.delete(msg.key); worker.send({ type: 'cache:miss', id: msg.id }); } return; } if (msg.type === 'cache:set') { sharedCache.set(msg.key, { value: msg.value, ts: Date.now(), ttl: msg.ttl || CACHE_TTL }); if (sharedCache.size > SHARED_CACHE_MAX) pruneCache(); for (const id in cluster.workers) { if (cluster.workers[id] !== worker) { try { cluster.workers[id]?.send({ type: 'cache:push', key: msg.key, value: msg.value, ttl: msg.ttl || CACHE_TTL }); } catch { } } } } }); const watchPaths = [ fileURLToPath(import.meta.url), './config.js', './src/routes/subtitles.js', './src/routes/downloads/main.js', './src/routes/health.js', ]; const intentionallyKilled = new Set(); let restarting = false; const scheduleRestart = () => { if (restarting) return; restarting = true; setTimeout(() => { for (const id in cluster.workers) { const w = cluster.workers[id]; if (!w) continue; intentionallyKilled.add(w.process.pid); w.kill(); } restarting = false; }, 500); }; // fs.watch('./src/sources', { persistent: false }, scheduleRestart); watchPaths.forEach(f => { try { fs.watch(f, scheduleRestart); } catch { } }); let pendingForks = 0; let nextWorkerId = workerCount; cluster.on('exit', (worker, code, signal) => { const isIntentional = code === 0 || intentionallyKilled.delete(worker.process.pid); const delay = isIntentional ? 0 : Math.min(++pendingForks * 1000, 5000); setTimeout(() => { pendingForks = Math.max(0, pendingForks - 1); const id = nextWorkerId++; const w = cluster.fork({ WORKER_ID: String(id) }); w.on('online', () => w.send({ type: 'worker:id', id })); }, delay); }); await new Promise(() => { }); } const _nativeFetch = globalThis.fetch; async function sendGAEvent(eventName, params = {}, clientId = null) { if (!GA_MEASUREMENT_ID || !GA_API_SECRET) return; const cid = clientId || `server.${Date.now()}.${Math.random().toString(36).slice(2)}`; const payload = { client_id: cid, timestamp_micros: String(Date.now() * 1000), non_personalized_ads: false, events: [{ name: eventName, params: { engagement_time_msec: 100, session_id: cid, ...params, }, }], }; try { const res = await fetch( `https://www.google-analytics.com/mp/collect?measurement_id=${GA_MEASUREMENT_ID}&api_secret=${GA_API_SECRET}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) } ); } catch (err) { } } async function validateGAConfig() { if (!GA_MEASUREMENT_ID || !GA_API_SECRET) { return; } const payload = { client_id: 'debug.startup', events: [{ name: 'server_start', params: { engagement_time_msec: 100, session_id: 'debug.startup' } }], }; try { const validateRes = await fetch( `https://www.google-analytics.com/debug/mp/collect?measurement_id=${GA_MEASUREMENT_ID}&api_secret=${GA_API_SECRET}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) } ); const body = await validateRes.json(); const issues = body?.validationMessages ?? []; if (issues.length === 0) { await sendGAEvent('server_start', { uptime: 0 }, 'debug.startup'); } } catch (err) { } } let outboundFetchActive = 0; const outboundFetchQueue = []; function acquireFetchSlot() { if (outboundFetchActive < MAX_OUTBOUND_FETCH_CONCURRENCY) { outboundFetchActive++; return Promise.resolve(); } return new Promise(resolve => outboundFetchQueue.push(resolve)); } function releaseFetchSlot() { const next = outboundFetchQueue.shift(); if (next) next(); else outboundFetchActive = Math.max(0, outboundFetchActive - 1); } globalThis.fetch = async (url, opts) => { const signal = opts?.signal ?? AbortSignal.timeout(HF_FETCH_TIMEOUT); await acquireFetchSlot(); try { return await _nativeFetch(url, opts?.signal ? opts : { ...opts, signal }); } finally { releaseFetchSlot(); } }; class LRUCache { #max; #ttl; #map; constructor(max, ttl) { this.#max = max; this.#ttl = ttl; this.#map = new Map(); } get(key) { const entry = this.#map.get(key); if (!entry) return undefined; if (Date.now() - entry.ts > this.#ttl) { this.#map.delete(key); return undefined; } this.#map.delete(key); this.#map.set(key, entry); return entry.val; } set(key, val) { if (this.#map.has(key)) this.#map.delete(key); else if (this.#map.size >= this.#max) this.#map.delete(this.#map.keys().next().value); this.#map.set(key, { val, ts: Date.now() }); } has(key) { const entry = this.#map.get(key); if (!entry) return false; if (Date.now() - entry.ts > this.#ttl) { this.#map.delete(key); return false; } return true; } get size() { return this.#map.size; } } const mainCache = new LRUCache(600, CACHE_TTL); const hlsVerifyCache = new LRUCache(400, 180_000); const testResultCache = new LRUCache(400, 90_000); const sharedInflight = new Map(); let ipcIdCounter = 0; const ipcPending = new Map(); const ipcSend = (msg) => new Promise(resolve => { if (ipcPending.size >= MAX_IPC_PENDING) { resolve(null); return; } const id = ++ipcIdCounter; ipcPending.set(id, resolve); try { process.send({ ...msg, id }); } catch { ipcPending.delete(id); resolve(null); return; } setTimeout(() => { if (ipcPending.has(id)) { ipcPending.delete(id); resolve(null); } }, 150); }); process.on('message', (msg) => { if (!msg) return; if (msg.type === 'worker:id') return; if (msg.type === 'cache:hit' || msg.type === 'cache:miss') { const resolve = ipcPending.get(msg.id); if (resolve) { ipcPending.delete(msg.id); resolve(msg); } return; } if (msg.type === 'cache:push' && msg.key && msg.value !== undefined) { mainCache.set(msg.key, msg.value); testResultCache.set(msg.key, msg.value); } }); async function sharedCacheGet(key) { const local = mainCache.get(key); if (local !== undefined) return local; if (!process.send) return undefined; const reply = await ipcSend({ type: 'cache:get', key }); if (reply?.type === 'cache:hit') { mainCache.set(key, reply.value); return reply.value; } return undefined; } function sharedCacheSet(key, value, ttl) { mainCache.set(key, value); testResultCache.set(key, value); if (process.send) { try { process.send({ type: 'cache:set', key, value, ttl: ttl || CACHE_TTL }); } catch { } } } function getSharedCached(key, fn, ttl) { const local = mainCache.get(key); if (local !== undefined) return Promise.resolve(local); const inflight = sharedInflight.get(key); if (inflight) return inflight; if (sharedInflight.size >= MAX_SHARED_INFLIGHT) { return (async () => { const shared = await sharedCacheGet(key); if (shared !== undefined) return shared; return fn(); })(); } const p = withTimeout((async () => { const shared = await sharedCacheGet(key); if (shared !== undefined) return shared; const val = await fn(); if (val != null) sharedCacheSet(key, val, ttl); return val; })(), 60_000).finally(() => sharedInflight.delete(key)); sharedInflight.set(key, p); return p; } const safeDecode = s => { try { return decodeURIComponent(s); } catch { return s; } }; const jitter = ms => ms > 0 ? new Promise(r => setTimeout(r, Math.random() * ms)) : Promise.resolve(); const withTimeout = (promise, ms) => Promise.race([promise, new Promise(r => setTimeout(() => r(null), ms))]); async function withRetry(fn, attempts = 2, delay = 300) { for (let i = 0; i < attempts; i++) { try { const result = await fn(); if (result != null) return result; } catch (err) { if (i === attempts - 1) throw err; await new Promise(r => setTimeout(r, delay + Math.random() * delay * 0.5)); } } return null; } const ACTIVE_SOURCES = sdk.getSources(true); const PROXY_PARAM_MAP = new Map(ACTIVE_SOURCES.map(cfg => [cfg.proxyParam, cfg])); const BLOCKED_IPS = new Set([]); let globalTestConcurrency = 0; const testQueue = []; function runTestQueue() { while (testQueue.length > 0 && globalTestConcurrency < MAX_GLOBAL_TEST_CONCURRENCY) { testQueue.shift().resolve(); globalTestConcurrency++; } } async function acquireTestSlot() { if (globalTestConcurrency < MAX_GLOBAL_TEST_CONCURRENCY) { globalTestConcurrency++; return; } await new Promise(resolve => testQueue.push({ resolve })); } function releaseTestSlot() { globalTestConcurrency = Math.max(0, globalTestConcurrency - 1); runTestQueue(); } const getAbsoluteBase = host => (host.startsWith('localhost') || host.startsWith('127.0.0.1')) ? `http://${host}` : `https://${host}`; function buildM3u8Rewriter(rewriteSegments) { return function rewrite(body, url, extraParam, absoluteBase) { const safeBase = absoluteBase.replace(/^https:\/\/(localhost|127\.0\.0\.1)/, 'http://$1'); const qmark = url.indexOf('?'); const base = qmark === -1 ? url : url.slice(0, qmark); const dir = base.slice(0, base.lastIndexOf('/') + 1); const schemeEnd = url.indexOf('//') + 2; const originBase = url.slice(0, url.indexOf('/', schemeEnd)); const prefix = `${safeBase}/api?url=`; const lines = body.split('\n'); for (let i = 0; i < lines.length; i++) { const t = lines[i].trim(); if (!t) continue; if (t.charCodeAt(0) === 35) { let uIdx = t.indexOf('URI="'); if (uIdx !== -1) { const end = t.indexOf('"', uIdx + 5); if (end !== -1) { const uri = t.slice(uIdx + 5, end); const abs = uri.startsWith('http') ? uri : uri.startsWith('/') ? originBase + uri : dir + uri; lines[i] = t.slice(0, uIdx + 5) + prefix + encodeURIComponent(abs) + extraParam + '"' + t.slice(end + 1); } } } else { const abs = t.startsWith('http') ? t : t.startsWith('/') ? originBase + t : dir + t; lines[i] = rewriteSegments ? `${prefix}${encodeURIComponent(abs)}${extraParam}${STRIP_TEST_FAST.test(abs) ? '&tt=1' : ''}` : abs; } } return lines.join('\n'); }; } const rewriteM3u8 = buildM3u8Rewriter(true); const rewriteM3u8KeyOnly = buildM3u8Rewriter(false); async function fetchUpstream(url, extraHeaders = {}, timeoutMs = 30_000) { let current = url.startsWith('http://') ? 'https://' + url.slice(7) : url; const headers = { 'User-Agent': getUA(), ...extraHeaders }; const opts = { headers, redirect: 'manual', signal: AbortSignal.timeout(timeoutMs) }; for (let i = 0; i <= 5; i++) { const res = await _nativeFetch(current, opts); if (res.status < 300 || res.status >= 400 || !res.headers.has('location')) { return res; } res.body?.cancel(); const loc = res.headers.get('location'); current = loc.startsWith('http') ? (loc.startsWith('http://') ? 'https://' + loc.slice(7) : loc) : new URL(loc, current).href; } throw new Error('redirect loop'); } async function verifyStream(rawUrl, sourceKey, extraHeaders = {}) { const cfg = sdk.getSources().find(c => c.key === sourceKey); if (cfg?.skipVerify) return true; const cacheKey = `vstream-${rawUrl}`; const cached = hlsVerifyCache.get(cacheKey); if (cached !== undefined) return cached; try { const res = await _nativeFetch(rawUrl, { method: 'HEAD', headers: { 'User-Agent': getUA(), ...(cfg?.verifyHeaders ?? {}), ...extraHeaders }, redirect: 'follow', signal: AbortSignal.timeout(6_000), }); res.body?.cancel(); const ok = res.status < 400; hlsVerifyCache.set(cacheKey, ok); return ok; } catch { hlsVerifyCache.set(cacheKey, false); return false; } } async function verifyPlayable(proxiedUrl, extraHeaders = {}, skipProxyCheck = false) { if (IS_HF && proxiedUrl.includes('.hf.space/api?url=')) { try { const parsed = new URL(proxiedUrl); const rawUrl = decodeURIComponent(parsed.searchParams.get('url') || ''); const ph = parsed.searchParams.get('proxyHeaders'); if (ph) try { Object.assign(extraHeaders, JSON.parse(decodeURIComponent(ph))); } catch { } if (rawUrl) { proxiedUrl = rawUrl; skipProxyCheck = true; } } catch { } } const cached = hlsVerifyCache.get(proxiedUrl); if (cached !== undefined) return cached; const store = val => { hlsVerifyCache.set(proxiedUrl, val); return val; }; const fail = error => ({ ok: false, error }); try { const fetchHeaders = extraHeaders['User-Agent'] ? extraHeaders : { 'User-Agent': getUA(), ...extraHeaders }; const m3u8Res = await _nativeFetch(proxiedUrl, { signal: AbortSignal.timeout(12000), headers: fetchHeaders }); if (!m3u8Res.ok) { const val = fail(`m3u8 failed: ${m3u8Res.status}`); if (m3u8Res.status !== 429) store(val); return val; } const text = await m3u8Res.text(); if (/\.mpd(\?|$)/i.test(proxiedUrl) || text.includes(' { const url = s ? `https://api.themoviedb.org/3/tv/${cleanId}/season/${s}/episode/${e || 1}?api_key=${key}` : `https://api.themoviedb.org/3/movie/${cleanId}?api_key=${key}`; const res = await _nativeFetch(url, { signal: AbortSignal.timeout(5_000), }); if (!res.ok) { res.body?.cancel(); return { error: `TMDB API error: ${res.status}` }; } return res.json(); }, 1_800_000); } function applyCdnHeaders(cleanUrl, extraHeaders, sourceKey) { const cfg = sdk.getSources().find(c => c.key === sourceKey); if (!cfg?.cdnHeaders) return; for (const rule of cfg.cdnHeaders) { if (rule.pattern.test(cleanUrl)) { Object.assign(extraHeaders, rule.headers); return; } } } async function getSdkStreams(sourceKey, id, s, e, clientIP) { if (typeof sdk.getStreams === 'function') { try { const res = await sdk.getStreams(sourceKey, id, s, e, clientIP); if (res && (Array.isArray(res) ? res.length > 0 : Object.keys(res).length > 0)) { return res; } } catch { } } return sdk.getStream(sourceKey, id, s, e, clientIP); } function fetchSource(cfg, cacheKey, id, s, e, clientIP, absoluteBase) { function fixSource(url) { if (typeof url === 'string' && url.includes('/api?url=')) { return `${absoluteBase}/api?url=${encodeURIComponent(url)}`; } return url; } function applyFix(result) { if (!result) return result; const fixEntry = item => { if (typeof item === 'string') return fixSource(item); if (item && typeof item === 'object') { const target = item.url || item.file || item.streamUrl || item.link; if (target) return { ...item, url: fixSource(target) }; } return item; }; if (Array.isArray(result)) return result.map(fixEntry); if (Array.isArray(result.streams)) result.streams = result.streams.map(fixEntry); if (Array.isArray(result.allUrls)) result.allUrls = result.allUrls.map(fixEntry); if (Array.isArray(result.sources)) result.sources = result.sources.map(fixEntry); if (Array.isArray(result.urls)) result.urls = result.urls.map(fixEntry); if (typeof result.url === 'string') result.url = fixSource(result.url); return result; } if (cfg.skipCache) { return withTimeout( jitter(cfg.jitter).then(() => withRetry(() => getSdkStreams(cfg.key, id, s, e, clientIP), cfg.retries, 300)), cfg.timeout ).then(applyFix); } return withTimeout( jitter(cfg.jitter).then(() => getSharedCached(`${cfg.key}-${cacheKey}`, () => withRetry(() => getSdkStreams(cfg.key, id, s, e, clientIP), cfg.retries, 300)) ), cfg.timeout ).then(applyFix); } function normalizeCandidates(rawResult) { if (!rawResult) return []; const pool = []; if (Array.isArray(rawResult)) { pool.push(...rawResult); } else if (typeof rawResult === 'object') { if (Array.isArray(rawResult.streams)) pool.push(...rawResult.streams); if (Array.isArray(rawResult.allUrls)) pool.push(...rawResult.allUrls); if (Array.isArray(rawResult.sources)) pool.push(...rawResult.sources); if (Array.isArray(rawResult.urls)) pool.push(...rawResult.urls); if (rawResult.url) { pool.push({ url: rawResult.url, headers: rawResult.headers, quality: rawResult.quality, type: rawResult.type, name: rawResult.name, label: rawResult.label, language: rawResult.language }); } } else if (typeof rawResult === 'string') { pool.push({ url: rawResult }); } const seen = new Set(); const candidates = []; for (const item of pool) { if (!item) continue; const candidate = typeof item === 'object' ? { ...item, url: item.url || item.file || item.streamUrl || item.link } : { url: item }; if (typeof candidate.url !== 'string') continue; const trimmed = candidate.url.trim(); if (!trimmed || seen.has(trimmed)) continue; seen.add(trimmed); candidate.url = trimmed; candidates.push(candidate); } return candidates; } const LOOPBACK = `http://127.0.0.1:${PORT}`; const MAX_STREAMS_PER_SOURCE = 8; const VERIFY_BUDGET_MS = 15_000; function toLoopback(url, absoluteBase) { const variants = new Set([ absoluteBase, absoluteBase.replace(/^http:\/\//, 'https://'), absoluteBase.replace(/^https:\/\//, 'http://'), ]); for (const b of variants) { if (url.startsWith(`${b}/api?`)) return LOOPBACK + url.slice(b.length); } return url; } async function readHead(res, limit) { const reader = res.body?.getReader(); if (!reader) return Buffer.alloc(0); const chunks = []; let total = 0; try { while (total < limit) { const { done, value } = await reader.read(); if (done) break; chunks.push(value); total += value.length; } } finally { reader.cancel().catch(() => { }); } return Buffer.concat(chunks).subarray(0, limit); } function looksLikeMedia(buf) { if (buf.length < 8) return false; if (buf[0] === 0x47 && (buf.length < 189 || buf[188] === 0x47)) return true; const box = buf.subarray(4, 8).toString('latin1'); if (['ftyp', 'styp', 'moof', 'moov', 'sidx', 'mdat', 'free', 'wide'].includes(box)) return true; if (buf[0] === 0x49 && buf[1] === 0x44 && buf[2] === 0x33) return true; if (buf[0] === 0xFF && (buf[1] & 0xF0) === 0xF0) return true; if (buf[0] === 0x1A && buf[1] === 0x45 && buf[2] === 0xDF && buf[3] === 0xA3) return true; return false; } function firstUri(text, base) { for (const line of text.split('\n')) { const t = line.trim(); if (t && t.charCodeAt(0) !== 35) return new URL(t, base).href; } return null; } async function verifyServed(finalUrl, headers, absoluteBase, trace = []) { const cacheKey = `served-${finalUrl}`; const cached = hlsVerifyCache.get(cacheKey); if (cached !== undefined) { trace.push(`cache:${cached}`); return cached; } const store = ok => { hlsVerifyCache.set(cacheKey, ok); return ok; }; const fail = why => { trace.push(why); return store(false); }; const get = url => { const target = toLoopback(url, absoluteBase); trace.push(`GET ${target}`); return _nativeFetch(target, { headers: { 'User-Agent': getUA(), ...headers }, redirect: 'follow', signal: AbortSignal.timeout(10_000), }); }; const accept = async (r, limit, lenient = false) => { const ct = (r.headers.get('content-type') || '').toLowerCase(); if (!r.ok && r.status !== 206) { trace.push(`status ${r.status} ct=${ct}`); r.body?.cancel(); return null; } if (!lenient && (ct.includes('text/html') || ct.includes('json'))) { trace.push(`rejected ct=${ct}`); r.body?.cancel(); return null; } return readHead(r, limit); }; try { let res = await get(finalUrl); let buf = await accept(res, 65536); if (!buf) return fail('playlist fetch rejected'); let text = buf.toString('utf8'); let base = res.url || finalUrl; trace.push(`playlist head: ${JSON.stringify(text.slice(0, 160))}`); if (!/^\s*#EXTM3U/.test(text)) { if (text.includes(' /^#EXT-X-KEY:/.test(l.trim()) && !/METHOD=NONE/.test(l)); const encrypted = !!keyLine; if (keyLine) { const m = /URI="([^"]+)"/.exec(keyLine); if (m && !m[1].startsWith('data:')) { const kres = await get(new URL(m[1], base).href); const kb = await accept(kres, 64); if (!kb) return fail('key fetch rejected'); if (kb.length !== 16) return fail(`key length ${kb.length}`); } } const segUrl = firstUri(text, base); if (!segUrl) return fail('no segment uri'); const sres = await get(segUrl); const sb = await accept(sres, 4096, true); if (!sb) return fail('segment fetch rejected'); if (sb.length < 188) return fail(`segment too short ${sb.length}`); if (encrypted) { const bad = /^\s*[<{]/.test(sb.toString('latin1', 0, 16)); return bad ? fail('encrypted segment looks like text') : store(true); } if (looksLikeMedia(sb)) return store(true); return fail(`segment unrecognized, first bytes ${sb.subarray(0, 12).toString('hex')}`); } catch (err) { trace.push(`threw ${err.message}`); return false; } } async function verifyCandidate(candidate, sourceKey, absoluteBase, trace = []) { if (!candidate?.url) return null; let wrapped = wrapUrl(candidate, sourceKey, absoluteBase, sdk); if (!wrapped || (!wrapped.includes('/api?url=') && !wrapped.includes('/api/?url='))) { const cfg = sdk.getSources().find(c => c.key === sourceKey); const extraParam = cfg?.proxyParam ? `&${cfg.proxyParam}=1` : '&vn=1'; const headersParam = candidate.headers && Object.keys(candidate.headers).length ? `&proxyHeaders=${encodeURIComponent(JSON.stringify(candidate.headers))}` : ''; wrapped = `${absoluteBase}/api?url=${encodeURIComponent(candidate.url)}${extraParam}${headersParam}`; } const viaProxy = wrapped !== candidate.url || toLoopback(wrapped, absoluteBase) !== wrapped; trace.push(`wrapped=${wrapped} viaProxy=${viaProxy}`); const ok = await withTimeout( verifyServed(wrapped, viaProxy ? {} : (candidate.headers ?? {}), absoluteBase, trace), VERIFY_BUDGET_MS ); if (!ok) { trace.push('verify failed or timed out'); return null; } const out = { url: wrapped, raw_url: candidate.url, proxied: true }; if (!viaProxy && candidate.headers && Object.keys(candidate.headers).length) out.headers = candidate.headers; for (const k of ['quality', 'type', 'name', 'label', 'language', 'resolution']) { if (candidate[k] !== undefined) out[k] = candidate[k]; } return out; } async function handleTestSource(sdk, sourceKey, id, s, e, clientIP, host) { const start = Date.now(); const cfg = sdk.getSources().find(c => c.key === sourceKey); const absoluteBase = getAbsoluteBase(host); const respond = result => ({ status: 200, body: JSON.stringify({ source: sourceKey, id, s: s || null, e: e || null, ok: !!result?.ok, url: result?.ok ? result.url : null, streams: result?.ok ? result.streams : [], raw_url: result?.raw_url ?? null, elapsed_ms: Date.now() - start, error: result?.ok ? null : (result?.error ?? null), ...(result?.debug ? { debug: result.debug } : {}), }, null, 2), contentType: 'application/json', }); if (!cfg) return respond({ ok: false, error: 'unknown source' }); if (cfg.disabled) return respond({ ok: false, error: 'source disabled' }); const cacheKey = `test-${sourceKey}-${id}-${s ?? ''}-${e ?? ''}-${host}`; const localCached = testResultCache.get(cacheKey); if (localCached !== undefined) return respond(localCached); const shared = await sharedCacheGet(cacheKey); if (shared !== undefined) { testResultCache.set(cacheKey, shared); return respond(shared); } const inflightKey = `inflight-${cacheKey}`; const existing = sharedInflight.get(inflightKey); if (existing) { try { return respond((await existing) ?? { ok: false, error: 'timed out' }); } catch { return respond({ ok: false, error: 'deduped request failed' }); } } if (sharedInflight.size >= MAX_SHARED_INFLIGHT) { return respond({ ok: false, error: 'server busy, try again shortly' }); } await acquireTestSlot(); const testPromise = withTimeout((async () => { try { let rawResult = null, fetchError = null; try { rawResult = await fetchSource(cfg, `${id}-${s ?? ''}-${e ?? ''}`, id, s, e, clientIP, absoluteBase); if (!rawResult) rawResult = await withTimeout(getSdkStreams(sourceKey, id, s, e, clientIP), 20_000); } catch (err) { fetchError = err.message; } const seen = new Set(); const candidates = normalizeCandidates(rawResult) .filter(c => { const u = typeof c?.url === 'string' ? c.url : null; if (!u || seen.has(u)) return false; seen.add(u); return true; }) .slice(0, MAX_STREAMS_PER_SOURCE); const traces = []; const verified = (await Promise.all( candidates.map(c => { const trace = []; traces.push({ url: c.url, trace }); return verifyCandidate(c, sourceKey, absoluteBase, trace).catch(err => { trace.push(`threw ${err.message}`); return null; }); }) )).filter(Boolean); if (!verified.length) { return { ok: false, url: null, raw_url: candidates[0]?.url || null, error: fetchError || (candidates.length ? 'no playable streams' : 'no streams returned'), debug: traces, }; } const result = { ok: true, url: verified[0].url, raw_url: verified[0].raw_url, streams: verified }; if (!rawResult?.skipCache) { testResultCache.set(cacheKey, result); sharedCacheSet(cacheKey, result, cfg.testCacheTtl ?? 90_000); } return result; } finally { releaseTestSlot(); sharedInflight.delete(inflightKey); } })(), 45_000); sharedInflight.set(inflightKey, testPromise); const result = await testPromise; return respond(result ?? { ok: false, error: 'timed out' }); } async function streamSources(sources, id, s, e, clientIP, absoluteBase, res) { const sent = new Set(); const host = absoluteBase.replace(/https?:\/\//, ''); const debugResults = []; let closed = false; const onClose = () => { closed = true; }; res.on('close', onClose); res.on('error', onClose); const safeWrite = data => { if (closed || res.writableEnded || res.destroyed) return false; try { res.write(data); return true; } catch { closed = true; return false; } }; const promises = sources.map(async cfg => { if (closed) return; try { const result = await handleTestSource(sdk, cfg.key, id, s, e, clientIP, host); if (closed) return; const parsed = JSON.parse(result.body); debugResults.push({ source: cfg.key, ok: parsed.ok, streams: parsed.streams?.length ?? 0, error: parsed.error || null, elapsed_ms: parsed.elapsed_ms, }); if (!parsed.ok) return; const label = cfg.label ?? cfg.key; let n = 0; for (const stream of parsed.streams || []) { if (sent.has(stream.url)) continue; sent.add(stream.url); n++; const { raw_url, ...publicStream } = stream; const streamLabel = publicStream.label || publicStream.name || (publicStream.quality ? `${label} (${publicStream.quality})` : (n > 1 ? `${label} ${n}` : label)); safeWrite(`data: ${JSON.stringify({ type: 'source', source: { source: cfg.key, index: n - 1, ...publicStream, label: streamLabel, }, })}\n\n`); } } catch (err) { debugResults.push({ source: cfg.key, ok: false, error: err.message }); } }); await Promise.race([ Promise.all(promises), new Promise(r => setTimeout(r, EARLY_CLOSE_MS)), ]); safeWrite(`data: ${JSON.stringify({ type: 'debug', results: debugResults })}\n\n`); return sent.size; } const respondJson = (status, data, extraHeaders) => ({ status, body: JSON.stringify(data), headers: extraHeaders ? { ...JSON_CORS, ...extraHeaders } : JSON_CORS, }); async function handleRequest(req, res) { const baseUrl = `http://${req.headers.host || 'localhost'}`; const reqUrl = new URL(req.url, baseUrl); const { pathname, searchParams } = reqUrl; const clientIP = req.headers['x-forwarded-for']?.split(',')[0].trim() || req.socket?.remoteAddress || null; if (BLOCKED_IPS.size && BLOCKED_IPS.has(clientIP)) return respondJson(403, { error: 'forbidden' }); if (req.method === 'OPTIONS') return { status: 204, body: '', headers: CORS_HEADERS }; if (pathname === '/' || pathname === '') { return { status: 200, body: `${LOGO_TEXT}\n\ndeveloped_by: @vyla-entertainment\ngitlab: https://gitlab.com/vyla-entertainment\ndocs: https://vyla.cc/stream-api/overview\ndmca: https://vyla.cc/misc/project/dmca`, headers: { 'Content-Type': 'text/plain; charset=utf-8', ...CORS_HEADERS }, }; } if (pathname === '/test' || pathname === '/api/test') { const tests = {}; ACTIVE_SOURCES.forEach(s => { tests[s.key] = { movie: `/api/test/155?source=${s.key}`, tv: `/api/test/1396?season=1&episode=1&source=${s.key}` }; }); return respondJson(200, tests); } if (pathname === '/health' || pathname === '/api/health') { const result = await handleHealth(sdk, mainCache); return { ...result, headers: { ...result.headers, ...CORS_HEADERS } }; } const absoluteBase = getAbsoluteBase(reqUrl.host); const getRequestedSources = () => { const raw = searchParams.get('sources')?.split(',').map(s => s.trim()).filter(Boolean) ?? []; return raw.length ? ACTIVE_SOURCES.filter(s => raw.includes(s.key)) : ACTIVE_SOURCES; }; const getRequestMeta = () => { const raw = { ip: clientIP, referer: req.headers['referer'] || null, origin: req.headers['origin'] || null, user_agent: req.headers['user-agent'] || null, host: req.headers['host'] || null, country: req.headers['cf-ipcountry'] || null, path: pathname, query: reqUrl.search || null, }; return Object.fromEntries(Object.entries(raw).filter(([, v]) => v !== null && v !== undefined)); }; const googleAnalytic = (event, extra = {}) => { if (!GA_MEASUREMENT_ID || !GA_API_SECRET) return; const gaClientId = (clientIP && clientIP !== '127.0.0.1' && clientIP !== '::1' && clientIP !== '::ffff:127.0.0.1') ? clientIP : `local.${Date.now()}.${Math.random().toString(36).slice(2, 9)}`; const safeName = event.replace(/-/g, '_'); const meta = getRequestMeta(); sendGAEvent(safeName, { ...meta, ...extra }, gaClientId); }; if (pathname === '/movie' || pathname === '/api/movie') { const id = searchParams.get('id'); if (!id) return respondJson(400, { error: 'missing id', route: '/movie?id=:tmdb_id', example: '/movie?id=155' }); const tmdbValidation = await validateTmdbId(id, 'movie'); if (!tmdbValidation.valid) { return respondJson(400, { error: tmdbValidation.error }); } res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'X-Accel-Buffering': 'no', ...CORS_HEADERS }); const [meta, subtitles] = await Promise.all([ getMetadata(id, null, null), sdk.getSubtitles(id, null, null) ]); if (!res.writableEnded && !res.destroyed) { try { res.write(`data: ${JSON.stringify({ type: 'meta', meta, subtitles })}\n\n`); } catch { return null; } } googleAnalytic('stream_movie', { id }); const total = await streamSources(getRequestedSources(), id, null, null, clientIP, absoluteBase, res); if (!res.writableEnded && !res.destroyed) { try { res.write(`data: ${JSON.stringify({ type: 'done', total })}\n\n`); res.end(); } catch { } } return null; } if (pathname === '/tv' || pathname === '/api/tv') { const id = searchParams.get('id'), s = searchParams.get('season'), e = searchParams.get('episode'); if (!id || !s || !e) return respondJson(400, { error: 'missing parameters', route: '/tv?id=:id&season=:s&episode=:e', example: '/tv?id=1396&season=1&episode=1' }); const tmdbValidation = await validateTmdbId(id, 'tv'); if (!tmdbValidation.valid) { return respondJson(400, { error: tmdbValidation.error }); } res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'X-Accel-Buffering': 'no', ...CORS_HEADERS }); const [meta, subtitles] = await Promise.all([ getMetadata(id, s, e), sdk.getSubtitles(id, s, e) ]); if (!res.writableEnded && !res.destroyed) { try { res.write(`data: ${JSON.stringify({ type: 'meta', meta, subtitles })}\n\n`); } catch { return null; } } googleAnalytic('stream_tv', { id, season: s, episode: e }); const total = await streamSources(getRequestedSources(), id, s, e, clientIP, absoluteBase, res); if (!res.writableEnded && !res.destroyed) { try { res.write(`data: ${JSON.stringify({ type: 'done', total })}\n\n`); res.end(); } catch { } } return null; } if (pathname === '/subtitle' || pathname === '/subtitles' || pathname === '/api/subtitle' || pathname === '/api/subtitles') { return respondJson(200, { routes: { movie: '/subtitles/movie/:id', tv: '/subtitles/tv/:id/:s/:e' }, examples: { movie: '/subtitles/movie/155', tv: '/subtitles/tv/1396/1/1' } }); } if (pathname === '/download' || pathname === '/downloads' || pathname === '/api/download' || pathname === '/api/downloads') { return respondJson(200, { routes: { movie: '/downloads/movie/:id', tv: '/downloads/tv/:id/:s/:e' }, examples: { movie: '/downloads/movie/155', tv: '/downloads/tv/1396/1/1' } }); } let match; match = ROUTE_PATTERNS.subtitleMovie.exec(pathname); if (match) { googleAnalytic('subtitles_movie', { id: match[1] }); return handleSubtitleMovie(match[1], CORS_HEADERS, sdk); } match = ROUTE_PATTERNS.subtitleTv.exec(pathname); if (match) { googleAnalytic('subtitles_tv', { id: match[1], season: match[2], episode: match[3] }); return handleSubtitleTv(match[1], match[2], match[3], CORS_HEADERS, sdk); } match = ROUTE_PATTERNS.downloadMovie.exec(pathname); if (match) { googleAnalytic('downloads_movie', { id: match[1] }); return handleDownloadMovie(match[1], CORS_HEADERS, sdk); } match = ROUTE_PATTERNS.downloadTv.exec(pathname); if (match) { googleAnalytic('downloads_tv', { id: match[1], season: match[2], episode: match[3] }); return handleDownloadTv(match[1], match[2], match[3], CORS_HEADERS, sdk); } match = ROUTE_PATTERNS.test.exec(pathname); if (match) { const result = await handleTestRoute(sdk, match, searchParams, clientIP, reqUrl.host, handleTestSource, googleAnalytic); return { status: result.status, body: result.body, headers: JSON_CORS }; } match = ENABLE_DEBUG_ROUTE ? ROUTE_PATTERNS.debug.exec(pathname) : null; if (match) { const result = await handleDebugRoute(sdk, match, searchParams, absoluteBase, _nativeFetch, verifyPlayable); return { status: result.status, body: result.body, headers: JSON_CORS }; } if (pathname === '/api' || pathname === '/api/') { const url = searchParams.get('url') || searchParams.get('proxy'); if (url) { if (IS_HF && searchParams.get('lm') === '1') { return { status: 302, body: '', headers: { 'Location': FALLBACK_BASE + req.url, ...CORS_HEADERS } }; } try { new URL(url); const extraHeaders = {}; const proxyHeaders = searchParams.get('proxyHeaders'); if (proxyHeaders) try { Object.assign(extraHeaders, JSON.parse(safeDecode(proxyHeaders))); } catch { } if (!extraHeaders['User-Agent'] && !extraHeaders['user-agent']) extraHeaders['User-Agent'] = getUA(); const vlHost = extraHeaders['x-vl-host']; delete extraHeaders['Host']; delete extraHeaders['x-vl-host']; if (vlHost) extraHeaders['Host'] = vlHost; let matchedSource = null; for (const [param, cfg] of PROXY_PARAM_MAP) { if (searchParams.has(param)) { matchedSource = cfg; break; } } let cleanUrl = url; if (matchedSource) { try { const qIndex = url.indexOf('?'); if (qIndex !== -1) { const params = new URLSearchParams(url.slice(qIndex + 1)); const headersParam = params.get('headers'); if (headersParam) { try { Object.assign(extraHeaders, JSON.parse(decodeURIComponent(headersParam))); } catch { } params.delete('headers'); } cleanUrl = `${url.slice(0, qIndex)}${params.toString() ? '?' + params.toString() : ''}`; } } catch { } applyCdnHeaders(cleanUrl, extraHeaders, matchedSource.key); } const upstream = await fetchUpstream(cleanUrl, extraHeaders, 30_000); const ct = (upstream.headers.get('content-type') || '').toLowerCase(); const looksLikeM3u8 = M3U8_REGEX.test(cleanUrl) || cleanUrl.includes('/playlist/') || cleanUrl.includes('/streamsvr/') || ct.includes('mpegurl') || ct.includes('m3u8'); if (looksLikeM3u8) { const text = await upstream.text(); if (text.trim().startsWith('#EXT') || /megacloud\.animanga\.fun\/(ts-proxy|proxy)/i.test(text.slice(0, 200))) { const isTesub = matchedSource?.proxyParam === 'tesub'; const extraParam = matchedSource ? `&${matchedSource.proxyParam}=1&proxyHeaders=${encodeURIComponent(JSON.stringify(extraHeaders))}` : '&vn=1'; const rewritten = isTesub ? rewriteM3u8KeyOnly(text, cleanUrl, extraParam, absoluteBase) : rewriteM3u8(text, matchedSource ? cleanUrl : url, extraParam, absoluteBase); return { status: 200, body: rewritten, headers: { 'Content-Type': 'application/vnd.apple.mpegurl', ...CORS_HEADERS } }; } return { status: 502, body: `expected m3u8 but got: ${text.slice(0, 100)}`, headers: { 'Content-Type': 'text/plain', 'Access-Control-Allow-Origin': '*' } }; } if (matchedSource) { const isTikTok = TIKTOK_REGEX.test(cleanUrl); const isMkv = cleanUrl.includes('.mkv') || ct.includes('matroska'); const isPngMasked = ct === 'image/png' || ct === 'image/jpeg' || /\.png(\?|$)/i.test(cleanUrl) || /letsgocdn\d+\.shop/i.test(cleanUrl); const needsStrip = searchParams.has('tt') || STRIP_REGEX.test(cleanUrl); const isKey = cleanUrl.endsWith('.key') || ct.includes('octet-stream') && cleanUrl.includes('mon.key'); if (isKey) { if (!upstream.ok) return { status: upstream.status, body: `upstream ${upstream.status}`, headers: { 'Content-Type': 'text/plain', ...CORS_HEADERS } }; const keyBytes = Buffer.from(await upstream.arrayBuffer()); return { status: 200, body: keyBytes, headers: { 'Content-Type': 'application/octet-stream', ...CORS_HEADERS } }; } if (isTikTok || isPngMasked || needsStrip) { if (!upstream.ok) return { status: upstream.status, body: `upstream ${upstream.status}`, headers: { 'Content-Type': 'text/plain', ...CORS_HEADERS } }; const full = Buffer.from(await upstream.arrayBuffer()); const stripped = (full[0] === 0x89 || full[0] === 0xFF || full[0] === 0x00) ? full.subarray(120) : full; return { status: 200, body: stripped, headers: { 'Content-Type': 'video/MP2T', ...CORS_HEADERS, 'Cache-Control': 'public, max-age=3600' } }; } if (!upstream.ok) return { status: upstream.status, body: `upstream ${upstream.status}`, headers: { 'Content-Type': 'text/plain', ...CORS_HEADERS } }; const rangeHeader = req.headers['range']; const streamUpstream = rangeHeader ? await _nativeFetch(cleanUrl, { headers: { 'User-Agent': getUA(), ...extraHeaders, 'Range': rangeHeader }, redirect: 'follow' }) : upstream; const responseHeaders = { 'Content-Type': isMkv || ct === 'application/octet-stream' ? 'video/mp4' : (ct || 'video/mp4'), 'Accept-Ranges': 'bytes', 'Cache-Control': 'no-store', ...CORS_HEADERS, }; if (streamUpstream.headers.has('content-length')) responseHeaders['Content-Length'] = streamUpstream.headers.get('content-length'); if (streamUpstream.headers.has('content-range')) responseHeaders['Content-Range'] = streamUpstream.headers.get('content-range'); return { status: rangeHeader && streamUpstream.status === 206 ? 206 : 200, stream: streamUpstream.body, headers: responseHeaders }; } const full = Buffer.from(await upstream.arrayBuffer()); const needsStrip = searchParams.has('tt') || TIKTOK_REGEX.test(url) || STRIP_REGEX.test(url); const stripped = (needsStrip && (full[0] === 0x89 || full[0] === 0xFF || full[0] === 0x00)) ? full.subarray(120) : full; return { status: 200, body: stripped, headers: { 'Content-Type': 'video/MP2T', ...CORS_HEADERS, 'Cache-Control': 'public, max-age=3600' } }; } catch (e) { return respondJson(502, { error: e.message }); } } if (searchParams.has('sources_meta')) { return respondJson(200, { sources: ACTIVE_SOURCES.map(c => ({ key: c.key, label: c.label, timeout: c.timeout })) }); } if (searchParams.has('tmdb_movie') || searchParams.has('tmdb_tv') || searchParams.has('tmdb_show') || searchParams.has('tmdb_season')) { const k = process.env.TMDB_API_KEY; if (!k) return respondJson(500, { error: 'no key' }); const tmdbId = searchParams.get('id'), tmdbSeason = searchParams.get('s'); let tmdbUrl; if (searchParams.has('tmdb_season')) tmdbUrl = `https://api.themoviedb.org/3/tv/${tmdbId}/season/${tmdbSeason}?api_key=${k}`; else if (searchParams.has('tmdb_movie')) tmdbUrl = `https://api.themoviedb.org/3/movie/${tmdbId}?api_key=${k}${searchParams.has('append_to_response') ? `&append_to_response=${searchParams.get('append_to_response')}` : ''}`; else tmdbUrl = `https://api.themoviedb.org/3/tv/${tmdbId}?api_key=${k}`; try { const r = await _nativeFetch(tmdbUrl); return respondJson(200, await r.json()); } catch (err) { return respondJson(500, { error: err.message }); } } return respondJson(400, { error: 'missing parameters' }); } return respondJson(404, { error: 'not found' }); } const server = http.createServer(async (req, res) => { req.socket.setTimeout(90_000); req.socket.setNoDelay(true); try { const result = await handleRequest(req, res); if (result === null || res.headersSent || res.writableEnded || res.destroyed) return; const headers = result.headers || {}; if (!headers['Content-Type']) headers['Content-Type'] = 'application/json'; res.writeHead(result.status, headers); if (result.stream) { const readable = Readable.fromWeb(result.stream); readable.on('error', () => { try { res.destroy(); } catch { } }); res.on('error', () => { try { readable.destroy(); } catch { } }); readable.pipe(res); } else { res.end(result.body ?? ''); } } catch (err) { if (!res.headersSent) { console.error(err) try { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end('{"error":"internal server error"}'); } catch { } } } }); server.keepAliveTimeout = 65_000; server.headersTimeout = 70_000; server.maxHeadersCount = 100; server.timeout = 90_000; server.on('error', err => { if (err.code !== 'EADDRINUSE') console.error('server error', err.message); }); await validateGAConfig(); server.listen(PORT, '0.0.0.0', () => console.log(`http://localhost:${PORT}`));