diff --git a/ScraperforTesting/uhdmovies.js b/ScraperforTesting/uhdmovies.js deleted file mode 100644 index aa325b46..00000000 --- a/ScraperforTesting/uhdmovies.js +++ /dev/null @@ -1,1234 +0,0 @@ -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'); -const RedisCache = require('../utils/redisCache'); - -// Dynamic import for axios-cookiejar-support -let axiosCookieJarSupport = null; -const getAxiosCookieJarSupport = async () => { - if (!axiosCookieJarSupport) { - axiosCookieJarSupport = await import('axios-cookiejar-support'); - } - return axiosCookieJarSupport; -}; - -// --- 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 axios.get('https://raw.githubusercontent.com/phisher98/TVVVV/refs/heads/main/domains.json'); - 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 TMDB_API_KEY_UHDMOVIES = "439c478a771f35c05022f9feabcca01c"; // Public TMDB API key - -// --- 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 - -// Initialize Redis cache -const redisCache = new RedisCache('UHDMovies'); - -// --- 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; - - // Try Redis cache first, then fallback to file system - const cachedData = await redisCache.getFromCache(key, '', CACHE_DIR); - if (cachedData) { - return cachedData.data || cachedData; // Support both new format (data field) and legacy format - } - - return null; -}; - -const saveToCache = async (key, data) => { - if (!CACHE_ENABLED) return; - - const cacheData = { - data: data - }; - - // Save to both Redis and file system - await redisCache.saveToCache(key, cacheData, '', CACHE_DIR); -}; - -// Initialize cache directory on startup -ensureCacheDir(); - -// Configure axios with headers to mimic a browser -const axiosInstance = axios.create({ - 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' - } -}); - -// Simple In-Memory Cache -const uhdMoviesCache = { - search: {}, - movie: {}, - show: {} -}; - -// 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 axiosInstance.get(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(); - let 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 axiosInstance.get(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
 or a 

with ) - // 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 }); - } - } - } - } - }); - - if (downloadLinks.length === 0) { - console.log('[UHDMovies] Main extraction logic failed. Trying fallback method without season scoping.'); - $('.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; - } - } - - 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 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 axiosInstance.get(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 axiosInstance.get(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' // Just request first byte to test - } - }); - - // 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; - } -} - -// Function to follow redirect links and get the final download URL with size info -async function getFinalLink(redirectUrl) { - try { - console.log(`[UHDMovies] Following redirect: ${redirectUrl}`); - - // Request the driveleech page - let response = await axiosInstance.get(redirectUrl, { maxRedirects: 10 }); - let $ = cheerio.load(response.data); - - // --- Check for JavaScript redirect --- - const scriptContent = $('script').html(); - const redirectMatch = scriptContent && scriptContent.match(/window\.location\.replace\("([^"]+)"\)/); - - if (redirectMatch && redirectMatch[1]) { - const newPath = redirectMatch[1]; - const newUrl = new URL(newPath, 'https://driveleech.net/').href; - console.log(`[UHDMovies] Found JavaScript redirect. Following to: ${newUrl}`); - response = await axiosInstance.get(newUrl, { maxRedirects: 10 }); - $ = cheerio.load(response.data); - } - - // Extract size and filename information from the page - 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)/i); - if (sizeMatch) sizeInfo = sizeMatch[1]; - } - - const nameElement = $('li.list-group-item:contains("Name :")').text(); - if (nameElement) { - fileName = nameElement.replace('Name :', '').trim(); - } - - // Try each download method in order until we find a working one - const downloadMethods = [ - { name: 'Resume Cloud', func: tryResumeCloud }, - { name: 'Instant Download', func: tryInstantDownload } - ]; - - for (const method of downloadMethods) { - try { - console.log(`[UHDMovies] Trying ${method.name}...`); - const finalUrl = await method.func($); - - if (finalUrl) { - // Validate the URL before using it - const isValid = await validateVideoUrl(finalUrl); - if (isValid) { - console.log(`[UHDMovies] ✓ Successfully resolved using ${method.name}`); - return { url: finalUrl, size: sizeInfo, fileName: fileName }; - } else { - console.log(`[UHDMovies] ✗ ${method.name} returned invalid/broken URL, trying next method...`); - } - } else { - console.log(`[UHDMovies] ✗ ${method.name} failed to resolve URL, trying next method...`); - } - } catch (error) { - console.log(`[UHDMovies] ✗ ${method.name} threw error: ${error.message}, trying next method...`); - } - } - - console.log('[UHDMovies] ✗ All download methods failed.'); - return null; - - } catch (error) { - console.error(`[UHDMovies] Error in getFinalLink: ${error.message}`); - return null; - } -} - -// 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 -function scoreResult(title) { - let score = 0; - const lowerTitle = title.toLowerCase(); - - 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; - - 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; -} - -// 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(); - - // Get the wrapper function from dynamic import - const { wrapper } = await getAxiosCookieJarSupport(); - const session = wrapper(axios.create({ - 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' - } - })); - - 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_v12_${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 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 - let 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; - - if (matchingResults.length === 1) { - matchingResult = matchingResults[0]; - } else { - console.log(`[UHDMovies] Found ${matchingResults.length} matching results. Scoring to find the best...`); - - const scoredResults = matchingResults.map(result => { - const score = scoreResult(result.title); - 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 - const downloadInfo = await (mediaType === 'tv' ? extractTvShowDownloadLinks(matchingResult.link, season, episode) : extractDownloadLinks(matchingResult.link, mediaInfo.year)); - 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 axiosInstance.get(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 axiosInstance.get(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, - size: sizeInfo, - fileName: fileName, - fullTitle: rawQuality, - codecs: codecs, - behaviorHints: { bingeGroup: `uhdmovies-${linkInfo.quality}` } - }; - } - } - } 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 }; \ No newline at end of file