mirror of
https://github.com/Inside4ndroid/TMDB-Embed-API.git
synced 2026-10-22 09:35:11 +00:00
1418 lines
57 KiB
JavaScript
1418 lines
57 KiB
JavaScript
const axios = require('axios');
|
||
const cheerio = require('cheerio');
|
||
const { URLSearchParams, URL } = require('url');
|
||
const FormData = require('form-data');
|
||
const { CookieJar } = require('tough-cookie');
|
||
const fs = require('fs').promises;
|
||
const path = require('path');
|
||
|
||
// Dynamic import for axios-cookiejar-support
|
||
let axiosCookieJarSupport = null;
|
||
const getAxiosCookieJarSupport = async () => {
|
||
if (!axiosCookieJarSupport) {
|
||
axiosCookieJarSupport = await import('axios-cookiejar-support');
|
||
}
|
||
return axiosCookieJarSupport;
|
||
};
|
||
|
||
// All requests are direct
|
||
|
||
// --- Domain Fetching ---
|
||
let uhdMoviesDomain = 'https://uhdmovies.email'; // Fallback domain
|
||
let domainCacheTimestamp = 0;
|
||
const DOMAIN_CACHE_TTL = 4 * 60 * 60 * 1000; // 4 hours
|
||
|
||
async function getUHDMoviesDomain() {
|
||
const now = Date.now();
|
||
if (now - domainCacheTimestamp < DOMAIN_CACHE_TTL) {
|
||
return uhdMoviesDomain;
|
||
}
|
||
|
||
try {
|
||
console.log('[UHDMovies] Fetching latest domain...');
|
||
const response = await makeRequest('https://raw.githubusercontent.com/phisher98/TVVVV/refs/heads/main/domains.json', { timeout: 10000 });
|
||
if (response.data && response.data.UHDMovies) {
|
||
uhdMoviesDomain = response.data.UHDMovies;
|
||
domainCacheTimestamp = now;
|
||
console.log(`[UHDMovies] Updated domain to: ${uhdMoviesDomain}`);
|
||
} else {
|
||
console.warn('[UHDMovies] Domain JSON fetched, but "UHDMovies" key was not found. Using fallback.');
|
||
}
|
||
} catch (error) {
|
||
console.error(`[UHDMovies] Failed to fetch latest domain, using fallback. Error: ${error.message}`);
|
||
}
|
||
return uhdMoviesDomain;
|
||
}
|
||
|
||
// Constants
|
||
const { getTmdbApiKey } = require('../utils/tmdbKey');
|
||
function getUhdMoviesKey() {
|
||
const k = getTmdbApiKey();
|
||
if (!k) console.warn('[UHDMovies] TMDB API key missing; certain metadata features skipped.');
|
||
return k;
|
||
}
|
||
|
||
// --- Caching Configuration ---
|
||
const CACHE_ENABLED = process.env.DISABLE_CACHE !== 'true'; // Set to true to disable caching for this provider
|
||
console.log(`[UHDMovies] Internal cache is ${CACHE_ENABLED ? 'enabled' : 'disabled'}.`);
|
||
const CACHE_DIR = process.env.VERCEL ? path.join('/tmp', '.uhd_cache') : path.join(__dirname, '.cache', 'uhdmovies'); // Cache directory inside providers/uhdmovies
|
||
|
||
// Caching layer simplified: placeholders only (extend for FS persistence if needed)
|
||
|
||
// --- Caching Helper Functions ---
|
||
const ensureCacheDir = async () => {
|
||
if (!CACHE_ENABLED) return;
|
||
try {
|
||
await fs.mkdir(CACHE_DIR, { recursive: true });
|
||
} catch (error) {
|
||
if (error.code !== 'EEXIST') {
|
||
console.error(`[UHDMovies Cache] Error creating cache directory: ${error.message}`);
|
||
}
|
||
}
|
||
};
|
||
|
||
const getFromCache = async (key) => {
|
||
if (!CACHE_ENABLED) return null;
|
||
return null; // no-op
|
||
};
|
||
|
||
const saveToCache = async (key, data) => {
|
||
if (!CACHE_ENABLED) return;
|
||
// no-op (placeholder for future FS caching)
|
||
};
|
||
|
||
// Initialize cache directory on startup
|
||
ensureCacheDir();
|
||
|
||
// Configure axios with headers to mimic a browser
|
||
const createAxiosInstance = () => {
|
||
const config = {
|
||
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',
|
||
'Connection': 'keep-alive',
|
||
'Upgrade-Insecure-Requests': '1',
|
||
'Cache-Control': 'max-age=0'
|
||
},
|
||
timeout: 30000
|
||
};
|
||
|
||
return axios.create(config);
|
||
};
|
||
|
||
const axiosInstance = createAxiosInstance();
|
||
|
||
// Request helper
|
||
const makeRequest = async (url, options = {}) => {
|
||
console.log(`[UHDMovies] Making direct request to: ${url}`);
|
||
return axiosInstance.get(url, options);
|
||
};
|
||
|
||
// Removed unused in-memory cache structure (uhdMoviesCache)
|
||
|
||
// Function to search for movies
|
||
async function searchMovies(query) {
|
||
try {
|
||
const baseUrl = await getUHDMoviesDomain();
|
||
console.log(`[UHDMovies] Searching for: ${query}`);
|
||
const searchUrl = `${baseUrl}/search/${encodeURIComponent(query)}`;
|
||
|
||
const response = await makeRequest(searchUrl);
|
||
const $ = cheerio.load(response.data);
|
||
|
||
const searchResults = [];
|
||
|
||
// New logic for grid-based search results
|
||
$('article.gridlove-post').each((index, element) => {
|
||
const linkElement = $(element).find('a[href*="/download-"]');
|
||
if (linkElement.length > 0) {
|
||
const link = linkElement.first().attr('href');
|
||
// Prefer the 'title' attribute, fallback to h1 text
|
||
const title = linkElement.first().attr('title') || $(element).find('h1.sanket').text().trim();
|
||
|
||
if (link && title && !searchResults.some(item => item.link === link)) {
|
||
searchResults.push({
|
||
title,
|
||
link: link.startsWith('http') ? link : `${baseUrl}${link}`
|
||
});
|
||
}
|
||
}
|
||
});
|
||
|
||
// Fallback for original list-based search if new logic fails
|
||
if (searchResults.length === 0) {
|
||
console.log('[UHDMovies] Grid search logic found no results, trying original list-based logic...');
|
||
$('a[href*="/download-"]').each((index, element) => {
|
||
const link = $(element).attr('href');
|
||
// Avoid duplicates by checking if link already exists in results
|
||
if (link && !searchResults.some(item => item.link === link)) {
|
||
const title = $(element).text().trim();
|
||
if (title) {
|
||
searchResults.push({
|
||
title,
|
||
link: link.startsWith('http') ? link : `${baseUrl}${link}`
|
||
});
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
console.log(`[UHDMovies] Found ${searchResults.length} results`);
|
||
return searchResults;
|
||
} catch (error) {
|
||
console.error(`[UHDMovies] Error searching movies: ${error.message}`);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
// Function to extract clean quality information from verbose text
|
||
function extractCleanQuality(fullQualityText) {
|
||
if (!fullQualityText || fullQualityText === 'Unknown Quality') {
|
||
return 'Unknown Quality';
|
||
}
|
||
|
||
const cleanedFullQualityText = fullQualityText.replace(/(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])/g, '').trim();
|
||
const text = cleanedFullQualityText.toLowerCase();
|
||
const quality = [];
|
||
|
||
// Extract resolution
|
||
if (text.includes('2160p') || text.includes('4k')) {
|
||
quality.push('4K');
|
||
} else if (text.includes('1080p')) {
|
||
quality.push('1080p');
|
||
} else if (text.includes('720p')) {
|
||
quality.push('720p');
|
||
} else if (text.includes('480p')) {
|
||
quality.push('480p');
|
||
}
|
||
|
||
// Extract special features
|
||
if (text.includes('hdr')) {
|
||
quality.push('HDR');
|
||
}
|
||
if (text.includes('dolby vision') || text.includes('dovi') || /\bdv\b/.test(text)) {
|
||
quality.push('DV');
|
||
}
|
||
if (text.includes('imax')) {
|
||
quality.push('IMAX');
|
||
}
|
||
if (text.includes('bluray') || text.includes('blu-ray')) {
|
||
quality.push('BluRay');
|
||
}
|
||
|
||
// If we found any quality indicators, join them
|
||
if (quality.length > 0) {
|
||
return quality.join(' | ');
|
||
}
|
||
|
||
// Fallback: try to extract a shorter version of the original text
|
||
// Look for patterns like "Movie Name (Year) Resolution ..."
|
||
const patterns = [
|
||
/(\d{3,4}p.*?(?:x264|x265|hevc).*?)[[(]/i,
|
||
/(\d{3,4}p.*?)[[(]/i,
|
||
/((?:720p|1080p|2160p|4k).*?)$/i
|
||
];
|
||
|
||
for (const pattern of patterns) {
|
||
const match = cleanedFullQualityText.match(pattern);
|
||
if (match && match[1].trim().length < 100) {
|
||
return match[1].trim().replace(/x265/ig, 'HEVC');
|
||
}
|
||
}
|
||
|
||
// Final fallback: truncate if too long
|
||
if (cleanedFullQualityText.length > 80) {
|
||
return cleanedFullQualityText.substring(0, 77).replace(/x265/ig, 'HEVC') + '...';
|
||
}
|
||
|
||
return cleanedFullQualityText.replace(/x265/ig, 'HEVC');
|
||
}
|
||
|
||
// Function to extract download links for TV shows from a page
|
||
async function extractTvShowDownloadLinks(showPageUrl, season, episode) {
|
||
try {
|
||
console.log(`[UHDMovies] Extracting TV show links from: ${showPageUrl} for S${season}E${episode}`);
|
||
const response = await makeRequest(showPageUrl);
|
||
const $ = cheerio.load(response.data);
|
||
|
||
const showTitle = $('h1').first().text().trim();
|
||
const downloadLinks = [];
|
||
|
||
// --- NEW LOGIC TO SCOPE SEARCH TO THE CORRECT SEASON ---
|
||
let inTargetSeason = false;
|
||
let qualityText = '';
|
||
|
||
$('.entry-content').find('*').each((index, element) => {
|
||
const $el = $(element);
|
||
const text = $el.text().trim();
|
||
const seasonMatch = text.match(/^SEASON\s+(\d+)/i);
|
||
|
||
// Check if we are entering a new season block
|
||
if (seasonMatch) {
|
||
const currentSeasonNum = parseInt(seasonMatch[1], 10);
|
||
if (currentSeasonNum === season) {
|
||
inTargetSeason = true;
|
||
console.log(`[UHDMovies] Entering Season ${season} block.`);
|
||
} else if (inTargetSeason) {
|
||
// We've hit the next season, so we stop.
|
||
console.log(`[UHDMovies] Exiting Season ${season} block, now in Season ${currentSeasonNum}.`);
|
||
inTargetSeason = false;
|
||
return false; // Exit .each() loop
|
||
}
|
||
}
|
||
|
||
if (inTargetSeason) {
|
||
// This element is within the correct season's block.
|
||
|
||
// Is this a quality header? (e.g., a <pre> or a <p> with <strong>)
|
||
// It often contains resolution, release group, etc.
|
||
const isQualityHeader = $el.is('pre, p:has(strong), p:has(b), h3, h4');
|
||
if (isQualityHeader) {
|
||
const headerText = $el.text().trim();
|
||
// Filter out irrelevant headers. We can be more aggressive here.
|
||
if (headerText.length > 5 && !/plot|download|screenshot|trailer|join|powered by|season/i.test(headerText) && !($el.find('a').length > 0)) {
|
||
qualityText = headerText; // Store the most recent quality header
|
||
}
|
||
}
|
||
|
||
// Is this a paragraph with episode links?
|
||
if ($el.is('p') && $el.find('a[href*="tech.unblockedgames.world"], a[href*="tech.examzculture.in"]').length > 0) {
|
||
const linksParagraph = $el;
|
||
const episodeRegex = new RegExp(`^Episode\\s+0*${episode}(?!\\d)`, 'i');
|
||
const targetEpisodeLink = linksParagraph.find('a').filter((i, el) => {
|
||
return episodeRegex.test($(el).text().trim());
|
||
}).first();
|
||
|
||
if (targetEpisodeLink.length > 0) {
|
||
const link = targetEpisodeLink.attr('href');
|
||
if (link && !downloadLinks.some(item => item.link === link)) {
|
||
const sizeMatch = qualityText.match(/\[\s*([0-9.,]+\s*[KMGT]B)/i);
|
||
const size = sizeMatch ? sizeMatch[1] : 'Unknown';
|
||
|
||
const cleanQuality = extractCleanQuality(qualityText);
|
||
const rawQuality = qualityText.replace(/(\r\n|\n|\r)/gm, " ").replace(/\s+/g, ' ').trim();
|
||
|
||
console.log(`[UHDMovies] Found match: Quality='${qualityText}', Link='${link}'`);
|
||
downloadLinks.push({ quality: cleanQuality, size: size, link: link, rawQuality: rawQuality });
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- ENHANCED: Check for maxbutton-gdrive-episode structure ---
|
||
if ($el.is('p') && $el.find('a.maxbutton-gdrive-episode').length > 0) {
|
||
const episodeRegex = new RegExp(`^Episode\\s+0*${episode}(?!\\d)`, 'i');
|
||
const targetEpisodeLink = $el.find('a.maxbutton-gdrive-episode').filter((i, el) => {
|
||
const episodeText = $(el).find('.mb-text').text().trim();
|
||
return episodeRegex.test(episodeText);
|
||
}).first();
|
||
|
||
if (targetEpisodeLink.length > 0) {
|
||
const link = targetEpisodeLink.attr('href');
|
||
if (link && !downloadLinks.some(item => item.link === link)) {
|
||
const sizeMatch = qualityText.match(/\[\s*([0-9.,]+\s*[KMGT]B)/i);
|
||
const size = sizeMatch ? sizeMatch[1] : 'Unknown';
|
||
|
||
const cleanQuality = extractCleanQuality(qualityText);
|
||
const rawQuality = qualityText.replace(/(\r\n|\n|\r)/gm, " ").replace(/\s+/g, ' ').trim();
|
||
|
||
console.log(`[UHDMovies] Found match (maxbutton): Quality='${qualityText}', Link='${link}'`);
|
||
downloadLinks.push({ quality: cleanQuality, size: size, link: link, rawQuality: rawQuality });
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
if (downloadLinks.length === 0) {
|
||
console.log('[UHDMovies] Main extraction logic failed. Checking if requested season exists on page before fallback.');
|
||
|
||
// Check if the requested season exists on the page at all
|
||
let seasonExists = false;
|
||
const actualSeasonsOnPage = new Set(); // Track what seasons actually have content
|
||
|
||
// First pass: Look for actual episode content to see what seasons are available
|
||
$('.entry-content').find('a[href*="tech.unblockedgames.world"], a[href*="tech.examzculture.in"], a.maxbutton-gdrive-episode').each((index, element) => {
|
||
const $el = $(element);
|
||
const linkText = $el.text().trim();
|
||
const episodeText = $el.find('.mb-text').text().trim() || linkText;
|
||
|
||
// Look for season indicators in episode links
|
||
const seasonMatches = [
|
||
episodeText.match(/S(\d{1,2})/i), // S01, S02, etc.
|
||
episodeText.match(/Season\s+(\d+)/i), // Season 1, Season 2, etc.
|
||
episodeText.match(/S(\d{1,2})E(\d{1,3})/i) // S01E01 format
|
||
];
|
||
|
||
for (const match of seasonMatches) {
|
||
if (match && match[1]) {
|
||
const foundSeason = parseInt(match[1], 10);
|
||
actualSeasonsOnPage.add(foundSeason);
|
||
}
|
||
}
|
||
});
|
||
|
||
console.log(`[UHDMovies] Actual seasons found on page: ${Array.from(actualSeasonsOnPage).sort((a, b) => a - b).join(', ')}`);
|
||
|
||
// Check if requested season is in the actual content
|
||
if (actualSeasonsOnPage.has(season)) {
|
||
seasonExists = true;
|
||
console.log(`[UHDMovies] Season ${season} confirmed to exist in actual episode content`);
|
||
} else {
|
||
// Fallback: Check page descriptions/titles for season mentions
|
||
$('.entry-content').find('*').each((index, element) => {
|
||
const $el = $(element);
|
||
const text = $el.text().trim();
|
||
// Match various season formats: "SEASON 2", "Season 2", "(Season 1 – 2)", "Season 1-2", etc.
|
||
const seasonMatches = [
|
||
text.match(/^SEASON\s+(\d+)/i),
|
||
text.match(/\bSeason\s+(\d+)/i),
|
||
text.match(/\(Season\s+\d+\s*[–-]\s*(\d+)\)/i), // Matches "(Season 1 – 2)"
|
||
text.match(/Season\s+\d+\s*[–-]\s*(\d+)/i), // Matches "Season 1-2"
|
||
text.match(/\bS(\d+)/i) // Matches "S2", "S02", etc.
|
||
];
|
||
|
||
for (const match of seasonMatches) {
|
||
if (match) {
|
||
const currentSeasonNum = parseInt(match[1], 10);
|
||
if (currentSeasonNum === season) {
|
||
seasonExists = true;
|
||
console.log(`[UHDMovies] Season ${season} found in page description: "${text.substring(0, 100)}..."`);
|
||
return false; // Exit .each() loop
|
||
}
|
||
// For range formats like "Season 1 – 2", check if requested season is in range
|
||
if (match[0].includes('–') || match[0].includes('-')) {
|
||
const rangeMatch = match[0].match(/Season\s+(\d+)\s*[–-]\s*(\d+)/i);
|
||
if (rangeMatch) {
|
||
const startSeason = parseInt(rangeMatch[1], 10);
|
||
const endSeason = parseInt(rangeMatch[2], 10);
|
||
if (season >= startSeason && season <= endSeason) {
|
||
seasonExists = true;
|
||
console.log(`[UHDMovies] Season ${season} found in range ${startSeason}-${endSeason} in page description`);
|
||
return false; // Exit .each() loop
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
if (!seasonExists) {
|
||
console.log(`[UHDMovies] Season ${season} not found on page. Available seasons may not include the requested season.`);
|
||
// Don't use fallback if the season doesn't exist to avoid wrong episodes
|
||
return { title: showTitle, links: [], seasonNotFound: true };
|
||
}
|
||
|
||
console.log(`[UHDMovies] Season ${season} exists on page but episode extraction failed. Trying fallback method with season filtering.`);
|
||
|
||
// --- ENHANCED FALLBACK LOGIC FOR NEW HTML STRUCTURE ---
|
||
// Try the new maxbutton-gdrive-episode structure first
|
||
$('.entry-content').find('a.maxbutton-gdrive-episode').each((i, el) => {
|
||
const linkElement = $(el);
|
||
const episodeText = linkElement.find('.mb-text').text().trim();
|
||
const episodeRegex = new RegExp(`^Episode\\s+0*${episode}(?!\\d)`, 'i');
|
||
|
||
if (episodeRegex.test(episodeText)) {
|
||
const link = linkElement.attr('href');
|
||
if (link && !downloadLinks.some(item => item.link === link)) {
|
||
let qualityText = 'Unknown Quality';
|
||
|
||
// Look for quality info in the preceding paragraph or heading
|
||
const parentP = linkElement.closest('p, div');
|
||
const prevElement = parentP.prev();
|
||
if (prevElement.length > 0) {
|
||
const prevText = prevElement.text().trim();
|
||
if (prevText && prevText.length > 5 && !prevText.toLowerCase().includes('download')) {
|
||
qualityText = prevText;
|
||
}
|
||
}
|
||
|
||
// Check if this episode belongs to the correct season
|
||
// Enhanced season check - look for various season formats
|
||
const seasonCheckRegexes = [
|
||
new RegExp(`\\.S0*${season}[\\.]`, 'i'), // .S01.
|
||
new RegExp(`S0*${season}[\\.]`, 'i'), // S01.
|
||
new RegExp(`S0*${season}\\b`, 'i'), // S01 (word boundary)
|
||
new RegExp(`Season\\s+0*${season}\\b`, 'i'), // Season 1
|
||
new RegExp(`S0*${season}`, 'i') // S01 anywhere
|
||
];
|
||
|
||
const seasonMatch = seasonCheckRegexes.some(regex => regex.test(qualityText));
|
||
if (!seasonMatch) {
|
||
console.log(`[UHDMovies] Skipping episode from different season: Quality='${qualityText}'`);
|
||
return; // Skip this episode as it's from a different season
|
||
}
|
||
|
||
const sizeMatch = qualityText.match(/\[([0-9.,]+[KMGT]B[^\]]*)\]/i);
|
||
const size = sizeMatch ? sizeMatch[1] : 'Unknown';
|
||
const cleanQuality = extractCleanQuality(qualityText);
|
||
const rawQuality = qualityText.replace(/(\r\n|\n|\r)/gm, " ").replace(/\s+/g, ' ').trim();
|
||
|
||
console.log(`[UHDMovies] Found match via enhanced fallback (maxbutton): Quality='${qualityText}', Link='${link}'`);
|
||
downloadLinks.push({ quality: cleanQuality, size: size, link: link, rawQuality: rawQuality });
|
||
}
|
||
}
|
||
});
|
||
|
||
// If still no results, try the original fallback logic
|
||
if (downloadLinks.length === 0) {
|
||
console.log(`[UHDMovies] Enhanced fallback failed, trying original fallback logic.`);
|
||
$('.entry-content').find('a[href*="tech.unblockedgames.world"], a[href*="tech.examzculture.in"]').each((i, el) => {
|
||
const linkElement = $(el);
|
||
const episodeRegex = new RegExp(`^Episode\\s+0*${episode}(?!\\d)`, 'i');
|
||
|
||
if (episodeRegex.test(linkElement.text().trim())) {
|
||
const link = linkElement.attr('href');
|
||
if (link && !downloadLinks.some(item => item.link === link)) {
|
||
let qualityText = 'Unknown Quality';
|
||
const parentP = linkElement.closest('p, div');
|
||
const prevElement = parentP.prev();
|
||
if (prevElement.length > 0) {
|
||
const prevText = prevElement.text().trim();
|
||
if (prevText && prevText.length > 5 && !prevText.toLowerCase().includes('download')) {
|
||
qualityText = prevText;
|
||
}
|
||
}
|
||
|
||
// Check if this episode belongs to the correct season
|
||
// Enhanced season check - look for various season formats
|
||
const seasonCheckRegexes = [
|
||
new RegExp(`\\.S0*${season}[\\.]`, 'i'), // .S01.
|
||
new RegExp(`S0*${season}[\\.]`, 'i'), // S01.
|
||
new RegExp(`S0*${season}\\b`, 'i'), // S01 (word boundary)
|
||
new RegExp(`Season\\s+0*${season}\\b`, 'i'), // Season 1
|
||
new RegExp(`S0*${season}`, 'i') // S01 anywhere
|
||
];
|
||
|
||
const seasonMatch = seasonCheckRegexes.some(regex => regex.test(qualityText));
|
||
if (!seasonMatch) {
|
||
console.log(`[UHDMovies] Skipping episode from different season: Quality='${qualityText}'`);
|
||
return; // Skip this episode as it's from a different season
|
||
}
|
||
|
||
const sizeMatch = qualityText.match(/\[([0-9.,]+[KMGT]B[^\]]*)\]/i);
|
||
const size = sizeMatch ? sizeMatch[1] : 'Unknown';
|
||
const cleanQuality = extractCleanQuality(qualityText);
|
||
const rawQuality = qualityText.replace(/(\r\n|\n|\r)/gm, " ").replace(/\s+/g, ' ').trim();
|
||
|
||
console.log(`[UHDMovies] Found match via original fallback: Quality='${qualityText}', Link='${link}'`);
|
||
downloadLinks.push({ quality: cleanQuality, size: size, link: link, rawQuality: rawQuality });
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
if (downloadLinks.length > 0) {
|
||
console.log(`[UHDMovies] Found ${downloadLinks.length} links for S${season}E${episode}.`);
|
||
} else {
|
||
console.log(`[UHDMovies] Could not find links for S${season}E${episode}. It's possible the logic needs adjustment or the links aren't on the page.`);
|
||
}
|
||
|
||
return { title: showTitle, links: downloadLinks };
|
||
|
||
} catch (error) {
|
||
console.error(`[UHDMovies] Error extracting TV show download links: ${error.message}`);
|
||
return { title: 'Unknown', links: [] };
|
||
}
|
||
}
|
||
|
||
// Function to extract download links from a movie page
|
||
async function extractDownloadLinks(moviePageUrl, targetYear = null) {
|
||
try {
|
||
console.log(`[UHDMovies] Extracting links from: ${moviePageUrl}`);
|
||
const response = await makeRequest(moviePageUrl);
|
||
const $ = cheerio.load(response.data);
|
||
|
||
const movieTitle = $('h1').first().text().trim();
|
||
const downloadLinks = [];
|
||
|
||
// Find all download links (the new SID links) and their associated quality information
|
||
$('a[href*="tech.unblockedgames.world"], a[href*="tech.examzculture.in"]').each((index, element) => {
|
||
const link = $(element).attr('href');
|
||
|
||
if (link && !downloadLinks.some(item => item.link === link)) {
|
||
let quality = 'Unknown Quality';
|
||
let size = 'Unknown';
|
||
|
||
// Method 1: Look for quality in the closest preceding paragraph or heading
|
||
const prevElement = $(element).closest('p').prev();
|
||
if (prevElement.length > 0) {
|
||
const prevText = prevElement.text().trim();
|
||
if (prevText && prevText.length > 20 && !prevText.includes('Download')) {
|
||
quality = prevText;
|
||
}
|
||
}
|
||
|
||
// Method 2: Look for quality in parent's siblings
|
||
if (quality === 'Unknown Quality') {
|
||
const parentSiblings = $(element).parent().prevAll().first().text().trim();
|
||
if (parentSiblings && parentSiblings.length > 20) {
|
||
quality = parentSiblings;
|
||
}
|
||
}
|
||
|
||
// Method 3: Look for bold/strong text above the link
|
||
if (quality === 'Unknown Quality') {
|
||
const strongText = $(element).closest('p').prevAll().find('strong, b').last().text().trim();
|
||
if (strongText && strongText.length > 20) {
|
||
quality = strongText;
|
||
}
|
||
}
|
||
|
||
// Method 4: Look for the entire paragraph containing quality info
|
||
if (quality === 'Unknown Quality') {
|
||
let currentElement = $(element).parent();
|
||
for (let i = 0; i < 5; i++) {
|
||
currentElement = currentElement.prev();
|
||
if (currentElement.length === 0) break;
|
||
|
||
const text = currentElement.text().trim();
|
||
if (text && text.length > 30 &&
|
||
(text.includes('1080p') || text.includes('720p') || text.includes('2160p') ||
|
||
text.includes('4K') || text.includes('HEVC') || text.includes('x264') || text.includes('x265'))) {
|
||
quality = text;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Year-based filtering for collections
|
||
if (targetYear && quality !== 'Unknown Quality') {
|
||
// Check for years in quality text
|
||
const yearMatches = quality.match(/\((\d{4})\)/g);
|
||
let hasMatchingYear = false;
|
||
|
||
if (yearMatches && yearMatches.length > 0) {
|
||
for (const yearMatch of yearMatches) {
|
||
const year = parseInt(yearMatch.replace(/[()]/g, ''));
|
||
if (year === targetYear) {
|
||
hasMatchingYear = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!hasMatchingYear) {
|
||
console.log(`[UHDMovies] Skipping link due to year mismatch. Target: ${targetYear}, Found: ${yearMatches.join(', ')} in "${quality}"`);
|
||
return; // Skip this link
|
||
}
|
||
} else {
|
||
// If no year in quality text, check filename and other indicators
|
||
const linkText = $(element).text().trim();
|
||
const parentText = $(element).parent().text().trim();
|
||
const combinedText = `${quality} ${linkText} ${parentText}`;
|
||
|
||
// Look for years in combined text
|
||
const allYearMatches = combinedText.match(/\((\d{4})\)/g) || combinedText.match(/(\d{4})/g);
|
||
if (allYearMatches) {
|
||
let foundTargetYear = false;
|
||
for (const yearMatch of allYearMatches) {
|
||
const year = parseInt(yearMatch.replace(/[()]/g, ''));
|
||
if (year >= 1900 && year <= 2030) { // Valid movie year range
|
||
if (year === targetYear) {
|
||
foundTargetYear = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
if (!foundTargetYear && allYearMatches.length > 0) {
|
||
console.log(`[UHDMovies] Skipping link due to no matching year found. Target: ${targetYear}, Found years: ${allYearMatches.join(', ')} in combined text`);
|
||
return; // Skip this link
|
||
}
|
||
}
|
||
|
||
// Additional check: if quality contains movie names that don't match target year
|
||
const lowerQuality = quality.toLowerCase();
|
||
if (targetYear === 2015) {
|
||
if (lowerQuality.includes('wasp') || lowerQuality.includes('quantumania')) {
|
||
console.log(`[UHDMovies] Skipping link for 2015 target as it contains 'wasp' or 'quantumania': "${quality}"`);
|
||
return; // Skip this link
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Extract size from quality text if present
|
||
const sizeMatch = quality.match(/\[([0-9.,]+\s*[KMGT]B[^\]]*)\]/);
|
||
if (sizeMatch) {
|
||
size = sizeMatch[1];
|
||
}
|
||
|
||
// Clean up the quality information
|
||
const cleanQuality = extractCleanQuality(quality);
|
||
|
||
downloadLinks.push({
|
||
quality: cleanQuality,
|
||
size: size,
|
||
link: link,
|
||
rawQuality: quality.replace(/(\r\n|\n|\r)/gm, " ").replace(/\s+/g, ' ').trim()
|
||
});
|
||
}
|
||
});
|
||
|
||
return {
|
||
title: movieTitle,
|
||
links: downloadLinks
|
||
};
|
||
|
||
} catch (error) {
|
||
console.error(`[UHDMovies] Error extracting download links: ${error.message}`);
|
||
return { title: 'Unknown', links: [] };
|
||
}
|
||
}
|
||
|
||
function extractCodecs(rawQuality) {
|
||
const codecs = [];
|
||
const text = rawQuality.toLowerCase();
|
||
|
||
if (text.includes('hevc') || text.includes('x265')) {
|
||
codecs.push('H.265');
|
||
} else if (text.includes('x264')) {
|
||
codecs.push('H.264');
|
||
}
|
||
|
||
if (text.includes('10bit') || text.includes('10-bit')) {
|
||
codecs.push('10-bit');
|
||
}
|
||
|
||
if (text.includes('atmos')) {
|
||
codecs.push('Atmos');
|
||
} else if (text.includes('dts-hd')) {
|
||
codecs.push('DTS-HD');
|
||
} else if (text.includes('dts')) {
|
||
codecs.push('DTS');
|
||
} else if (text.includes('ddp5.1') || text.includes('dd+ 5.1') || text.includes('eac3')) {
|
||
codecs.push('EAC3');
|
||
} else if (text.includes('ac3')) {
|
||
codecs.push('AC3');
|
||
}
|
||
|
||
if (text.includes('dovi') || text.includes('dolby vision') || /\bdv\b/.test(text)) {
|
||
codecs.push('DV');
|
||
} else if (text.includes('hdr')) {
|
||
codecs.push('HDR');
|
||
}
|
||
|
||
return codecs;
|
||
}
|
||
|
||
// Function to try Instant Download method
|
||
async function tryInstantDownload($) {
|
||
const instantDownloadLink = $('a:contains("Instant Download")').attr('href');
|
||
if (!instantDownloadLink) {
|
||
return null;
|
||
}
|
||
|
||
console.log('[UHDMovies] Found "Instant Download" link, attempting to extract final URL...');
|
||
|
||
try {
|
||
const urlParams = new URLSearchParams(new URL(instantDownloadLink).search);
|
||
const keys = urlParams.get('url');
|
||
|
||
if (keys) {
|
||
const apiUrl = `${new URL(instantDownloadLink).origin}/api`;
|
||
const formData = new FormData();
|
||
formData.append('keys', keys);
|
||
|
||
const apiResponse = await axiosInstance.post(apiUrl, formData, {
|
||
headers: {
|
||
...formData.getHeaders(),
|
||
'x-token': new URL(instantDownloadLink).hostname
|
||
}
|
||
});
|
||
|
||
if (apiResponse.data && apiResponse.data.url) {
|
||
let finalUrl = apiResponse.data.url;
|
||
// Fix spaces in workers.dev URLs by encoding them properly
|
||
if (finalUrl.includes('workers.dev')) {
|
||
const urlParts = finalUrl.split('/');
|
||
const filename = urlParts[urlParts.length - 1];
|
||
const encodedFilename = filename.replace(/ /g, '%20');
|
||
urlParts[urlParts.length - 1] = encodedFilename;
|
||
finalUrl = urlParts.join('/');
|
||
}
|
||
console.log('[UHDMovies] Extracted final link from API:', finalUrl);
|
||
return finalUrl;
|
||
}
|
||
}
|
||
|
||
console.log('[UHDMovies] Could not find a valid final download link from Instant Download.');
|
||
return null;
|
||
} catch (error) {
|
||
console.log(`[UHDMovies] Error processing "Instant Download": ${error.message}`);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// Function to try Resume Cloud method
|
||
async function tryResumeCloud($) {
|
||
// Look for both "Resume Cloud" and "Cloud Resume Download" buttons
|
||
const resumeCloudButton = $('a:contains("Resume Cloud"), a:contains("Cloud Resume Download")');
|
||
|
||
if (resumeCloudButton.length === 0) {
|
||
return null;
|
||
}
|
||
|
||
const resumeLink = resumeCloudButton.attr('href');
|
||
if (!resumeLink) {
|
||
return null;
|
||
}
|
||
|
||
// Check if it's already a direct download link (workers.dev)
|
||
if (resumeLink.includes('workers.dev') || resumeLink.startsWith('http')) {
|
||
let directLink = resumeLink;
|
||
// Fix spaces in workers.dev URLs by encoding them properly
|
||
if (directLink.includes('workers.dev')) {
|
||
const urlParts = directLink.split('/');
|
||
const filename = urlParts[urlParts.length - 1];
|
||
const encodedFilename = filename.replace(/ /g, '%20');
|
||
urlParts[urlParts.length - 1] = encodedFilename;
|
||
directLink = urlParts.join('/');
|
||
}
|
||
console.log(`[UHDMovies] Found direct "Cloud Resume Download" link: ${directLink}`);
|
||
return directLink;
|
||
}
|
||
|
||
// Otherwise, follow the link to get the final download
|
||
try {
|
||
const resumeUrl = new URL(resumeLink, 'https://driveleech.net').href;
|
||
console.log(`[UHDMovies] Found 'Resume Cloud' page link. Following to: ${resumeUrl}`);
|
||
|
||
// "Click" the link by making another request
|
||
const finalPageResponse = await makeRequest(resumeUrl, { maxRedirects: 10 });
|
||
const $$ = cheerio.load(finalPageResponse.data);
|
||
|
||
// Look for direct download links
|
||
let finalDownloadLink = $$('a.btn-success[href*="workers.dev"], a[href*="driveleech.net/d/"]').attr('href');
|
||
|
||
if (finalDownloadLink) {
|
||
// Fix spaces in workers.dev URLs by encoding them properly
|
||
if (finalDownloadLink.includes('workers.dev')) {
|
||
// Split the URL at the last slash to separate the base URL from the filename
|
||
const urlParts = finalDownloadLink.split('/');
|
||
const filename = urlParts[urlParts.length - 1];
|
||
// Encode spaces in the filename part only
|
||
const encodedFilename = filename.replace(/ /g, '%20');
|
||
urlParts[urlParts.length - 1] = encodedFilename;
|
||
finalDownloadLink = urlParts.join('/');
|
||
}
|
||
console.log(`[UHDMovies] Extracted final Resume Cloud link: ${finalDownloadLink}`);
|
||
return finalDownloadLink;
|
||
} else {
|
||
console.log('[UHDMovies] Could not find the final download link on the "Resume Cloud" page.');
|
||
return null;
|
||
}
|
||
} catch (error) {
|
||
console.log(`[UHDMovies] Error processing "Resume Cloud": ${error.message}`);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// Environment variable to control URL validation
|
||
const URL_VALIDATION_ENABLED = process.env.DISABLE_URL_VALIDATION !== 'true';
|
||
console.log(`[UHDMovies] URL validation is ${URL_VALIDATION_ENABLED ? 'enabled' : 'disabled'}.`);
|
||
|
||
// Validate if a video URL is working (not 404 or broken)
|
||
async function validateVideoUrl(url, timeout = 10000) {
|
||
// Skip validation if disabled via environment variable
|
||
if (!URL_VALIDATION_ENABLED) {
|
||
console.log(`[UHDMovies] URL validation disabled, skipping validation for: ${url.substring(0, 100)}...`);
|
||
return true;
|
||
}
|
||
|
||
try {
|
||
console.log(`[UHDMovies] Validating URL: ${url.substring(0, 100)}...`);
|
||
|
||
const response = await axiosInstance.head(url, {
|
||
timeout,
|
||
headers: {
|
||
'Range': 'bytes=0-1'
|
||
}
|
||
});
|
||
|
||
// Check if status is OK (200-299) or partial content (206)
|
||
if (response.status >= 200 && response.status < 400) {
|
||
console.log(`[UHDMovies] ✓ URL validation successful (${response.status})`);
|
||
return true;
|
||
} else {
|
||
console.log(`[UHDMovies] ✗ URL validation failed with status: ${response.status}`);
|
||
return false;
|
||
}
|
||
} catch (error) {
|
||
console.log(`[UHDMovies] ✗ URL validation failed: ${error.message}`);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
|
||
// Compare media to find matching result
|
||
function compareMedia(mediaInfo, searchResult) {
|
||
const normalizeString = (str) => String(str || '').toLowerCase().replace(/[^a-zA-Z0-9]/g, '');
|
||
|
||
const titleWithAnd = mediaInfo.title.replace(/\s*&\s*/g, ' and ');
|
||
const normalizedMediaTitle = normalizeString(titleWithAnd);
|
||
const normalizedResultTitle = normalizeString(searchResult.title);
|
||
|
||
console.log(`[UHDMovies] Comparing: "${mediaInfo.title}" (${mediaInfo.year}) vs "${searchResult.title}"`);
|
||
console.log(`[UHDMovies] Normalized: "${normalizedMediaTitle}" vs "${normalizedResultTitle}"`);
|
||
|
||
// Check if titles match or result title contains media title
|
||
let titleMatches = normalizedResultTitle.includes(normalizedMediaTitle);
|
||
|
||
// If direct match fails, try checking for franchise/collection matches
|
||
if (!titleMatches) {
|
||
const mainTitle = normalizedMediaTitle.split('and')[0];
|
||
const isCollection = normalizedResultTitle.includes('duology') ||
|
||
normalizedResultTitle.includes('trilogy') ||
|
||
normalizedResultTitle.includes('quadrilogy') ||
|
||
normalizedResultTitle.includes('collection') ||
|
||
normalizedResultTitle.includes('saga');
|
||
|
||
if (isCollection && normalizedResultTitle.includes(mainTitle)) {
|
||
console.log(`[UHDMovies] Found collection match: "${mainTitle}" in collection "${searchResult.title}"`);
|
||
titleMatches = true;
|
||
}
|
||
}
|
||
|
||
if (!titleMatches) {
|
||
console.log(`[UHDMovies] Title mismatch: "${normalizedResultTitle}" does not contain "${normalizedMediaTitle}"`);
|
||
return false;
|
||
}
|
||
|
||
// NEW: Negative keyword check for spinoffs
|
||
const negativeKeywords = ['challenge', 'conversation', 'story', 'in conversation'];
|
||
const originalTitleLower = mediaInfo.title.toLowerCase();
|
||
for (const keyword of negativeKeywords) {
|
||
if (normalizedResultTitle.includes(keyword.replace(/\s/g, '')) && !originalTitleLower.includes(keyword)) {
|
||
console.log(`[UHDMovies] Rejecting spinoff due to keyword: "${keyword}"`);
|
||
return false; // It's a spinoff, reject it.
|
||
}
|
||
}
|
||
|
||
// Check year if both are available
|
||
if (mediaInfo.year && searchResult.title) {
|
||
const yearRegex = /\b(19[89]\d|20\d{2})\b/g; // Look for years 1980-2099
|
||
const yearMatchesInResult = searchResult.title.match(yearRegex);
|
||
const yearRangeMatch = searchResult.title.match(/\((\d{4})\s*-\s*(\d{4})\)/);
|
||
|
||
let hasMatchingYear = false;
|
||
|
||
if (yearMatchesInResult) {
|
||
console.log(`[UHDMovies] Found years in result: ${yearMatchesInResult.join(', ')}`);
|
||
if (yearMatchesInResult.some(yearStr => Math.abs(parseInt(yearStr) - mediaInfo.year) <= 1)) {
|
||
hasMatchingYear = true;
|
||
}
|
||
}
|
||
|
||
if (!hasMatchingYear && yearRangeMatch) {
|
||
console.log(`[UHDMovies] Found year range in result: ${yearRangeMatch[0]}`);
|
||
const startYear = parseInt(yearRangeMatch[1]);
|
||
const endYear = parseInt(yearRangeMatch[2]);
|
||
if (mediaInfo.year >= startYear - 1 && mediaInfo.year <= endYear + 1) {
|
||
hasMatchingYear = true;
|
||
}
|
||
}
|
||
|
||
// If there are any years found in the title, one of them MUST match.
|
||
if ((yearMatchesInResult || yearRangeMatch) && !hasMatchingYear) {
|
||
console.log(`[UHDMovies] Year mismatch. Target: ${mediaInfo.year}, but no matching year found in result.`);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
console.log(`[UHDMovies] Match successful!`);
|
||
return true;
|
||
}
|
||
|
||
// Function to score search results based on quality keywords and season coverage
|
||
function scoreResult(title, requestedSeason = null) {
|
||
let score = 0;
|
||
const lowerTitle = title.toLowerCase();
|
||
|
||
// Quality scoring
|
||
if (lowerTitle.includes('remux')) score += 10;
|
||
if (lowerTitle.includes('bluray') || lowerTitle.includes('blu-ray')) score += 8;
|
||
if (lowerTitle.includes('imax')) score += 6;
|
||
if (lowerTitle.includes('4k') || lowerTitle.includes('2160p')) score += 5;
|
||
if (lowerTitle.includes('dovi') || lowerTitle.includes('dolby vision') || /\\bdv\\b/.test(lowerTitle)) score += 4;
|
||
if (lowerTitle.includes('hdr')) score += 3;
|
||
if (lowerTitle.includes('1080p')) score += 2;
|
||
if (lowerTitle.includes('hevc') || lowerTitle.includes('x265')) score += 1;
|
||
|
||
// Season coverage scoring (for TV shows)
|
||
if (requestedSeason !== null) {
|
||
// Check for season range formats like "Season 1 – 2" or "Season 1-2"
|
||
const seasonRangeMatch = lowerTitle.match(/season\s+(\d+)\s*[–-]\s*(\d+)/i);
|
||
if (seasonRangeMatch) {
|
||
const startSeason = parseInt(seasonRangeMatch[1], 10);
|
||
const endSeason = parseInt(seasonRangeMatch[2], 10);
|
||
if (requestedSeason >= startSeason && requestedSeason <= endSeason) {
|
||
score += 50; // High bonus for season range that includes requested season
|
||
console.log(`[UHDMovies] Season range bonus (+50): ${startSeason}-${endSeason} includes requested season ${requestedSeason}`);
|
||
}
|
||
}
|
||
|
||
// Check for specific season mentions
|
||
const specificSeasonMatch = lowerTitle.match(/season\s+(\d+)/i);
|
||
if (specificSeasonMatch) {
|
||
const mentionedSeason = parseInt(specificSeasonMatch[1], 10);
|
||
if (mentionedSeason === requestedSeason) {
|
||
score += 30; // Good bonus for exact season match
|
||
console.log(`[UHDMovies] Exact season bonus (+30): Season ${mentionedSeason} matches requested season ${requestedSeason}`);
|
||
} else if (mentionedSeason < requestedSeason) {
|
||
score -= 20; // Penalty for older season when requesting newer season
|
||
console.log(`[UHDMovies] Season penalty (-20): Season ${mentionedSeason} is older than requested season ${requestedSeason}`);
|
||
}
|
||
}
|
||
|
||
// Check for "Season X Added" or similar indicators
|
||
if (lowerTitle.includes('season') && lowerTitle.includes('added')) {
|
||
const addedSeasonMatch = lowerTitle.match(/season\s+(\d+)\s+added/i);
|
||
if (addedSeasonMatch) {
|
||
const addedSeason = parseInt(addedSeasonMatch[1], 10);
|
||
if (addedSeason === requestedSeason) {
|
||
score += 40; // High bonus for newly added season
|
||
console.log(`[UHDMovies] Added season bonus (+40): Season ${addedSeason} was recently added`);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return score;
|
||
}
|
||
|
||
// Function to parse size string into MB
|
||
function parseSize(sizeString) {
|
||
if (!sizeString || typeof sizeString !== 'string') {
|
||
return 0;
|
||
}
|
||
|
||
const upperCaseSizeString = sizeString.toUpperCase();
|
||
|
||
// Regex to find a number (integer or float) followed by GB, MB, or KB
|
||
const match = upperCaseSizeString.match(/([0-9.,]+)\s*(GB|MB|KB)/);
|
||
|
||
if (!match) {
|
||
return 0;
|
||
}
|
||
|
||
const sizeValue = parseFloat(match[1].replace(/,/g, ''));
|
||
if (isNaN(sizeValue)) {
|
||
return 0;
|
||
}
|
||
|
||
const unit = match[2];
|
||
|
||
if (unit === 'GB') {
|
||
return sizeValue * 1024;
|
||
} else if (unit === 'MB') {
|
||
return sizeValue;
|
||
} else if (unit === 'KB') {
|
||
return sizeValue / 1024;
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
// Helper function to extract cookies from jar for a specific URL
|
||
const getCookiesForUrl = async (jar, url) => {
|
||
try {
|
||
const cookies = await jar.getCookies(url);
|
||
if (cookies && cookies.length > 0) {
|
||
return cookies.map(cookie => cookie.toString()).join('; ');
|
||
}
|
||
} catch (error) {
|
||
console.log(`[UHDMovies] Error extracting cookies for ${url}: ${error.message}`);
|
||
}
|
||
return null;
|
||
};
|
||
|
||
// Helper function to create a session for SID resolution
|
||
const createSession = async (jar) => {
|
||
const { wrapper } = await getAxiosCookieJarSupport();
|
||
|
||
const sessionConfig = {
|
||
jar,
|
||
headers: {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36',
|
||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
|
||
'Accept-Language': 'en-US,en;q=0.5',
|
||
'Connection': 'keep-alive',
|
||
'Upgrade-Insecure-Requests': '1'
|
||
}
|
||
};
|
||
|
||
const session = wrapper(axios.create(sessionConfig));
|
||
|
||
|
||
|
||
return session;
|
||
};
|
||
|
||
// New function to resolve the tech.unblockedgames.world links
|
||
async function resolveSidToDriveleech(sidUrl) {
|
||
console.log(`[UHDMovies] Resolving SID link: ${sidUrl}`);
|
||
const { origin } = new URL(sidUrl);
|
||
const jar = new CookieJar();
|
||
|
||
// Create session
|
||
const session = await createSession(jar);
|
||
|
||
try {
|
||
// Step 0: Get the _wp_http value
|
||
console.log(" [SID] Step 0: Fetching initial page...");
|
||
const responseStep0 = await session.get(sidUrl);
|
||
let $ = cheerio.load(responseStep0.data);
|
||
const initialForm = $('#landing');
|
||
const wp_http_step1 = initialForm.find('input[name="_wp_http"]').val();
|
||
const action_url_step1 = initialForm.attr('action');
|
||
|
||
if (!wp_http_step1 || !action_url_step1) {
|
||
console.error(" [SID] Error: Could not find _wp_http in initial form.");
|
||
return null;
|
||
}
|
||
|
||
// Step 1: POST to the first form's action URL
|
||
console.log(" [SID] Step 1: Submitting initial form...");
|
||
const step1Data = new URLSearchParams({ '_wp_http': wp_http_step1 });
|
||
const responseStep1 = await session.post(action_url_step1, step1Data, {
|
||
headers: { 'Referer': sidUrl, 'Content-Type': 'application/x-www-form-urlencoded' }
|
||
});
|
||
|
||
// Step 2: Parse verification page for second form
|
||
console.log(" [SID] Step 2: Parsing verification page...");
|
||
$ = cheerio.load(responseStep1.data);
|
||
const verificationForm = $('#landing');
|
||
const action_url_step2 = verificationForm.attr('action');
|
||
const wp_http2 = verificationForm.find('input[name="_wp_http2"]').val();
|
||
const token = verificationForm.find('input[name="token"]').val();
|
||
|
||
if (!action_url_step2) {
|
||
console.error(" [SID] Error: Could not find verification form.");
|
||
return null;
|
||
}
|
||
|
||
// Step 3: POST to the verification URL
|
||
console.log(" [SID] Step 3: Submitting verification...");
|
||
const step2Data = new URLSearchParams({ '_wp_http2': wp_http2, 'token': token });
|
||
const responseStep2 = await session.post(action_url_step2, step2Data, {
|
||
headers: { 'Referer': responseStep1.request.res.responseUrl, 'Content-Type': 'application/x-www-form-urlencoded' }
|
||
});
|
||
|
||
// Step 4: Find dynamic cookie and link from JavaScript
|
||
console.log(" [SID] Step 4: Parsing final page for JS data...");
|
||
let finalLinkPath = null;
|
||
let cookieName = null;
|
||
let cookieValue = null;
|
||
|
||
const scriptContent = responseStep2.data;
|
||
const cookieMatch = scriptContent.match(/s_343\('([^']+)',\s*'([^']+)'/);
|
||
const linkMatch = scriptContent.match(/c\.setAttribute\("href",\s*"([^"]+)"\)/);
|
||
|
||
if (cookieMatch) {
|
||
cookieName = cookieMatch[1].trim();
|
||
cookieValue = cookieMatch[2].trim();
|
||
}
|
||
if (linkMatch) {
|
||
finalLinkPath = linkMatch[1].trim();
|
||
}
|
||
|
||
if (!finalLinkPath || !cookieName || !cookieValue) {
|
||
console.error(" [SID] Error: Could not extract dynamic cookie/link from JS.");
|
||
return null;
|
||
}
|
||
|
||
const finalUrl = new URL(finalLinkPath, origin).href;
|
||
console.log(` [SID] Dynamic link found: ${finalUrl}`);
|
||
console.log(` [SID] Dynamic cookie found: ${cookieName}`);
|
||
|
||
// Step 5: Set cookie and make final request
|
||
console.log(" [SID] Step 5: Setting cookie and making final request...");
|
||
await jar.setCookie(`${cookieName}=${cookieValue}`, origin);
|
||
|
||
const finalResponse = await session.get(finalUrl, {
|
||
headers: { 'Referer': responseStep2.request.res.responseUrl }
|
||
});
|
||
|
||
// Step 6: Extract driveleech URL from meta refresh tag
|
||
$ = cheerio.load(finalResponse.data);
|
||
const metaRefresh = $('meta[http-equiv="refresh"]');
|
||
if (metaRefresh.length > 0) {
|
||
const content = metaRefresh.attr('content');
|
||
const urlMatch = content.match(/url=(.*)/i);
|
||
if (urlMatch && urlMatch[1]) {
|
||
const driveleechUrl = urlMatch[1].replace(/"/g, "").replace(/'/g, "");
|
||
console.log(` [SID] SUCCESS! Resolved Driveleech URL: ${driveleechUrl}`);
|
||
return driveleechUrl;
|
||
}
|
||
}
|
||
|
||
console.error(" [SID] Error: Could not find meta refresh tag with Driveleech URL.");
|
||
return null;
|
||
|
||
} catch (error) {
|
||
console.error(` [SID] Error during SID resolution: ${error.message}`);
|
||
if (error.response) {
|
||
console.error(` [SID] Status: ${error.response.status}`);
|
||
}
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// Main function to get streams for TMDB content
|
||
async function getUHDMoviesStreams(tmdbId, mediaType = 'movie', season = null, episode = null) {
|
||
console.log(`[UHDMovies] Attempting to fetch streams for TMDB ID: ${tmdbId}, Type: ${mediaType}${mediaType === 'tv' ? `, S:${season}E:${episode}` : ''}`);
|
||
|
||
const cacheKey = `uhd_final_v18_${tmdbId}_${mediaType}${season ? `_s${season}e${episode}` : ''}`;
|
||
|
||
try {
|
||
// 1. Check cache first
|
||
let cachedLinks = await getFromCache(cacheKey);
|
||
if (cachedLinks && cachedLinks.length > 0) {
|
||
console.log(`[UHDMovies] Cache HIT for ${cacheKey}. Using ${cachedLinks.length} cached Driveleech links.`);
|
||
} else {
|
||
if (cachedLinks && cachedLinks.length === 0) {
|
||
console.log(`[UHDMovies] Cache contains empty data for ${cacheKey}. Refetching from source.`);
|
||
} else {
|
||
console.log(`[UHDMovies] Cache MISS for ${cacheKey}. Fetching from source.`);
|
||
}
|
||
console.log(`[UHDMovies] Cache MISS for ${cacheKey}. Fetching from source.`);
|
||
// 2. If cache miss, get TMDB info to perform search
|
||
const TMDB_API_KEY_UHDMOVIES = getUhdMoviesKey();
|
||
const tmdbUrl = `https://api.themoviedb.org/3/${mediaType === 'tv' ? 'tv' : 'movie'}/${tmdbId}?api_key=${TMDB_API_KEY_UHDMOVIES}`;
|
||
const tmdbResponse = await axios.get(tmdbUrl);
|
||
const tmdbData = tmdbResponse.data;
|
||
const mediaInfo = {
|
||
title: mediaType === 'tv' ? tmdbData.name : tmdbData.title,
|
||
year: parseInt(((mediaType === 'tv' ? tmdbData.first_air_date : tmdbData.release_date) || '').split('-')[0], 10)
|
||
};
|
||
|
||
if (!mediaInfo.title) throw new Error('Could not extract title from TMDB response.');
|
||
console.log(`[UHDMovies] TMDB Info: "${mediaInfo.title}" (${mediaInfo.year || 'N/A'})`);
|
||
|
||
// 3. Search for the media on UHDMovies
|
||
const searchTitle = mediaInfo.title.replace(/:/g, '').replace(/\s*&\s*/g, ' and ');
|
||
let searchResults = await searchMovies(searchTitle);
|
||
|
||
// If no results or only wrong year results, try fallback search with just main title
|
||
if (searchResults.length === 0 || !searchResults.some(result => compareMedia(mediaInfo, result))) {
|
||
console.log(`[UHDMovies] Primary search failed or no matches. Trying fallback search...`);
|
||
|
||
// Extract main title (remove subtitles after colon, "and the", etc.)
|
||
let fallbackTitle = mediaInfo.title.split(':')[0].trim();
|
||
if (fallbackTitle.includes('and the')) {
|
||
fallbackTitle = fallbackTitle.split('and the')[0].trim();
|
||
}
|
||
if (fallbackTitle !== searchTitle) {
|
||
console.log(`[UHDMovies] Fallback search with: "${fallbackTitle}"`);
|
||
const fallbackResults = await searchMovies(fallbackTitle);
|
||
if (fallbackResults.length > 0) {
|
||
searchResults = fallbackResults;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (searchResults.length === 0) {
|
||
console.log(`[UHDMovies] No search results found for "${mediaInfo.title}".`);
|
||
// Don't cache empty results to allow retrying later
|
||
return [];
|
||
}
|
||
|
||
// 4. Find the best matching result
|
||
const matchingResults = searchResults.filter(result => compareMedia(mediaInfo, result));
|
||
|
||
if (matchingResults.length === 0) {
|
||
console.log(`[UHDMovies] No matching content found for "${mediaInfo.title}" (${mediaInfo.year}).`);
|
||
// Don't cache empty results to allow retrying later
|
||
return [];
|
||
}
|
||
|
||
let matchingResult;
|
||
let scoredResults = null; // Declare outside the conditional
|
||
|
||
if (matchingResults.length === 1) {
|
||
matchingResult = matchingResults[0];
|
||
} else {
|
||
console.log(`[UHDMovies] Found ${matchingResults.length} matching results. Scoring to find the best...`);
|
||
|
||
scoredResults = matchingResults.map(result => {
|
||
const score = scoreResult(result.title, mediaType === 'tv' ? season : null);
|
||
console.log(` - Score ${score}: ${result.title}`);
|
||
return { ...result, score };
|
||
}).sort((a, b) => b.score - a.score);
|
||
|
||
matchingResult = scoredResults[0];
|
||
console.log(`[UHDMovies] Best match selected with score ${matchingResult.score}: "${matchingResult.title}"`);
|
||
}
|
||
|
||
console.log(`[UHDMovies] Found matching content: "${matchingResult.title}"`);
|
||
|
||
// 5. Extract SID links from the movie/show page
|
||
let downloadInfo = await (mediaType === 'tv' ? extractTvShowDownloadLinks(matchingResult.link, season, episode) : extractDownloadLinks(matchingResult.link, mediaInfo.year));
|
||
|
||
// Check if season was not found or episode extraction failed, and we have multiple results to try
|
||
if (downloadInfo.links.length === 0 && matchingResults.length > 1 && scoredResults &&
|
||
(downloadInfo.seasonNotFound || (mediaType === 'tv' && downloadInfo.title))) {
|
||
console.log(`[UHDMovies] Season ${season} not found or episode extraction failed on best match. Trying next best match...`);
|
||
|
||
// Try the next best match
|
||
const nextBestMatch = scoredResults[1];
|
||
console.log(`[UHDMovies] Trying next best match: "${nextBestMatch.title}"`);
|
||
|
||
downloadInfo = await (mediaType === 'tv' ? extractTvShowDownloadLinks(nextBestMatch.link, season, episode) : extractDownloadLinks(nextBestMatch.link, mediaInfo.year));
|
||
|
||
if (downloadInfo.links.length > 0) {
|
||
console.log(`[UHDMovies] Successfully found links on next best match!`);
|
||
} else {
|
||
console.log(`[UHDMovies] Next best match also failed. No download links found.`);
|
||
}
|
||
}
|
||
|
||
if (downloadInfo.links.length === 0) {
|
||
console.log('[UHDMovies] No download links found on page.');
|
||
// Don't cache empty results to allow retrying later
|
||
return [];
|
||
}
|
||
|
||
// 6. Resolve all SID links to driveleech redirect URLs (intermediate step)
|
||
console.log(`[UHDMovies] Resolving ${downloadInfo.links.length} SID link(s) to driveleech redirect URLs...`);
|
||
const resolutionPromises = downloadInfo.links.map(async (linkInfo) => {
|
||
try {
|
||
let driveleechUrl = null;
|
||
|
||
if (linkInfo.link && (linkInfo.link.includes('tech.unblockedgames.world') || linkInfo.link.includes('tech.creativeexpressionsblog.com') || linkInfo.link.includes('tech.examzculture.in'))) {
|
||
driveleechUrl = await resolveSidToDriveleech(linkInfo.link);
|
||
} else if (linkInfo.link && (linkInfo.link.includes('driveseed.org') || linkInfo.link.includes('driveleech.net'))) {
|
||
// If it's already a direct driveseed/driveleech link, use it
|
||
driveleechUrl = linkInfo.link;
|
||
}
|
||
|
||
if (!driveleechUrl) return null;
|
||
|
||
console.log(`[UHDMovies] Caching driveleech redirect URL for ${linkInfo.quality}: ${driveleechUrl}`);
|
||
return { ...linkInfo, driveleechRedirectUrl: driveleechUrl };
|
||
} catch (error) {
|
||
console.error(`[UHDMovies] Error resolving ${linkInfo.quality}: ${error.message}`);
|
||
return null;
|
||
}
|
||
});
|
||
|
||
cachedLinks = (await Promise.all(resolutionPromises)).filter(Boolean);
|
||
|
||
// 7. Save the successfully resolved driveleech redirect URLs to the cache
|
||
if (cachedLinks.length > 0) {
|
||
console.log(`[UHDMovies] Caching ${cachedLinks.length} resolved driveleech redirect URLs for key: ${cacheKey}`);
|
||
await saveToCache(cacheKey, cachedLinks);
|
||
} else {
|
||
console.log(`[UHDMovies] No driveleech redirect URLs could be resolved. Not caching to allow retrying later.`);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
if (!cachedLinks || cachedLinks.length === 0) {
|
||
console.log('[UHDMovies] No final file page URLs found after scraping/cache check.');
|
||
return [];
|
||
}
|
||
|
||
// 8. Process all cached driveleech redirect URLs to get streaming links
|
||
console.log(`[UHDMovies] Processing ${cachedLinks.length} cached driveleech redirect URL(s) to get streaming links.`);
|
||
const streamPromises = cachedLinks.map(async (linkInfo) => {
|
||
try {
|
||
// First, resolve the driveleech redirect URL to get the final file page URL
|
||
const response = await makeRequest(linkInfo.driveleechRedirectUrl, { maxRedirects: 10 });
|
||
let $ = cheerio.load(response.data);
|
||
|
||
// Check for JavaScript redirect (window.location.replace)
|
||
const scriptContent = $('script').html();
|
||
const redirectMatch = scriptContent && scriptContent.match(/window\.location\.replace\("([^"]+)"\)/);
|
||
|
||
let finalFilePageUrl = linkInfo.driveleechRedirectUrl;
|
||
if (redirectMatch && redirectMatch[1]) {
|
||
finalFilePageUrl = new URL(redirectMatch[1], 'https://driveleech.net/').href;
|
||
console.log(`[UHDMovies] Resolved redirect to final file page: ${finalFilePageUrl}`);
|
||
|
||
// Load the final file page
|
||
const finalResponse = await makeRequest(finalFilePageUrl, { maxRedirects: 10 });
|
||
$ = cheerio.load(finalResponse.data);
|
||
}
|
||
|
||
// Extract file size and name information
|
||
let sizeInfo = 'Unknown';
|
||
let fileName = null;
|
||
|
||
const sizeElement = $('li.list-group-item:contains("Size :")').text();
|
||
if (sizeElement) {
|
||
const sizeMatch = sizeElement.match(/Size\s*:\s*([0-9.,]+\s*[KMGT]B)/);
|
||
if (sizeMatch) {
|
||
sizeInfo = sizeMatch[1];
|
||
}
|
||
}
|
||
|
||
const nameElement = $('li.list-group-item:contains("Name :")');
|
||
if (nameElement.length > 0) {
|
||
fileName = nameElement.text().replace('Name :', '').trim();
|
||
} else {
|
||
const h5Title = $('div.card-header h5').clone().children().remove().end().text().trim();
|
||
if (h5Title) {
|
||
fileName = h5Title.replace(/\[.*\]/, '').trim();
|
||
}
|
||
}
|
||
|
||
// Try download methods to get final streaming URL
|
||
const downloadMethods = [
|
||
{ name: 'Resume Cloud', func: tryResumeCloud },
|
||
{ name: 'Instant Download', func: tryInstantDownload }
|
||
];
|
||
|
||
for (const method of downloadMethods) {
|
||
try {
|
||
const finalUrl = await method.func($);
|
||
|
||
if (finalUrl) {
|
||
const isValid = await validateVideoUrl(finalUrl);
|
||
if (isValid) {
|
||
const rawQuality = linkInfo.rawQuality || '';
|
||
const codecs = extractCodecs(rawQuality);
|
||
const cleanFileName = fileName ? fileName.replace(/\.[^/.]+$/, "").replace(/[._]/g, ' ') : (linkInfo.quality || 'Unknown');
|
||
|
||
return {
|
||
name: `UHDMovies`,
|
||
title: `${cleanFileName}\n${sizeInfo}`,
|
||
url: finalUrl,
|
||
quality: linkInfo.quality,
|
||
provider: 'uhdmovies',
|
||
headers: {}
|
||
};
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.log(`[UHDMovies] ${method.name} failed: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
return null;
|
||
} catch (error) {
|
||
console.error(`[UHDMovies] Error processing cached driveleech redirect ${linkInfo.driveleechRedirectUrl}: ${error.message}`);
|
||
return null;
|
||
}
|
||
});
|
||
|
||
const streams = (await Promise.all(streamPromises)).filter(Boolean);
|
||
console.log(`[UHDMovies] Successfully processed ${streams.length} final stream links.`);
|
||
|
||
// Sort final streams by size
|
||
streams.sort((a, b) => {
|
||
const sizeA = parseSize(a.size);
|
||
const sizeB = parseSize(b.size);
|
||
return sizeB - sizeA;
|
||
});
|
||
|
||
return streams;
|
||
} catch (error) {
|
||
console.error(`[UHDMovies] A critical error occurred in getUHDMoviesStreams for ${tmdbId}: ${error.message}`);
|
||
if (error.stack) console.error(error.stack);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
module.exports = { getUHDMoviesStreams }; |