/** * MoviesMod provider (standalone API) * Supports both movies and TV series */ const axios = require('axios'); const cheerio = require('cheerio'); const FormData = require('form-data'); const { CookieJar } = require('tough-cookie'); const { URLSearchParams, URL } = require('url'); const fs = require('fs').promises; const path = require('path'); const { findBestMatch } = require('string-similarity'); // Dynamic import for axios-cookiejar-support let axiosCookieJarSupport = null; const getAxiosCookieJarSupport = async () => { if (!axiosCookieJarSupport) { axiosCookieJarSupport = await import('axios-cookiejar-support'); } return axiosCookieJarSupport; }; function escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string } // --- Domain Fetching --- let moviesModDomain = 'https://moviesmod.chat'; // Fallback domain let domainCacheTimestamp = 0; const DOMAIN_CACHE_TTL = 4 * 60 * 60 * 1000; // 4 hours async function getMoviesModDomain() { const now = Date.now(); if (now - domainCacheTimestamp < DOMAIN_CACHE_TTL) { return moviesModDomain; } try { console.log('[MoviesMod] 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.moviesmod) { moviesModDomain = response.data.moviesmod; domainCacheTimestamp = now; console.log(`[MoviesMod] Updated domain to: ${moviesModDomain}`); } else { console.warn('[MoviesMod] Domain JSON fetched, but "moviesmod" key was not found. Using fallback.'); } } catch (error) { console.error(`[MoviesMod] Failed to fetch latest domain, using fallback. Error: ${error.message}`); } return moviesModDomain; } // --- Caching Configuration --- const CACHE_ENABLED = process.env.DISABLE_CACHE !== 'true'; console.log(`[MoviesMod Cache] Internal cache is ${CACHE_ENABLED ? 'enabled' : 'disabled'}.`); const CACHE_DIR = process.env.VERCEL ? path.join('/tmp', '.moviesmod_cache') : path.join(__dirname, '.cache', 'moviesmod'); // 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(`[MoviesMod 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(); // Request helper const makeRequest = async (url, options = {}) => { console.log(`[MoviesMod] Making direct request to: ${url}`); return axios.get(url, options); }; // 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)); // Helper function to extract cookies from jar for a given URL const getCookiesForUrl = async (url) => { try { const cookies = await jar.getCookies(url); return cookies.map(cookie => `${cookie.key}=${cookie.value}`).join('; '); } catch (error) { console.error(`[MoviesMod] Error getting cookies for ${url}: ${error.message}`); return ''; } }; return session; }; // Helper function to extract quality from text function extractQuality(text) { if (!text) return 'Unknown'; const qualityMatch = text.match(/(480p|720p|1080p|2160p|4k)/i); if (qualityMatch) { return qualityMatch[1]; } // Try to extract from full text const cleanMatch = text.match(/(480p|720p|1080p|2160p|4k)[^)]*\)/i); if (cleanMatch) { return cleanMatch[0]; } return 'Unknown'; } function parseQualityForSort(qualityString) { if (!qualityString) return 0; const match = qualityString.match(/(\d{3,4})p/i); return match ? parseInt(match[1], 10) : 0; } function getTechDetails(qualityString) { if (!qualityString) return []; const details = []; const lowerText = qualityString.toLowerCase(); if (lowerText.includes('10bit')) details.push('10-bit'); if (lowerText.includes('hevc') || lowerText.includes('x265')) details.push('HEVC'); if (lowerText.includes('hdr')) details.push('HDR'); return details; } // Search for content on MoviesMod async function searchMoviesMod(query) { try { const baseUrl = await getMoviesModDomain(); const searchUrl = `${baseUrl}/?s=${encodeURIComponent(query)}`; const { data } = await makeRequest(searchUrl); const $ = cheerio.load(data); const results = []; $('.latestPost').each((i, element) => { const linkElement = $(element).find('a'); const title = linkElement.attr('title'); const url = linkElement.attr('href'); if (title && url) { results.push({ title, url }); } }); return results; } catch (error) { console.error(`[MoviesMod] Error searching: ${error.message}`); return []; } } // Extract download links from a movie/series page async function extractDownloadLinks(moviePageUrl) { try { const { data } = await makeRequest(moviePageUrl); const $ = cheerio.load(data); const links = []; const contentBox = $('.thecontent'); // Get all relevant headers (for movies and TV shows) in document order const headers = contentBox.find('h3:contains("Season"), h4'); headers.each((i, el) => { const header = $(el); const headerText = header.text().trim(); // Define the content block for this header const blockContent = header.nextUntil('h3, h4'); if (header.is('h3') && headerText.toLowerCase().includes('season')) { // TV Show Logic const linkElements = blockContent.find('a.maxbutton-episode-links, a.maxbutton-batch-zip'); linkElements.each((j, linkEl) => { const buttonText = $(linkEl).text().trim(); const linkUrl = $(linkEl).attr('href'); if (linkUrl && !buttonText.toLowerCase().includes('batch')) { links.push({ quality: `${headerText} - ${buttonText}`, url: linkUrl }); } }); } else if (header.is('h4')) { // Movie Logic const linkElement = blockContent.find('a[href*="modrefer.in"]').first(); if (linkElement.length > 0) { const link = linkElement.attr('href'); const cleanQuality = extractQuality(headerText); links.push({ quality: cleanQuality, url: link }); } } }); return links; } catch (error) { console.error(`[MoviesMod] Error extracting download links: ${error.message}`); return []; } } // Resolve intermediate links (dramadrip, episodes.modpro.blog, modrefer.in) async function resolveIntermediateLink(initialUrl, refererUrl, quality) { try { const urlObject = new URL(initialUrl); if (urlObject.hostname.includes('dramadrip.com')) { const { data: dramaData } = await makeRequest(initialUrl, { headers: { 'Referer': refererUrl } }); const $$ = cheerio.load(dramaData); let episodePageLink = null; const seasonMatch = quality.match(/Season \d+/i); // Extract the specific quality details, e.g., "1080p x264" const specificQualityMatch = quality.match(/(480p|720p|1080p|2160p|4k)[ \w\d-]*/i); if (seasonMatch && specificQualityMatch) { const seasonIdentifier = seasonMatch[0].toLowerCase(); // Clean up the identifier to get only the essential parts const specificQualityIdentifier = specificQualityMatch[0].toLowerCase().replace(/msubs.*/i, '').replace(/esubs.*/i, '').replace(/\{.*/, '').trim(); const qualityParts = specificQualityIdentifier.split(/\s+/); // -> ['1080p', 'x264'] $$('a[href*="episodes.modpro.blog"], a[href*="cinematickit.org"]').each((i, el) => { const link = $$(el); const linkText = link.text().trim().toLowerCase(); const seasonHeader = link.closest('.wp-block-buttons').prevAll('h2.wp-block-heading').first().text().trim().toLowerCase(); const seasonIsMatch = seasonHeader.includes(seasonIdentifier); // Ensure that the link text contains all parts of our specific quality const allPartsMatch = qualityParts.every(part => linkText.includes(part)); if (seasonIsMatch && allPartsMatch) { episodePageLink = link.attr('href'); console.log(`[MoviesMod] Found specific match for "${quality}" -> "${link.text().trim()}": ${episodePageLink}`); return false; // Break loop, we found our specific link } }); } if (!episodePageLink) { console.error(`[MoviesMod] Could not find a specific quality match on dramadrip page for: ${quality}`); return []; } // Pass quality to recursive call return await resolveIntermediateLink(episodePageLink, initialUrl, quality); } else if (urlObject.hostname.includes('cinematickit.org')) { // Handle cinematickit.org pages const { data } = await makeRequest(initialUrl, { headers: { 'Referer': refererUrl } }); const $ = cheerio.load(data); const finalLinks = []; // Look for episode links on cinematickit.org $('a[href*="driveseed.org"]').each((i, el) => { const link = $(el).attr('href'); const text = $(el).text().trim(); if (link && text && !text.toLowerCase().includes('batch')) { finalLinks.push({ server: text.replace(/\s+/g, ' '), url: link, }); } }); // If no driveseed links found, try other patterns if (finalLinks.length === 0) { $('a[href*="modrefer.in"], a[href*="dramadrip.com"]').each((i, el) => { const link = $(el).attr('href'); const text = $(el).text().trim(); if (link && text) { finalLinks.push({ server: text.replace(/\s+/g, ' '), url: link, }); } }); } return finalLinks; } else if (urlObject.hostname.includes('episodes.modpro.blog')) { const { data } = await makeRequest(initialUrl, { headers: { 'Referer': refererUrl } }); const $ = cheerio.load(data); const finalLinks = []; $('.entry-content a[href*="driveseed.org"], .entry-content a[href*="tech.unblockedgames.world"], .entry-content a[href*="tech.creativeexpressionsblog.com"], .entry-content a[href*="tech.examzculture.in"]').each((i, el) => { const link = $(el).attr('href'); const text = $(el).text().trim(); if (link && text && !text.toLowerCase().includes('batch')) { finalLinks.push({ server: text.replace(/\s+/g, ' '), url: link, }); } }); return finalLinks; } else if (urlObject.hostname.includes('modrefer.in')) { const encodedUrl = urlObject.searchParams.get('url'); if (!encodedUrl) { console.error('[MoviesMod] Could not find encoded URL in modrefer.in link.'); return []; } const decodedUrl = Buffer.from(encodedUrl, 'base64').toString('utf8'); const { data } = await makeRequest(decodedUrl, { 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', 'Referer': refererUrl, } }); const $ = cheerio.load(data); const finalLinks = []; $('.timed-content-client_show_0_5_0 a').each((i, el) => { const link = $(el).attr('href'); const text = $(el).text().trim(); if (link) { finalLinks.push({ server: text, url: link, }); } }); return finalLinks; } else { console.warn(`[MoviesMod] Unknown hostname: ${urlObject.hostname}`); return []; } } catch (error) { console.error(`[MoviesMod] Error resolving intermediate link: ${error.message}`); return []; } } // Function to resolve tech.unblockedgames.world links to driveleech URLs (adapted from UHDMovies) async function resolveTechUnblockedLink(sidUrl) { console.log(`[MoviesMod] 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; } } // Resolve Resume Cloud link to final download URL async function resolveResumeCloudLink(resumeUrl) { try { const { data } = await makeRequest(resumeUrl, { 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', 'Referer': 'https://driveseed.org/', } }); const $ = cheerio.load(data); const downloadLink = $('a:contains("Cloud Resume Download")').attr('href'); return downloadLink || null; } catch (error) { console.error(`[MoviesMod] Error resolving Resume Cloud link: ${error.message}`); return null; } } // Resolve Worker Seed link to final download URL async function resolveWorkerSeedLink(workerSeedUrl) { try { console.log(`[MoviesMod] Resolving Worker-seed link: ${workerSeedUrl}`); 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/91.0.4472.124 Safari/537.36', } })); // Step 1: GET the page to get the script content and cookies console.log(`[MoviesMod] Step 1: Fetching page to get script content and cookies...`); const { data: pageHtml } = await session.get(workerSeedUrl); // Step 2: Use regex to extract the token and the correct ID from the script const scriptTags = pageHtml.match(/