/** * 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); });