Download server.js from stnh70/vid: direct link, hf CLI and curl.
- Browser
- Download file 38.1 kB
-
https://huggingface.co/spaces/stnh70/vid/resolve/main/server.js
- Command line
-
hf download hf://spaces/stnh70/vid/server.js
-
curl -L -o server.js https://huggingface.co/spaces/stnh70/vid/resolve/main/server.js
38.1 kB
| import express, { json } from "express"; | |
| import cors from "cors"; | |
| import pLimit from "p-limit"; | |
| import BrowserPool from './pool/BrowserPool.js'; | |
| import { LRUCache } from 'lru-cache'; | |
| import fetch from 'node-fetch'; | |
| import { JSDOM } from 'jsdom'; | |
| import fs from 'fs/promises'; | |
| // Playwright with stealth plugin | |
| import { chromium } from 'playwright-extra'; | |
| import StealthPlugin from 'puppeteer-extra-plugin-stealth'; | |
| chromium.use(StealthPlugin()); | |
| const app = express(); | |
| const PORT = process.env.PORT || 3000; | |
| app.use(cors()); | |
| app.use(json()); | |
| const PROVIDERS = ["https://cdn.moviesapi.club"]; | |
| let browserPool; | |
| const cache = new LRUCache({ max: 500, ttl: 15 * 60 * 1000 }); | |
| const limit = pLimit(2); | |
| async function extractFirstIframeSrc(page, domain) { | |
| try { | |
| await page.waitForSelector('#player_iframe', { timeout: 15000 }); | |
| const src = await page.evaluate(() => { | |
| const iframe = document.getElementById('player_iframe'); | |
| if (!iframe) return null; | |
| let src = iframe.getAttribute('src') || ''; | |
| if (src.startsWith('//')) { | |
| src = `https:${src}`; | |
| } | |
| return src; | |
| }); | |
| if (!src) { | |
| throw new Error('First iframe src not found'); | |
| } | |
| console.log(`[${domain}] Found first iframe src: ${src}`); | |
| return src; | |
| } catch (error) { | |
| console.error(`[${domain}] Error finding first iframe:`, error); | |
| throw error; | |
| } | |
| } | |
| async function getVideoAndSubtitles(finalUrl) { | |
| try { | |
| const response = await fetch(finalUrl); | |
| if (!response.ok) { | |
| throw new Error(`HTTP error! Status: ${response.status}`); | |
| } | |
| const html = await response.text(); | |
| const dom = new JSDOM(html); | |
| const document = dom.window.document; | |
| const scriptTags = document.querySelectorAll('script'); | |
| let videoFileUrl = null; | |
| let subtitleSources = null; | |
| for (const script of scriptTags) { | |
| if (script.textContent.includes('new Playerjs({')) { | |
| const fileMatch = script.textContent.match(/file:\s*['"](.*?)['"]/); | |
| if (fileMatch && fileMatch[1]) { | |
| videoFileUrl = fileMatch[1]; | |
| } | |
| const subtitlesMatch = script.textContent.match(/default_subtitles\s*=\s*['"](.*?)['"]/); | |
| if (subtitlesMatch && subtitlesMatch[1]) { | |
| try { | |
| subtitleSources = JSON.parse(subtitlesMatch[1]); | |
| } catch (e) { | |
| subtitleSources = subtitlesMatch[1].split(',').map(s => s.trim()).filter(Boolean); | |
| } | |
| } | |
| if (videoFileUrl) { | |
| break; | |
| } | |
| } | |
| } | |
| return { videoFileUrl, subtitleSources }; | |
| } catch (error) { | |
| console.error('An error occurred during the process:', error); | |
| throw error; | |
| } | |
| } | |
| // 修改 scrapeProvider 函数 | |
| // 在 scrapeProvider 函数中添加调试代码 | |
| async function scrapeProvider_(domain, url, signal) { | |
| if (signal.aborted) throw new Error('Aborted'); | |
| console.log(`\n[${domain}] Starting scrape for URL: ${url}`); | |
| let browserInstance = null; | |
| try { | |
| browserInstance = await browserPool.get(); | |
| const browser = browserInstance.browser; | |
| // 创建初始页面 | |
| const page = await browser.newPage(); | |
| // 启用请求拦截 | |
| await page.route('**/*', route => { | |
| console.log(`[${domain}] Request: ${route.request().url()}`); | |
| route.continue(); | |
| }); | |
| await page.goto(url, { | |
| waitUntil: 'networkidle', | |
| timeout: 60000 | |
| }); | |
| // 打印页面HTML内容用于调试 | |
| const html = await page.content(); | |
| console.log(`[${domain}] Page HTML:\n${html}`); | |
| // 检查是否有Turnstile | |
| const hasTurnstile = await page.evaluate(() => { | |
| return document.querySelector('.cf-turnstile') !== null; | |
| }); | |
| console.log(`[${domain}] Has Turnstile: ${hasTurnstile}`); | |
| // 处理Turnstile | |
| if (hasTurnstile) { | |
| await handleSpecificTurnstile(page, domain); | |
| } | |
| // 获取第一个iframe的URL | |
| const firstIframeSrc = await extractFirstIframeSrc(page, domain); | |
| if (!firstIframeSrc) throw new Error('First iframe not found'); | |
| // 创建新页面来加载iframe内容 | |
| const iframePage = await browser.newPage(); | |
| await iframePage.goto(firstIframeSrc, { | |
| waitUntil: 'networkidle', | |
| timeout: 60000 | |
| }); | |
| // 打印iframe页面的HTML内容 | |
| const iframeHtml = await iframePage.content(); | |
| console.log(`[${domain}] Iframe HTML:\n${iframeHtml}`); | |
| // 处理可能存在的第二个Turnstile | |
| const iframeHasTurnstile = await iframePage.evaluate(() => { | |
| return document.querySelector('.cf-turnstile') !== null; | |
| }); | |
| console.log(`[${domain}] Iframe has Turnstile: ${iframeHasTurnstile}`); | |
| if (iframeHasTurnstile) { | |
| await handleSpecificTurnstile(iframePage, domain); | |
| } | |
| // 获取最终iframe的URL | |
| const finalIframeSrc = await extractFinalIframeSrc(iframePage, domain); | |
| if (!finalIframeSrc) throw new Error('Final iframe not found'); | |
| // 获取视频和字幕 | |
| const { videoFileUrl, subtitleSources } = await getVideoAndSubtitles(finalIframeSrc); | |
| if (!videoFileUrl) throw new Error("HLS URL not found"); | |
| return { source_domain: domain, hls_url: videoFileUrl, subtitles: subtitleSources, error: null }; | |
| } catch (error) { | |
| console.error(`[${domain}] Error in scrapeProvider: ${error.message}`); | |
| throw error; | |
| } finally { | |
| if (browserInstance) { | |
| console.log(`[${domain}] Releasing browser ${browserInstance.id} back to pool.`); | |
| await browserPool.release(browserInstance); | |
| } | |
| } | |
| } | |
| async function scrapeProvider__(domain, url, signal) { | |
| if (signal.aborted) throw new Error('Aborted'); | |
| console.log(`\n[${domain}] Starting scrape for URL: ${url}`); | |
| let browserInstance = null; | |
| try { | |
| browserInstance = await browserPool.get(); | |
| const browser = browserInstance.browser; | |
| // 创建初始页面 | |
| const page = await browser.newPage(); | |
| // 启用请求拦截 | |
| await page.route('**/*', route => { | |
| const requestUrl = route.request().url(); | |
| console.log(`[${domain}] Request: ${requestUrl}`); | |
| // 如果是 cloudnestra.com/rcp/ 的请求,记录响应但不拦截 | |
| if (requestUrl.includes('cloudnestra.com/rcp/')) { | |
| route.continue().then(response => { | |
| response.text().then(text => { | |
| console.log(`[${domain}] Cloudnestra response:`, text); | |
| // 将响应保存到页面属性中 | |
| page.cloudnestraResponse = text; | |
| }); | |
| }); | |
| } else { | |
| route.continue(); | |
| } | |
| }); | |
| await page.goto(url, { | |
| waitUntil: 'networkidle', | |
| timeout: 60000 | |
| }); | |
| // 检查是否有Turnstile | |
| const hasTurnstile = await page.evaluate(() => { | |
| return document.querySelector('.cf-turnstile') !== null; | |
| }); | |
| console.log(`[${domain}] Has Turnstile: ${hasTurnstile}`); | |
| // 处理Turnstile | |
| if (hasTurnstile) { | |
| await handleSpecificTurnstile(page, domain); | |
| } | |
| // 获取第一个iframe的URL | |
| const firstIframeSrc = await extractFirstIframeSrc(page, domain); | |
| if (!firstIframeSrc) throw new Error('First iframe not found'); | |
| // 创建新页面来加载iframe内容 | |
| const iframePage = await browser.newPage(); | |
| await iframePage.goto(firstIframeSrc, { | |
| waitUntil: 'networkidle', | |
| timeout: 60000 | |
| }); | |
| // 处理可能存在的第二个Turnstile | |
| const iframeHasTurnstile = await iframePage.evaluate(() => { | |
| return document.querySelector('.cf-turnstile') !== null; | |
| }); | |
| console.log(`[${domain}] Iframe has Turnstile: ${iframeHasTurnstile}`); | |
| if (iframeHasTurnstile) { | |
| await handleSpecificTurnstile(iframePage, domain); | |
| } | |
| // 获取最终iframe的URL | |
| const finalIframeSrc = await extractFinalIframeSrc(iframePage, domain); | |
| if (!finalIframeSrc) throw new Error('Final iframe not found'); | |
| // 检查是否是 cloudnestra.com/rcp/ 的URL | |
| if (finalIframeSrc.includes('cloudnestra.com/rcp/')) { | |
| // 使用保存的响应 | |
| const responseText = page.cloudnestraResponse; | |
| if (!responseText) throw new Error('No cloudnestra response found'); | |
| // 解析响应 | |
| try { | |
| const data = JSON.parse(responseText); | |
| if (data.file) { | |
| console.log(`[${domain}] Found video URL in cloudnestra response:`, data.file); | |
| return { | |
| source_domain: domain, | |
| hls_url: data.file, | |
| subtitles: data.subtitles || [], | |
| error: null | |
| }; | |
| } | |
| throw new Error("No video URL found in cloudnestra response"); | |
| } catch (e) { | |
| throw new Error("Failed to parse cloudnestra response"); | |
| } | |
| } | |
| // 如果不是 cloudnestra.com/rcp/ 的URL,使用原有的处理方式 | |
| const { videoFileUrl, subtitleSources } = await getVideoAndSubtitles(finalIframeSrc); | |
| if (!videoFileUrl) throw new Error("HLS URL not found"); | |
| return { source_domain: domain, hls_url: videoFileUrl, subtitles: subtitleSources, error: null }; | |
| } catch (error) { | |
| console.error(`[${domain}] Error in scrapeProvider: ${error.message}`); | |
| throw error; | |
| } finally { | |
| if (browserInstance) { | |
| console.log(`[${domain}] Releasing browser ${browserInstance.id} back to pool.`); | |
| await browserPool.release(browserInstance); | |
| } | |
| } | |
| } | |
| async function scrapeProvider6(domain, url, signal) { | |
| if (signal.aborted) throw new Error('Aborted'); | |
| console.log(`\n[${domain}] Starting scrape for URL: ${url}`); | |
| let browserInstance = null; | |
| try { | |
| browserInstance = await browserPool.get(); | |
| const browser = browserInstance.browser; | |
| // 创建初始页面 | |
| const page = await browser.newPage(); | |
| let videoResult = null; | |
| // 启用请求拦截 | |
| // await page.route('**/*', async (route) => { | |
| // const requestUrl = route.request().url(); | |
| // console.log(`[${domain}] Request: ${requestUrl}`); | |
| // // 如果是 cloudnestra.com/rcp/ 的请求,使用 getVideoAndSubtitles 解析 | |
| // if (requestUrl.includes('cloudnestra.com/rcp/')) { | |
| // try { | |
| // const { videoFileUrl, subtitleSources } = await getVideoAndSubtitles(requestUrl); | |
| // if (!videoFileUrl) throw new Error("HLS URL not found"); | |
| // videoResult = { | |
| // source_domain: domain, | |
| // hls_url: videoFileUrl, | |
| // subtitles: subtitleSources, | |
| // error: null | |
| // }; | |
| // route.abort(); | |
| // } catch (error) { | |
| // console.error(`[${domain}] Error processing cloudnestra request:`, error); | |
| // route.continue(); | |
| // } | |
| // } else { | |
| // route.continue(); | |
| // } | |
| // }); | |
| await page.route('**/*', async (route) => { | |
| const requestUrl = route.request().url(); | |
| console.log(`[${domain}] Request: ${requestUrl}`); | |
| // 如果是 cloudnestra.com/rcp/ 请求,输出它的内容 | |
| if (requestUrl.includes('cloudnestra.com/rcp/')) { | |
| console.log(`[${domain}] 🎯 检测到 cloudnestra.com/rcp/ 请求:`); | |
| console.log(`[${domain}] URL: ${requestUrl}`); | |
| try { | |
| // 获取请求的响应内容 | |
| const response = await route.fetch(); | |
| const content = await response.text(); | |
| console.log(`[${domain}] 响应状态: ${response.status()}`); | |
| console.log(`[${domain}] 响应头:`, response.headers()); | |
| console.log(`[${domain}] 响应内容长度: ${content.length}`); | |
| console.log(`[${domain}] 响应内容预览 (前500字符):`); | |
| console.log(content.substring(0, 500)); | |
| // 如果内容看起来像JSON,尝试解析 | |
| if (content.trim().startsWith('{') || content.trim().startsWith('[')) { | |
| try { | |
| const jsonContent = JSON.parse(content); | |
| console.log(`[${domain}] JSON内容:`, JSON.stringify(jsonContent, null, 2)); | |
| } catch (e) { | |
| console.log(`[${domain}] 不是有效的JSON格式`); | |
| } | |
| } | |
| // 继续正常的请求流程 | |
| route.continue(); | |
| } catch (error) { | |
| console.error(`[${domain}] 获取cloudnestra内容时出错:`, error); | |
| route.continue(); | |
| } | |
| } else { | |
| route.continue(); | |
| } | |
| }); | |
| await page.goto(url, { | |
| waitUntil: 'networkidle', | |
| timeout: 60000 | |
| }); | |
| // 如果已经从 cloudnestra 获取了视频链接,直接返回 | |
| if (videoResult) { | |
| return videoResult; | |
| } | |
| // 检查是否有Turnstile | |
| const hasTurnstile = await page.evaluate(() => { | |
| return document.querySelector('.cf-turnstile') !== null; | |
| }); | |
| console.log(`[${domain}] Has Turnstile: ${hasTurnstile}`); | |
| // 处理Turnstile | |
| if (hasTurnstile) { | |
| await handleSpecificTurnstile(page, domain); | |
| } | |
| // 获取第一个iframe的URL | |
| const firstIframeSrc = await extractFirstIframeSrc(page, domain); | |
| if (!firstIframeSrc) throw new Error('First iframe not found'); | |
| // 创建新页面来加载iframe内容 | |
| const iframePage = await browser.newPage(); | |
| await iframePage.goto(firstIframeSrc, { | |
| waitUntil: 'networkidle', | |
| timeout: 60000 | |
| }); | |
| // 处理可能存在的第二个Turnstile | |
| const iframeHasTurnstile = await iframePage.evaluate(() => { | |
| return document.querySelector('.cf-turnstile') !== null; | |
| }); | |
| console.log(`[${domain}] Iframe has Turnstile: ${iframeHasTurnstile}`); | |
| if (iframeHasTurnstile) { | |
| await handleSpecificTurnstile(iframePage, domain); | |
| } | |
| // 获取最终iframe的URL | |
| const finalIframeSrc = await extractFinalIframeSrc(iframePage, domain); | |
| if (!finalIframeSrc) throw new Error('Final iframe not found'); | |
| // 使用 getVideoAndSubtitles 函数获取视频和字幕 | |
| const { videoFileUrl, subtitleSources } = await getVideoAndSubtitles(finalIframeSrc); | |
| if (!videoFileUrl) throw new Error("HLS URL not found"); | |
| return { source_domain: domain, hls_url: videoFileUrl, subtitles: subtitleSources, error: null }; | |
| } catch (error) { | |
| console.error(`[${domain}] Error in scrapeProvider: ${error.message}`); | |
| throw error; | |
| } finally { | |
| if (browserInstance) { | |
| console.log(`[${domain}] Releasing browser ${browserInstance.id} back to pool.`); | |
| await browserPool.release(browserInstance); | |
| } | |
| } | |
| } | |
| async function scrapeProvider(domain, url, signal) { | |
| if (signal.aborted) throw new Error('Aborted'); | |
| console.log(`\n[${domain}] Starting scrape for URL: ${url}`); | |
| let browserInstance = null; | |
| let cloudnestraData = null; | |
| try { | |
| browserInstance = await browserPool.get(); | |
| const browser = browserInstance.browser; | |
| // 创建带有上下文的页面 | |
| const context = await browser.newContext({ | |
| userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', | |
| extraHTTPHeaders: { | |
| 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', | |
| 'Accept-Language': 'en-US,en;q=0.5', | |
| 'Accept-Encoding': 'gzip, deflate, br', | |
| 'DNT': '1', | |
| 'Connection': 'keep-alive', | |
| 'Upgrade-Insecure-Requests': '1', | |
| }, | |
| viewport: { width: 1920, height: 1080 } | |
| }); | |
| // 移除自动化痕迹 | |
| await context.addInitScript(() => { | |
| Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); | |
| Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5] }); | |
| Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] }); | |
| }); | |
| const page = await context.newPage(); | |
| // 启用请求拦截 | |
| await page.route('**/*', async (route) => { | |
| const requestUrl = route.request().url(); | |
| console.log(`[${domain}] Request: ${requestUrl}`); | |
| // 如果是 cloudnestra.com/rcp/ 请求,特殊处理 | |
| if (requestUrl.includes('cloudnestra.com/rcp/')) { | |
| console.log(`[${domain}] 🎯 检测到 cloudnestra.com/rcp/ 请求: ${requestUrl}`); | |
| try { | |
| // 添加随机延迟模拟人类行为 | |
| await new Promise(resolve => setTimeout(resolve, Math.random() * 1000 + 500)); | |
| // 使用真实的请求头获取内容 | |
| const response = await route.fetch({ | |
| headers: { | |
| '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', | |
| 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', | |
| 'Accept-Language': 'en-US,en;q=0.9', | |
| 'Referer': domain, | |
| 'Cache-Control': 'no-cache' | |
| } | |
| }); | |
| console.log(`[${domain}] Cloudnestra 响应状态: ${response.status()}`); | |
| if (response.status() === 200) { | |
| const content = await response.text(); | |
| console.log(`[${domain}] ✅ 成功获取 cloudnestra 内容,长度: ${content.length}`); | |
| // 尝试从内容中提取 src | |
| const srcMatch = content.match(/src\s*:\s*['"](.*?)['"]/); | |
| if (srcMatch && srcMatch[1]) { | |
| let finalUrl = srcMatch[1]; | |
| // 构造完整URL | |
| if (finalUrl.startsWith('/')) { | |
| const baseUrl = requestUrl.split('/rcp')[0]; | |
| finalUrl = `${baseUrl}${finalUrl}`; | |
| } | |
| finalUrl = finalUrl.replace('http://', 'https://'); | |
| console.log(`[${domain}] 从 cloudnestra 提取到的 URL: ${finalUrl}`); | |
| // 获取视频和字幕 | |
| const { videoFileUrl, subtitleSources } = await getVideoAndSubtitles(finalUrl); | |
| if (videoFileUrl) { | |
| cloudnestraData = { | |
| source_domain: domain, | |
| hls_url: videoFileUrl, | |
| subtitles: subtitleSources, | |
| error: null | |
| }; | |
| console.log(`[${domain}] 🎉 成功从 cloudnestra 获取视频: ${videoFileUrl}`); | |
| } | |
| } | |
| // 如果内容看起来像JSON,尝试直接解析 | |
| if (content.trim().startsWith('{') || content.trim().startsWith('[')) { | |
| try { | |
| const jsonContent = JSON.parse(content); | |
| if (jsonContent.file) { | |
| cloudnestraData = { | |
| source_domain: domain, | |
| hls_url: jsonContent.file, | |
| subtitles: jsonContent.subtitles || [], | |
| error: null | |
| }; | |
| console.log(`[${domain}] 🎉 从 JSON 响应获取视频: ${jsonContent.file}`); | |
| } | |
| } catch (e) { | |
| console.log(`[${domain}] 内容不是有效的JSON格式`); | |
| } | |
| } | |
| // 继续正常的请求流程 | |
| route.continue(); | |
| } else if (response.status() === 522) { | |
| console.log(`[${domain}] ❌ Cloudflare 连接超时 (522)`); | |
| route.continue(); | |
| } else if (response.status() === 403) { | |
| console.log(`[${domain}] ❌ 被 Cloudflare 拒绝访问 (403)`); | |
| route.continue(); | |
| } else { | |
| console.log(`[${domain}] ⚠️ 异常响应状态: ${response.status()}`); | |
| route.continue(); | |
| } | |
| } catch (error) { | |
| console.error(`[${domain}] ❌ 处理 cloudnestra 请求失败:`, error.message); | |
| route.continue(); | |
| } | |
| } else { | |
| route.continue(); | |
| } | |
| }); | |
| await page.goto(url, { | |
| waitUntil: 'networkidle', | |
| timeout: 60000 | |
| }); | |
| // 如果已经从 cloudnestra 获取了视频链接,直接返回 | |
| if (cloudnestraData) { | |
| console.log(`[${domain}] 🎯 使用 cloudnestra 数据`); | |
| return cloudnestraData; | |
| } | |
| // 继续原有的处理流程... | |
| // 检查是否有Turnstile | |
| const hasTurnstile = await page.evaluate(() => { | |
| return document.querySelector('.cf-turnstile') !== null; | |
| }); | |
| console.log(`[${domain}] Has Turnstile: ${hasTurnstile}`); | |
| // 处理Turnstile | |
| if (hasTurnstile) { | |
| await handleSpecificTurnstile(page, domain); | |
| } | |
| // 获取第一个iframe的URL | |
| const firstIframeSrc = await extractFirstIframeSrc(page, domain); | |
| if (!firstIframeSrc) throw new Error('First iframe not found'); | |
| // 创建新页面来加载iframe内容 | |
| const iframePage = await context.newPage(); | |
| await iframePage.goto(firstIframeSrc, { | |
| waitUntil: 'networkidle', | |
| timeout: 60000 | |
| }); | |
| // 处理可能存在的第二个Turnstile | |
| const iframeHasTurnstile = await iframePage.evaluate(() => { | |
| return document.querySelector('.cf-turnstile') !== null; | |
| }); | |
| console.log(`[${domain}] Iframe has Turnstile: ${iframeHasTurnstile}`); | |
| if (iframeHasTurnstile) { | |
| await handleSpecificTurnstile(iframePage, domain); | |
| } | |
| // 获取最终iframe的URL | |
| const finalIframeSrc = await extractFinalIframeSrc(iframePage, domain); | |
| if (!finalIframeSrc) throw new Error('Final iframe not found'); | |
| // 使用 getVideoAndSubtitles 函数获取视频和字幕 | |
| const { videoFileUrl, subtitleSources } = await getVideoAndSubtitles(finalIframeSrc); | |
| if (!videoFileUrl) throw new Error("HLS URL not found"); | |
| return { source_domain: domain, hls_url: videoFileUrl, subtitles: subtitleSources, error: null }; | |
| } catch (error) { | |
| console.error(`[${domain}] Error in scrapeProvider: ${error.message}`); | |
| throw error; | |
| } finally { | |
| if (browserInstance) { | |
| console.log(`[${domain}] Releasing browser ${browserInstance.id} back to pool.`); | |
| // 关闭上下文 | |
| if (typeof context !== 'undefined') { | |
| await context.close(); | |
| } | |
| await browserPool.release(browserInstance); | |
| } | |
| } | |
| } | |
| async function scrapeProvider4(domain, url, signal) { | |
| if (signal.aborted) throw new Error('Aborted'); | |
| console.log(`\n[${domain}] Starting scrape for URL: ${url}`); | |
| let browserInstance = null; | |
| try { | |
| browserInstance = await browserPool.get(); | |
| const browser = browserInstance.browser; | |
| // 创建初始页面 | |
| const page = await browser.newPage(); | |
| let videoResult = null; | |
| // 启用请求拦截 | |
| await page.route('**/*', async (route) => { | |
| const requestUrl = route.request().url(); | |
| console.log(`[${domain}] Request: ${requestUrl}`); | |
| // 如果是 cloudnestra.com/rcp/ 的请求,使用 getVideoAndSubtitles 解析 | |
| if (requestUrl.includes('cloudnestra.com/rcp/')) { | |
| try { | |
| const { videoFileUrl, subtitleSources } = await getVideoAndSubtitles(requestUrl); | |
| if (!videoFileUrl) throw new Error("HLS URL not found"); | |
| videoResult = { | |
| source_domain: domain, | |
| hls_url: videoFileUrl, | |
| subtitles: subtitleSources, | |
| error: null | |
| }; | |
| route.abort(); | |
| // 中止所有后续请求 | |
| await page.unroute('**/*'); | |
| } catch (error) { | |
| console.error(`[${domain}] Error processing cloudnestra request:`, error); | |
| route.abort(); | |
| // 中止所有后续请求 | |
| await page.unroute('**/*'); | |
| } | |
| } else { | |
| route.abort(); // 中止所有非 cloudnestra.com/rcp/ 的请求 | |
| } | |
| }); | |
| // 等待 videoResult 被设置 | |
| await page.waitForFunction( | |
| () => videoResult !== null, | |
| { timeout: 30000 } | |
| ).catch(() => { | |
| throw new Error('Timeout waiting for cloudnestra.com/rcp/ request'); | |
| }); | |
| return videoResult; | |
| } catch (error) { | |
| console.error(`[${domain}] Error in scrapeProvider: ${error.message}`); | |
| throw error; | |
| } finally { | |
| if (browserInstance) { | |
| console.log(`[${domain}] Releasing browser ${browserInstance.id} back to pool.`); | |
| await browserPool.release(browserInstance); | |
| } | |
| } | |
| } | |
| async function scrapeProvider5(domain, url, signal) { | |
| if (signal.aborted) throw new Error('Aborted'); | |
| console.log(`\n[${domain}] Starting scrape for URL: ${url}`); | |
| let browserInstance = null; | |
| try { | |
| browserInstance = await browserPool.get(); | |
| const browser = browserInstance.browser; | |
| // 创建初始页面 | |
| const page = await browser.newPage(); | |
| let videoResult = null; | |
| // 启用请求拦截 | |
| await page.route('**/*', async (route) => { | |
| const requestUrl = route.request().url(); | |
| console.log(`[${domain}] Request: ${requestUrl}`); | |
| // 如果是 cloudnestra.com/rcp/ 的请求 | |
| if (requestUrl.includes('cloudnestra.com/rcp/')) { | |
| try { | |
| // 获取响应 | |
| const response = await route.fetch(); | |
| const html = await response.text(); | |
| // 使用正则表达式提取 src 值 | |
| const proMatch = html.match(/src\s*:\s*'([^']+)'/); | |
| let finalUrl = proMatch?.[1]; | |
| if (!finalUrl) { | |
| throw new Error('Could not find src in response'); | |
| } | |
| // 确保使用 HTTPS 协议 | |
| const baseUrl = requestUrl.split('/rcp')[0]; // 获取基础 URL | |
| finalUrl = `${baseUrl}${finalUrl}`; // 拼接完整 URL | |
| finalUrl = finalUrl.replace('http://', 'https://'); // 确保使用 HTTPS | |
| console.log('Final URL:', finalUrl); | |
| // 调用 getVideoAndSubtitles | |
| const { videoFileUrl, subtitleSources } = await getVideoAndSubtitles(finalUrl); | |
| if (!videoFileUrl) throw new Error("HLS URL not found"); | |
| videoResult = { | |
| source_domain: domain, | |
| hls_url: videoFileUrl, | |
| subtitles: subtitleSources, | |
| error: null | |
| }; | |
| // 中止所有后续请求 | |
| await page.unroute('**/*'); | |
| route.abort(); | |
| } catch (error) { | |
| console.error(`[${domain}] Error processing cloudnestra request:`, error); | |
| route.abort(); | |
| await page.unroute('**/*'); | |
| } | |
| } else { | |
| route.abort(); | |
| } | |
| }); | |
| // 等待 videoResult 被设置 | |
| await page.waitForFunction( | |
| () => videoResult !== null, | |
| { timeout: 30000 } | |
| ).catch(() => { | |
| throw new Error('Timeout waiting for cloudnestra.com/rcp/ request'); | |
| }); | |
| return videoResult; | |
| } catch (error) { | |
| console.error(`[${domain}] Error in scrapeProvider: ${error.message}`); | |
| throw error; | |
| } finally { | |
| if (browserInstance) { | |
| console.log(`[${domain}] Releasing browser ${browserInstance.id} back to pool.`); | |
| await browserPool.release(browserInstance); | |
| } | |
| } | |
| } | |
| // 修改 handleSpecificTurnstile 函数 | |
| async function handleSpecificTurnstile(page, domain) { | |
| try { | |
| console.log(`[${domain}] Checking for Turnstile...`); | |
| // 检查是否存在Turnstile | |
| const turnstileExists = await page.waitForSelector('.cf-turnstile', { | |
| timeout: 10000, | |
| state: 'attached' | |
| }).then(() => true).catch(() => false); | |
| if (!turnstileExists) { | |
| console.log(`[${domain}] No Turnstile detected`); | |
| return; | |
| } | |
| console.log(`[${domain}] Turnstile detected - bypassing...`); | |
| // 等待Turnstile加载 | |
| await page.waitForSelector('.cf-turnstile', { | |
| timeout: 10000, | |
| state: 'visible' | |
| }); | |
| // 直接触发回调 | |
| await page.evaluate(() => { | |
| const mockToken = 'mock-token-' + Math.random().toString(36).substring(2); | |
| if (typeof window.cftCallback === 'function') { | |
| window.cftCallback(mockToken); | |
| } | |
| // 提交表单 | |
| const form = document.createElement('form'); | |
| form.method = 'POST'; | |
| form.action = '/rcp_verify'; | |
| const input = document.createElement('input'); | |
| input.type = 'hidden'; | |
| input.name = 'token'; | |
| input.value = mockToken; | |
| form.appendChild(input); | |
| document.body.appendChild(form); | |
| form.submit(); | |
| }); | |
| // 等待页面响应 | |
| await page.waitForNavigation({ | |
| waitUntil: 'networkidle', // 使用Playwright支持的等待策略 | |
| timeout: 30000 | |
| }); | |
| console.log(`[${domain}] Turnstile bypass completed`); | |
| } catch (error) { | |
| console.warn(`[${domain}] Turnstile bypass warning: ${error.message}`); | |
| } | |
| } | |
| // 修改 extractFinalIframeSrc 函数 | |
| async function extractFinalIframeSrc(page, domain) { | |
| try { | |
| // 等待动态内容加载 | |
| await page.waitForFunction(() => { | |
| return document.querySelectorAll('script, iframe').length > 0; | |
| }, { timeout: 15000 }); | |
| return await page.evaluate(() => { | |
| // Method 1: 检查直接iframe | |
| const iframe = document.querySelector('iframe:not([style*="display:none"])'); | |
| if (iframe?.src) return iframe.src; | |
| // Method 2: 检查脚本中的iframe URL | |
| const scripts = Array.from(document.querySelectorAll('script')); | |
| for (const script of scripts) { | |
| if (!script.textContent) continue; | |
| // 查找loadIframe模式 | |
| if (script.textContent.includes('loadIframe')) { | |
| const srcMatch = script.textContent.match(/src:\s*['"](.*?)['"]/); | |
| if (srcMatch) return new URL(srcMatch[1], window.location.href).href; | |
| } | |
| // 查找直接URL模式 | |
| const urlMatch = script.textContent.match(/(https?:)?\/\/[^'"]+embed[^'"]*/); | |
| if (urlMatch) { | |
| return urlMatch[0].startsWith('http') ? urlMatch[0] : `https:${urlMatch[0]}`; | |
| } | |
| } | |
| return null; | |
| }); | |
| } catch (error) { | |
| console.error(`[${domain}] Error finding final iframe: ${error.message}`); | |
| return null; | |
| } | |
| } | |
| app.get("/extract", async (req, res) => { | |
| const type = req.query.type || "movie"; | |
| const tmdb_id = req.query.tmdb_id; | |
| const season = req.query.season ? parseInt(req.query.season) : undefined; | |
| const episode = req.query.episode ? parseInt(req.query.episode) : undefined; | |
| if (!tmdb_id) { return res.status(400).json({ success: false, error: "tmdb_id is required" }); } | |
| if (type === "tv" && (season == null || episode == null)) { return res.status(400).json({ success: false, error: "season and episode are required" }); } | |
| const cacheKey = JSON.stringify(req.query); | |
| const cached = cache.get(cacheKey); | |
| if (cached) { | |
| console.log("Serving from cache"); | |
| return res.json(cached); | |
| } | |
| const urls = PROVIDERS.reduce((acc, domain) => { | |
| acc[domain] = type === "tv" ? `${domain}/embed/tv?tmdb=${tmdb_id}&season=${season}&episode=${episode}` : `${domain}/embed/movie/${tmdb_id}`; | |
| return acc; | |
| }, {}); | |
| const controller = new AbortController(); | |
| const signal = controller.signal; | |
| try { | |
| const promises = Object.entries(urls).map(([domain, url]) => | |
| limit(() => scrapeProvider(domain, url, signal)) | |
| ); | |
| const firstSuccessfulResult = await Promise.any(promises); | |
| controller.abort(); | |
| const response = { success: true, result: firstSuccessfulResult }; | |
| cache.set(cacheKey, response); | |
| res.json(response); | |
| } catch (err) { | |
| if (err instanceof AggregateError) { | |
| console.error("All providers failed to find a link."); | |
| res.status(404).json({ success: false, error: "Could not find the video from any provider." }); | |
| } else { | |
| console.error("An unexpected server error occurred:", err); | |
| res.status(500).json({ success: false, error: "Unexpected server error" }); | |
| } | |
| } | |
| }); | |
| (async () => { | |
| try { | |
| browserPool = new BrowserPool({ | |
| chromium: chromium, | |
| minSize: 1, | |
| maxSize: 5, | |
| maxUsage: 100, | |
| launchOptions: { | |
| headless: true, | |
| args: [ | |
| '--no-sandbox', | |
| '--disable-setuid-sandbox', | |
| '--disable-dev-shm-usage', | |
| '--disable-gpu', | |
| '--disable-extensions', | |
| '--disable-notifications', | |
| '--disable-infobars', | |
| '--disable-web-security', | |
| '--disable-features=IsolateOrigins,site-per-process' | |
| ] | |
| } | |
| }); | |
| await browserPool.initialize(); | |
| console.log("Browser pool initialized successfully."); | |
| app.listen(PORT, () => console.log(`🚀 Universal Video Extractor running at http://localhost:${PORT}`)); | |
| } catch (error) { | |
| console.error("Failed to initialize browser pool:", error); | |
| process.exit(1); | |
| } | |
| })(); | |
| process.on("SIGINT", async () => { | |
| console.log("Shutting down gracefully..."); | |
| if (browserPool) await browserPool.shutdown(); | |
| process.exit(0); | |
| }); | |
| process.on("SIGTERM", async () => { | |
| console.log("Shutting down gracefully..."); | |
| if (browserPool) await browserPool.shutdown(); | |
| process.exit(0); | |
| }); | |