Files
TMDB-Embed-API/providers/vidsrcextractor.js
T

423 lines
19 KiB
JavaScript

#!/usr/bin/env node
const cheerio = require('cheerio');
// --- Constants ---
const VIDSRC_PROXY_URL = process.env.VIDSRC_PROXY_URL;
let BASEDOM = "https://cloudnestra.com"; // This can be updated by serversLoad
const SOURCE_URL = "https://vidsrc.xyz/embed";
// --- Helper: Conditional Proxied Fetch ---
// This function wraps the native fetch. If VIDSRC_PROXY_URL is set in the environment,
// it routes requests through the proxy. Otherwise, it makes a direct request.
async function fetchWrapper(url, options) {
if (VIDSRC_PROXY_URL) {
const proxiedUrl = `${VIDSRC_PROXY_URL}${encodeURIComponent(url)}`;
console.log(`[VidSrc Proxy] Fetching: ${url} via proxy`);
// Note: The proxy will handle the actual fetching, so we send the request to the proxy URL.
// We pass the original headers in the options, the proxy should forward them.
return fetch(proxiedUrl, options);
}
// If no proxy is set, fetch directly.
console.log(`[VidSrc Direct] Fetching: ${url}`);
return fetch(url, options);
}
// --- Helper Functions (copied and adapted from src/extractor.ts) ---
async function serversLoad(html) {
const $ = cheerio.load(html);
const servers = [];
const title = $("title").text() ?? "";
const baseFrameSrc = $("iframe").attr("src") ?? "";
if (baseFrameSrc) {
try {
const fullUrl = baseFrameSrc.startsWith("//") ? "https:" + baseFrameSrc : baseFrameSrc;
BASEDOM = new URL(fullUrl).origin;
}
catch (e) {
console.warn(`(Attempt 1) Failed to parse base URL from iframe src: ${baseFrameSrc} using new URL(), error: ${e.message}`);
// Attempt 2: Regex fallback for origin
const originMatch = (baseFrameSrc.startsWith("//") ? "https:" + baseFrameSrc : baseFrameSrc).match(/^(https?:\/\/[^/]+)/);
if (originMatch && originMatch[1]) {
BASEDOM = originMatch[1];
console.log(`(Attempt 2) Successfully extracted origin using regex: ${BASEDOM}`);
} else {
console.error(`(Attempt 2) Failed to extract origin using regex from: ${baseFrameSrc}. Using default: ${BASEDOM}`);
// Keep the default BASEDOM = "https://cloudnestra.com" if all fails
}
}
}
$(".serversList .server").each((index, element) => {
const server = $(element);
servers.push({
name: server.text().trim(),
dataHash: server.attr("data-hash") ?? null,
});
});
return {
servers: servers,
title: title,
};
}
async function parseMasterM3U8(m3u8Content, masterM3U8Url) {
const lines = m3u8Content.split('\n').map(line => line.trim());
const streams = [];
for (let i = 0; i < lines.length; i++) {
if (lines[i].startsWith("#EXT-X-STREAM-INF:")) {
const infoLine = lines[i];
let quality = "unknown";
const resolutionMatch = infoLine.match(/RESOLUTION=(\d+x\d+)/);
if (resolutionMatch && resolutionMatch[1]) {
quality = resolutionMatch[1];
}
else {
const bandwidthMatch = infoLine.match(/BANDWIDTH=(\d+)/);
if (bandwidthMatch && bandwidthMatch[1]) {
quality = `${Math.round(parseInt(bandwidthMatch[1]) / 1000)}kbps`;
}
}
if (i + 1 < lines.length && lines[i + 1] && !lines[i + 1].startsWith("#")) {
const streamUrlPart = lines[i + 1];
try {
const fullStreamUrl = new URL(streamUrlPart, masterM3U8Url).href;
streams.push({ quality: quality, url: fullStreamUrl });
}
catch (e) {
console.error(`Error constructing URL for stream part: ${streamUrlPart} with base: ${masterM3U8Url}`, e);
streams.push({ quality: quality, url: streamUrlPart }); // Store partial URL as a fallback
}
i++;
}
}
}
// Sort streams by quality (highest first)
streams.sort((a, b) => {
// Extract resolution height from quality (e.g., "1280x720" -> 720)
const getHeight = (quality) => {
const match = quality.match(/(\d+)x(\d+)/);
return match ? parseInt(match[2], 10) : 0;
};
const heightA = getHeight(a.quality);
const heightB = getHeight(b.quality);
// Higher resolution comes first
return heightB - heightA;
});
return streams;
}
async function PRORCPhandler(prorcp) {
try {
const prorcpUrl = `${BASEDOM}/prorcp/${prorcp}`;
const prorcpFetch = await fetchWrapper(prorcpUrl, {
headers: {
"accept": "*/*", "accept-language": "en-US,en;q=0.9", "priority": "u=1",
"sec-ch-ua": "\"Chromium\";v=\"128\", \"Not;A=Brand\";v=\"24\", \"Google Chrome\";v=\"128\"",
"sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": "\"Windows\"",
"sec-fetch-dest": "script", "sec-fetch-mode": "no-cors", "sec-fetch-site": "same-origin",
'Sec-Fetch-Dest': 'iframe', "Referer": `${BASEDOM}/`, "Referrer-Policy": "origin",
},
timeout: 10000
});
if (!prorcpFetch.ok) {
console.error(`Failed to fetch prorcp: ${prorcpUrl}, status: ${prorcpFetch.status}`);
return null;
}
const prorcpResponse = await prorcpFetch.text();
const regex = /file:\s*'([^']*)'/gm;
const match = regex.exec(prorcpResponse);
if (match && match[1]) {
const masterM3U8Url = match[1];
const m3u8FileFetch = await fetchWrapper(masterM3U8Url, {
headers: { "Referer": prorcpUrl, "Accept": "*/*" },
timeout: 10000
});
if (!m3u8FileFetch.ok) {
console.error(`Failed to fetch master M3U8: ${masterM3U8Url}, status: ${m3u8FileFetch.status}`);
return null;
}
const m3u8Content = await m3u8FileFetch.text();
return parseMasterM3U8(m3u8Content, masterM3U8Url);
}
console.warn("No master M3U8 URL found in prorcp response for:", prorcpUrl);
return null;
}
catch (error) {
console.error(`Error in PRORCPhandler for ${BASEDOM}/prorcp/${prorcp}:`, error);
return null;
}
}
async function SRCRCPhandler(srcrcpPath, refererForSrcrcp) {
try {
const srcrcpUrl = BASEDOM + srcrcpPath;
console.log(`[VidSrc - SRCRCP] Fetching: ${srcrcpUrl} (Referer: ${refererForSrcrcp})`);
const response = await fetchWrapper(srcrcpUrl, {
headers: {
"accept": "*/*",
"accept-language": "en-US,en;q=0.9",
"sec-ch-ua": "\"Chromium\";v=\"128\", \"Not;A=Brand\";v=\"24\", \"Google Chrome\";v=\"128\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"Windows\"",
"sec-fetch-dest": "iframe",
"sec-fetch-mode": "navigate",
"sec-fetch-site": "same-origin", // This might need to be 'cross-site' if BASEDOM's origin differs from srcrcp link's origin
"Referer": refererForSrcrcp,
"Referrer-Policy": "origin",
},
timeout: 10000
});
if (!response.ok) {
console.error(`[VidSrc - SRCRCP] Failed to fetch ${srcrcpUrl}, status: ${response.status}`);
return null;
}
const responseText = await response.text();
console.log(`[VidSrc - SRCRCP] Response from ${srcrcpUrl} (first 500 chars): ${responseText.substring(0, 500)}`);
// Attempt 1: Check for "file: '...'" like in PRORCP
const fileRegex = /file:\s*'([^']*)'/gm;
const fileMatch = fileRegex.exec(responseText);
if (fileMatch && fileMatch[1]) {
const masterM3U8Url = fileMatch[1];
console.log(`[VidSrc - SRCRCP] Found M3U8 URL (via fileMatch): ${masterM3U8Url}`);
const m3u8FileFetch = await fetchWrapper(masterM3U8Url, {
headers: { "Referer": srcrcpUrl, "Accept": "*/*" },
timeout: 10000
});
if (!m3u8FileFetch.ok) {
console.error(`[VidSrc - SRCRCP] Failed to fetch master M3U8: ${masterM3U8Url}, status: ${m3u8FileFetch.status}`);
return null;
}
const m3u8Content = await m3u8FileFetch.text();
return parseMasterM3U8(m3u8Content, masterM3U8Url);
}
// Attempt 2: Check if the responseText itself is an M3U8 playlist
if (responseText.trim().startsWith("#EXTM3U")) {
console.log(`[VidSrc - SRCRCP] Response from ${srcrcpUrl} appears to be an M3U8 playlist directly.`);
return parseMasterM3U8(responseText, srcrcpUrl);
}
// Attempt 3: Look for sources = [...] or sources: [...] in script tags or in JSON-like structures
const $ = cheerio.load(responseText);
let sourcesFound = null;
$('script').each((i, script) => {
const scriptContent = $(script).html();
if (scriptContent) {
// Regex for various ways sources might be defined
const sourcesRegexes = [
/sources\s*[:=]\s*(\[[^\]]*\{(?:\s*|.*?)file\s*:\s*['"]([^'"]+)['"](?:\s*|.*?)\}[^\]]*\])/si, // extracts the URL from sources: [{file: "URL"}]
/playerInstance\.setup\s*\(\s*\{\s*sources\s*:\s*(\[[^\]]*\{(?:\s*|.*?)file\s*:\s*['"]([^'"]+)['"](?:\s*|.*?)\}[^\]]*\])/si, // for playerInstance.setup({sources: [{file: "URL"}]})
/file\s*:\s*['"]([^'"]+\.m3u8[^'"]*)['"]/i, // Direct M3U8 link in a var or object e.g. file: "URL.m3u8"
/src\s*:\s*['"]([^'"]+\.m3u8[^'"]*)['"]/i, // Direct M3U8 link e.g. src: "URL.m3u8"
/loadSource\(['"]([^'"]+\.m3u8[^'"]*)['"]\)/i, // For .loadSource("URL.m3u8")
/new\s+Player\([^)]*\{\s*src\s*:\s*['"]([^'"]+)['"]\s*\}\s*\)/i // For new Player({src: "URL"})
];
for (const regex of sourcesRegexes) {
const sourcesMatch = scriptContent.match(regex);
// For regexes that capture a JSON array-like structure in group 1 and then the URL in group 2 (first two regexes)
if (regex.source.includes('file') && regex.source.includes('sources')) { // Heuristic to identify complex sources regex capturing group 2
if (sourcesMatch && sourcesMatch[2]) { // URL is in group 2
console.log(`[VidSrc - SRCRCP] Found M3U8 URL (script complex): ${sourcesMatch[2]}`);
sourcesFound = [{ quality: 'default', url: sourcesMatch[2] }];
return false; // break cheerio loop
}
}
// For simpler regexes where URL is in group 1
else if (sourcesMatch && sourcesMatch[1]) {
console.log(`[VidSrc - SRCRCP] Found M3U8 URL (script simple): ${sourcesMatch[1]}`);
sourcesFound = [{ quality: 'default', url: sourcesMatch[1] }];
return false; // break cheerio loop
}
}
// Fallback: Look for any absolute .m3u8 URL within the script tag
if (!sourcesFound) {
const m3u8GenericMatch = scriptContent.match(/['"](https?:\/\/[^'"\s]+\.m3u8[^'"\s]*)['"]/i);
if (m3u8GenericMatch && m3u8GenericMatch[1]) {
console.log(`[VidSrc - SRCRCP] Found M3U8 URL (script generic fallback): ${m3u8GenericMatch[1]}`);
sourcesFound = [{ quality: 'default', url: m3u8GenericMatch[1] }];
return false; // break cheerio loop
}
}
}
});
if (sourcesFound && sourcesFound.length > 0) {
// Process the first valid M3U8 URL found, or return direct links
const m3u8Source = sourcesFound.find(s => s.url && s.url.includes('.m3u8'));
if (m3u8Source) {
console.log(`[VidSrc - SRCRCP] First M3U8 source from script: ${m3u8Source.url}`);
// Ensure URL is absolute
const absoluteM3u8Url = m3u8Source.url.startsWith('http') ? m3u8Source.url : new URL(m3u8Source.url, srcrcpUrl).href;
const m3u8FileFetch = await fetchWrapper(absoluteM3u8Url, {
headers: { "Referer": srcrcpUrl, "Accept": "*/*" },
timeout: 10000
});
if (!m3u8FileFetch.ok) {
console.error(`[VidSrc - SRCRCP] Failed to fetch M3U8 from script source: ${absoluteM3u8Url}, status: ${m3u8FileFetch.status}`);
return null;
}
const m3u8Content = await m3u8FileFetch.text();
return parseMasterM3U8(m3u8Content, absoluteM3u8Url);
} else {
// Assuming direct links if no .m3u8 found in the sources array
console.log(`[VidSrc - SRCRCP] Assuming direct links from script sources:`, sourcesFound);
return sourcesFound.map(s => ({
quality: s.quality || s.label || 'auto',
url: s.url.startsWith('http') ? s.url : new URL(s.url, srcrcpUrl).href
}));
}
}
console.warn(`[VidSrc - SRCRCP] No stream extraction method succeeded for ${srcrcpUrl}`);
return null;
} catch (error) {
console.error(`[VidSrc - SRCRCP] Error in SRCRCPhandler for ${srcrcpPath}:`, error);
return null;
}
}
async function rcpGrabber(html) {
const regex = /src:\s*'([^']*)'/;
const match = html.match(regex);
if (!match || !match[1])
return null;
return { metadata: { image: "" }, data: match[1] };
}
function getObject(id) {
const arr = id.split(':');
return { id: arr[0], season: arr[1], episode: arr[2] };
}
function getUrl(id, type) {
if (type === "movie") {
return `${SOURCE_URL}/movie/${id}`;
}
else {
const obj = getObject(id);
return `${SOURCE_URL}/tv/${obj.id}/${obj.season}-${obj.episode}`;
}
}
async function getStreamContent(id, type) {
const url = getUrl(id, type);
const embedRes = await fetchWrapper(url, { headers: { "Referer": SOURCE_URL } });
if (!embedRes.ok) {
console.error(`Failed to fetch embed page ${url}: ${embedRes.status}`);
return [];
}
const embedResp = await embedRes.text();
const { servers, title } = await serversLoad(embedResp);
const apiResponse = [];
// MODIFIED: Process servers in parallel
const serverPromises = servers.map(async (server) => {
if (!server.dataHash) return null; // Skip servers without dataHash
try {
const rcpUrl = `${BASEDOM}/rcp/${server.dataHash}`;
const rcpRes = await fetchWrapper(rcpUrl, {
headers: { 'Sec-Fetch-Dest': 'iframe', "Referer": url }
});
if (!rcpRes.ok) {
console.warn(`RCP fetch failed for server ${server.name}: ${rcpRes.status}`);
return null; // Failed to fetch RCP
}
const rcpHtml = await rcpRes.text();
const rcpData = await rcpGrabber(rcpHtml);
if (!rcpData || !rcpData.data) {
console.warn(`Skipping server ${server.name} due to missing rcp data.`);
return null; // Missing RCP data
}
let streamDetails = null;
if (rcpData.data.startsWith("/prorcp/")) {
streamDetails = await PRORCPhandler(rcpData.data.replace("/prorcp/", ""));
} else if (rcpData.data.startsWith("/srcrcp/")) {
if (server.name === "Superembed" || server.name === "2Embed") {
console.warn(`[VidSrc] Skipping SRCRCP for known problematic server: ${server.name}`);
return null;
}
streamDetails = await SRCRCPhandler(rcpData.data, rcpUrl); // Pass rcpUrl as referer
} else {
console.warn(`Unhandled rcp data type for server ${server.name}: ${rcpData.data.substring(0, 50)}`);
return null; // Unhandled type
}
if (streamDetails && streamDetails.length > 0) {
return {
name: title,
image: rcpData.metadata.image,
mediaId: id,
streams: streamDetails,
referer: BASEDOM,
};
} else {
console.warn(`No stream details from handler for server ${server.name} (${rcpData.data})`);
return null; // No stream details found by the handler
}
} catch (e) {
console.error(`Error processing server ${server.name} (${server.dataHash}):`, e);
return null; // Error during server processing
}
});
const results = await Promise.all(serverPromises);
// Filter out null results (failed servers or no streams) and add valid results to apiResponse
results.forEach(result => {
if (result) {
apiResponse.push(result);
}
});
// END MODIFICATION
return apiResponse;
}
// --- Main execution logic (conditional) ---
async function main() {
const args = process.argv.slice(2);
if (args.length < 2) {
console.error("Usage: node vidsrcextractor.js <id> <type>");
console.error("Example (movie): node vidsrcextractor.js tt0111161 movie");
console.error("Example (series): node vidsrcextractor.js tt0944947:1:1 series"); // Game of Thrones S1E1
return 1; // error code
}
const id = args[0];
const type = args[1];
if (type !== "movie" && type !== "series") {
console.error("Invalid type. Must be 'movie' or 'series'.");
return 1;
}
if (type === "series" && id.split(':').length < 3) {
console.error("Invalid series ID format. Expected 'ttID:season:episode' (e.g., tt0944947:1:1).");
return 1;
}
console.log(`Fetching streams for ID: ${id}, Type: ${type}`);
try {
const results = await getStreamContent(id, type);
if (results && results.length > 0) {
console.log("Extracted Data:");
console.log(JSON.stringify(results, null, 2));
} else {
console.log("No streams found or an error occurred.");
}
return 0;
} catch (error) {
console.error("Error during extraction process:", error);
return 1;
}
}
// Export the function for use in other modules
module.exports = { getStreamContent };
// Run main only if the script is executed directly
if (require.main === module) {
main()
.then(code => {
if (code !== 0) {
console.error(`[vidsrcextractor] Finished with non-zero code ${code}`);
}
})
.catch(error => {
console.error("Unhandled error in main execution:", error);
});
}