mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
971 lines
No EOL
42 KiB
JavaScript
971 lines
No EOL
42 KiB
JavaScript
/**
|
|
* MoviesMod Provider for Stremio Addon
|
|
* 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 { wrapper } = require('axios-cookiejar-support');
|
|
const { URLSearchParams, URL } = require('url');
|
|
const fs = require('fs').promises;
|
|
const path = require('path');
|
|
const { findBestMatch } = require('string-similarity');
|
|
|
|
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 axios.get('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');
|
|
const CACHE_TTL = 4 * 60 * 60 * 1000; // 4 hours in milliseconds
|
|
|
|
// --- 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;
|
|
const cacheFile = path.join(CACHE_DIR, `${key}.json`);
|
|
try {
|
|
const data = await fs.readFile(cacheFile, 'utf-8');
|
|
const cached = JSON.parse(data);
|
|
|
|
if (Date.now() > cached.expiry) {
|
|
console.log(`[MoviesMod Cache] EXPIRED for key: ${key}`);
|
|
await fs.unlink(cacheFile).catch(() => {});
|
|
return null;
|
|
}
|
|
|
|
console.log(`[MoviesMod Cache] HIT for key: ${key}`);
|
|
return cached.data;
|
|
} catch (error) {
|
|
if (error.code !== 'ENOENT') {
|
|
console.error(`[MoviesMod Cache] READ ERROR for key ${key}: ${error.message}`);
|
|
}
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const saveToCache = async (key, data) => {
|
|
if (!CACHE_ENABLED) return;
|
|
const cacheFile = path.join(CACHE_DIR, `${key}.json`);
|
|
const cacheData = {
|
|
expiry: Date.now() + CACHE_TTL,
|
|
data: data
|
|
};
|
|
try {
|
|
await fs.writeFile(cacheFile, JSON.stringify(cacheData, null, 2), 'utf-8');
|
|
console.log(`[MoviesMod Cache] SAVED for key: ${key}`);
|
|
} catch (error) {
|
|
console.error(`[MoviesMod Cache] WRITE ERROR for key ${key}: ${error.message}`);
|
|
}
|
|
};
|
|
|
|
// Initialize cache directory on startup
|
|
ensureCacheDir();
|
|
|
|
// 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 axios.get(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 axios.get(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 axios.get(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
|
|
let 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 axios.get(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 axios.get(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"]').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 axios.get(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();
|
|
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;
|
|
}
|
|
}
|
|
|
|
// Resolve driveseed.org links to get download options
|
|
async function resolveDriveseedLink(driveseedUrl) {
|
|
try {
|
|
const { data } = await axios.get(driveseedUrl, {
|
|
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://links.modpro.blog/',
|
|
}
|
|
});
|
|
|
|
const redirectMatch = data.match(/window\.location\.replace\("([^"]+)"\)/);
|
|
|
|
if (redirectMatch && redirectMatch[1]) {
|
|
const finalPath = redirectMatch[1];
|
|
const finalUrl = `https://driveseed.org${finalPath}`;
|
|
|
|
const finalResponse = await axios.get(finalUrl, {
|
|
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': driveseedUrl,
|
|
}
|
|
});
|
|
|
|
const $ = cheerio.load(finalResponse.data);
|
|
const downloadOptions = [];
|
|
let size = null;
|
|
let fileName = null;
|
|
|
|
// Extract size and filename from the list
|
|
$('ul.list-group li').each((i, el) => {
|
|
const text = $(el).text();
|
|
if (text.includes('Size :')) {
|
|
size = text.split(':')[1].trim();
|
|
} else if (text.includes('Name :')) {
|
|
fileName = text.split(':')[1].trim();
|
|
}
|
|
});
|
|
|
|
// Find Resume Cloud button (primary)
|
|
const resumeCloudLink = $('a:contains("Resume Cloud")').attr('href');
|
|
if (resumeCloudLink) {
|
|
downloadOptions.push({
|
|
title: 'Resume Cloud',
|
|
type: 'resume',
|
|
url: `https://driveseed.org${resumeCloudLink}`,
|
|
priority: 1
|
|
});
|
|
}
|
|
|
|
// Find Resume Worker Bot (fallback)
|
|
const workerSeedLink = $('a:contains("Resume Worker Bot")').attr('href');
|
|
if (workerSeedLink) {
|
|
downloadOptions.push({
|
|
title: 'Resume Worker Bot',
|
|
type: 'worker',
|
|
url: workerSeedLink,
|
|
priority: 2
|
|
});
|
|
}
|
|
|
|
// Find Instant Download (final fallback)
|
|
const instantDownloadLink = $('a:contains("Instant Download")').attr('href');
|
|
if (instantDownloadLink) {
|
|
downloadOptions.push({
|
|
title: 'Instant Download',
|
|
type: 'instant',
|
|
url: instantDownloadLink,
|
|
priority: 3
|
|
});
|
|
}
|
|
|
|
// Sort by priority
|
|
downloadOptions.sort((a, b) => a.priority - b.priority);
|
|
return { downloadOptions, size, fileName };
|
|
}
|
|
return { downloadOptions: [], size: null, fileName: null };
|
|
} catch (error) {
|
|
console.error(`[MoviesMod] Error resolving Driveseed link: ${error.message}`);
|
|
return { downloadOptions: [], size: null, fileName: null };
|
|
}
|
|
}
|
|
|
|
// Resolve Resume Cloud link to final download URL
|
|
async function resolveResumeCloudLink(resumeUrl) {
|
|
try {
|
|
const { data } = await axios.get(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();
|
|
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(/<script type="text\/javascript">([\s\S]*?)<\/script>/g);
|
|
|
|
if (!scriptTags) {
|
|
console.error('[MoviesMod] Could not find any script tags on the page.');
|
|
return null;
|
|
}
|
|
|
|
const scriptContent = scriptTags.find(s => s.includes("formData.append('token'"));
|
|
|
|
if (!scriptContent) {
|
|
console.error('[MoviesMod] Could not find the relevant script tag containing formData.append.');
|
|
|
|
// Debug: Log available script content
|
|
console.log(`[MoviesMod] Found ${scriptTags.length} script tags. Checking for token patterns...`);
|
|
scriptTags.forEach((script, i) => {
|
|
if (script.includes('token') || script.includes('formData')) {
|
|
console.log(`[MoviesMod] Script ${i} snippet:`, script.substring(0, 300));
|
|
}
|
|
});
|
|
|
|
return null;
|
|
}
|
|
|
|
const tokenMatch = scriptContent.match(/formData\.append\('token', '([^']+)'\)/);
|
|
const idMatch = scriptContent.match(/fetch\('\/download\?id=([^']+)',/);
|
|
|
|
if (!tokenMatch || !tokenMatch[1] || !idMatch || !idMatch[1]) {
|
|
console.error('[MoviesMod] Could not extract token or correct ID from the script.');
|
|
console.log('[MoviesMod] Script content snippet:', scriptContent.substring(0, 500));
|
|
|
|
// Try alternative patterns
|
|
const altTokenMatch = scriptContent.match(/token['"]?\s*[:=]\s*['"]([^'"]+)['"]/);
|
|
const altIdMatch = scriptContent.match(/id['"]?\s*[:=]\s*['"]([^'"]+)['"]/);
|
|
|
|
if (altTokenMatch && altIdMatch) {
|
|
console.log('[MoviesMod] Found alternative patterns, trying those...');
|
|
const token = altTokenMatch[1];
|
|
const id = altIdMatch[1];
|
|
console.log(`[MoviesMod] Alternative token: ${token.substring(0, 20)}...`);
|
|
console.log(`[MoviesMod] Alternative id: ${id}`);
|
|
|
|
// Continue with these values
|
|
return await makeWorkerSeedRequest(session, token, id, workerSeedUrl);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
const token = tokenMatch[1];
|
|
const correctId = idMatch[1];
|
|
console.log(`[MoviesMod] Step 2: Extracted token: ${token.substring(0, 20)}...`);
|
|
console.log(`[MoviesMod] Step 2: Extracted correct ID: ${correctId}`);
|
|
|
|
return await makeWorkerSeedRequest(session, token, correctId, workerSeedUrl);
|
|
|
|
} catch (error) {
|
|
console.error(`[MoviesMod] Error resolving WorkerSeed link: ${error.message}`);
|
|
if (error.response) {
|
|
console.error('[MoviesMod] Error response data:', error.response.data);
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Helper function to make the actual WorkerSeed API request
|
|
async function makeWorkerSeedRequest(session, token, correctId, workerSeedUrl) {
|
|
// Step 3: Make the POST request with the correct data using the same session
|
|
const apiUrl = `https://workerseed.dev/download?id=${correctId}`;
|
|
|
|
const formData = new FormData();
|
|
formData.append('token', token);
|
|
|
|
console.log(`[MoviesMod] Step 3: POSTing to endpoint: ${apiUrl} with extracted token.`);
|
|
|
|
// Use the session instance, which will automatically include the cookies
|
|
const { data: apiResponse } = await session.post(apiUrl, formData, {
|
|
headers: {
|
|
...formData.getHeaders(),
|
|
'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': workerSeedUrl,
|
|
'x-requested-with': 'XMLHttpRequest'
|
|
}
|
|
});
|
|
|
|
if (apiResponse && apiResponse.url) {
|
|
console.log(`[MoviesMod] SUCCESS! Final video link from Worker-seed API: ${apiResponse.url}`);
|
|
return apiResponse.url;
|
|
} else {
|
|
console.log('[MoviesMod] Worker-seed API did not return a URL. Full response:');
|
|
console.log(apiResponse);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Resolve Video Seed (Instant Download) link
|
|
async function resolveVideoSeedLink(videoSeedUrl) {
|
|
try {
|
|
const urlParams = new URLSearchParams(new URL(videoSeedUrl).search);
|
|
const keys = urlParams.get('url');
|
|
|
|
if (keys) {
|
|
const apiUrl = `${new URL(videoSeedUrl).origin}/api`;
|
|
const formData = new FormData();
|
|
formData.append('keys', keys);
|
|
|
|
const apiResponse = await axios.post(apiUrl, formData, {
|
|
headers: {
|
|
...formData.getHeaders(),
|
|
'x-token': new URL(videoSeedUrl).hostname
|
|
}
|
|
});
|
|
|
|
if (apiResponse.data && apiResponse.data.url) {
|
|
return apiResponse.data.url;
|
|
}
|
|
}
|
|
return null;
|
|
} catch (error) {
|
|
console.error(`[MoviesMod] Error resolving VideoSeed link: ${error.message}`);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Validate if a video URL is working (not 404 or broken)
|
|
async function validateVideoUrl(url, timeout = 10000) {
|
|
try {
|
|
console.log(`[MoviesMod] Validating URL: ${url.substring(0, 100)}...`);
|
|
const response = await axios.head(url, {
|
|
timeout,
|
|
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',
|
|
'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(`[MoviesMod] ✓ URL validation successful (${response.status})`);
|
|
return true;
|
|
} else {
|
|
console.log(`[MoviesMod] ✗ URL validation failed with status: ${response.status}`);
|
|
return false;
|
|
}
|
|
} catch (error) {
|
|
console.log(`[MoviesMod] ✗ URL validation failed: ${error.message}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Main function to get streams for TMDB content
|
|
async function getMoviesModStreams(tmdbId, mediaType, seasonNum = null, episodeNum = null) {
|
|
try {
|
|
console.log(`[MoviesMod] Fetching streams for TMDB ${mediaType}/${tmdbId}${seasonNum ? `, S${seasonNum}E${episodeNum}`: ''}`);
|
|
|
|
// Define a cache key based on the media type and ID. For series, cache per season.
|
|
const cacheKey = `moviesmod_driveseed_v6_${tmdbId}_${mediaType}${seasonNum ? `_s${seasonNum}` : ''}`;
|
|
let resolvedQualities = await getFromCache(cacheKey);
|
|
|
|
if (!resolvedQualities) {
|
|
console.log(`[MoviesMod Cache] MISS for key: ${cacheKey}. Fetching from source.`);
|
|
|
|
// We need to fetch title and year from TMDB API
|
|
const TMDB_API_KEY = process.env.TMDB_API_KEY;
|
|
if (!TMDB_API_KEY) throw new Error('TMDB_API_KEY not configured.');
|
|
|
|
const { default: fetch } = await import('node-fetch');
|
|
const tmdbUrl = `https://api.themoviedb.org/3/${mediaType === 'tv' ? 'tv' : 'movie'}/${tmdbId}?api_key=${TMDB_API_KEY}&language=en-US`;
|
|
const tmdbDetails = await (await fetch(tmdbUrl)).json();
|
|
|
|
const title = mediaType === 'tv' ? tmdbDetails.name : tmdbDetails.title;
|
|
const year = mediaType === 'tv' ? tmdbDetails.first_air_date?.substring(0, 4) : tmdbDetails.release_date?.substring(0, 4);
|
|
if (!title) throw new Error('Could not get title from TMDB');
|
|
|
|
console.log(`[MoviesMod] Found metadata: ${title} (${year})`);
|
|
const searchResults = await searchMoviesMod(title);
|
|
if (searchResults.length === 0) throw new Error(`No search results found for "${title}"`);
|
|
|
|
// --- NEW: Use string similarity to find the best match ---
|
|
const titles = searchResults.map(r => r.title);
|
|
const bestMatch = findBestMatch(title, titles);
|
|
|
|
console.log(`[MoviesMod] Best match for "${title}" is "${bestMatch.bestMatch.target}" with a rating of ${bestMatch.bestMatch.rating.toFixed(2)}`);
|
|
|
|
let selectedResult = null;
|
|
// Set a minimum similarity threshold (e.g., 0.3) to avoid obviously wrong matches
|
|
if (bestMatch.bestMatch.rating > 0.3) {
|
|
selectedResult = searchResults[bestMatch.bestMatchIndex];
|
|
|
|
// Additional check for year if it's a movie
|
|
if (mediaType === 'movie' && year) {
|
|
if (!selectedResult.title.includes(year)) {
|
|
console.warn(`[MoviesMod] Title match found, but year mismatch. Matched: "${selectedResult.title}", Expected year: ${year}. Discarding match.`);
|
|
selectedResult = null; // Discard if year doesn't match
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!selectedResult) {
|
|
// If no good match is found, try a stricter direct search using regex with word boundaries
|
|
console.log('[MoviesMod] Similarity match failed or was below threshold. Trying stricter name/year search with word boundaries...');
|
|
const titleRegex = new RegExp(`\\b${escapeRegExp(title.toLowerCase())}\\b`);
|
|
|
|
if (mediaType === 'movie') {
|
|
selectedResult = searchResults.find(r =>
|
|
titleRegex.test(r.title.toLowerCase()) &&
|
|
(!year || r.title.includes(year))
|
|
);
|
|
} else { // for 'tv'
|
|
// For TV, be more lenient on year, but check for title and 'season' keyword.
|
|
selectedResult = searchResults.find(r =>
|
|
titleRegex.test(r.title.toLowerCase()) &&
|
|
r.title.toLowerCase().includes('season')
|
|
);
|
|
}
|
|
}
|
|
|
|
if (!selectedResult) {
|
|
throw new Error(`No suitable search result found for "${title} (${year})". Best similarity match was too low or failed year check.`);
|
|
}
|
|
|
|
console.log(`[MoviesMod] Selected: ${selectedResult.title}`);
|
|
const downloadLinks = await extractDownloadLinks(selectedResult.url);
|
|
if (downloadLinks.length === 0) throw new Error('No download links found');
|
|
|
|
let relevantLinks = downloadLinks;
|
|
if ((mediaType === 'tv' || mediaType === 'series') && seasonNum !== null) {
|
|
relevantLinks = downloadLinks.filter(link => link.quality.toLowerCase().includes(`season ${seasonNum}`) || link.quality.toLowerCase().includes(`s${seasonNum}`));
|
|
}
|
|
|
|
// Filter out 480p links before processing
|
|
relevantLinks = relevantLinks.filter(link => !link.quality.toLowerCase().includes('480p'));
|
|
console.log(`[MoviesMod] ${relevantLinks.length} links remaining after 480p filter.`);
|
|
|
|
if (relevantLinks.length > 0) {
|
|
console.log(`[MoviesMod] Found ${relevantLinks.length} relevant quality links.`);
|
|
const qualityPromises = relevantLinks.map(async (link) => {
|
|
const finalLinks = await resolveIntermediateLink(link.url, selectedResult.url, link.quality);
|
|
if (finalLinks && finalLinks.length > 0) {
|
|
return { quality: link.quality, finalLinks: finalLinks };
|
|
}
|
|
return null;
|
|
});
|
|
|
|
resolvedQualities = (await Promise.all(qualityPromises)).filter(Boolean);
|
|
} else {
|
|
resolvedQualities = [];
|
|
}
|
|
|
|
await saveToCache(cacheKey, resolvedQualities);
|
|
}
|
|
|
|
if (!resolvedQualities || resolvedQualities.length === 0) {
|
|
console.log('[MoviesMod] No intermediate links found from cache or scraping.');
|
|
return [];
|
|
}
|
|
|
|
console.log(`[MoviesMod] Processing ${resolvedQualities.length} qualities to get final streams.`);
|
|
const streams = [];
|
|
const processedFileNames = new Set();
|
|
|
|
const qualityProcessingPromises = resolvedQualities.map(async (qualityInfo) => {
|
|
const { quality, finalLinks } = qualityInfo;
|
|
|
|
let targetLinks = finalLinks;
|
|
if ((mediaType === 'tv' || mediaType === 'series') && episodeNum !== null) {
|
|
targetLinks = finalLinks.filter(fl => fl.server.toLowerCase().includes(`episode ${episodeNum}`) || fl.server.toLowerCase().includes(`ep ${episodeNum}`) || fl.server.toLowerCase().includes(`e${episodeNum}`));
|
|
if (targetLinks.length === 0) {
|
|
console.log(`[MoviesMod] No episode ${episodeNum} found for ${quality}`);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
const finalStreamPromises = targetLinks.map(async (targetLink) => {
|
|
try {
|
|
let currentUrl = targetLink.url;
|
|
|
|
// Step 1: Handle SID links if they appear
|
|
if (currentUrl.includes('tech.unblockedgames.world') || currentUrl.includes('tech.creativeexpressionsblog.com')) {
|
|
console.log(`[MoviesMod] Bypassing SID link: ${currentUrl}`);
|
|
const resolvedUrl = await resolveTechUnblockedLink(currentUrl);
|
|
if (!resolvedUrl) {
|
|
console.warn(`[MoviesMod] Failed to bypass tech.unblockedgames.world for: ${currentUrl}`);
|
|
return null;
|
|
}
|
|
console.log(`[MoviesMod] Bypassed. Continuing with resolved URL: ${resolvedUrl}`);
|
|
currentUrl = resolvedUrl; // The resolved URL should be a driveseed link
|
|
}
|
|
|
|
// Step 2: Now process the (potentially resolved) driveseed link
|
|
if (currentUrl.includes('driveseed.org')) {
|
|
const { downloadOptions, size: driveseedSize, fileName } = await resolveDriveseedLink(currentUrl);
|
|
|
|
if (fileName && processedFileNames.has(fileName)) {
|
|
console.log(`[MoviesMod] Skipping duplicate file: ${fileName}`);
|
|
return null;
|
|
}
|
|
if (fileName) processedFileNames.add(fileName);
|
|
|
|
if (!downloadOptions || downloadOptions.length === 0) return null;
|
|
|
|
// Try each download method in order of priority until we find a working one
|
|
let selectedResult = null;
|
|
for (const option of downloadOptions) {
|
|
try {
|
|
console.log(`[MoviesMod] Trying ${option.title} for ${quality}...`);
|
|
let finalDownloadUrl = null;
|
|
|
|
if (option.type === 'resume') {
|
|
finalDownloadUrl = await resolveResumeCloudLink(option.url);
|
|
} else if (option.type === 'worker') {
|
|
finalDownloadUrl = await resolveWorkerSeedLink(option.url);
|
|
} else if (option.type === 'instant') {
|
|
finalDownloadUrl = await resolveVideoSeedLink(option.url);
|
|
}
|
|
|
|
if (finalDownloadUrl) {
|
|
// Validate the URL before using it
|
|
const isValid = await validateVideoUrl(finalDownloadUrl);
|
|
if (isValid) {
|
|
selectedResult = { url: finalDownloadUrl, method: option.title };
|
|
console.log(`[MoviesMod] ✓ Successfully resolved ${quality} using ${option.title}`);
|
|
break; // Found working URL, stop trying other methods
|
|
} else {
|
|
console.log(`[MoviesMod] ✗ ${option.title} returned invalid/broken URL, trying next method...`);
|
|
}
|
|
} else {
|
|
console.log(`[MoviesMod] ✗ ${option.title} failed to resolve URL, trying next method...`);
|
|
}
|
|
} catch (error) {
|
|
console.log(`[MoviesMod] ✗ ${option.title} threw error: ${error.message}, trying next method...`);
|
|
}
|
|
}
|
|
|
|
if (!selectedResult) {
|
|
console.log(`[MoviesMod] ✗ All download methods failed for ${quality}`);
|
|
return null;
|
|
}
|
|
|
|
let actualQuality = extractQuality(quality);
|
|
const sizeInfo = driveseedSize || quality.match(/\[([^\]]+)\]/)?.[1];
|
|
const cleanFileName = fileName ? fileName.replace(/\.[^/.]+$/, "").replace(/[._]/g, ' ') : `Stream from ${quality}`;
|
|
const techDetails = getTechDetails(quality);
|
|
const techDetailsString = techDetails.length > 0 ? ` • ${techDetails.join(' • ')}` : '';
|
|
|
|
return {
|
|
name: `MoviesMod\n${actualQuality}`,
|
|
title: `${cleanFileName}\n${sizeInfo || ''}${techDetailsString}`,
|
|
url: selectedResult.url,
|
|
quality: actualQuality,
|
|
};
|
|
} else {
|
|
console.warn(`[MoviesMod] Unsupported URL type for final processing: ${currentUrl}`);
|
|
return null;
|
|
}
|
|
} catch (e) {
|
|
console.error(`[MoviesMod] Error processing target link ${targetLink.url}: ${e.message}`);
|
|
return null;
|
|
}
|
|
});
|
|
|
|
return (await Promise.all(finalStreamPromises)).filter(Boolean);
|
|
});
|
|
|
|
const allResults = await Promise.all(qualityProcessingPromises);
|
|
allResults.flat().forEach(s => streams.push(s));
|
|
|
|
// Sort by quality descending
|
|
streams.sort((a, b) => {
|
|
const qualityA = parseQualityForSort(a.quality);
|
|
const qualityB = parseQualityForSort(b.quality);
|
|
return qualityB - qualityA;
|
|
});
|
|
|
|
console.log(`[MoviesMod] Successfully extracted and sorted ${streams.length} streams`);
|
|
return streams;
|
|
|
|
} catch (error) {
|
|
console.error(`[MoviesMod] Error getting streams: ${error.message}`);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
getMoviesModStreams
|
|
};
|