]*href="([^"]*)"[^>]*>.*?Download.*?<\/a>/i
+ ];
+
+ let finalDownloadLink = null;
+ for (const pattern of downloadLinkPatterns) {
+ const linkMatch = pattern.exec(finalPageHtml);
+ if (linkMatch && linkMatch[1]) {
+ finalDownloadLink = linkMatch[1];
+ break;
+ }
+ }
+
+ if (finalDownloadLink) {
+ // Fix spaces in workers.dev URLs by encoding them properly
+ if (finalDownloadLink.includes('workers.dev')) {
+ const urlParts = finalDownloadLink.split('/');
+ const filename = urlParts[urlParts.length - 1];
+ 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;
+ }
+}
+
+// Validate if a video URL is working (not 404 or broken)
+async function validateVideoUrl(url, timeout = 10000) {
+ try {
+ console.log(`[UHDMovies] Validating URL: ${url.substring(0, 100)}...`);
+ const response = await fetch(url, {
+ method: 'HEAD',
+ headers: {
+ 'Range': 'bytes=0-1',
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
+ }
+ });
+
+ if (response.ok || response.status === 206) {
+ 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 get final download URL from driveleech page
+async function getFinalLink(driveleechUrl) {
+ try {
+ console.log(`[UHDMovies] Processing driveleech page: ${driveleechUrl}`);
+
+ const response = await makeRequest(driveleechUrl);
+ const html = await response.text();
+
+ // Check for JavaScript redirect
+ const jsRedirectRegex = /window\.location\.replace\("([^"]+)"\)/;
+ const jsMatch = jsRedirectRegex.exec(html);
+
+ let finalHtml = html;
+ if (jsMatch) {
+ const newUrl = new URL(jsMatch[1], 'https://driveleech.net/').href;
+ console.log(`[UHDMovies] Found JavaScript redirect to: ${newUrl}`);
+ const newResponse = await makeRequest(newUrl);
+ finalHtml = await newResponse.text();
+ }
+
+ // Extract size and filename information
+ let sizeInfo = 'Unknown';
+ let fileName = null;
+
+ const sizeRegex = /Size\s*:\s*([0-9.,]+\s*[KMGT]B)/i;
+ const sizeMatch = sizeRegex.exec(finalHtml);
+ if (sizeMatch) {
+ sizeInfo = sizeMatch[1];
+ }
+
+ const nameRegex = /Name\s*:\s*([^<\n]+)/i;
+ const nameMatch = nameRegex.exec(finalHtml);
+ if (nameMatch) {
+ fileName = nameMatch[1].trim();
+ }
+
+ // Try download methods
+ 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(finalHtml);
+
+ if (finalUrl) {
+ // Check if URL validation is enabled
+ if (typeof URL_VALIDATION_ENABLED !== 'undefined' && !URL_VALIDATION_ENABLED) {
+ console.log(`[UHDMovies] ✓ URL validation disabled, accepting ${method.name} result`);
+ return { url: finalUrl, size: sizeInfo, fileName: fileName };
+ }
+
+ 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 URL, trying next method...`);
+ }
+ }
+ } catch (error) {
+ console.log(`[UHDMovies] ✗ ${method.name} failed: ${error.message}`);
+ }
+ }
+
+ console.log('[UHDMovies] ✗ All download methods failed');
+ return null;
+
+ } catch (error) {
+ console.error(`[UHDMovies] Error in getFinalLink: ${error.message}`);
+ return null;
+ }
+}
+
+// Resolve download links with full processing chain
+async function resolveDownloadLink(linkInfo) {
+ try {
+ console.log(`[UHDMovies] Resolving link: ${linkInfo.quality}`);
+
+ // Step 1: Resolve SID link to driveleech URL
+ let driveleechUrl = null;
+
+ if (linkInfo.url.includes('tech.unblockedgames.world') ||
+ linkInfo.url.includes('tech.examzculture.in') ||
+ linkInfo.url.includes('tech.examdegree.site') ||
+ linkInfo.url.includes('tech.creativeexpressionsblog.com')) {
+ driveleechUrl = await resolveSidToDriveleech(linkInfo.url);
+ } else if (linkInfo.url.includes('driveleech.net') || linkInfo.url.includes('driveseed.org')) {
+ driveleechUrl = linkInfo.url;
+ }
+
+ if (!driveleechUrl) {
+ console.log(`[UHDMovies] Could not resolve SID link for ${linkInfo.quality}`);
+ return null;
+ }
+
+ // Filter out non-driveleech/driveseed URLs
+ if (!driveleechUrl.includes('driveleech.net') && !driveleechUrl.includes('driveseed.org')) {
+ console.log(`[UHDMovies] Skipping non-driveleech URL: ${driveleechUrl}`);
+ return null;
+ }
+
+ // Step 2: Get final streaming URL from driveleech page
+ const finalLinkInfo = await getFinalLink(driveleechUrl);
+
+ if (!finalLinkInfo) {
+ console.log(`[UHDMovies] Could not get final link for ${linkInfo.quality}`);
+ return null;
+ }
+
+ // Step 3: Return formatted stream info
+ const fileName = finalLinkInfo.fileName || linkInfo.quality;
+ const cleanFileName = fileName.replace(/\.[^/.]+$/, "").replace(/[._]/g, ' ');
+
+ return {
+ name: `UHD Movies`,
+ title: `${cleanFileName}\n${finalLinkInfo.size}`,
+ url: finalLinkInfo.url,
+ quality: linkInfo.quality,
+ size: finalLinkInfo.size,
+ fileName: finalLinkInfo.fileName,
+ type: 'direct'
+ };
+
+ } catch (error) {
+ console.error(`[UHDMovies] Failed to resolve link: ${error.message}`);
+ return null;
+ }
+}
+
+// Extract TV show download links from show page using Cheerio (same approach as Node.js version)
+async function extractTvShowDownloadLinks(showPageUrl, targetSeason, targetEpisode) {
+ try {
+ console.log(`[UHDMovies] Extracting TV show links from: ${showPageUrl} for S${targetSeason}E${targetEpisode}`);
+
+ const response = await makeRequest(showPageUrl);
+ const html = await response.text();
+
+ const links = [];
+ const $ = cheerio.load(html);
+ const showTitle = $('h1').first().text().trim();
+
+ // --- 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 == targetSeason) {
+ inTargetSeason = true;
+ console.log(`[UHDMovies] Entering Season ${targetSeason} block.`);
+ } else if (inTargetSeason) {
+ // We've hit the next season, so we stop.
+ console.log(`[UHDMovies] Exiting Season ${targetSeason} 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"], a[href*="tech.examdegree.site"]').length > 0) {
+ const linksParagraph = $el;
+ const episodeRegex = new RegExp(`^Episode\\s+0*${targetEpisode}(?!\\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 && !links.some(item => item.url === 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}'`);
+ links.push({
+ url: link,
+ quality: cleanQuality,
+ size: size,
+ rawQuality: rawQuality
+ });
+ }
+ }
+ }
+ }
+ });
+
+ if (links.length === 0) {
+ console.log('[UHDMovies] Main extraction logic failed. Trying fallback method with season filtering.');
+ $('.entry-content').find('a[href*="tech.unblockedgames.world"], a[href*="tech.examzculture.in"], a[href*="tech.examdegree.site"]').each((i, el) => {
+ const linkElement = $(el);
+ const episodeRegex = new RegExp(`^Episode\\s+0*${targetEpisode}(?!\\d)`, 'i');
+
+ if (episodeRegex.test(linkElement.text().trim())) {
+ const link = linkElement.attr('href');
+ if (link && !links.some(item => item.url === link)) {
+ let qualityText = 'Unknown Quality';
+ const parentP = linkElement.closest('p, div');
+
+ // Look for season information in the quality text and surrounding context
+ let foundSeasonMatch = false;
+
+ // Check previous elements for quality and season info
+ let currentElement = parentP;
+ for (let j = 0; j < 10; j++) {
+ currentElement = currentElement.prev();
+ if (currentElement.length === 0) break;
+
+ const prevText = currentElement.text().trim();
+ if (prevText && prevText.length > 5) {
+ // Check if this text contains season information
+ const seasonRegex = new RegExp(`S0?${targetSeason}(?![0-9])`, 'i');
+ const seasonWordRegex = new RegExp(`Season\\s+0*${targetSeason}(?![0-9])`, 'i');
+
+ if (seasonRegex.test(prevText) || seasonWordRegex.test(prevText)) {
+ qualityText = prevText;
+ foundSeasonMatch = true;
+ break;
+ }
+
+ // If we find a different season, skip this link
+ const otherSeasonRegex = /S0?(\d+)(?![0-9])|Season\s+(\d+)(?![0-9])/i;
+ const otherSeasonMatch = otherSeasonRegex.exec(prevText);
+ if (otherSeasonMatch) {
+ const foundSeason = parseInt(otherSeasonMatch[1] || otherSeasonMatch[2]);
+ if (foundSeason !== targetSeason) {
+ console.log(`[UHDMovies] Skipping link - found Season ${foundSeason}, looking for Season ${targetSeason}`);
+ return; // Skip this link
+ }
+ }
+ }
+ }
+
+ // Only add the link if we found a season match or no season info at all
+ if (foundSeasonMatch || qualityText === 'Unknown Quality') {
+ if (qualityText === 'Unknown Quality') {
+ // Last resort: check immediate previous element
+ 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}'`);
+ links.push({
+ url: link,
+ quality: cleanQuality,
+ size: size,
+ rawQuality: rawQuality
+ });
+ }
+ }
+ }
+ });
+ }
+
+ console.log(`[UHDMovies] Found ${links.length} episode links for S${targetSeason}E${targetEpisode}`);
+ return links;
+ } catch (error) {
+ console.error(`[UHDMovies] Failed to extract TV show links: ${error.message}`);
+ return [];
+ }
+}
+
+// Main function - this is the interface our local scraper service expects
+async function getStreams(tmdbId, mediaType = 'movie', season = null, episode = null) {
+ console.log(`[UHDMovies] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}${mediaType === 'tv' ? `, S:${season}E:${episode}` : ''}`);
+
+ try {
+ // Get TMDB info
+ const tmdbUrl = `https://api.themoviedb.org/3/${mediaType === 'tv' ? 'tv' : 'movie'}/${tmdbId}?api_key=${TMDB_API_KEY}`;
+ const tmdbResponse = await makeRequest(tmdbUrl);
+ const tmdbData = await tmdbResponse.json();
+
+ 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'})`);
+
+ // Search for the media
+ let searchTitle = mediaInfo.title.replace(/:/g, '').replace(/\s*&\s*/g, ' and ');
+ let searchResults = await searchMovies(searchTitle);
+
+ // Try fallback search if no results
+ if (searchResults.length === 0 || !searchResults.some(result => compareMedia(mediaInfo, result))) {
+ console.log(`[UHDMovies] Primary search failed, trying fallback...`);
+ const fallbackTitle = mediaInfo.title.split(':')[0].trim();
+ if (fallbackTitle !== searchTitle) {
+ searchResults = await searchMovies(fallbackTitle);
+ }
+ }
+
+ if (searchResults.length === 0) {
+ console.log(`[UHDMovies] No search results found`);
+ return [];
+ }
+
+ // Find best match
+ const bestMatch = searchResults.find(result => compareMedia(mediaInfo, result)) || searchResults[0];
+ console.log(`[UHDMovies] Using result: "${bestMatch.title}" (${bestMatch.year})`);
+
+ // Extract download links based on media type
+ let downloadLinks = [];
+ if (mediaType === 'tv' && season && episode) {
+ downloadLinks = await extractTvShowDownloadLinks(bestMatch.url, season, episode);
+ } else {
+ downloadLinks = await extractDownloadLinks(bestMatch.url);
+ }
+
+ if (downloadLinks.length === 0) {
+ console.log(`[UHDMovies] No download links found`);
+ return [];
+ }
+
+ // Resolve links to streams
+ const streamPromises = downloadLinks.map(link => resolveDownloadLink(link));
+ const streams = (await Promise.all(streamPromises)).filter(Boolean);
+
+ // Sort by size (largest first)
+ streams.sort((a, b) => {
+ const sizeA = parseSize(a.size);
+ const sizeB = parseSize(b.size);
+ return sizeB - sizeA;
+ });
+
+ console.log(`[UHDMovies] Successfully processed ${streams.length} streams`);
+ return streams;
+
+ } catch (error) {
+ console.error(`[UHDMovies] Error in getStreams: ${error.message}`);
+ return [];
+ }
+}
+
+// Export the main function
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = { getStreams };
+} else {
+ // For React Native environment
+ global.getStreams = getStreams;
+}
\ No newline at end of file
diff --git a/local-scrapers-repo/providers/watch32.js b/local-scrapers-repo/providers/watch32.js
new file mode 100644
index 00000000..299ed97a
--- /dev/null
+++ b/local-scrapers-repo/providers/watch32.js
@@ -0,0 +1,547 @@
+// Watch32 Scraper for Nuvio Local Scrapers
+// React Native compatible version - Standalone (no external dependencies)
+
+// Import cheerio-without-node-native for React Native
+const cheerio = require('cheerio-without-node-native');
+console.log('[Watch32] Using cheerio-without-node-native for DOM parsing');
+
+// Constants
+const TMDB_API_KEY = "439c478a771f35c05022f9feabcca01c";
+const MAIN_URL = 'https://watch32.sx';
+const VIDEOSTR_URL = 'https://videostr.net';
+
+// Helper function to make HTTP requests
+function makeRequest(url, options = {}) {
+ const defaultHeaders = {
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 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'
+ };
+
+ return fetch(url, {
+ method: options.method || 'GET',
+ headers: { ...defaultHeaders, ...options.headers },
+ ...options
+ })
+ .then(response => {
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
+ }
+ return response;
+ })
+ .catch(error => {
+ console.error(`[Watch32] Request failed for ${url}: ${error.message}`);
+ throw error;
+ });
+}
+
+// Search for content
+function searchContent(query) {
+ const searchUrl = `${MAIN_URL}/search/${query.replace(/\s+/g, '-')}`;
+ console.log(`[Watch32] Searching: ${searchUrl}`);
+
+ return makeRequest(searchUrl)
+ .then(response => response.text())
+ .then(html => {
+ const $ = cheerio.load(html);
+ const results = [];
+
+ $('.flw-item').each((i, element) => {
+ const title = $(element).find('h2.film-name > a').attr('title');
+ const link = $(element).find('h2.film-name > a').attr('href');
+ const poster = $(element).find('img.film-poster-img').attr('data-src');
+
+ if (title && link) {
+ results.push({
+ title,
+ url: link.startsWith('http') ? link : `${MAIN_URL}${link}`,
+ poster
+ });
+ }
+ });
+
+ console.log(`[Watch32] Found ${results.length} search results`);
+ return results;
+ })
+ .catch(error => {
+ console.error(`[Watch32] Search error: ${error.message}`);
+ return [];
+ });
+}
+
+// Get content details (movie or TV series)
+function getContentDetails(url) {
+ console.log(`[Watch32] Getting content details: ${url}`);
+
+ return makeRequest(url)
+ .then(response => response.text())
+ .then(html => {
+ const $ = cheerio.load(html);
+ const contentId = $('.detail_page-watch').attr('data-id');
+ const name = $('.detail_page-infor h2.heading-name > a').text();
+ const isMovie = url.includes('movie');
+
+ if (isMovie) {
+ return {
+ type: 'movie',
+ name,
+ data: `list/${contentId}`
+ };
+ } else {
+ // Get TV series episodes
+ return makeRequest(`${MAIN_URL}/ajax/season/list/${contentId}`)
+ .then(response => response.text())
+ .then(seasonsHtml => {
+ const $seasons = cheerio.load(seasonsHtml);
+ const episodes = [];
+ const seasonPromises = [];
+
+ $seasons('a.ss-item').each((i, season) => {
+ const seasonId = $(season).attr('data-id');
+ const seasonNum = $(season).text().replace('Season ', '');
+
+ const episodePromise = makeRequest(`${MAIN_URL}/ajax/season/episodes/${seasonId}`)
+ .then(response => response.text())
+ .then(episodesHtml => {
+ const $episodes = cheerio.load(episodesHtml);
+
+ $episodes('a.eps-item').each((i, episode) => {
+ const epId = $(episode).attr('data-id');
+ const title = $(episode).attr('title');
+ const match = title.match(/Eps (\d+): (.+)/);
+
+ if (match) {
+ episodes.push({
+ id: epId,
+ episode: parseInt(match[1]),
+ name: match[2],
+ season: parseInt(seasonNum.replace('Series', '').trim()),
+ data: `servers/${epId}`
+ });
+ }
+ });
+ });
+
+ seasonPromises.push(episodePromise);
+ });
+
+ return Promise.all(seasonPromises)
+ .then(() => ({
+ type: 'series',
+ name,
+ episodes
+ }));
+ });
+ }
+ })
+ .catch(error => {
+ console.error(`[Watch32] Content details error: ${error.message}`);
+ return null;
+ });
+}
+
+// Get server links for content
+function getServerLinks(data) {
+ console.log(`[Watch32] Getting server links: ${data}`);
+
+ return makeRequest(`${MAIN_URL}/ajax/episode/${data}`)
+ .then(response => response.text())
+ .then(html => {
+ const $ = cheerio.load(html);
+ const servers = [];
+
+ $('a.link-item').each((i, element) => {
+ const linkId = $(element).attr('data-linkid') || $(element).attr('data-id');
+ if (linkId) {
+ servers.push(linkId);
+ }
+ });
+
+ return servers;
+ })
+ .catch(error => {
+ console.error(`[Watch32] Server links error: ${error.message}`);
+ return [];
+ });
+}
+
+// Get source URL from link ID
+function getSourceUrl(linkId) {
+ console.log(`[Watch32] Getting source URL for linkId: ${linkId}`);
+
+ return makeRequest(`${MAIN_URL}/ajax/episode/sources/${linkId}`)
+ .then(response => response.json())
+ .then(data => data.link)
+ .catch(error => {
+ console.error(`[Watch32] Source URL error: ${error.message}`);
+ return null;
+ });
+}
+
+// Extract M3U8 from Videostr
+function extractVideostrM3u8(url) {
+ console.log(`[Watch32] Extracting from Videostr: ${url}`);
+
+ const headers = {
+ 'Accept': '*/*',
+ 'X-Requested-With': 'XMLHttpRequest',
+ 'Referer': VIDEOSTR_URL,
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; Win64; x64) AppleWebKit/537.36'
+ };
+
+ // Extract ID from URL
+ const id = url.split('/').pop().split('?')[0];
+
+ // Get nonce from embed page
+ return makeRequest(url, { headers })
+ .then(response => response.text())
+ .then(embedHtml => {
+ // Try to find 48-character nonce
+ let nonce = embedHtml.match(/\b[a-zA-Z0-9]{48}\b/);
+ if (nonce) {
+ nonce = nonce[0];
+ } else {
+ // Try to find three 16-character segments
+ const matches = embedHtml.match(/\b([a-zA-Z0-9]{16})\b.*?\b([a-zA-Z0-9]{16})\b.*?\b([a-zA-Z0-9]{16})\b/);
+ if (matches) {
+ nonce = matches[1] + matches[2] + matches[3];
+ }
+ }
+
+ if (!nonce) {
+ throw new Error('Could not extract nonce');
+ }
+
+ console.log(`[Watch32] Extracted nonce: ${nonce}`);
+
+ // Get sources from API
+ const apiUrl = `${VIDEOSTR_URL}/embed-1/v3/e-1/getSources?id=${id}&_k=${nonce}`;
+ console.log(`[Watch32] API URL: ${apiUrl}`);
+
+ return makeRequest(apiUrl, { headers })
+ .then(response => response.json())
+ .then(sourcesData => {
+ console.log('[Watch32] Sources data:', JSON.stringify(sourcesData, null, 2));
+
+ if (!sourcesData.sources || sourcesData.sources.length === 0) {
+ throw new Error('No sources found in response');
+ }
+
+ // Get the first source file (matching Kotlin logic)
+ const encoded = sourcesData.sources[0].file;
+ console.log('[Watch32] Encoded source:', encoded);
+
+ // Check if sources is already an M3U8 URL
+ if (encoded.includes('.m3u8')) {
+ console.log('[Watch32] Source is already M3U8 URL');
+ return encoded;
+ }
+
+ console.log('[Watch32] Sources are encrypted, attempting to decrypt...');
+
+ // Get decryption key - use 'mega' key like Kotlin version
+ return makeRequest('https://raw.githubusercontent.com/yogesh-hacker/MegacloudKeys/refs/heads/main/keys.json')
+ .then(response => response.json())
+ .then(keyData => {
+ console.log('[Watch32] Key data:', JSON.stringify(keyData, null, 2));
+
+ const key = keyData.mega; // Use 'mega' key like Kotlin
+
+ if (!key) {
+ throw new Error('Could not get decryption key (mega)');
+ }
+
+ console.log('[Watch32] Using mega key for decryption');
+
+ // Decrypt using Google Apps Script - exact same logic as Kotlin
+ const decodeUrl = 'https://script.google.com/macros/s/AKfycbxHbYHbrGMXYD2-bC-C43D3njIbU-wGiYQuJL61H4vyy6YVXkybMNNEPJNPPuZrD1gRVA/exec';
+ const fullUrl = `${decodeUrl}?encrypted_data=${encodeURIComponent(encoded)}&nonce=${encodeURIComponent(nonce)}&secret=${encodeURIComponent(key)}`;
+
+ console.log('[Watch32] Decryption URL:', fullUrl);
+
+ return makeRequest(fullUrl)
+ .then(response => response.text())
+ .then(decryptedData => {
+ console.log('[Watch32] Decrypted response:', decryptedData);
+
+ // Extract file URL from decrypted response - exact same regex as Kotlin
+ const fileMatch = decryptedData.match(/"file":"(.*?)"/);
+ if (fileMatch) {
+ const m3u8Url = fileMatch[1];
+ console.log('[Watch32] Extracted M3U8 URL:', m3u8Url);
+ return m3u8Url;
+ } else {
+ throw new Error('Video URL not found in decrypted response');
+ }
+ });
+ });
+ })
+ .then(finalM3u8Url => {
+ console.log(`[Watch32] Final M3U8 URL: ${finalM3u8Url}`);
+
+ // Accept both megacdn and other reliable CDN links
+ if (!finalM3u8Url.includes('megacdn.co') && !finalM3u8Url.includes('akmzed.cloud') && !finalM3u8Url.includes('sunnybreeze')) {
+ console.log('[Watch32] Skipping unreliable CDN link');
+ return null;
+ }
+
+ // Parse master playlist to extract quality streams
+ return parseM3U8Qualities(finalM3u8Url)
+ .then(qualities => ({
+ m3u8Url: finalM3u8Url,
+ qualities,
+ headers: {
+ 'Referer': 'https://videostr.net/',
+ 'Origin': 'https://videostr.net/',
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
+ }
+ }));
+ });
+ })
+ .catch(error => {
+ console.error(`[Watch32] Videostr extraction error: ${error.message}`);
+ return null;
+ });
+}
+
+// Parse M3U8 master playlist to extract qualities
+function parseM3U8Qualities(masterUrl) {
+ return makeRequest(masterUrl, {
+ headers: {
+ 'Referer': 'https://videostr.net/',
+ 'Origin': 'https://videostr.net/'
+ }
+ })
+ .then(response => response.text())
+ .then(playlist => {
+ const qualities = [];
+
+ // Parse M3U8 master playlist
+ const lines = playlist.split('\n');
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i].trim();
+ if (line.startsWith('#EXT-X-STREAM-INF:')) {
+ const nextLine = lines[i + 1]?.trim();
+ if (nextLine && !nextLine.startsWith('#')) {
+ // Extract resolution and bandwidth
+ const resolutionMatch = line.match(/RESOLUTION=(\d+x\d+)/);
+ const bandwidthMatch = line.match(/BANDWIDTH=(\d+)/);
+
+ const resolution = resolutionMatch ? resolutionMatch[1] : 'Unknown';
+ const bandwidth = bandwidthMatch ? parseInt(bandwidthMatch[1]) : 0;
+
+ // Determine quality label
+ let quality = 'Unknown';
+ if (resolution.includes('1920x1080')) quality = '1080p';
+ else if (resolution.includes('1280x720')) quality = '720p';
+ else if (resolution.includes('640x360')) quality = '360p';
+ else if (resolution.includes('854x480')) quality = '480p';
+
+ qualities.push({
+ quality,
+ resolution,
+ bandwidth,
+ url: nextLine.startsWith('http') ? nextLine : new URL(nextLine, masterUrl).href
+ });
+ }
+ }
+ }
+
+ // Sort by bandwidth (highest first)
+ qualities.sort((a, b) => b.bandwidth - a.bandwidth);
+
+ return qualities;
+ })
+ .catch(error => {
+ console.error(`[Watch32] Error parsing M3U8 qualities: ${error.message}`);
+ return [];
+ });
+}
+
+// Main scraping function
+function getStreams(tmdbId, mediaType, season, episode) {
+ console.log(`[Watch32] Searching for: ${tmdbId} (${mediaType})`);
+
+ // First, get movie/TV show details from TMDB
+ const tmdbUrl = `https://api.themoviedb.org/3/${mediaType}/${tmdbId}?api_key=${TMDB_API_KEY}`;
+
+ return makeRequest(tmdbUrl)
+ .then(response => response.json())
+ .then(tmdbData => {
+ const title = mediaType === 'tv' ? tmdbData.name : tmdbData.title;
+ const year = mediaType === 'tv' ? tmdbData.first_air_date?.substring(0, 4) : tmdbData.release_date?.substring(0, 4);
+
+ if (!title) {
+ throw new Error('Could not extract title from TMDB response');
+ }
+
+ console.log(`[Watch32] TMDB Info: "${title}" (${year || 'N/A'})`);
+
+ // Build search query - use title instead of TMDB ID
+ const query = year ? `${title} ${year}` : title;
+
+ return searchContent(query).then(searchResults => ({ searchResults, query, tmdbData }));
+ })
+ .then(({ searchResults, query, tmdbData }) => {
+ if (searchResults.length === 0) {
+ console.log('[Watch32] No search results found');
+ return [];
+ }
+
+ console.log(`[Watch32] Found ${searchResults.length} results`);
+
+ // Try to find exact match first, then partial match
+ let selectedResult = searchResults.find(result =>
+ result.title.toLowerCase() === query.toLowerCase()
+ );
+
+ if (!selectedResult) {
+ // Look for best partial match (contains all words from query)
+ const queryWords = query.toLowerCase().split(' ');
+ selectedResult = searchResults.find(result => {
+ const titleLower = result.title.toLowerCase();
+ return queryWords.every(word => titleLower.includes(word));
+ });
+ }
+
+ // Fallback to first result if no good match found
+ if (!selectedResult) {
+ selectedResult = searchResults[0];
+ }
+
+ console.log(`[Watch32] Selected: ${selectedResult.title}`);
+
+ // Get content details
+ return getContentDetails(selectedResult.url).then(contentDetails => ({ contentDetails, tmdbData }));
+ })
+ .then(({ contentDetails, tmdbData }) => {
+ if (!contentDetails) {
+ console.log('[Watch32] Could not get content details');
+ return [];
+ }
+
+ let itemsToProcess = [];
+
+ if (contentDetails.type === 'movie') {
+ itemsToProcess.push({ data: contentDetails.data, episodeMeta: null });
+ } else {
+ // For TV series, filter by episode/season if specified
+ let episodes = contentDetails.episodes;
+
+ if (season) {
+ episodes = episodes.filter(ep => ep.season === season);
+ }
+
+ if (episode) {
+ episodes = episodes.filter(ep => ep.episode === episode);
+ }
+
+ if (episodes.length === 0) {
+ console.log('[Watch32] No matching episodes found');
+ return [];
+ }
+
+ // Process all matching episodes in parallel
+ episodes.forEach(ep => {
+ console.log(`[Watch32] Queue episode: S${ep.season}E${ep.episode} - ${ep.name}`);
+ itemsToProcess.push({ data: ep.data, episodeMeta: ep });
+ });
+ }
+
+ // Process all data
+ const allPromises = itemsToProcess.map(item => {
+ return getServerLinks(item.data)
+ .then(serverLinks => {
+ console.log(`[Watch32] Found ${serverLinks.length} servers`);
+
+ // Process all server links
+ const linkPromises = serverLinks.map(linkId => {
+ return getSourceUrl(linkId)
+ .then(sourceUrl => {
+ if (!sourceUrl) return null;
+
+ console.log(`[Watch32] Source URL: ${sourceUrl}`);
+
+ // Check if it's a videostr URL
+ if (sourceUrl.includes('videostr.net')) {
+ return extractVideostrM3u8(sourceUrl);
+ }
+ return null;
+ })
+ .catch(error => {
+ console.error(`[Watch32] Error processing link ${linkId}: ${error.message}`);
+ return null;
+ });
+ });
+
+ return Promise.all(linkPromises);
+ })
+ .then(results => ({ results, episodeMeta: item.episodeMeta }));
+ });
+
+ return Promise.all(allPromises).then(resultsWithMeta => ({ resultsWithMeta, tmdbData, contentDetails }));
+ })
+ .then(({ resultsWithMeta, tmdbData, contentDetails }) => {
+ // Flatten and filter results
+ const allM3u8Links = [];
+ for (const item of resultsWithMeta) {
+ const serverResults = item.results;
+ for (const result of serverResults) {
+ if (result) {
+ allM3u8Links.push({ link: result, episodeMeta: item.episodeMeta });
+ }
+ }
+ }
+
+ // Build title with year and episode info
+ const title = mediaType === 'tv' ? tmdbData.name : tmdbData.title;
+ const year = mediaType === 'tv' ? tmdbData.first_air_date?.substring(0, 4) : tmdbData.release_date?.substring(0, 4);
+
+ // Convert to Nuvio format
+ const formattedLinks = [];
+
+ allM3u8Links.forEach(item => {
+ const link = item.link;
+ const episodeMeta = item.episodeMeta;
+ let perItemTitle = `${title} (${year || 'N/A'})`;
+ if (mediaType === 'tv' && episodeMeta) {
+ perItemTitle += ` - S${episodeMeta.season}E${episodeMeta.episode}`;
+ }
+ if (link.qualities && link.qualities.length > 0) {
+ link.qualities.forEach(quality => {
+ formattedLinks.push({
+ name: `Watch32 - ${quality.quality}`,
+ title: perItemTitle,
+ url: quality.url,
+ quality: quality.quality,
+ headers: link.headers || {},
+ subtitles: []
+ });
+ });
+ } else {
+ // Skip unknown quality links
+ console.log('[Watch32] Skipping unknown quality link');
+ }
+ });
+
+ console.log(`[Watch32] Total found: ${formattedLinks.length} streams`);
+ return formattedLinks;
+ })
+ .catch(error => {
+ console.error(`[Watch32] Scraping error: ${error.message}`);
+ return [];
+ })
+ .catch(error => {
+ console.error(`[Watch32] TMDB API error: ${error.message}`);
+ return [];
+ });
+}
+
+// Export the main function
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = { getStreams };
+} else {
+ // For React Native environment
+ global.Watch32ScraperModule = { getStreams };
+}
\ No newline at end of file
diff --git a/local-scrapers-repo/providers/xprime.js b/local-scrapers-repo/providers/xprime.js
new file mode 100644
index 00000000..635207d8
--- /dev/null
+++ b/local-scrapers-repo/providers/xprime.js
@@ -0,0 +1,800 @@
+// Xprime Scraper for Nuvio Local Scrapers
+// React Native compatible version - Standalone (no external dependencies)
+
+// TMDB API Configuration
+const TMDB_API_KEY = '439c478a771f35c05022f9feabcca01c';
+const TMDB_BASE_URL = 'https://api.themoviedb.org/3';
+
+// Working headers for Cloudflare Workers URLs
+const WORKING_HEADERS = {
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
+ 'Accept': 'video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5',
+ 'Accept-Language': 'en-US,en;q=0.9',
+ 'Accept-Encoding': 'identity',
+ 'Origin': 'https://xprime.tv',
+ 'Referer': 'https://xprime.tv/',
+ 'Sec-Fetch-Dest': 'video',
+ 'Sec-Fetch-Mode': 'no-cors',
+ 'Sec-Fetch-Site': 'cross-site',
+ 'DNT': '1'
+};
+
+// M3U8 Resolver Functions (inlined to remove external dependency)
+
+// Parse M3U8 content and extract quality streams
+function parseM3U8(content, baseUrl) {
+ const lines = content.split('\n').map(line => line.trim()).filter(line => line);
+ const streams = [];
+
+ let currentStream = null;
+
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i];
+
+ if (line.startsWith('#EXT-X-STREAM-INF:')) {
+ // Parse stream info
+ currentStream = {
+ bandwidth: null,
+ resolution: null,
+ codecs: null,
+ url: null
+ };
+
+ // Extract bandwidth
+ const bandwidthMatch = line.match(/BANDWIDTH=(\d+)/);
+ if (bandwidthMatch) {
+ currentStream.bandwidth = parseInt(bandwidthMatch[1]);
+ }
+
+ // Extract resolution
+ const resolutionMatch = line.match(/RESOLUTION=(\d+x\d+)/);
+ if (resolutionMatch) {
+ currentStream.resolution = resolutionMatch[1];
+ }
+
+ // Extract codecs
+ const codecsMatch = line.match(/CODECS="([^"]+)"/);
+ if (codecsMatch) {
+ currentStream.codecs = codecsMatch[1];
+ }
+
+ } else if (currentStream && !line.startsWith('#')) {
+ // This is the URL for the current stream
+ currentStream.url = resolveUrl(line, baseUrl);
+ streams.push(currentStream);
+ currentStream = null;
+ }
+ }
+
+ return streams;
+}
+
+// Resolve relative URLs against base URL
+function resolveUrl(url, baseUrl) {
+ if (url.startsWith('http')) {
+ return url;
+ }
+
+ try {
+ return new URL(url, baseUrl).toString();
+ } catch (error) {
+ console.log(`⚠️ Could not resolve URL: ${url} against ${baseUrl}`);
+ return url;
+ }
+}
+
+// Determine quality from resolution or bandwidth
+function getQualityFromStream(stream) {
+ if (stream.resolution) {
+ const [width, height] = stream.resolution.split('x').map(Number);
+
+ if (height >= 2160) return '4K';
+ if (height >= 1440) return '1440p';
+ if (height >= 1080) return '1080p';
+ if (height >= 720) return '720p';
+ if (height >= 480) return '480p';
+ if (height >= 360) return '360p';
+ return '240p';
+ }
+
+ if (stream.bandwidth) {
+ const mbps = stream.bandwidth / 1000000;
+
+ if (mbps >= 15) return '4K';
+ if (mbps >= 8) return '1440p';
+ if (mbps >= 5) return '1080p';
+ if (mbps >= 3) return '720p';
+ if (mbps >= 1.5) return '480p';
+ if (mbps >= 0.8) return '360p';
+ return '240p';
+ }
+
+ return 'Unknown';
+}
+
+// Fetch and resolve M3U8 playlist
+function resolveM3U8(url, sourceName = 'Unknown') {
+ console.log(`🔍 Resolving M3U8 playlist for ${sourceName}...`);
+ console.log(`📡 URL: ${url.substring(0, 80)}...`);
+
+ return fetch(url, {
+ method: 'GET',
+ headers: WORKING_HEADERS,
+ timeout: 15000
+ }).then(function(response) {
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
+ }
+
+ return response.text().then(function(content) {
+ console.log(`✅ Fetched M3U8 content (${content.length} bytes)`);
+
+ // Check if it's a master playlist (contains #EXT-X-STREAM-INF)
+ if (content.includes('#EXT-X-STREAM-INF:')) {
+ console.log(`📋 Master playlist detected - parsing quality streams...`);
+
+ const streams = parseM3U8(content, url);
+ console.log(`🎬 Found ${streams.length} quality streams`);
+
+ const resolvedStreams = [];
+
+ for (const stream of streams) {
+ const quality = getQualityFromStream(stream);
+
+ // Extract clean server name from sourceName
+ const cleanServerName = sourceName.replace(/^XPRIME\s+/i, '').replace(/\s+-\s+.*$/, '');
+ const formattedName = `XPRIME ${cleanServerName.charAt(0).toUpperCase() + cleanServerName.slice(1)} - ${quality}`;
+
+ resolvedStreams.push({
+ source: sourceName,
+ name: formattedName,
+ url: stream.url,
+ quality: quality,
+ resolution: stream.resolution,
+ bandwidth: stream.bandwidth,
+ codecs: stream.codecs,
+ type: 'M3U8',
+ headers: WORKING_HEADERS,
+ referer: 'https://xprime.tv'
+ });
+
+ console.log(` 📊 ${quality} (${stream.resolution || 'Unknown resolution'}) - ${Math.round((stream.bandwidth || 0) / 1000000 * 10) / 10} Mbps`);
+ }
+
+ // Sort by quality (highest first)
+ resolvedStreams.sort((a, b) => {
+ const qualityOrder = { '4K': 4, '1440p': 3, '1080p': 2, '720p': 1, '480p': 0, '360p': -1, '240p': -2, 'Unknown': -3 };
+ return (qualityOrder[b.quality] || -3) - (qualityOrder[a.quality] || -3);
+ });
+
+ return {
+ success: true,
+ type: 'master',
+ streams: resolvedStreams,
+ originalUrl: url
+ };
+
+ } else if (content.includes('#EXTINF:')) {
+ console.log(`📺 Media playlist detected - single quality stream`);
+
+ // Extract clean server name from sourceName
+ const cleanServerName = sourceName.replace(/^XPRIME\s+/i, '').replace(/\s+-\s+.*$/, '');
+ const formattedName = `XPRIME ${cleanServerName.charAt(0).toUpperCase() + cleanServerName.slice(1)} - Unknown`;
+
+ return {
+ success: true,
+ type: 'media',
+ streams: [{
+ source: sourceName,
+ name: formattedName,
+ url: url,
+ quality: 'Unknown',
+ type: 'M3U8',
+ headers: WORKING_HEADERS,
+ referer: 'https://xprime.tv'
+ }],
+ originalUrl: url
+ };
+
+ } else {
+ throw new Error('Invalid M3U8 content - no playlist markers found');
+ }
+ });
+ }).catch(function(error) {
+ console.log(`❌ Failed to resolve M3U8: ${error.message}`);
+
+ return {
+ success: false,
+ error: error.message,
+ streams: [],
+ originalUrl: url
+ };
+ });
+}
+
+// Resolve multiple M3U8 URLs
+function resolveMultipleM3U8(links) {
+ console.log(`🚀 Resolving ${links.length} M3U8 playlists in parallel...`);
+
+ const resolvePromises = links.map(function(link) {
+ return resolveM3U8(link.url, link.name).then(function(result) {
+ return {
+ originalLink: link,
+ resolution: result
+ };
+ });
+ });
+
+ return Promise.allSettled(resolvePromises).then(function(results) {
+ const allResolvedStreams = [];
+ const failedResolutions = [];
+
+ for (const result of results) {
+ if (result.status === 'fulfilled') {
+ const { originalLink, resolution } = result.value;
+
+ if (resolution.success) {
+ allResolvedStreams.push(...resolution.streams);
+ } else {
+ failedResolutions.push({
+ link: originalLink,
+ error: resolution.error
+ });
+ }
+ } else {
+ failedResolutions.push({
+ link: 'Unknown',
+ error: result.reason.message
+ });
+ }
+ }
+
+ console.log(`\n📊 Resolution Summary:`);
+ console.log(`✅ Successfully resolved: ${allResolvedStreams.length} streams`);
+ console.log(`❌ Failed resolutions: ${failedResolutions.length}`);
+
+ if (failedResolutions.length > 0) {
+ console.log(`\n❌ Failed resolutions:`);
+ failedResolutions.forEach((failure, index) => {
+ console.log(` ${index + 1}. ${failure.link.name || 'Unknown'}: ${failure.error}`);
+ });
+ }
+
+ return {
+ success: allResolvedStreams.length > 0,
+ streams: allResolvedStreams,
+ failed: failedResolutions,
+ summary: {
+ total: links.length,
+ resolved: allResolvedStreams.length,
+ failed: failedResolutions.length
+ }
+ };
+ });
+}
+
+// Constants
+const FALLBACK_DOMAIN = 'https://xprime.tv';
+const DOMAIN_CACHE_TTL = 4 * 60 * 60 * 1000; // 4 hours
+
+// Global variables for domain caching
+let xprimeDomain = FALLBACK_DOMAIN;
+let domainCacheTimestamp = 0;
+
+// Utility Functions
+function getQualityFromName(qualityStr) {
+ if (!qualityStr) return 'Unknown';
+
+ const quality = qualityStr.toLowerCase();
+ const qualityMap = {
+ '2160p': '4K', '4k': '4K',
+ '1440p': '1440p', '2k': '1440p',
+ '1080p': '1080p', 'fhd': '1080p', 'full hd': '1080p',
+ '720p': '720p', 'hd': '720p',
+ '480p': '480p', 'sd': '480p',
+ '360p': '360p',
+ '240p': '240p'
+ };
+
+ for (const [key, value] of Object.entries(qualityMap)) {
+ if (quality.includes(key)) return value;
+ }
+
+ // Try to extract number from string and format consistently
+ const match = qualityStr.match(/(\d{3,4})[pP]?/);
+ if (match) {
+ const resolution = parseInt(match[1]);
+ if (resolution >= 2160) return '4K';
+ if (resolution >= 1440) return '1440p';
+ if (resolution >= 1080) return '1080p';
+ if (resolution >= 720) return '720p';
+ if (resolution >= 480) return '480p';
+ if (resolution >= 360) return '360p';
+ return '240p';
+ }
+
+ return 'Unknown';
+}
+
+// Fetch latest domain from GitHub
+function getXprimeDomain() {
+ const now = Date.now();
+ if (now - domainCacheTimestamp < DOMAIN_CACHE_TTL) {
+ return Promise.resolve(xprimeDomain);
+ }
+
+ console.log('[Xprime] Fetching latest domain...');
+ return fetch('https://raw.githubusercontent.com/phisher98/TVVVV/refs/heads/main/domains.json', {
+ method: 'GET',
+ headers: {
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
+ }
+ }).then(function(response) {
+ if (response.ok) {
+ return response.json().then(function(data) {
+ if (data && data.xprime) {
+ xprimeDomain = data.xprime;
+ domainCacheTimestamp = now;
+ console.log(`[Xprime] Updated domain to: ${xprimeDomain}`);
+ }
+ return xprimeDomain;
+ });
+ }
+ return xprimeDomain;
+ }).catch(function(error) {
+ console.error(`[Xprime] Failed to fetch latest domain: ${error.message}`);
+ return xprimeDomain;
+ });
+}
+
+// Helper function to make HTTP requests
+function makeRequest(url, options = {}) {
+ const defaultHeaders = {
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
+ 'Accept': 'application/json, text/plain, */*',
+ 'Accept-Language': 'en-US,en;q=0.9',
+ 'Accept-Encoding': 'gzip, deflate, br'
+ };
+
+ return fetch(url, {
+ method: options.method || 'GET',
+ headers: { ...defaultHeaders, ...options.headers },
+ ...options
+ }).then(function(response) {
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
+ }
+ return response;
+ }).catch(function(error) {
+ console.error(`[Xprime] Request failed for ${url}: ${error.message}`);
+ throw error;
+ });
+}
+
+// Hardcoded Server List
+function getXprimeServers(api) {
+ console.log('[Xprime] Using hardcoded servers...');
+ const hardcodedServers = [
+ { name: 'primebox', status: 'ok' },
+ { name: 'rage', status: 'ok' },
+ // Temporarily disabled Phoenix server
+ // { name: 'phoenix', status: 'ok' },
+ // Temporarily disabled Fox server
+ // { name: 'fox', status: 'ok' }
+ ];
+ console.log(`[Xprime] Using ${hardcodedServers.length} hardcoded servers: ${hardcodedServers.map(s => s.name).join(', ')}`);
+ return Promise.resolve(hardcodedServers);
+}
+
+// Build Query Parameters
+function buildQueryParams(serverName, title, year, id, season, episode) {
+ const params = new URLSearchParams();
+ params.append('name', title || '');
+
+ if (serverName === 'primebox') {
+ if (year) params.append('fallback_year', year.toString());
+ if (season && episode) {
+ params.append('season', season.toString());
+ params.append('episode', episode.toString());
+ }
+ } else {
+ if (year) params.append('year', year.toString());
+ if (id) {
+ params.append('id', id);
+ params.append('imdb', id);
+ }
+ if (season && episode) {
+ params.append('season', season.toString());
+ params.append('episode', episode.toString());
+ }
+ }
+
+ return params.toString();
+}
+
+// Process PrimeBox Response
+function processPrimeBoxResponse(data, serverLabel, serverName) {
+ const links = [];
+ const subtitles = [];
+
+ try {
+ if (data.streams) {
+ // Process quality streams - fix: use available_qualities instead of qualities
+ if (data.available_qualities && Array.isArray(data.available_qualities)) {
+ data.available_qualities.forEach(quality => {
+ const url = data.streams[quality];
+ if (url) {
+ const normalizedQuality = getQualityFromName(quality);
+ links.push({
+ source: serverLabel,
+ name: `XPRIME ${serverName.charAt(0).toUpperCase() + serverName.slice(1)} - ${normalizedQuality}`,
+ url: url.trim(), // Remove any whitespace
+ quality: normalizedQuality,
+ type: 'VIDEO',
+ headers: WORKING_HEADERS,
+ referer: 'https://xprime.tv'
+ });
+ }
+ });
+ }
+ }
+
+ // Process subtitles
+ if (data.has_subtitles && data.subtitles && Array.isArray(data.subtitles)) {
+ data.subtitles.forEach(sub => {
+ if (sub.file) {
+ subtitles.push({
+ language: sub.label || 'Unknown',
+ url: sub.file.trim() // Remove any whitespace
+ });
+ }
+ });
+ }
+ } catch (error) {
+ console.error(`[Xprime] Error parsing PrimeBox response: ${error.message}`);
+ }
+
+ return { links, subtitles };
+}
+
+// Process Other Server Response
+function processOtherServerResponse(data, serverLabel, serverName) {
+ const links = [];
+
+ try {
+ // Special handling for Rage server response
+ if (serverName === 'rage' && data && data.success && Array.isArray(data.qualities)) {
+ data.qualities.forEach(function(q) {
+ if (q && q.url) {
+ const normalizedQuality = getQualityFromName(q.quality);
+ // Normalize size to a human-readable string
+ let sizeStr = 'Unknown';
+ if (typeof q.size === 'number' && isFinite(q.size)) {
+ const gb = q.size / (1024 * 1024 * 1024);
+ const mb = q.size / (1024 * 1024);
+ sizeStr = gb >= 1 ? `${gb.toFixed(2)} GB` : `${mb.toFixed(0)} MB`;
+ } else if (typeof q.size === 'string' && q.size.trim()) {
+ sizeStr = q.size.trim();
+ }
+ links.push({
+ source: serverLabel,
+ name: `XPRIME ${serverName.charAt(0).toUpperCase() + serverName.slice(1)} - ${normalizedQuality}`,
+ url: q.url,
+ quality: normalizedQuality,
+ size: sizeStr,
+ type: 'VIDEO',
+ headers: WORKING_HEADERS,
+ referer: 'https://xprime.tv'
+ });
+ }
+ });
+ } else if (data.url) {
+ // Try to extract quality from the URL or response data
+ let quality = 'Unknown';
+
+ // Check if there's quality information in the response
+ if (data.quality) {
+ quality = getQualityFromName(data.quality);
+ } else {
+ // Try to extract quality from URL patterns
+ const urlQualityMatch = data.url.match(/(\d{3,4})p/i);
+ if (urlQualityMatch) {
+ quality = getQualityFromName(urlQualityMatch[1] + 'p');
+ }
+ }
+
+ links.push({
+ source: serverLabel,
+ name: `XPRIME ${serverName.charAt(0).toUpperCase() + serverName.slice(1)} - ${quality}`,
+ url: data.url,
+ quality: quality,
+ type: 'M3U8',
+ headers: WORKING_HEADERS,
+ referer: 'https://xprime.tv'
+ });
+ }
+ } catch (error) {
+ console.error(`[Xprime] Error parsing server response: ${error.message}`);
+ }
+
+ return { links, subtitles: [] };
+}
+
+// Group streams by quality for better organization
+function groupStreamsByQuality(streams, subtitles, mediaInfo = {}) {
+ // Create media title with details
+ let mediaTitle = '';
+ if (mediaInfo.title) {
+ if (mediaInfo.mediaType === 'tv' && mediaInfo.season && mediaInfo.episode) {
+ mediaTitle = `${mediaInfo.title} S${String(mediaInfo.season).padStart(2, '0')}E${String(mediaInfo.episode).padStart(2, '0')}`;
+ } else if (mediaInfo.year) {
+ mediaTitle = `${mediaInfo.title} (${mediaInfo.year})`;
+ } else {
+ mediaTitle = mediaInfo.title;
+ }
+ }
+
+ // Group streams by quality
+ const qualityGroups = {};
+
+ streams.forEach(stream => {
+ const quality = stream.quality || 'Unknown';
+ if (!qualityGroups[quality]) {
+ qualityGroups[quality] = [];
+ }
+
+ qualityGroups[quality].push({
+ name: stream.name,
+ title: mediaTitle || '',
+ url: stream.url,
+ quality: quality,
+ size: stream.size || 'Unknown',
+ headers: stream.headers || WORKING_HEADERS,
+ subtitles: subtitles
+ });
+ });
+
+ // Define quality order (highest to lowest)
+ const qualityOrder = ['4K', '1440p', '1080p', '720p', '480p', '360p', '240p', 'Unknown'];
+
+ // Sort and flatten the grouped streams
+ const sortedStreams = [];
+ qualityOrder.forEach(quality => {
+ if (qualityGroups[quality]) {
+ // Sort streams within the same quality by server name
+ qualityGroups[quality].sort((a, b) => a.name.localeCompare(b.name));
+ sortedStreams.push(...qualityGroups[quality]);
+ }
+ });
+
+ // Add any qualities not in the predefined order
+ Object.keys(qualityGroups).forEach(quality => {
+ if (!qualityOrder.includes(quality)) {
+ qualityGroups[quality].sort((a, b) => a.name.localeCompare(b.name));
+ sortedStreams.push(...qualityGroups[quality]);
+ }
+ });
+
+ return sortedStreams;
+}
+
+// Get movie/TV show details from TMDB
+function getTMDBDetails(tmdbId, mediaType) {
+ const endpoint = mediaType === 'tv' ? 'tv' : 'movie';
+ const url = `${TMDB_BASE_URL}/${endpoint}/${tmdbId}?api_key=${TMDB_API_KEY}&append_to_response=external_ids`;
+
+ return makeRequest(url)
+ .then(response => {
+ if (!response.ok) {
+ throw new Error(`TMDB API error: ${response.status}`);
+ }
+ return response.json();
+ })
+ .then(data => {
+ const title = mediaType === 'tv' ? data.name : data.title;
+ const releaseDate = mediaType === 'tv' ? data.first_air_date : data.release_date;
+ const year = releaseDate ? parseInt(releaseDate.split('-')[0]) : null;
+
+ return {
+ title: title,
+ year: year,
+ imdbId: data.external_ids?.imdb_id || null
+ };
+ });
+}
+
+// Main scraping function - Updated to match Nuvio interface
+function getStreams(tmdbId, mediaType = 'movie', season = null, episode = null) {
+ console.log(`[Xprime] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}${mediaType === 'tv' ? `, S:${season}E:${episode}` : ''}`);
+
+ // First, get movie/TV show details from TMDB
+ return getTMDBDetails(tmdbId, mediaType)
+ .then(mediaInfo => {
+ if (!mediaInfo.title) {
+ throw new Error('Could not extract title from TMDB response');
+ }
+
+ console.log(`[Xprime] TMDB Info: "${mediaInfo.title}" (${mediaInfo.year || 'N/A'})`);
+ console.log(`[Xprime] Searching for: ${mediaInfo.title} (${mediaInfo.year})`);
+
+ const { title, year, imdbId } = mediaInfo;
+ const type = mediaType; // Keep the original mediaType
+
+ return getXprimeDomain().then(function(api) {
+ return getXprimeServers(api).then(function(servers) {
+ if (servers.length === 0) {
+ console.log('[Xprime] No active servers found');
+ return [];
+ }
+
+ console.log(`[Xprime] Processing ${servers.length} servers in parallel`);
+
+ const allLinks = [];
+ const allSubtitles = [];
+
+ // Process servers in parallel for better performance
+ const serverPromises = servers.map(function(server) {
+ console.log(`[Xprime] Processing server: ${server.name}`);
+
+ // Rage server requires a different endpoint (backend.xprime.tv) and TMDB id param
+ let serverUrl;
+ if (server.name === 'rage') {
+ if (type === 'tv' && season && episode) {
+ serverUrl = `https://backend.xprime.tv/rage?id=${encodeURIComponent(tmdbId)}&season=${encodeURIComponent(season)}&episode=${encodeURIComponent(episode)}`;
+ } else {
+ serverUrl = `https://backend.xprime.tv/rage?id=${encodeURIComponent(tmdbId)}`;
+ }
+ } else {
+ const queryParams = buildQueryParams(server.name, title, year, imdbId, season, episode);
+ serverUrl = `${api}/${server.name}?${queryParams}`;
+ }
+
+ console.log(`[Xprime] Request URL: ${serverUrl}`);
+
+ return makeRequest(serverUrl, {
+ headers: {
+ 'Origin': server.name === 'rage' ? 'https://xprime.tv' : api,
+ 'Referer': server.name === 'rage' ? 'https://xprime.tv/' : api
+ }
+ }).then(function(response) {
+ return response.json().then(function(data) {
+ const serverLabel = `Xprime ${server.name.charAt(0).toUpperCase() + server.name.slice(1)}`;
+ let result;
+
+ if (server.name === 'primebox') {
+ result = processPrimeBoxResponse(data, serverLabel, server.name);
+ } else {
+ result = processOtherServerResponse(data, serverLabel, server.name);
+ }
+
+ console.log(`[Xprime] Server ${server.name}: Found ${result.links.length} links, ${result.subtitles.length} subtitles`);
+ return result;
+ });
+ }).catch(function(error) {
+ console.error(`[Xprime] Error on server ${server.name}: ${error.message}`);
+ return { links: [], subtitles: [] };
+ });
+ });
+
+ // Wait for all server requests to complete
+ return Promise.allSettled(serverPromises).then(function(results) {
+ // Process results
+ for (const result of results) {
+ if (result.status === 'fulfilled') {
+ const { links, subtitles } = result.value;
+ allLinks.push(...links);
+ allSubtitles.push(...subtitles);
+ }
+ }
+
+ console.log(`[Xprime] Total found: ${allLinks.length} links, ${allSubtitles.length} subtitles`);
+
+ // Separate M3U8 links from direct video links
+ const m3u8Links = allLinks.filter(link => link.type === 'M3U8');
+ const directLinks = allLinks.filter(link => link.type !== 'M3U8');
+
+ let resolvedStreams = [];
+
+ // Resolve M3U8 playlists to extract individual quality streams
+ if (m3u8Links.length > 0) {
+ console.log(`[Xprime] Resolving ${m3u8Links.length} M3U8 playlists...`);
+
+ return resolveMultipleM3U8(m3u8Links).then(function(resolutionResult) {
+ if (resolutionResult.success && resolutionResult.streams.length > 0) {
+ console.log(`[Xprime] Successfully resolved ${resolutionResult.streams.length} quality streams`);
+ resolvedStreams = resolutionResult.streams;
+ } else {
+ console.log(`[Xprime] M3U8 resolution failed, using master playlist URLs`);
+ resolvedStreams = m3u8Links;
+ }
+
+ // Combine resolved streams with direct links
+ const finalLinks = [...directLinks, ...resolvedStreams];
+
+ console.log(`[Xprime] Final result: ${finalLinks.length} total streams (${resolvedStreams.length} from M3U8, ${directLinks.length} direct)`);
+
+ // Group streams by quality and format for Nuvio
+ const mediaInfoForGrouping = {
+ title: title,
+ year: year,
+ mediaType: mediaType,
+ season: season,
+ episode: episode
+ };
+ const formattedLinks = groupStreamsByQuality(finalLinks, allSubtitles, mediaInfoForGrouping);
+
+ // Add provider identifier for header detection
+ formattedLinks.forEach(link => {
+ link.provider = 'xprime';
+ });
+
+ return formattedLinks;
+ }).catch(function(error) {
+ console.error(`[Xprime] M3U8 resolution error: ${error.message}`);
+ resolvedStreams = m3u8Links;
+
+ // Combine resolved streams with direct links
+ const finalLinks = [...directLinks, ...resolvedStreams];
+
+ console.log(`[Xprime] Final result: ${finalLinks.length} total streams (${resolvedStreams.length} from M3U8, ${directLinks.length} direct)`);
+
+ // Group streams by quality and format for Nuvio
+ const mediaInfoForGrouping = {
+ title: title,
+ year: year,
+ mediaType: mediaType,
+ season: season,
+ episode: episode
+ };
+ const formattedLinks = groupStreamsByQuality(finalLinks, allSubtitles, mediaInfoForGrouping);
+
+ // Add provider identifier for header detection
+ formattedLinks.forEach(link => {
+ link.provider = 'xprime';
+ });
+
+ return formattedLinks;
+ });
+ } else {
+ // No M3U8 links, just return direct links
+ const finalLinks = [...directLinks, ...resolvedStreams];
+
+ console.log(`[Xprime] Final result: ${finalLinks.length} total streams (${resolvedStreams.length} from M3U8, ${directLinks.length} direct)`);
+
+ // Group streams by quality and format for Nuvio
+ const mediaInfoForGrouping = {
+ title: title,
+ year: year,
+ mediaType: mediaType,
+ season: season,
+ episode: episode
+ };
+ const formattedLinks = groupStreamsByQuality(finalLinks, allSubtitles, mediaInfoForGrouping);
+
+ // Add provider identifier for header detection
+ formattedLinks.forEach(link => {
+ link.provider = 'xprime';
+ });
+
+ return formattedLinks;
+ }
+ });
+ });
+ }).catch(function(error) {
+ console.error(`[Xprime] Scraping error: ${error.message}`);
+ return [];
+ });
+ })
+ .catch(function(error) {
+ console.error(`[Xprime] TMDB or scraping error: ${error.message}`);
+ return [];
+ });
+}
+
+// Export the main function
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = { getStreams };
+} else {
+ // For React Native environment
+ global.XprimeScraperModule = { getStreams };
+}
\ No newline at end of file