Spaces:
Running
Running
Upload cinemar.js
Browse files- cinemar.js +63 -117
cinemar.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
/**
|
| 2 |
-
* Cinemar (cinemar.cc) stream extractor using
|
| 3 |
-
* cinemar.cc uses Cloudflare —
|
| 4 |
*/
|
| 5 |
const { getBrowser } = require('./alloha');
|
| 6 |
const https = require('https');
|
|
@@ -104,7 +104,7 @@ function parseSubs(subtitle) {
|
|
| 104 |
return subs;
|
| 105 |
}
|
| 106 |
|
| 107 |
-
// ── Get HTML via Puppeteer (
|
| 108 |
async function getEmbedHtmlViaPuppeteer(embedUrl) {
|
| 109 |
const browser = await getBrowser();
|
| 110 |
const page = await browser.newPage();
|
|
@@ -194,7 +194,6 @@ function parseCinemarData(html) {
|
|
| 194 |
if (!buf || buf.length < 20) return null;
|
| 195 |
|
| 196 |
const isSerial = buf.indexOf(FOLDER_MARKER_BUF) >= 0;
|
| 197 |
-
console.log('[cinemar/parse] buf length:', buf.length, 'isSerial:', isSerial);
|
| 198 |
|
| 199 |
if (!isSerial) {
|
| 200 |
try {
|
|
@@ -241,17 +240,12 @@ function parseCinemarData(html) {
|
|
| 241 |
allEpPositions.push({ id: fullId, seasonId: seasonMatch[1], epNum: parseInt(seasonMatch[2], 10), title: t ? t.str : ('Серия ' + seasonMatch[2]), pos });
|
| 242 |
});
|
| 243 |
|
| 244 |
-
console.log('[cinemar/parse] found', allEpPositions.length, 'episode positions');
|
| 245 |
-
|
| 246 |
const seasonMap = {};
|
| 247 |
allEpPositions.forEach(ep => {
|
| 248 |
if (!seasonMap[ep.seasonId]) seasonMap[ep.seasonId] = [];
|
| 249 |
if (!seasonMap[ep.seasonId].find(e => e.id === ep.id)) seasonMap[ep.seasonId].push(ep);
|
| 250 |
});
|
| 251 |
const seasonIds = Object.keys(seasonMap).sort((a, b) => parseInt(a.substring(1)) - parseInt(b.substring(1)));
|
| 252 |
-
|
| 253 |
-
console.log('[cinemar/parse] seasonIds:', seasonIds.length, seasonIds.slice(0, 3));
|
| 254 |
-
|
| 255 |
if (!seasonIds.length) return null;
|
| 256 |
|
| 257 |
const episodes = {};
|
|
@@ -288,24 +282,68 @@ async function getCinemarStream(pageUrl) {
|
|
| 288 |
});
|
| 289 |
|
| 290 |
console.log('[cinemar] page HTML length:', pageHtml.length);
|
|
|
|
|
|
|
| 291 |
|
| 292 |
const em = pageHtml.match(/"embedUrl"\s*:\s*"(https:\/\/cinemar\.cc\/embed\/[^"]+)"/);
|
| 293 |
if (!em) {
|
| 294 |
console.log('[cinemar] ERROR: embedUrl not found in page');
|
|
|
|
| 295 |
throw new Error('embed_url not found');
|
| 296 |
}
|
| 297 |
const embedUrl = em[1];
|
| 298 |
console.log('[cinemar] embed URL:', embedUrl.substring(0, 80));
|
| 299 |
|
| 300 |
-
// Step 2: get embed HTML via
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
|
| 305 |
-
|
| 306 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
const merged = {};
|
| 308 |
-
|
| 309 |
const addEpisodes = (data) => {
|
| 310 |
if (!data || !data.isSerial) return;
|
| 311 |
data.seasonIds.forEach(sid => {
|
|
@@ -317,120 +355,28 @@ async function getCinemarStream(pageUrl) {
|
|
| 317 |
});
|
| 318 |
};
|
| 319 |
|
| 320 |
-
|
| 321 |
-
|
|
|
|
|
|
|
| 322 |
for (let b = 0; b < MAX_BATCHES; b++) {
|
| 323 |
const prevCount = Object.keys(merged).length;
|
| 324 |
-
|
| 325 |
const htmls = await Promise.all(
|
| 326 |
Array.from({ length: BATCH }, () =>
|
| 327 |
-
|
| 328 |
-
'
|
| 329 |
-
'accept': 'text/html'
|
| 330 |
-
}).then(html => {
|
| 331 |
-
console.log('[cinemar] worker response len:', html.length, 'hasFile:', html.includes('"file"'));
|
| 332 |
-
return html;
|
| 333 |
-
}).catch(e => {
|
| 334 |
-
console.log('[cinemar] worker request failed:', e.message);
|
| 335 |
return null;
|
| 336 |
-
})
|
| 337 |
)
|
| 338 |
);
|
| 339 |
-
|
| 340 |
-
htmls.filter(Boolean).forEach(html => {
|
| 341 |
-
const parsed = parseCinemarData(html);
|
| 342 |
-
if (parsed) {
|
| 343 |
-
if (!firstParsed) firstParsed = parsed;
|
| 344 |
-
addEpisodes(parsed);
|
| 345 |
-
}
|
| 346 |
-
});
|
| 347 |
-
|
| 348 |
const newCount = Object.keys(merged).length;
|
| 349 |
console.log(`[cinemar] batch ${b+1}: ${newCount} eps (+${newCount - prevCount})`);
|
| 350 |
-
|
| 351 |
-
// For movies, one successful fetch is enough
|
| 352 |
-
if (firstParsed && !firstParsed.isSerial) break;
|
| 353 |
-
|
| 354 |
-
// For serials, stop if no new episodes found
|
| 355 |
if (newCount === prevCount && b >= 1) break;
|
| 356 |
-
|
| 357 |
if (b < MAX_BATCHES - 1) await new Promise(r => setTimeout(r, 200));
|
| 358 |
}
|
| 359 |
|
| 360 |
-
|
| 361 |
-
console.log('[cinemar] worker proxy failed, trying Puppeteer fallback...');
|
| 362 |
-
|
| 363 |
-
// Fallback: try Puppeteer
|
| 364 |
-
let embedHtml = null;
|
| 365 |
-
let attempts = 0;
|
| 366 |
-
const maxAttempts = 3;
|
| 367 |
-
|
| 368 |
-
while (attempts < maxAttempts) {
|
| 369 |
-
attempts++;
|
| 370 |
-
console.log(`[cinemar] Puppeteer attempt ${attempts}/${maxAttempts}`);
|
| 371 |
-
|
| 372 |
-
try {
|
| 373 |
-
embedHtml = await getEmbedHtmlViaPuppeteer(embedUrl);
|
| 374 |
-
|
| 375 |
-
if (embedHtml && embedHtml.includes('"file"') && embedHtml.includes('W3s')) {
|
| 376 |
-
console.log('[cinemar] Puppeteer success on attempt', attempts);
|
| 377 |
-
firstParsed = parseCinemarData(embedHtml);
|
| 378 |
-
if (firstParsed) {
|
| 379 |
-
// Add Puppeteer data to merged episodes for serials
|
| 380 |
-
addEpisodes(firstParsed);
|
| 381 |
-
break;
|
| 382 |
-
}
|
| 383 |
-
}
|
| 384 |
-
|
| 385 |
-
console.log('[cinemar] Puppeteer attempt', attempts, 'failed, no file data');
|
| 386 |
-
|
| 387 |
-
if (attempts < maxAttempts) {
|
| 388 |
-
const delay = 2000 * attempts;
|
| 389 |
-
console.log(`[cinemar] waiting ${delay}ms before retry...`);
|
| 390 |
-
await new Promise(r => setTimeout(r, delay));
|
| 391 |
-
}
|
| 392 |
-
} catch(e) {
|
| 393 |
-
console.log(`[cinemar] Puppeteer attempt ${attempts} error:`, e.message);
|
| 394 |
-
if (attempts >= maxAttempts) throw e;
|
| 395 |
-
}
|
| 396 |
-
}
|
| 397 |
-
|
| 398 |
-
if (!firstParsed) {
|
| 399 |
-
throw new Error('no file data after worker and Puppeteer attempts (Cloudflare blocked?)');
|
| 400 |
-
}
|
| 401 |
-
}
|
| 402 |
-
|
| 403 |
-
// If serial and we have Puppeteer data, try to get more episodes via Puppeteer
|
| 404 |
-
if (firstParsed && firstParsed.isSerial && Object.keys(merged).length > 0) {
|
| 405 |
-
console.log('[cinemar] serial detected, fetching more episodes via Puppeteer...');
|
| 406 |
-
const PUPBATCH = 3;
|
| 407 |
-
for (let pb = 0; pb < PUPBATCH; pb++) {
|
| 408 |
-
const prevCount = Object.keys(merged).length;
|
| 409 |
-
try {
|
| 410 |
-
const moreHtml = await getEmbedHtmlViaPuppeteer(embedUrl);
|
| 411 |
-
if (moreHtml && moreHtml.includes('"file"')) {
|
| 412 |
-
const moreParsed = parseCinemarData(moreHtml);
|
| 413 |
-
if (moreParsed) addEpisodes(moreParsed);
|
| 414 |
-
}
|
| 415 |
-
} catch(e) {
|
| 416 |
-
console.log('[cinemar] Puppeteer batch', pb+1, 'error:', e.message);
|
| 417 |
-
}
|
| 418 |
-
const newCount = Object.keys(merged).length;
|
| 419 |
-
console.log(`[cinemar] Puppeteer batch ${pb+1}: ${newCount} eps (+${newCount - prevCount})`);
|
| 420 |
-
if (newCount === prevCount) break;
|
| 421 |
-
await new Promise(r => setTimeout(r, 500));
|
| 422 |
-
}
|
| 423 |
-
}
|
| 424 |
-
|
| 425 |
-
// Step 3: build response
|
| 426 |
-
if (!firstParsed.isSerial) {
|
| 427 |
-
const voices = firstParsed.items.map(item => ({
|
| 428 |
-
label: item.title, url: item.file, subtitles: parseSubs(item.subtitle)
|
| 429 |
-
}));
|
| 430 |
-
return { content_type: 'movie', embed_url: embedUrl, voices, serial: null, stream: voices[0] ? voices[0].url : null };
|
| 431 |
-
}
|
| 432 |
-
|
| 433 |
-
// Serial: build structure from merged episodes
|
| 434 |
const seasonMap = {};
|
| 435 |
Object.values(merged).forEach(ep => {
|
| 436 |
if (!seasonMap[ep.seasonId]) seasonMap[ep.seasonId] = [];
|
|
|
|
| 1 |
/**
|
| 2 |
+
* Cinemar (cinemar.cc) stream extractor using Puppeteer
|
| 3 |
+
* cinemar.cc now uses Cloudflare Rocket Loader — data only available after JS execution
|
| 4 |
*/
|
| 5 |
const { getBrowser } = require('./alloha');
|
| 6 |
const https = require('https');
|
|
|
|
| 104 |
return subs;
|
| 105 |
}
|
| 106 |
|
| 107 |
+
// ── Get HTML via Puppeteer (bypasses Cloudflare Rocket Loader) ────────────────
|
| 108 |
async function getEmbedHtmlViaPuppeteer(embedUrl) {
|
| 109 |
const browser = await getBrowser();
|
| 110 |
const page = await browser.newPage();
|
|
|
|
| 194 |
if (!buf || buf.length < 20) return null;
|
| 195 |
|
| 196 |
const isSerial = buf.indexOf(FOLDER_MARKER_BUF) >= 0;
|
|
|
|
| 197 |
|
| 198 |
if (!isSerial) {
|
| 199 |
try {
|
|
|
|
| 240 |
allEpPositions.push({ id: fullId, seasonId: seasonMatch[1], epNum: parseInt(seasonMatch[2], 10), title: t ? t.str : ('Серия ' + seasonMatch[2]), pos });
|
| 241 |
});
|
| 242 |
|
|
|
|
|
|
|
| 243 |
const seasonMap = {};
|
| 244 |
allEpPositions.forEach(ep => {
|
| 245 |
if (!seasonMap[ep.seasonId]) seasonMap[ep.seasonId] = [];
|
| 246 |
if (!seasonMap[ep.seasonId].find(e => e.id === ep.id)) seasonMap[ep.seasonId].push(ep);
|
| 247 |
});
|
| 248 |
const seasonIds = Object.keys(seasonMap).sort((a, b) => parseInt(a.substring(1)) - parseInt(b.substring(1)));
|
|
|
|
|
|
|
|
|
|
| 249 |
if (!seasonIds.length) return null;
|
| 250 |
|
| 251 |
const episodes = {};
|
|
|
|
| 282 |
});
|
| 283 |
|
| 284 |
console.log('[cinemar] page HTML length:', pageHtml.length);
|
| 285 |
+
console.log('[cinemar] page has embedUrl:', pageHtml.includes('embedUrl'));
|
| 286 |
+
console.log('[cinemar] page has cinemar.cc:', pageHtml.includes('cinemar.cc'));
|
| 287 |
|
| 288 |
const em = pageHtml.match(/"embedUrl"\s*:\s*"(https:\/\/cinemar\.cc\/embed\/[^"]+)"/);
|
| 289 |
if (!em) {
|
| 290 |
console.log('[cinemar] ERROR: embedUrl not found in page');
|
| 291 |
+
console.log('[cinemar] Page snippet:', pageHtml.substring(0, 500));
|
| 292 |
throw new Error('embed_url not found');
|
| 293 |
}
|
| 294 |
const embedUrl = em[1];
|
| 295 |
console.log('[cinemar] embed URL:', embedUrl.substring(0, 80));
|
| 296 |
|
| 297 |
+
// Step 2: get embed HTML via Puppeteer (Cloudflare blocks plain HTTP)
|
| 298 |
+
let embedHtml = null;
|
| 299 |
+
let attempts = 0;
|
| 300 |
+
const maxAttempts = 3;
|
| 301 |
|
| 302 |
+
while (attempts < maxAttempts) {
|
| 303 |
+
attempts++;
|
| 304 |
+
console.log(`[cinemar] Puppeteer attempt ${attempts}/${maxAttempts}`);
|
| 305 |
+
|
| 306 |
+
try {
|
| 307 |
+
embedHtml = await getEmbedHtmlViaPuppeteer(embedUrl);
|
| 308 |
+
|
| 309 |
+
if (embedHtml && embedHtml.includes('"file"') && embedHtml.includes('W3s')) {
|
| 310 |
+
console.log('[cinemar] Puppeteer success on attempt', attempts);
|
| 311 |
+
break;
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
console.log('[cinemar] Puppeteer attempt', attempts, 'failed, no file data');
|
| 315 |
+
|
| 316 |
+
if (attempts < maxAttempts) {
|
| 317 |
+
const delay = 2000 * attempts;
|
| 318 |
+
console.log(`[cinemar] waiting ${delay}ms before retry...`);
|
| 319 |
+
await new Promise(r => setTimeout(r, delay));
|
| 320 |
+
}
|
| 321 |
+
} catch(e) {
|
| 322 |
+
console.log(`[cinemar] Puppeteer attempt ${attempts} error:`, e.message);
|
| 323 |
+
if (attempts >= maxAttempts) throw e;
|
| 324 |
+
}
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
if (!embedHtml || !embedHtml.includes('"file"')) {
|
| 328 |
+
console.log('[cinemar] ERROR: no file data after all attempts');
|
| 329 |
+
console.log('[cinemar] embed HTML length:', embedHtml ? embedHtml.length : 0);
|
| 330 |
+
console.log('[cinemar] embed HTML snippet:', embedHtml ? embedHtml.substring(0, 500) : 'null');
|
| 331 |
+
throw new Error('no file data in embed (Cloudflare blocked?)');
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
// Step 3: parse data
|
| 335 |
+
const parsed = parseCinemarData(embedHtml);
|
| 336 |
+
if (!parsed) throw new Error('failed to parse cinemar data');
|
| 337 |
+
|
| 338 |
+
if (!parsed.isSerial) {
|
| 339 |
+
const voices = parsed.items.map(item => ({
|
| 340 |
+
label: item.title, url: item.file, subtitles: parseSubs(item.subtitle)
|
| 341 |
+
}));
|
| 342 |
+
return { content_type: 'movie', embed_url: embedUrl, voices, serial: null, stream: voices[0] ? voices[0].url : null };
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
// Serial: fetch multiple times and merge
|
| 346 |
const merged = {};
|
|
|
|
| 347 |
const addEpisodes = (data) => {
|
| 348 |
if (!data || !data.isSerial) return;
|
| 349 |
data.seasonIds.forEach(sid => {
|
|
|
|
| 355 |
});
|
| 356 |
};
|
| 357 |
|
| 358 |
+
addEpisodes(parsed);
|
| 359 |
+
|
| 360 |
+
// Fetch more in parallel to get all episodes
|
| 361 |
+
const BATCH = 5, MAX_BATCHES = 4;
|
| 362 |
for (let b = 0; b < MAX_BATCHES; b++) {
|
| 363 |
const prevCount = Object.keys(merged).length;
|
|
|
|
| 364 |
const htmls = await Promise.all(
|
| 365 |
Array.from({ length: BATCH }, () =>
|
| 366 |
+
getEmbedHtmlViaPuppeteer(embedUrl).then(html => {
|
| 367 |
+
if (html && html.includes('"file"')) return html;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 368 |
return null;
|
| 369 |
+
}).catch(() => null)
|
| 370 |
)
|
| 371 |
);
|
| 372 |
+
htmls.filter(Boolean).forEach(html => addEpisodes(parseCinemarData(html)));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 373 |
const newCount = Object.keys(merged).length;
|
| 374 |
console.log(`[cinemar] batch ${b+1}: ${newCount} eps (+${newCount - prevCount})`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 375 |
if (newCount === prevCount && b >= 1) break;
|
|
|
|
| 376 |
if (b < MAX_BATCHES - 1) await new Promise(r => setTimeout(r, 200));
|
| 377 |
}
|
| 378 |
|
| 379 |
+
// Build serial structure
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 380 |
const seasonMap = {};
|
| 381 |
Object.values(merged).forEach(ep => {
|
| 382 |
if (!seasonMap[ep.seasonId]) seasonMap[ep.seasonId] = [];
|