recycleactor commited on
Commit
033f465
·
verified ·
1 Parent(s): ba56981

Delete turbo.js

Browse files
Files changed (1) hide show
  1. turbo.js +0 -382
turbo.js DELETED
@@ -1,382 +0,0 @@
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); }
12
- const mod = u.protocol === 'https:' ? https : http;
13
- const req = mod.request({
14
- hostname: u.hostname, path: u.pathname + u.search,
15
- headers: headers || {}
16
- }, res => {
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',
40
- 'referer': 'https://web.kinojump.com/'
41
- });
42
- const results = [];
43
- const re = /href="(https?:\/\/(?:web\.)?kinojump\.com\/(\d+)-([^"]+)\.html)"/g;
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
- }
50
- }
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;
140
- const decoded = Buffer.from(b64, 'base64').toString('utf8');
141
- for (let i = 0; i < decoded.length; i++) {
142
- const code = decoded.charCodeAt(i);
143
- if (code > 127 || (code < 32 && code !== 9 && code !== 10 && code !== 13)) {
144
- return decoded.substring(0, i);
145
- }
146
- }
147
- return decoded;
148
- }
149
-
150
- function parseFileStr(fileStr) {
151
- const streams = [];
152
- const re = /\[(\w+)\](https?:\/\/[^,\[]+)/g;
153
- let m;
154
- while ((m = re.exec(fileStr)) !== null) streams.push({ quality: m[1], url: m[2].trim() });
155
- return streams;
156
- }
157
-
158
- function extractEntries(text) {
159
- const re = /"title":"([^"]+)","t1":"([^"]+)","poster":"[^"]*","file":"((?:\[\w+\]https?:\\\/\\\/[^"]+))"/g;
160
- const entries = [];
161
- let m;
162
- while ((m = re.exec(text)) !== null) {
163
- if (!m[2]) continue;
164
- const fileStr = m[3].replace(/\\\//g, '/');
165
- const streams = parseFileStr(fileStr);
166
- if (streams.length > 0) entries.push({ voice: m[1], episode: m[2], streams });
167
- }
168
- return entries;
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 - use Cloudflare Worker as proxy to bypass IP blocking
185
- const WORKER_URL = 'https://proxy.recycleactor.workers.dev/turbo/proxy';
186
-
187
- console.log('[turbo] parsing obrut embed via Cloudflare Worker (bypass Cloudflare IP block)');
188
-
189
- // Try worker proxy first (parallel requests for better data coverage)
190
- const BATCH_SIZE = 10;
191
- const MAX_BATCHES = 5;
192
- const mergedEntries = new Map();
193
- const mergedVoices = new Map();
194
-
195
- for (let batch = 0; batch < MAX_BATCHES; batch++) {
196
- const promises = Array.from({ length: BATCH_SIZE }, () =>
197
- fetchRaw(WORKER_URL + '?url=' + encodeURIComponent(embedUrl), {
198
- 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120 Safari/537.36',
199
- 'accept': 'text/html'
200
- }).then(html => {
201
- console.log('[turbo] worker response len:', html.length, 'hasPlayer:', html.includes('new Player('));
202
- const pm = html.match(/new\s+Player\s*\(\s*"([A-Za-z0-9+/=]{20,})"/);
203
- if (!pm) return null;
204
- const text = getCleanText(pm[1]);
205
- return { entries: extractEntries(text), movieVoices: extractMovieVoices(text) };
206
- }).catch(e => {
207
- console.log('[turbo] worker request failed:', e.message);
208
- return null;
209
- })
210
- );
211
-
212
- const results = await Promise.all(promises);
213
-
214
- for (const r of results) {
215
- if (!r) continue;
216
- for (const e of r.entries) {
217
- const key = e.voice + '|' + e.episode;
218
- if (!mergedEntries.has(key)) mergedEntries.set(key, e);
219
- }
220
- for (const v of r.movieVoices) {
221
- if (!mergedVoices.has(v.label)) mergedVoices.set(v.label, v);
222
- }
223
- }
224
-
225
- const e = mergedEntries.size, v = mergedVoices.size;
226
- console.log(`[turbo] batch ${batch + 1}: entries=${e} voices=${v}`);
227
- if (v >= 5 || e >= 10) break;
228
- }
229
-
230
- const entries = Array.from(mergedEntries.values());
231
- const movieVoices = Array.from(mergedVoices.values());
232
-
233
- if (entries.length > 0 || movieVoices.length > 0) {
234
- console.log('[turbo] worker proxy success: entries=', entries.length, 'voices=', movieVoices.length);
235
- return { entries, movieVoices };
236
- }
237
-
238
- // Fallback: try Puppeteer if worker failed
239
- console.log('[turbo] worker proxy failed, trying Puppeteer fallback');
240
- const page = await browser.newPage();
241
- try {
242
- 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');
243
- await page.setExtraHTTPHeaders({
244
- 'Accept-Language': 'ru-RU,ru;q=0.9,en;q=0.8',
245
- 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
246
- 'Referer': 'https://kinojump.com/',
247
- 'Sec-Fetch-Dest': 'iframe',
248
- 'Sec-Fetch-Mode': 'navigate',
249
- 'Sec-Fetch-Site': 'cross-site',
250
- });
251
-
252
- await page.setViewport({ width: 1920, height: 1080 });
253
-
254
- // Disable automation detection
255
- await page.evaluateOnNewDocument(() => {
256
- Object.defineProperty(navigator, 'webdriver', { get: () => false });
257
- window.chrome = { runtime: {} };
258
- Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5] });
259
- Object.defineProperty(navigator, 'languages', { get: () => ['ru-RU', 'ru', 'en-US', 'en'] });
260
- });
261
-
262
- await page.setRequestInterception(true);
263
- page.on('request', req => {
264
- const resourceType = req.resourceType();
265
- if (['image', 'stylesheet', 'font', 'media'].includes(resourceType)) {
266
- req.abort();
267
- } else {
268
- req.continue();
269
- }
270
- });
271
-
272
- console.log('[turbo] Puppeteer navigating to obrut embed:', embedUrl.substring(0, 80));
273
-
274
- await page.goto(embedUrl, {
275
- waitUntil: 'domcontentloaded',
276
- timeout: 30000,
277
- });
278
-
279
- // Wait for Cloudflare challenge to complete
280
- console.log('[turbo] waiting for Cloudflare challenge...');
281
- await new Promise(r => setTimeout(r, 5000));
282
-
283
- const currentUrl = page.url();
284
- const title = await page.title().catch(() => '');
285
- console.log('[turbo] current URL:', currentUrl.substring(0, 80));
286
- console.log('[turbo] page title:', title);
287
-
288
- if (title.includes('Just a moment') || title.includes('Checking your browser')) {
289
- console.log('[turbo] still on Cloudflare challenge, waiting more...');
290
- await new Promise(r => setTimeout(r, 5000));
291
- }
292
-
293
- // Wait for player to initialize
294
- await new Promise(r => setTimeout(r, 2000));
295
-
296
- const html = await page.content();
297
- console.log('[turbo] Puppeteer got HTML, len:', html.length, 'hasPlayer:', html.includes('new Player('));
298
-
299
- if (!html.includes('new Player(')) {
300
- console.log('[turbo] Puppeteer: no Player() found');
301
- return null;
302
- }
303
-
304
- const pm = html.match(/new\s+Player\s*\(\s*"([A-Za-z0-9+/=]{20,})"/);
305
- if (!pm) {
306
- console.log('[turbo] Puppeteer: Player() found but no base64 data');
307
- return null;
308
- }
309
-
310
- const text = getCleanText(pm[1]);
311
- const puppeteerEntries = extractEntries(text);
312
- const puppeteerVoices = extractMovieVoices(text);
313
- console.log('[turbo] Puppeteer: entries=', puppeteerEntries.length, 'voices=', puppeteerVoices.length);
314
-
315
- return { entries: puppeteerEntries, movieVoices: puppeteerVoices };
316
- } finally {
317
- await page.close();
318
- }
319
- }
320
-
321
- // ── Build serial structure ────────────────────────────────────────────────────
322
- function buildSerialFromEntries(entries) {
323
- const seasonMap = {};
324
- for (const e of entries) {
325
- const seMatch = e.episode.match(/S(\d+)E(\d+)/i);
326
- if (!seMatch) continue;
327
- const sNum = parseInt(seMatch[1], 10);
328
- const eNum = parseInt(seMatch[2], 10);
329
- const sKey = 's' + String(sNum).padStart(2, '0');
330
- const eKey = 'e' + String(eNum).padStart(2, '0');
331
- if (!seasonMap[sKey]) seasonMap[sKey] = {};
332
- if (!seasonMap[sKey][eKey]) seasonMap[sKey][eKey] = [];
333
- seasonMap[sKey][eKey].push({ label: e.voice, streams: e.streams, url: e.streams[0] ? e.streams[0].url : '' });
334
- }
335
-
336
- const seasons = Object.keys(seasonMap).sort().map(sk => ({ id: sk, title: 'Season ' + parseInt(sk.slice(1), 10) }));
337
- const episodes = {};
338
- const voices = {};
339
- for (const sk of Object.keys(seasonMap)) {
340
- episodes[sk] = Object.keys(seasonMap[sk]).sort().map(ek => ({ id: ek, title: 'Episode ' + parseInt(ek.slice(1), 10) }));
341
- voices[sk] = {};
342
- for (const ek of Object.keys(seasonMap[sk])) {
343
- voices[sk][ek] = seasonMap[sk][ek].map(v => ({ label: v.label, url: v.url, qualities: v.streams, subtitles: [] }));
344
- }
345
- }
346
-
347
- return { seasons, episodes, voices };
348
- }
349
-
350
- // ── Main: get stream ──────────────────────────────────────────────────────────
351
- async function getTurboStream(pageUrl) {
352
- const browser = await getBrowser();
353
-
354
- console.log('[turbo] getting embed URL from:', pageUrl);
355
- const html = await getKinojumpHtml(pageUrl, browser);
356
- const m = html.match(/(?:([a-z0-9]+)\.)?obrut\.show\/embed\/([A-Za-z0-9]+)\/content\/([A-Za-z0-9]+)/);
357
- if (!m) throw new Error('obrut embed URL not found');
358
-
359
- const subdomain = m[1] ? m[1] + '.obrut.show' : '49372504.obrut.show';
360
- const embedUrl = 'https://' + subdomain + '/embed/' + m[2] + '/content/' + m[3];
361
- console.log('[turbo] embed URL:', embedUrl);
362
-
363
- console.log('[turbo] parsing obrut embed');
364
- const result = await parseObrutEmbed(embedUrl, browser);
365
- if (!result) throw new Error('failed to extract data from obrut embed');
366
-
367
- const hasSerial = result.entries.length > 0;
368
- const hasMovie = result.movieVoices.length > 0;
369
-
370
- if (hasSerial) {
371
- const serial = buildSerialFromEntries(result.entries);
372
- console.log('[turbo] serial:', serial.seasons.length, 'seasons');
373
- return { embed_url: embedUrl, content_type: 'serial', serial };
374
- } else if (hasMovie) {
375
- console.log('[turbo] movie:', result.movieVoices.length, 'voices');
376
- return { embed_url: embedUrl, content_type: 'movie', voices: result.movieVoices };
377
- } else {
378
- throw new Error('no data found in obrut embed');
379
- }
380
- }
381
-
382
- module.exports = { turboSearch, getTurboStream };