recycleactor commited on
Commit
6bc0113
·
verified ·
1 Parent(s): 18c7675

Upload 4 files

Browse files
Files changed (4) hide show
  1. Dockerfile +14 -36
  2. README.md +24 -8
  3. package.json +20 -17
  4. server.js +253 -932
Dockerfile CHANGED
@@ -1,36 +1,14 @@
1
- FROM node:20-slim
2
-
3
- RUN apt-get update && apt-get install -y \
4
- chromium \
5
- fonts-liberation \
6
- libatk-bridge2.0-0 \
7
- libatk1.0-0 \
8
- libcups2 \
9
- libdrm2 \
10
- libgbm1 \
11
- libgtk-3-0 \
12
- libnspr4 \
13
- libnss3 \
14
- libxcomposite1 \
15
- libxdamage1 \
16
- libxfixes3 \
17
- libxkbcommon0 \
18
- libxrandr2 \
19
- xdg-utils \
20
- --no-install-recommends \
21
- && rm -rf /var/lib/apt/lists/*
22
-
23
- ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
24
- ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
25
- ENV PORT=7860
26
-
27
- WORKDIR /app
28
- COPY package*.json ./
29
- RUN npm install
30
- COPY . .
31
-
32
- RUN useradd -m -u 1001 user && chown -R user:user /app
33
- USER user
34
-
35
- EXPOSE 7860
36
- CMD ["node", "server.js"]
 
1
+ FROM node:20-alpine
2
+
3
+ WORKDIR /app
4
+
5
+ COPY package.json package-lock.json* ./
6
+ RUN npm install --omit=dev
7
+
8
+ COPY server.js ./
9
+
10
+ ENV PORT=7860
11
+ EXPOSE 7860
12
+
13
+ CMD ["npm","start"]
14
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -1,8 +1,24 @@
1
- ---
2
- title: Prox Deploy
3
- emoji: 🎬
4
- colorFrom: blue
5
- colorTo: purple
6
- sdk: docker
7
- pinned: false
8
- ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # lampa-es5-proxy (HF Space)
2
+
3
+ Эндпоинты:
4
+ - `/client/es5_proxy_store.js` — клиентский плагин для Лампы (добавляет в меню расширений включение/выключение ES5-прокси для выбранных плагинов).
5
+ - `/p?url=...` — JS: скачивает исходник, прогоняет через Babel под Chrome 38, добавляет обёртку для подгрузок и базовые полифиллы.
6
+ - `/s?url=...` — CSS: скачивает CSS, инлайнит `@import`, делает `url(...)` абсолютными.
7
+ - `/health`
8
+
9
+ Запуск локально:
10
+ ```bash
11
+ cd es5/server
12
+ npm i
13
+ npm start
14
+ ```
15
+
16
+ Пример URL в Лампе:
17
+ ```
18
+ https://<ваш-space>.hf.space/p?url=https%3A%2F%2Fexample.com%2Fplugin.js
19
+ ```
20
+
21
+ Установка клиентского плагина:
22
+ ```
23
+ https://<ваш-space>.hf.space/client/es5_proxy_store.js
24
+ ```
package.json CHANGED
@@ -1,17 +1,20 @@
1
- {
2
- "name": "alloha-puppeteer",
3
- "version": "1.0.0",
4
- "description": "Alloha stream extractor using Puppeteer",
5
- "main": "server.js",
6
- "scripts": {
7
- "start": "node server.js",
8
- "test": "node test.js",
9
- "postinstall": "node -e \"try { const p = require('puppeteer'); const path = p.executablePath ? p.executablePath() : 'unknown'; console.log('[postinstall] Chromium path:', path); } catch(e) { console.log('[postinstall] puppeteer error:', e.message); }\""
10
- },
11
- "dependencies": {
12
- "puppeteer": "24.15.0"
13
- },
14
- "engines": {
15
- "node": ">=18"
16
- }
17
- }
 
 
 
 
1
+ {
2
+ "name": "lampa-es5-proxy",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "main": "server.js",
6
+ "type": "commonjs",
7
+ "scripts": {
8
+ "start": "node server.js"
9
+ },
10
+ "engines": {
11
+ "node": ">=18"
12
+ },
13
+ "dependencies": {
14
+ "@babel/core": "^7.26.10",
15
+ "@babel/preset-env": "^7.26.9",
16
+ "express": "^4.19.2",
17
+ "regenerator-runtime": "^0.14.1"
18
+ }
19
+ }
20
+
server.js CHANGED
@@ -1,932 +1,253 @@
1
- /**
2
- * Alloha + Cinemar + Turbo Proxy Server
3
- * Endpoints: /alloha/streams, /alloha/player-info, /hls, /srt2vtt,
4
- * /uakinogo/search, /uakinogo/stream, /uakinogo/embed,
5
- * /turbo/search, /turbo/stream, /health
6
- */
7
- const http = require('http');
8
- const https = require('https');
9
-
10
- // Load alloha with error handling — Puppeteer/Chromium may not be available on all platforms
11
- let getStreams, closeBrowser;
12
- try {
13
- const alloha = require('./alloha');
14
- getStreams = alloha.getStreams;
15
- closeBrowser = alloha.closeBrowser;
16
- console.log('[server] Alloha/Puppeteer loaded OK');
17
- } catch(e) {
18
- console.log('[server] Alloha/Puppeteer NOT available:', e.message);
19
- getStreams = async () => { throw new Error('Puppeteer not available on this platform'); };
20
- closeBrowser = async () => {};
21
- }
22
-
23
- const { turboSearch, getTurboStream } = require('./turbo');
24
- const { getCinemarStream } = require('./cinemar');
25
-
26
- const PORT = process.env.PORT || 3000;
27
- const ALLOHA_HOST = 'streamalloha.live';
28
- const ALLOHA_TOKEN = '7fda2b04f6ae5e0e228bda812b0dee';
29
-
30
- const CORS = {
31
- 'Access-Control-Allow-Origin': '*',
32
- 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
33
- 'Access-Control-Allow-Headers': '*',
34
- 'Content-Type': 'application/json',
35
- };
36
-
37
- function jsonResp(res, data, status) {
38
- res.writeHead(status || 200, CORS);
39
- res.end(JSON.stringify(data));
40
- }
41
-
42
- function parseQuery(reqUrl) {
43
- try {
44
- const u = new URL('http://x' + reqUrl);
45
- const q = {};
46
- u.searchParams.forEach((v, k) => { q[k] = v; });
47
- return { pathname: u.pathname, query: q };
48
- } catch(e) {
49
- return { pathname: reqUrl.split('?')[0], query: {} };
50
- }
51
- }
52
-
53
- function readBody(req) {
54
- return new Promise(resolve => {
55
- let body = '';
56
- req.setEncoding('utf8');
57
- req.on('data', c => { body += c; });
58
- req.on('end', () => resolve(body));
59
- });
60
- }
61
-
62
- async function fetchRaw(url, headers = {}) {
63
- let retries = 3;
64
- while (retries > 0) {
65
- try {
66
- return await new Promise((resolve, reject) => {
67
- let pu;
68
- try {
69
- pu = new URL(url);
70
- } catch (e) {
71
- return reject(new Error('Invalid URL: ' + url));
72
- }
73
- const mod = pu.protocol === 'https:' ? https : http;
74
- const req = mod.get(url, {
75
- headers: {
76
- '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',
77
- 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
78
- 'accept-language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
79
- 'accept-encoding': 'gzip, deflate, br',
80
- 'connection': 'close',
81
- ...headers
82
- },
83
- timeout: 15000
84
- }, (res) => {
85
- if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
86
- let loc = res.headers.location;
87
- if (!loc.startsWith('http')) loc = new URL(loc, url).href;
88
- resolve(fetchRaw(loc, headers));
89
- return;
90
- }
91
- let data = [];
92
- res.on('data', (chunk) => data.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
93
- res.on('end', () => {
94
- try {
95
- const buf = Buffer.concat(data);
96
- const enc = String(res.headers['content-encoding'] || '').toLowerCase();
97
- if (enc.includes('br')) {
98
- const zlib = require('zlib');
99
- return resolve(zlib.brotliDecompressSync(buf).toString('utf8'));
100
- }
101
- if (enc.includes('gzip')) {
102
- const zlib = require('zlib');
103
- return resolve(zlib.gunzipSync(buf).toString('utf8'));
104
- }
105
- if (enc.includes('deflate')) {
106
- const zlib = require('zlib');
107
- return resolve(zlib.inflateSync(buf).toString('utf8'));
108
- }
109
- resolve(buf.toString('utf8'));
110
- } catch (e) {
111
- resolve(Buffer.concat(data).toString('utf8'));
112
- }
113
- });
114
- });
115
- req.on('error', reject);
116
- req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
117
- });
118
- } catch (e) {
119
- console.log(`[fetchRaw] error for ${url}: ${e.message}, retries left: ${retries - 1}`);
120
- retries--;
121
- if (retries === 0) throw e;
122
- await new Promise(r => setTimeout(r, 1500));
123
- }
124
- }
125
- }
126
-
127
- // ── Cinemar parser ────────────────────────────────────────────────────────────
128
-
129
- function findAllInBuf(buf, pattern) {
130
- const positions = [];
131
- let pos = 0;
132
- while (pos < buf.length) {
133
- const idx = buf.indexOf(pattern, pos);
134
- if (idx < 0) break;
135
- positions.push(idx);
136
- pos = idx + 1;
137
- }
138
- return positions;
139
- }
140
-
141
- function readJsonStr(buf, pos) {
142
- let str = '', i = pos;
143
- while (i < buf.length) {
144
- const b = buf[i];
145
- if (b === 0x22) return { str, end: i };
146
- if (b === 0x5C) {
147
- i++;
148
- if (i >= buf.length) break;
149
- const esc = buf[i];
150
- if (esc === 0x75 && i + 4 < buf.length) {
151
- const hex = buf.slice(i + 1, i + 5).toString('ascii');
152
- if (/^[0-9a-fA-F]{4}$/.test(hex)) { str += String.fromCharCode(parseInt(hex, 16)); i += 5; continue; }
153
- }
154
- str += String.fromCharCode(esc);
155
- } else if (b >= 0x20 && b <= 0x7E) {
156
- str += String.fromCharCode(b);
157
- } else if ((b & 0xE0) === 0xC0 && i + 1 < buf.length && (buf[i+1] & 0xC0) === 0x80) {
158
- str += String.fromCodePoint(((b & 0x1F) << 6) | (buf[i+1] & 0x3F)); i += 2; continue;
159
- } else if ((b & 0xF0) === 0xE0 && i + 2 < buf.length && (buf[i+1] & 0xC0) === 0x80 && (buf[i+2] & 0xC0) === 0x80) {
160
- str += String.fromCodePoint(((b & 0x0F) << 12) | ((buf[i+1] & 0x3F) << 6) | (buf[i+2] & 0x3F)); i += 3; continue;
161
- } else break;
162
- i++;
163
- }
164
- return { str, end: i };
165
- }
166
-
167
- function findBufVal(buf, key, fromPos, maxDist) {
168
- const keyBuf = Buffer.from('"' + key + '":"');
169
- const idx = buf.indexOf(keyBuf, fromPos);
170
- if (idx < 0 || idx > fromPos + (maxDist || 400)) return null;
171
- return readJsonStr(buf, idx + keyBuf.length);
172
- }
173
-
174
- const FOLDER_MARKER_BUF = Buffer.from('"folder":[');
175
-
176
- // Cinemar inserts garbage: 4 hex chars + '&' every ~4000 chars into the base64 string.
177
- // Must split on '&', strip the 4 trailing hex chars from each chunk, decode separately, concat.
178
- function decodeCinemarFile(fileValue) {
179
- const w3sPos = fileValue.indexOf('W3s');
180
- if (w3sPos < 0) return null;
181
- const raw = fileValue.substring(w3sPos);
182
- const chunks = raw.split('&');
183
- const buffers = [];
184
- for (let i = 0; i < chunks.length; i++) {
185
- let chunk = chunks[i];
186
- if (i < chunks.length - 1) chunk = chunk.slice(0, -4); // strip 4 hex garbage chars
187
- const clean = chunk.replace(/[^A-Za-z0-9+/=]/g, '');
188
- if (clean.length > 0) {
189
- const padded = clean + '='.repeat((4 - clean.length % 4) % 4);
190
- try { buffers.push(Buffer.from(padded, 'base64')); } catch(e) {}
191
- }
192
- }
193
- if (!buffers.length) return null;
194
- return Buffer.concat(buffers);
195
- }
196
-
197
- function normUrl(f) { return f ? (f.startsWith('//') ? 'https:' + f : f) : ''; }
198
-
199
- function parseSubs(subtitle) {
200
- const subs = [];
201
- if (!subtitle) return subs;
202
- subtitle.split(',').forEach(part => {
203
- const sm = part.match(/^\[([^\]]+)\](\/\/.+|https?:\/\/.+)/);
204
- if (sm) subs.push({ label: sm[1], url: normUrl(sm[2]) });
205
- });
206
- return subs;
207
- }
208
-
209
- function parseCinemarBase64(html) {
210
- const fileMatch = html.match(/"file":"([^"]+)"/);
211
- if (!fileMatch) return null;
212
- const buf = decodeCinemarFile(fileMatch[1]);
213
- if (!buf || buf.length < 20) return null;
214
-
215
- const isSerial = buf.indexOf(FOLDER_MARKER_BUF) >= 0;
216
-
217
- if (!isSerial) {
218
- const items = [];
219
- findAllInBuf(buf, Buffer.from('"src_id"')).forEach(pos => {
220
- const t = findBufVal(buf, 'title', pos, 300);
221
- const f = findBufVal(buf, 'file', pos, 5000); // dlink field can be very long
222
- const s = findBufVal(buf, 'subtitle', pos, 6000);
223
- if (!f) return;
224
- const url = normUrl(f.str.replace(/\\\//g, '/'));
225
- if (!url || !url.includes('cinemap')) return; // must be a real stream URL
226
- const title = t ? t.str : 'Unknown';
227
- if (!items.find(i => i.title === title)) items.push({ title, file: url, subtitle: s ? s.str.replace(/\\\//g, '/') : '' });
228
- });
229
- return items.length ? items : null;
230
- }
231
-
232
- // Serial: find all episode IDs (sXXeYY) and group by season
233
- const epIdPrefix = Buffer.from('"id":"s');
234
- const allEpPositions = [];
235
- findAllInBuf(buf, epIdPrefix).forEach(pos => {
236
- let i = pos + epIdPrefix.length;
237
- while (i < buf.length && buf[i] >= 0x30 && buf[i] <= 0x39) i++;
238
- if (buf[i] !== 0x65) return;
239
- i++;
240
- while (i < buf.length && buf[i] >= 0x30 && buf[i] <= 0x39) i++;
241
- if (buf[i] !== 0x22) return;
242
- const fullId = buf.slice(pos + 6, i).toString('ascii');
243
- const seasonMatch = fullId.match(/^(s\d+)e(\d+)$/);
244
- if (!seasonMatch) return;
245
- const folderIdx = buf.indexOf(FOLDER_MARKER_BUF, pos);
246
- if (folderIdx < 0 || folderIdx > pos + 2000) return;
247
- const t = findBufVal(buf, 'title', pos, 200);
248
- allEpPositions.push({ id: fullId, seasonId: seasonMatch[1], epNum: parseInt(seasonMatch[2], 10), title: t ? t.str : ('Серия ' + seasonMatch[2]), pos });
249
- });
250
-
251
- const seasonMap = {};
252
- allEpPositions.forEach(ep => {
253
- if (!seasonMap[ep.seasonId]) seasonMap[ep.seasonId] = [];
254
- if (!seasonMap[ep.seasonId].find(e => e.id === ep.id)) seasonMap[ep.seasonId].push(ep);
255
- });
256
- const seasonIds = Object.keys(seasonMap).sort((a, b) => parseInt(a.substring(1)) - parseInt(b.substring(1)));
257
- if (!seasonIds.length) return null;
258
-
259
- return seasonIds.map(seasonId => {
260
- const eps = seasonMap[seasonId].sort((a, b) => a.epNum - b.epNum);
261
- const episodesWithVoices = eps.map((ep, ei) => {
262
- const nextEpPos = ei + 1 < eps.length ? eps[ei + 1].pos : buf.length;
263
- const eBuf = buf.slice(ep.pos, nextEpPos);
264
- const voices = [];
265
- findAllInBuf(eBuf, Buffer.from('"src_id"')).forEach(vpos => {
266
- const t = findBufVal(eBuf, 'title', vpos, 300);
267
- const f = findBufVal(eBuf, 'file', vpos, 2000);
268
- const s = findBufVal(eBuf, 'subtitle', vpos, 3000);
269
- if (!f) return;
270
- const url = normUrl(f.str.replace(/\\\//g, '/'));
271
- if (!url || !url.includes('.m3u8')) return;
272
- const title = t ? t.str : 'Unknown';
273
- if (!voices.find(v => v.title === title)) voices.push({ title, file: url, subtitle: s ? s.str.replace(/\\\//g, '/') : '' });
274
- });
275
- return { id: ep.id, title: ep.title, folder: voices };
276
- });
277
- return { id: seasonId, title: 'Сезон ' + parseInt(seasonId.substring(1)), folder: episodesWithVoices };
278
- });
279
- }
280
-
281
- // ── Multi-fetch helpers ───────────────────────────────────────────────────────
282
-
283
- function parseEpisodesFromHtml(html) {
284
- const fileMatch = html.match(/"file":"([^"]+)"/);
285
- if (!fileMatch) return {};
286
- const buf = decodeCinemarFile(fileMatch[1]);
287
- if (!buf || buf.length < 20) return {};
288
- if (buf.indexOf(FOLDER_MARKER_BUF) < 0) return {};
289
-
290
- const epIdPrefix = Buffer.from('"id":"s');
291
- const episodes = {};
292
- findAllInBuf(buf, epIdPrefix).forEach(pos => {
293
- let i = pos + epIdPrefix.length;
294
- let seasonNum = '';
295
- while (i < buf.length && buf[i] >= 0x30 && buf[i] <= 0x39) { seasonNum += String.fromCharCode(buf[i]); i++; }
296
- if (buf[i] !== 0x65) return;
297
- i++;
298
- let epNum = '';
299
- while (i < buf.length && buf[i] >= 0x30 && buf[i] <= 0x39) { epNum += String.fromCharCode(buf[i]); i++; }
300
- if (buf[i] !== 0x22) return;
301
-
302
- const fullId = 's' + seasonNum + 'e' + epNum;
303
- const seasonId = 's' + seasonNum;
304
- const t = findBufVal(buf, 'title', pos, 200);
305
- const title = t ? t.str : ('Серия ' + epNum);
306
-
307
- const nextEpPos = buf.indexOf(epIdPrefix, pos + 1);
308
- const endPos = nextEpPos > 0 ? Math.min(nextEpPos, pos + 5000) : pos + 5000;
309
- const eBuf = buf.slice(pos, endPos);
310
- const voices = [];
311
- findAllInBuf(eBuf, Buffer.from('"src_id"')).forEach(vpos => {
312
- const vt = findBufVal(eBuf, 'title', vpos, 300);
313
- const f = findBufVal(eBuf, 'file', vpos, 2000);
314
- const s = findBufVal(eBuf, 'subtitle', vpos, 3000);
315
- if (!f) return;
316
- const url = normUrl(f.str.replace(/\\\//g, '/'));
317
- if (!url || !url.includes('.m3u8')) return;
318
- const vtitle = vt ? vt.str : 'Unknown';
319
- if (!voices.find(v => v.title === vtitle)) voices.push({ title: vtitle, file: url, subtitle: s ? s.str.replace(/\\\//g, '/') : '' });
320
- });
321
-
322
- if (!episodes[fullId] || voices.length > episodes[fullId].voices.length)
323
- episodes[fullId] = { id: fullId, seasonId, seasonNum: parseInt(seasonNum), epNum: parseInt(epNum), title, voices };
324
- });
325
- return episodes;
326
- }
327
-
328
- function buildSerialDataFromMerged(mergedEpisodes) {
329
- const seasonMap = {};
330
- Object.values(mergedEpisodes).forEach(ep => {
331
- if (!seasonMap[ep.seasonId]) seasonMap[ep.seasonId] = [];
332
- seasonMap[ep.seasonId].push(ep);
333
- });
334
- const seasonIds = Object.keys(seasonMap).sort((a, b) => parseInt(a.substring(1)) - parseInt(b.substring(1)));
335
- const seasons = [], episodes = {}, voicesMap = {};
336
- seasonIds.forEach(sid => {
337
- const eps = seasonMap[sid].sort((a, b) => a.epNum - b.epNum);
338
- seasons.push({ id: sid, title: 'Сезон ' + parseInt(sid.substring(1)) });
339
- episodes[sid] = [];
340
- voicesMap[sid] = {};
341
- eps.forEach(ep => {
342
- episodes[sid].push({ id: ep.id, title: ep.title });
343
- voicesMap[sid][ep.id] = ep.voices.map(v => ({ label: v.title, url: normUrl(v.file), subtitles: parseSubs(v.subtitle) }));
344
- });
345
- });
346
- return { seasons, episodes, voices: voicesMap };
347
- }
348
-
349
- function parseCinemar(embedHtml) {
350
- let voices = [], masterUrl = null, serialData = null, contentType = 'movie';
351
- const rawItems = parseCinemarBase64(embedHtml);
352
- if (rawItems && rawItems.length) {
353
- const first = rawItems[0];
354
- if (first.folder && first.folder.length && first.folder[0] && first.folder[0].folder) {
355
- contentType = 'serial';
356
- const seasons = [], episodes = {}, voicesMap = {};
357
- rawItems.forEach((season, si) => {
358
- const sid = season.id || String(si + 1);
359
- seasons.push({ id: sid, title: season.title || ('Сезон ' + (si + 1)) });
360
- episodes[sid] = [];
361
- voicesMap[sid] = {};
362
- (season.folder || []).forEach((ep, ei) => {
363
- const eid = ep.id || String(ei + 1);
364
- episodes[sid].push({ id: eid, title: ep.title || ('Серия ' + (ei + 1)) });
365
- voicesMap[sid][eid] = (ep.folder || []).map(v => ({ label: v.title || 'Unknown', url: normUrl(v.file || ''), subtitles: parseSubs(v.subtitle) })).filter(v => v.url);
366
- });
367
- });
368
- serialData = { seasons, episodes, voices: voicesMap };
369
- const fs = seasons[0];
370
- if (fs && episodes[fs.id] && episodes[fs.id][0]) {
371
- const fv = voicesMap[fs.id][episodes[fs.id][0].id];
372
- if (fv && fv[0]) masterUrl = fv[0].url;
373
- }
374
- } else if (first.folder) {
375
- contentType = 'serial';
376
- const seasons = [], episodes = {}, voicesMap = {};
377
- rawItems.forEach((season, si) => {
378
- const sid = season.id || String(si + 1);
379
- seasons.push({ id: sid, title: season.title || ('Сезон ' + (si + 1)) });
380
- episodes[sid] = [];
381
- voicesMap[sid] = {};
382
- (season.folder || []).forEach((ep, ei) => {
383
- const eid = ep.id || String(ei + 1);
384
- episodes[sid].push({ id: eid, title: ep.title || ('Серия ' + (ei + 1)) });
385
- const u = normUrl(ep.file || '');
386
- voicesMap[sid][eid] = u ? [{ label: ep.title || 'Unknown', url: u, subtitles: parseSubs(ep.subtitle) }] : [];
387
- });
388
- });
389
- serialData = { seasons, episodes, voices: voicesMap };
390
- const fs = seasons[0];
391
- if (fs && episodes[fs.id] && episodes[fs.id][0]) {
392
- const fv = voicesMap[fs.id][episodes[fs.id][0].id];
393
- if (fv && fv[0]) masterUrl = fv[0].url;
394
- }
395
- } else {
396
- contentType = 'movie';
397
- voices = rawItems.map(item => ({ label: item.title || 'Unknown', url: normUrl(item.file || ''), subtitles: parseSubs(item.subtitle) })).filter(v => v.url);
398
- if (voices.length) masterUrl = voices[0].url;
399
- }
400
- }
401
- if (!masterUrl) {
402
- const fm = embedHtml.match(/["'](https?:\/\/[^"']*cinemap[^"']*\.m3u8[^"']*)['"]/);
403
- if (fm) masterUrl = fm[1];
404
- }
405
- return { contentType, voices, masterUrl, serialData };
406
- }
407
-
408
- // ── HLS proxy ─────────────────────────────────────────────────────────────────
409
-
410
- function handleHls(req, res) {
411
- const { query } = parseQuery(req.url);
412
- const targetUrl = query.url;
413
- if (!targetUrl) { res.writeHead(400, { 'Access-Control-Allow-Origin': '*' }); res.end('no url'); return; }
414
-
415
- function doRequest(reqUrl, redirectCount) {
416
- if (redirectCount > 5) { res.writeHead(500, { 'Access-Control-Allow-Origin': '*' }); res.end('too many redirects'); return; }
417
- let pu;
418
- try { pu = new URL(reqUrl); } catch(e) { res.writeHead(400); res.end('bad url'); return; }
419
- const mod = pu.protocol === 'https:' ? https : http;
420
- const isObrut = pu.hostname.includes('obrut') || pu.hostname.includes('superdupercdn');
421
- const isCinemap = pu.hostname.includes('cinemap');
422
- const isAlloha = pu.hostname.includes('stream-balancer') || pu.hostname.includes('streamalloha') || pu.hostname.includes('allo-');
423
- const isRstprg = pu.hostname.includes('rstprgapipt.com');
424
- const opts = {
425
- hostname: pu.hostname,
426
- port: pu.port || (pu.protocol === 'https:' ? 443 : 80),
427
- path: pu.pathname + pu.search,
428
- headers: {
429
- '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',
430
- 'accept': '*/*',
431
- 'accept-language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
432
- 'origin': isCinemap ? 'https://uakinogo.io' : isObrut ? 'https://kinojump.com' : isAlloha ? 'https://streamalloha.live' : isRstprg ? 'https://tv-1-kinoserial.net' : 'https://streamalloha.live',
433
- 'referer': isCinemap ? 'https://uakinogo.io/' : isObrut ? 'https://kinojump.com/' : isAlloha ? 'https://streamalloha.live/' : isRstprg ? 'https://tv-1-kinoserial.net/' : 'https://streamalloha.live/',
434
- 'sec-fetch-dest': isRstprg ? 'video' : 'empty',
435
- 'sec-fetch-mode': 'cors',
436
- 'sec-fetch-site': 'cross-site',
437
- },
438
- };
439
- if (req.headers['range']) opts.headers['range'] = req.headers['range'];
440
- const pr = mod.request(opts, upstream => {
441
- // Follow redirects
442
- if (upstream.statusCode >= 300 && upstream.statusCode < 400 && upstream.headers.location) {
443
- upstream.resume();
444
- let loc = upstream.headers.location;
445
- if (!loc.startsWith('http')) loc = pu.origin + loc;
446
- return doRequest(loc, redirectCount + 1);
447
- }
448
- const ct = upstream.headers['content-type'] || '';
449
- const isM3u8 = ct.includes('mpegurl') || (reqUrl.includes('.m3u8') || reqUrl.includes(':hls:manifest')) && !reqUrl.includes(':hls:seg-');
450
- const rh = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': '*' };
451
- if (isM3u8) {
452
- rh['Content-Type'] = 'application/vnd.apple.mpegurl';
453
- res.writeHead(upstream.statusCode, rh);
454
- let body = '';
455
- upstream.setEncoding('utf8');
456
- upstream.on('data', c => { body += c; });
457
- upstream.on('end', () => {
458
- const base = reqUrl.substring(0, reqUrl.lastIndexOf('/') + 1);
459
- const serverBase = 'https://' + (req.headers.host || 'localhost:' + PORT);
460
- function rewriteUrl(rel) {
461
- let a = rel.trim();
462
- if (!a) return a;
463
- if (a.startsWith('//')) a = 'https:' + a;
464
- else if (!a.startsWith('http')) a = base + a;
465
- return serverBase + '/hls?url=' + encodeURIComponent(a);
466
- }
467
- res.end(body.split('\n').map(line => {
468
- const tr = line.trim();
469
- if (!tr) return line;
470
- if (tr.startsWith('#')) return line.replace(/URI="([^"]+)"/g, (m, uri) => `URI="${rewriteUrl(uri)}"`);
471
- return rewriteUrl(tr);
472
- }).join('\n'));
473
- });
474
- } else {
475
- rh['Content-Type'] = ct || 'video/mp2t';
476
- if (upstream.headers['content-length']) rh['Content-Length'] = upstream.headers['content-length'];
477
- if (upstream.headers['content-range']) rh['Content-Range'] = upstream.headers['content-range'];
478
- res.writeHead(upstream.statusCode, rh);
479
- upstream.pipe(res);
480
- }
481
- });
482
- pr.on('error', e => { res.writeHead(500); res.end(e.message); });
483
- pr.end();
484
- }
485
-
486
- doRequest(targetUrl, 0);
487
- }
488
-
489
- // ── Server ────────────────────────────────────────────────────────────────────
490
-
491
- const server = http.createServer(async (req, res) => {
492
- const { pathname, query } = parseQuery(req.url);
493
- const startedAt = Date.now();
494
- const shouldLog = pathname === '/health' || pathname.startsWith('/uakinogo/') || pathname.startsWith('/alloha/') || pathname.startsWith('/turbo/');
495
- if (shouldLog) {
496
- try {
497
- const q = (pathname === '/uakinogo/search' && query.q) ? (' q=' + String(query.q).slice(0, 120)) : '';
498
- console.log('[req]', req.method, pathname + q);
499
- } catch (e) {}
500
- res.on('finish', () => {
501
- try { console.log('[res]', req.method, pathname, res.statusCode, (Date.now() - startedAt) + 'ms'); } catch (e) {}
502
- });
503
- }
504
-
505
- if (req.method === 'OPTIONS') {
506
- res.writeHead(204, { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Headers': '*' });
507
- res.end();
508
- return;
509
- }
510
-
511
- if (pathname === '/health') { jsonResp(res, { ok: true, time: new Date().toISOString() }); return; }
512
-
513
- if (pathname === '/veoveo/embed') {
514
- const embedUrl = query.url;
515
- if (!embedUrl || !embedUrl.includes('kinoserial')) {
516
- jsonResp(res, { ok: false, error: 'invalid url' }, 400); return;
517
- }
518
- console.log('[veoveo/embed] fetching:', embedUrl);
519
- fetchRaw(embedUrl, {
520
- 'referer': 'https://veoveo.ru/',
521
- 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
522
- 'sec-fetch-dest': 'document',
523
- 'sec-fetch-mode': 'navigate',
524
- 'sec-fetch-site': 'cross-site',
525
- }).then(text => {
526
- if (text.includes('id="cf-challenge"') || text.includes('cf-browser-verification')) {
527
- console.log('[veoveo/embed] Cloudflare challenge detected');
528
- // Could potentially fallback to puppeteer here if needed
529
- }
530
- res.writeHead(200, {
531
- 'Access-Control-Allow-Origin': '*',
532
- 'Content-Type': 'text/html; charset=utf-8'
533
- });
534
- res.end(text);
535
- }).catch(e => {
536
- console.log('[veoveo/embed] error:', e.message);
537
- jsonResp(res, { ok: false, error: e.message }, 500);
538
- });
539
- return;
540
- }
541
-
542
- if (pathname === '/veoveo/balancer') {
543
- const contentId = query.id;
544
- if (!contentId) { jsonResp(res, { ok: false, error: 'no id' }, 400); return; }
545
- console.log('[veoveo/balancer] contentId:', contentId);
546
- fetchRaw('https://api.rstprgapipt.com/balancer-api/proxy/playlists/catalog-api/episodes?content-id=' + contentId, {
547
- 'referer': 'https://veoveo.ru/',
548
- 'accept': 'application/json, text/plain, */*',
549
- 'sec-fetch-dest': 'empty',
550
- 'sec-fetch-mode': 'cors',
551
- 'sec-fetch-site': 'cross-site',
552
- }).then(text => {
553
- res.writeHead(200, {
554
- 'Access-Control-Allow-Origin': '*',
555
- 'Content-Type': 'application/json; charset=utf-8'
556
- });
557
- res.end(text);
558
- }).catch(e => {
559
- console.log('[veoveo/balancer] error:', e.message);
560
- jsonResp(res, { ok: false, error: e.message }, 500);
561
- });
562
- return;
563
- }
564
-
565
- // Show outbound IP of this server
566
- if (pathname === '/debug/ip') {
567
- fetchRaw('https://api.ipify.org?format=json', { 'user-agent': 'curl/7.0' })
568
- .then(d => { jsonResp(res, { ok: true, ip: JSON.parse(d).ip }); })
569
- .catch(e => { jsonResp(res, { ok: false, error: e.message }); });
570
- return;
571
- }
572
-
573
- if (pathname === '/debug/platform') {
574
- const fs = require('fs');
575
- const chromiumPaths = [
576
- process.env.PUPPETEER_EXECUTABLE_PATH,
577
- '/usr/bin/chromium',
578
- '/usr/bin/chromium-browser',
579
- '/usr/bin/google-chrome'
580
- ].filter(Boolean);
581
- const available = chromiumPaths.filter(p => { try { return fs.existsSync(p); } catch(e) { return false; } });
582
- jsonResp(res, {
583
- platform: process.platform,
584
- node: process.version,
585
- puppeteer_path: process.env.PUPPETEER_EXECUTABLE_PATH || 'not set',
586
- chromium_found: available,
587
- alloha_available: typeof getStreams === 'function' && getStreams.toString().includes('getBrowser') ? 'yes' : 'fallback'
588
- });
589
- return;
590
- }
591
- if (pathname === '/hls') { handleHls(req, res); return; }
592
-
593
- if (pathname === '/srt2vtt') {
594
- const corsH = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': '*', 'Content-Type': 'text/vtt; charset=utf-8' };
595
- const subUrl = query.url;
596
- if (!subUrl) { res.writeHead(400, corsH); res.end('no url'); return; }
597
- fetchRaw(subUrl, { 'user-agent': 'Mozilla/5.0 Chrome/120' }).then(body => {
598
- 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');
599
- res.writeHead(200, corsH); res.end(vtt);
600
- }).catch(e => { res.writeHead(500, corsH); res.end(e.message); });
601
- return;
602
- }
603
-
604
- if (pathname === '/alloha/player-info' && req.method === 'POST') {
605
- try {
606
- const params = JSON.parse(await readBody(req));
607
- const { token_movie, partner_token } = params;
608
- if (!token_movie) { jsonResp(res, { ok: false, error: 'token_movie required' }, 400); return; }
609
- const token = partner_token || ALLOHA_TOKEN;
610
- const playerUrl = `https://${ALLOHA_HOST}/?token_movie=${token_movie}&token=${encodeURIComponent(token)}`;
611
- console.log(`[server] /alloha/player-info token_movie=${token_movie}`);
612
- const html = await fetchRaw(playerUrl, { 'user-agent': 'Mozilla/5.0 Chrome/120', 'referer': 'https://kinokrad.my/' });
613
- const nkMatch = html.match(/name="viewporti"\s+content="([^"]+)"/);
614
- const nk = nkMatch ? nkMatch[1] : null;
615
- if (!nk) { jsonResp(res, { ok: false, error: 'nk_not_found' }); return; }
616
- let fileId = null;
617
- const flMatch = html.match(/"active"\s*:\s*\{[^}]*?"id"\s*:\s*(\d+)/);
618
- if (flMatch) fileId = flMatch[1];
619
- if (!fileId) { const m = html.match(/"id"\s*:\s*(\d+)/); if (m) fileId = m[1]; }
620
- let fileList = null;
621
- const flRaw = html.match(/fileList\s*=\s*JSON\.parse\('([\s\S]*?)'\)/);
622
- if (flRaw) { try { fileList = JSON.parse(flRaw[1]); } catch(e) {} }
623
- const appBundle = (html.match(/src="(\/build\/app\.[a-f0-9]+\.js)"/) || [])[1] || null;
624
- const runtimeBundle = (html.match(/src="(\/build\/runtime\.[a-f0-9]+\.js)"/) || [])[1] || null;
625
- const bundle539 = (html.match(/src="(\/build\/539\.[a-f0-9]+\.js)"/) || [])[1] || null;
626
- jsonResp(res, {
627
- ok: true, nk, file_id: fileId, player_url: playerUrl, file_list: fileList,
628
- bundle_app: appBundle ? `https://${ALLOHA_HOST}${appBundle}` : null,
629
- bundle_runtime: runtimeBundle ? `https://${ALLOHA_HOST}${runtimeBundle}` : null,
630
- bundle_539: bundle539 ? `https://${ALLOHA_HOST}${bundle539}` : null,
631
- });
632
- } catch(e) {
633
- console.log('[server] player-info ERROR:', e.message);
634
- jsonResp(res, { ok: false, error: e.message }, 500);
635
- }
636
- return;
637
- }
638
-
639
- if (pathname === '/alloha/streams' && req.method === 'POST') {
640
- try {
641
- const params = JSON.parse(await readBody(req));
642
- const { token_movie, partner_token, referer } = params;
643
- if (!token_movie) { jsonResp(res, { ok: false, error: 'token_movie required' }, 400); return; }
644
- console.log(`[server] /alloha/streams token_movie=${token_movie}`);
645
- const result = await getStreams({ token_movie, partner_token, referer });
646
- const serverBase = 'https://' + (req.headers.host || 'localhost:' + PORT);
647
- if (result.hlsSource) {
648
- result.hlsSource = result.hlsSource.map(src => {
649
- if (src.quality) {
650
- const newQ = {};
651
- for (const [q, u] of Object.entries(src.quality)) {
652
- newQ[q] = u.split(' or ').map(raw => {
653
- const t = raw.trim();
654
- const full = t.startsWith('//') ? 'https:' + t : t;
655
- return serverBase + '/hls?url=' + encodeURIComponent(full);
656
- }).join(' or ');
657
- }
658
- src.quality = newQ;
659
- }
660
- return src;
661
- });
662
- }
663
- jsonResp(res, result);
664
- } catch(e) {
665
- console.log('[server] streams ERROR:', e.message);
666
- jsonResp(res, { ok: false, error: e.message }, 500);
667
- }
668
- return;
669
- }
670
-
671
- if (pathname === '/uakinogo/search') {
672
- const q = query.q || '';
673
- if (!q) { jsonResp(res, { ok: false, error: 'no query' }, 400); return; }
674
- const qEnc = encodeURIComponent(q);
675
- const searchUrlSeo = 'https://uakinogo.io/search/' + qEnc;
676
- const searchUrlLegacy = 'https://uakinogo.io/index.php?do=search&subaction=search&story=' + qEnc;
677
- const parseSearchHtml = (html) => {
678
- const results = [];
679
- const addResult = (rawUrl, id, slug) => {
680
- if (!rawUrl || !id) return;
681
- let url = rawUrl;
682
- if (url.startsWith('//')) url = 'https:' + url;
683
- if (url.startsWith('/')) url = 'https://uakinogo.io' + url;
684
- if (!url.startsWith('http')) return;
685
- if (!url.includes('uakinogo.io/')) return;
686
- if (url.includes('do=search') || url.includes('/index.php')) return;
687
- if (!results.find(r => r.id === id)) results.push({ url, id, slug: slug || '' });
688
- };
689
-
690
- const reHref = /href\s*=\s*["']([^"']*?\/(\d+)-([^"'/]+)\.html(?:#[^"']*)?)["']/g;
691
- const reEscAbs = /(https?:\\\\\/\\\\\/uakinogo\.io\\\\\/[^"']*?\\\\\/(\d+)-([^"\\\/]+)\.html)/g;
692
- const reEscRel = /(\\\\\/[^"']*?\\\\\/(\d+)-([^"\\\/]+)\.html)/g;
693
- let m;
694
- while ((m = reHref.exec(html)) !== null) addResult(m[1], m[2], m[3]);
695
- while ((m = reEscAbs.exec(html)) !== null) addResult(m[1].replace(/\\\\\//g, '/'), m[2], m[3]);
696
- while ((m = reEscRel.exec(html)) !== null) addResult(m[1].replace(/\\\\\//g, '/'), m[2], m[3]);
697
- return results;
698
- };
699
-
700
- const looksLikeChallenge = (html) => {
701
- const t = String(html || '');
702
- if (!t) return true;
703
- if (t.includes('cf-challenge') || t.includes('cf-browser-verification')) return true;
704
- if (/Just a moment/i.test(t) && /Cloudflare/i.test(t)) return true;
705
- if (t.includes('data-sitekey') && t.includes('cf-turnstile')) return true;
706
- return false;
707
- };
708
-
709
- const fetchAndParse = async (url) => {
710
- const html = await fetchRaw(url, { 'user-agent': 'Mozilla/5.0 Chrome/120', 'referer': 'https://uakinogo.io/' });
711
- return { html, results: parseSearchHtml(html), challenged: looksLikeChallenge(html) };
712
- };
713
-
714
- fetchAndParse(searchUrlSeo).then(async (first) => {
715
- let html = first.html;
716
- let results = first.results;
717
- let challenged = first.challenged;
718
-
719
- if (challenged || results.length === 0) {
720
- try {
721
- const second = await fetchAndParse(searchUrlLegacy);
722
- if (second.results.length > results.length) {
723
- html = second.html;
724
- results = second.results;
725
- challenged = challenged || second.challenged;
726
- }
727
- } catch (e) {}
728
- }
729
-
730
- if ((challenged || results.length === 0) && typeof getStreams === 'function' && getStreams.toString) {
731
- try {
732
- const { getBrowser } = require('./alloha');
733
- const browser = await getBrowser();
734
- const page = await browser.newPage();
735
- 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');
736
- await page.setExtraHTTPHeaders({ 'Accept-Language': 'ru-RU,ru;q=0.9,en;q=0.8', 'Referer': 'https://uakinogo.io/' });
737
-
738
- await page.goto(searchUrlSeo, { waitUntil: 'networkidle2', timeout: 25000 });
739
- try { await page.waitForSelector('a[href*=".html"]', { timeout: 8000 }); } catch (e) {}
740
- await new Promise(r => setTimeout(r, 1200));
741
- const html2 = await page.content();
742
- await page.close();
743
-
744
- const parsed = parseSearchHtml(html2);
745
- if (parsed.length >= results.length) {
746
- html = html2;
747
- results = parsed;
748
- challenged = looksLikeChallenge(html2);
749
- }
750
- } catch (e) {}
751
- }
752
-
753
- try { console.log('[uakinogo/search] results:', results.length, results[0] ? results[0].url : 'none'); } catch (e) {}
754
- if (results.length === 0) {
755
- try {
756
- const t = String(html || '');
757
- console.log('[uakinogo/search] html:', 'len=' + t.length, 'cf=' + (looksLikeChallenge(t) ? '1' : '0'), 'sample=' + t.slice(0, 160).replace(/\s+/g, ' '));
758
- } catch (e) {}
759
- }
760
- jsonResp(res, { ok: true, results: results.slice(0, 10) });
761
- }).catch(e => jsonResp(res, { ok: false, error: e.message }, 500));
762
- return;
763
- }
764
-
765
- if (pathname === '/uakinogo/embed') {
766
- const pageUrl = query.url;
767
- if (!pageUrl) { jsonResp(res, { ok: false, error: 'no url' }, 400); return; }
768
- fetchRaw(pageUrl, { 'user-agent': 'Mozilla/5.0 Chrome/120', 'referer': 'https://uakinogo.io/' }).then(html => {
769
- const m = html.match(/"embedUrl"\s*:\s*"(https:\/\/cinemar\.cc\/embed\/[^"]+)"/);
770
- jsonResp(res, { ok: true, embed_url: m ? m[1] : null });
771
- }).catch(e => jsonResp(res, { ok: false, error: e.message }, 500));
772
- return;
773
- }
774
-
775
- // ── /turbo/search?q=TITLE ─────────────────────────────────────────────────
776
- if (pathname === '/turbo/search') {
777
- const q = query.q || '';
778
- if (!q) { jsonResp(res, { ok: false, error: 'no query' }, 400); return; }
779
- turboSearch(q)
780
- .then(results => jsonResp(res, { ok: true, results }))
781
- .catch(e => jsonResp(res, { ok: false, error: e.message }, 500));
782
- return;
783
- }
784
-
785
- // ── /turbo/search-embed?q=TITLE ──────────────────────────────────────────
786
- if (pathname === '/turbo/search-embed') {
787
- const q = query.q || '';
788
- if (!q) { jsonResp(res, { ok: false, error: 'no query' }, 400); return; }
789
- const { parseObrutEmbed, buildSerialFromEntries, extractMovieVoices, getKinojumpHtml } = require('./turbo');
790
- (async () => {
791
- // Step 1: search kinojump
792
- const results = await turboSearch(q);
793
- if (!results.length) throw new Error('not found on kinojump');
794
- const pageUrl = results[0].url;
795
- console.log('[turbo/search-embed] page:', pageUrl);
796
-
797
- // Step 2: get embed URL — plain HTTP first, Puppeteer fallback
798
- const { getBrowser } = require('./alloha');
799
- const browser = await getBrowser();
800
- const pageHtml = await getKinojumpHtml(pageUrl, browser).catch(e => {
801
- console.log('[turbo/search-embed] getKinojumpHtml error:', e.message);
802
- return '';
803
- });
804
- const em = pageHtml.match(/(?:https?:\/\/)?(?:([a-z0-9]+)\.)?obrut\.show\/embed\/([A-Za-z0-9]+)\/content\/([A-Za-z0-9]+)/);
805
- if (!em) throw new Error('obrut embed URL not found');
806
- const subdomain = em[1] ? em[1] + '.obrut.show' : '49372504.obrut.show';
807
- const embedUrl = 'https://' + subdomain + '/embed/' + em[2] + '/content/' + em[3];
808
- console.log('[turbo/search-embed] embed URL:', embedUrl);
809
- // Step 3: parse embed (retry until enough entries)
810
- const result = await parseObrutEmbed(embedUrl);
811
- if (!result) throw new Error('failed to parse obrut embed');
812
-
813
- if (result.entries.length > 0) {
814
- const serial = buildSerialFromEntries(result.entries);
815
- jsonResp(res, { ok: true, embed_url: embedUrl, content_type: 'serial', serial });
816
- } else if (result.movieVoices.length > 0) {
817
- jsonResp(res, { ok: true, embed_url: embedUrl, content_type: 'movie', voices: result.movieVoices });
818
- } else {
819
- throw new Error('no data found in obrut embed');
820
- }
821
- })().catch(e => { console.log('[turbo/search-embed] error:', e.message); jsonResp(res, { ok: false, error: e.message }, 500); });
822
- return;
823
- }
824
-
825
- // ── /turbo/stream?url=PAGE_URL ────────────────────────────────────────────
826
- if (pathname === '/turbo/stream') {
827
- const pageUrl = query.url;
828
- if (!pageUrl) { jsonResp(res, { ok: false, error: 'no url' }, 400); return; }
829
- getTurboStream(pageUrl)
830
- .then(data => jsonResp(res, { ok: true, ...data }))
831
- .catch(e => { console.log('[turbo] error:', e.message); jsonResp(res, { ok: false, error: e.message }, 500); });
832
- return;
833
- }
834
-
835
- // ── /turbo/proxy-embed?url=OBRUT_EMBED_URL ───────────────────────────────
836
- // Raw HTTP proxy for obrut embed — adds correct Referer header
837
- // Used by browser clients that can't set Referer (file:/// origin)
838
- if (pathname === '/turbo/proxy-embed') {
839
- const embedUrl = query.url;
840
- if (!embedUrl || !embedUrl.includes('obrut.show')) {
841
- jsonResp(res, { ok: false, error: 'invalid url' }, 400); return;
842
- }
843
- (async () => {
844
- const html = await fetchRaw(embedUrl, {
845
- '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',
846
- 'referer': 'https://kinojump.com/',
847
- 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
848
- 'accept-language': 'ru-RU,ru;q=0.9,en;q=0.8',
849
- 'accept-encoding': 'identity',
850
- });
851
- console.log('[turbo/proxy-embed] len:', html.length, 'hasPlayer:', html.includes('new Player('));
852
- res.writeHead(200, {
853
- 'Access-Control-Allow-Origin': '*',
854
- 'Access-Control-Allow-Headers': '*',
855
- 'Content-Type': 'text/html; charset=utf-8',
856
- });
857
- res.end(html);
858
- })().catch(e => {
859
- console.log('[turbo/proxy-embed] error:', e.message);
860
- res.writeHead(500, { 'Access-Control-Allow-Origin': '*' });
861
- res.end('error: ' + e.message);
862
- });
863
- return;
864
- }
865
-
866
- // ── /turbo/embed?url=OBRUT_EMBED_URL ─────────────────────────────────────
867
- // Parse obrut embed page directly (no Puppeteer needed)
868
- if (pathname === '/turbo/embed') {
869
- const embedUrl = query.url;
870
- if (!embedUrl) { jsonResp(res, { ok: false, error: 'no url' }, 400); return; }
871
- const { parseObrutEmbed, buildSerialFromEntries } = require('./turbo');
872
- parseObrutEmbed(embedUrl)
873
- .then(result => {
874
- if (!result || (result.entries.length === 0 && result.movieVoices.length === 0)) {
875
- jsonResp(res, { ok: false, error: 'failed to parse embed' }); return;
876
- }
877
- if (result.entries.length > 0) {
878
- const serial = buildSerialFromEntries(result.entries);
879
- jsonResp(res, { ok: true, embed_url: embedUrl, content_type: 'serial', serial, entry_count: result.entries.length });
880
- } else {
881
- jsonResp(res, { ok: true, embed_url: embedUrl, content_type: 'movie', voices: result.movieVoices });
882
- }
883
- })
884
- .catch(e => { console.log('[turbo/embed] error:', e.message); jsonResp(res, { ok: false, error: e.message }, 500); });
885
- return;
886
- }
887
-
888
- // ── /uakinogo/stream?url=PAGE_URL ─────────────────────────────────────────
889
- if (pathname === '/uakinogo/stream') {
890
- const pageUrl = query.url;
891
- if (!pageUrl) { jsonResp(res, { ok: false, error: 'no url' }, 400); return; }
892
- getCinemarStream(pageUrl)
893
- .then(data => jsonResp(res, { ok: true, ...data }))
894
- .catch(e => { console.log('[cinemar] error:', e.message); jsonResp(res, { ok: false, error: e.message }, 500); });
895
- return;
896
- }
897
-
898
- // ── /puppeteer/fetch?url=URL — fetch URL via Puppeteer (bypasses Cloudflare JS challenge)
899
- if (pathname === '/puppeteer/fetch') {
900
- const targetUrl = query.url;
901
- if (!targetUrl) { jsonResp(res, { ok: false, error: 'no url' }, 400); return; }
902
- (async () => {
903
- let browser;
904
- try {
905
- const { getBrowser } = require('./alloha');
906
- browser = await getBrowser();
907
- const page = await browser.newPage();
908
- 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');
909
- await page.setExtraHTTPHeaders({ 'Accept-Language': 'ru-RU,ru;q=0.9,en;q=0.8' });
910
- // Navigate and wait for JS challenge to complete
911
- await page.goto(targetUrl, { waitUntil: 'networkidle2', timeout: 20000 });
912
- // Extra wait for fingerprint redirect
913
- await new Promise(r => setTimeout(r, 2500));
914
- const html = await page.content();
915
- const finalUrl = page.url();
916
- await page.close();
917
- jsonResp(res, { ok: true, html, url: finalUrl });
918
- } catch(e) {
919
- console.log('[puppeteer/fetch] error:', e.message);
920
- jsonResp(res, { ok: false, error: e.message }, 500);
921
- }
922
- })();
923
- return;
924
- }
925
-
926
- jsonResp(res, { ok: false, error: 'not found' }, 404);
927
- });
928
-
929
- server.listen(PORT, () => console.log(`[server] Alloha+Cinemar proxy on port ${PORT}`));
930
-
931
- process.on('SIGTERM', async () => { await closeBrowser(); server.close(); });
932
- process.on('SIGINT', async () => { await closeBrowser(); process.exit(0); });
 
1
+ const express = require('express')
2
+ const fs = require('fs')
3
+ const path = require('path')
4
+ const babel = require('@babel/core')
5
+
6
+ const regeneratorRuntimeCode = fs.readFileSync(require.resolve('regenerator-runtime/runtime'), 'utf8')
7
+
8
+ const app = express()
9
+
10
+ app.disable('x-powered-by')
11
+ app.set('trust proxy', true)
12
+
13
+ const MAX_BYTES = 2 * 1024 * 1024
14
+ const MAX_CSS_DEPTH = 6
15
+
16
+ function getSelfBase(req) {
17
+ const protoRaw = (req.headers['x-forwarded-proto'] || req.protocol || 'https') + ''
18
+ const proto = protoRaw.split(',')[0].trim() || 'https'
19
+ const host = (req.headers['x-forwarded-host'] || req.get('host') || '').split(',')[0].trim()
20
+ return proto + '://' + host
21
+ }
22
+
23
+ function normalizeUrl(u) {
24
+ if (!u) return null
25
+ let s = String(u).trim()
26
+ if (!s) return null
27
+ try {
28
+ const url = new URL(s)
29
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') return null
30
+ url.hash = ''
31
+ return url.toString()
32
+ } catch (e) {
33
+ return null
34
+ }
35
+ }
36
+
37
+ async function fetchText(url, timeoutMs) {
38
+ const controller = new AbortController()
39
+ const t = setTimeout(() => controller.abort(), timeoutMs)
40
+
41
+ try {
42
+ const res = await fetch(url, {
43
+ redirect: 'follow',
44
+ signal: controller.signal,
45
+ headers: {
46
+ 'user-agent': 'lampa-es5-proxy/0.1'
47
+ }
48
+ })
49
+
50
+ const status = res.status
51
+ if (!res.ok) {
52
+ const txt = await res.text().catch(() => '')
53
+ const e = new Error('Fetch failed: ' + status)
54
+ e.status = status
55
+ e.body = txt
56
+ throw e
57
+ }
58
+
59
+ const buf = await res.arrayBuffer()
60
+ if (buf.byteLength > MAX_BYTES) {
61
+ const e = new Error('Too large')
62
+ e.status = 413
63
+ throw e
64
+ }
65
+
66
+ const text = Buffer.from(buf).toString('utf8')
67
+ const contentType = res.headers.get('content-type') || ''
68
+ return { text, contentType, status }
69
+ } finally {
70
+ clearTimeout(t)
71
+ }
72
+ }
73
+
74
+ function jsWrapper(serverBase, baseUrl, transpiledCode, needRegenerator) {
75
+ const safeServer = JSON.stringify(String(serverBase || ''))
76
+ const safeBaseUrl = JSON.stringify(String(baseUrl || ''))
77
+
78
+ const fetchPolyfill =
79
+ '(function(){' +
80
+ "if(typeof window.fetch==='function')return;" +
81
+ "function H(h){this.map={};if(!h)return;for(var k in h)if(h.hasOwnProperty(k))this.map[k.toLowerCase()]=String(h[k]);}" +
82
+ "H.prototype.get=function(k){return this.map[String(k).toLowerCase()]||null;};" +
83
+ "function R(b,o){this._body=b||'';this.status=o&&o.status||200;this.statusText=o&&o.statusText||'OK';this.headers=new H(o&&o.headers||{});this.ok=this.status>=200&&this.status<300;}" +
84
+ "R.prototype.text=function(){return Promise.resolve(String(this._body));};" +
85
+ "window.fetch=function(u,opt){opt=opt||{};return new Promise(function(resolve,reject){try{var x=new XMLHttpRequest();x.open((opt.method||'GET'),u,true);var h=opt.headers||{};for(var k in h)if(h.hasOwnProperty(k))x.setRequestHeader(k,h[k]);x.onreadystatechange=function(){if(x.readyState===4){resolve(new R(x.responseText,{status:x.status||0,statusText:x.statusText||'',headers:{}}));}};x.onerror=function(){reject(new Error('Network error'));};x.send(opt.body||null);}catch(e){reject(e);}});};" +
86
+ '})();'
87
+
88
+ const cssVarsPonyfill =
89
+ '(function(){' +
90
+ 'function t(v){return (v===null||v===undefined)?\'\':String(v);}' +
91
+ 'function gOrigin(u){try{var a=document.createElement("a");a.href=u;return (a.protocol? a.protocol:"") + "//" + (a.host||"");}catch(e){return "";}}' +
92
+ 'function collect(){var map={};var styles=document.getElementsByTagName("style");for(var i=0;i<styles.length;i++){var css=t(styles[i].textContent||styles[i].innerText||"");var m=css.match(/:root\\s*\\{[\\s\\S]*?\\}/g);if(!m)continue;for(var j=0;j<m.length;j++){var block=m[j];var r=/--([A-Za-z0-9_-]+)\\s*:\\s*([^;]+);/g;var mm;while((mm=r.exec(block))){map["--"+mm[1]]=t(mm[2]).replace(/^\\s+|\\s+$/g,"");}}}return map;}' +
93
+ 'function applyVars(map){var styles=document.getElementsByTagName("style");for(var i=0;i<styles.length;i++){var css=t(styles[i].textContent||styles[i].innerText||"");if(css.indexOf("var(")===-1)continue;var out=css.replace(/var\\(\\s*(--[A-Za-z0-9_-]+)\\s*(?:,\\s*([^\\)]+))?\\)/g,function(_,name,fb){var v=map[name];if(v)return v;return fb?String(fb).replace(/^\\s+|\\s+$/g,""):"";});if(out!==css){try{styles[i].textContent=out;}catch(e){try{styles[i].innerText=out;}catch(e2){}}}}}' +
94
+ 'window.__es5CssVarsApply=function(){try{var map=collect();applyVars(map);}catch(e){}};' +
95
+ '})();'
96
+
97
+ const patchLoaders =
98
+ '(function(){' +
99
+ 'var __es5_server=' +
100
+ safeServer +
101
+ ';' +
102
+ 'var __es5_base=' +
103
+ safeBaseUrl +
104
+ ';' +
105
+ 'var __es5_active=true;' +
106
+ 'function t(v){return (v===null||v===undefined)?\'\':String(v);}' +
107
+ 'function baseDir(u){u=t(u);u=u.replace(/[?#].*$/,"");return u.replace(/[^\\/]*$/,"");}' +
108
+ 'var __es5_base_dir=baseDir(__es5_base);' +
109
+ 'function abs(u){u=t(u);if(!u)return u;if(u.indexOf("/p?url=")!==-1||u.indexOf("/s?url=")!==-1)return u;if(u.indexOf("http://")===0||u.indexOf("https://")===0)return u;if(u.indexOf("//")===0)return (location.protocol||"https:")+u;if(u.charAt(0)=="/"){var a=document.createElement("a");a.href=__es5_base;return (a.protocol? a.protocol:"") + "//" + (a.host||"") + u;}return __es5_base_dir + u;}' +
110
+ 'function origin(u){try{var a=document.createElement("a");a.href=u;return (a.protocol? a.protocol:"") + "//" + (a.host||"");}catch(e){return "";}}' +
111
+ 'var allow=window.__es5ProxyAllow||{};window.__es5ProxyAllow=allow;' +
112
+ 'allow[origin(__es5_base)]=1;' +
113
+ 'function proxyJs(u){var a=abs(u);var o=origin(a);if(__es5_active)allow[o]=1;if(allow[o]&&__es5_server)return __es5_server + "/p?url=" + encodeURIComponent(a);return a;}' +
114
+ 'function proxyCss(u){var a=abs(u);var o=origin(a);if(__es5_active)allow[o]=1;if(allow[o]&&__es5_server)return __es5_server + "/s?url=" + encodeURIComponent(a);return a;}' +
115
+ 'function mapArr(items, fn){if(!items||!items.length)return items;for(var i=0;i<items.length;i++){items[i]=fn(items[i]);}return items;}' +
116
+ 'try{' +
117
+ 'if(window.Lampa&&Lampa.Utils){' +
118
+ 'if(Lampa.Utils.putScriptAsync&&!Lampa.Utils.putScriptAsync.__es5_proxy){var o=Lampa.Utils.putScriptAsync;var w=function(items,complite,error,success,show_logs){try{items=mapArr(items,proxyJs);}catch(e){}return o.call(this,items,complite,error,success,show_logs);};w.__es5_proxy=1;Lampa.Utils.putScriptAsync=w;}' +
119
+ 'if(Lampa.Utils.putScript&&!Lampa.Utils.putScript.__es5_proxy){var o2=Lampa.Utils.putScript;var w2=function(items,complite,error,success,show_logs){try{items=mapArr(items,proxyJs);}catch(e){}return o2.call(this,items,complite,error,success,show_logs);};w2.__es5_proxy=1;Lampa.Utils.putScript=w2;}' +
120
+ 'if(Lampa.Utils.putStyle&&!Lampa.Utils.putStyle.__es5_proxy){var o3=Lampa.Utils.putStyle;var w3=function(items,complite,error){try{items=mapArr(items,proxyCss);}catch(e){}return o3.call(this,items,complite,error);};w3.__es5_proxy=1;Lampa.Utils.putStyle=w3;}' +
121
+ '}' +
122
+ '}catch(e){}' +
123
+ 'try{if(window.__es5CssVarsApply)window.__es5CssVarsApply();}catch(e){}' +
124
+ 'try{' +
125
+ transpiledCode +
126
+ '}catch(e){try{console.error("ES5 proxy plugin error",e&&e.message?e.message:e);}catch(e2){}}' +
127
+ '__es5_active=false;' +
128
+ 'try{if(window.__es5CssVarsApply)window.__es5CssVarsApply();}catch(e){}' +
129
+ '})();'
130
+
131
+ const parts = []
132
+ parts.push(fetchPolyfill)
133
+ if (needRegenerator) parts.push(regeneratorRuntimeCode)
134
+ parts.push(cssVarsPonyfill)
135
+ parts.push(patchLoaders)
136
+ return parts.join('\n')
137
+ }
138
+
139
+ async function cssInline(url, depth) {
140
+ if (depth > MAX_CSS_DEPTH) return ''
141
+ const { text } = await fetchText(url, 25_000)
142
+ const base = url.replace(/[?#].*$/, '').replace(/[^/]*$/, '')
143
+ let css = text
144
+
145
+ css = css.replace(/@import\s+(?:url\()?\s*["']?([^"')\s]+)["']?\s*\)?\s*;/gi, (m, imp) => {
146
+ try {
147
+ const abs = new URL(String(imp), base).toString()
148
+ return '/*__es5_import__:' + abs + '*/'
149
+ } catch (e) {
150
+ return ''
151
+ }
152
+ })
153
+
154
+ const imports = []
155
+ css.replace(/\/\*__es5_import__:(.*?)\*\//g, (m, u) => {
156
+ imports.push(u)
157
+ return m
158
+ })
159
+
160
+ for (let i = 0; i < imports.length; i++) {
161
+ let inlined = ''
162
+ try {
163
+ inlined = await cssInline(imports[i], depth + 1)
164
+ } catch (e) {
165
+ inlined = ''
166
+ }
167
+ css = css.replace('/*__es5_import__:' + imports[i] + '*/', inlined)
168
+ }
169
+
170
+ css = css.replace(/url\(\s*(['"]?)([^'")]+)\1\s*\)/gi, (m, q, u) => {
171
+ const raw = String(u || '').trim()
172
+ if (!raw) return m
173
+ if (/^(data:|blob:|https?:\/\/|\/\/)/i.test(raw)) return 'url(' + raw + ')'
174
+ try {
175
+ const abs = new URL(raw, base).toString()
176
+ return 'url(' + abs + ')'
177
+ } catch (e) {
178
+ return m
179
+ }
180
+ })
181
+
182
+ return css
183
+ }
184
+
185
+ app.get('/health', (req, res) => {
186
+ res.status(200).json({ ok: true })
187
+ })
188
+
189
+ app.get('/client/es5_proxy_store.js', (req, res) => {
190
+ res.setHeader('content-type', 'application/javascript; charset=utf-8')
191
+ res.setHeader('cache-control', 'public, max-age=600')
192
+ res.sendFile(path.join(__dirname, '..', 'es5_proxy_store.js'))
193
+ })
194
+
195
+ app.get('/s', async (req, res) => {
196
+ const source = normalizeUrl(req.query.url)
197
+ if (!source) return res.status(400).type('text/plain').send('Bad url')
198
+
199
+ try {
200
+ const css = await cssInline(source, 0)
201
+ res.setHeader('content-type', 'text/css; charset=utf-8')
202
+ res.setHeader('cache-control', 'public, max-age=600')
203
+ res.status(200).send(css)
204
+ } catch (e) {
205
+ const code = e && e.status ? e.status : 500
206
+ res.status(code).type('text/plain').send('CSS fetch error')
207
+ }
208
+ })
209
+
210
+ app.get('/p', async (req, res) => {
211
+ const source = normalizeUrl(req.query.url)
212
+ if (!source) return res.status(400).type('text/plain').send('Bad url')
213
+
214
+ try {
215
+ const { text } = await fetchText(source, 25_000)
216
+ const result = await babel.transformAsync(text, {
217
+ babelrc: false,
218
+ configFile: false,
219
+ sourceType: 'script',
220
+ compact: true,
221
+ comments: false,
222
+ presets: [
223
+ [
224
+ require('@babel/preset-env'),
225
+ {
226
+ targets: { chrome: '38' },
227
+ bugfixes: true,
228
+ loose: true,
229
+ modules: false
230
+ }
231
+ ]
232
+ ]
233
+ })
234
+
235
+ const code = (result && result.code) ? result.code : ''
236
+ const needRegenerator = code.indexOf('regeneratorRuntime') !== -1
237
+
238
+ const serverBase = getSelfBase(req)
239
+ const out = jsWrapper(serverBase, source, code, needRegenerator)
240
+
241
+ res.setHeader('content-type', 'application/javascript; charset=utf-8')
242
+ res.setHeader('cache-control', 'public, max-age=600')
243
+ res.status(200).send(out)
244
+ } catch (e) {
245
+ const code = e && e.status ? e.status : 500
246
+ res.status(code).type('text/plain').send('JS transform error')
247
+ }
248
+ })
249
+
250
+ const port = Number(process.env.PORT || 7860)
251
+ app.listen(port, () => {
252
+ console.log('lampa-es5-proxy on :' + port)
253
+ })