recycleactor commited on
Commit
76e0f14
Β·
verified Β·
1 Parent(s): b8c5695

Upload turbo.js

Browse files
Files changed (1) hide show
  1. turbo.js +168 -269
turbo.js CHANGED
@@ -1,14 +1,11 @@
1
  /**
2
- * Turbo (obrut.show) stream extractor
3
- * Flow: embed URL β†’ fetch HTML β†’ parse Player() base64 β†’ extract entries via regex
4
  */
5
  const { getBrowser } = require('./alloha');
6
  const https = require('https');
7
  const http = require('http');
8
 
9
- const KINOJUMP_BASE = 'https://kinojump.com';
10
-
11
- // ── HTTP helper ───────────────────────────────────────────────────────────────
12
  function fetchRaw(reqUrl, headers) {
13
  return new Promise((resolve, reject) => {
14
  let u; try { u = new URL(reqUrl); } catch(e) { return reject(e); }
@@ -20,33 +17,23 @@ function fetchRaw(reqUrl, headers) {
20
  if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
21
  let loc = res.headers.location.startsWith('http')
22
  ? res.headers.location
23
- : new URL(reqUrl).origin + res.headers.location;
24
- // Don't follow redirect to web.kinojump.com β€” stay on kinojump.com
25
  loc = loc.replace('web.kinojump.com', 'kinojump.com');
 
26
  return fetchRaw(loc, headers).then(resolve).catch(reject);
27
  }
28
- let d = '';
29
- // Handle gzip/deflate (Railway proxy may add it)
30
- const encoding = res.headers['content-encoding'];
31
- let stream = res;
32
- if (encoding === 'gzip' || encoding === 'deflate') {
33
- const zlib = require('zlib');
34
- stream = encoding === 'gzip' ? res.pipe(zlib.createGunzip()) : res.pipe(zlib.createInflate());
35
- }
36
- stream.setEncoding('utf8');
37
- stream.on('data', c => d += c);
38
- stream.on('end', () => resolve(d));
39
- stream.on('error', reject);
40
  });
41
  req.on('error', reject);
42
- req.setTimeout(15000, () => { req.destroy(); reject(new Error('timeout')); });
43
  req.end();
44
  });
45
  }
46
 
47
  // ── Search kinojump ───────────────────────────────────────────────────────────
48
  async function turboSearch(query) {
49
- // Search on web.kinojump.com (kinojump.com redirects to web. for search)
50
  const searchUrl = 'https://web.kinojump.com/index.php?do=search&subaction=search&story=' + encodeURIComponent(query);
51
  const html = await fetchRaw(searchUrl, {
52
  'user-agent': 'Mozilla/5.0 Chrome/120',
@@ -57,7 +44,6 @@ async function turboSearch(query) {
57
  let m;
58
  while ((m = re.exec(html)) !== null) {
59
  if (!results.find(r => r.id === m[2])) {
60
- // Always use kinojump.com (not web.) β€” web. has no player
61
  const url = m[1].replace('web.kinojump.com', 'kinojump.com');
62
  results.push({ url, id: m[2], slug: m[3] });
63
  }
@@ -65,191 +51,89 @@ async function turboSearch(query) {
65
  return results.slice(0, 10);
66
  }
67
 
68
- // ── Get kinojump page HTML β€” try plain HTTP first, then Puppeteer ────────────
69
  async function getKinojumpHtml(pageUrl, browser) {
70
  const normalizedUrl = pageUrl.replace('web.kinojump.com', 'kinojump.com');
71
- // Try plain HTTP first β€” with PHPSESSID cookie to bypass Cloudflare Rocket Loader
72
- // Retry multiple times since kinojump may load-balance between servers
73
- for (let attempt = 0; attempt < 5; attempt++) {
74
  try {
75
  const html = await fetchRaw(normalizedUrl, {
76
  'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120 Safari/537.36',
77
- 'referer': KINOJUMP_BASE + '/',
78
  'accept': 'text/html',
79
- 'accept-language': 'ru-RU,ru;q=0.9',
80
- 'accept-encoding': 'identity',
81
- 'cookie': 'PHPSESSID=a1b2c3d4e5f6; dle_user_id=0; dle_password=0'
82
  });
83
- if (html.includes('obrut')) {
84
- const refs = html.match(/[a-z0-9]*obrut[^\s"'<>]{0,100}/g);
85
- console.log('[turbo] got HTML via plain HTTP');
86
- console.log('[turbo] obrut refs:', refs ? refs.slice(0,3) : 'none');
87
- const hasEmbed = html.includes('obrut.show/embed/');
88
- if (hasEmbed) return html;
89
- if (attempt < 4) {
90
- console.log('[turbo] no embed URL, retrying... (' + (attempt+1) + '/5)');
91
- await new Promise(r => setTimeout(r, 500));
92
- continue;
93
- }
94
- console.log('[turbo] obrut found but no embed URL - trying Puppeteer');
95
  }
96
- console.log('[turbo] plain HTTP no obrut, len:', html.length, 'has helper:', html.includes('s1obrut'), 'has rocket:', html.includes('rocket-loader'));
97
- const playerIdx = html.indexOf('pmovie__player');
98
- if (playerIdx > 0) console.log('[turbo] player area:', html.substring(playerIdx, playerIdx + 500));
99
- } catch(e) { console.log('[turbo] plain HTTP failed:', e.message); }
100
- break;
 
 
101
  }
102
 
103
- // Fallback: Puppeteer β€” intercept network requests to catch obrut embed URL
104
  console.log('[turbo] using Puppeteer for kinojump page');
105
  const page = await browser.newPage();
106
  try {
107
  await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120 Safari/537.36');
108
  await page.setExtraHTTPHeaders({ 'Accept-Language': 'ru-RU,ru;q=0.9' });
109
- await page.setCookie({ name: 'PHPSESSID', value: 'a1b2c3d4e5f6', domain: 'kinojump.com' });
 
 
 
 
 
110
 
111
  let obrutEmbedUrl = null;
112
- let generalJsContent = null;
113
-
114
  await page.setRequestInterception(true);
115
  page.on('request', req => {
116
  const u = req.url();
117
  if (u.includes('obrut.show/embed/') && !obrutEmbedUrl) {
118
  obrutEmbedUrl = u;
119
- console.log('[turbo] intercepted obrut embed request:', u.substring(0, 120));
 
 
 
 
 
 
 
120
  }
121
- req.continue();
122
- });
123
- page.on('response', async res => {
124
- try {
125
- const u = res.url();
126
- if (u.includes('general') && u.includes('kinojump') && !generalJsContent) {
127
- generalJsContent = await res.text().catch(() => null);
128
- if (generalJsContent) console.log('[turbo] captured general.js len:', generalJsContent.length);
129
- }
130
- } catch(e) {}
131
- });
132
-
133
- await page.goto(normalizedUrl, { waitUntil: 'domcontentloaded', timeout: 20000 });
134
- await new Promise(r => setTimeout(r, 2000));
135
-
136
- if (obrutEmbedUrl) return `<!-- obrut-embed: ${obrutEmbedUrl} -->`;
137
-
138
- // Try executing general.js to trigger iframe creation
139
- if (generalJsContent) {
140
- try {
141
- await page.evaluate(generalJsContent);
142
- console.log('[turbo] executed general.js');
143
- await new Promise(r => setTimeout(r, 3000));
144
- if (obrutEmbedUrl) return `<!-- obrut-embed: ${obrutEmbedUrl} -->`;
145
- } catch(e) { console.log('[turbo] general.js exec error:', e.message); }
146
- }
147
-
148
- // Fix Rocket Loader β€” re-execute blocked scripts
149
- await page.evaluate(() => {
150
- document.querySelectorAll('script[type]').forEach(s => {
151
- if (s.type && s.type !== 'text/javascript' && s.type !== 'module' && s.src) {
152
- const ns = document.createElement('script');
153
- ns.src = s.src;
154
- document.head.appendChild(ns);
155
- }
156
- });
157
  });
158
 
159
- // Wait up to 10s for obrut request
160
- for (let i = 0; i < 10; i++) {
 
 
 
 
 
 
161
  await new Promise(r => setTimeout(r, 1000));
162
- if (obrutEmbedUrl) return `<!-- obrut-embed: ${obrutEmbedUrl} -->`;
163
- // Also check DOM
164
  const html = await page.content();
165
- if (html.includes('obrut.show/embed/')) return html;
 
 
 
166
  }
167
 
 
 
168
  const finalHtml = await page.content();
169
- console.log('[turbo] Puppeteer final HTML len:', finalHtml.length, 'has embed:', finalHtml.includes('obrut.show/embed/'));
170
  return finalHtml;
171
  } finally {
172
  await page.close();
173
  }
174
  }
175
 
176
- // ── Get embed URL from kinojump via Puppeteer ─────────────────────────────────
177
- async function getEmbedUrlViaPuppeteer(browser, pageUrl) {
178
- // First try plain HTTP β€” embed URL is in raw HTML without https:// prefix
179
- try {
180
- // kinojump may redirect to web.kinojump.com β€” force stay on kinojump.com
181
- const normalizedUrl = pageUrl.replace('web.kinojump.com', 'kinojump.com');
182
- const html = await fetchRaw(normalizedUrl, {
183
- 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120 Safari/537.36',
184
- 'referer': KINOJUMP_BASE + '/',
185
- 'accept': 'text/html',
186
- 'accept-language': 'ru-RU,ru;q=0.9',
187
- 'accept-encoding': 'identity'
188
- });
189
- // Pattern: XXXXXXXX.obrut.show/embed/... OR obrut.show/embed/... (with or without subdomain)
190
- const m = html.match(/([a-z0-9]*\.?obrut\.show\/embed\/[A-Za-z0-9]+\/content\/[A-Za-z0-9]+)/);
191
- if (m) {
192
- // Ensure it has a subdomain β€” if not, use default
193
- let embedPath = m[1];
194
- if (!embedPath.includes('.obrut.show')) {
195
- embedPath = '49372504.' + embedPath; // fallback subdomain
196
- }
197
- const embedUrl = 'https://' + embedPath;
198
- console.log('[turbo] found embed URL in raw HTML:', embedUrl);
199
- return embedUrl;
200
- }
201
- console.log('[turbo] embed not found in raw HTML, falling back to Puppeteer');
202
- console.log('[turbo] HTML length:', html.length, '| contains obrut:', html.includes('obrut'));
203
- // Log what URL we actually got (after redirects)
204
- const titleMatch = html.match(/<title>([^<]{0,80})<\/title>/);
205
- if (titleMatch) console.log('[turbo] page title from raw HTML:', titleMatch[1]);
206
- } catch(e) {
207
- console.log('[turbo] raw HTTP failed:', e.message);
208
- }
209
- const page = await browser.newPage();
210
- try {
211
- await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120 Safari/537.36');
212
- await page.setExtraHTTPHeaders({ 'Accept-Language': 'ru-RU,ru;q=0.9' });
213
- let foundUrl = null;
214
- await page.setRequestInterception(true);
215
- page.on('request', req => {
216
- const u = req.url();
217
- if (u.includes('obrut.show') && u.includes('/embed/') && !foundUrl) {
218
- foundUrl = u;
219
- console.log('[turbo] caught embed URL:', u.substring(0, 120));
220
- }
221
- req.continue();
222
- });
223
- await page.goto(pageUrl, { waitUntil: 'networkidle2', timeout: 30000 });
224
- for (let i = 0; i < 20 && !foundUrl; i++) {
225
- await new Promise(r => setTimeout(r, 1000));
226
- const found = await page.evaluate((host) => {
227
- for (const el of document.querySelectorAll('iframe, [src], [data-src]')) {
228
- const src = el.src || el.getAttribute('src') || el.getAttribute('data-src') || '';
229
- if (src.includes(host) && src.includes('/embed/')) return src;
230
- }
231
- return null;
232
- }, 'obrut.show');
233
- if (found) { foundUrl = found; break; }
234
- if (i % 5 === 4) console.log('[turbo] waiting for obrut embed... (' + (i+1) + 's)');
235
- }
236
- if (!foundUrl) {
237
- const html = await page.content();
238
- const m = html.match(/https?:\/\/[^"'\s<>]*obrut\.show\/embed\/[^"'\s<>]*/g);
239
- if (m && m.length) foundUrl = m[0];
240
- console.log('[turbo] page title:', await page.title());
241
- console.log('[turbo] obrut embed in HTML:', m ? m.slice(0,3) : 'none');
242
- }
243
- return foundUrl;
244
- } finally {
245
- await page.close();
246
- }
247
- }
248
-
249
- // ── Parse obrut embed page ────────────────────────────────────────────────────
250
- // The Player() data has a random prefix + base64 JSON with garbage bytes injected.
251
- // We extract entries via regex on the decoded text, retrying until we get enough.
252
-
253
  function getCleanText(raw) {
254
  const eyJIdx = raw.indexOf('eyJ');
255
  const b64 = eyJIdx > 0 ? raw.substring(eyJIdx) : raw;
@@ -272,7 +156,6 @@ function parseFileStr(fileStr) {
272
  }
273
 
274
  function extractEntries(text) {
275
- // Serial: t1 is non-empty like "S01E01 - Title"
276
  const re = /"title":"([^"]+)","t1":"([^"]+)","poster":"[^"]*","file":"((?:\[\w+\]https?:\\\/\\\/[^"]+))"/g;
277
  const entries = [];
278
  let m;
@@ -286,85 +169,121 @@ function extractEntries(text) {
286
  }
287
 
288
  function extractMovieVoices(text) {
289
- // Movie: t1 is empty string ""
290
  const re = /"title":"([^"]+)","t1":"","poster":"[^"]*","file":"((?:\[\w+\]https?:\\\/\\\/[^"]+))"/g;
291
  const voices = [];
292
  let m;
293
  while ((m = re.exec(text)) !== null) {
294
- const fileStr = m[3].replace(/\\\//g, '/');
295
  const streams = parseFileStr(fileStr);
296
- if (streams.length > 0) voices.push({ label: m[1], url: streams[0].url, qualities: streams, subtitles: [] });
297
  }
298
  return voices;
299
  }
300
 
301
- async function parseObrutEmbed(embedUrl) {
302
- // Run parallel batches of requests and merge results
303
- // obrut injects random garbage bytes that truncate data β€” more parallel requests = better coverage
304
- const BATCH_SIZE = 10;
305
- const MAX_BATCHES = 8;
306
-
307
- // Merged data structures
308
- const mergedEntries = new Map(); // key: voice+episode -> entry
309
- const mergedVoices = new Map(); // key: label -> voice
310
-
311
- // First try plain HTTP
312
- const testHtml = await fetchRaw(embedUrl, {
313
- 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120 Safari/537.36',
314
- 'referer': 'https://web.kinojump.com/',
315
- 'accept': 'text/html,application/xhtml+xml',
316
- 'accept-encoding': 'identity'
317
- }).catch(() => '');
318
-
319
- const hasPlayer = testHtml.includes('new Player(');
320
- console.log('[turbo] obrut embed test: len=', testHtml.length, 'hasPlayer=', hasPlayer, 'start:', testHtml.substring(0, 100));
321
-
322
- if (!hasPlayer) {
323
- // obrut blocked plain HTTP β€” use Puppeteer to get embed HTML
324
- console.log('[turbo] obrut blocked plain HTTP, using Puppeteer for embed');
325
- const browser = await getBrowser();
326
- const page = await browser.newPage();
327
- try {
328
- await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120 Safari/537.36');
329
- await page.setExtraHTTPHeaders({ 'referer': 'https://web.kinojump.com/' });
330
- await page.goto(embedUrl, { waitUntil: 'networkidle2', timeout: 25000 });
331
- await new Promise(r => setTimeout(r, 2000));
332
- const html = await page.content();
333
- console.log('[turbo] Puppeteer embed len:', html.length, 'hasPlayer:', html.includes('new Player('));
334
-
335
- if (html.includes('new Player(')) {
336
- const pm = html.match(/new\s+Player\s*\(\s*"([A-Za-z0-9+/=]{20,})"/);
337
- if (pm) {
338
- const text = getCleanText(pm[1]);
339
- const entries = extractEntries(text);
340
- const movieVoices = extractMovieVoices(text);
341
- console.log('[turbo] Puppeteer embed: entries=', entries.length, 'voices=', movieVoices.length);
342
- if (entries.length > 0 || movieVoices.length > 0) {
343
- return { entries, movieVoices };
344
- }
345
- // If Puppeteer got data but not enough, continue with parallel HTTP
346
- console.log('[turbo] Puppeteer got Player but no entries, trying parallel HTTP');
347
- }
348
  }
349
- } finally {
350
- await page.close();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
352
  }
353
 
 
 
 
 
 
 
354
  for (let batch = 0; batch < MAX_BATCHES; batch++) {
355
- // Fetch BATCH_SIZE pages in parallel
356
  const promises = Array.from({ length: BATCH_SIZE }, () =>
357
  fetchRaw(embedUrl, {
358
  'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120 Safari/537.36',
359
- 'referer': 'https://web.kinojump.com/',
360
- 'accept': 'text/html,application/xhtml+xml',
361
- 'accept-encoding': 'identity'
362
  }).then(html => {
363
  const pm = html.match(/new\s+Player\s*\(\s*"([A-Za-z0-9+/=]{20,})"/);
364
- if (!pm) {
365
- if (batch === 0) console.log('[turbo] no Player() in embed, len:', html.length, 'start:', html.substring(0, 150));
366
- return null;
367
- }
368
  const text = getCleanText(pm[1]);
369
  return { entries: extractEntries(text), movieVoices: extractMovieVoices(text) };
370
  }).catch(() => null)
@@ -384,8 +303,7 @@ async function parseObrutEmbed(embedUrl) {
384
  }
385
 
386
  const e = mergedEntries.size, v = mergedVoices.size;
387
- console.log(`[turbo] batch ${batch + 1}: merged entries=${e} voices=${v}`);
388
- // For movies stop early, for serials always run all batches
389
  if (v >= 5) break;
390
  }
391
 
@@ -396,16 +314,10 @@ async function parseObrutEmbed(embedUrl) {
396
  return { entries, movieVoices };
397
  }
398
 
399
- // ── Build serial structure from entries ────────────────────────��─────────────
400
  function buildSerialFromEntries(entries) {
401
- // entries: [{voice, episode (e.g. "S01E01 - Title"), streams}]
402
- // Group by season/episode
403
  const seasonMap = {};
404
- const voiceSet = new Set();
405
-
406
  for (const e of entries) {
407
- voiceSet.add(e.voice);
408
- // Parse season/episode from t1: "S01E01 - Title" or "S01E01"
409
  const seMatch = e.episode.match(/S(\d+)E(\d+)/i);
410
  if (!seMatch) continue;
411
  const sNum = parseInt(seMatch[1], 10);
@@ -424,41 +336,28 @@ function buildSerialFromEntries(entries) {
424
  episodes[sk] = Object.keys(seasonMap[sk]).sort().map(ek => ({ id: ek, title: 'Episode ' + parseInt(ek.slice(1), 10) }));
425
  voices[sk] = {};
426
  for (const ek of Object.keys(seasonMap[sk])) {
427
- voices[sk][ek] = seasonMap[sk][ek].map(v => ({
428
- label: v.label,
429
- url: v.url,
430
- qualities: v.streams,
431
- subtitles: []
432
- }));
433
  }
434
  }
435
 
436
  return { seasons, episodes, voices };
437
  }
438
 
439
- // ── Main: get stream for a kinojump page URL ──────────────────────────────────
440
  async function getTurboStream(pageUrl) {
441
  const browser = await getBrowser();
442
 
443
- // Step 1: try plain HTTP first (fast, no Puppeteer)
444
- console.log('[turbo] Step 1 - getting embed URL from:', pageUrl);
445
- let embedUrl = null;
446
- try {
447
- const html = await getKinojumpHtml(pageUrl, browser);
448
- const m = html.match(/(?:([a-z0-9]+)\.)?obrut\.show\/embed\/([A-Za-z0-9]+)\/content\/([A-Za-z0-9]+)/);
449
- if (m) {
450
- // m[1] = subdomain (may be undefined), m[2] = content type, m[3] = content id
451
- const subdomain = m[1] ? m[1] + '.obrut.show' : '49372504.obrut.show';
452
- embedUrl = 'https://' + subdomain + '/embed/' + m[2] + '/content/' + m[3];
453
- console.log('[turbo] embed URL:', embedUrl);
454
- } else { console.log('[turbo] embed not found in HTML'); }
455
- } catch(e) { console.log('[turbo] getKinojumpHtml failed:', e.message); }
456
-
457
- if (!embedUrl) throw new Error('obrut embed URL not found on kinojump page');
458
-
459
- // Step 3: parse embed
460
- console.log('[turbo] Step 2 - parsing obrut embed:', embedUrl);
461
- const result = await parseObrutEmbed(embedUrl);
462
  if (!result) throw new Error('failed to extract data from obrut embed');
463
 
464
  const hasSerial = result.entries.length > 0;
@@ -472,8 +371,8 @@ async function getTurboStream(pageUrl) {
472
  console.log('[turbo] movie:', result.movieVoices.length, 'voices');
473
  return { embed_url: embedUrl, content_type: 'movie', voices: result.movieVoices };
474
  } else {
475
- throw new Error('no entries or voices found in obrut embed');
476
  }
477
  }
478
 
479
- module.exports = { turboSearch, getTurboStream, parseObrutEmbed, extractEntries, extractMovieVoices, buildSerialFromEntries, getKinojumpHtml };
 
1
  /**
2
+ * Turbo (kinojump.com β†’ obrut.show) stream extractor using Puppeteer
3
+ * kinojump.com and obrut.show use Cloudflare β€” data only available after JS execution
4
  */
5
  const { getBrowser } = require('./alloha');
6
  const https = require('https');
7
  const http = require('http');
8
 
 
 
 
9
  function fetchRaw(reqUrl, headers) {
10
  return new Promise((resolve, reject) => {
11
  let u; try { u = new URL(reqUrl); } catch(e) { return reject(e); }
 
17
  if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
18
  let loc = res.headers.location.startsWith('http')
19
  ? res.headers.location
20
+ : u.origin + res.headers.location;
 
21
  loc = loc.replace('web.kinojump.com', 'kinojump.com');
22
+ res.resume();
23
  return fetchRaw(loc, headers).then(resolve).catch(reject);
24
  }
25
+ let d = ''; res.setEncoding('utf8');
26
+ res.on('data', c => d += c);
27
+ res.on('end', () => resolve(d));
 
 
 
 
 
 
 
 
 
28
  });
29
  req.on('error', reject);
30
+ req.setTimeout(12000, () => { req.destroy(); reject(new Error('timeout')); });
31
  req.end();
32
  });
33
  }
34
 
35
  // ── Search kinojump ───────────────────────────────────────────────────────────
36
  async function turboSearch(query) {
 
37
  const searchUrl = 'https://web.kinojump.com/index.php?do=search&subaction=search&story=' + encodeURIComponent(query);
38
  const html = await fetchRaw(searchUrl, {
39
  'user-agent': 'Mozilla/5.0 Chrome/120',
 
44
  let m;
45
  while ((m = re.exec(html)) !== null) {
46
  if (!results.find(r => r.id === m[2])) {
 
47
  const url = m[1].replace('web.kinojump.com', 'kinojump.com');
48
  results.push({ url, id: m[2], slug: m[3] });
49
  }
 
51
  return results.slice(0, 10);
52
  }
53
 
54
+ // ── Get kinojump page HTML via Puppeteer (bypasses Cloudflare) ───────────────
55
  async function getKinojumpHtml(pageUrl, browser) {
56
  const normalizedUrl = pageUrl.replace('web.kinojump.com', 'kinojump.com');
57
+
58
+ // Try plain HTTP first with retry
59
+ for (let attempt = 0; attempt < 3; attempt++) {
60
  try {
61
  const html = await fetchRaw(normalizedUrl, {
62
  'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120 Safari/537.36',
63
+ 'referer': 'https://kinojump.com/',
64
  'accept': 'text/html',
65
+ 'cookie': 'PHPSESSID=a1b2c3d4e5f6'
 
 
66
  });
67
+ if (html.includes('obrut.show/embed/')) {
68
+ console.log('[turbo] got embed URL via plain HTTP');
69
+ return html;
 
 
 
 
 
 
 
 
 
70
  }
71
+ if (attempt < 2) {
72
+ console.log('[turbo] no embed URL, retrying... (' + (attempt+1) + '/3)');
73
+ await new Promise(r => setTimeout(r, 1000));
74
+ }
75
+ } catch(e) {
76
+ console.log('[turbo] plain HTTP failed:', e.message);
77
+ }
78
  }
79
 
80
+ // Fallback: Puppeteer
81
  console.log('[turbo] using Puppeteer for kinojump page');
82
  const page = await browser.newPage();
83
  try {
84
  await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120 Safari/537.36');
85
  await page.setExtraHTTPHeaders({ 'Accept-Language': 'ru-RU,ru;q=0.9' });
86
+
87
+ // Disable automation detection
88
+ await page.evaluateOnNewDocument(() => {
89
+ Object.defineProperty(navigator, 'webdriver', { get: () => false });
90
+ window.chrome = { runtime: {} };
91
+ });
92
 
93
  let obrutEmbedUrl = null;
 
 
94
  await page.setRequestInterception(true);
95
  page.on('request', req => {
96
  const u = req.url();
97
  if (u.includes('obrut.show/embed/') && !obrutEmbedUrl) {
98
  obrutEmbedUrl = u;
99
+ console.log('[turbo] intercepted obrut embed:', u.substring(0, 100));
100
+ }
101
+ // Block images/fonts to speed up
102
+ const resourceType = req.resourceType();
103
+ if (['image', 'stylesheet', 'font', 'media'].includes(resourceType)) {
104
+ req.abort();
105
+ } else {
106
+ req.continue();
107
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  });
109
 
110
+ await page.goto(normalizedUrl, { waitUntil: 'domcontentloaded', timeout: 25000 });
111
+
112
+ // Wait for Cloudflare challenge
113
+ console.log('[turbo] waiting for Cloudflare challenge...');
114
+ await new Promise(r => setTimeout(r, 5000));
115
+
116
+ // Wait for obrut embed
117
+ for (let i = 0; i < 10 && !obrutEmbedUrl; i++) {
118
  await new Promise(r => setTimeout(r, 1000));
 
 
119
  const html = await page.content();
120
+ if (html.includes('obrut.show/embed/')) {
121
+ console.log('[turbo] found obrut embed in DOM');
122
+ return html;
123
+ }
124
  }
125
 
126
+ if (obrutEmbedUrl) return `<!-- obrut-embed: ${obrutEmbedUrl} -->`;
127
+
128
  const finalHtml = await page.content();
129
+ console.log('[turbo] Puppeteer final HTML len:', finalHtml.length);
130
  return finalHtml;
131
  } finally {
132
  await page.close();
133
  }
134
  }
135
 
136
+ // ── Parse obrut embed ─────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  function getCleanText(raw) {
138
  const eyJIdx = raw.indexOf('eyJ');
139
  const b64 = eyJIdx > 0 ? raw.substring(eyJIdx) : raw;
 
156
  }
157
 
158
  function extractEntries(text) {
 
159
  const re = /"title":"([^"]+)","t1":"([^"]+)","poster":"[^"]*","file":"((?:\[\w+\]https?:\\\/\\\/[^"]+))"/g;
160
  const entries = [];
161
  let m;
 
169
  }
170
 
171
  function extractMovieVoices(text) {
 
172
  const re = /"title":"([^"]+)","t1":"","poster":"[^"]*","file":"((?:\[\w+\]https?:\\\/\\\/[^"]+))"/g;
173
  const voices = [];
174
  let m;
175
  while ((m = re.exec(text)) !== null) {
176
+ const fileStr = m[2].replace(/\\\//g, '/');
177
  const streams = parseFileStr(fileStr);
178
+ if (streams.length > 0) voices.push({ label: m[1], url: streams[0].url, qualities: streams });
179
  }
180
  return voices;
181
  }
182
 
183
+ async function parseObrutEmbed(embedUrl, browser) {
184
+ // obrut.show uses Cloudflare - need Puppeteer with anti-detection
185
+ console.log('[turbo] parsing obrut embed with Puppeteer (Cloudflare bypass)');
186
+ const page = await browser.newPage();
187
+ try {
188
+ // More realistic browser fingerprint
189
+ 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');
190
+ await page.setExtraHTTPHeaders({
191
+ 'Accept-Language': 'ru-RU,ru;q=0.9,en;q=0.8',
192
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
193
+ 'Referer': 'https://kinojump.com/',
194
+ 'Sec-Fetch-Dest': 'iframe',
195
+ 'Sec-Fetch-Mode': 'navigate',
196
+ 'Sec-Fetch-Site': 'cross-site',
197
+ });
198
+
199
+ await page.setViewport({ width: 1920, height: 1080 });
200
+
201
+ // Disable automation detection
202
+ await page.evaluateOnNewDocument(() => {
203
+ Object.defineProperty(navigator, 'webdriver', { get: () => false });
204
+ window.chrome = { runtime: {} };
205
+ Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5] });
206
+ Object.defineProperty(navigator, 'languages', { get: () => ['ru-RU', 'ru', 'en-US', 'en'] });
207
+ });
208
+
209
+ await page.setRequestInterception(true);
210
+ page.on('request', req => {
211
+ const resourceType = req.resourceType();
212
+ if (['image', 'stylesheet', 'font', 'media'].includes(resourceType)) {
213
+ req.abort();
214
+ } else {
215
+ req.continue();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  }
217
+ });
218
+
219
+ console.log('[turbo] navigating to obrut embed:', embedUrl.substring(0, 80));
220
+
221
+ await page.goto(embedUrl, {
222
+ waitUntil: 'domcontentloaded',
223
+ timeout: 30000,
224
+ });
225
+
226
+ // Wait for Cloudflare challenge to complete
227
+ console.log('[turbo] waiting for Cloudflare challenge...');
228
+ await new Promise(r => setTimeout(r, 5000));
229
+
230
+ const currentUrl = page.url();
231
+ const title = await page.title().catch(() => '');
232
+ console.log('[turbo] current URL:', currentUrl.substring(0, 80));
233
+ console.log('[turbo] page title:', title);
234
+
235
+ if (title.includes('Just a moment') || title.includes('Checking your browser')) {
236
+ console.log('[turbo] still on Cloudflare challenge, waiting more...');
237
+ await new Promise(r => setTimeout(r, 5000));
238
+ }
239
+
240
+ // Wait for player to initialize
241
+ await new Promise(r => setTimeout(r, 2000));
242
+
243
+ const html = await page.content();
244
+ console.log('[turbo] Puppeteer got HTML, len:', html.length, 'hasPlayer:', html.includes('new Player('));
245
+
246
+ if (!html.includes('new Player(')) {
247
+ console.log('[turbo] no Player() found after Cloudflare bypass');
248
+ return null;
249
+ }
250
+
251
+ const pm = html.match(/new\s+Player\s*\(\s*"([A-Za-z0-9+/=]{20,})"/);
252
+ if (!pm) {
253
+ console.log('[turbo] Player() found but no base64 data');
254
+ return null;
255
  }
256
+
257
+ const text = getCleanText(pm[1]);
258
+ const entries = extractEntries(text);
259
+ const movieVoices = extractMovieVoices(text);
260
+ console.log('[turbo] Puppeteer: entries=', entries.length, 'voices=', movieVoices.length);
261
+
262
+ if (entries.length === 0 && movieVoices.length === 0) {
263
+ console.log('[turbo] no data extracted, retrying with parallel requests...');
264
+ // Fallback to parallel HTTP if Puppeteer got Player but no data
265
+ } else {
266
+ return { entries, movieVoices };
267
+ }
268
+ } finally {
269
+ await page.close();
270
  }
271
 
272
+ // Fallback: parallel HTTP requests (may work after Puppeteer warmed up the session)
273
+ const BATCH_SIZE = 10;
274
+ const MAX_BATCHES = 5;
275
+ const mergedEntries = new Map();
276
+ const mergedVoices = new Map();
277
+
278
  for (let batch = 0; batch < MAX_BATCHES; batch++) {
 
279
  const promises = Array.from({ length: BATCH_SIZE }, () =>
280
  fetchRaw(embedUrl, {
281
  'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120 Safari/537.36',
282
+ 'referer': 'https://kinojump.com/',
283
+ 'accept': 'text/html'
 
284
  }).then(html => {
285
  const pm = html.match(/new\s+Player\s*\(\s*"([A-Za-z0-9+/=]{20,})"/);
286
+ if (!pm) return null;
 
 
 
287
  const text = getCleanText(pm[1]);
288
  return { entries: extractEntries(text), movieVoices: extractMovieVoices(text) };
289
  }).catch(() => null)
 
303
  }
304
 
305
  const e = mergedEntries.size, v = mergedVoices.size;
306
+ console.log(`[turbo] batch ${batch + 1}: entries=${e} voices=${v}`);
 
307
  if (v >= 5) break;
308
  }
309
 
 
314
  return { entries, movieVoices };
315
  }
316
 
317
+ // ── Build serial structure ────────────────────────────────────────────────────
318
  function buildSerialFromEntries(entries) {
 
 
319
  const seasonMap = {};
 
 
320
  for (const e of entries) {
 
 
321
  const seMatch = e.episode.match(/S(\d+)E(\d+)/i);
322
  if (!seMatch) continue;
323
  const sNum = parseInt(seMatch[1], 10);
 
336
  episodes[sk] = Object.keys(seasonMap[sk]).sort().map(ek => ({ id: ek, title: 'Episode ' + parseInt(ek.slice(1), 10) }));
337
  voices[sk] = {};
338
  for (const ek of Object.keys(seasonMap[sk])) {
339
+ voices[sk][ek] = seasonMap[sk][ek].map(v => ({ label: v.label, url: v.url, qualities: v.streams, subtitles: [] }));
 
 
 
 
 
340
  }
341
  }
342
 
343
  return { seasons, episodes, voices };
344
  }
345
 
346
+ // ── Main: get stream ──────────────────────────────────────────────────────────
347
  async function getTurboStream(pageUrl) {
348
  const browser = await getBrowser();
349
 
350
+ console.log('[turbo] getting embed URL from:', pageUrl);
351
+ const html = await getKinojumpHtml(pageUrl, browser);
352
+ const m = html.match(/(?:([a-z0-9]+)\.)?obrut\.show\/embed\/([A-Za-z0-9]+)\/content\/([A-Za-z0-9]+)/);
353
+ if (!m) throw new Error('obrut embed URL not found');
354
+
355
+ const subdomain = m[1] ? m[1] + '.obrut.show' : '49372504.obrut.show';
356
+ const embedUrl = 'https://' + subdomain + '/embed/' + m[2] + '/content/' + m[3];
357
+ console.log('[turbo] embed URL:', embedUrl);
358
+
359
+ console.log('[turbo] parsing obrut embed');
360
+ const result = await parseObrutEmbed(embedUrl, browser);
 
 
 
 
 
 
 
 
361
  if (!result) throw new Error('failed to extract data from obrut embed');
362
 
363
  const hasSerial = result.entries.length > 0;
 
371
  console.log('[turbo] movie:', result.movieVoices.length, 'voices');
372
  return { embed_url: embedUrl, content_type: 'movie', voices: result.movieVoices };
373
  } else {
374
+ throw new Error('no data found in obrut embed');
375
  }
376
  }
377
 
378
+ module.exports = { turboSearch, getTurboStream };