mirror of
https://github.com/Inside4ndroid/TMDB-Embed-API.git
synced 2026-10-22 09:35:11 +00:00
chore(release): 1.0.8
This commit is contained in:
+24
-22
@@ -770,7 +770,9 @@ function extractHubCloudLinks(url, referer) {
|
||||
headers = { Referer: baseUrl + '/', Origin: baseUrl };
|
||||
console.log('[4KHDHub] Added Referer/Origin headers for r2.dev FSL link');
|
||||
}
|
||||
} catch {}
|
||||
} catch {
|
||||
// Ignore URL parsing errors
|
||||
}
|
||||
|
||||
resolve({
|
||||
name: `4KHDHub - FSL Server${qualityLabel}`,
|
||||
@@ -794,7 +796,9 @@ function extractHubCloudLinks(url, referer) {
|
||||
headers = { Referer: baseUrl + '/', Origin: baseUrl };
|
||||
console.log('[4KHDHub] Added Referer/Origin headers for r2.dev FSL link (fallback)');
|
||||
}
|
||||
} catch {}
|
||||
} catch {
|
||||
// Ignore URL parsing errors
|
||||
}
|
||||
|
||||
resolve({
|
||||
name: `4KHDHub - FSL Server${qualityLabel}`,
|
||||
@@ -1332,24 +1336,18 @@ function extractStreamingLinks(downloadLinks) {
|
||||
|
||||
return Promise.all(promises)
|
||||
.then(results => {
|
||||
const validResults = results.filter(result => result !== null);
|
||||
const flatResults = validResults.flat();
|
||||
// Keep .zip links but strip the trailing .zip extension (attempt direct file URL)
|
||||
const transformedResults = flatResults
|
||||
.filter(link => link && link.url)
|
||||
.map(link => {
|
||||
try {
|
||||
const lower = link.url.toLowerCase();
|
||||
if (lower.endsWith('.zip')) {
|
||||
const newUrl = link.url.slice(0, -4); // remove .zip
|
||||
console.log(`[4KHDHub] Converted zip URL to non-zip: ${link.url} -> ${newUrl}`);
|
||||
return { ...link, url: newUrl };
|
||||
}
|
||||
} catch {}
|
||||
return link;
|
||||
});
|
||||
// Note: Link count will be logged after validation completes
|
||||
return transformedResults;
|
||||
const validResults = results.filter(r => r !== null).flat();
|
||||
const filtered = validResults.filter(link => {
|
||||
if (!link || !link.url) return false;
|
||||
const lower = link.url.toLowerCase();
|
||||
if (lower.endsWith('.zip')) {
|
||||
console.log(`[4KHDHub] Skipping archive (zip) link: ${link.url}`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
console.log(`[4KHDHub] Post-processing: kept ${filtered.length}, skipped ${validResults.length - filtered.length} zip archive links`);
|
||||
return filtered;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1841,11 +1839,15 @@ async function get4KHDHubStreams(tmdbId, type, season = null, episode = null) {
|
||||
try {
|
||||
const h = new URL(s.url).hostname;
|
||||
acc[h] = (acc[h] || 0) + 1;
|
||||
} catch {}
|
||||
} catch {
|
||||
// Ignore URL parsing errors
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
console.log('[4KHDHub] Final host distribution:', hostCounts);
|
||||
} catch {}
|
||||
} catch {
|
||||
// Ignore URL parsing errors
|
||||
}
|
||||
|
||||
console.log(`[4KHDHub] Returning ${streams.length} streams`);
|
||||
return streams;
|
||||
|
||||
@@ -2679,10 +2679,9 @@ const sortStreamsByQuality = (streams) => {
|
||||
|
||||
// Provider priority ordering (lower number = higher priority)
|
||||
const providerSortKeys = {
|
||||
'ShowBox': 1,
|
||||
'Xprime.tv': 2,
|
||||
'HollyMovieHD': 3,
|
||||
'Soaper TV': 4,
|
||||
'ShowBox': 1,
|
||||
'HollyMovieHD': 2,
|
||||
'Soaper TV': 3,
|
||||
// Default for unknown providers
|
||||
default: 99
|
||||
};
|
||||
|
||||
+46
-5
@@ -1,5 +1,43 @@
|
||||
const axios = require('axios');
|
||||
|
||||
// Confirmed AES-CBC decoder for VidZee encrypted link format.
|
||||
// Format (after initial atob in site script): ivBase64:cipherBase64
|
||||
// ivBase64 -> CryptoJS Base64 parsed to WordArray (IV)
|
||||
// Key: UTF-8 bytes of "qrincywincyspider" padded with nulls to 32 bytes (AES-256-CBC)
|
||||
// Cipher: base64 ciphertext, PKCS7 padding, mode CBC.
|
||||
let cryptoJs; // lazy load to avoid cost if not needed
|
||||
function decodeVidZeeToken(token, debug) {
|
||||
try {
|
||||
if (typeof token !== 'string') return null;
|
||||
if (/^https?:\/\//i.test(token)) return null; // already a URL
|
||||
// Expect pattern base64:base64 (iv:cipher)
|
||||
const raw = Buffer.from(token, 'base64').toString('utf8');
|
||||
if (!raw.includes(':')) return null;
|
||||
const [ivB64, cipherB64] = raw.split(':');
|
||||
if (!ivB64 || !cipherB64) return null;
|
||||
if (!cryptoJs) cryptoJs = require('crypto-js');
|
||||
const iv = cryptoJs.enc.Base64.parse(ivB64.trim());
|
||||
const keyStr = 'qrincywincyspider';
|
||||
// Pad key with nulls to 32 bytes
|
||||
const keyUtf8 = cryptoJs.enc.Utf8.parse(keyStr.padEnd(32, '\0'));
|
||||
const decrypted = cryptoJs.AES.decrypt(cipherB64.trim(), keyUtf8, {
|
||||
iv,
|
||||
mode: cryptoJs.mode.CBC,
|
||||
padding: cryptoJs.pad.Pkcs7
|
||||
}).toString(cryptoJs.enc.Utf8);
|
||||
if (!decrypted) return null;
|
||||
if (!/^https?:\/\//i.test(decrypted)) {
|
||||
if (debug) console.log('[VidZee] decrypted but not a URL', decrypted.slice(0,80));
|
||||
return null;
|
||||
}
|
||||
if (debug) console.log('[VidZee] AES decoded token', { tokenSnippet: token.slice(0, 24)+'...', url: decrypted.slice(0,120) });
|
||||
return decrypted.trim();
|
||||
} catch (e) {
|
||||
if (debug) console.log('[VidZee] AES decode error', e.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Removed unused parseArgs helper
|
||||
|
||||
const getVidZeeStreams = async (tmdbId, mediaType, seasonNum, episodeNum) => {
|
||||
@@ -73,20 +111,23 @@ const getVidZeeStreams = async (tmdbId, mediaType, seasonNum, episodeNum) => {
|
||||
}
|
||||
|
||||
const streams = apiSources.map(sourceItem => {
|
||||
// Prefer sourceItem.name as label, fallback to sourceItem.type, then 'VidZee Stream'
|
||||
const label = sourceItem.name || sourceItem.type || 'VidZee';
|
||||
// Ensure quality has 'p' if it's a resolution, or keep it as is
|
||||
let quality = String(label).match(/^\d+$/) ? `${label}p` : label;
|
||||
// Normalize non-numeric or ambiguous quality labels to a baseline so they survive minQuality filter
|
||||
if (!/(\d{3,4})p/.test(quality.toLowerCase())) {
|
||||
quality = '720p';
|
||||
}
|
||||
const language = sourceItem.language || sourceItem.lang;
|
||||
|
||||
let rawLink = sourceItem.link;
|
||||
const debug = process.env.VIDZEE_DEBUG === '1';
|
||||
const decoded = decodeVidZeeToken(rawLink, debug);
|
||||
if (decoded && /^https?:\/\//i.test(decoded)) {
|
||||
if (debug) console.log('[VidZee] decoded link', { beforeSample: rawLink.slice(0,40)+'...', after: decoded.slice(0,80) });
|
||||
rawLink = decoded;
|
||||
}
|
||||
return {
|
||||
name: `VidZee Server${sr} - ${quality} - ${language}`,
|
||||
title: `VidZee Server${sr} - ${quality} - ${language}`,
|
||||
url: sourceItem.link, // Use sourceItem.link for the URL
|
||||
url: rawLink,
|
||||
quality: quality,
|
||||
provider: "VidZee",
|
||||
headers: {
|
||||
|
||||
@@ -1,651 +0,0 @@
|
||||
// MoviesClub Provider
|
||||
// Extracts streams from moviesapi.club
|
||||
|
||||
const axios = require('axios');
|
||||
const JsUnpacker = require('../utils/jsunpack');
|
||||
const { config } = require('../utils/config');
|
||||
|
||||
function applyMoviesClubProxy(url) {
|
||||
try {
|
||||
const base = config.moviesclubProxy;
|
||||
if (!base || typeof base !== 'string') return url;
|
||||
// Ensure base ends with either = or a delimiter allowing raw append
|
||||
const proxied = base + encodeURIComponent(url);
|
||||
return proxied;
|
||||
} catch {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
async function getMoviesClubStreams(tmdbId, mediaType, season, episode) {
|
||||
console.log(`[moviesclub] Fetching streams for ${mediaType} tmdbId=${tmdbId} season=${season} episode=${episode}`);
|
||||
|
||||
const streams = [];
|
||||
|
||||
try {
|
||||
// Build the URL based on media type
|
||||
let url;
|
||||
if (mediaType === 'movie') {
|
||||
url = `https://moviesapi.club/movie/${tmdbId}`;
|
||||
} else if (mediaType === 'tv' && season && episode) {
|
||||
url = `https://moviesapi.club/tv/${tmdbId}-${season}-${episode}`;
|
||||
} else {
|
||||
console.log('[moviesclub] Invalid parameters for TV show');
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log(`[moviesclub] Fetching from: ${url}`);
|
||||
|
||||
// Fetch the main page
|
||||
const response = await axios.get(url, {
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||||
'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',
|
||||
'Connection': 'keep-alive',
|
||||
'Upgrade-Insecure-Requests': '1'
|
||||
}
|
||||
});
|
||||
|
||||
const html = response.data;
|
||||
console.log(`[moviesclub] Fetched ${html.length} characters`);
|
||||
|
||||
// Extract data-url from srvswitch-menu
|
||||
const dataUrlMatch = html.match(/data-url="([^"]+)"/);
|
||||
if (!dataUrlMatch) {
|
||||
console.log('[moviesclub] No data-url found in HTML');
|
||||
return [];
|
||||
}
|
||||
|
||||
const embedUrl = dataUrlMatch[1];
|
||||
console.log(`[moviesclub] Found embed URL: ${embedUrl}`);
|
||||
|
||||
// Check if this is a cdn.moviesapi.club embed URL with multiple servers
|
||||
if (embedUrl.includes('cdn.moviesapi.club')) {
|
||||
console.log('[moviesclub] Detected cdn.moviesapi.club embed URL, handling multiple servers');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Handle regular embed URLs (existing logic)
|
||||
return await handleSingleServer(embedUrl, mediaType, season, episode, tmdbId);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[moviesclub] Error:`, error.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Handle cdn.moviesapi.club URLs with multiple servers
|
||||
async function handleMultipleServers(embedUrl, mediaType, season, episode, tmdbId) {
|
||||
console.log(`[moviesclub] Handling multiple servers from: ${embedUrl}`);
|
||||
|
||||
const streams = [];
|
||||
|
||||
try {
|
||||
// Fetch the embed page
|
||||
const embedResponse = await axios.get(embedUrl, {
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||||
'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',
|
||||
'Connection': 'keep-alive'
|
||||
}
|
||||
});
|
||||
|
||||
const embedHtml = embedResponse.data;
|
||||
console.log(`[moviesclub] Fetched embed page: ${embedHtml.length} characters`);
|
||||
|
||||
// Extract server options from the HTML
|
||||
const serverMatches = embedHtml.match(/<div class="server"[^>]*data-hash="([^"]+)"[^>]*>([^<]+)<\/div>/gi);
|
||||
|
||||
if (!serverMatches || serverMatches.length === 0) {
|
||||
console.log('[moviesclub] No server options found, trying direct iframe extraction');
|
||||
// Try to extract the main iframe src
|
||||
const iframeMatch = embedHtml.match(/<iframe[^>]*src="([^"]*)"[^>]*>/i);
|
||||
if (iframeMatch) {
|
||||
const iframeSrc = iframeMatch[1];
|
||||
console.log(`[moviesclub] Found iframe src: ${iframeSrc}`);
|
||||
|
||||
// Try to decode if it's base64
|
||||
try {
|
||||
const decodedSrc = Buffer.from(iframeSrc.split('/').pop(), 'base64').toString('utf-8');
|
||||
console.log(`[moviesclub] Decoded iframe src: ${decodedSrc}`);
|
||||
|
||||
// If it's a URL, use it directly
|
||||
if (decodedSrc.startsWith('http')) {
|
||||
const rawUrl = decodedSrc;
|
||||
const stream = {
|
||||
name: `MoviesClub - Direct Stream`,
|
||||
title: `${mediaType === 'movie' ? 'Movie' : `S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`} ${tmdbId} - MoviesClub Direct`,
|
||||
url: applyMoviesClubProxy(rawUrl),
|
||||
quality: extractQualityFromUrl(rawUrl) || '720p'
|
||||
};
|
||||
streams.push(stream);
|
||||
console.log(`[moviesclub] Added direct stream from iframe`);
|
||||
return streams;
|
||||
}
|
||||
} catch (decodeError) {
|
||||
console.log(`[moviesclub] Could not decode iframe src: ${decodeError.message}`);
|
||||
}
|
||||
}
|
||||
return streams;
|
||||
}
|
||||
|
||||
console.log(`[moviesclub] Found ${serverMatches.length} server options`);
|
||||
|
||||
// Process each server
|
||||
for (let i = 0; i < serverMatches.length; i++) {
|
||||
const serverMatch = serverMatches[i];
|
||||
const hashMatch = serverMatch.match(/data-hash="([^"]+)"/);
|
||||
const nameMatch = serverMatch.match(/>([^<]+)<\/div>/);
|
||||
|
||||
if (!hashMatch || !nameMatch) {
|
||||
console.log(`[moviesclub] Could not parse server ${i + 1}, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const encodedHash = hashMatch[1];
|
||||
const serverName = nameMatch[1].trim();
|
||||
|
||||
console.log(`[moviesclub] Processing server ${i + 1}: ${serverName}`);
|
||||
|
||||
try {
|
||||
// Construct the rcp URL from the hash
|
||||
const rcpUrl = `https://cloudnestra.com/rcp/${encodedHash}`;
|
||||
console.log(`[moviesclub] RCP URL: ${rcpUrl}`);
|
||||
|
||||
// Fetch the rcp URL to get the JavaScript with prorcp URL
|
||||
const rcpResponse = await axios.get(rcpUrl, {
|
||||
timeout: 8000,
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||
'Accept-Language': 'en-US,en;q=0.5',
|
||||
'Accept-Encoding': 'gzip, deflate',
|
||||
'Connection': 'keep-alive',
|
||||
'Referer': embedUrl
|
||||
}
|
||||
});
|
||||
|
||||
const rcpHtml = rcpResponse.data;
|
||||
console.log(`[moviesclub] RCP response length: ${rcpHtml.length}`);
|
||||
|
||||
// Extract prorcp URL from the JavaScript
|
||||
const prorcpMatch = rcpHtml.match(/src:\s*['"]([^'"]*prorcp[^'"]*)['"]/i);
|
||||
if (!prorcpMatch) {
|
||||
// Try alternative patterns
|
||||
const altMatch = rcpHtml.match(/['"](\/prorcp\/[^'"]*)['"]/i);
|
||||
if (altMatch) {
|
||||
prorcpUrl = altMatch[1];
|
||||
} else {
|
||||
console.log(`[moviesclub] No prorcp URL found in rcp response`);
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
prorcpUrl = prorcpMatch[1];
|
||||
}
|
||||
|
||||
console.log(`[moviesclub] Found prorcp URL: ${prorcpUrl}`);
|
||||
|
||||
// If it's a relative URL, make it absolute
|
||||
if (prorcpUrl.startsWith('/')) {
|
||||
prorcpUrl = `https://cloudnestra.com${prorcpUrl}`;
|
||||
console.log(`[moviesclub] Converted to absolute URL: ${prorcpUrl}`);
|
||||
}
|
||||
|
||||
// Fetch the prorcp URL to get the player HTML
|
||||
const prorcpResponse = await axios.get(prorcpUrl, {
|
||||
timeout: 8000,
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||
'Accept-Language': 'en-US,en;q=0.5',
|
||||
'Accept-Encoding': 'gzip, deflate',
|
||||
'Connection': 'keep-alive',
|
||||
'Referer': rcpUrl
|
||||
}
|
||||
});
|
||||
|
||||
const prorcpHtml = prorcpResponse.data;
|
||||
console.log(`[moviesclub] Prorcp response length: ${prorcpHtml.length}`);
|
||||
|
||||
// Extract stream URL from Playerjs configuration
|
||||
const streamUrl = extractStreamFromPlayerjsUpdated(prorcpHtml);
|
||||
if (!streamUrl) {
|
||||
console.log(`[moviesclub] No stream URL found in prorcp response`);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`[moviesclub] Extracted stream URL: ${streamUrl.substring(0, 100)}...`);
|
||||
|
||||
// Create stream object
|
||||
const rawUrl = streamUrl;
|
||||
const stream = {
|
||||
name: `MoviesClub - ${serverName}`,
|
||||
title: `${mediaType === 'movie' ? 'Movie' : `S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`} ${tmdbId} - ${serverName}`,
|
||||
url: applyMoviesClubProxy(rawUrl),
|
||||
quality: extractQualityFromUrl(rawUrl) || '720p',
|
||||
headers: {}
|
||||
};
|
||||
|
||||
streams.push(stream);
|
||||
console.log(`[moviesclub] Added stream from server: ${serverName}`);
|
||||
|
||||
} catch (serverError) {
|
||||
console.error(`[moviesclub] Error processing server ${serverName}:`, serverError.message);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[moviesclub] Total streams found: ${streams.length}`);
|
||||
return streams;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[moviesclub] Error in handleMultipleServers:`, error.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Handle regular embed URLs (existing logic)
|
||||
async function handleSingleServer(embedUrl, mediaType, season, episode, tmdbId) {
|
||||
console.log(`[moviesclub] Handling single server from: ${embedUrl}`);
|
||||
|
||||
const streams = [];
|
||||
|
||||
try {
|
||||
// Fetch the embed page
|
||||
const embedResponse = await axios.get(embedUrl, {
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||||
'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',
|
||||
'Connection': 'keep-alive'
|
||||
}
|
||||
});
|
||||
|
||||
const embedHtml = embedResponse.data;
|
||||
console.log(`[moviesclub] Fetched embed page: ${embedHtml.length} characters`);
|
||||
|
||||
// Look for packed JavaScript using JsUnpacker.detect()
|
||||
const scriptTags = embedHtml.match(/<script[^>]*>[\s\S]*?<\/script>/gi) || [];
|
||||
let packedCode = null;
|
||||
|
||||
for (const scriptTag of scriptTags) {
|
||||
// Extract script content
|
||||
const scriptContent = scriptTag.replace(/<\/?script[^>]*>/gi, '').trim();
|
||||
|
||||
if (scriptContent) {
|
||||
// Create unpacker instance and check if it's packed
|
||||
const unpacker = new JsUnpacker(scriptContent);
|
||||
|
||||
if (unpacker.detect()) {
|
||||
console.log(`[moviesclub] Found packed JavaScript in script tag, length: ${scriptContent.length}`);
|
||||
packedCode = scriptContent;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!packedCode) {
|
||||
console.log('[moviesclub] No packed JavaScript found');
|
||||
return [];
|
||||
}
|
||||
|
||||
// Unpack using JsUnpacker
|
||||
const unpacker = new JsUnpacker(packedCode);
|
||||
const unpackedCode = unpacker.unpack();
|
||||
|
||||
if (!unpackedCode) {
|
||||
console.log('[moviesclub] Failed to unpack JavaScript');
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log(`[moviesclub] Successfully unpacked code, length: ${unpackedCode.length}`);
|
||||
|
||||
// Parse the player configuration from unpacked code
|
||||
const playerConfig = parsePlayerConfig(unpackedCode);
|
||||
if (!playerConfig) {
|
||||
console.log('[moviesclub] Failed to parse player configuration');
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log('[moviesclub] Parsed player config:', playerConfig);
|
||||
|
||||
// Extract streams from the configuration
|
||||
if (playerConfig.file) {
|
||||
const rawUrl = playerConfig.file;
|
||||
const stream = {
|
||||
name: `MoviesClub - ${playerConfig.title || 'Server 1'}`,
|
||||
title: playerConfig.title || `${mediaType === 'movie' ? 'Movie' : `S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`} ${tmdbId} - MoviesClub`,
|
||||
url: applyMoviesClubProxy(rawUrl),
|
||||
quality: extractQualityFromUrl(rawUrl) || '720p',
|
||||
// Always include vidora headers for single-server results to prevent 403 playback issues
|
||||
headers: {
|
||||
Origin: 'https://vidora.stream',
|
||||
Referer: 'https://vidora.stream/'
|
||||
}
|
||||
};
|
||||
|
||||
// Add poster if available
|
||||
if (playerConfig.poster) {
|
||||
stream.poster = playerConfig.poster;
|
||||
}
|
||||
|
||||
// Add subtitles if available
|
||||
if (playerConfig.subtitle) {
|
||||
stream.subtitles = parseSubtitles(playerConfig.subtitle);
|
||||
}
|
||||
|
||||
streams.push(stream);
|
||||
}
|
||||
|
||||
console.log(`[moviesclub] Found ${streams.length} streams`);
|
||||
return streams;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[moviesclub] Error in handleSingleServer:`, error.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Extract stream URL from Playerjs configuration in HTML
|
||||
function extractStreamFromPlayerjs(html) {
|
||||
try {
|
||||
// Look for the Playerjs constructor call
|
||||
const playerMatch = html.match(/new Playerjs\(\s*(\{[^}]*\})\s*\)/);
|
||||
if (!playerMatch) {
|
||||
console.log('[moviesclub] No Playerjs constructor found in HTML');
|
||||
return null;
|
||||
}
|
||||
|
||||
const configStr = playerMatch[1];
|
||||
console.log(`[moviesclub] Found Playerjs config: ${configStr.substring(0, 200)}...`);
|
||||
|
||||
// Extract the file URL from the configuration
|
||||
const fileMatch = configStr.match(/file:\s*['"]([^'"]+)['"]/);
|
||||
if (!fileMatch) {
|
||||
console.log('[moviesclub] No file URL found in Playerjs config');
|
||||
return null;
|
||||
}
|
||||
|
||||
const streamUrl = fileMatch[1];
|
||||
console.log(`[moviesclub] Extracted stream URL from Playerjs: ${streamUrl.substring(0, 100)}...`);
|
||||
|
||||
return streamUrl;
|
||||
|
||||
} catch (error) {
|
||||
console.error('[moviesclub] Error extracting stream from Playerjs:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Extract streams from server response
|
||||
function extractStreamsFromServer(serverData, serverName, mediaType, season, episode, tmdbId) {
|
||||
const streams = [];
|
||||
|
||||
try {
|
||||
// Handle different response formats
|
||||
if (typeof serverData === 'string') {
|
||||
// Try to parse as JSON
|
||||
try {
|
||||
serverData = JSON.parse(serverData);
|
||||
} catch (parseError) {
|
||||
console.log(`[moviesclub] Server response is not JSON, treating as direct URL: ${serverData.substring(0, 100)}`);
|
||||
// Treat as direct stream URL
|
||||
if (serverData.startsWith('http')) {
|
||||
const rawUrl = serverData;
|
||||
const stream = {
|
||||
name: `MoviesClub - ${serverName}`,
|
||||
title: `${mediaType === 'movie' ? 'Movie' : `S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`} ${tmdbId} - ${serverName}`,
|
||||
url: applyMoviesClubProxy(rawUrl),
|
||||
quality: extractQualityFromUrl(rawUrl) || '720p',
|
||||
headers: serverData.includes('vidora.stream') ? { Origin: 'https://vidora.stream', Referer: 'https://vidora.stream/' } : {}
|
||||
};
|
||||
streams.push(stream);
|
||||
}
|
||||
return streams;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle JSON response
|
||||
if (serverData && typeof serverData === 'object') {
|
||||
// Look for common stream URL patterns
|
||||
const possibleKeys = ['file', 'url', 'src', 'stream', 'video', 'source'];
|
||||
|
||||
for (const key of possibleKeys) {
|
||||
if (serverData[key] && typeof serverData[key] === 'string' && serverData[key].startsWith('http')) {
|
||||
console.log(`[moviesclub] Found stream URL in key '${key}': ${serverData[key].substring(0, 100)}`);
|
||||
|
||||
const rawUrl = serverData[key];
|
||||
const stream = {
|
||||
name: `MoviesClub - ${serverName}`,
|
||||
title: `${mediaType === 'movie' ? 'Movie' : `S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`} ${tmdbId} - ${serverName}`,
|
||||
url: applyMoviesClubProxy(rawUrl),
|
||||
quality: extractQualityFromUrl(rawUrl) || '720p',
|
||||
headers: rawUrl.includes('vidora.stream') ? { Origin: 'https://vidora.stream', Referer: 'https://vidora.stream/' } : {}
|
||||
};
|
||||
|
||||
// Add poster if available
|
||||
if (serverData.poster) {
|
||||
stream.poster = serverData.poster;
|
||||
}
|
||||
|
||||
// Add subtitles if available
|
||||
if (serverData.subtitle || serverData.subtitles) {
|
||||
const subtitleData = serverData.subtitle || serverData.subtitles;
|
||||
stream.subtitles = parseSubtitles(subtitleData);
|
||||
}
|
||||
|
||||
streams.push(stream);
|
||||
break; // Only take the first valid stream URL
|
||||
}
|
||||
}
|
||||
|
||||
// If no direct URL found, check for nested objects
|
||||
if (streams.length === 0) {
|
||||
for (const key of possibleKeys) {
|
||||
if (serverData[key] && typeof serverData[key] === 'object') {
|
||||
const nestedStreams = extractStreamsFromServer(serverData[key], serverName, mediaType, season, episode, tmdbId);
|
||||
streams.push(...nestedStreams);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[moviesclub] Error extracting streams from server ${serverName}:`, error.message);
|
||||
}
|
||||
|
||||
return streams;
|
||||
}
|
||||
|
||||
// Extract stream URL from Playerjs configuration in HTML
|
||||
function extractStreamFromPlayerjs(html) {
|
||||
try {
|
||||
// Look for the Playerjs constructor call
|
||||
const playerMatch = html.match(/new Playerjs\(\s*(\{[^}]*\})\s*\)/);
|
||||
if (!playerMatch) {
|
||||
console.log('[moviesclub] No Playerjs constructor found in HTML');
|
||||
return null;
|
||||
}
|
||||
|
||||
const configStr = playerMatch[1];
|
||||
console.log(`[moviesclub] Found Playerjs config: ${configStr.substring(0, 200)}...`);
|
||||
|
||||
// Extract the file URL from the configuration
|
||||
const fileMatch = configStr.match(/file:\s*['"]([^'"]+)['"]/);
|
||||
if (!fileMatch) {
|
||||
console.log('[moviesclub] No file URL found in Playerjs config');
|
||||
return null;
|
||||
}
|
||||
|
||||
const streamUrl = fileMatch[1];
|
||||
console.log(`[moviesclub] Extracted stream URL from Playerjs: ${streamUrl.substring(0, 100)}...`);
|
||||
|
||||
return streamUrl;
|
||||
|
||||
} catch (error) {
|
||||
console.error('[moviesclub] Error extracting stream from Playerjs:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse player configuration from unpacked JavaScript
|
||||
function parsePlayerConfig(unpackedCode) {
|
||||
try {
|
||||
// Look for the player configuration object
|
||||
const configMatch = unpackedCode.match(/new Playerjs\(\s*(\{[^}]*\})\s*\)/);
|
||||
if (!configMatch) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Extract the configuration object
|
||||
const configStr = configMatch[1];
|
||||
|
||||
// Parse the configuration (this is a simple parser for the expected format)
|
||||
const config = {};
|
||||
|
||||
// Extract key-value pairs
|
||||
const pairs = configStr.match(/(\w+):"([^"]*)"/g) || [];
|
||||
pairs.forEach(pair => {
|
||||
const [, key, value] = pair.match(/(\w+):"([^"]*)"/);
|
||||
config[key] = value;
|
||||
});
|
||||
|
||||
// Also handle non-quoted values
|
||||
const numberPairs = configStr.match(/(\w+):(\d+(?:\.\d+)?)/g) || [];
|
||||
numberPairs.forEach(pair => {
|
||||
const [, key, value] = pair.match(/(\w+):(\d+(?:\.\d+)?)/);
|
||||
config[key] = parseFloat(value);
|
||||
});
|
||||
|
||||
return config;
|
||||
} catch (error) {
|
||||
console.error('[moviesclub] Error parsing player config:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse subtitles from the subtitle string
|
||||
function parseSubtitles(subtitleStr) {
|
||||
if (!subtitleStr) return [];
|
||||
|
||||
const subtitles = [];
|
||||
const subtitleMatches = subtitleStr.match(/\[([^\]]+)\]([^,\[]*)/g);
|
||||
|
||||
if (subtitleMatches) {
|
||||
subtitleMatches.forEach(match => {
|
||||
const [, language, url] = match.match(/\[([^\]]+)\]([^,\[]*)/);
|
||||
if (url && url.trim()) {
|
||||
subtitles.push({
|
||||
language: language.trim(),
|
||||
url: url.trim(),
|
||||
type: 'vtt'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return subtitles;
|
||||
}
|
||||
|
||||
// Extract quality from URL
|
||||
function extractQualityFromUrl(url) {
|
||||
if (!url) return '720p';
|
||||
|
||||
const qualityPatterns = [
|
||||
{ pattern: /1080p|1080/i, quality: '1080p' },
|
||||
{ pattern: /720p|720/i, quality: '720p' },
|
||||
{ pattern: /480p|480/i, quality: '480p' },
|
||||
{ pattern: /360p|360/i, quality: '360p' },
|
||||
{ pattern: /2160p|4k|uhd/i, quality: '2160p' }
|
||||
];
|
||||
|
||||
for (const { pattern, quality } of qualityPatterns) {
|
||||
if (pattern.test(url)) {
|
||||
return quality;
|
||||
}
|
||||
}
|
||||
|
||||
return '720p';
|
||||
}
|
||||
|
||||
// Extract stream URL from Playerjs configuration in HTML (updated version)
|
||||
function extractStreamFromPlayerjsUpdated(html) {
|
||||
try {
|
||||
// Look for the Playerjs constructor call
|
||||
const playerMatch = html.match(/new Playerjs\(\s*(\{[^}]*\})\s*\)/);
|
||||
if (!playerMatch) {
|
||||
console.log('[moviesclub] No Playerjs constructor found in HTML');
|
||||
return null;
|
||||
}
|
||||
|
||||
const configStr = playerMatch[1];
|
||||
console.log(`[moviesclub] Found Playerjs config: ${configStr.substring(0, 200)}...`);
|
||||
|
||||
// Extract the file URL from the configuration
|
||||
let streamUrl = null;
|
||||
|
||||
// Try multiple patterns to extract the file URL
|
||||
const patterns = [
|
||||
/file:\s*['"]([^'"]+)['"]/,
|
||||
/file:\s*([^,\s}]+)/,
|
||||
/file\s*:\s*['"]([^'"]+)['"]/,
|
||||
/file\s*:\s*([^,\s}]+)/
|
||||
];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const match = configStr.match(pattern);
|
||||
if (match && match[1] && match[1].trim() !== '' && match[1].trim() !== '""') {
|
||||
streamUrl = match[1].replace(/['"]/g, '').trim();
|
||||
console.log(`[moviesclub] Found stream URL with pattern: ${streamUrl.substring(0, 100)}...`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no URL found in config, look for streaming URLs in the HTML
|
||||
if (!streamUrl) {
|
||||
console.log('[moviesclub] No file URL in Playerjs config, searching HTML for streaming URLs...');
|
||||
|
||||
// Look for m3u8 URLs
|
||||
const m3u8Match = html.match(/https?:\/\/[^'"\s]+\.m3u8[^'"\s]*/i);
|
||||
if (m3u8Match) {
|
||||
streamUrl = m3u8Match[0];
|
||||
console.log(`[moviesclub] Found m3u8 URL in HTML: ${streamUrl.substring(0, 100)}...`);
|
||||
}
|
||||
|
||||
// Look for other streaming URLs
|
||||
if (!streamUrl) {
|
||||
const streamMatch = html.match(/https?:\/\/[^'"\s]+(?:\.mp4|\.m3u8|\.ts)[^'"\s]*/i);
|
||||
if (streamMatch) {
|
||||
streamUrl = streamMatch[0];
|
||||
console.log(`[moviesclub] Found streaming URL in HTML: ${streamUrl.substring(0, 100)}...`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!streamUrl) {
|
||||
console.log('[moviesclub] No file URL found in Playerjs config after trying all patterns');
|
||||
console.log('[moviesclub] Config string:', configStr.substring(0, 500));
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(`[moviesclub] Extracted stream URL from Playerjs: ${streamUrl.substring(0, 100)}...`);
|
||||
|
||||
return streamUrl;
|
||||
|
||||
} catch (error) {
|
||||
console.error('[moviesclub] Error extracting stream from Playerjs:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getMoviesClubStreams
|
||||
};
|
||||
@@ -13,9 +13,7 @@ const providerFunctionMap = {
|
||||
'MP4Hydra.js': 'getMP4HydraStreams',
|
||||
'VidZee.js': 'getVidZeeStreams',
|
||||
'vixsrc.js': 'getVixsrcStreams',
|
||||
'xprime.js': 'getXprimeStreams',
|
||||
'uhdmovies.js': 'getUHDMoviesStreams',
|
||||
'moviesclub.js': 'getMoviesClubStreams',
|
||||
};
|
||||
|
||||
// Stats for debug endpoint
|
||||
@@ -92,46 +90,6 @@ function createFetchFunction(providerInfo) {
|
||||
lastCookieStats.remainingMB = global.currentRequestUserCookieRemainingMB;
|
||||
}
|
||||
global.currentRequestConfig = previousConfig || {};
|
||||
} else if (providerInfo.name === 'xprime') {
|
||||
// Special case for Xprime with TMDB metadata lookup
|
||||
const { getTmdbApiKey } = require('../utils/tmdbKey');
|
||||
const tmdbApiKey = getTmdbApiKey();
|
||||
if (!tmdbApiKey) {
|
||||
console.warn('[registry] xprime skipped: TMDB API key missing');
|
||||
return [];
|
||||
}
|
||||
// Fetch minimal TMDB metadata for title + year
|
||||
let title = null; let year = null;
|
||||
try {
|
||||
const axios = require('axios');
|
||||
const metaUrl = `https://api.themoviedb.org/3/${mediaType}/${ctx.tmdbId}?api_key=${tmdbApiKey}`;
|
||||
const { data: meta } = await axios.get(metaUrl, { timeout: 8000 });
|
||||
title = mediaType === 'movie' ? (meta.title || meta.original_title) : (meta.name || meta.original_name);
|
||||
const dateStr = mediaType === 'movie' ? meta.release_date : meta.first_air_date;
|
||||
if (dateStr) year = dateStr.split('-')[0];
|
||||
} catch (mErr) {
|
||||
console.warn('[registry] xprime metadata fetch failed:', mErr.message);
|
||||
return [];
|
||||
}
|
||||
if (!title || !year) {
|
||||
console.log('[registry] xprime missing title/year after TMDB lookup');
|
||||
return [];
|
||||
}
|
||||
result = await module[funcName](title, year, mediaType, ctx.season || null, ctx.episode || null);
|
||||
// Normalize quality labels
|
||||
if (Array.isArray(result)) {
|
||||
result = result.map(s => {
|
||||
let q = s.quality || 'Unknown';
|
||||
if (!/(\d{3,4})p/.test(q.toLowerCase())) {
|
||||
const qLower = q.toLowerCase();
|
||||
if (qLower.includes('fhd') || qLower.includes('full')) q = '1080p';
|
||||
else if (qLower === 'hd' || qLower.includes('720')) q = '720p';
|
||||
else if (qLower.includes('sd') || qLower.includes('480')) q = '480p';
|
||||
else if (qLower.includes('cam')) q = '360p';
|
||||
}
|
||||
return { ...s, quality: q, name: s.name || 'Xprime', provider: 'xprime' };
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Standard provider call
|
||||
result = await module[funcName](ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null);
|
||||
|
||||
+123
-124
@@ -6,7 +6,8 @@
|
||||
// Constants
|
||||
const TMDB_API_KEY = "68e094699525b18a70bab2f86b1fa706";
|
||||
const BASE_URL = 'https://vixsrc.to';
|
||||
|
||||
let vixsrcUrl;
|
||||
let subtitleApiUrl;
|
||||
// Helper function to make HTTP requests with default headers
|
||||
function makeRequest(url, options = {}) {
|
||||
const defaultHeaders = {
|
||||
@@ -23,24 +24,24 @@ function makeRequest(url, options = {}) {
|
||||
headers: defaultHeaders,
|
||||
...options
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
return response;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`[Vixsrc] Request failed for ${url}: ${error.message}`);
|
||||
throw error;
|
||||
});
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
return response;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`[Vixsrc] Request failed for ${url}: ${error.message}`);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to get TMDB info
|
||||
function getTmdbInfo(tmdbId, mediaType) {
|
||||
const url = `https://api.themoviedb.org/3/${mediaType === 'tv' ? 'tv' : 'movie'}/${tmdbId}?api_key=${TMDB_API_KEY}`;
|
||||
|
||||
|
||||
return makeRequest(url)
|
||||
.then(response => response.json())
|
||||
.then(response => response.json())
|
||||
.then(data => ({ data }));
|
||||
}
|
||||
|
||||
@@ -51,8 +52,6 @@ function getTmdbInfo(tmdbId, mediaType) {
|
||||
|
||||
// Helper function to extract stream URL from Vixsrc page
|
||||
function extractStreamFromPage(url, contentType, contentId, seasonNum, episodeNum) {
|
||||
let vixsrcUrl;
|
||||
let subtitleApiUrl;
|
||||
|
||||
if (contentType === 'movie') {
|
||||
vixsrcUrl = `${BASE_URL}/movie/${contentId}`;
|
||||
@@ -64,152 +63,152 @@ function extractStreamFromPage(url, contentType, contentId, seasonNum, episodeNu
|
||||
|
||||
console.log(`[Vixsrc] Fetching: ${vixsrcUrl}`);
|
||||
|
||||
return makeRequest(vixsrcUrl, {
|
||||
return makeRequest(vixsrcUrl, {
|
||||
headers: {
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
|
||||
}
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
console.log(`[Vixsrc] HTML length: ${html.length} characters`);
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
console.log(`[Vixsrc] HTML length: ${html.length} characters`);
|
||||
|
||||
let masterPlaylistUrl = null;
|
||||
let masterPlaylistUrl = null;
|
||||
|
||||
// Method 1: Look for window.masterPlaylist (primary method)
|
||||
if (html.includes('window.masterPlaylist')) {
|
||||
console.log('[Vixsrc] Found window.masterPlaylist');
|
||||
// Method 1: Look for window.masterPlaylist (primary method)
|
||||
if (html.includes('window.masterPlaylist')) {
|
||||
console.log('[Vixsrc] Found window.masterPlaylist');
|
||||
|
||||
const urlMatch = html.match(/url:\s*['"]([^'"]+)['"]/);
|
||||
const tokenMatch = html.match(/['"]?token['"]?\s*:\s*['"]([^'"]+)['"]/);
|
||||
const expiresMatch = html.match(/['"]?expires['"]?\s*:\s*['"]([^'"]+)['"]/);
|
||||
const urlMatch = html.match(/url:\s*['"]([^'"]+)['"]/);
|
||||
const tokenMatch = html.match(/['"]?token['"]?\s*:\s*['"]([^'"]+)['"]/);
|
||||
const expiresMatch = html.match(/['"]?expires['"]?\s*:\s*['"]([^'"]+)['"]/);
|
||||
|
||||
if (urlMatch && tokenMatch && expiresMatch) {
|
||||
const baseUrl = urlMatch[1];
|
||||
const token = tokenMatch[1];
|
||||
const expires = expiresMatch[1];
|
||||
if (urlMatch && tokenMatch && expiresMatch) {
|
||||
const baseUrl = urlMatch[1];
|
||||
const token = tokenMatch[1];
|
||||
const expires = expiresMatch[1];
|
||||
|
||||
console.log('[Vixsrc] Extracted tokens:');
|
||||
console.log(` - Base URL: ${baseUrl}`);
|
||||
console.log(` - Token: ${token.substring(0, 20)}...`);
|
||||
console.log(` - Expires: ${expires}`);
|
||||
console.log('[Vixsrc] Extracted tokens:');
|
||||
console.log(` - Base URL: ${baseUrl}`);
|
||||
console.log(` - Token: ${token.substring(0, 20)}...`);
|
||||
console.log(` - Expires: ${expires}`);
|
||||
|
||||
// Construct the master playlist URL
|
||||
if (baseUrl.includes('?b=1')) {
|
||||
masterPlaylistUrl = `${baseUrl}&token=${token}&expires=${expires}&h=1&lang=en`;
|
||||
} else {
|
||||
masterPlaylistUrl = `${baseUrl}?token=${token}&expires=${expires}&h=1&lang=en`;
|
||||
// Construct the master playlist URL
|
||||
if (baseUrl.includes('?b=1')) {
|
||||
masterPlaylistUrl = `${baseUrl}&token=${token}&expires=${expires}&h=1&lang=en`;
|
||||
} else {
|
||||
masterPlaylistUrl = `${baseUrl}?token=${token}&expires=${expires}&h=1&lang=en`;
|
||||
}
|
||||
|
||||
console.log(`[Vixsrc] Constructed master playlist URL: ${masterPlaylistUrl}`);
|
||||
}
|
||||
|
||||
console.log(`[Vixsrc] Constructed master playlist URL: ${masterPlaylistUrl}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Method 2: Look for direct .m3u8 URLs
|
||||
if (!masterPlaylistUrl) {
|
||||
const m3u8Match = html.match(/(https?:\/\/[^'"\s]+\.m3u8[^'"\s]*)/);
|
||||
if (m3u8Match) {
|
||||
masterPlaylistUrl = m3u8Match[1];
|
||||
console.log('[Vixsrc] Found direct .m3u8 URL:', masterPlaylistUrl);
|
||||
// Method 2: Look for direct .m3u8 URLs
|
||||
if (!masterPlaylistUrl) {
|
||||
const m3u8Match = html.match(/(https?:\/\/[^'"\s]+\.m3u8[^'"\s]*)/);
|
||||
if (m3u8Match) {
|
||||
masterPlaylistUrl = m3u8Match[1];
|
||||
console.log('[Vixsrc] Found direct .m3u8 URL:', masterPlaylistUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Method 3: Look for stream URLs in script tags
|
||||
if (!masterPlaylistUrl) {
|
||||
const scriptMatches = html.match(/<script[^>]*>(.*?)<\/script>/gs);
|
||||
if (scriptMatches) {
|
||||
for (const script of scriptMatches) {
|
||||
const streamMatch = script.match(/['"]?(https?:\/\/[^'"\s]+(?:\.m3u8|playlist)[^'"\s]*)/);
|
||||
if (streamMatch) {
|
||||
masterPlaylistUrl = streamMatch[1];
|
||||
console.log('[Vixsrc] Found stream in script:', masterPlaylistUrl);
|
||||
break;
|
||||
// Method 3: Look for stream URLs in script tags
|
||||
if (!masterPlaylistUrl) {
|
||||
const scriptMatches = html.match(/<script[^>]*>(.*?)<\/script>/gs);
|
||||
if (scriptMatches) {
|
||||
for (const script of scriptMatches) {
|
||||
const streamMatch = script.match(/['"]?(https?:\/\/[^'"\s]+(?:\.m3u8|playlist)[^'"\s]*)/);
|
||||
if (streamMatch) {
|
||||
masterPlaylistUrl = streamMatch[1];
|
||||
console.log('[Vixsrc] Found stream in script:', masterPlaylistUrl);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!masterPlaylistUrl) {
|
||||
console.log('[Vixsrc] No master playlist URL found');
|
||||
return null;
|
||||
}
|
||||
if (!masterPlaylistUrl) {
|
||||
console.log('[Vixsrc] No master playlist URL found');
|
||||
return null;
|
||||
}
|
||||
|
||||
return { masterPlaylistUrl, subtitleApiUrl };
|
||||
});
|
||||
return { masterPlaylistUrl, subtitleApiUrl };
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to get subtitles
|
||||
function getSubtitles(subtitleApiUrl) {
|
||||
return makeRequest(subtitleApiUrl)
|
||||
.then(response => response.json())
|
||||
.then(subtitleData => {
|
||||
// Find English subtitle track (same logic as original)
|
||||
let subtitleTrack = subtitleData.find(track =>
|
||||
track.display.includes('English') && (track.encoding === 'ASCII' || track.encoding === 'UTF-8')
|
||||
);
|
||||
.then(response => response.json())
|
||||
.then(subtitleData => {
|
||||
// Find English subtitle track (same logic as original)
|
||||
let subtitleTrack = subtitleData.find(track =>
|
||||
track.display.includes('English') && (track.encoding === 'ASCII' || track.encoding === 'UTF-8')
|
||||
);
|
||||
|
||||
if (!subtitleTrack) {
|
||||
subtitleTrack = subtitleData.find(track => track.display.includes('English') && track.encoding === 'CP1252');
|
||||
}
|
||||
if (!subtitleTrack) {
|
||||
subtitleTrack = subtitleData.find(track => track.display.includes('English') && track.encoding === 'CP1252');
|
||||
}
|
||||
|
||||
if (!subtitleTrack) {
|
||||
subtitleTrack = subtitleData.find(track => track.display.includes('English') && track.encoding === 'CP1250');
|
||||
}
|
||||
if (!subtitleTrack) {
|
||||
subtitleTrack = subtitleData.find(track => track.display.includes('English') && track.encoding === 'CP1250');
|
||||
}
|
||||
|
||||
if (!subtitleTrack) {
|
||||
subtitleTrack = subtitleData.find(track => track.display.includes('English') && track.encoding === 'CP850');
|
||||
}
|
||||
if (!subtitleTrack) {
|
||||
subtitleTrack = subtitleData.find(track => track.display.includes('English') && track.encoding === 'CP850');
|
||||
}
|
||||
|
||||
const subtitles = subtitleTrack ? subtitleTrack.url : '';
|
||||
console.log(subtitles ? `[Vixsrc] Found subtitles: ${subtitles}` : '[Vixsrc] No English subtitles found');
|
||||
return subtitles;
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('[Vixsrc] Subtitle fetch failed:', error.message);
|
||||
return '';
|
||||
});
|
||||
const subtitles = subtitleTrack ? subtitleTrack.url : '';
|
||||
console.log(subtitles ? `[Vixsrc] Found subtitles: ${subtitles}` : '[Vixsrc] No English subtitles found');
|
||||
return subtitles;
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('[Vixsrc] Subtitle fetch failed:', error.message);
|
||||
return '';
|
||||
});
|
||||
}
|
||||
|
||||
// Main function to get streams
|
||||
function getVixsrcStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) {
|
||||
console.log(`[Vixsrc] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}`);
|
||||
|
||||
|
||||
return getTmdbInfo(tmdbId, mediaType)
|
||||
.then(() => extractStreamFromPage(null, mediaType, tmdbId, seasonNum, episodeNum))
|
||||
.then(streamData => {
|
||||
if (!streamData) {
|
||||
console.log('[Vixsrc] No stream data found');
|
||||
.then(() => extractStreamFromPage(null, mediaType, tmdbId, seasonNum, episodeNum))
|
||||
.then(streamData => {
|
||||
if (!streamData) {
|
||||
console.log('[Vixsrc] No stream data found');
|
||||
return [];
|
||||
}
|
||||
|
||||
const { masterPlaylistUrl, subtitleApiUrl } = streamData;
|
||||
|
||||
// Return single master playlist with Auto quality
|
||||
console.log('[Vixsrc] Returning master playlist with Auto quality...');
|
||||
|
||||
// Get subtitles
|
||||
return getSubtitles(subtitleApiUrl)
|
||||
.then(subtitles => {
|
||||
// Return single stream with master playlist
|
||||
const tmdbStreams = [{
|
||||
name: "Vixsrc",
|
||||
title: "Auto Quality Stream",
|
||||
url: masterPlaylistUrl,
|
||||
quality: '1080p',
|
||||
provider: 'Vidsrc',
|
||||
headers: {
|
||||
'Referer': vixsrcUrl,
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
|
||||
}
|
||||
}];
|
||||
|
||||
console.log('[Vixsrc] Successfully processed 1 stream with Auto quality');
|
||||
return tmdbStreams;
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`[Vixsrc] Error in getVixsrcStreams: ${error.message}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
const { masterPlaylistUrl, subtitleApiUrl } = streamData;
|
||||
|
||||
// Return single master playlist with Auto quality
|
||||
console.log('[Vixsrc] Returning master playlist with Auto quality...');
|
||||
|
||||
// Get subtitles
|
||||
return getSubtitles(subtitleApiUrl)
|
||||
.then(subtitles => {
|
||||
// Return single stream with master playlist
|
||||
const tmdbStreams = [{
|
||||
name: "Vixsrc",
|
||||
title: "Auto Quality Stream",
|
||||
url: masterPlaylistUrl,
|
||||
quality: '1080p',
|
||||
provider: 'Vidsrc',
|
||||
headers: {
|
||||
'Referer': BASE_URL,
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
|
||||
}
|
||||
}];
|
||||
|
||||
console.log('[Vixsrc] Successfully processed 1 stream with Auto quality');
|
||||
return tmdbStreams;
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`[Vixsrc] Error in getVixsrcStreams: ${error.message}`);
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { getVixsrcStreams };
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
// xprime.js (simplified & cleaned)
|
||||
require('dotenv').config();
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const RETRY_DELAY_MS_XPRIME = 1000;
|
||||
const MAX_RETRIES_XPRIME = 3;
|
||||
|
||||
// Cache directory (Vercel ephemeral vs local)
|
||||
const CACHE_DIR = process.env.VERCEL ? path.join('/tmp', '.cache') : path.join(__dirname, '.cache');
|
||||
|
||||
async function ensureCacheDir(dirPath) {
|
||||
try { await fs.mkdir(dirPath, { recursive: true }); } catch (_) {
|
||||
// swallow errors creating cache dir (ephemeral env or permission)
|
||||
}
|
||||
}
|
||||
|
||||
async function getFromCache(cacheKey, subDir = '') {
|
||||
if (process.env.DISABLE_CACHE === 'true') return null;
|
||||
try {
|
||||
const fullDir = path.join(CACHE_DIR, subDir);
|
||||
const filePath = path.join(fullDir, cacheKey);
|
||||
const data = await fs.readFile(filePath, 'utf8');
|
||||
return data;
|
||||
} catch (_) { return null; }
|
||||
}
|
||||
|
||||
async function saveToCache(cacheKey, data, subDir = '') {
|
||||
if (process.env.DISABLE_CACHE === 'true') return;
|
||||
try {
|
||||
const fullDir = path.join(CACHE_DIR, subDir);
|
||||
await fs.mkdir(fullDir, { recursive: true });
|
||||
const filePath = path.join(fullDir, cacheKey);
|
||||
await fs.writeFile(filePath, data, 'utf8');
|
||||
} catch (_) {
|
||||
// ignore cache write errors (non-fatal)
|
||||
}
|
||||
}
|
||||
|
||||
const BROWSER_HEADERS_XPRIME = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36',
|
||||
'Accept': '*/*',
|
||||
'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Pragma': 'no-cache',
|
||||
'Sec-Ch-Ua': '"Chromium";v="136", "Google Chrome";v="136", "Not.A/Brand";v="99"',
|
||||
'Sec-Ch-Ua-Mobile': '?0',
|
||||
'Sec-Ch-Ua-Platform': '"Windows"',
|
||||
'Connection': 'keep-alive'
|
||||
};
|
||||
|
||||
async function fetchWithRetry(url, options, maxRetries = MAX_RETRIES_XPRIME) {
|
||||
const { default: fetch } = await import('node-fetch');
|
||||
let lastError;
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
const res = await fetch(url, options);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
return res;
|
||||
} catch (err) {
|
||||
lastError = err;
|
||||
if (attempt < maxRetries) {
|
||||
await new Promise(r => setTimeout(r, RETRY_DELAY_MS_XPRIME * Math.pow(2, attempt - 1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
async function fetchStreamSize(url) {
|
||||
const subDir = 'xprime_stream_sizes';
|
||||
const hash = crypto.createHash('md5').update(url).digest('hex');
|
||||
const key = `${hash}.txt`;
|
||||
const cached = await getFromCache(key, subDir);
|
||||
if (cached) return cached;
|
||||
if (url.toLowerCase().includes('.m3u8')) {
|
||||
await saveToCache(key, 'Playlist (size N/A)', subDir);
|
||||
return 'Playlist (size N/A)';
|
||||
}
|
||||
try {
|
||||
const { default: fetch } = await import('node-fetch');
|
||||
const controller = new AbortController();
|
||||
const t = setTimeout(() => controller.abort(), 5000);
|
||||
let size = 'Unknown size';
|
||||
try {
|
||||
const head = await fetch(url, { method: 'HEAD', signal: controller.signal });
|
||||
const cl = head.headers.get('content-length');
|
||||
if (cl) {
|
||||
const bytes = parseInt(cl, 10);
|
||||
if (!isNaN(bytes)) {
|
||||
if (bytes < 1024) size = `${bytes} B`;
|
||||
else if (bytes < 1024 * 1024) size = `${(bytes / 1024).toFixed(2)} KB`;
|
||||
else if (bytes < 1024 * 1024 * 1024) size = `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
||||
else size = `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
||||
}
|
||||
}
|
||||
} finally { clearTimeout(t); }
|
||||
await saveToCache(key, size, subDir);
|
||||
return size;
|
||||
} catch (_) {
|
||||
await saveToCache(key, 'Unknown size', subDir);
|
||||
return 'Unknown size';
|
||||
}
|
||||
}
|
||||
|
||||
async function getXprimeStreams(title, year, type, seasonNum, episodeNum) {
|
||||
if (!title || !year) return [];
|
||||
const encoded = encodeURIComponent(title);
|
||||
let apiUrl;
|
||||
if (type === 'movie') {
|
||||
apiUrl = `https://backend.xprime.tv/primebox?name=${encoded}&year=${year}&fallback_year=${year}`;
|
||||
} else if (type === 'tv') {
|
||||
if (seasonNum == null || episodeNum == null) return [];
|
||||
apiUrl = `https://backend.xprime.tv/primebox?name=${encoded}&year=${year}&fallback_year=${year}&season=${seasonNum}&episode=${episodeNum}`;
|
||||
} else return [];
|
||||
|
||||
console.log(`[Xprime.tv] Fetch attempt '${title}' (${year}) type=${type}`);
|
||||
|
||||
let resJson;
|
||||
try {
|
||||
const r = await fetchWithRetry(apiUrl, { headers: BROWSER_HEADERS_XPRIME });
|
||||
resJson = await r.json();
|
||||
} catch (e) {
|
||||
console.error('[Xprime.tv] Fetch failed:', e.message);
|
||||
return [];
|
||||
}
|
||||
|
||||
const streams = [];
|
||||
const processItem = (item) => {
|
||||
if (!item || item.error || !item.streams) return;
|
||||
Object.entries(item.streams).forEach(([quality, url]) => {
|
||||
if (!url || typeof url !== 'string') return;
|
||||
streams.push({
|
||||
name: title,
|
||||
title: `${title} - ${type === 'tv' ? `S${String(seasonNum).padStart(2,'0')}E${String(episodeNum).padStart(2,'0')} ` : ''}${quality}`,
|
||||
url,
|
||||
quality: quality || 'Unknown',
|
||||
provider: 'Xprime.tv',
|
||||
headers: {},
|
||||
});
|
||||
});
|
||||
};
|
||||
if (Array.isArray(resJson)) resJson.forEach(processItem); else processItem(resJson);
|
||||
|
||||
if (streams.length) {
|
||||
console.time('[Xprime.tv] Fetch stream sizes');
|
||||
await Promise.all(streams.map(async s => { s.size = await fetchStreamSize(s.url); }));
|
||||
console.timeEnd('[Xprime.tv] Fetch stream sizes');
|
||||
}
|
||||
|
||||
console.log(`[Xprime.tv] Returning ${streams.length} streams.`);
|
||||
return streams;
|
||||
}
|
||||
|
||||
ensureCacheDir(CACHE_DIR).catch(()=>{});
|
||||
|
||||
module.exports = { getXprimeStreams };
|
||||
Reference in New Issue
Block a user