diff --git a/CHANGELOG.md b/CHANGELOG.md index 565e457..1680020 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to this project will be documented in this file. +## [1.1.1] - 2026-05-29 + +### Removed +- **MoviesMod provider**: Permanently removed — `moviesmod.farm` returns HTTP 403 with Cloudflare managed challenge (`cf-mitigated: challenge`). No server-side workaround exists without a browser runtime. +- **VidZee provider**: Permanently removed — hardcoded AES-256-GCM salt is no longer valid against VidZee's current API key blob, causing all decryption to fail. + +### Fixed +- **LordFlix provider**: Updated upstream domain (`network.hasta-la-vista.site` → `snowhouse.lordflix.club`) and refreshed server list (10 servers). +- **Docker startup crash**: Added missing `COPY proxy ./proxy` to both build and runtime stages in `Dockerfile` — `apiServer.js` unconditionally requires `./proxy/proxyServer` at startup. + +--- + ## [1.1.0] - 2026-05-16 ## Dependency Security diff --git a/Dockerfile b/Dockerfile index d7fa6d7..bb00b68 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,7 @@ RUN npm install --omit=dev # Copy only required source (avoid sending screenshots, node_modules already installed) COPY apiServer.js ./ COPY providers ./providers +COPY proxy ./proxy COPY public ./public COPY utils ./utils COPY README.md ./ @@ -33,6 +34,7 @@ COPY --from=build /app/node_modules ./node_modules COPY --from=build /app/apiServer.js ./ COPY --from=build /app/public ./public COPY --from=build /app/providers ./providers +COPY --from=build /app/proxy ./proxy COPY --from=build /app/utils ./utils COPY --from=build /app/package.json ./ COPY --from=build /app/README.md ./ diff --git a/README.md b/README.md index 6ec1940..264958f 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ - +

@@ -34,7 +34,7 @@ ## ✨ Features - **Multi‑TMDB Key Rotation** – Supply multiple API keys; one is chosen randomly per request. -- **Provider Aggregation** – Pluggable providers (Showbox/FebBox, 4KHDHub, MoviesMod, VidZee, VixSrc, Videasy, Vidlink, LordFlix, NoTorrent, DahmerMovies) with per‑provider enable toggles + default selection. +- **Provider Aggregation** – Pluggable providers (Showbox/FebBox, 4KHDHub, VixSrc, Videasy, Vidlink, LordFlix, NoTorrent, DahmerMovies) with per‑provider enable toggles + default selection. - **🔥 Plugin System** – Drop new provider files in `providers/` and add its exported function to the registry map (`providers/registry.js` → `providerFunctionMap`). - **Dynamic Filtering** – Minimum quality presets, custom JSON quality map, codec exclusion rules (presets + JSON). - **Runtime Overrides UI** – Fully interactive web admin at `/` (login protected) writing to `utils/user-config.json`. @@ -199,7 +199,7 @@ Saving in the UI writes only changed keys. Setting a field to empty removes the **Override File:** `utils/user-config.json` ```json { - "defaultProviders": ["4khdhub","moviesmod"], + "defaultProviders": ["4khdhub"], "tmdbApiKeys": ["KEY_A","KEY_B"], "enable4khdhubProvider": true } @@ -227,8 +227,6 @@ The API supports a plugin system. Drop a new provider file in the `providers/` f ### Current Built-in Providers - `showbox` - Showbox/FebBox streams (requires FebBox JWT cookie) - `4khdhub` - 4KHDHub streams -- `moviesmod` - MoviesMod streams -- `vidzee` - VidZee streams - `vixsrc` - VixSrc streams - `videasy` - Videasy streams (10 servers via enc-dec.app) - `vidlink` - Vidlink streams (via enc-dec.app) @@ -246,8 +244,6 @@ The API supports a plugin system. Drop a new provider file in the `providers/` f const providerFunctionMap = { 'Showbox.js': 'getStreamsFromTmdbId', '4khdhub.js': 'get4KHDHubStreams', - 'moviesmod.js': 'getMoviesModStreams', - 'VidZee.js': 'getVidZeeStreams', 'vixsrc.js': 'getVixsrcStreams', 'videasy.js': 'getVideasyStreams', 'vidlink.js': 'getVidlinkStreams', diff --git a/package-lock.json b/package-lock.json index 0a8d4fd..10969a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "tmdb-embed-api", - "version": "1.1.0", + "version": "1.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tmdb-embed-api", - "version": "1.1.0", + "version": "1.1.1", "dependencies": { "aromanize": "^0.1.5", "axios": "^1.9.0", diff --git a/package.json b/package.json index 06e4a08..abbf3df 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tmdb-embed-api", - "version": "1.1.0", + "version": "1.1.1", "description": "Standalone streaming embed source aggregation API (TMDB ID in, streams out)", "main": "apiServer.js", "scripts": { diff --git a/providers/VidZee.js b/providers/VidZee.js deleted file mode 100644 index 5909df7..0000000 --- a/providers/VidZee.js +++ /dev/null @@ -1,201 +0,0 @@ -const crypto = require('crypto'); -const axios = require('axios'); - -const BASE_URL = 'https://core.vidzee.wtf'; -const PLAYER_URL = 'https://player.vidzee.wtf'; -const VIDZEE_HEADERS = { - 'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.7051.98 Safari/537.36', - 'Accept': 'application/json, text/javascript, */*; q=0.01', - 'Accept-Language': 'en-US,en;q=0.9', - 'Referer': PLAYER_URL, - 'Origin': PLAYER_URL -}; - -// Pad/truncate key string to 32 bytes (UTF-8, mirrors CryptoJS behaviour) -function getKeyBytes(key) { - const encoded = Buffer.from(key, 'utf8'); - const result = Buffer.alloc(32); - encoded.copy(result, 0, 0, Math.min(encoded.length, 32)); - return result; -} - -// AES-256-CBC decrypt a VidZee stream link. -// encryptedData is outer-base64 of "ivBase64:cipherBase64" -async function decryptLink(encryptedData, decryptionKey) { - try { - if (!encryptedData || !decryptionKey) return ''; - - const decoded = Buffer.from(encryptedData, 'base64').toString('utf8'); - const colonIdx = decoded.indexOf(':'); - if (colonIdx === -1) return ''; - - const ivBase64 = decoded.slice(0, colonIdx); - const cipherBase64 = decoded.slice(colonIdx + 1); - if (!ivBase64 || !cipherBase64) return ''; - - const iv = Buffer.from(ivBase64, 'base64'); - const cipherBytes = Buffer.from(cipherBase64, 'base64'); - const keyBytes = getKeyBytes(decryptionKey); - - const decipher = crypto.createDecipheriv('aes-256-cbc', keyBytes, iv); - const decrypted = Buffer.concat([decipher.update(cipherBytes), decipher.final()]); - return decrypted.toString('utf8'); - } catch { - return ''; - } -} - -// Derive the real AES-CBC decryption key from VidZee's API-key blob via AES-GCM. -// Mirrors the TypeScript deriveKey() in the reference implementation. -async function deriveKey(e) { - try { - if (!e) return ''; - - const t = Buffer.from(e.replace(/\s+/g, ''), 'base64'); - if (t.length <= 28) return ''; - - const n = t.slice(0, 12); // AES-GCM IV - const r = t.slice(12, 28); - const a = t.slice(28); - - // i = a || r (reference: i.set(a, 0); i.set(r, a.length)) - const i = Buffer.concat([a, r]); - - // AES-256-GCM key = SHA-256 of fixed salt string - const l = crypto.createHash('sha256') - .update('4f2a9c7d1e8b3a6f0d5c2e9a7b1f4d8c', 'utf8') - .digest(); - - // WebCrypto passes ciphertext+tag as one buffer; tag is last 16 bytes - const ciphertext = i.slice(0, i.length - 16); - const authTag = i.slice(i.length - 16); - - const decipher = crypto.createDecipheriv('aes-256-gcm', l, n); - decipher.setAuthTag(authTag); - const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]); - return decrypted.toString('utf8'); - } catch { - return ''; - } -} - -// Fetch and derive the current decryption key from VidZee's API -async function fetchDecryptionKey() { - try { - const response = await axios.get(`${BASE_URL}/api-key`, { - headers: VIDZEE_HEADERS, - responseType: 'text', - timeout: 10000 - }); - if (response.status === 200 && response.data) { - return await deriveKey(response.data); - } - return null; - } catch { - return null; - } -} - -const getVidZeeStreams = async (tmdbId, mediaType, seasonNum, episodeNum) => { - if (!tmdbId) { - console.error('[VidZee] Error: TMDB ID (tmdbId) is required.'); - return []; - } - - if (!mediaType || (mediaType !== 'movie' && mediaType !== 'tv')) { - console.error('[VidZee] Error: mediaType is required and must be either "movie" or "tv".'); - return []; - } - - if (mediaType === 'tv') { - if (!seasonNum) { - console.error('[VidZee] Error: Season (seasonNum) is required for TV shows.'); - return []; - } - if (!episodeNum) { - console.error('[VidZee] Error: Episode (episodeNum) is required for TV shows.'); - return []; - } - } - - // Fetch and derive the decryption key before hitting servers - const decKey = await fetchDecryptionKey(); - if (!decKey) { - console.error('[VidZee] Failed to fetch/derive decryption key.'); - return []; - } - console.log('[VidZee] Decryption key derived successfully.'); - - // Servers 0–13 (matches reference implementation) - const servers = Array.from({ length: 14 }, (_, i) => i); - - const streamPromises = servers.map(async (sr) => { - let apiUrl = `${PLAYER_URL}/api/server?id=${tmdbId}&sr=${sr}`; - if (mediaType === 'tv') { - apiUrl += `&ss=${seasonNum}&ep=${episodeNum}`; - } - - try { - const response = await axios.get(apiUrl, { - headers: VIDZEE_HEADERS, - timeout: 7000 - }); - - const responseData = response.data; - if (!responseData || typeof responseData !== 'object') return []; - - let apiSources = []; - if (responseData.url && Array.isArray(responseData.url)) { - apiSources = responseData.url; - } else if (responseData.link && typeof responseData.link === 'string') { - apiSources = [responseData]; - } - - if (apiSources.length === 0) return []; - - const streams = await Promise.all(apiSources.map(async (sourceItem) => { - const label = sourceItem.name || sourceItem.type || 'VidZee'; - let quality = String(label).match(/^\d+$/) ? `${label}p` : label; - if (!/\d{3,4}p/.test(quality)) quality = '720p'; - const language = sourceItem.language || sourceItem.lang || 'Unknown'; - - let rawLink = sourceItem.link; - if (rawLink && !/^https?:\/\//i.test(rawLink)) { - const decoded = await decryptLink(rawLink, decKey); - if (decoded && /^https?:\/\//i.test(decoded)) { - rawLink = decoded; - } else { - console.log(`[VidZee S${sr}] Decryption yielded non-URL, skipping.`); - return null; - } - } - - if (!rawLink || !/^https?:\/\//i.test(rawLink)) return null; - - return { - name: `VidZee Server${sr} - ${quality} - ${language}`, - title: `VidZee Server${sr} - ${quality} - ${language}`, - url: rawLink, - quality, - provider: 'VidZee', - headers: { 'Referer': `${BASE_URL}/` } - }; - })); - - const valid = streams.filter(Boolean); - if (valid.length > 0) { - console.log(`[VidZee S${sr}] ${valid.length} stream(s) extracted.`); - } - return valid; - } catch (error) { - console.error(`[VidZee S${sr}] Error: ${error.message}`); - return []; - } - }); - - const allStreams = (await Promise.all(streamPromises)).flat(); - console.log(`[VidZee] Total streams: ${allStreams.length}`); - return allStreams; -}; - -module.exports = { getVidZeeStreams }; diff --git a/providers/lordflix.js b/providers/lordflix.js index 6b92f27..8012255 100644 --- a/providers/lordflix.js +++ b/providers/lordflix.js @@ -8,9 +8,9 @@ const LORDFLIX_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' }; -const LORDFLIX_API = 'https://network.hasta-la-vista.site'; +const LORDFLIX_API = 'https://snowhouse.lordflix.club'; const MULTI_DECRYPT_API = 'https://enc-dec.app/api'; -const SERVERS = ['Berlin', 'Tokyo', 'Bogota', 'Oslo', 'Luna', 'LordFlix', 'Sakura', 'Rio', 'Ativa']; +const SERVERS = ['Berlin', 'Marseille', 'Backrooms', 'Phoenix', 'Oslo', 'Luna', 'Sakura', 'Rio', 'Ativa', 'Moscow']; // Matches the reference project's encodeQuote: encodeURIComponent with + encoded as %20 function encodeQuote(str) { diff --git a/providers/moviesmod.js b/providers/moviesmod.js deleted file mode 100644 index d3d054e..0000000 --- a/providers/moviesmod.js +++ /dev/null @@ -1,1205 +0,0 @@ -/** - * 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, a.maxbutton-download-links, a[href*="links.modpro.blog"], a[href*="posts.modpro.blog"]'); - 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 - site changed from modrefer.in to links.modpro.blog - const linkElement = blockContent.find('a.maxbutton-download-links, a[href*="links.modpro.blog"], a[href*="posts.modpro.blog"], a[href*="modrefer.in"]').first(); - if (linkElement.length > 0) { - const link = linkElement.attr('href'); - const cleanQuality = extractQuality(headerText); - if (link && cleanQuality && cleanQuality !== 'Unknown') { - 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('links.modpro.blog') || urlObject.hostname.includes('posts.modpro.blog')) { - const { data } = await makeRequest(initialUrl, { headers: { 'Referer': refererUrl } }); - const $ = cheerio.load(data); - const finalLinks = []; - - // Primary: scoped to .entry-content - $('.entry-content a[href*="driveseed.org"], .entry-content a[href*="cloud.unblockedgames.world"], .entry-content a[href*="tech.creativeexpressionsblog.com"], .entry-content a[href*="tech.examzculture.in"], .entry-content a[href*="tech.unblockedgames.world"], .entry-content a[href*="tech.examdegree.site"]').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 }); - } - }); - - // Fallback: page-wide search - if (finalLinks.length === 0) { - $('a[href*="driveseed.org"], a[href*="cloud.unblockedgames.world"], a[href*="tech.creativeexpressionsblog.com"], a[href*="tech.examzculture.in"], a[href*="tech.unblockedgames.world"], a[href*="tech.examdegree.site"]').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, ' ') || 'Download Link', url: link }); - } - }); - } - - console.log(`[MoviesMod] Found ${finalLinks.length} links from ${urlObject.hostname}`); - return finalLinks; - - } else 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(/