prox / server.js
recycleactor's picture
Upload server.js
7ff7a07 verified
Raw
History Blame
48.2 kB
/**
* Alloha + Cinemar + Turbo Proxy Server
* Endpoints: /alloha/streams, /alloha/player-info, /hls, /srt2vtt,
* /uakinogo/search, /uakinogo/stream, /uakinogo/embed,
* /turbo/search, /turbo/stream, /health
*/
const http = require('http');
const https = require('https');
// Load alloha with error handling β€” Puppeteer/Chromium may not be available on all platforms
let getStreams, closeBrowser;
try {
const alloha = require('./alloha');
getStreams = alloha.getStreams;
closeBrowser = alloha.closeBrowser;
console.log('[server] Alloha/Puppeteer loaded OK');
} catch(e) {
console.log('[server] Alloha/Puppeteer NOT available:', e.message);
getStreams = async () => { throw new Error('Puppeteer not available on this platform'); };
closeBrowser = async () => {};
}
const { turboSearch, getTurboStream } = require('./turbo');
const { getCinemarStream } = require('./cinemar');
const PORT = process.env.PORT || 3000;
const ALLOHA_HOST = 'streamalloha.live';
const ALLOHA_TOKEN = '7fda2b04f6ae5e0e228bda812b0dee';
const CORS = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': '*',
'Content-Type': 'application/json',
};
function jsonResp(res, data, status) {
res.writeHead(status || 200, CORS);
res.end(JSON.stringify(data));
}
function parseQuery(reqUrl) {
try {
const u = new URL('http://x' + reqUrl);
const q = {};
u.searchParams.forEach((v, k) => { q[k] = v; });
return { pathname: u.pathname, query: q };
} catch(e) {
return { pathname: reqUrl.split('?')[0], query: {} };
}
}
function readBody(req) {
return new Promise(resolve => {
let body = '';
req.setEncoding('utf8');
req.on('data', c => { body += c; });
req.on('end', () => resolve(body));
});
}
async function fetchRaw(url, headers = {}) {
let retries = 3;
while (retries > 0) {
try {
return await new Promise((resolve, reject) => {
let pu;
try {
pu = new URL(url);
} catch (e) {
return reject(new Error('Invalid URL: ' + url));
}
const mod = pu.protocol === 'https:' ? https : http;
const req = mod.get(url, {
headers: {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
'accept-language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
'accept-encoding': 'gzip, deflate, br',
'connection': 'close',
...headers
},
timeout: 15000
}, (res) => {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
let loc = res.headers.location;
if (!loc.startsWith('http')) loc = new URL(loc, url).href;
resolve(fetchRaw(loc, headers));
return;
}
let data = [];
res.on('data', (chunk) => data.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
res.on('end', () => {
try {
const buf = Buffer.concat(data);
const enc = String(res.headers['content-encoding'] || '').toLowerCase();
if (enc.includes('br')) {
const zlib = require('zlib');
return resolve(zlib.brotliDecompressSync(buf).toString('utf8'));
}
if (enc.includes('gzip')) {
const zlib = require('zlib');
return resolve(zlib.gunzipSync(buf).toString('utf8'));
}
if (enc.includes('deflate')) {
const zlib = require('zlib');
return resolve(zlib.inflateSync(buf).toString('utf8'));
}
resolve(buf.toString('utf8'));
} catch (e) {
resolve(Buffer.concat(data).toString('utf8'));
}
});
});
req.on('error', reject);
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
});
} catch (e) {
console.log(`[fetchRaw] error for ${url}: ${e.message}, retries left: ${retries - 1}`);
retries--;
if (retries === 0) throw e;
await new Promise(r => setTimeout(r, 1500));
}
}
}
// ── Cinemar parser ────────────────────────────────────────────────────────────
function findAllInBuf(buf, pattern) {
const positions = [];
let pos = 0;
while (pos < buf.length) {
const idx = buf.indexOf(pattern, pos);
if (idx < 0) break;
positions.push(idx);
pos = idx + 1;
}
return positions;
}
function readJsonStr(buf, pos) {
let str = '', i = pos;
while (i < buf.length) {
const b = buf[i];
if (b === 0x22) return { str, end: i };
if (b === 0x5C) {
i++;
if (i >= buf.length) break;
const esc = buf[i];
if (esc === 0x75 && i + 4 < buf.length) {
const hex = buf.slice(i + 1, i + 5).toString('ascii');
if (/^[0-9a-fA-F]{4}$/.test(hex)) { str += String.fromCharCode(parseInt(hex, 16)); i += 5; continue; }
}
str += String.fromCharCode(esc);
} else if (b >= 0x20 && b <= 0x7E) {
str += String.fromCharCode(b);
} else if ((b & 0xE0) === 0xC0 && i + 1 < buf.length && (buf[i+1] & 0xC0) === 0x80) {
str += String.fromCodePoint(((b & 0x1F) << 6) | (buf[i+1] & 0x3F)); i += 2; continue;
} else if ((b & 0xF0) === 0xE0 && i + 2 < buf.length && (buf[i+1] & 0xC0) === 0x80 && (buf[i+2] & 0xC0) === 0x80) {
str += String.fromCodePoint(((b & 0x0F) << 12) | ((buf[i+1] & 0x3F) << 6) | (buf[i+2] & 0x3F)); i += 3; continue;
} else break;
i++;
}
return { str, end: i };
}
function findBufVal(buf, key, fromPos, maxDist) {
const keyBuf = Buffer.from('"' + key + '":"');
const idx = buf.indexOf(keyBuf, fromPos);
if (idx < 0 || idx > fromPos + (maxDist || 400)) return null;
return readJsonStr(buf, idx + keyBuf.length);
}
const FOLDER_MARKER_BUF = Buffer.from('"folder":[');
// Cinemar inserts garbage: 4 hex chars + '&' every ~4000 chars into the base64 string.
// Must split on '&', strip the 4 trailing hex chars from each chunk, decode separately, concat.
function decodeCinemarFile(fileValue) {
const w3sPos = fileValue.indexOf('W3s');
if (w3sPos < 0) return null;
const raw = fileValue.substring(w3sPos);
const chunks = raw.split('&');
const buffers = [];
for (let i = 0; i < chunks.length; i++) {
let chunk = chunks[i];
if (i < chunks.length - 1) chunk = chunk.slice(0, -4); // strip 4 hex garbage chars
const clean = chunk.replace(/[^A-Za-z0-9+/=]/g, '');
if (clean.length > 0) {
const padded = clean + '='.repeat((4 - clean.length % 4) % 4);
try { buffers.push(Buffer.from(padded, 'base64')); } catch(e) {}
}
}
if (!buffers.length) return null;
return Buffer.concat(buffers);
}
function normUrl(f) { return f ? (f.startsWith('//') ? 'https:' + f : f) : ''; }
function parseSubs(subtitle) {
const subs = [];
if (!subtitle) return subs;
subtitle.split(',').forEach(part => {
const sm = part.match(/^\[([^\]]+)\](\/\/.+|https?:\/\/.+)/);
if (sm) subs.push({ label: sm[1], url: normUrl(sm[2]) });
});
return subs;
}
function parseCinemarBase64(html) {
const fileMatch = html.match(/"file":"([^"]+)"/);
if (!fileMatch) return null;
const buf = decodeCinemarFile(fileMatch[1]);
if (!buf || buf.length < 20) return null;
const isSerial = buf.indexOf(FOLDER_MARKER_BUF) >= 0;
if (!isSerial) {
const items = [];
findAllInBuf(buf, Buffer.from('"src_id"')).forEach(pos => {
const t = findBufVal(buf, 'title', pos, 300);
const f = findBufVal(buf, 'file', pos, 5000); // dlink field can be very long
const s = findBufVal(buf, 'subtitle', pos, 6000);
if (!f) return;
const url = normUrl(f.str.replace(/\\\//g, '/'));
if (!url || !url.includes('cinemap')) return; // must be a real stream URL
const title = t ? t.str : 'Unknown';
if (!items.find(i => i.title === title)) items.push({ title, file: url, subtitle: s ? s.str.replace(/\\\//g, '/') : '' });
});
return items.length ? items : null;
}
// Serial: find all episode IDs (sXXeYY) and group by season
const epIdPrefix = Buffer.from('"id":"s');
const allEpPositions = [];
findAllInBuf(buf, epIdPrefix).forEach(pos => {
let i = pos + epIdPrefix.length;
while (i < buf.length && buf[i] >= 0x30 && buf[i] <= 0x39) i++;
if (buf[i] !== 0x65) return;
i++;
while (i < buf.length && buf[i] >= 0x30 && buf[i] <= 0x39) i++;
if (buf[i] !== 0x22) return;
const fullId = buf.slice(pos + 6, i).toString('ascii');
const seasonMatch = fullId.match(/^(s\d+)e(\d+)$/);
if (!seasonMatch) return;
const folderIdx = buf.indexOf(FOLDER_MARKER_BUF, pos);
if (folderIdx < 0 || folderIdx > pos + 2000) return;
const t = findBufVal(buf, 'title', pos, 200);
allEpPositions.push({ id: fullId, seasonId: seasonMatch[1], epNum: parseInt(seasonMatch[2], 10), title: t ? t.str : ('БСрия ' + seasonMatch[2]), pos });
});
const seasonMap = {};
allEpPositions.forEach(ep => {
if (!seasonMap[ep.seasonId]) seasonMap[ep.seasonId] = [];
if (!seasonMap[ep.seasonId].find(e => e.id === ep.id)) seasonMap[ep.seasonId].push(ep);
});
const seasonIds = Object.keys(seasonMap).sort((a, b) => parseInt(a.substring(1)) - parseInt(b.substring(1)));
if (!seasonIds.length) return null;
return seasonIds.map(seasonId => {
const eps = seasonMap[seasonId].sort((a, b) => a.epNum - b.epNum);
const episodesWithVoices = eps.map((ep, ei) => {
const nextEpPos = ei + 1 < eps.length ? eps[ei + 1].pos : buf.length;
const eBuf = buf.slice(ep.pos, nextEpPos);
const voices = [];
findAllInBuf(eBuf, Buffer.from('"src_id"')).forEach(vpos => {
const t = findBufVal(eBuf, 'title', vpos, 300);
const f = findBufVal(eBuf, 'file', vpos, 2000);
const s = findBufVal(eBuf, 'subtitle', vpos, 3000);
if (!f) return;
const url = normUrl(f.str.replace(/\\\//g, '/'));
if (!url || !url.includes('.m3u8')) return;
const title = t ? t.str : 'Unknown';
if (!voices.find(v => v.title === title)) voices.push({ title, file: url, subtitle: s ? s.str.replace(/\\\//g, '/') : '' });
});
return { id: ep.id, title: ep.title, folder: voices };
});
return { id: seasonId, title: 'Π‘Π΅Π·ΠΎΠ½ ' + parseInt(seasonId.substring(1)), folder: episodesWithVoices };
});
}
// ── Multi-fetch helpers ───────────────────────────────────────────────────────
function parseEpisodesFromHtml(html) {
const fileMatch = html.match(/"file":"([^"]+)"/);
if (!fileMatch) return {};
const buf = decodeCinemarFile(fileMatch[1]);
if (!buf || buf.length < 20) return {};
if (buf.indexOf(FOLDER_MARKER_BUF) < 0) return {};
const epIdPrefix = Buffer.from('"id":"s');
const episodes = {};
findAllInBuf(buf, epIdPrefix).forEach(pos => {
let i = pos + epIdPrefix.length;
let seasonNum = '';
while (i < buf.length && buf[i] >= 0x30 && buf[i] <= 0x39) { seasonNum += String.fromCharCode(buf[i]); i++; }
if (buf[i] !== 0x65) return;
i++;
let epNum = '';
while (i < buf.length && buf[i] >= 0x30 && buf[i] <= 0x39) { epNum += String.fromCharCode(buf[i]); i++; }
if (buf[i] !== 0x22) return;
const fullId = 's' + seasonNum + 'e' + epNum;
const seasonId = 's' + seasonNum;
const t = findBufVal(buf, 'title', pos, 200);
const title = t ? t.str : ('БСрия ' + epNum);
const nextEpPos = buf.indexOf(epIdPrefix, pos + 1);
const endPos = nextEpPos > 0 ? Math.min(nextEpPos, pos + 5000) : pos + 5000;
const eBuf = buf.slice(pos, endPos);
const voices = [];
findAllInBuf(eBuf, Buffer.from('"src_id"')).forEach(vpos => {
const vt = findBufVal(eBuf, 'title', vpos, 300);
const f = findBufVal(eBuf, 'file', vpos, 2000);
const s = findBufVal(eBuf, 'subtitle', vpos, 3000);
if (!f) return;
const url = normUrl(f.str.replace(/\\\//g, '/'));
if (!url || !url.includes('.m3u8')) return;
const vtitle = vt ? vt.str : 'Unknown';
if (!voices.find(v => v.title === vtitle)) voices.push({ title: vtitle, file: url, subtitle: s ? s.str.replace(/\\\//g, '/') : '' });
});
if (!episodes[fullId] || voices.length > episodes[fullId].voices.length)
episodes[fullId] = { id: fullId, seasonId, seasonNum: parseInt(seasonNum), epNum: parseInt(epNum), title, voices };
});
return episodes;
}
function buildSerialDataFromMerged(mergedEpisodes) {
const seasonMap = {};
Object.values(mergedEpisodes).forEach(ep => {
if (!seasonMap[ep.seasonId]) seasonMap[ep.seasonId] = [];
seasonMap[ep.seasonId].push(ep);
});
const seasonIds = Object.keys(seasonMap).sort((a, b) => parseInt(a.substring(1)) - parseInt(b.substring(1)));
const seasons = [], episodes = {}, voicesMap = {};
seasonIds.forEach(sid => {
const eps = seasonMap[sid].sort((a, b) => a.epNum - b.epNum);
seasons.push({ id: sid, title: 'Π‘Π΅Π·ΠΎΠ½ ' + parseInt(sid.substring(1)) });
episodes[sid] = [];
voicesMap[sid] = {};
eps.forEach(ep => {
episodes[sid].push({ id: ep.id, title: ep.title });
voicesMap[sid][ep.id] = ep.voices.map(v => ({ label: v.title, url: normUrl(v.file), subtitles: parseSubs(v.subtitle) }));
});
});
return { seasons, episodes, voices: voicesMap };
}
function parseCinemar(embedHtml) {
let voices = [], masterUrl = null, serialData = null, contentType = 'movie';
const rawItems = parseCinemarBase64(embedHtml);
if (rawItems && rawItems.length) {
const first = rawItems[0];
if (first.folder && first.folder.length && first.folder[0] && first.folder[0].folder) {
contentType = 'serial';
const seasons = [], episodes = {}, voicesMap = {};
rawItems.forEach((season, si) => {
const sid = season.id || String(si + 1);
seasons.push({ id: sid, title: season.title || ('Π‘Π΅Π·ΠΎΠ½ ' + (si + 1)) });
episodes[sid] = [];
voicesMap[sid] = {};
(season.folder || []).forEach((ep, ei) => {
const eid = ep.id || String(ei + 1);
episodes[sid].push({ id: eid, title: ep.title || ('БСрия ' + (ei + 1)) });
voicesMap[sid][eid] = (ep.folder || []).map(v => ({ label: v.title || 'Unknown', url: normUrl(v.file || ''), subtitles: parseSubs(v.subtitle) })).filter(v => v.url);
});
});
serialData = { seasons, episodes, voices: voicesMap };
const fs = seasons[0];
if (fs && episodes[fs.id] && episodes[fs.id][0]) {
const fv = voicesMap[fs.id][episodes[fs.id][0].id];
if (fv && fv[0]) masterUrl = fv[0].url;
}
} else if (first.folder) {
contentType = 'serial';
const seasons = [], episodes = {}, voicesMap = {};
rawItems.forEach((season, si) => {
const sid = season.id || String(si + 1);
seasons.push({ id: sid, title: season.title || ('Π‘Π΅Π·ΠΎΠ½ ' + (si + 1)) });
episodes[sid] = [];
voicesMap[sid] = {};
(season.folder || []).forEach((ep, ei) => {
const eid = ep.id || String(ei + 1);
episodes[sid].push({ id: eid, title: ep.title || ('БСрия ' + (ei + 1)) });
const u = normUrl(ep.file || '');
voicesMap[sid][eid] = u ? [{ label: ep.title || 'Unknown', url: u, subtitles: parseSubs(ep.subtitle) }] : [];
});
});
serialData = { seasons, episodes, voices: voicesMap };
const fs = seasons[0];
if (fs && episodes[fs.id] && episodes[fs.id][0]) {
const fv = voicesMap[fs.id][episodes[fs.id][0].id];
if (fv && fv[0]) masterUrl = fv[0].url;
}
} else {
contentType = 'movie';
voices = rawItems.map(item => ({ label: item.title || 'Unknown', url: normUrl(item.file || ''), subtitles: parseSubs(item.subtitle) })).filter(v => v.url);
if (voices.length) masterUrl = voices[0].url;
}
}
if (!masterUrl) {
const fm = embedHtml.match(/["'](https?:\/\/[^"']*cinemap[^"']*\.m3u8[^"']*)['"]/);
if (fm) masterUrl = fm[1];
}
return { contentType, voices, masterUrl, serialData };
}
// ── HLS proxy ─────────────────────────────────────────────────────────────────
function handleHls(req, res) {
const { query } = parseQuery(req.url);
const targetUrl = query.url;
if (!targetUrl) { res.writeHead(400, { 'Access-Control-Allow-Origin': '*' }); res.end('no url'); return; }
function doRequest(reqUrl, redirectCount) {
if (redirectCount > 5) { res.writeHead(500, { 'Access-Control-Allow-Origin': '*' }); res.end('too many redirects'); return; }
let pu;
try { pu = new URL(reqUrl); } catch(e) { res.writeHead(400); res.end('bad url'); return; }
const mod = pu.protocol === 'https:' ? https : http;
const isObrut = pu.hostname.includes('obrut') || pu.hostname.includes('superdupercdn');
const isCinemap = pu.hostname.includes('cinemap');
const isAlloha = pu.hostname.includes('stream-balancer') || pu.hostname.includes('streamalloha') || pu.hostname.includes('allo-');
const isRstprg = pu.hostname.includes('rstprgapipt.com');
const opts = {
hostname: pu.hostname,
port: pu.port || (pu.protocol === 'https:' ? 443 : 80),
path: pu.pathname + pu.search,
headers: {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
'accept': '*/*',
'accept-language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
'origin': isCinemap ? 'https://uakinogo.io' : isObrut ? 'https://kinojump.com' : isAlloha ? 'https://streamalloha.live' : isRstprg ? 'https://tv-1-kinoserial.net' : 'https://streamalloha.live',
'referer': isCinemap ? 'https://uakinogo.io/' : isObrut ? 'https://kinojump.com/' : isAlloha ? 'https://streamalloha.live/' : isRstprg ? 'https://tv-1-kinoserial.net/' : 'https://streamalloha.live/',
'sec-fetch-dest': isRstprg ? 'video' : 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'cross-site',
},
};
if (req.headers['range']) opts.headers['range'] = req.headers['range'];
const pr = mod.request(opts, upstream => {
// Follow redirects
if (upstream.statusCode >= 300 && upstream.statusCode < 400 && upstream.headers.location) {
upstream.resume();
let loc = upstream.headers.location;
if (!loc.startsWith('http')) loc = pu.origin + loc;
return doRequest(loc, redirectCount + 1);
}
const ct = upstream.headers['content-type'] || '';
const isM3u8 = ct.includes('mpegurl') || (reqUrl.includes('.m3u8') || reqUrl.includes(':hls:manifest')) && !reqUrl.includes(':hls:seg-');
const rh = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': '*' };
if (isM3u8) {
rh['Content-Type'] = 'application/vnd.apple.mpegurl';
res.writeHead(upstream.statusCode, rh);
let body = '';
upstream.setEncoding('utf8');
upstream.on('data', c => { body += c; });
upstream.on('end', () => {
const base = reqUrl.substring(0, reqUrl.lastIndexOf('/') + 1);
const serverBase = 'https://' + (req.headers.host || 'localhost:' + PORT);
function rewriteUrl(rel) {
let a = rel.trim();
if (!a) return a;
if (a.startsWith('//')) a = 'https:' + a;
else if (!a.startsWith('http')) a = base + a;
return serverBase + '/hls?url=' + encodeURIComponent(a);
}
res.end(body.split('\n').map(line => {
const tr = line.trim();
if (!tr) return line;
if (tr.startsWith('#')) return line.replace(/URI="([^"]+)"/g, (m, uri) => `URI="${rewriteUrl(uri)}"`);
return rewriteUrl(tr);
}).join('\n'));
});
} else {
rh['Content-Type'] = ct || 'video/mp2t';
if (upstream.headers['content-length']) rh['Content-Length'] = upstream.headers['content-length'];
if (upstream.headers['content-range']) rh['Content-Range'] = upstream.headers['content-range'];
res.writeHead(upstream.statusCode, rh);
upstream.pipe(res);
}
});
pr.on('error', e => { res.writeHead(500); res.end(e.message); });
pr.end();
}
doRequest(targetUrl, 0);
}
// ── Server ────────────────────────────────────────────────────────────────────
const server = http.createServer(async (req, res) => {
const { pathname, query } = parseQuery(req.url);
const startedAt = Date.now();
const shouldLog = pathname === '/health' || pathname.startsWith('/uakinogo/') || pathname.startsWith('/alloha/') || pathname.startsWith('/turbo/');
if (shouldLog) {
try {
const q = (pathname === '/uakinogo/search' && query.q) ? (' q=' + String(query.q).slice(0, 120)) : '';
console.log('[req]', req.method, pathname + q);
} catch (e) {}
res.on('finish', () => {
try { console.log('[res]', req.method, pathname, res.statusCode, (Date.now() - startedAt) + 'ms'); } catch (e) {}
});
}
if (req.method === 'OPTIONS') {
res.writeHead(204, { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Headers': '*' });
res.end();
return;
}
if (pathname === '/health') { jsonResp(res, { ok: true, time: new Date().toISOString() }); return; }
if (pathname === '/veoveo/embed') {
const embedUrl = query.url;
if (!embedUrl || !embedUrl.includes('kinoserial')) {
jsonResp(res, { ok: false, error: 'invalid url' }, 400); return;
}
console.log('[veoveo/embed] fetching:', embedUrl);
fetchRaw(embedUrl, {
'referer': 'https://veoveo.ru/',
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
'sec-fetch-dest': 'document',
'sec-fetch-mode': 'navigate',
'sec-fetch-site': 'cross-site',
}).then(text => {
if (text.includes('id="cf-challenge"') || text.includes('cf-browser-verification')) {
console.log('[veoveo/embed] Cloudflare challenge detected');
// Could potentially fallback to puppeteer here if needed
}
res.writeHead(200, {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'text/html; charset=utf-8'
});
res.end(text);
}).catch(e => {
console.log('[veoveo/embed] error:', e.message);
jsonResp(res, { ok: false, error: e.message }, 500);
});
return;
}
if (pathname === '/veoveo/balancer') {
const contentId = query.id;
if (!contentId) { jsonResp(res, { ok: false, error: 'no id' }, 400); return; }
console.log('[veoveo/balancer] contentId:', contentId);
fetchRaw('https://api.rstprgapipt.com/balancer-api/proxy/playlists/catalog-api/episodes?content-id=' + contentId, {
'referer': 'https://veoveo.ru/',
'accept': 'application/json, text/plain, */*',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'cross-site',
}).then(text => {
res.writeHead(200, {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json; charset=utf-8'
});
res.end(text);
}).catch(e => {
console.log('[veoveo/balancer] error:', e.message);
jsonResp(res, { ok: false, error: e.message }, 500);
});
return;
}
// Show outbound IP of this server
if (pathname === '/debug/ip') {
fetchRaw('https://api.ipify.org?format=json', { 'user-agent': 'curl/7.0' })
.then(d => { jsonResp(res, { ok: true, ip: JSON.parse(d).ip }); })
.catch(e => { jsonResp(res, { ok: false, error: e.message }); });
return;
}
if (pathname === '/debug/platform') {
const fs = require('fs');
const chromiumPaths = [
process.env.PUPPETEER_EXECUTABLE_PATH,
'/usr/bin/chromium',
'/usr/bin/chromium-browser',
'/usr/bin/google-chrome'
].filter(Boolean);
const available = chromiumPaths.filter(p => { try { return fs.existsSync(p); } catch(e) { return false; } });
jsonResp(res, {
platform: process.platform,
node: process.version,
puppeteer_path: process.env.PUPPETEER_EXECUTABLE_PATH || 'not set',
chromium_found: available,
alloha_available: typeof getStreams === 'function' && getStreams.toString().includes('getBrowser') ? 'yes' : 'fallback'
});
return;
}
if (pathname === '/hls') { handleHls(req, res); return; }
if (pathname === '/srt2vtt') {
const corsH = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': '*', 'Content-Type': 'text/vtt; charset=utf-8' };
const subUrl = query.url;
if (!subUrl) { res.writeHead(400, corsH); res.end('no url'); return; }
fetchRaw(subUrl, { 'user-agent': 'Mozilla/5.0 Chrome/120' }).then(body => {
const vtt = 'WEBVTT\n\n' + body.replace(/\r\n/g, '\n').replace(/\r/g, '\n').replace(/(\d{2}:\d{2}:\d{2}),(\d{3})/g, '$1.$2');
res.writeHead(200, corsH); res.end(vtt);
}).catch(e => { res.writeHead(500, corsH); res.end(e.message); });
return;
}
if (pathname === '/alloha/player-info' && req.method === 'POST') {
try {
const params = JSON.parse(await readBody(req));
const { token_movie, partner_token } = params;
if (!token_movie) { jsonResp(res, { ok: false, error: 'token_movie required' }, 400); return; }
const token = partner_token || ALLOHA_TOKEN;
const playerUrl = `https://${ALLOHA_HOST}/?token_movie=${token_movie}&token=${encodeURIComponent(token)}`;
console.log(`[server] /alloha/player-info token_movie=${token_movie}`);
const html = await fetchRaw(playerUrl, { 'user-agent': 'Mozilla/5.0 Chrome/120', 'referer': 'https://kinokrad.my/' });
const nkMatch = html.match(/name="viewporti"\s+content="([^"]+)"/);
const nk = nkMatch ? nkMatch[1] : null;
if (!nk) { jsonResp(res, { ok: false, error: 'nk_not_found' }); return; }
let fileId = null;
const flMatch = html.match(/"active"\s*:\s*\{[^}]*?"id"\s*:\s*(\d+)/);
if (flMatch) fileId = flMatch[1];
if (!fileId) { const m = html.match(/"id"\s*:\s*(\d+)/); if (m) fileId = m[1]; }
let fileList = null;
const flRaw = html.match(/fileList\s*=\s*JSON\.parse\('([\s\S]*?)'\)/);
if (flRaw) { try { fileList = JSON.parse(flRaw[1]); } catch(e) {} }
const appBundle = (html.match(/src="(\/build\/app\.[a-f0-9]+\.js)"/) || [])[1] || null;
const runtimeBundle = (html.match(/src="(\/build\/runtime\.[a-f0-9]+\.js)"/) || [])[1] || null;
const bundle539 = (html.match(/src="(\/build\/539\.[a-f0-9]+\.js)"/) || [])[1] || null;
jsonResp(res, {
ok: true, nk, file_id: fileId, player_url: playerUrl, file_list: fileList,
bundle_app: appBundle ? `https://${ALLOHA_HOST}${appBundle}` : null,
bundle_runtime: runtimeBundle ? `https://${ALLOHA_HOST}${runtimeBundle}` : null,
bundle_539: bundle539 ? `https://${ALLOHA_HOST}${bundle539}` : null,
});
} catch(e) {
console.log('[server] player-info ERROR:', e.message);
jsonResp(res, { ok: false, error: e.message }, 500);
}
return;
}
if (pathname === '/alloha/streams' && req.method === 'POST') {
try {
const params = JSON.parse(await readBody(req));
const { token_movie, partner_token, referer } = params;
if (!token_movie) { jsonResp(res, { ok: false, error: 'token_movie required' }, 400); return; }
console.log(`[server] /alloha/streams token_movie=${token_movie}`);
const result = await getStreams({ token_movie, partner_token, referer });
const serverBase = 'https://' + (req.headers.host || 'localhost:' + PORT);
if (result.hlsSource) {
result.hlsSource = result.hlsSource.map(src => {
if (src.quality) {
const newQ = {};
for (const [q, u] of Object.entries(src.quality)) {
newQ[q] = u.split(' or ').map(raw => {
const t = raw.trim();
const full = t.startsWith('//') ? 'https:' + t : t;
return serverBase + '/hls?url=' + encodeURIComponent(full);
}).join(' or ');
}
src.quality = newQ;
}
return src;
});
}
jsonResp(res, result);
} catch(e) {
console.log('[server] streams ERROR:', e.message);
jsonResp(res, { ok: false, error: e.message }, 500);
}
return;
}
if (pathname === '/uakinogo/search') {
const q = query.q || '';
if (!q) { jsonResp(res, { ok: false, error: 'no query' }, 400); return; }
const qEnc = encodeURIComponent(q);
const searchUrlSeo = 'https://uakinogo.io/search/' + qEnc;
const searchUrlLegacy = 'https://uakinogo.io/index.php?do=search&subaction=search&story=' + qEnc;
const parseSearchHtml = (html) => {
const results = [];
const addResult = (rawUrl, id, slug) => {
if (!rawUrl || !id) return;
let url = rawUrl;
if (url.startsWith('//')) url = 'https:' + url;
if (url.startsWith('/')) url = 'https://uakinogo.io' + url;
if (!url.startsWith('http')) return;
if (!url.includes('uakinogo.io/')) return;
if (url.includes('do=search') || url.includes('/index.php')) return;
if (!results.find(r => r.id === id)) results.push({ url, id, slug: slug || '' });
};
const reHrefAny = /href\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s>]+))/gi;
const reIdSlug = /\/(\d+)-([^\/?#"'<>]+)\.html/i;
const reEscAbs = /(https?:\\\\\/\\\\\/uakinogo\.io\\\\\/[^"']*?\\\\\/(\d+)-([^"\\\/]+)\.html)/gi;
const reEscRel = /(\\\\\/[^"']*?\\\\\/(\d+)-([^"\\\/]+)\.html)/gi;
const reLoose = /(https?:\/\/uakinogo\.io\/[^"'<>\\s]*\/(\d+)-([^\/?#"'<>\\s]+)\.html)/gi;
const reLooseRel = /(\/[^"'<>\\s]*\/(\d+)-([^\/?#"'<>\\s]+)\.html)/gi;
let m;
while ((m = reHrefAny.exec(html)) !== null) {
const href = (m[1] || m[2] || m[3] || '').trim();
if (!href) continue;
const mm = href.match(reIdSlug);
if (mm) addResult(href, mm[1], mm[2]);
}
while ((m = reEscAbs.exec(html)) !== null) addResult(m[1].replace(/\\\\\//g, '/'), m[2], m[3]);
while ((m = reEscRel.exec(html)) !== null) addResult(m[1].replace(/\\\\\//g, '/'), m[2], m[3]);
while ((m = reLoose.exec(html)) !== null) addResult(m[1], m[2], m[3]);
while ((m = reLooseRel.exec(html)) !== null) addResult(m[1], m[2], m[3]);
return results;
};
const looksLikeChallenge = (html) => {
const t = String(html || '');
if (!t) return true;
if (t.includes('cf-challenge') || t.includes('cf-browser-verification')) return true;
if (/Just a moment/i.test(t) && /Cloudflare/i.test(t)) return true;
if (t.includes('data-sitekey') && t.includes('cf-turnstile')) return true;
return false;
};
const fetchAndParse = async (url) => {
const html = await fetchRaw(url, { 'user-agent': 'Mozilla/5.0 Chrome/120', 'referer': 'https://uakinogo.io/' });
return { html, results: parseSearchHtml(html), challenged: looksLikeChallenge(html) };
};
fetchAndParse(searchUrlSeo).then(async (first) => {
let html = first.html;
let results = first.results;
let challenged = first.challenged;
if (challenged || results.length === 0) {
try {
const second = await fetchAndParse(searchUrlLegacy);
if (second.results.length > results.length) {
html = second.html;
results = second.results;
challenged = challenged || second.challenged;
}
} catch (e) {}
}
if ((challenged || results.length === 0) && typeof getStreams === 'function' && getStreams.toString) {
try {
const { getBrowser } = require('./alloha');
const browser = await getBrowser();
const page = await browser.newPage();
await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
await page.setExtraHTTPHeaders({ 'Accept-Language': 'ru-RU,ru;q=0.9,en;q=0.8', 'Referer': 'https://uakinogo.io/' });
await page.goto(searchUrlSeo, { waitUntil: 'networkidle2', timeout: 25000 });
try { await page.waitForSelector('a[href*=".html"]', { timeout: 8000 }); } catch (e) {}
await new Promise(r => setTimeout(r, 1200));
const html2 = await page.content();
await page.close();
const parsed = parseSearchHtml(html2);
if (parsed.length >= results.length) {
html = html2;
results = parsed;
challenged = looksLikeChallenge(html2);
}
} catch (e) {}
}
try { console.log('[uakinogo/search] results:', results.length, results[0] ? results[0].url : 'none'); } catch (e) {}
if (results.length === 0) {
try {
const t = String(html || '');
console.log('[uakinogo/search] html:', 'len=' + t.length, 'cf=' + (looksLikeChallenge(t) ? '1' : '0'), 'sample=' + t.slice(0, 160).replace(/\s+/g, ' '));
} catch (e) {}
}
jsonResp(res, { ok: true, results: results.slice(0, 10) });
}).catch(e => jsonResp(res, { ok: false, error: e.message }, 500));
return;
}
if (pathname === '/uakinogo/embed') {
const pageUrl = query.url;
if (!pageUrl) { jsonResp(res, { ok: false, error: 'no url' }, 400); return; }
fetchRaw(pageUrl, { 'user-agent': 'Mozilla/5.0 Chrome/120', 'referer': 'https://uakinogo.io/' }).then(html => {
const m = html.match(/"embedUrl"\s*:\s*"(https:\/\/cinemar\.cc\/embed\/[^"]+)"/);
jsonResp(res, { ok: true, embed_url: m ? m[1] : null });
}).catch(e => jsonResp(res, { ok: false, error: e.message }, 500));
return;
}
// ── /turbo/search?q=TITLE ─────────────────────────────────────────────────
if (pathname === '/turbo/search') {
const q = query.q || '';
if (!q) { jsonResp(res, { ok: false, error: 'no query' }, 400); return; }
turboSearch(q)
.then(results => jsonResp(res, { ok: true, results }))
.catch(e => jsonResp(res, { ok: false, error: e.message }, 500));
return;
}
// ── /turbo/search-embed?q=TITLE ──────────────────────────────────────────
if (pathname === '/turbo/search-embed') {
const q = query.q || '';
if (!q) { jsonResp(res, { ok: false, error: 'no query' }, 400); return; }
const { parseObrutEmbed, buildSerialFromEntries, extractMovieVoices, getKinojumpHtml } = require('./turbo');
(async () => {
// Step 1: search kinojump
const results = await turboSearch(q);
if (!results.length) throw new Error('not found on kinojump');
const pageUrl = results[0].url;
console.log('[turbo/search-embed] page:', pageUrl);
// Step 2: get embed URL β€” plain HTTP first, Puppeteer fallback
const { getBrowser } = require('./alloha');
const browser = await getBrowser();
const pageHtml = await getKinojumpHtml(pageUrl, browser).catch(e => {
console.log('[turbo/search-embed] getKinojumpHtml error:', e.message);
return '';
});
const em = pageHtml.match(/(?:https?:\/\/)?(?:([a-z0-9]+)\.)?obrut\.show\/embed\/([A-Za-z0-9]+)\/content\/([A-Za-z0-9]+)/);
if (!em) throw new Error('obrut embed URL not found');
const subdomain = em[1] ? em[1] + '.obrut.show' : '49372504.obrut.show';
const embedUrl = 'https://' + subdomain + '/embed/' + em[2] + '/content/' + em[3];
console.log('[turbo/search-embed] embed URL:', embedUrl);
// Step 3: parse embed (retry until enough entries)
const result = await parseObrutEmbed(embedUrl);
if (!result) throw new Error('failed to parse obrut embed');
if (result.entries.length > 0) {
const serial = buildSerialFromEntries(result.entries);
jsonResp(res, { ok: true, embed_url: embedUrl, content_type: 'serial', serial });
} else if (result.movieVoices.length > 0) {
jsonResp(res, { ok: true, embed_url: embedUrl, content_type: 'movie', voices: result.movieVoices });
} else {
throw new Error('no data found in obrut embed');
}
})().catch(e => { console.log('[turbo/search-embed] error:', e.message); jsonResp(res, { ok: false, error: e.message }, 500); });
return;
}
// ── /turbo/stream?url=PAGE_URL ────────────────────────────────────────────
if (pathname === '/turbo/stream') {
const pageUrl = query.url;
if (!pageUrl) { jsonResp(res, { ok: false, error: 'no url' }, 400); return; }
getTurboStream(pageUrl)
.then(data => jsonResp(res, { ok: true, ...data }))
.catch(e => { console.log('[turbo] error:', e.message); jsonResp(res, { ok: false, error: e.message }, 500); });
return;
}
// ── /turbo/proxy-embed?url=OBRUT_EMBED_URL ───────────────────────────────
// Raw HTTP proxy for obrut embed β€” adds correct Referer header
// Used by browser clients that can't set Referer (file:/// origin)
if (pathname === '/turbo/proxy-embed') {
const embedUrl = query.url;
if (!embedUrl || !embedUrl.includes('obrut.show')) {
jsonResp(res, { ok: false, error: 'invalid url' }, 400); return;
}
(async () => {
const html = await fetchRaw(embedUrl, {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'referer': 'https://kinojump.com/',
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'accept-language': 'ru-RU,ru;q=0.9,en;q=0.8',
'accept-encoding': 'identity',
});
console.log('[turbo/proxy-embed] len:', html.length, 'hasPlayer:', html.includes('new Player('));
res.writeHead(200, {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': '*',
'Content-Type': 'text/html; charset=utf-8',
});
res.end(html);
})().catch(e => {
console.log('[turbo/proxy-embed] error:', e.message);
res.writeHead(500, { 'Access-Control-Allow-Origin': '*' });
res.end('error: ' + e.message);
});
return;
}
// ── /turbo/embed?url=OBRUT_EMBED_URL ─────────────────────────────────────
// Parse obrut embed page directly (no Puppeteer needed)
if (pathname === '/turbo/embed') {
const embedUrl = query.url;
if (!embedUrl) { jsonResp(res, { ok: false, error: 'no url' }, 400); return; }
const { parseObrutEmbed, buildSerialFromEntries } = require('./turbo');
parseObrutEmbed(embedUrl)
.then(result => {
if (!result || (result.entries.length === 0 && result.movieVoices.length === 0)) {
jsonResp(res, { ok: false, error: 'failed to parse embed' }); return;
}
if (result.entries.length > 0) {
const serial = buildSerialFromEntries(result.entries);
jsonResp(res, { ok: true, embed_url: embedUrl, content_type: 'serial', serial, entry_count: result.entries.length });
} else {
jsonResp(res, { ok: true, embed_url: embedUrl, content_type: 'movie', voices: result.movieVoices });
}
})
.catch(e => { console.log('[turbo/embed] error:', e.message); jsonResp(res, { ok: false, error: e.message }, 500); });
return;
}
// ── /uakinogo/stream?url=PAGE_URL ─────────────────────────────────────────
if (pathname === '/uakinogo/stream') {
const pageUrl = query.url;
if (!pageUrl) { jsonResp(res, { ok: false, error: 'no url' }, 400); return; }
getCinemarStream(pageUrl)
.then(data => jsonResp(res, { ok: true, ...data }))
.catch(e => { console.log('[cinemar] error:', e.message); jsonResp(res, { ok: false, error: e.message }, 500); });
return;
}
// ── /puppeteer/fetch?url=URL β€” fetch URL via Puppeteer (bypasses Cloudflare JS challenge)
if (pathname === '/puppeteer/fetch') {
const targetUrl = query.url;
if (!targetUrl) { jsonResp(res, { ok: false, error: 'no url' }, 400); return; }
(async () => {
let browser;
try {
const { getBrowser } = require('./alloha');
browser = await getBrowser();
const page = await browser.newPage();
await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
await page.setExtraHTTPHeaders({ 'Accept-Language': 'ru-RU,ru;q=0.9,en;q=0.8' });
// Navigate and wait for JS challenge to complete
await page.goto(targetUrl, { waitUntil: 'networkidle2', timeout: 20000 });
// Extra wait for fingerprint redirect
await new Promise(r => setTimeout(r, 2500));
const html = await page.content();
const finalUrl = page.url();
await page.close();
jsonResp(res, { ok: true, html, url: finalUrl });
} catch(e) {
console.log('[puppeteer/fetch] error:', e.message);
jsonResp(res, { ok: false, error: e.message }, 500);
}
})();
return;
}
jsonResp(res, { ok: false, error: 'not found' }, 404);
});
server.listen(PORT, () => console.log(`[server] Alloha+Cinemar proxy on port ${PORT}`));
process.on('SIGTERM', async () => { await closeBrowser(); server.close(); });
process.on('SIGINT', async () => { await closeBrowser(); process.exit(0); });