]*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
deleted file mode 100644
index 299ed97..0000000
--- a/local-scrapers-repo/providers/watch32.js
+++ /dev/null
@@ -1,547 +0,0 @@
-// 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
deleted file mode 100644
index 635207d..0000000
--- a/local-scrapers-repo/providers/xprime.js
+++ /dev/null
@@ -1,800 +0,0 @@
-// 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