├── Assets ├── Logo-2.png └── streamflix-1.png ├── .gitignore ├── providers ├── showbox.js ├── vixsrc.js ├── 4khdhub.js ├── dahmermovies.js ├── yflix.js ├── cinevibe.js ├── streamflix.js ├── hdrezka.js ├── mapple.js ├── vidnest.js └── myflixer-extractor.js ├── README.md └── manifest.json /Assets/Logo-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tapframe/nuvio-providers/HEAD/Assets/Logo-2.png -------------------------------------------------------------------------------- /Assets/streamflix-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tapframe/nuvio-providers/HEAD/Assets/streamflix-1.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | *.pid.lock 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage/ 15 | 16 | # nyc test coverage 17 | .nyc_output 18 | 19 | # Grunt intermediate storage 20 | .grunt 21 | 22 | # Bower dependency directory 23 | bower_components 24 | 25 | # node-waf configuration 26 | .lock-wscript 27 | 28 | # Compiled binary addons 29 | build/Release 30 | 31 | # Dependency directories 32 | node_modules/ 33 | jspm_packages/ 34 | 35 | # Optional npm cache directory 36 | .npm 37 | 38 | # Optional REPL history 39 | .node_repl_history 40 | 41 | # Output of 'npm pack' 42 | *.tgz 43 | 44 | # Yarn Integrity file 45 | .yarn-integrity 46 | 47 | # dotenv environment variables file 48 | .env 49 | 50 | # IDE files 51 | .vscode/ 52 | .idea/ 53 | *.swp 54 | *.swo 55 | *~ 56 | 57 | # OS generated files 58 | .DS_Store 59 | .DS_Store? 60 | ._* 61 | .Spotlight-V100 62 | .Trashes 63 | ehthumbs.db 64 | Thumbs.db 65 | 66 | # Logs 67 | logs 68 | *.log 69 | 70 | # Cache files 71 | .cache/ 72 | *.cache 73 | 74 | # Temporary files 75 | tmp/ 76 | temp/ 77 | 78 | # Test files (if any) 79 | test/ 80 | *.test.js 81 | *.spec.js 82 | debug_driveleech.js 83 | debug_instant_download.js 84 | debug_instant_real.js 85 | debug_uhdmovies_mov.js 86 | test_instant_fixed.js 87 | test_instant_priority.js 88 | .gitignore 89 | test_instant_real.js 90 | test_instant.js 91 | test_uhdmovies.js 92 | episodesrefer.html 93 | test_moviesmod.js 94 | moviesmodOG.js 95 | discord-post.md 96 | test_hdrezka.js 97 | hdrezkaOG.js 98 | test_dahmermovies.js 99 | dahmer-movies-fetcher.js 100 | xprime-scraper.js 101 | test_xprime.js 102 | m3u8-resolver.js 103 | test_myflixer.js 104 | 4khdhubog.js 105 | ShowboxOG.js 106 | test_showboxog.js 107 | test_vidsrc.js 108 | vidsrcextractor.js 109 | 110 | test_showbox.js 111 | videasy-scraper.js 112 | test_videasy.js 113 | test_vidnest.js 114 | test_mapple.js 115 | test_cinevibe.js 116 | castle_cli-2.py 117 | -------------------------------------------------------------------------------- /providers/showbox.js: -------------------------------------------------------------------------------- 1 | // ShowBox Scraper for Nuvio Local Scrapers 2 | // React Native compatible version - Promise-based approach only 3 | 4 | // TMDB API Configuration 5 | const TMDB_API_KEY = '439c478a771f35c05022f9feabcca01c'; 6 | const TMDB_BASE_URL = 'https://api.themoviedb.org/3'; 7 | 8 | // ShowBox API Configuration 9 | const SHOWBOX_API_BASE = 'https://febapi.nuvioapp.space/api/media'; 10 | 11 | // Working headers for ShowBox API 12 | const WORKING_HEADERS = { 13 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36', 14 | 'Accept': 'application/json', 15 | 'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8', 16 | 'Accept-Encoding': 'gzip, deflate, br, zstd', 17 | 'Content-Type': 'application/json' 18 | }; 19 | 20 | // UI token (cookie) is provided by the host app via per-scraper settings (Plugin Screen) 21 | function getUiToken() { 22 | try { 23 | // Prefer sandbox-injected globals 24 | if (typeof global !== 'undefined' && global.SCRAPER_SETTINGS && global.SCRAPER_SETTINGS.uiToken) { 25 | return String(global.SCRAPER_SETTINGS.uiToken); 26 | } 27 | if (typeof window !== 'undefined' && window.SCRAPER_SETTINGS && window.SCRAPER_SETTINGS.uiToken) { 28 | return String(window.SCRAPER_SETTINGS.uiToken); 29 | } 30 | } catch (e) { 31 | // ignore and fall through 32 | } 33 | return ''; 34 | } 35 | 36 | // OSS Group is provided by the host app via per-scraper settings (Plugin Screen) - optional 37 | function getOssGroup() { 38 | try { 39 | // Prefer sandbox-injected globals 40 | if (typeof global !== 'undefined' && global.SCRAPER_SETTINGS && global.SCRAPER_SETTINGS.ossGroup) { 41 | return String(global.SCRAPER_SETTINGS.ossGroup); 42 | } 43 | if (typeof window !== 'undefined' && window.SCRAPER_SETTINGS && window.SCRAPER_SETTINGS.ossGroup) { 44 | return String(window.SCRAPER_SETTINGS.ossGroup); 45 | } 46 | } catch (e) { 47 | // ignore and fall through 48 | } 49 | return null; // OSS group is optional 50 | } 51 | 52 | // Utility Functions 53 | function getQualityFromName(qualityStr) { 54 | if (!qualityStr) return 'Unknown'; 55 | 56 | const quality = qualityStr.toUpperCase(); 57 | 58 | // Map API quality values to normalized format 59 | if (quality === 'ORG' || quality === 'ORIGINAL') return 'Original'; 60 | if (quality === '4K' || quality === '2160P') return '4K'; 61 | if (quality === '1440P' || quality === '2K') return '1440p'; 62 | if (quality === '1080P' || quality === 'FHD') return '1080p'; 63 | if (quality === '720P' || quality === 'HD') return '720p'; 64 | if (quality === '480P' || quality === 'SD') return '480p'; 65 | if (quality === '360P') return '360p'; 66 | if (quality === '240P') return '240p'; 67 | 68 | // Try to extract number from string and format consistently 69 | const match = qualityStr.match(/(\d{3,4})[pP]?/); 70 | if (match) { 71 | const resolution = parseInt(match[1]); 72 | if (resolution >= 2160) return '4K'; 73 | if (resolution >= 1440) return '1440p'; 74 | if (resolution >= 1080) return '1080p'; 75 | if (resolution >= 720) return '720p'; 76 | if (resolution >= 480) return '480p'; 77 | if (resolution >= 360) return '360p'; 78 | return '240p'; 79 | } 80 | 81 | return 'Unknown'; 82 | } 83 | 84 | function formatFileSize(sizeStr) { 85 | if (!sizeStr) return 'Unknown'; 86 | 87 | // If it's already formatted (like "15.44 GB" or "224.39 MB"), return as is 88 | if (typeof sizeStr === 'string' && (sizeStr.includes('GB') || sizeStr.includes('MB') || sizeStr.includes('KB'))) { 89 | return sizeStr; 90 | } 91 | 92 | // If it's a number, convert to GB/MB 93 | if (typeof sizeStr === 'number') { 94 | const gb = sizeStr / (1024 * 1024 * 1024); 95 | if (gb >= 1) { 96 | return `${gb.toFixed(2)} GB`; 97 | } else { 98 | const mb = sizeStr / (1024 * 1024); 99 | return `${mb.toFixed(2)} MB`; 100 | } 101 | } 102 | 103 | return sizeStr; 104 | } 105 | 106 | // Helper function to make HTTP requests 107 | function makeRequest(url, options = {}) { 108 | return fetch(url, { 109 | method: options.method || 'GET', 110 | headers: { ...WORKING_HEADERS, ...options.headers }, 111 | ...options 112 | }).then(function(response) { 113 | if (!response.ok) { 114 | throw new Error(`HTTP ${response.status}: ${response.statusText}`); 115 | } 116 | return response; 117 | }).catch(function(error) { 118 | console.error(`[ShowBox] Request failed for ${url}: ${error.message}`); 119 | throw error; 120 | }); 121 | } 122 | 123 | // Get movie/TV show details from TMDB 124 | function getTMDBDetails(tmdbId, mediaType) { 125 | const endpoint = mediaType === 'tv' ? 'tv' : 'movie'; 126 | const url = `${TMDB_BASE_URL}/${endpoint}/${tmdbId}?api_key=${TMDB_API_KEY}`; 127 | 128 | return makeRequest(url) 129 | .then(function(response) { 130 | return response.json(); 131 | }) 132 | .then(function(data) { 133 | const title = mediaType === 'tv' ? data.name : data.title; 134 | const releaseDate = mediaType === 'tv' ? data.first_air_date : data.release_date; 135 | const year = releaseDate ? parseInt(releaseDate.split('-')[0]) : null; 136 | 137 | return { 138 | title: title, 139 | year: year 140 | }; 141 | }) 142 | .catch(function(error) { 143 | console.log(`[ShowBox] TMDB lookup failed: ${error.message}`); 144 | return { 145 | title: `TMDB ID ${tmdbId}`, 146 | year: null 147 | }; 148 | }); 149 | } 150 | 151 | // Process ShowBox API response - new format with versions and links 152 | function processShowBoxResponse(data, mediaInfo, mediaType, seasonNum, episodeNum) { 153 | const streams = []; 154 | 155 | try { 156 | if (!data || !data.success) { 157 | console.log(`[ShowBox] API returned unsuccessful response`); 158 | return streams; 159 | } 160 | 161 | if (!data.versions || !Array.isArray(data.versions) || data.versions.length === 0) { 162 | console.log(`[ShowBox] No versions found in API response`); 163 | return streams; 164 | } 165 | 166 | console.log(`[ShowBox] Processing ${data.versions.length} version(s)`); 167 | 168 | // Build title with year and episode info if TV 169 | let streamTitle = mediaInfo.title || 'Unknown Title'; 170 | if (mediaInfo.year) { 171 | streamTitle += ` (${mediaInfo.year})`; 172 | } 173 | if (mediaType === 'tv' && seasonNum && episodeNum) { 174 | streamTitle = `${mediaInfo.title || 'Unknown'} S${String(seasonNum).padStart(2, '0')}E${String(episodeNum).padStart(2, '0')}`; 175 | if (mediaInfo.year) { 176 | streamTitle += ` (${mediaInfo.year})`; 177 | } 178 | } 179 | 180 | // Process each version 181 | data.versions.forEach(function(version, versionIndex) { 182 | const versionName = version.name || `Version ${versionIndex + 1}`; 183 | const versionSize = version.size || 'Unknown'; 184 | 185 | // Process each link in the version 186 | if (version.links && Array.isArray(version.links)) { 187 | version.links.forEach(function(link) { 188 | if (!link.url) return; 189 | 190 | const normalizedQuality = getQualityFromName(link.quality || 'Unknown'); 191 | const linkSize = link.size || versionSize; 192 | const linkName = link.name || `${normalizedQuality}`; 193 | 194 | // Create stream name - use version number if multiple versions exist 195 | let streamName = 'ShowBox'; 196 | if (data.versions.length > 1) { 197 | streamName += ` V${versionIndex + 1}`; 198 | } 199 | streamName += ` ${normalizedQuality}`; 200 | 201 | streams.push({ 202 | name: streamName, 203 | title: streamTitle, 204 | url: link.url, 205 | quality: normalizedQuality, 206 | size: formatFileSize(linkSize), 207 | provider: 'showbox', 208 | speed: link.speed || null 209 | }); 210 | 211 | console.log(`[ShowBox] Added ${normalizedQuality} stream from ${versionName}: ${link.url.substring(0, 50)}...`); 212 | }); 213 | } 214 | }); 215 | 216 | } catch (error) { 217 | console.error(`[ShowBox] Error processing response: ${error.message}`); 218 | } 219 | 220 | return streams; 221 | } 222 | 223 | // Main scraping function 224 | function getStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) { 225 | console.log(`[ShowBox] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}${mediaType === 'tv' ? `, S:${seasonNum}E:${episodeNum}` : ''}`); 226 | 227 | // Get cookie (uiToken) - required 228 | const cookie = getUiToken(); 229 | if (!cookie) { 230 | console.error('[ShowBox] No UI token (cookie) found in scraper settings'); 231 | return Promise.resolve([]); 232 | } 233 | 234 | // Get OSS group - optional 235 | const ossGroup = getOssGroup(); 236 | console.log(`[ShowBox] Using cookie: ${cookie.substring(0, 20)}...${ossGroup ? `, OSS Group: ${ossGroup}` : ' (no OSS group)'}`); 237 | 238 | // Get TMDB details for title formatting 239 | return getTMDBDetails(tmdbId, mediaType) 240 | .then(function(mediaInfo) { 241 | console.log(`[ShowBox] TMDB Info: "${mediaInfo.title}" (${mediaInfo.year || 'N/A'})`); 242 | 243 | // Build API URL based on media type 244 | let apiUrl; 245 | if (mediaType === 'tv' && seasonNum && episodeNum) { 246 | // TV format: /api/media/tv/:tmdbId/oss=:ossGroup/:season/:episode?cookie=:cookie 247 | if (ossGroup) { 248 | apiUrl = `${SHOWBOX_API_BASE}/tv/${tmdbId}/oss=${ossGroup}/${seasonNum}/${episodeNum}?cookie=${encodeURIComponent(cookie)}`; 249 | } else { 250 | apiUrl = `${SHOWBOX_API_BASE}/tv/${tmdbId}/${seasonNum}/${episodeNum}?cookie=${encodeURIComponent(cookie)}`; 251 | } 252 | } else { 253 | // Movie format: /api/media/movie/:tmdbId?cookie=:cookie 254 | apiUrl = `${SHOWBOX_API_BASE}/movie/${tmdbId}?cookie=${encodeURIComponent(cookie)}`; 255 | } 256 | 257 | console.log(`[ShowBox] Requesting: ${apiUrl}`); 258 | 259 | // Make request to ShowBox API 260 | return makeRequest(apiUrl) 261 | .then(function(response) { 262 | console.log(`[ShowBox] API Response status: ${response.status}`); 263 | return response.json(); 264 | }) 265 | .then(function(data) { 266 | console.log(`[ShowBox] API Response received:`, JSON.stringify(data, null, 2)); 267 | 268 | // Process the response 269 | const streams = processShowBoxResponse(data, mediaInfo, mediaType, seasonNum, episodeNum); 270 | 271 | if (streams.length === 0) { 272 | console.log(`[ShowBox] No streams found in API response`); 273 | return []; 274 | } 275 | 276 | // Sort streams by quality (highest first) 277 | streams.sort(function(a, b) { 278 | const qualityOrder = { 279 | 'Original': 6, 280 | '4K': 5, 281 | '1440p': 4, 282 | '1080p': 3, 283 | '720p': 2, 284 | '480p': 1, 285 | '360p': 0, 286 | '240p': -1, 287 | 'Unknown': -2 288 | }; 289 | return (qualityOrder[b.quality] || -2) - (qualityOrder[a.quality] || -2); 290 | }); 291 | 292 | console.log(`[ShowBox] Returning ${streams.length} streams`); 293 | return streams; 294 | }) 295 | .catch(function(error) { 296 | console.error(`[ShowBox] API request failed: ${error.message}`); 297 | throw error; 298 | }); 299 | }) 300 | .catch(function(error) { 301 | console.error(`[ShowBox] Error in getStreams: ${error.message}`); 302 | return []; // Return empty array on error as per Nuvio scraper guidelines 303 | }); 304 | } 305 | 306 | // Export the main function 307 | if (typeof module !== 'undefined' && module.exports) { 308 | module.exports = { getStreams }; 309 | } else { 310 | // For React Native environment 311 | global.ShowBoxScraperModule = { getStreams }; 312 | } 313 | -------------------------------------------------------------------------------- /providers/vixsrc.js: -------------------------------------------------------------------------------- 1 | // Vixsrc Scraper for Nuvio Local Scrapers 2 | // React Native compatible version - Standalone (no external dependencies) 3 | // Converted to Promise-based syntax for sandbox compatibility 4 | 5 | // Constants 6 | const TMDB_API_KEY = "68e094699525b18a70bab2f86b1fa706"; 7 | const BASE_URL = 'https://vixsrc.to'; 8 | 9 | // Helper function to make HTTP requests with default headers 10 | function makeRequest(url, options = {}) { 11 | const defaultHeaders = { 12 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 13 | 'Accept': 'application/json,*/*', 14 | 'Accept-Language': 'en-US,en;q=0.5', 15 | 'Accept-Encoding': 'gzip, deflate', 16 | 'Connection': 'keep-alive', 17 | ...options.headers 18 | }; 19 | 20 | return fetch(url, { 21 | method: options.method || 'GET', 22 | headers: defaultHeaders, 23 | ...options 24 | }) 25 | .then(response => { 26 | if (!response.ok) { 27 | throw new Error(`HTTP ${response.status}: ${response.statusText}`); 28 | } 29 | return response; 30 | }) 31 | .catch(error => { 32 | console.error(`[Vixsrc] Request failed for ${url}: ${error.message}`); 33 | throw error; 34 | }); 35 | } 36 | 37 | // Helper function to get TMDB info 38 | function getTmdbInfo(tmdbId, mediaType) { 39 | const url = `https://api.themoviedb.org/3/${mediaType === 'tv' ? 'tv' : 'movie'}/${tmdbId}?api_key=${TMDB_API_KEY}`; 40 | 41 | return makeRequest(url) 42 | .then(response => response.json()) 43 | .then(data => { 44 | const title = mediaType === 'tv' ? data.name : data.title; 45 | const year = mediaType === 'tv' ? data.first_air_date?.substring(0, 4) : data.release_date?.substring(0, 4); 46 | 47 | if (!title) { 48 | throw new Error('Could not extract title from TMDB response'); 49 | } 50 | 51 | console.log(`[Vixsrc] TMDB Info: "${title}" (${year})`); 52 | return { title, year, data }; 53 | }); 54 | } 55 | 56 | // Helper function to parse M3U8 playlist 57 | function parseM3U8Playlist(content, baseUrl) { 58 | const streams = []; 59 | const audioTracks = []; 60 | const lines = content.split('\n'); 61 | 62 | let currentStream = null; 63 | 64 | for (let i = 0; i < lines.length; i++) { 65 | const line = lines[i].trim(); 66 | 67 | // Parse video streams 68 | if (line.startsWith('#EXT-X-STREAM-INF:')) { 69 | // Parse stream info 70 | const bandwidthMatch = line.match(/BANDWIDTH=(\d+)/); 71 | const resolutionMatch = line.match(/RESOLUTION=(\d+x\d+)/); 72 | const nameMatch = line.match(/NAME="([^"]+)"/) || line.match(/NAME=([^,]+)/); 73 | 74 | if (bandwidthMatch) { 75 | currentStream = { 76 | bandwidth: parseInt(bandwidthMatch[1]), 77 | resolution: resolutionMatch ? resolutionMatch[1] : 'Unknown', 78 | quality: nameMatch ? nameMatch[1] : getQualityFromResolution(resolutionMatch ? resolutionMatch[1] : 'Unknown'), 79 | url: '' 80 | }; 81 | } 82 | } 83 | // Parse audio tracks 84 | else if (line.startsWith('#EXT-X-MEDIA:')) { 85 | const typeMatch = line.match(/TYPE=([^,]+)/); 86 | const nameMatch = line.match(/NAME="([^"]+)"/); 87 | const groupIdMatch = line.match(/GROUP-ID="([^"]+)"/); 88 | const languageMatch = line.match(/LANGUAGE="([^"]+)"/); 89 | const uriMatch = line.match(/URI="([^"]+)"/); 90 | 91 | if (typeMatch && typeMatch[1] === 'AUDIO') { 92 | const audioTrack = { 93 | type: 'audio', 94 | name: nameMatch ? nameMatch[1] : 'Unknown Audio', 95 | groupId: groupIdMatch ? groupIdMatch[1] : 'unknown', 96 | language: languageMatch ? languageMatch[1] : 'unknown', 97 | url: uriMatch ? resolveUrl(uriMatch[1], baseUrl) : null 98 | }; 99 | audioTracks.push(audioTrack); 100 | } 101 | } 102 | // Handle URLs for video streams 103 | else if (line.startsWith('http') && currentStream) { 104 | // This is the URL for the current video stream 105 | currentStream.url = line.startsWith('http') ? line : resolveUrl(line, baseUrl); 106 | streams.push(currentStream); 107 | currentStream = null; 108 | } 109 | } 110 | 111 | console.log(`[Vixsrc] Found ${audioTracks.length} audio tracks:`); 112 | audioTracks.forEach((track, index) => { 113 | console.log(` ${index + 1}. ${track.name} (${track.language}) - ${track.url ? 'Available' : 'No URL'}`); 114 | }); 115 | 116 | return { streams, audioTracks }; 117 | } 118 | 119 | // Helper function to get quality from resolution 120 | function getQualityFromResolution(resolution) { 121 | if (resolution.includes('1920x1080') || resolution.includes('1080')) { 122 | return '1080p'; 123 | } else if (resolution.includes('1280x720') || resolution.includes('720')) { 124 | return '720p'; 125 | } else if (resolution.includes('854x480') || resolution.includes('640x480') || resolution.includes('480')) { 126 | return '480p'; 127 | } else if (resolution.includes('640x360') || resolution.includes('360')) { 128 | return '360p'; 129 | } else { 130 | return resolution; 131 | } 132 | } 133 | 134 | // Helper function to resolve URLs 135 | function resolveUrl(url, baseUrl) { 136 | if (url.startsWith('http')) { 137 | return url; 138 | } 139 | 140 | // Handle relative URLs 141 | const baseUrlObj = new URL(baseUrl); 142 | if (url.startsWith('/')) { 143 | return `${baseUrlObj.protocol}//${baseUrlObj.host}${url}`; 144 | } else { 145 | const basePath = baseUrlObj.pathname.substring(0, baseUrlObj.pathname.lastIndexOf('/') + 1); 146 | return `${baseUrlObj.protocol}//${baseUrlObj.host}${basePath}${url}`; 147 | } 148 | } 149 | 150 | // Helper function to extract stream URL from Vixsrc page 151 | function extractStreamFromPage(url, contentType, contentId, seasonNum, episodeNum) { 152 | let vixsrcUrl; 153 | let subtitleApiUrl; 154 | 155 | if (contentType === 'movie') { 156 | vixsrcUrl = `${BASE_URL}/movie/${contentId}`; 157 | subtitleApiUrl = `https://sub.wyzie.ru/search?id=${contentId}`; 158 | } else { 159 | vixsrcUrl = `${BASE_URL}/tv/${contentId}/${seasonNum}/${episodeNum}`; 160 | subtitleApiUrl = `https://sub.wyzie.ru/search?id=${contentId}&season=${seasonNum}&episode=${episodeNum}`; 161 | } 162 | 163 | console.log(`[Vixsrc] Fetching: ${vixsrcUrl}`); 164 | 165 | return makeRequest(vixsrcUrl, { 166 | headers: { 167 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' 168 | } 169 | }) 170 | .then(response => response.text()) 171 | .then(html => { 172 | console.log(`[Vixsrc] HTML length: ${html.length} characters`); 173 | 174 | let masterPlaylistUrl = null; 175 | 176 | // Method 1: Look for window.masterPlaylist (primary method) 177 | if (html.includes('window.masterPlaylist')) { 178 | console.log('[Vixsrc] Found window.masterPlaylist'); 179 | 180 | const urlMatch = html.match(/url:\s*['"]([^'"]+)['"]/); 181 | const tokenMatch = html.match(/['"]?token['"]?\s*:\s*['"]([^'"]+)['"]/); 182 | const expiresMatch = html.match(/['"]?expires['"]?\s*:\s*['"]([^'"]+)['"]/); 183 | 184 | if (urlMatch && tokenMatch && expiresMatch) { 185 | const baseUrl = urlMatch[1]; 186 | const token = tokenMatch[1]; 187 | const expires = expiresMatch[1]; 188 | 189 | console.log('[Vixsrc] Extracted tokens:'); 190 | console.log(` - Base URL: ${baseUrl}`); 191 | console.log(` - Token: ${token.substring(0, 20)}...`); 192 | console.log(` - Expires: ${expires}`); 193 | 194 | // Construct the master playlist URL 195 | if (baseUrl.includes('?b=1')) { 196 | masterPlaylistUrl = `${baseUrl}&token=${token}&expires=${expires}&h=1&lang=en`; 197 | } else { 198 | masterPlaylistUrl = `${baseUrl}?token=${token}&expires=${expires}&h=1&lang=en`; 199 | } 200 | 201 | console.log(`[Vixsrc] Constructed master playlist URL: ${masterPlaylistUrl}`); 202 | } 203 | } 204 | 205 | // Method 2: Look for direct .m3u8 URLs 206 | if (!masterPlaylistUrl) { 207 | const m3u8Match = html.match(/(https?:\/\/[^'"\s]+\.m3u8[^'"\s]*)/); 208 | if (m3u8Match) { 209 | masterPlaylistUrl = m3u8Match[1]; 210 | console.log('[Vixsrc] Found direct .m3u8 URL:', masterPlaylistUrl); 211 | } 212 | } 213 | 214 | // Method 3: Look for stream URLs in script tags 215 | if (!masterPlaylistUrl) { 216 | const scriptMatches = html.match(/]*>(.*?)<\/script>/gs); 217 | if (scriptMatches) { 218 | for (const script of scriptMatches) { 219 | const streamMatch = script.match(/['"]?(https?:\/\/[^'"\s]+(?:\.m3u8|playlist)[^'"\s]*)/); 220 | if (streamMatch) { 221 | masterPlaylistUrl = streamMatch[1]; 222 | console.log('[Vixsrc] Found stream in script:', masterPlaylistUrl); 223 | break; 224 | } 225 | } 226 | } 227 | } 228 | 229 | if (!masterPlaylistUrl) { 230 | console.log('[Vixsrc] No master playlist URL found'); 231 | return null; 232 | } 233 | 234 | return { masterPlaylistUrl, subtitleApiUrl }; 235 | }); 236 | } 237 | 238 | // Helper function to get subtitles 239 | function getSubtitles(subtitleApiUrl) { 240 | return makeRequest(subtitleApiUrl) 241 | .then(response => response.json()) 242 | .then(subtitleData => { 243 | // Find English subtitle track (same logic as original) 244 | let subtitleTrack = subtitleData.find(track => 245 | track.display.includes('English') && (track.encoding === 'ASCII' || track.encoding === 'UTF-8') 246 | ); 247 | 248 | if (!subtitleTrack) { 249 | subtitleTrack = subtitleData.find(track => track.display.includes('English') && track.encoding === 'CP1252'); 250 | } 251 | 252 | if (!subtitleTrack) { 253 | subtitleTrack = subtitleData.find(track => track.display.includes('English') && track.encoding === 'CP1250'); 254 | } 255 | 256 | if (!subtitleTrack) { 257 | subtitleTrack = subtitleData.find(track => track.display.includes('English') && track.encoding === 'CP850'); 258 | } 259 | 260 | const subtitles = subtitleTrack ? subtitleTrack.url : ''; 261 | console.log(subtitles ? `[Vixsrc] Found subtitles: ${subtitles}` : '[Vixsrc] No English subtitles found'); 262 | return subtitles; 263 | }) 264 | .catch(error => { 265 | console.log('[Vixsrc] Subtitle fetch failed:', error.message); 266 | return ''; 267 | }); 268 | } 269 | 270 | // Main function to get streams - adapted for Nuvio provider format 271 | function getStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) { 272 | console.log(`[Vixsrc] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}`); 273 | 274 | return getTmdbInfo(tmdbId, mediaType) 275 | .then(tmdbInfo => { 276 | const { title, year } = tmdbInfo; 277 | 278 | // Extract stream from Vixsrc page 279 | return extractStreamFromPage(null, mediaType, tmdbId, seasonNum, episodeNum); 280 | }) 281 | .then(streamData => { 282 | if (!streamData) { 283 | console.log('[Vixsrc] No stream data found'); 284 | return []; 285 | } 286 | 287 | const { masterPlaylistUrl, subtitleApiUrl } = streamData; 288 | 289 | // Return single master playlist with Auto quality 290 | console.log('[Vixsrc] Returning master playlist with Auto quality...'); 291 | 292 | // Get subtitles 293 | return getSubtitles(subtitleApiUrl) 294 | .then(subtitles => { 295 | // Return single stream with master playlist 296 | const nuvioStreams = [{ 297 | name: "Vixsrc", 298 | title: "Auto Quality Stream", 299 | url: masterPlaylistUrl, 300 | quality: 'Auto', 301 | type: 'direct', 302 | headers: { 303 | 'Referer': BASE_URL, 304 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36' 305 | } 306 | }]; 307 | 308 | console.log('[Vixsrc] Successfully processed 1 stream with Auto quality'); 309 | return nuvioStreams; 310 | }); 311 | }) 312 | .catch(error => { 313 | console.error(`[Vixsrc] Error in getStreams: ${error.message}`); 314 | return []; 315 | }); 316 | } 317 | 318 | // Export for React Native 319 | if (typeof module !== 'undefined' && module.exports) { 320 | module.exports = { getStreams }; 321 | } else { 322 | global.VixsrcScraperModule = { getStreams }; 323 | } 324 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nuvio Local Scrapers 2 | 3 | A collection of local scrapers for the Nuvio streaming application. These scrapers allow you to fetch streams from various sources directly within the app. 4 | 5 | ## Installation 6 | 7 | 1. Open Nuvio app 8 | 2. Go to Settings → Local Scrapers 9 | 3. Add this repository URL: 10 | ``` 11 | https://raw.githubusercontent.com/tapframe/nuvio-providers/refs/heads/main/ 12 | ``` 13 | 4. Enable the scrapers you want to use 14 | 15 | ## Scraper Development 16 | 17 | **💡 Tip:** Check existing scrapers in the `providers/` directory for real working examples before starting your own. 18 | 19 | ### Core Function 20 | **⚠️ IMPORTANT:** Your scraper must use Promise-based approach only. **async/await is NOT supported** in this sandboxed environment. 21 | 22 | Your scraper must export a `getStreams` function that returns a Promise: 23 | 24 | ```javascript 25 | function getStreams(tmdbId, mediaType, seasonNum, episodeNum) { 26 | return new Promise((resolve, reject) => { 27 | // Your scraping logic here - NO async/await allowed 28 | // Use .then() and .catch() for all async operations 29 | // Return array of stream objects or empty array on error 30 | resolve(streams); 31 | }); 32 | } 33 | 34 | // Export for React Native compatibility 35 | if (typeof module !== 'undefined' && module.exports) { 36 | module.exports = { getStreams }; 37 | } else { 38 | global.getStreams = getStreams; 39 | } 40 | ``` 41 | 42 | **Parameters:** 43 | - `tmdbId` (string): TMDB ID 44 | - `mediaType` (string): "movie" or "tv" 45 | - `seasonNum` (number): Season number (TV only) 46 | - `episodeNum` (number): Episode number (TV only) 47 | 48 | ### Stream Object Format 49 | Each stream must return this exact format (see `providers/xprime.js` for real examples): 50 | 51 | ```javascript 52 | { 53 | name: "XPrime Primebox - 1080p", // Provider + server name 54 | title: "Movie Title (2024)", // Media title with year 55 | url: "https://stream.url", // Direct stream URL 56 | quality: "1080p", // Quality (720p, 1080p, 4K, etc.) 57 | size: "Unknown", // Optional file size 58 | headers: WORKING_HEADERS, // Required headers for playback 59 | provider: "xprime" // Provider identifier 60 | } 61 | ``` 62 | 63 | ### Headers (When Needed) 64 | Include headers if the stream requires them for playback. Check `providers/xprime.js` for real WORKING_HEADERS example: 65 | 66 | ```javascript 67 | // From providers/xprime.js - real working headers 68 | const WORKING_HEADERS = { 69 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 70 | 'Accept': 'video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5', 71 | 'Accept-Language': 'en-US,en;q=0.9', 72 | 'Accept-Encoding': 'identity', 73 | 'Origin': 'https://xprime.tv', 74 | 'Referer': 'https://xprime.tv/', 75 | 'Sec-Fetch-Dest': 'video', 76 | 'Sec-Fetch-Mode': 'no-cors', 77 | 'Sec-Fetch-Site': 'cross-site', 78 | 'DNT': '1' 79 | }; 80 | ``` 81 | 82 | ### React Native Compatibility 83 | - **❌ async/await is NOT supported** in this sandboxed environment 84 | - **✅ Promise-based approach is COMPULSORY** - use `.then()` and `.catch()` 85 | - Use `fetch()` for HTTP requests (no axios) 86 | - Use `cheerio-without-node-native` for HTML parsing 87 | - Avoid Node.js modules (fs, path, crypto) 88 | 89 | ### Testing 90 | Create a test file to verify your scraper (see existing scrapers for examples): 91 | 92 | ```javascript 93 | const { getStreams } = require('./providers/xprime.js'); 94 | 95 | getStreams('550', 'movie').then(streams => { 96 | console.log('Found', streams.length, 'streams'); 97 | streams.forEach(stream => console.log(`${stream.name}: ${stream.quality}`)); 98 | }).catch(console.error); 99 | ``` 100 | 101 | ### Manifest Entry 102 | Add your scraper to `manifest.json` (see existing entries for examples): 103 | 104 | ```json 105 | { 106 | "id": "yourscraper", 107 | "name": "Your Scraper", 108 | "description": "Brief description of what your scraper does", 109 | "version": "1.0.0", 110 | "author": "Your Name", 111 | "supportedTypes": ["movie", "tv"], 112 | "filename": "providers/yourscraper.js", 113 | "enabled": true, 114 | "formats": ["mkv"], 115 | "logo": "https://your-logo-url.com/logo.png", 116 | "contentLanguage": ["en"] 117 | } 118 | ``` 119 | 120 | ## Publishing to GitHub 121 | 122 | 1. **Create a new repository on GitHub:** 123 | - Go to github.com 124 | - Click "New repository" 125 | - Name it `nuvio-local-scrapers` 126 | - Make it public 127 | - Don't initialize with README (we already have one) 128 | 129 | 2. **Upload files:** 130 | ```bash 131 | cd /path/to/local-scrapers-repo 132 | git init 133 | git add . 134 | git commit -m "Initial commit with UHD Movies scraper" 135 | git branch -M main 136 | git remote add origin https://github.com/tapframe/nuvio-local-scrapers.git 137 | git push -u origin main 138 | ``` 139 | 140 | 3. **Get the raw URL:** 141 | ``` 142 | https://raw.githubusercontent.com/tapframe/nuvio-local-scrapers/main/ 143 | ``` 144 | 145 | ## Contributing 146 | 147 | ### Development Workflow 148 | 149 | 1. **Fork this repository** 150 | ```bash 151 | # Clone your fork 152 | git clone https://github.com/tapframe/nuvio-local-scrapers.git 153 | cd nuvio-local-scrapers 154 | ``` 155 | 156 | 2. **Create a new branch** 157 | ```bash 158 | git checkout -b add-newscraper 159 | ``` 160 | 161 | 3. **Develop your scraper** 162 | - Create `newscraper.js` 163 | - Update `manifest.json` 164 | - Create `test_newscraper.js` 165 | - Test thoroughly 166 | 167 | 4. **Test your scraper** 168 | ```bash 169 | # Run tests 170 | node test_newscraper.js 171 | 172 | # Test with different content types 173 | # Verify stream URLs work 174 | # Check error handling 175 | ``` 176 | 177 | 5. **Commit and push** 178 | ```bash 179 | git add . 180 | git commit -m "Add NewScraper with support for movies and TV shows" 181 | git push origin add-newscraper 182 | ``` 183 | 184 | 6. **Submit a pull request** 185 | - Include description of the scraper 186 | - List supported features 187 | - Provide test results 188 | - Mention any limitations 189 | 190 | ### Code Review Checklist 191 | 192 | Before submitting, ensure your scraper: 193 | 194 | - [ ] **Follows naming conventions** (camelCase, descriptive names) 195 | - [ ] **Has proper error handling** (try-catch blocks, graceful failures) 196 | - [ ] **Includes comprehensive logging** (with scraper name prefix) 197 | - [ ] **Is React Native compatible** (no Node.js modules, uses fetch()) 198 | - [ ] **Has a working test file** (tests movies and TV shows) 199 | - [ ] **Updates manifest.json** (correct metadata and version) 200 | - [ ] **Respects rate limits** (reasonable delays between requests) 201 | - [ ] **Handles edge cases** (missing content, network errors) 202 | - [ ] **Returns proper stream objects** (correct format and required fields) 203 | - [ ] **Is well-documented** (comments explaining complex logic) 204 | 205 | ### Scraper Quality Standards 206 | 207 | #### Performance 208 | - Response time < 15 seconds for most requests 209 | - Handles concurrent requests gracefully 210 | - Minimal memory usage 211 | - Efficient DOM parsing 212 | 213 | #### Reliability 214 | - Success rate > 80% for popular content 215 | - Graceful degradation when source is unavailable 216 | - Proper timeout handling 217 | - Retry logic for transient failures 218 | 219 | #### User Experience 220 | - Clear, descriptive stream titles 221 | - Accurate quality and size information 222 | - Sorted results (highest quality first) 223 | - Consistent naming conventions 224 | 225 | ### Debugging Tips 226 | 227 | #### 1. Network Issues 228 | ```javascript 229 | // Add request/response logging 230 | console.log(`[YourScraper] Requesting: ${url}`); 231 | console.log(`[YourScraper] Response status: ${response.status}`); 232 | console.log(`[YourScraper] Response headers:`, response.headers); 233 | ``` 234 | 235 | #### 2. HTML Parsing Issues 236 | ```javascript 237 | // Log HTML content for inspection 238 | console.log(`[YourScraper] HTML length: ${html.length}`); 239 | console.log(`[YourScraper] Page title: ${$('title').text()}`); 240 | console.log(`[YourScraper] Found ${$('.target-selector').length} elements`); 241 | ``` 242 | 243 | #### 3. URL Resolution Issues 244 | ```javascript 245 | // Validate URLs before returning 246 | async function validateUrl(url) { 247 | try { 248 | const response = await fetch(url, { method: 'HEAD' }); 249 | return response.ok || response.status === 206; // 206 for partial content 250 | } catch (error) { 251 | return false; 252 | } 253 | } 254 | ``` 255 | 256 | ### Real-World Examples 257 | 258 | #### UHDMovies Scraper Features 259 | - **Episode-specific extraction** for TV shows 260 | - **Multiple tech domains** (tech.unblockedgames.world, tech.examzculture.in, etc.) 261 | - **SID link resolution** with multi-step form submission 262 | - **Driveleech URL processing** with multiple download methods 263 | - **Quality parsing** with technical details (10-bit, HEVC, HDR) 264 | 265 | #### MoviesMod Scraper Features 266 | - **Dynamic domain fetching** from GitHub repository 267 | - **String similarity matching** for content selection 268 | - **Intermediate link resolution** (modrefer.in decoding) 269 | - **Multiple download servers** (Resume Cloud, Worker Bot, Instant Download) 270 | - **Broken link filtering** (report pages, invalid URLs) 271 | - **Parallel processing** of multiple quality options 272 | 273 | ### Advanced Techniques 274 | 275 | #### 1. Multi-Domain Support 276 | ```javascript 277 | const TECH_DOMAINS = [ 278 | 'tech.unblockedgames.world', 279 | 'tech.examzculture.in', 280 | 'tech.creativeexpressionsblog.com', 281 | 'tech.examdegree.site' 282 | ]; 283 | 284 | function isTechDomain(url) { 285 | return TECH_DOMAINS.some(domain => url.includes(domain)); 286 | } 287 | ``` 288 | 289 | #### 2. Form-Based Authentication 290 | ```javascript 291 | async function submitVerificationForm(formUrl, formData) { 292 | const response = await fetch(formUrl, { 293 | method: 'POST', 294 | headers: { 295 | 'Content-Type': 'application/x-www-form-urlencoded', 296 | 'Referer': previousUrl 297 | }, 298 | body: new URLSearchParams(formData).toString() 299 | }); 300 | return response; 301 | } 302 | ``` 303 | 304 | #### 3. JavaScript Execution Simulation 305 | ```javascript 306 | // Extract dynamic values from JavaScript code 307 | function extractFromJavaScript(html) { 308 | const cookieMatch = html.match(/s_343\('([^']+)',\s*'([^']+)'/); 309 | const linkMatch = html.match(/c\.setAttribute\("href",\s*"([^"]+)"\)/); 310 | 311 | return { 312 | cookieName: cookieMatch?.[1], 313 | cookieValue: cookieMatch?.[2], 314 | linkPath: linkMatch?.[1] 315 | }; 316 | } 317 | ``` 318 | 319 | ### Maintenance 320 | 321 | #### Updating Existing Scrapers 322 | - Monitor source website changes 323 | - Update selectors and logic as needed 324 | - Test after updates 325 | - Increment version number in manifest 326 | 327 | #### Handling Source Changes 328 | - Implement fallback mechanisms 329 | - Use multiple extraction methods 330 | - Add domain rotation support 331 | - Monitor for breaking changes 332 | 333 | ### Troubleshooting 334 | 335 | #### Common Issues 336 | 337 | 1. **CORS Errors** 338 | - Use appropriate headers 339 | - Consider proxy solutions 340 | - Check source website restrictions 341 | 342 | 2. **Rate Limiting** 343 | - Add delays between requests 344 | - Implement exponential backoff 345 | - Use different user agents 346 | 347 | 3. **Captcha/Bot Detection** 348 | - Rotate user agents 349 | - Add realistic delays 350 | - Implement session management 351 | 352 | 4. **Dynamic Content** 353 | - Look for API endpoints 354 | - Parse JavaScript for data 355 | - Use multiple extraction methods 356 | 357 | #### Getting Help 358 | 359 | - Check existing scraper implementations 360 | - Review error logs carefully 361 | - Test with different content types 362 | - Ask for help in community discussions 363 | 364 | --- 365 | 366 | ## 🧰 Tools & Technologies 367 | 368 |

369 | 370 | 371 | 372 |

373 | 374 | --- 375 | 376 | 377 | 378 | ## 📄 License 379 | 380 | [![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](http://www.gnu.org/licenses/gpl-3.0.en.html) 381 | 382 | These scrapers are **free software**: you can use, study, share, and modify them as you wish. 383 | 384 | They are distributed under the terms of the [GNU General Public License](https://www.gnu.org/licenses/gpl.html) version 3 or later, published by the Free Software Foundation. 385 | 386 | --- 387 | 388 | ## ⚖️ DMCA Disclaimer 389 | 390 | We hereby issue this notice to clarify that these scrapers function similarly to a standard web browser by fetching video files from the internet. 391 | 392 | - **No content is hosted by this repository or the Nuvio application.** 393 | - Any content accessed is hosted by third-party websites. 394 | - Users are solely responsible for their usage and must comply with their local laws. 395 | 396 | If you believe content is violating copyright laws, please contact the **actual file hosts**, **not** the developers of this repository or the Nuvio app. 397 | 398 | --- 399 | 400 | ## Support 401 | 402 | For issues or questions: 403 | - Open an issue on GitHub 404 | - Check the Nuvio app documentation 405 | - Join the community discussions 406 | 407 | --- 408 | 409 | **Thank You for using Nuvio Local Scrapers!** 410 | -------------------------------------------------------------------------------- /providers/4khdhub.js: -------------------------------------------------------------------------------- 1 | const cheerio = require('cheerio-without-node-native'); 2 | 3 | // Constants 4 | const BASE_URL = 'https://4khdhub.fans'; 5 | const TMDB_API_KEY = '439c478a771f35c05022f9feabcca01c'; 6 | 7 | // Polyfills & Helpers 8 | // ----------------------------------------------------------------------------- 9 | 10 | // atob Polyfill 11 | const atob = (input) => { 12 | const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; 13 | let str = String(input).replace(/=+$/, ''); 14 | if (str.length % 4 === 1) throw new Error("'atob' failed: The string to be decoded is not correctly encoded."); 15 | let output = ''; 16 | for ( 17 | let bc = 0, bs, buffer, i = 0; 18 | (buffer = str.charAt(i++)); 19 | ~buffer && ((bs = bc % 4 ? bs * 64 + buffer : buffer), bc++ % 4) 20 | ? (output += String.fromCharCode(255 & (bs >> ((-2 * bc) & 6)))) 21 | : 0 22 | ) { 23 | buffer = chars.indexOf(buffer); 24 | } 25 | return output; 26 | }; 27 | 28 | // Rot13 Cipher 29 | const rot13Cipher = (str) => { 30 | return str.replace(/[a-zA-Z]/g, function (c) { 31 | return String.fromCharCode((c <= 'Z' ? 90 : 122) >= (c = c.charCodeAt(0) + 13) ? c : c - 26); 32 | }); 33 | }; 34 | 35 | // Levenshtein Distance 36 | const levenshtein = { 37 | get: function (s, t) { 38 | if (s === t) { 39 | return 0; 40 | } 41 | var n = s.length, m = t.length; 42 | if (n === 0) { 43 | return m; 44 | } 45 | if (m === 0) { 46 | return n; 47 | } 48 | var d = []; 49 | for (var i = 0; i <= n; i++) { 50 | d[i] = []; 51 | d[i][0] = i; 52 | } 53 | for (var j = 0; j <= m; j++) { 54 | d[0][j] = j; 55 | } 56 | for (var i = 1; i <= n; i++) { 57 | for (var j = 1; j <= m; j++) { 58 | var cost = (s.charAt(i - 1) === t.charAt(j - 1)) ? 0 : 1; 59 | d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost); 60 | } 61 | } 62 | return d[n][m]; 63 | } 64 | }; 65 | 66 | // Bytes Parser/Formatter 67 | const bytes = { 68 | parse: function (val) { 69 | if (typeof val === 'number') return val; 70 | if (!val) return 0; 71 | var match = val.match(/^([0-9.]+)\s*([a-zA-Z]+)$/); 72 | if (!match) return 0; 73 | var num = parseFloat(match[1]); 74 | var unit = match[2].toLowerCase(); 75 | var multiplier = 1; 76 | if (unit.indexOf('k') === 0) multiplier = 1024; 77 | else if (unit.indexOf('m') === 0) multiplier = 1024 * 1024; 78 | else if (unit.indexOf('g') === 0) multiplier = 1024 * 1024 * 1024; 79 | else if (unit.indexOf('t') === 0) multiplier = 1024 * 1024 * 1024 * 1024; 80 | return num * multiplier; 81 | }, 82 | format: function (val) { 83 | if (val === 0) return '0 B'; 84 | var k = 1024; 85 | var sizes = ['B', 'KB', 'MB', 'GB', 'TB']; 86 | var i = Math.floor(Math.log(val) / Math.log(k)); 87 | if (i < 0) i = 0; 88 | return parseFloat((val / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; 89 | } 90 | }; 91 | 92 | // Fetch Helper 93 | function fetchText(url, options) { 94 | options = options || {}; 95 | return fetch(url, { 96 | headers: Object.assign({ 97 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' 98 | }, options.headers || {}) 99 | }) 100 | .then(function (res) { 101 | return res.text(); 102 | }) 103 | .catch(function (err) { 104 | console.log('[4KHDHub] Request failed for ' + url + ': ' + err.message); 105 | return null; 106 | }); 107 | } 108 | 109 | // ----------------------------------------------------------------------------- 110 | // Core Logic 111 | // ----------------------------------------------------------------------------- 112 | 113 | function getTmdbDetails(tmdbId, type) { 114 | var isSeries = type === 'series' || type === 'tv'; 115 | var url = 'https://api.themoviedb.org/3/' + (isSeries ? 'tv' : 'movie') + '/' + tmdbId + '?api_key=' + TMDB_API_KEY; 116 | console.log('[4KHDHub] Fetching TMDB details from: ' + url); 117 | return fetch(url) 118 | .then(function (res) { return res.json(); }) 119 | .then(function (data) { 120 | if (isSeries) { 121 | return { 122 | title: data.name, 123 | year: data.first_air_date ? parseInt(data.first_air_date.split('-')[0]) : 0 124 | }; 125 | } else { 126 | return { 127 | title: data.title, 128 | year: data.release_date ? parseInt(data.release_date.split('-')[0]) : 0 129 | }; 130 | } 131 | }) 132 | .catch(function (error) { 133 | console.log('[4KHDHub] TMDB request failed: ' + error.message); 134 | return null; 135 | }); 136 | } 137 | 138 | function fetchPageUrl(name, year, isSeries) { 139 | var searchUrl = BASE_URL + '/?s=' + encodeURIComponent(name + ' ' + year); 140 | return fetchText(searchUrl).then(function (html) { 141 | if (!html) return null; 142 | var $ = cheerio.load(html); 143 | var targetType = isSeries ? 'Series' : 'Movies'; 144 | 145 | var matchingCards = $('.movie-card') 146 | .filter(function (_i, el) { 147 | var hasFormat = $(el).find('.movie-card-format:contains("' + targetType + '")').length > 0; 148 | return hasFormat; 149 | }) 150 | .filter(function (_i, el) { 151 | var metaText = $(el).find('.movie-card-meta').text(); 152 | var movieCardYear = parseInt(metaText); 153 | return !isNaN(movieCardYear) && Math.abs(movieCardYear - year) <= 1; 154 | }) 155 | .filter(function (_i, el) { 156 | var movieCardTitle = $(el).find('.movie-card-title') 157 | .text() 158 | .replace(/\[.*?]/g, '') 159 | .trim(); 160 | return levenshtein.get(movieCardTitle.toLowerCase(), name.toLowerCase()) < 5; 161 | }) 162 | .map(function (_i, el) { 163 | var href = $(el).attr('href'); 164 | if (href && href.indexOf('http') !== 0) { 165 | href = BASE_URL + (href.indexOf('/') === 0 ? '' : '/') + href; 166 | } 167 | return href; 168 | }) 169 | .get(); 170 | 171 | return matchingCards.length > 0 ? matchingCards[0] : null; 172 | }); 173 | } 174 | 175 | function resolveRedirectUrl(redirectUrl) { 176 | return fetchText(redirectUrl).then(function (redirectHtml) { 177 | if (!redirectHtml) return null; 178 | 179 | try { 180 | var redirectDataMatch = redirectHtml.match(/'o','(.*?)'/); 181 | if (!redirectDataMatch) return null; 182 | 183 | var step1 = atob(redirectDataMatch[1]); 184 | var step2 = atob(step1); 185 | var step3 = rot13Cipher(step2); 186 | var step4 = atob(step3); 187 | var redirectData = JSON.parse(step4); 188 | 189 | if (redirectData && redirectData.o) { 190 | return atob(redirectData.o); 191 | } 192 | } catch (e) { 193 | console.log('[4KHDHub] Error resolving redirect: ' + e.message); 194 | } 195 | return null; 196 | }); 197 | } 198 | 199 | function extractSourceResults($, el) { 200 | // This function returns a Promise resolving to { url, meta } or null 201 | var localHtml = $(el).html(); 202 | var sizeMatch = localHtml.match(/([\d.]+ ?[GM]B)/); 203 | var heightMatch = localHtml.match(/\d{3,}p/); 204 | 205 | var title = $(el).find('.file-title, .episode-file-title').text().trim(); 206 | 207 | if (!heightMatch) { 208 | heightMatch = title.match(/(\d{3,4})p/i); 209 | } 210 | 211 | var height = heightMatch ? parseInt(heightMatch[0]) : 0; 212 | if (height === 0 && (title.indexOf('4K') !== -1 || title.indexOf('4k') !== -1 || localHtml.indexOf('4K') !== -1 || localHtml.indexOf('4k') !== -1)) { 213 | height = 2160; 214 | } 215 | 216 | var meta = { 217 | bytes: sizeMatch ? bytes.parse(sizeMatch[1]) : 0, 218 | height: height, 219 | title: title 220 | }; 221 | 222 | var hubCloudLink = $(el).find('a') 223 | .filter(function (_i, a) { return $(a).text().indexOf('HubCloud') !== -1; }) 224 | .attr('href'); 225 | 226 | if (hubCloudLink) { 227 | return resolveRedirectUrl(hubCloudLink).then(function (resolved) { 228 | return { url: resolved, meta: meta }; 229 | }); 230 | } 231 | 232 | var hubDriveLink = $(el).find('a') 233 | .filter(function (_i, a) { return $(a).text().indexOf('HubDrive') !== -1; }) 234 | .attr('href'); 235 | 236 | if (hubDriveLink) { 237 | return resolveRedirectUrl(hubDriveLink).then(function (resolvedDrive) { 238 | if (resolvedDrive) { 239 | return fetchText(resolvedDrive).then(function (hubDriveHtml) { 240 | if (hubDriveHtml) { 241 | var $2 = cheerio.load(hubDriveHtml); 242 | var innerCloudLink = $2('a:contains("HubCloud")').attr('href'); 243 | if (innerCloudLink) { 244 | return { url: innerCloudLink, meta: meta }; 245 | } 246 | } 247 | return null; 248 | }); 249 | } 250 | return null; 251 | }); 252 | } 253 | 254 | return Promise.resolve(null); 255 | } 256 | 257 | function extractHubCloud(hubCloudUrl, baseMeta) { 258 | if (!hubCloudUrl) return Promise.resolve([]); 259 | 260 | // Referer: hubCloudUrl 261 | return fetchText(hubCloudUrl, { headers: { Referer: hubCloudUrl } }).then(function (redirectHtml) { 262 | if (!redirectHtml) return []; 263 | 264 | var redirectUrlMatch = redirectHtml.match(/var url ?= ?'(.*?)'/); 265 | if (!redirectUrlMatch) return []; 266 | 267 | var finalLinksUrl = redirectUrlMatch[1]; 268 | return fetchText(finalLinksUrl, { headers: { Referer: hubCloudUrl } }).then(function (linksHtml) { 269 | if (!linksHtml) return []; 270 | 271 | var $ = cheerio.load(linksHtml); 272 | var results = []; 273 | var sizeText = $('#size').text(); 274 | var titleText = $('title').text().trim(); 275 | 276 | var currentMeta = Object.assign({}, baseMeta, { 277 | bytes: bytes.parse(sizeText) || baseMeta.bytes, 278 | title: titleText || baseMeta.title 279 | }); 280 | 281 | $('a').each(function (_i, el) { 282 | var text = $(el).text(); 283 | var href = $(el).attr('href'); 284 | if (!href) return; 285 | 286 | if (text.indexOf('FSL') !== -1 || text.indexOf('Download File') !== -1) { 287 | results.push({ 288 | source: 'FSL', 289 | url: href, 290 | meta: currentMeta 291 | }); 292 | } 293 | else if (text.indexOf('PixelServer') !== -1) { 294 | var pixelUrl = href.replace('/u/', '/api/file/'); 295 | results.push({ 296 | source: 'PixelServer', 297 | url: pixelUrl, 298 | meta: currentMeta 299 | }); 300 | } 301 | }); 302 | return results; 303 | }); 304 | }); 305 | } 306 | 307 | function getStreams(tmdbId, type, season, episode) { 308 | return getTmdbDetails(tmdbId, type).then(function (tmdbDetails) { 309 | if (!tmdbDetails) return []; 310 | 311 | var title = tmdbDetails.title; 312 | var year = tmdbDetails.year; 313 | console.log('[4KHDHub] Search: ' + title + ' (' + year + ')'); 314 | 315 | var isSeries = type === 'series' || type === 'tv'; 316 | return fetchPageUrl(title, year, isSeries).then(function (pageUrl) { 317 | if (!pageUrl) { 318 | console.log('[4KHDHub] Page not found'); 319 | return []; 320 | } 321 | console.log('[4KHDHub] Found page: ' + pageUrl); 322 | 323 | return fetchText(pageUrl).then(function (html) { 324 | if (!html) return []; 325 | var $ = cheerio.load(html); 326 | 327 | var itemsToProcess = []; 328 | 329 | if (isSeries && season && episode) { 330 | // Find specific season and episode 331 | var seasonStr = 'S' + String(season).padStart(2, '0'); 332 | var episodeStr = 'Episode-' + String(episode).padStart(2, '0'); 333 | 334 | $('.episode-item').each(function (_i, el) { 335 | if ($('.episode-title', el).text().indexOf(seasonStr) !== -1) { 336 | var downloadItems = $('.episode-download-item', el) 337 | .filter(function (_j, item) { return $(item).text().indexOf(episodeStr) !== -1; }); 338 | 339 | downloadItems.each(function (_k, item) { 340 | itemsToProcess.push(item); 341 | }); 342 | } 343 | }); 344 | } else { 345 | // Movies 346 | $('.download-item').each(function (_i, el) { 347 | itemsToProcess.push(el); 348 | }); 349 | } 350 | 351 | console.log('[4KHDHub] Processing ' + itemsToProcess.length + ' items'); 352 | 353 | var streamPromises = itemsToProcess.map(function (item) { 354 | return extractSourceResults($, item) 355 | .then(function (sourceResult) { 356 | if (sourceResult && sourceResult.url) { 357 | console.log('[4KHDHub] Extracting from HubCloud: ' + sourceResult.url); 358 | return extractHubCloud(sourceResult.url, sourceResult.meta).then(function (extractedLinks) { 359 | return extractedLinks.map(function (link) { 360 | return { 361 | name: '4KHDHub - ' + link.source + (sourceResult.meta.height ? ' ' + sourceResult.meta.height + 'p' : ''), 362 | title: link.meta.title + '\n' + bytes.format(link.meta.bytes || 0), 363 | url: link.url, 364 | quality: sourceResult.meta.height ? sourceResult.meta.height + 'p' : undefined, 365 | behaviorHints: { 366 | bingeGroup: '4khdhub-' + link.source 367 | } 368 | }; 369 | }); 370 | }); 371 | } 372 | return []; 373 | }) 374 | .catch(function (err) { 375 | console.log('[4KHDHub] Item processing error: ' + err.message); 376 | return []; 377 | }); 378 | }); 379 | 380 | return Promise.all(streamPromises).then(function (results) { 381 | // Flatten results 382 | return results.reduce(function (acc, val) { return acc.concat(val); }, []); 383 | }); 384 | }); 385 | }); 386 | }); 387 | } 388 | 389 | // Export for React Native compatibility 390 | if (typeof module !== 'undefined' && module.exports) { 391 | module.exports = { getStreams }; 392 | } else { 393 | global.getStreams = getStreams; 394 | } 395 | -------------------------------------------------------------------------------- /providers/dahmermovies.js: -------------------------------------------------------------------------------- 1 | // Dahmer Movies Scraper for Nuvio Local Scrapers 2 | // React Native compatible version 3 | 4 | console.log('[DahmerMovies] Initializing Dahmer Movies scraper'); 5 | 6 | // Constants 7 | const TMDB_API_KEY = "439c478a771f35c05022f9feabcca01c"; 8 | const DAHMER_MOVIES_API = 'https://a.111477.xyz'; 9 | const TIMEOUT = 60000; // 60 seconds 10 | 11 | // Quality mapping 12 | const Qualities = { 13 | Unknown: 0, 14 | P144: 144, 15 | P240: 240, 16 | P360: 360, 17 | P480: 480, 18 | P720: 720, 19 | P1080: 1080, 20 | P1440: 1440, 21 | P2160: 2160 22 | }; 23 | 24 | // Helper function to make HTTP requests 25 | function makeRequest(url, options = {}) { 26 | const requestOptions = { 27 | timeout: TIMEOUT, 28 | headers: { 29 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 30 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 31 | 'Accept-Language': 'en-US,en;q=0.5', 32 | 'Connection': 'keep-alive', 33 | ...options.headers 34 | }, 35 | ...options 36 | }; 37 | 38 | return fetch(url, requestOptions).then(function(response) { 39 | if (!response.ok) { 40 | throw new Error(`HTTP ${response.status}: ${response.statusText}`); 41 | } 42 | return response; 43 | }); 44 | } 45 | 46 | // Utility functions 47 | function getEpisodeSlug(season = null, episode = null) { 48 | if (season === null && episode === null) { 49 | return ['', '']; 50 | } 51 | const seasonSlug = season < 10 ? `0${season}` : `${season}`; 52 | const episodeSlug = episode < 10 ? `0${episode}` : `${episode}`; 53 | return [seasonSlug, episodeSlug]; 54 | } 55 | 56 | function getIndexQuality(str) { 57 | if (!str) return Qualities.Unknown; 58 | const match = str.match(/(\d{3,4})[pP]/); 59 | return match ? parseInt(match[1]) : Qualities.Unknown; 60 | } 61 | 62 | // Extract quality with codec information 63 | function getQualityWithCodecs(str) { 64 | if (!str) return 'Unknown'; 65 | 66 | // Extract base quality (resolution) 67 | const qualityMatch = str.match(/(\d{3,4})[pP]/); 68 | const baseQuality = qualityMatch ? `${qualityMatch[1]}p` : 'Unknown'; 69 | 70 | // Extract codec information (excluding HEVC and bit depth) 71 | const codecs = []; 72 | const lowerStr = str.toLowerCase(); 73 | 74 | // HDR formats 75 | if (lowerStr.includes('dv') || lowerStr.includes('dolby vision')) codecs.push('DV'); 76 | if (lowerStr.includes('hdr10+')) codecs.push('HDR10+'); 77 | else if (lowerStr.includes('hdr10') || lowerStr.includes('hdr')) codecs.push('HDR'); 78 | 79 | // Special formats 80 | if (lowerStr.includes('remux')) codecs.push('REMUX'); 81 | if (lowerStr.includes('imax')) codecs.push('IMAX'); 82 | 83 | // Combine quality with codecs using pipeline separator 84 | if (codecs.length > 0) { 85 | return `${baseQuality} | ${codecs.join(' | ')}`; 86 | } 87 | 88 | return baseQuality; 89 | } 90 | 91 | function getIndexQualityTags(str, fullTag = false) { 92 | if (!str) return ''; 93 | 94 | if (fullTag) { 95 | const match = str.match(/(.*)\.(?:mkv|mp4|avi)/i); 96 | return match ? match[1].trim() : str; 97 | } else { 98 | const match = str.match(/\d{3,4}[pP]\.?(.*?)\.(mkv|mp4|avi)/i); 99 | return match ? match[1].replace(/\./g, ' ').trim() : str; 100 | } 101 | } 102 | 103 | function encodeUrl(url) { 104 | try { 105 | return encodeURI(url); 106 | } catch (e) { 107 | return url; 108 | } 109 | } 110 | 111 | function decode(input) { 112 | try { 113 | return decodeURIComponent(input); 114 | } catch (e) { 115 | return input; 116 | } 117 | } 118 | 119 | // Format file size from bytes to human readable format 120 | function formatFileSize(sizeText) { 121 | if (!sizeText) return null; 122 | 123 | // If it's already formatted (contains GB, MB, etc.), return as is 124 | if (/\d+(\.\d+)?\s*(GB|MB|KB|TB)/i.test(sizeText)) { 125 | return sizeText; 126 | } 127 | 128 | // If it's a number (bytes), convert to human readable 129 | const bytes = parseInt(sizeText); 130 | if (isNaN(bytes)) return sizeText; 131 | 132 | const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; 133 | if (bytes === 0) return '0 Bytes'; 134 | 135 | const i = Math.floor(Math.log(bytes) / Math.log(1024)); 136 | const size = (bytes / Math.pow(1024, i)).toFixed(2); 137 | 138 | return `${size} ${sizes[i]}`; 139 | } 140 | 141 | // Parse HTML using basic string manipulation (React Native compatible) 142 | function parseLinks(html) { 143 | const links = []; 144 | 145 | // Parse table rows to get both links and file sizes 146 | const rowRegex = /]*>(.*?)<\/tr>/gis; 147 | let rowMatch; 148 | 149 | while ((rowMatch = rowRegex.exec(html)) !== null) { 150 | const rowContent = rowMatch[1]; 151 | 152 | // Extract link from the row 153 | const linkMatch = rowContent.match(/]*href=["']([^"']*)["'][^>]*>([^<]*)<\/a>/i); 154 | if (!linkMatch) continue; 155 | 156 | const href = linkMatch[1]; 157 | const text = linkMatch[2].trim(); 158 | 159 | // Skip parent directory and empty links 160 | if (!text || href === '../' || text === '../') continue; 161 | 162 | // Extract file size from the same row - try multiple patterns 163 | let size = null; 164 | 165 | // Pattern 1: DahmerMovies specific - data-sort attribute with byte size 166 | const sizeMatch1 = rowContent.match(/]*data-sort=["']?(\d+)["']?[^>]*>(\d+)<\/td>/i); 167 | if (sizeMatch1) { 168 | size = sizeMatch1[2]; // Use the displayed number (already in bytes) 169 | } 170 | 171 | // Pattern 2: Standard Apache directory listing with filesize class 172 | if (!size) { 173 | const sizeMatch2 = rowContent.match(/]*class=["']filesize["'][^>]*[^>]*>([^<]+)<\/td>/i); 174 | if (sizeMatch2) { 175 | size = sizeMatch2[1].trim(); 176 | } 177 | } 178 | 179 | // Pattern 3: Look for size in any td element after the link (formatted sizes) 180 | if (!size) { 181 | const sizeMatch3 = rowContent.match(/<\/a><\/td>\s*]*>([^<]+(?:GB|MB|KB|B|\d+\s*(?:GB|MB|KB|B)))<\/td>/i); 182 | if (sizeMatch3) { 183 | size = sizeMatch3[1].trim(); 184 | } 185 | } 186 | 187 | // Pattern 4: Look for size anywhere in the row (more permissive) 188 | if (!size) { 189 | const sizeMatch4 = rowContent.match(/(\d+(?:\.\d+)?\s*(?:GB|MB|KB|B|bytes?))/i); 190 | if (sizeMatch4) { 191 | size = sizeMatch4[1].trim(); 192 | } 193 | } 194 | 195 | links.push({ text, href, size }); 196 | } 197 | 198 | // Fallback to simple link parsing if table parsing fails 199 | if (links.length === 0) { 200 | const linkRegex = /]*href=["']([^"']*)["'][^>]*>([^<]*)<\/a>/gi; 201 | let match; 202 | 203 | while ((match = linkRegex.exec(html)) !== null) { 204 | const href = match[1]; 205 | const text = match[2].trim(); 206 | if (text && href && href !== '../' && text !== '../') { 207 | links.push({ text, href, size: null }); 208 | } 209 | } 210 | } 211 | 212 | return links; 213 | } 214 | 215 | // Main Dahmer Movies fetcher function 216 | function invokeDahmerMovies(title, year, season = null, episode = null) { 217 | console.log(`[DahmerMovies] Searching for: ${title} (${year})${season ? ` Season ${season}` : ''}${episode ? ` Episode ${episode}` : ''}`); 218 | 219 | // Construct URL based on content type (with proper encoding) 220 | const encodedUrl = season === null 221 | ? `${DAHMER_MOVIES_API}/movies/${encodeURIComponent(title.replace(/:/g, '') + ' (' + year + ')')}/` 222 | : `${DAHMER_MOVIES_API}/tvs/${encodeURIComponent(title.replace(/:/g, ' -'))}/Season ${season}/`; 223 | 224 | console.log(`[DahmerMovies] Fetching from: ${encodedUrl}`); 225 | 226 | return makeRequest(encodedUrl).then(function(response) { 227 | return response.text(); 228 | }).then(function(html) { 229 | console.log(`[DahmerMovies] Response length: ${html.length}`); 230 | 231 | // Parse HTML to extract links 232 | const paths = parseLinks(html); 233 | console.log(`[DahmerMovies] Found ${paths.length} total links`); 234 | 235 | // Filter based on content type 236 | let filteredPaths; 237 | if (season === null) { 238 | // For movies, filter by quality (1080p or 2160p) 239 | filteredPaths = paths.filter(path => 240 | /(1080p|2160p)/i.test(path.text) 241 | ); 242 | console.log(`[DahmerMovies] Filtered to ${filteredPaths.length} movie links (1080p/2160p only)`); 243 | } else { 244 | // For TV shows, filter by season and episode 245 | const [seasonSlug, episodeSlug] = getEpisodeSlug(season, episode); 246 | const episodePattern = new RegExp(`S${seasonSlug}E${episodeSlug}`, 'i'); 247 | filteredPaths = paths.filter(path => 248 | episodePattern.test(path.text) 249 | ); 250 | console.log(`[DahmerMovies] Filtered to ${filteredPaths.length} TV episode links (S${seasonSlug}E${episodeSlug})`); 251 | } 252 | 253 | if (filteredPaths.length === 0) { 254 | console.log('[DahmerMovies] No matching content found'); 255 | return []; 256 | } 257 | 258 | // Process and return results 259 | const results = filteredPaths.map(path => { 260 | const quality = getIndexQuality(path.text); 261 | const qualityWithCodecs = getQualityWithCodecs(path.text); 262 | const tags = getIndexQualityTags(path.text); 263 | 264 | // Construct proper URL - handle relative paths correctly 265 | let fullUrl; 266 | if (path.href.startsWith('http')) { 267 | // Already a full URL - need to encode it properly 268 | try { 269 | // Parse the URL and let the URL constructor handle encoding 270 | const url = new URL(path.href); 271 | // Reconstruct the URL with properly encoded pathname 272 | fullUrl = `${url.protocol}//${url.host}${url.pathname}`; 273 | } catch (error) { 274 | // Fallback: manually encode if URL parsing fails 275 | console.log(`[DahmerMovies] URL parsing failed, manually encoding: ${path.href}`); 276 | fullUrl = path.href.replace(/ /g, '%20'); 277 | } 278 | } else { 279 | // Relative path - combine with encoded base URL 280 | const baseUrl = encodedUrl.endsWith('/') ? encodedUrl : encodedUrl + '/'; 281 | const relativePath = path.href.startsWith('/') ? path.href.substring(1) : path.href; 282 | const encodedFilename = encodeURIComponent(relativePath); 283 | fullUrl = baseUrl + encodedFilename; 284 | } 285 | 286 | return { 287 | name: "DahmerMovies", 288 | title: `DahmerMovies ${tags || path.text}`, 289 | url: fullUrl, 290 | quality: qualityWithCodecs, // Use enhanced quality with codecs 291 | size: formatFileSize(path.size), // Format file size 292 | headers: {}, // No special headers needed for direct downloads 293 | provider: "dahmermovies", // Provider identifier 294 | filename: path.text 295 | }; 296 | }); 297 | 298 | // Sort by quality (highest first) 299 | results.sort((a, b) => { 300 | const qualityA = getIndexQuality(a.filename); 301 | const qualityB = getIndexQuality(b.filename); 302 | return qualityB - qualityA; 303 | }); 304 | 305 | console.log(`[DahmerMovies] Successfully processed ${results.length} streams`); 306 | return results; 307 | 308 | }).catch(function(error) { 309 | if (error.name === 'AbortError') { 310 | console.log('[DahmerMovies] Request timeout - server took too long to respond'); 311 | } else { 312 | console.log(`[DahmerMovies] Error: ${error.message}`); 313 | } 314 | return []; 315 | }); 316 | } 317 | 318 | // Main function to get streams for TMDB content 319 | function getStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) { 320 | console.log(`[DahmerMovies] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}${seasonNum ? `, S${seasonNum}E${episodeNum}` : ''}`); 321 | 322 | // Get TMDB info 323 | const tmdbUrl = `https://api.themoviedb.org/3/${mediaType === 'tv' ? 'tv' : 'movie'}/${tmdbId}?api_key=${TMDB_API_KEY}`; 324 | return makeRequest(tmdbUrl).then(function(tmdbResponse) { 325 | return tmdbResponse.json(); 326 | }).then(function(tmdbData) { 327 | const title = mediaType === 'tv' ? tmdbData.name : tmdbData.title; 328 | const year = mediaType === 'tv' ? tmdbData.first_air_date?.substring(0, 4) : tmdbData.release_date?.substring(0, 4); 329 | 330 | if (!title) { 331 | throw new Error('Could not extract title from TMDB response'); 332 | } 333 | 334 | console.log(`[DahmerMovies] TMDB Info: "${title}" (${year})`); 335 | 336 | // Call the main scraper function 337 | return invokeDahmerMovies( 338 | title, 339 | year ? parseInt(year) : null, 340 | seasonNum, 341 | episodeNum 342 | ); 343 | 344 | }).catch(function(error) { 345 | console.error(`[DahmerMovies] Error in getStreams: ${error.message}`); 346 | return []; 347 | }); 348 | } 349 | 350 | // Export the main function 351 | if (typeof module !== 'undefined' && module.exports) { 352 | module.exports = { getStreams }; 353 | } else { 354 | // For React Native environment 355 | global.getStreams = getStreams; 356 | } -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Tapframe's Repo", 3 | "version": "1.0.0", 4 | "scrapers": [ 5 | { 6 | "id": "4khdhub", 7 | "name": "4KHDHub", 8 | "description": "4KHDHub direct links", 9 | "version": "1.0.2", 10 | "author": "Nuvio Team", 11 | "supportedTypes": [ 12 | "movie", 13 | "tv" 14 | ], 15 | "filename": "providers/4khdhub.js", 16 | "enabled": true, 17 | "formats": [ 18 | "mkv" 19 | ], 20 | "logo": "https://4khdhub.fans/favicon.ico", 21 | "contentLanguage": [ 22 | "en" 23 | ] 24 | }, 25 | { 26 | "id": "animekai", 27 | "name": "AnimeKai", 28 | "description": "AnimeKai TV episodes via AnimeKai servers", 29 | "version": "1.1.0", 30 | "author": "Nuvio Team", 31 | "supportedTypes": [ 32 | "tv" 33 | ], 34 | "filename": "providers/animekai.js", 35 | "enabled": true, 36 | "limited": true, 37 | "logo": "https://animekai.to/assets/uploads/37585a39fe8c8d8fafaa2c7bfbf5374ecac859ea6a0288a6da2c61f5.png", 38 | "contentLanguage": [ 39 | "en" 40 | ] 41 | }, 42 | { 43 | "id": "uhdmovies", 44 | "name": "UHDMovies", 45 | "description": "UHD Movies streaming with multiple resolution", 46 | "version": "1.1.0", 47 | "author": "Nuvio Team", 48 | "supportedTypes": [ 49 | "movie", 50 | "tv" 51 | ], 52 | "filename": "providers/uhdmovies.js", 53 | "enabled": true, 54 | "formats": [ 55 | "mkv" 56 | ], 57 | "logo": "https://uhdmovies.rip/wp-content/uploads/2021/03/cropped-output-onlinepngtools-1-32x32.png", 58 | "contentLanguage": [ 59 | "en" 60 | ] 61 | }, 62 | { 63 | "id": "moviesmod", 64 | "name": "MoviesMod", 65 | "description": "Movie and TV show streams from MoviesMod with multiple quality options", 66 | "version": "1.0.1", 67 | "author": "Nuvio Team", 68 | "supportedTypes": [ 69 | "movie", 70 | "tv" 71 | ], 72 | "filename": "providers/moviesmod.js", 73 | "enabled": true, 74 | "formats": [ 75 | "mkv" 76 | ], 77 | "logo": "https://moviesmod.plus/wp-content/uploads/2022/10/moviesmod.png", 78 | "contentLanguage": [ 79 | "en" 80 | ] 81 | }, 82 | { 83 | "id": "hdrezka", 84 | "name": "HDRezka", 85 | "description": "HDRezka streaming platform with multi-language support", 86 | "version": "1.0.0", 87 | "author": "Nuvio Team", 88 | "supportedTypes": [ 89 | "movie", 90 | "tv" 91 | ], 92 | "filename": "providers/hdrezka.js", 93 | "enabled": true, 94 | "logo": "https://hdrezka.ag/templates/hdrezka/images/favicon.ico", 95 | "contentLanguage": [ 96 | "ru", 97 | "en" 98 | ] 99 | }, 100 | { 101 | "id": "dahmermovies", 102 | "name": "DahmerMovies", 103 | "description": "Dahmer Movies streaming service. Can be slow at times to fetch and stream. Only use if needed.", 104 | "version": "1.0.1", 105 | "author": "Nuvio Team", 106 | "supportedTypes": [ 107 | "movie", 108 | "tv" 109 | ], 110 | "filename": "providers/dahmermovies.js", 111 | "enabled": true, 112 | "formats": [ 113 | "mkv" 114 | ], 115 | "logo": "https://image.similarpng.com/file/similarpng/very-thumbnail/2021/05/Letter-D-logo-design-template-with-geometric-shape-style-on-transparent-background-PNG.png", 116 | "contentLanguage": [ 117 | "en" 118 | ] 119 | }, 120 | { 121 | "id": "watch32", 122 | "name": "Watch32", 123 | "description": "Lightweight M3U8 Links", 124 | "version": "1.1.0", 125 | "author": "Nuvio Team", 126 | "supportedTypes": [ 127 | "movie", 128 | "tv" 129 | ], 130 | "filename": "providers/watch32.js", 131 | "enabled": false, 132 | "logo": "https://img.watch32.sx/xxrz/400x400/100/a9/5e/a95e15a880a9df3c045f6a5224daf576/a95e15a880a9df3c045f6a5224daf576.png", 133 | "contentLanguage": [ 134 | "en" 135 | ] 136 | }, 137 | { 138 | "id": "netmirror", 139 | "name": "NetMirror", 140 | "description": "Streaming links from netmirror. Automatically disabled on iOS due to compatibility issues.", 141 | "version": "1.0.2", 142 | "author": "Nuvio Team", 143 | "supportedTypes": [ 144 | "movie", 145 | "tv" 146 | ], 147 | "filename": "providers/netmirror.js", 148 | "enabled": true, 149 | "logo": "https://net2025.cc/favicon.ico", 150 | "contentLanguage": [ 151 | "en" 152 | ], 153 | "disabledPlatforms": [ 154 | "ios" 155 | ], 156 | "supportsExternalPlayer": false 157 | }, 158 | { 159 | "id": "vidsrc", 160 | "name": "VidSrc", 161 | "description": "VidSrc M3U8 streaming with multiple server support", 162 | "version": "1.0.0", 163 | "author": "Nuvio Team", 164 | "supportedTypes": [ 165 | "movie", 166 | "tv" 167 | ], 168 | "filename": "providers/vidsrc.js", 169 | "enabled": false, 170 | "logo": "https://vidsrc.xyz/template/vidsrc-ico.png", 171 | "contentLanguage": [ 172 | "en" 173 | ] 174 | }, 175 | { 176 | "id": "streamflix", 177 | "name": "StreamFlix", 178 | "description": "Direct links via StreamFlix API", 179 | "version": "1.0.0", 180 | "author": "Nuvio Team", 181 | "supportedTypes": [ 182 | "movie", 183 | "tv" 184 | ], 185 | "filename": "providers/streamflix.js", 186 | "enabled": true, 187 | "formats": [ 188 | "mkv" 189 | ], 190 | "logo": "https://streamflix.app/assets/images/logo-text-148x72.png", 191 | "contentLanguage": [ 192 | "en", 193 | "hin" 194 | ] 195 | }, 196 | { 197 | "id": "moviebox", 198 | "name": "MovieBox", 199 | "description": "MovieBox streaming with multiple language support", 200 | "version": "1.0.1", 201 | "author": "Nuvio Team", 202 | "supportedTypes": [ 203 | "movie", 204 | "tv" 205 | ], 206 | "filename": "providers/moviebox.js", 207 | "enabled": false, 208 | "formats": [ 209 | "mp4" 210 | ], 211 | "logo": "https://h5-static.aoneroom.com/oneroomStatic/public/_nuxt/web-logo.apJjVir2.svg", 212 | "contentLanguage": [ 213 | "en", 214 | "hin", 215 | "tam", 216 | "tel" 217 | ] 218 | }, 219 | { 220 | "id": "vixsrc", 221 | "name": "Vixsrc", 222 | "description": "Vixsrc M3U8 streaming with auto quality selection", 223 | "version": "1.0.0", 224 | "author": "Nuvio Team", 225 | "supportedTypes": [ 226 | "movie", 227 | "tv" 228 | ], 229 | "filename": "providers/vixsrc.js", 230 | "enabled": true, 231 | "logo": "https://vixsrc.to/favicon.ico", 232 | "contentLanguage": [ 233 | "en" 234 | ] 235 | }, 236 | { 237 | "id": "dvdplay", 238 | "name": "DVDPlay", 239 | "description": "DVDPlay direct download links with HubCloud extraction", 240 | "version": "1.0.1", 241 | "author": "Nuvio Team", 242 | "supportedTypes": [ 243 | "movie", 244 | "tv" 245 | ], 246 | "filename": "providers/dvdplay.js", 247 | "enabled": true, 248 | "formats": [ 249 | "mkv" 250 | ], 251 | "logo": "https://dvdplay.rodeo/dvdplay-icon.png", 252 | "contentLanguage": [ 253 | "mal", 254 | "tam", 255 | "hin" 256 | ] 257 | }, 258 | { 259 | "id": "yflix", 260 | "name": "YFlix", 261 | "description": "YFlix streaming", 262 | "version": "1.1.2", 263 | "author": "Nuvio Team", 264 | "supportedTypes": [ 265 | "movie", 266 | "tv" 267 | ], 268 | "filename": "providers/yflix.js", 269 | "enabled": true, 270 | "limited": true, 271 | "formats": [ 272 | "m3u8", 273 | "mp4" 274 | ], 275 | "logo": "https://yflix.to/assets/uploads/2f505f3de3c99889c1a72557f3e3714fc0c457b0.png", 276 | "contentLanguage": [ 277 | "en" 278 | ] 279 | }, 280 | { 281 | "id": "videasy", 282 | "name": "VIDEASY", 283 | "description": "VideoEasy streaming with multiple server support and languages", 284 | "version": "1.0.0", 285 | "author": "Nuvio Team", 286 | "supportedTypes": [ 287 | "movie", 288 | "tv" 289 | ], 290 | "filename": "providers/videasy.js", 291 | "enabled": true, 292 | "limited": true, 293 | "formats": [ 294 | "m3u8", 295 | "mp4", 296 | "mkv" 297 | ], 298 | "logo": "https://www.videasy.net/logo.png", 299 | "contentLanguage": [ 300 | "en", 301 | "de", 302 | "it", 303 | "fr", 304 | "hi", 305 | "es", 306 | "pt" 307 | ] 308 | }, 309 | { 310 | "id": "mallumv", 311 | "name": "MalluMV", 312 | "description": "MalluMV direct links with support for Tamil, Telugu, Hindi, Malayalam, and Kannada movies", 313 | "version": "1.0.0", 314 | "author": "Nuvio Team", 315 | "supportedTypes": [ 316 | "movie" 317 | ], 318 | "filename": "providers/mallumv.js", 319 | "enabled": true, 320 | "formats": [ 321 | "mkv", 322 | "mp4" 323 | ], 324 | "logo": "https://mallumv.fit/favicon.ico", 325 | "contentLanguage": [ 326 | "ta", 327 | "te", 328 | "hi", 329 | "ml", 330 | "kn" 331 | ] 332 | }, 333 | { 334 | "id": "vidlink", 335 | "name": "Vidlink", 336 | "description": "Vidlink streaming with encrypted TMDB ID support for movies and TV shows", 337 | "version": "1.0.0", 338 | "author": "Nuvio Team", 339 | "supportedTypes": [ 340 | "movie", 341 | "tv" 342 | ], 343 | "filename": "providers/vidlink.js", 344 | "enabled": true, 345 | "formats": [ 346 | "m3u8", 347 | "mp4" 348 | ], 349 | "logo": "https://vidlink.pro/favicon.ico", 350 | "contentLanguage": [ 351 | "en" 352 | ] 353 | }, 354 | { 355 | "id": "vidnest", 356 | "name": "Vidnest", 357 | "description": "Vidnest streaming with encrypted AES-GCM sources", 358 | "version": "1.0.0", 359 | "author": "Nuvio Team", 360 | "supportedTypes": [ 361 | "movie", 362 | "tv" 363 | ], 364 | "filename": "providers/vidnest.js", 365 | "enabled": true, 366 | "formats": [ 367 | "mp4", 368 | "m3u8" 369 | ], 370 | "logo": "https://vidnest.fun/favicon.ico", 371 | "contentLanguage": [ 372 | "en" 373 | ] 374 | }, 375 | { 376 | "id": "vidnest-anime", 377 | "name": "VidnestAnime", 378 | "description": "Vidnest anime streaming with TMDB→AniList mapping and 5 servers (hindi, satoru, miko, pahe, anya)", 379 | "version": "1.0.0", 380 | "author": "Nuvio Team", 381 | "supportedTypes": [ 382 | "movie", 383 | "tv" 384 | ], 385 | "filename": "providers/vidnest-anime.js", 386 | "enabled": true, 387 | "formats": [ 388 | "m3u8" 389 | ], 390 | "logo": "https://vidnest.fun/favicon.ico", 391 | "contentLanguage": [ 392 | "en", 393 | "hi", 394 | "ja" 395 | ], 396 | "limited": true, 397 | "notes": "Automatically maps TMDB IDs to AniList. Supports sub/dub." 398 | }, 399 | { 400 | "id": "vidrock", 401 | "name": "Vidrock", 402 | "description": "Vidrock streaming with AES-CBC encrypted sources", 403 | "version": "1.0.0", 404 | "author": "Nuvio Team", 405 | "supportedTypes": [ 406 | "movie", 407 | "tv" 408 | ], 409 | "filename": "providers/vidrock.js", 410 | "enabled": true, 411 | "formats": [ 412 | "mp4", 413 | "m3u8" 414 | ], 415 | "logo": "https://vidrock.net/Rock.png", 416 | "contentLanguage": [ 417 | "en" 418 | ] 419 | }, 420 | { 421 | "id": "mapple", 422 | "name": "Mapple", 423 | "description": "Mapple streaming with multiple sources (Mapple 4K, Sakura, Pinecone, Oak, Willow)", 424 | "version": "1.0.0", 425 | "author": "Nuvio Team", 426 | "supportedTypes": [ 427 | "movie", 428 | "tv" 429 | ], 430 | "filename": "providers/mapple.js", 431 | "enabled": false, 432 | "formats": [ 433 | "mp4", 434 | "m3u8" 435 | ], 436 | "logo": "https://mapple.uk/favicon.ico", 437 | "contentLanguage": [ 438 | "en" 439 | ] 440 | }, 441 | { 442 | "id": "cinevibe", 443 | "name": "Cinevibe", 444 | "description": "Cinevibe (Movies)", 445 | "version": "1.0.0", 446 | "author": "Nuvio Team", 447 | "supportedTypes": [ 448 | "movie" 449 | ], 450 | "filename": "providers/cinevibe.js", 451 | "enabled": true, 452 | "formats": [ 453 | "mp4", 454 | "m3u8" 455 | ], 456 | "logo": "https://cinevibe.asia/assets/favicon-32x32.png", 457 | "contentLanguage": [ 458 | "en" 459 | ] 460 | }, 461 | { 462 | "id": "castle", 463 | "name": "Castle", 464 | "description": "Multi Lang provider", 465 | "version": "1.0.0", 466 | "author": "Nuvio Team", 467 | "supportedTypes": [ 468 | "movie", 469 | "tv" 470 | ], 471 | "filename": "providers/castle.js", 472 | "enabled": true, 473 | "formats": [ 474 | "mp4", 475 | "m3u8" 476 | ], 477 | "logo": "https://static.hbayy.com/simple-blog-13/prod/_nuxt/big-logo.DESZ4mBj.png", 478 | "contentLanguage": [ 479 | "en", 480 | "hi", 481 | "ta", 482 | "te", 483 | "ml", 484 | "kn" 485 | ] 486 | }, 487 | { 488 | "id": "showbox", 489 | "name": "ShowBox", 490 | "description": "ShowBox streaming with multiple quality options and versions", 491 | "version": "1.0.0", 492 | "author": "Nuvio Team", 493 | "supportedTypes": [ 494 | "movie", 495 | "tv" 496 | ], 497 | "filename": "providers/showbox.js", 498 | "enabled": true, 499 | "formats": [ 500 | "mp4", 501 | "mkv" 502 | ], 503 | "logo": "https://raw.githubusercontent.com/tapframe/nuvio-providers/main/Assets/Logo-2.png", 504 | "contentLanguage": [ 505 | "en" 506 | ] 507 | } 508 | ] 509 | } -------------------------------------------------------------------------------- /providers/yflix.js: -------------------------------------------------------------------------------- 1 | // YFlix Scraper for Nuvio Local Scrapers 2 | // React Native compatible version - Uses enc-dec.app database for accurate matching 3 | 4 | // Headers for requests 5 | const HEADERS = { 6 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36', 7 | 'Connection': 'keep-alive' 8 | }; 9 | 10 | const API = 'https://enc-dec.app/api'; 11 | const DB_API = 'https://enc-dec.app/db/flix'; 12 | const YFLIX_AJAX = 'https://yflix.to/ajax'; 13 | 14 | // Debug helpers 15 | function createRequestId() { 16 | try { 17 | const rand = Math.random().toString(36).slice(2, 8); 18 | const ts = Date.now().toString(36).slice(-6); 19 | return `${rand}${ts}`; 20 | } catch (e) { 21 | return String(Date.now()); 22 | } 23 | } 24 | 25 | function logRid(rid, msg, extra) { 26 | try { 27 | if (extra !== undefined) { 28 | console.log(`[YFlix][rid:${rid}] ${msg}`, extra); 29 | } else { 30 | console.log(`[YFlix][rid:${rid}] ${msg}`); 31 | } 32 | } catch (e) { 33 | // ignore logging errors 34 | } 35 | } 36 | 37 | // Helper functions for HTTP requests (React Native compatible) 38 | function getText(url) { 39 | return fetch(url, { headers: HEADERS }) 40 | .then(response => { 41 | if (!response.ok) { 42 | throw new Error(`HTTP ${response.status}: ${response.statusText}`); 43 | } 44 | return response.text(); 45 | }); 46 | } 47 | 48 | function getJson(url) { 49 | return fetch(url, { headers: HEADERS }) 50 | .then(response => { 51 | if (!response.ok) { 52 | throw new Error(`HTTP ${response.status}: ${response.statusText}`); 53 | } 54 | return response.json(); 55 | }); 56 | } 57 | 58 | function postJson(url, jsonBody, extraHeaders) { 59 | const body = JSON.stringify(jsonBody); 60 | const headers = Object.assign( 61 | {}, 62 | HEADERS, 63 | { 'Content-Type': 'application/json', 'Content-Length': body.length.toString() }, 64 | extraHeaders || {} 65 | ); 66 | 67 | return fetch(url, { 68 | method: 'POST', 69 | headers, 70 | body 71 | }).then(response => { 72 | if (!response.ok) { 73 | throw new Error(`HTTP ${response.status}: ${response.statusText}`); 74 | } 75 | return response.json(); 76 | }); 77 | } 78 | 79 | // Enc/Dec helpers 80 | function encrypt(text) { 81 | return getJson(`${API}/enc-movies-flix?text=${encodeURIComponent(text)}`).then(j => j.result); 82 | } 83 | 84 | function decrypt(text) { 85 | return postJson(`${API}/dec-movies-flix`, { text: text }).then(j => j.result); 86 | } 87 | 88 | function parseHtml(html) { 89 | return postJson(`${API}/parse-html`, { text: html }).then(j => j.result); 90 | } 91 | 92 | function decryptRapidMedia(embedUrl) { 93 | const media = embedUrl.replace('/e/', '/media/').replace('/e2/', '/media/'); 94 | return getJson(media) 95 | .then((mediaJson) => { 96 | const encrypted = mediaJson && mediaJson.result; 97 | if (!encrypted) throw new Error('No encrypted media result from RapidShare media endpoint'); 98 | return postJson(`${API}/dec-rapid`, { text: encrypted, agent: HEADERS['User-Agent'] }); 99 | }) 100 | .then(j => j.result); 101 | } 102 | 103 | // Database lookup - replaces title matching 104 | function findInDatabase(tmdbId, mediaType) { 105 | const type = mediaType === 'movie' ? 'movie' : 'tv'; 106 | const url = `${DB_API}/find?tmdb_id=${tmdbId}&type=${type}`; 107 | 108 | return getJson(url) 109 | .then(results => { 110 | if (!results || results.length === 0) { 111 | return null; 112 | } 113 | return results[0]; // Return first match 114 | }); 115 | } 116 | 117 | // HLS helpers (Promise-based) 118 | function parseQualityFromM3u8(m3u8Text, baseUrl = '') { 119 | const streams = []; 120 | const lines = m3u8Text.split(/\r?\n/); 121 | let currentInfo = null; 122 | 123 | for (let i = 0; i < lines.length; i++) { 124 | const line = lines[i].trim(); 125 | if (line.startsWith('#EXT-X-STREAM-INF')) { 126 | const bwMatch = line.match(/BANDWIDTH=\s*(\d+)/i); 127 | const resMatch = line.match(/RESOLUTION=\s*(\d+)x(\d+)/i); 128 | 129 | currentInfo = { 130 | bandwidth: bwMatch ? parseInt(bwMatch[1]) : null, 131 | width: resMatch ? parseInt(resMatch[1]) : null, 132 | height: resMatch ? parseInt(resMatch[2]) : null, 133 | quality: null 134 | }; 135 | 136 | if (currentInfo.height) { 137 | currentInfo.quality = `${currentInfo.height}p`; 138 | } else if (currentInfo.bandwidth) { 139 | const bps = currentInfo.bandwidth; 140 | if (bps >= 6_000_000) currentInfo.quality = '2160p'; 141 | else if (bps >= 4_000_000) currentInfo.quality = '1440p'; 142 | else if (bps >= 2_500_000) currentInfo.quality = '1080p'; 143 | else if (bps >= 1_500_000) currentInfo.quality = '720p'; 144 | else if (bps >= 800_000) currentInfo.quality = '480p'; 145 | else if (bps >= 400_000) currentInfo.quality = '360p'; 146 | else currentInfo.quality = '240p'; 147 | } 148 | } else if (line && !line.startsWith('#') && currentInfo) { 149 | let streamUrl = line; 150 | if (!streamUrl.startsWith('http') && baseUrl) { 151 | try { 152 | const url = new URL(streamUrl, baseUrl); 153 | streamUrl = url.href; 154 | } catch (e) { 155 | // Ignore URL parsing errors 156 | } 157 | } 158 | 159 | streams.push({ 160 | url: streamUrl, 161 | quality: currentInfo.quality || 'unknown', 162 | bandwidth: currentInfo.bandwidth, 163 | width: currentInfo.width, 164 | height: currentInfo.height, 165 | type: 'hls' 166 | }); 167 | 168 | currentInfo = null; 169 | } 170 | } 171 | 172 | return { 173 | isMaster: m3u8Text.includes('#EXT-X-STREAM-INF'), 174 | streams: streams.sort((a, b) => (b.height || 0) - (a.height || 0)) 175 | }; 176 | } 177 | 178 | function enhanceStreamsWithQuality(streams) { 179 | const enhancedStreams = []; 180 | 181 | const tasks = streams.map(s => { 182 | if (s && s.url && typeof s.url === 'string' && s.url.includes('.m3u8')) { 183 | return getText(s.url) 184 | .then(text => { 185 | const info = parseQualityFromM3u8(text, s.url); 186 | if (info.isMaster && info.streams.length > 0) { 187 | info.streams.forEach(qualityStream => { 188 | enhancedStreams.push({ 189 | ...s, 190 | ...qualityStream, 191 | masterUrl: s.url 192 | }); 193 | }); 194 | } else { 195 | enhancedStreams.push({ 196 | ...s, 197 | quality: s.quality || 'unknown' 198 | }); 199 | } 200 | }) 201 | .catch(() => { 202 | enhancedStreams.push({ 203 | ...s, 204 | quality: s.quality || 'Adaptive' 205 | }); 206 | }); 207 | } else { 208 | enhancedStreams.push(s); 209 | } 210 | return Promise.resolve(); 211 | }); 212 | 213 | return Promise.all(tasks).then(() => enhancedStreams); 214 | } 215 | 216 | function formatStreamsData(rapidResult) { 217 | const streams = []; 218 | const subtitles = []; 219 | const thumbnails = []; 220 | if (rapidResult && typeof rapidResult === 'object') { 221 | (rapidResult.sources || []).forEach(src => { 222 | const fileUrl = src && src.file; 223 | if (fileUrl) { 224 | streams.push({ 225 | url: fileUrl, 226 | quality: fileUrl.includes('.m3u8') ? 'Adaptive' : 'unknown', 227 | type: fileUrl.includes('.m3u8') ? 'hls' : 'file', 228 | provider: 'rapidshare', 229 | }); 230 | } 231 | }); 232 | (rapidResult.tracks || []).forEach(tr => { 233 | if (tr && tr.kind === 'thumbnails' && tr.file) { 234 | thumbnails.push({ url: tr.file, type: 'vtt' }); 235 | } else if (tr && (tr.kind === 'captions' || tr.kind === 'subtitles') && tr.file) { 236 | subtitles.push({ url: tr.file, language: tr.label || '', default: !!tr.default }); 237 | } 238 | }); 239 | } 240 | return { streams, subtitles, thumbnails, totalStreams: streams.length }; 241 | } 242 | 243 | function runStreamFetch(eid, title, year, mediaType, seasonNum, episodeNum, rid) { 244 | logRid(rid, `runStreamFetch: start eid=${eid}`); 245 | 246 | return encrypt(eid) 247 | .then(encEid => { 248 | logRid(rid, 'links/list: enc(eid) ready'); 249 | return getJson(`${YFLIX_AJAX}/links/list?eid=${eid}&_=${encEid}`); 250 | }) 251 | .then(serversResp => parseHtml(serversResp.result)) 252 | .then(servers => { 253 | const serverTypes = Object.keys(servers || {}); 254 | const byTypeCounts = serverTypes.map(stype => ({ type: stype, count: Object.keys(servers[stype] || {}).length })); 255 | logRid(rid, 'servers available', byTypeCounts); 256 | 257 | const allStreams = []; 258 | const allSubtitles = []; 259 | const allThumbnails = []; 260 | 261 | const serverPromises = []; 262 | const lids = []; 263 | Object.keys(servers).forEach(serverType => { 264 | Object.keys(servers[serverType]).forEach(serverKey => { 265 | const lid = servers[serverType][serverKey].lid; 266 | lids.push(lid); 267 | const p = encrypt(lid) 268 | .then(encLid => { 269 | logRid(rid, `links/view: enc(lid) ready`, { serverType, serverKey, lid }); 270 | return getJson(`${YFLIX_AJAX}/links/view?id=${lid}&_=${encLid}`); 271 | }) 272 | .then(embedResp => { 273 | logRid(rid, `decrypt(embed)`, { serverType, serverKey, lid }); 274 | return decrypt(embedResp.result); 275 | }) 276 | .then(decrypted => { 277 | if (decrypted && typeof decrypted === 'object' && decrypted.url && decrypted.url.includes('rapidshare.cc')) { 278 | logRid(rid, `rapid.media → dec-rapid`, { lid }); 279 | return decryptRapidMedia(decrypted.url) 280 | .then(rapidData => formatStreamsData(rapidData)) 281 | .then(formatted => enhanceStreamsWithQuality(formatted.streams) 282 | .then(enhanced => { 283 | enhanced.forEach(s => { 284 | s.serverType = serverType; 285 | s.serverKey = serverKey; 286 | s.serverLid = lid; 287 | allStreams.push(s); 288 | }); 289 | allSubtitles.push(...formatted.subtitles); 290 | allThumbnails.push(...formatted.thumbnails); 291 | }) 292 | ); 293 | } 294 | return null; 295 | }) 296 | .catch(() => null); 297 | serverPromises.push(p); 298 | }); 299 | }); 300 | const uniqueLids = Array.from(new Set(lids)); 301 | logRid(rid, `fan-out: lids`, { total: lids.length, unique: uniqueLids.length }); 302 | 303 | return Promise.all(serverPromises).then(() => { 304 | // Deduplicate streams by URL 305 | const seen = new Set(); 306 | let dedupedStreams = allStreams.filter(s => { 307 | if (!s || !s.url) return false; 308 | if (seen.has(s.url)) return false; 309 | seen.add(s.url); 310 | return true; 311 | }); 312 | logRid(rid, `streams: deduped`, { count: dedupedStreams.length }); 313 | 314 | // Convert to Nuvio format 315 | const nuvioStreams = dedupedStreams.map(stream => ({ 316 | name: `YFlix ${stream.serverType || 'Server'} - ${stream.quality || 'Unknown'}`, 317 | title: `${title}${year ? ` (${year})` : ''}${mediaType === 'tv' && seasonNum && episodeNum ? ` S${seasonNum}E${episodeNum}` : ''}`, 318 | url: stream.url, 319 | quality: stream.quality || 'Unknown', 320 | size: 'Unknown', 321 | headers: HEADERS, 322 | provider: 'yflix' 323 | })); 324 | 325 | return nuvioStreams; 326 | }); 327 | }); 328 | } 329 | 330 | // Main getStreams function 331 | function getStreams(tmdbId, mediaType, seasonNum, episodeNum) { 332 | return new Promise((resolve, reject) => { 333 | const rid = createRequestId(); 334 | logRid(rid, `getStreams start tmdbId=${tmdbId} type=${mediaType} S=${seasonNum || ''} E=${episodeNum || ''}`); 335 | 336 | // Look up content in database by TMDB ID 337 | findInDatabase(tmdbId, mediaType) 338 | .then(dbResult => { 339 | if (!dbResult) { 340 | logRid(rid, 'no match found in database'); 341 | resolve([]); 342 | return; 343 | } 344 | 345 | const info = dbResult.info; 346 | const episodes = dbResult.episodes; 347 | 348 | logRid(rid, `database match found`, { 349 | title: info.title_en, 350 | year: info.year, 351 | flixId: info.flix_id, 352 | episodeCount: info.episode_count 353 | }); 354 | 355 | // Get the episode ID 356 | let eid = null; 357 | const selectedSeason = String(seasonNum || 1); 358 | const selectedEpisode = String(episodeNum || 1); 359 | 360 | if (episodes && episodes[selectedSeason] && episodes[selectedSeason][selectedEpisode]) { 361 | eid = episodes[selectedSeason][selectedEpisode].eid; 362 | logRid(rid, `found episode eid=${eid} for S${selectedSeason}E${selectedEpisode}`); 363 | } else { 364 | // Fallback: try to find any available episode 365 | const seasons = Object.keys(episodes || {}); 366 | if (seasons.length > 0) { 367 | const firstSeason = seasons[0]; 368 | const episodesInSeason = Object.keys(episodes[firstSeason] || {}); 369 | if (episodesInSeason.length > 0) { 370 | const firstEp = episodesInSeason[0]; 371 | eid = episodes[firstSeason][firstEp].eid; 372 | logRid(rid, `fallback: using S${firstSeason}E${firstEp}, eid=${eid}`); 373 | } 374 | } 375 | } 376 | 377 | if (!eid) { 378 | logRid(rid, 'no episode ID found'); 379 | resolve([]); 380 | return; 381 | } 382 | 383 | // Fetch streams using the episode ID 384 | return runStreamFetch(eid, info.title_en, info.year, mediaType, seasonNum, episodeNum, rid); 385 | }) 386 | .then(streams => { 387 | if (streams) { 388 | logRid(rid, `returning streams`, { count: streams.length }); 389 | resolve(streams); 390 | } else { 391 | resolve([]); 392 | } 393 | }) 394 | .catch(error => { 395 | logRid(rid, `ERROR ${error && error.message ? error.message : String(error)}`); 396 | resolve([]); // Return empty array on error, don't reject 397 | }); 398 | }); 399 | } 400 | 401 | // Export for React Native compatibility 402 | if (typeof module !== 'undefined' && module.exports) { 403 | module.exports = { getStreams }; 404 | } else { 405 | global.getStreams = getStreams; 406 | } 407 | -------------------------------------------------------------------------------- /providers/cinevibe.js: -------------------------------------------------------------------------------- 1 | // Cinevibe Scraper for Nuvio Local Scrapers 2 | // React Native compatible version - Promise-based approach 3 | 4 | // Constants 5 | const BASE_URL = 'https://cinevibe.asia'; 6 | const TMDB_API_KEY = '439c478a771f35c05022f9feabcca01c'; // Same key used by other providers 7 | const TMDB_BASE_URL = 'https://api.themoviedb.org/3'; 8 | 9 | const USER_AGENT = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36"; 10 | const BROWSER_FINGERPRINT = "eyJzY3JlZW4iOiIzNjB4ODA2eDI0Iiwi"; 11 | const SESSION_ENTROPY = "pjght152dw2rb.ssst4bzleDI0Iiwibv78"; 12 | 13 | // Working headers for Cinevibe requests 14 | const WORKING_HEADERS = { 15 | 'Referer': BASE_URL + '/', 16 | 'User-Agent': USER_AGENT, 17 | 'X-CV-Fingerprint': BROWSER_FINGERPRINT, 18 | 'X-CV-Session': SESSION_ENTROPY, 19 | 'X-Requested-With': 'XMLHttpRequest' 20 | }; 21 | 22 | // Utility Functions 23 | 24 | /** 25 | * A 32-bit FNV-1a Hash Function 26 | */ 27 | function fnv1a32(s) { 28 | let hash = 2166136261; 29 | for (let i = 0; i < s.length; i++) { 30 | hash ^= s.charCodeAt(i); 31 | hash = (hash + (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24)) & 0xffffffff; 32 | } 33 | return hash.toString(16).padStart(8, '0'); 34 | } 35 | 36 | /** 37 | * ROT13 encoding function 38 | */ 39 | function rot13(str) { 40 | return str.replace(/[A-Za-z]/g, function(char) { 41 | const code = char.charCodeAt(0); 42 | if (code >= 65 && code <= 90) { 43 | return String.fromCharCode(((code - 65 + 13) % 26) + 65); 44 | } else if (code >= 97 && code <= 122) { 45 | return String.fromCharCode(((code - 97 + 13) % 26) + 97); 46 | } 47 | return char; 48 | }); 49 | } 50 | 51 | /** 52 | * Base64 encoding helper (using btoa for browser/React Native) 53 | */ 54 | function base64Encode(str) { 55 | try { 56 | // For React Native, we need to handle Unicode properly 57 | const utf8Bytes = unescape(encodeURIComponent(str)); 58 | return btoa(utf8Bytes); 59 | } catch (error) { 60 | console.error('[Cinevibe] Base64 encode error:', error); 61 | throw error; 62 | } 63 | } 64 | 65 | /** 66 | * Base64 decoding helper (using atob for browser/React Native) 67 | */ 68 | function base64Decode(str) { 69 | try { 70 | const decoded = atob(str); 71 | return decodeURIComponent(escape(decoded)); 72 | } catch (error) { 73 | console.error('[Cinevibe] Base64 decode error:', error); 74 | throw error; 75 | } 76 | } 77 | 78 | /** 79 | * Deterministic string obfuscator using layered reversible encodings 80 | * Equivalent to Python's custom_encode function 81 | */ 82 | function customEncode(e) { 83 | // Step 1: Base64 encode 84 | let encoded = base64Encode(e); 85 | 86 | // Step 2: Reverse string 87 | encoded = encoded.split('').reverse().join(''); 88 | 89 | // Step 3: ROT13 encode 90 | encoded = rot13(encoded); 91 | 92 | // Step 4: Base64 encode again 93 | encoded = base64Encode(encoded); 94 | 95 | // Step 5: Replace characters 96 | encoded = encoded.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); 97 | 98 | return encoded; 99 | } 100 | 101 | /** 102 | * Get movie/TV show details from TMDB 103 | */ 104 | function getTMDBDetails(tmdbId, mediaType) { 105 | const endpoint = mediaType === 'tv' ? 'tv' : 'movie'; 106 | const url = `${TMDB_BASE_URL}/${endpoint}/${tmdbId}?api_key=${TMDB_API_KEY}`; 107 | 108 | console.log(`[Cinevibe] Fetching TMDB details for ${mediaType} ID: ${tmdbId}`); 109 | 110 | return fetch(url, { 111 | method: 'GET', 112 | headers: { 113 | 'User-Agent': USER_AGENT, 114 | 'Accept': 'application/json' 115 | } 116 | }).then(function(response) { 117 | if (!response.ok) { 118 | throw new Error(`TMDB API error: ${response.status} ${response.statusText}`); 119 | } 120 | return response.json(); 121 | }).then(function(data) { 122 | const title = mediaType === 'tv' ? data.name : data.title; 123 | const releaseDate = mediaType === 'tv' ? data.first_air_date : data.release_date; 124 | const releaseYear = releaseDate ? releaseDate.split('-')[0] : null; 125 | const imdbId = data.imdb_id || null; 126 | 127 | console.log(`[Cinevibe] TMDB Info: "${title}" (${releaseYear || 'N/A'})`); 128 | 129 | return { 130 | title: title, 131 | releaseYear: releaseYear, 132 | imdbId: imdbId 133 | }; 134 | }).catch(function(error) { 135 | console.error(`[Cinevibe] TMDB fetch error: ${error.message}`); 136 | throw error; 137 | }); 138 | } 139 | 140 | /** 141 | * Generate token for Cinevibe API 142 | */ 143 | function generateToken(tmdbId, title, releaseYear, mediaType) { 144 | // Clean title for token (remove non-alphanumeric chars, lowercase) 145 | const cleanTitle = title.toLowerCase().replace(/[^a-z0-9]/g, ''); 146 | 147 | // Time-based key: current time in milliseconds divided by 300000 (5 minutes) 148 | const timeWindow = Math.floor(Date.now() / 300000); 149 | const timeBasedKey = `${timeWindow}_${BROWSER_FINGERPRINT}_cinevibe_2025`; 150 | 151 | // Hash the time-based key 152 | const hashedKey = fnv1a32(timeBasedKey); 153 | 154 | // Current time in seconds divided by 600 (10 minutes) 155 | // Python: int(time.time() // 600) where time.time() is seconds 156 | const timeStamp = Math.floor(Date.now() / 1000 / 600); 157 | 158 | // Construct token string 159 | const tokenString = `${SESSION_ENTROPY}|${tmdbId}|${cleanTitle}|${releaseYear}||${hashedKey}|${timeStamp}|${BROWSER_FINGERPRINT}`; 160 | 161 | // Encode token 162 | const token = customEncode(tokenString); 163 | 164 | return token; 165 | } 166 | 167 | /** 168 | * Extract quality from stream source or URL 169 | */ 170 | function getQualityFromSource(source) { 171 | if (!source) { 172 | return 'Auto'; 173 | } 174 | 175 | // Check label first 176 | if (source.label) { 177 | const label = source.label.toLowerCase(); 178 | if (label.includes('2160') || label.includes('4k')) return '4K'; 179 | if (label.includes('1440') || label.includes('2k')) return '1440p'; 180 | if (label.includes('1080')) return '1080p'; 181 | if (label.includes('720')) return '720p'; 182 | if (label.includes('480')) return '480p'; 183 | if (label.includes('360')) return '360p'; 184 | if (label.includes('240')) return '240p'; 185 | if (label.includes('auto')) return 'Auto'; 186 | return source.label; // Use the label as quality if it's descriptive 187 | } 188 | 189 | // Check other possible quality fields 190 | if (source.quality) { 191 | const quality = source.quality.toLowerCase(); 192 | if (quality.includes('2160') || quality.includes('4k')) return '4K'; 193 | if (quality.includes('1440') || quality.includes('2k')) return '1440p'; 194 | if (quality.includes('1080')) return '1080p'; 195 | if (quality.includes('720')) return '720p'; 196 | if (quality.includes('480')) return '480p'; 197 | if (quality.includes('360')) return '360p'; 198 | if (quality.includes('240')) return '240p'; 199 | return source.quality; 200 | } 201 | 202 | // Try to extract from URL 203 | if (source.url) { 204 | const urlMatch = source.url.match(/(\d{3,4})[pP]/); 205 | if (urlMatch) { 206 | const res = parseInt(urlMatch[1]); 207 | if (res >= 2160) return '4K'; 208 | if (res >= 1440) return '1440p'; 209 | if (res >= 1080) return '1080p'; 210 | if (res >= 720) return '720p'; 211 | if (res >= 480) return '480p'; 212 | if (res >= 360) return '360p'; 213 | return '240p'; 214 | } 215 | } 216 | 217 | // Default to Auto since Cinevibe provides adaptive streaming 218 | return 'Auto'; 219 | } 220 | 221 | /** 222 | * Make HEAD request to detect stream quality 223 | */ 224 | function detectStreamQuality(url) { 225 | console.log(`[Cinevibe] Detecting quality for: ${url.substring(0, 50)}...`); 226 | 227 | return fetch(url, { 228 | method: 'HEAD', 229 | headers: WORKING_HEADERS 230 | }).then(function(response) { 231 | // Try to extract quality from Content-Disposition header filename 232 | let quality = 'Auto'; // Default fallback 233 | 234 | const contentDisposition = response.headers.get('content-disposition'); 235 | if (contentDisposition) { 236 | const filenameMatch = contentDisposition.match(/filename[^;]*=([^;]*)/i); 237 | if (filenameMatch) { 238 | const filename = filenameMatch[1].replace(/["']/g, ''); 239 | // Extract quality from filename (e.g., "Movie-720P.mp4", "Movie-1080P.mp4") 240 | const qualityMatch = filename.match(/-(\d{3,4})[pP]/i); 241 | if (qualityMatch) { 242 | const res = parseInt(qualityMatch[1]); 243 | if (res >= 2160) quality = '4K'; 244 | else if (res >= 1440) quality = '1440p'; 245 | else if (res >= 1080) quality = '1080p'; 246 | else if (res >= 720) quality = '720p'; 247 | else if (res >= 480) quality = '480p'; 248 | else if (res >= 360) quality = '360p'; 249 | else quality = '240p'; 250 | } 251 | } 252 | } 253 | 254 | // Fallback: Check Content-Type for video format hints 255 | if (quality === 'Auto') { 256 | const contentType = response.headers.get('content-type'); 257 | if (contentType) { 258 | if (contentType.includes('avc1.6400') || contentType.includes('hev1.2.4.L150') || contentType.includes('hvc1.2.4.L150')) { 259 | quality = '4K'; 260 | } else if (contentType.includes('avc1.6400') || contentType.includes('hev1.2.4.L120') || contentType.includes('hvc1.2.4.L120')) { 261 | quality = '1440p'; 262 | } else if (contentType.includes('avc1.4d00') || contentType.includes('hev1.1.6.L93') || contentType.includes('hvc1.1.6.L93')) { 263 | quality = '1080p'; 264 | } else if (contentType.includes('avc1.4200') || contentType.includes('hev1.1.6.L63') || contentType.includes('hvc1.1.6.L63')) { 265 | quality = '720p'; 266 | } else if (contentType.includes('avc1.42C0')) { 267 | quality = '480p'; 268 | } 269 | } 270 | } 271 | 272 | // Fallback: Check for resolution in custom headers 273 | if (quality === 'Auto') { 274 | const resolution = response.headers.get('x-resolution') || response.headers.get('resolution'); 275 | if (resolution) { 276 | const resMatch = resolution.match(/(\d+)x(\d+)/); 277 | if (resMatch) { 278 | const height = parseInt(resMatch[2]); 279 | if (height >= 2160) quality = '4K'; 280 | else if (height >= 1440) quality = '1440p'; 281 | else if (height >= 1080) quality = '1080p'; 282 | else if (height >= 720) quality = '720p'; 283 | else if (height >= 480) quality = '480p'; 284 | else if (height >= 360) quality = '360p'; 285 | else quality = '240p'; 286 | } 287 | } 288 | } 289 | 290 | // Fallback: Check Content-Length for file size estimation 291 | if (quality === 'Auto') { 292 | const contentLength = response.headers.get('content-length'); 293 | if (contentLength && !isNaN(contentLength)) { 294 | const sizeGB = parseInt(contentLength) / (1024 * 1024 * 1024); 295 | const sizeMB = parseInt(contentLength) / (1024 * 1024); 296 | if (sizeGB >= 4) quality = '4K'; 297 | else if (sizeGB >= 2) quality = '1440p'; 298 | else if (sizeGB >= 1) quality = '1080p'; 299 | else if (sizeMB >= 500) quality = '720p'; 300 | else if (sizeMB >= 200) quality = '480p'; 301 | } 302 | } 303 | 304 | return quality; 305 | 306 | }).catch(function(error) { 307 | console.log(`[Cinevibe] HEAD request failed, using Auto quality: ${error.message}`); 308 | return 'Auto'; 309 | }); 310 | } 311 | 312 | /** 313 | * Fetch streaming data from Cinevibe API 314 | */ 315 | function fetchStreams(tmdbId, mediaType, seasonNum, episodeNum, mediaInfo) { 316 | const { title, releaseYear } = mediaInfo; 317 | 318 | // Generate token 319 | const token = generateToken(tmdbId, title, releaseYear, mediaType); 320 | const timestamp = Date.now(); 321 | 322 | // Build API URL 323 | const apiUrl = `${BASE_URL}/api/stream/fetch?server=cinebox-1&type=${mediaType}&mediaId=${tmdbId}&title=${encodeURIComponent(title)}&releaseYear=${releaseYear}&_token=${token}&_ts=${timestamp}`; 324 | 325 | console.log(`[Cinevibe] Fetching streams from API...`); 326 | 327 | return fetch(apiUrl, { 328 | method: 'GET', 329 | headers: WORKING_HEADERS 330 | }).then(function(response) { 331 | if (!response.ok) { 332 | throw new Error(`Cinevibe API error: ${response.status} ${response.statusText}`); 333 | } 334 | return response.json(); 335 | }).then(function(data) { 336 | console.log(`[Cinevibe] API response received`); 337 | 338 | if (!data || !data.sources || !Array.isArray(data.sources) || data.sources.length === 0) { 339 | throw new Error('No sources found in API response'); 340 | } 341 | 342 | // Process sources and detect qualities 343 | const qualityPromises = data.sources.map(function(source, index) { 344 | if (!source || !source.url) { 345 | return Promise.resolve({ 346 | index: index, 347 | source: source, 348 | quality: 'Auto' 349 | }); 350 | } 351 | 352 | return detectStreamQuality(source.url).then(function(quality) { 353 | return { 354 | index: index, 355 | source: source, 356 | quality: quality 357 | }; 358 | }).catch(function() { 359 | return { 360 | index: index, 361 | source: source, 362 | quality: 'Auto' 363 | }; 364 | }); 365 | }); 366 | 367 | return Promise.allSettled(qualityPromises).then(function(results) { 368 | const streams = []; 369 | 370 | results.forEach(function(result) { 371 | if (result.status === 'fulfilled') { 372 | const { index, source, quality } = result.value; 373 | 374 | // Build media title 375 | let mediaTitle = title; 376 | if (mediaType === 'tv' && seasonNum && episodeNum) { 377 | mediaTitle = `${title} S${String(seasonNum).padStart(2, '0')}E${String(episodeNum).padStart(2, '0')}`; 378 | } else if (releaseYear) { 379 | mediaTitle = `${title} (${releaseYear})`; 380 | } 381 | 382 | streams.push({ 383 | name: `Cinevibe - ${quality}`, 384 | title: mediaTitle, 385 | url: source.url, 386 | quality: quality, 387 | size: 'Unknown', 388 | headers: WORKING_HEADERS, 389 | provider: 'cinevibe' 390 | }); 391 | } 392 | }); 393 | 394 | console.log(`[Cinevibe] Found ${streams.length} streams with detected qualities`); 395 | 396 | return streams; 397 | }); 398 | }).catch(function(error) { 399 | console.error(`[Cinevibe] Stream fetch error: ${error.message}`); 400 | throw error; 401 | }); 402 | } 403 | 404 | /** 405 | * Main scraping function 406 | * @param {string} tmdbId - TMDB ID 407 | * @param {string} mediaType - "movie" or "tv" 408 | * @param {number} seasonNum - Season number (TV only) 409 | * @param {number} episodeNum - Episode number (TV only) 410 | */ 411 | function getStreams(tmdbId, mediaType, seasonNum, episodeNum) { 412 | console.log(`[Cinevibe] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}${mediaType === 'tv' ? `, S:${seasonNum}E:${episodeNum}` : ''}`); 413 | 414 | // Check if TV series is supported (Python code shows it's not supported yet) 415 | if (mediaType === 'tv') { 416 | console.log('[Cinevibe] TV Series currently not supported'); 417 | return Promise.resolve([]); 418 | } 419 | 420 | // Get TMDB details first 421 | return getTMDBDetails(tmdbId, mediaType).then(function(mediaInfo) { 422 | if (!mediaInfo.title || !mediaInfo.releaseYear) { 423 | throw new Error('Could not extract title and release year from TMDB response'); 424 | } 425 | 426 | // Fetch streams from Cinevibe API 427 | return fetchStreams(tmdbId, mediaType, seasonNum, episodeNum, mediaInfo); 428 | }).catch(function(error) { 429 | console.error(`[Cinevibe] Scraping error: ${error.message}`); 430 | return []; 431 | }); 432 | } 433 | 434 | // Export the main function 435 | if (typeof module !== 'undefined' && module.exports) { 436 | module.exports = { getStreams }; 437 | } else { 438 | // For React Native environment 439 | global.CinevibeScraperModule = { getStreams }; 440 | } 441 | 442 | -------------------------------------------------------------------------------- /providers/streamflix.js: -------------------------------------------------------------------------------- 1 | // StreamFlix Provider for Nuvio 2 | // Ported from StreamFlix API 3 | const cheerio = require('cheerio-without-node-native'); 4 | 5 | // Constants 6 | const TMDB_API_KEY = "439c478a771f35c05022f9feabcca01c"; 7 | const STREAMFLIX_API_BASE = "https://api.streamflix.app"; 8 | const CONFIG_URL = `${STREAMFLIX_API_BASE}/config/config-streamflixapp.json`; 9 | const DATA_URL = `${STREAMFLIX_API_BASE}/data.json`; 10 | const WEBSOCKET_URL = "wss://chilflix-410be-default-rtdb.asia-southeast1.firebasedatabase.app/.ws?ns=chilflix-410be-default-rtdb&v=5"; 11 | 12 | // Global cache 13 | let cache = { 14 | config: null, 15 | configTimestamp: 0, 16 | data: null, 17 | dataTimestamp: 0, 18 | }; 19 | const CACHE_TTL = 1000 * 60 * 5; // 5 minutes 20 | 21 | // Helper function for HTTP requests 22 | function makeRequest(url, options = {}) { 23 | const defaultHeaders = { 24 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 25 | 'Accept': 'application/json, text/plain, */*', 26 | 'Accept-Language': 'en-US,en;q=0.5', 27 | 'Connection': 'keep-alive' 28 | }; 29 | 30 | return fetch(url, { 31 | ...options, 32 | headers: { 33 | ...defaultHeaders, 34 | ...options.headers 35 | } 36 | }).then(response => { 37 | if (!response.ok) { 38 | throw new Error(`HTTP ${response.status}: ${response.statusText}`); 39 | } 40 | return response; 41 | }); 42 | } 43 | 44 | // Get config data with caching 45 | function getConfig() { 46 | const now = Date.now(); 47 | if (cache.config && now - cache.configTimestamp < CACHE_TTL) { 48 | return Promise.resolve(cache.config); 49 | } 50 | 51 | console.log('[StreamFlix] Fetching config data...'); 52 | return makeRequest(CONFIG_URL) 53 | .then(response => response.json()) 54 | .then(json => { 55 | cache.config = json; 56 | cache.configTimestamp = now; 57 | console.log('[StreamFlix] Config data cached successfully'); 58 | return json; 59 | }) 60 | .catch(error => { 61 | console.error('[StreamFlix] Failed to fetch config:', error.message); 62 | throw error; 63 | }); 64 | } 65 | 66 | // Get data with caching 67 | function getData() { 68 | const now = Date.now(); 69 | if (cache.data && now - cache.dataTimestamp < CACHE_TTL) { 70 | return Promise.resolve(cache.data); 71 | } 72 | 73 | console.log('[StreamFlix] Fetching data...'); 74 | return makeRequest(DATA_URL) 75 | .then(response => response.json()) 76 | .then(json => { 77 | cache.data = json; 78 | cache.dataTimestamp = now; 79 | console.log('[StreamFlix] Data cached successfully'); 80 | return json; 81 | }) 82 | .catch(error => { 83 | console.error('[StreamFlix] Failed to fetch data:', error.message); 84 | throw error; 85 | }); 86 | } 87 | 88 | // Search for content by title 89 | function searchContent(title, year, mediaType) { 90 | console.log(`[StreamFlix] Searching for: "${title}" (${year})`); 91 | 92 | return getData() 93 | .then(data => { 94 | if (!data || !data.data) { 95 | throw new Error('Invalid data structure received'); 96 | } 97 | 98 | const searchQuery = title.toLowerCase(); 99 | const results = data.data.filter(item => { 100 | if (!item.moviename) return false; 101 | 102 | const itemTitle = item.moviename.toLowerCase(); 103 | const titleWords = searchQuery.split(/\s+/); 104 | 105 | // Check if all words from search query are present in the item title 106 | return titleWords.every(word => itemTitle.includes(word)); 107 | }); 108 | 109 | console.log(`[StreamFlix] Found ${results.length} search results`); 110 | return results; 111 | }); 112 | } 113 | 114 | // Find best match from search results 115 | function findBestMatch(targetTitle, results) { 116 | if (!results || results.length === 0) { 117 | return null; 118 | } 119 | 120 | let bestMatch = null; 121 | let bestScore = 0; 122 | 123 | for (const result of results) { 124 | const score = calculateSimilarity( 125 | targetTitle.toLowerCase(), 126 | result.moviename.toLowerCase() 127 | ); 128 | 129 | if (score > bestScore) { 130 | bestScore = score; 131 | bestMatch = result; 132 | } 133 | } 134 | 135 | console.log(`[StreamFlix] Best match: "${bestMatch?.moviename}" (score: ${bestScore.toFixed(2)})`); 136 | return bestMatch; 137 | } 138 | 139 | // Calculate string similarity 140 | function calculateSimilarity(str1, str2) { 141 | const words1 = str1.split(/\s+/); 142 | const words2 = str2.split(/\s+/); 143 | 144 | let matches = 0; 145 | for (const word of words1) { 146 | if (word.length > 2 && words2.some(w => w.includes(word) || word.includes(w))) { 147 | matches++; 148 | } 149 | } 150 | 151 | return matches / Math.max(words1.length, words2.length); 152 | } 153 | 154 | // WebSocket-based episode fetching (real implementation per series.py/api.js) 155 | function getEpisodesFromWebSocket(movieKey, totalSeasons = 1) { 156 | return new Promise((resolve, reject) => { 157 | let WSImpl = null; 158 | try { 159 | WSImpl = typeof WebSocket !== 'undefined' ? WebSocket : require('ws'); 160 | } catch (e) { 161 | WSImpl = null; 162 | } 163 | 164 | if (!WSImpl) { 165 | return reject(new Error('WebSocket implementation not available')); 166 | } 167 | 168 | const ws = new WSImpl( 169 | 'wss://chilflix-410be-default-rtdb.asia-southeast1.firebasedatabase.app/.ws?ns=chilflix-410be-default-rtdb&v=5' 170 | ); 171 | 172 | const seasonsData = {}; 173 | let currentSeason = 1; 174 | let completedSeasons = 0; 175 | let messageBuffer = ''; 176 | let expectedResponses = 0; 177 | let responsesReceived = 0; 178 | 179 | const overallTimeout = setTimeout(() => { 180 | try { ws.close(); } catch {} 181 | reject(new Error('WebSocket timeout')); 182 | }, 30000); 183 | 184 | function sendSeasonRequest(season) { 185 | const payload = { 186 | t: 'd', 187 | d: { a: 'q', r: season, b: { p: `Data/${movieKey}/seasons/${season}/episodes`, h: '' } } 188 | }; 189 | try { 190 | ws.send(JSON.stringify(payload)); 191 | } catch (e) { 192 | // Ignore send errors; will be picked up by 'error' event 193 | } 194 | } 195 | 196 | ws.onopen = function () { 197 | sendSeasonRequest(currentSeason); 198 | }; 199 | 200 | ws.onmessage = function (evt) { 201 | try { 202 | const message = (typeof evt.data === 'string') ? evt.data : evt.data.toString(); 203 | 204 | // numeric count of expected messages sometimes sent 205 | if (/^\d+$/.test(message.trim())) { 206 | expectedResponses = parseInt(message.trim(), 10); 207 | responsesReceived = 0; 208 | return; 209 | } 210 | 211 | messageBuffer += message; 212 | 213 | try { 214 | const data = JSON.parse(messageBuffer); 215 | messageBuffer = ''; 216 | 217 | if (data.t === 'c') { 218 | return; // handshake complete 219 | } 220 | 221 | if (data.t === 'd') { 222 | const d_data = data.d || {}; 223 | const b_data = d_data.b || {}; 224 | 225 | // completion for current season 226 | if (d_data.r === currentSeason && b_data.s === 'ok') { 227 | completedSeasons++; 228 | if (completedSeasons < totalSeasons) { 229 | currentSeason++; 230 | expectedResponses = 0; 231 | responsesReceived = 0; 232 | sendSeasonRequest(currentSeason); 233 | } else { 234 | clearTimeout(overallTimeout); 235 | try { ws.close(); } catch {} 236 | resolve(seasonsData); 237 | } 238 | return; 239 | } 240 | 241 | // episode data 242 | if (b_data.d) { 243 | const episodes = b_data.d; 244 | const seasonEpisodes = seasonsData[currentSeason] || {}; 245 | for (const [epKey, epData] of Object.entries(episodes)) { 246 | if (epData && typeof epData === 'object') { 247 | seasonEpisodes[parseInt(epKey, 10)] = { 248 | key: epData.key, 249 | link: epData.link, 250 | name: epData.name, 251 | overview: epData.overview, 252 | runtime: epData.runtime, 253 | still_path: epData.still_path, 254 | vote_average: epData.vote_average 255 | }; 256 | responsesReceived++; 257 | } 258 | } 259 | seasonsData[currentSeason] = seasonEpisodes; 260 | 261 | // If we know how many to expect and we reached/exceeded it, do nothing here. 262 | // The season completion is signaled by b.s === 'ok' above which we handle to advance. 263 | } 264 | } 265 | } catch (e) { 266 | // Incomplete JSON in buffer, wait for more 267 | if (messageBuffer.length > 100000) { 268 | messageBuffer = ''; 269 | } 270 | } 271 | } catch (err) { 272 | // ignore parse errors; will continue buffering 273 | } 274 | }; 275 | 276 | ws.onerror = function (err) { 277 | clearTimeout(overallTimeout); 278 | reject(new Error('WebSocket error')); 279 | }; 280 | 281 | ws.onclose = function () { 282 | clearTimeout(overallTimeout); 283 | }; 284 | }); 285 | } 286 | 287 | // Main function that Nuvio will call 288 | function getStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) { 289 | console.log(`[StreamFlix] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}`); 290 | 291 | if (seasonNum !== null) { 292 | console.log(`[StreamFlix] Season: ${seasonNum}, Episode: ${episodeNum}`); 293 | } 294 | 295 | // Get TMDB info first 296 | const tmdbUrl = `https://api.themoviedb.org/3/${mediaType === 'tv' ? 'tv' : 'movie'}/${tmdbId}?api_key=${TMDB_API_KEY}`; 297 | 298 | return makeRequest(tmdbUrl) 299 | .then(response => response.json()) 300 | .then(tmdbData => { 301 | const title = mediaType === 'tv' ? tmdbData.name : tmdbData.title; 302 | const year = mediaType === 'tv' 303 | ? tmdbData.first_air_date?.substring(0, 4) 304 | : tmdbData.release_date?.substring(0, 4); 305 | 306 | if (!title) { 307 | throw new Error('Could not extract title from TMDB response'); 308 | } 309 | 310 | console.log(`[StreamFlix] TMDB Info: "${title}" (${year})`); 311 | 312 | // Search for content 313 | return searchContent(title, year, mediaType) 314 | .then(searchResults => { 315 | if (searchResults.length === 0) { 316 | console.log('[StreamFlix] No search results found'); 317 | return []; 318 | } 319 | 320 | const selectedResult = findBestMatch(title, searchResults); 321 | if (!selectedResult) { 322 | console.log('[StreamFlix] No suitable match found'); 323 | return []; 324 | } 325 | 326 | // Get config for stream URLs 327 | return getConfig() 328 | .then(config => { 329 | if (mediaType === 'movie') { 330 | // Process movie streams 331 | return processMovieStreams(selectedResult, config); 332 | } else { 333 | // Process TV show streams 334 | return processTVStreams(selectedResult, config, seasonNum, episodeNum); 335 | } 336 | }); 337 | }); 338 | }) 339 | .catch(error => { 340 | console.error(`[StreamFlix] Error in getStreams: ${error.message}`); 341 | return []; 342 | }); 343 | } 344 | 345 | // Process movie streams 346 | function processMovieStreams(movieData, config) { 347 | console.log(`[StreamFlix] Processing movie streams for: ${movieData.moviename}`); 348 | 349 | const streams = []; 350 | 351 | // Premium streams (higher quality) 352 | if (config.premium && movieData.movielink) { 353 | config.premium.forEach((baseUrl, index) => { 354 | const streamUrl = `${baseUrl}${movieData.movielink}`; 355 | streams.push({ 356 | name: "StreamFlix", 357 | title: `${movieData.moviename} - Premium Quality`, 358 | url: streamUrl, 359 | quality: "1080p", 360 | size: movieData.movieduration || "Unknown", 361 | type: 'direct', 362 | headers: { 363 | 'Referer': 'https://api.streamflix.app', 364 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' 365 | } 366 | }); 367 | }); 368 | } 369 | 370 | // Regular movie streams 371 | if (config.movies && movieData.movielink) { 372 | config.movies.forEach((baseUrl, index) => { 373 | const streamUrl = `${baseUrl}${movieData.movielink}`; 374 | streams.push({ 375 | name: "StreamFlix", 376 | title: `${movieData.moviename} - Standard Quality`, 377 | url: streamUrl, 378 | quality: "720p", 379 | size: movieData.movieduration || "Unknown", 380 | type: 'direct', 381 | headers: { 382 | 'Referer': 'https://api.streamflix.app', 383 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' 384 | } 385 | }); 386 | }); 387 | } 388 | 389 | console.log(`[StreamFlix] Generated ${streams.length} movie streams`); 390 | return streams; 391 | } 392 | 393 | // Process TV show streams 394 | function processTVStreams(tvData, config, seasonNum, episodeNum) { 395 | console.log(`[StreamFlix] Processing TV streams for: ${tvData.moviename}`); 396 | 397 | // Extract total seasons from duration field 398 | const seasonMatch = tvData.movieduration?.match(/(\d+)\s+Season/); 399 | const totalSeasons = seasonMatch ? parseInt(seasonMatch[1]) : 1; 400 | 401 | return getEpisodesFromWebSocket(tvData.moviekey, totalSeasons) 402 | .then(seasonsData => { 403 | const streams = []; 404 | 405 | // If specific episode requested 406 | if (seasonNum !== null && episodeNum !== null) { 407 | const seasonData = seasonsData[seasonNum]; 408 | if (seasonData) { 409 | const episodeData = seasonData[episodeNum - 1]; 410 | if (episodeData && config.premium) { 411 | config.premium.forEach(baseUrl => { 412 | const streamUrl = `${baseUrl}${episodeData.link}`; 413 | streams.push({ 414 | name: "StreamFlix", 415 | title: `${tvData.moviename} S${seasonNum}E${episodeNum} - ${episodeData.name}`, 416 | url: streamUrl, 417 | quality: "1080p", 418 | size: episodeData.runtime ? `${episodeData.runtime}min` : "Unknown", 419 | type: 'direct', 420 | headers: { 421 | 'Referer': 'https://api.streamflix.app', 422 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' 423 | } 424 | }); 425 | }); 426 | } 427 | } 428 | } else { 429 | // Return all episodes for all seasons 430 | for (const [season, episodes] of Object.entries(seasonsData)) { 431 | for (const [epIndex, episodeData] of Object.entries(episodes)) { 432 | if (config.premium && episodeData.link) { 433 | const epNum = parseInt(epIndex) + 1; 434 | config.premium.forEach(baseUrl => { 435 | const streamUrl = `${baseUrl}${episodeData.link}`; 436 | streams.push({ 437 | name: "StreamFlix", 438 | title: `${tvData.moviename} S${season}E${epNum} - ${episodeData.name}`, 439 | url: streamUrl, 440 | quality: "1080p", 441 | size: episodeData.runtime ? `${episodeData.runtime}min` : "Unknown", 442 | type: 'direct', 443 | headers: { 444 | 'Referer': 'https://api.streamflix.app', 445 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' 446 | } 447 | }); 448 | }); 449 | } 450 | } 451 | } 452 | } 453 | 454 | // Fallback if no episodes found 455 | if (streams.length === 0 && config.premium && seasonNum !== null && episodeNum !== null) { 456 | const fallbackUrl = `${config.premium[0]}tv/${tvData.moviekey}/s${seasonNum}/episode${episodeNum}.mkv`; 457 | streams.push({ 458 | name: "StreamFlix", 459 | title: `${tvData.moviename} S${seasonNum}E${episodeNum} (Fallback)`, 460 | url: fallbackUrl, 461 | quality: "720p", 462 | size: "Unknown", 463 | type: 'direct', 464 | headers: { 465 | 'Referer': 'https://api.streamflix.app', 466 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' 467 | } 468 | }); 469 | } 470 | 471 | console.log(`[StreamFlix] Generated ${streams.length} TV streams`); 472 | return streams; 473 | }) 474 | .catch(error => { 475 | console.error('[StreamFlix] WebSocket failed, using fallback:', error.message); 476 | 477 | // Generate fallback stream 478 | if (config.premium && seasonNum !== null && episodeNum !== null) { 479 | const fallbackUrl = `${config.premium[0]}tv/${tvData.moviekey}/s${seasonNum}/episode${episodeNum}.mkv`; 480 | return [{ 481 | name: "StreamFlix", 482 | title: `${tvData.moviename} S${seasonNum}E${episodeNum} (Fallback)`, 483 | url: fallbackUrl, 484 | quality: "720p", 485 | size: "Unknown", 486 | type: 'direct', 487 | headers: { 488 | 'Referer': 'https://api.streamflix.app', 489 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' 490 | } 491 | }]; 492 | } 493 | 494 | return []; 495 | }); 496 | } 497 | 498 | // Export for React Native 499 | if (typeof module !== 'undefined' && module.exports) { 500 | module.exports = { getStreams }; 501 | } else { 502 | global.getStreams = getStreams; 503 | } 504 | -------------------------------------------------------------------------------- /providers/hdrezka.js: -------------------------------------------------------------------------------- 1 | // HDRezka Scraper for Nuvio Local Scrapers 2 | // React Native compatible version - No async/await for sandbox compatibility 3 | 4 | // Import cheerio for HTML parsing (React Native compatible) 5 | const cheerio = require('cheerio-without-node-native'); 6 | 7 | console.log('[HDRezka] Using cheerio-without-node-native for DOM parsing'); 8 | 9 | // Constants 10 | const TMDB_API_KEY = "439c478a771f35c05022f9feabcca01c"; 11 | const REZKA_BASE = 'https://hdrezka.ag/'; 12 | const BASE_HEADERS = { 13 | 'X-Hdrezka-Android-App': '1', 14 | 'X-Hdrezka-Android-App-Version': '2.2.0', 15 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 16 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 17 | 'Accept-Language': 'en-US,en;q=0.5', 18 | 'Connection': 'keep-alive' 19 | }; 20 | 21 | // Helper function to make HTTP requests 22 | function makeRequest(url, options = {}) { 23 | return fetch(url, { 24 | ...options, 25 | headers: { 26 | ...BASE_HEADERS, 27 | ...options.headers 28 | } 29 | }).then(function (response) { 30 | if (!response.ok) { 31 | throw new Error(`HTTP ${response.status}: ${response.statusText}`); 32 | } 33 | return response; 34 | }); 35 | } 36 | 37 | // Generate random favs parameter 38 | function generateRandomFavs() { 39 | const randomHex = () => Math.floor(Math.random() * 16).toString(16); 40 | const generateSegment = (length) => Array.from({ length }, randomHex).join(''); 41 | 42 | return `${generateSegment(8)}-${generateSegment(4)}-${generateSegment(4)}-${generateSegment(4)}-${generateSegment(12)}`; 43 | } 44 | 45 | // Extract title and year from search result 46 | function extractTitleAndYear(input) { 47 | const regex = /^(.*?),.*?(\d{4})/; 48 | const match = input.match(regex); 49 | 50 | if (match) { 51 | const title = match[1]; 52 | const year = match[2]; 53 | return { title: title.trim(), year: year ? parseInt(year, 10) : null }; 54 | } 55 | return null; 56 | } 57 | 58 | // Parse video links from HDRezka response (optimized) 59 | function parseVideoLinks(inputString) { 60 | if (!inputString) { 61 | console.log('[HDRezka] No video links found'); 62 | return {}; 63 | } 64 | 65 | console.log(`[HDRezka] Parsing video links from stream URL data`); 66 | const linksArray = inputString.split(','); 67 | const result = {}; 68 | 69 | // Pre-compile regex patterns for better performance 70 | const simplePattern = /\[([^<\]]+)\](https?:\/\/[^\s,]+\.mp4|null)/; 71 | const qualityPattern = /\[]*>([^<]+)/; 72 | const urlPattern = /\][^[]*?(https?:\/\/[^\s,]+\.mp4|null)/; 73 | 74 | for (const link of linksArray) { 75 | // Try simple format first (non-HTML) 76 | let match = link.match(simplePattern); 77 | 78 | // If not found, try HTML format with more flexible pattern 79 | if (!match) { 80 | const qualityMatch = link.match(qualityPattern); 81 | const urlMatch = link.match(urlPattern); 82 | 83 | if (qualityMatch && urlMatch) { 84 | match = [null, qualityMatch[1].trim(), urlMatch[1]]; 85 | } 86 | } 87 | 88 | if (match) { 89 | const qualityText = match[1].trim(); 90 | const mp4Url = match[2]; 91 | 92 | // Skip null URLs (premium content that requires login) 93 | if (mp4Url !== 'null') { 94 | result[qualityText] = { type: 'mp4', url: mp4Url }; 95 | console.log(`[HDRezka] Found ${qualityText}: ${mp4Url.substring(0, 50)}...`); 96 | } else { 97 | console.log(`[HDRezka] Premium quality ${qualityText} requires login (null URL)`); 98 | } 99 | } else { 100 | console.log(`[HDRezka] Could not parse quality from: ${link.substring(0, 100)}...`); 101 | } 102 | } 103 | 104 | console.log(`[HDRezka] Found ${Object.keys(result).length} valid qualities: ${Object.keys(result).join(', ')}`); 105 | return result; 106 | } 107 | 108 | // Parse subtitles from HDRezka response (optimized) 109 | function parseSubtitles(inputString) { 110 | if (!inputString) { 111 | console.log('[HDRezka] No subtitles found'); 112 | return []; 113 | } 114 | 115 | console.log(`[HDRezka] Parsing subtitles data`); 116 | const linksArray = inputString.split(','); 117 | const captions = []; 118 | 119 | // Pre-compile regex pattern for better performance 120 | const subtitlePattern = /\[([^\]]+)\](https?:\/\/\S+?)(?=,\[|$)/; 121 | 122 | for (const link of linksArray) { 123 | const match = link.match(subtitlePattern); 124 | 125 | if (match) { 126 | const language = match[1]; 127 | const url = match[2]; 128 | 129 | captions.push({ 130 | id: url, 131 | language, 132 | hasCorsRestrictions: false, 133 | type: 'vtt', 134 | url: url, 135 | }); 136 | console.log(`[HDRezka] Found subtitle ${language}: ${url.substring(0, 50)}...`); 137 | } 138 | } 139 | 140 | console.log(`[HDRezka] Found ${captions.length} subtitles`); 141 | return captions; 142 | } 143 | 144 | // Search for content and find media ID 145 | function searchAndFindMediaId(media) { 146 | console.log(`[HDRezka] Searching for title: ${media.title}, type: ${media.type}, year: ${media.releaseYear || 'any'}`); 147 | 148 | const itemRegexPattern = /([^<]+)<\/span> \(([^)]+)\)/g; 149 | const idRegexPattern = /\/(\d+)-[^/]+\.html$/; 150 | 151 | const fullUrl = new URL('/engine/ajax/search.php', REZKA_BASE); 152 | fullUrl.searchParams.append('q', media.title); 153 | 154 | console.log(`[HDRezka] Making search request to: ${fullUrl.toString()}`); 155 | return makeRequest(fullUrl.toString()).then(function (response) { 156 | return response.text(); 157 | }).then(function (searchData) { 158 | console.log(`[HDRezka] Search response length: ${searchData.length}`); 159 | 160 | const movieData = []; 161 | let match; 162 | 163 | while ((match = itemRegexPattern.exec(searchData)) !== null) { 164 | const url = match[1]; 165 | const titleAndYear = match[3]; 166 | 167 | const result = extractTitleAndYear(titleAndYear); 168 | if (result !== null) { 169 | const id = url.match(idRegexPattern)?.[1] || null; 170 | const isMovie = url.includes('/films/'); 171 | const isShow = url.includes('/series/'); 172 | const type = isMovie ? 'movie' : isShow ? 'tv' : 'unknown'; 173 | 174 | movieData.push({ 175 | id: id ?? '', 176 | year: result.year ?? 0, 177 | type, 178 | url, 179 | title: match[2] 180 | }); 181 | console.log(`[HDRezka] Found: id=${id}, title=${match[2]}, type=${type}, year=${result.year}`); 182 | } 183 | } 184 | 185 | // Filter by year if provided 186 | let filteredItems = movieData; 187 | if (media.releaseYear) { 188 | filteredItems = movieData.filter(item => item.year === media.releaseYear); 189 | console.log(`[HDRezka] Items filtered by year ${media.releaseYear}: ${filteredItems.length}`); 190 | } 191 | 192 | // Filter by type if provided 193 | if (media.type) { 194 | filteredItems = filteredItems.filter(item => item.type === media.type); 195 | console.log(`[HDRezka] Items filtered by type ${media.type}: ${filteredItems.length}`); 196 | } 197 | 198 | if (filteredItems.length === 0 && movieData.length > 0) { 199 | console.log(`[HDRezka] No exact match found, using first result: ${movieData[0].title}`); 200 | return movieData[0]; 201 | } 202 | 203 | if (filteredItems.length > 0) { 204 | console.log(`[HDRezka] Selected item: id=${filteredItems[0].id}, title=${filteredItems[0].title}`); 205 | return filteredItems[0]; 206 | } else { 207 | console.log(`[HDRezka] No matching items found`); 208 | return null; 209 | } 210 | }); 211 | } 212 | 213 | // Get translator ID from media page 214 | function getTranslatorId(url, id, media) { 215 | console.log(`[HDRezka] Getting translator ID for url=${url}, id=${id}`); 216 | 217 | // Make sure the URL is absolute 218 | const fullUrl = url.startsWith('http') ? url : `${REZKA_BASE}${url.startsWith('/') ? url.substring(1) : url}`; 219 | console.log(`[HDRezka] Making request to: ${fullUrl}`); 220 | 221 | return makeRequest(fullUrl).then(function (response) { 222 | return response.text(); 223 | }).then(function (responseText) { 224 | console.log(`[HDRezka] Translator page response length: ${responseText.length}`); 225 | 226 | // Translator ID 238 represents the Original + subtitles player. 227 | if (responseText.includes(`data-translator_id="238"`)) { 228 | console.log(`[HDRezka] Found translator ID 238 (Original + subtitles)`); 229 | return '238'; 230 | } 231 | 232 | const functionName = media.type === 'movie' ? 'initCDNMoviesEvents' : 'initCDNSeriesEvents'; 233 | const regexPattern = new RegExp(`sof\.tv\.${functionName}\\(${id}, ([^,]+)`, 'i'); 234 | const match = responseText.match(regexPattern); 235 | const translatorId = match ? match[1] : null; 236 | 237 | console.log(`[HDRezka] Extracted translator ID: ${translatorId}`); 238 | return translatorId; 239 | }); 240 | } 241 | 242 | // Get stream data from HDRezka 243 | function getStreamData(id, translatorId, media) { 244 | console.log(`[HDRezka] Getting stream for id=${id}, translatorId=${translatorId}`); 245 | 246 | const searchParams = new URLSearchParams(); 247 | searchParams.append('id', id); 248 | searchParams.append('translator_id', translatorId); 249 | 250 | if (media.type === 'tv') { 251 | searchParams.append('season', media.season.number.toString()); 252 | searchParams.append('episode', media.episode.number.toString()); 253 | console.log(`[HDRezka] TV params: season=${media.season.number}, episode=${media.episode.number}`); 254 | } 255 | 256 | const randomFavs = generateRandomFavs(); 257 | searchParams.append('favs', randomFavs); 258 | searchParams.append('action', media.type === 'tv' ? 'get_stream' : 'get_movie'); 259 | 260 | const fullUrl = `${REZKA_BASE}ajax/get_cdn_series/`; 261 | console.log(`[HDRezka] Making stream request with action=${media.type === 'tv' ? 'get_stream' : 'get_movie'}`); 262 | 263 | return makeRequest(fullUrl, { 264 | method: 'POST', 265 | body: searchParams, 266 | headers: { 267 | 'Content-Type': 'application/x-www-form-urlencoded' 268 | } 269 | }).then(function (response) { 270 | return response.text(); 271 | }).then(function (rawText) { 272 | console.log(`[HDRezka] Stream response length: ${rawText.length}`); 273 | 274 | try { 275 | const parsedResponse = JSON.parse(rawText); 276 | console.log(`[HDRezka] Parsed response successfully`); 277 | 278 | // Process video qualities and subtitles synchronously 279 | const qualities = parseVideoLinks(parsedResponse.url); 280 | const captions = parseSubtitles(parsedResponse.subtitle); 281 | 282 | return { qualities, captions }; 283 | } catch (e) { 284 | console.error(`[HDRezka] Failed to parse JSON response: ${e.message}`); 285 | console.log(`[HDRezka] Raw response: ${rawText.substring(0, 200)}...`); 286 | return null; 287 | } 288 | }); 289 | } 290 | 291 | // Get file size using HEAD request 292 | function getFileSize(url) { 293 | console.log(`[HDRezka] Getting file size for: ${url.substring(0, 60)}...`); 294 | 295 | return fetch(url, { 296 | method: 'HEAD', 297 | headers: { 298 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' 299 | } 300 | }).then(function (response) { 301 | if (response.ok) { 302 | const contentLength = response.headers.get('content-length'); 303 | if (contentLength) { 304 | const bytes = parseInt(contentLength, 10); 305 | const sizeFormatted = formatFileSize(bytes); 306 | console.log(`[HDRezka] File size: ${sizeFormatted}`); 307 | return sizeFormatted; 308 | } 309 | } 310 | 311 | console.log(`[HDRezka] Could not determine file size`); 312 | return null; 313 | }).catch(function (error) { 314 | console.log(`[HDRezka] Error getting file size: ${error.message}`); 315 | return null; 316 | }); 317 | } 318 | 319 | // Format file size in human readable format 320 | function formatFileSize(bytes) { 321 | if (bytes === 0) return '0 Bytes'; 322 | 323 | const k = 1024; 324 | const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; 325 | const i = Math.floor(Math.log(bytes) / Math.log(k)); 326 | 327 | return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; 328 | } 329 | 330 | // Parse quality for sorting 331 | function parseQualityForSort(qualityString) { 332 | if (!qualityString) return 0; 333 | const match = qualityString.match(/(\d{3,4})p/i); 334 | return match ? parseInt(match[1], 10) : 0; 335 | } 336 | 337 | // Main function to get streams for TMDB content 338 | function getStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) { 339 | console.log(`[HDRezka] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}${seasonNum ? `, S${seasonNum}E${episodeNum}` : ''}`); 340 | 341 | // Get TMDB info 342 | const tmdbUrl = `https://api.themoviedb.org/3/${mediaType === 'tv' ? 'tv' : 'movie'}/${tmdbId}?api_key=${TMDB_API_KEY}`; 343 | return makeRequest(tmdbUrl).then(function (tmdbResponse) { 344 | return tmdbResponse.json(); 345 | }).then(function (tmdbData) { 346 | const title = mediaType === 'tv' ? tmdbData.name : tmdbData.title; 347 | const year = mediaType === 'tv' ? tmdbData.first_air_date?.substring(0, 4) : tmdbData.release_date?.substring(0, 4); 348 | 349 | if (!title) { 350 | throw new Error('Could not extract title from TMDB response'); 351 | } 352 | 353 | console.log(`[HDRezka] TMDB Info: "${title}" (${year})`); 354 | 355 | // Create media object 356 | const media = { 357 | type: mediaType === 'tv' ? 'tv' : 'movie', 358 | title: title, 359 | releaseYear: year ? parseInt(year) : null 360 | }; 361 | 362 | // Add season/episode for TV shows 363 | if (mediaType === 'tv') { 364 | media.season = { number: seasonNum || 1 }; 365 | media.episode = { number: episodeNum || 1 }; 366 | } 367 | 368 | // Step 1: Search and find media ID 369 | return searchAndFindMediaId(media).then(function (searchResult) { 370 | if (!searchResult || !searchResult.id) { 371 | console.log('[HDRezka] No search result found'); 372 | return []; 373 | } 374 | 375 | // Step 2: Get translator ID 376 | return getTranslatorId(searchResult.url, searchResult.id, media).then(function (translatorId) { 377 | if (!translatorId) { 378 | console.log('[HDRezka] No translator ID found'); 379 | return []; 380 | } 381 | 382 | // Step 3: Get stream data 383 | return getStreamData(searchResult.id, translatorId, media).then(function (streamData) { 384 | if (!streamData || !streamData.qualities) { 385 | console.log('[HDRezka] No stream data found'); 386 | return []; 387 | } 388 | 389 | // Convert to Nuvio stream format with size detection 390 | const streamEntries = Object.entries(streamData.qualities); 391 | const streamPromises = streamEntries 392 | .filter(([quality, data]) => data.url && data.url !== 'null') 393 | .map(([quality, data]) => { 394 | const cleanQuality = quality.replace(/p.*$/, 'p'); // "1080p Ultra" -> "1080p" 395 | 396 | // Get file size using Promise chain 397 | return getFileSize(data.url).then(function (fileSize) { 398 | return { 399 | name: "HDRezka", 400 | title: `${title} ${year ? `(${year})` : ''} ${quality}${mediaType === 'tv' ? ` S${seasonNum}E${episodeNum}` : ''}`, 401 | url: data.url, 402 | quality: cleanQuality, 403 | size: fileSize, 404 | type: 'direct' 405 | }; 406 | }); 407 | }); 408 | 409 | return Promise.all(streamPromises).then(function (streams) { 410 | // Sort by quality (highest first) - optimized 411 | if (streams.length > 1) { 412 | streams.sort(function (a, b) { 413 | const qualityA = parseQualityForSort(a.quality); 414 | const qualityB = parseQualityForSort(b.quality); 415 | return qualityB - qualityA; 416 | }); 417 | } 418 | 419 | console.log(`[HDRezka] Successfully processed ${streams.length} streams`); 420 | return streams; 421 | }); 422 | }); 423 | }); 424 | }); 425 | }).catch(function (error) { 426 | console.error(`[HDRezka] Error in getStreams: ${error.message}`); 427 | return []; 428 | }); 429 | } 430 | 431 | // Export the main function 432 | if (typeof module !== 'undefined' && module.exports) { 433 | module.exports = { getStreams }; 434 | } else { 435 | // For React Native environment 436 | global.getStreams = getStreams; 437 | } -------------------------------------------------------------------------------- /providers/mapple.js: -------------------------------------------------------------------------------- 1 | // Mapple Scraper for Nuvio Local Scrapers 2 | // React Native compatible version - Promise-based approach 3 | 4 | // Constants 5 | const API_BASE = "https://enc-dec.app/api"; 6 | const MAPLE_BASE = "https://mapple.uk"; 7 | 8 | // Working headers for Mapple requests 9 | const WORKING_HEADERS = { 10 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", 11 | "Connection": "keep-alive", 12 | "Referer": "https://mapple.uk/", 13 | "Next-Action": "40c2896f5f22d9d6342e5a6d8f4d8c58d69654bacd" // Necessary header 14 | }; 15 | 16 | // Available sources 17 | const SOURCES = ["mapple", "sakura", "alfa", "oak", "wiggles"]; 18 | 19 | // Get session ID from enc-dec API 20 | function getSessionId() { 21 | console.log('[Mapple] Fetching session ID...'); 22 | 23 | return fetch(`${API_BASE}/enc-mapple`, { 24 | method: 'GET', 25 | headers: WORKING_HEADERS 26 | }).then(function(response) { 27 | if (!response.ok) { 28 | throw new Error(`Session ID API error: ${response.status} ${response.statusText}`); 29 | } 30 | return response.json(); 31 | }).then(function(data) { 32 | if (data && data.result && data.result.sessionId) { 33 | console.log('[Mapple] Successfully obtained session ID'); 34 | return data.result.sessionId; 35 | } else { 36 | throw new Error('Invalid session ID response format'); 37 | } 38 | }).catch(function(error) { 39 | console.error(`[Mapple] Failed to get session ID: ${error.message}`); 40 | throw error; 41 | }); 42 | } 43 | 44 | // Build payload for the request 45 | function buildPayload(tmdbId, mediaType, seasonNum, episodeNum, source, sessionId) { 46 | const payload = [{ 47 | mediaId: tmdbId, 48 | mediaType: mediaType, 49 | tv_slug: mediaType === 'tv' ? `${seasonNum}-${episodeNum}` : "", 50 | source: source, 51 | sessionId: sessionId 52 | }]; 53 | 54 | return payload; 55 | } 56 | 57 | // Parse the response from Mapple API 58 | function parseMappleResponse(responseText) { 59 | try { 60 | // The response seems to be JSONP format, take second line and remove "1:" prefix 61 | const lines = responseText.split('\n'); 62 | if (lines.length < 2) { 63 | throw new Error('Invalid response format - not enough lines'); 64 | } 65 | 66 | const dataLine = lines[1].replace(/^1:/, ''); 67 | const streamsData = JSON.parse(dataLine); 68 | 69 | return streamsData; 70 | } catch (error) { 71 | console.error(`[Mapple] Failed to parse response: ${error.message}`); 72 | throw error; 73 | } 74 | } 75 | 76 | // M3U8 Parsing Functions (inlined for React Native compatibility) 77 | 78 | // Parse M3U8 content and extract quality streams 79 | function parseM3U8(content) { 80 | const lines = content.split('\n').map(line => line.trim()).filter(line => line); 81 | const streams = []; 82 | 83 | let currentStream = null; 84 | 85 | for (let i = 0; i < lines.length; i++) { 86 | const line = lines[i]; 87 | 88 | if (line.startsWith('#EXT-X-STREAM-INF:')) { 89 | // Parse stream info 90 | currentStream = { 91 | bandwidth: null, 92 | resolution: null, 93 | codecs: null, 94 | url: null 95 | }; 96 | 97 | // Extract bandwidth 98 | const bandwidthMatch = line.match(/BANDWIDTH=(\d+)/); 99 | if (bandwidthMatch) { 100 | currentStream.bandwidth = parseInt(bandwidthMatch[1]); 101 | } 102 | 103 | // Extract resolution 104 | const resolutionMatch = line.match(/RESOLUTION=(\d+x\d+)/); 105 | if (resolutionMatch) { 106 | currentStream.resolution = resolutionMatch[1]; 107 | } 108 | 109 | // Extract codecs 110 | const codecsMatch = line.match(/CODECS="([^"]+)"/); 111 | if (codecsMatch) { 112 | currentStream.codecs = codecsMatch[1]; 113 | } 114 | 115 | } else if (currentStream && !line.startsWith('#')) { 116 | // This is the URL for the current stream 117 | currentStream.url = line; 118 | streams.push(currentStream); 119 | currentStream = null; 120 | } 121 | } 122 | 123 | return streams; 124 | } 125 | 126 | // Determine quality from resolution or bandwidth 127 | function getQualityFromStream(stream) { 128 | if (stream.resolution) { 129 | const [width, height] = stream.resolution.split('x').map(Number); 130 | 131 | if (height >= 2160) return '4K'; 132 | if (height >= 1440) return '1440p'; 133 | if (height >= 1080) return '1080p'; 134 | if (height >= 720) return '720p'; 135 | if (height >= 480) return '480p'; 136 | if (height >= 360) return '360p'; 137 | return '240p'; 138 | } 139 | 140 | if (stream.bandwidth) { 141 | const mbps = stream.bandwidth / 1000000; 142 | 143 | if (mbps >= 15) return '4K'; 144 | if (mbps >= 8) return '1440p'; 145 | if (mbps >= 5) return '1080p'; 146 | if (mbps >= 3) return '720p'; 147 | if (mbps >= 1.5) return '480p'; 148 | if (mbps >= 0.8) return '360p'; 149 | return '240p'; 150 | } 151 | 152 | return 'Unknown'; 153 | } 154 | 155 | // Fetch and parse M3U8 playlist 156 | function resolveM3U8(url, sourceName) { 157 | console.log(`[Mapple] Resolving M3U8 playlist for ${sourceName}...`); 158 | 159 | // Special handling for Sakura - return master URL directly with "Auto" quality 160 | if (sourceName === 'sakura') { 161 | console.log(`[Mapple] Sakura source detected - returning master URL with Auto quality`); 162 | const capitalizedSource = sourceName.charAt(0).toUpperCase() + sourceName.slice(1); 163 | return Promise.resolve([{ 164 | name: `Mapple ${capitalizedSource} - Auto`, 165 | title: "", // Will be filled by caller 166 | url: url, 167 | quality: 'Auto', 168 | size: "Unknown", 169 | headers: WORKING_HEADERS, 170 | provider: "mapple" 171 | }]); 172 | } 173 | 174 | return fetch(url, { 175 | method: 'GET', 176 | headers: WORKING_HEADERS 177 | }).then(function(response) { 178 | if (!response.ok) { 179 | throw new Error(`HTTP ${response.status}: ${response.statusText}`); 180 | } 181 | 182 | return response.text().then(function(content) { 183 | console.log(`[Mapple] Fetched M3U8 content (${content.length} bytes) for ${sourceName}`); 184 | 185 | // Check if it's a master playlist (contains #EXT-X-STREAM-INF) 186 | if (content.includes('#EXT-X-STREAM-INF:')) { 187 | console.log(`[Mapple] Master playlist detected for ${sourceName} - parsing quality streams...`); 188 | 189 | const streams = parseM3U8(content); 190 | console.log(`[Mapple] Found ${streams.length} quality streams for ${sourceName}`); 191 | 192 | const resolvedStreams = []; 193 | 194 | for (const stream of streams) { 195 | const quality = getQualityFromStream(stream); 196 | const capitalizedSource = sourceName.charAt(0).toUpperCase() + sourceName.slice(1); 197 | 198 | resolvedStreams.push({ 199 | name: `Mapple ${capitalizedSource} - ${quality}`, 200 | title: "", // Will be filled by caller 201 | url: stream.url, 202 | quality: quality, 203 | size: "Unknown", 204 | headers: WORKING_HEADERS, 205 | provider: "mapple", 206 | resolution: stream.resolution, 207 | bandwidth: stream.bandwidth, 208 | codecs: stream.codecs 209 | }); 210 | } 211 | 212 | // Sort by quality (highest first) 213 | resolvedStreams.sort(function(a, b) { 214 | const qualityOrder = { 'Auto': 5, '4K': 4, '1440p': 3, '1080p': 2, '720p': 1, '480p': 0, '360p': -1, '240p': -2, 'Unknown': -3 }; 215 | return (qualityOrder[b.quality] || -3) - (qualityOrder[a.quality] || -3); 216 | }); 217 | 218 | return resolvedStreams; 219 | 220 | } else if (content.includes('#EXTINF:')) { 221 | console.log(`[Mapple] Media playlist detected for ${sourceName} - single quality stream`); 222 | 223 | const capitalizedSource = sourceName.charAt(0).toUpperCase() + sourceName.slice(1); 224 | return [{ 225 | name: `Mapple ${capitalizedSource} - Unknown`, 226 | title: "", // Will be filled by caller 227 | url: url, 228 | quality: 'Unknown', 229 | size: "Unknown", 230 | headers: WORKING_HEADERS, 231 | provider: "mapple" 232 | }]; 233 | 234 | } else { 235 | console.log(`[Mapple] Invalid M3U8 content for ${sourceName} - returning master URL`); 236 | const capitalizedSource = sourceName.charAt(0).toUpperCase() + sourceName.slice(1); 237 | return [{ 238 | name: `Mapple ${capitalizedSource} - Unknown`, 239 | title: "", // Will be filled by caller 240 | url: url, 241 | quality: 'Unknown', 242 | size: "Unknown", 243 | headers: WORKING_HEADERS, 244 | provider: "mapple" 245 | }]; 246 | } 247 | }); 248 | }).catch(function(error) { 249 | console.error(`[Mapple] Failed to resolve M3U8 for ${sourceName}: ${error.message}`); 250 | 251 | // Return the original URL if M3U8 parsing fails 252 | const capitalizedSource = sourceName.charAt(0).toUpperCase() + sourceName.slice(1); 253 | return [{ 254 | name: `Mapple ${capitalizedSource} - Unknown`, 255 | title: "", // Will be filled by caller 256 | url: url, 257 | quality: 'Unknown', 258 | size: "Unknown", 259 | headers: WORKING_HEADERS, 260 | provider: "mapple" 261 | }]; 262 | }); 263 | } 264 | 265 | // Extract streams from parsed data 266 | function extractStreams(streamsData, source, mediaType, seasonNum, episodeNum) { 267 | const streams = []; 268 | 269 | try { 270 | // Check if the response was successful 271 | if (!streamsData.success) { 272 | console.log(`[Mapple] Source ${source} returned error: ${streamsData.error || 'Unknown error'}`); 273 | return streams; 274 | } 275 | 276 | // Check if we have stream data 277 | if (!streamsData.data || !streamsData.data.stream_url) { 278 | console.log(`[Mapple] Source ${source} has no stream URL in response`); 279 | return streams; 280 | } 281 | 282 | const streamUrl = streamsData.data.stream_url.trim(); 283 | 284 | // Skip error messages 285 | if (streamUrl.includes('Content not found in streaming databases')) { 286 | console.log(`[Mapple] Source ${source} returned 'content not found' message`); 287 | return streams; 288 | } 289 | 290 | console.log(`[Mapple] Found master URL for ${source}: ${streamUrl.substring(0, 60)}...`); 291 | 292 | // For now, return the master URL - we'll resolve it later in the main function 293 | const capitalizedSource = source.charAt(0).toUpperCase() + source.slice(1); 294 | streams.push({ 295 | name: `Mapple ${capitalizedSource} - Master`, 296 | title: "", // Will be filled by caller 297 | url: streamUrl, 298 | quality: 'Master', 299 | size: "Unknown", 300 | headers: WORKING_HEADERS, 301 | provider: "mapple", 302 | source: source 303 | }); 304 | 305 | } catch (error) { 306 | console.error(`[Mapple] Error extracting streams: ${error.message}`); 307 | } 308 | 309 | return streams; 310 | } 311 | 312 | // Fetch streams for a specific source 313 | function fetchStreamsForSource(tmdbId, mediaType, seasonNum, episodeNum, source, sessionId) { 314 | console.log(`[Mapple] Fetching streams for source: ${source}`); 315 | 316 | const payload = buildPayload(tmdbId, mediaType, seasonNum, episodeNum, source, sessionId); 317 | 318 | // Build URL 319 | let url; 320 | if (mediaType === 'tv') { 321 | url = `${MAPLE_BASE}/watch/tv/${tmdbId}/${seasonNum}-${episodeNum}`; 322 | } else { 323 | url = `${MAPLE_BASE}/watch/movie/${tmdbId}`; 324 | } 325 | 326 | console.log(`[Mapple] Making request to: ${url}`); 327 | 328 | return fetch(url, { 329 | method: 'POST', 330 | headers: { 331 | ...WORKING_HEADERS, 332 | 'Content-Type': 'application/json' 333 | }, 334 | body: JSON.stringify(payload) 335 | }).then(function(response) { 336 | if (!response.ok) { 337 | throw new Error(`Mapple API error: ${response.status} ${response.statusText}`); 338 | } 339 | return response.text(); 340 | }).then(function(responseText) { 341 | const streamsData = parseMappleResponse(responseText); 342 | const masterStreams = extractStreams(streamsData, source, mediaType, seasonNum, episodeNum); 343 | 344 | if (masterStreams.length === 0) { 345 | return []; 346 | } 347 | 348 | // Resolve M3U8 playlists to get individual quality streams (PARALLEL within source) 349 | const resolvePromises = masterStreams.map(function(masterStream) { 350 | return resolveM3U8(masterStream.url, source); 351 | }); 352 | 353 | return Promise.all(resolvePromises).then(function(resolvedStreamArrays) { 354 | // Flatten all resolved streams 355 | const allStreams = []; 356 | resolvedStreamArrays.forEach(function(streamArray) { 357 | allStreams.push(...streamArray); 358 | }); 359 | return allStreams; 360 | }); 361 | }).catch(function(error) { 362 | console.error(`[Mapple] Error fetching streams for source ${source}: ${error.message}`); 363 | return []; 364 | }); 365 | } 366 | 367 | // Get TMDB details for title formatting 368 | function getTMDBDetails(tmdbId, mediaType) { 369 | const TMDB_API_KEY = '439c478a771f35c05022f9feabcca01c'; 370 | const TMDB_BASE_URL = 'https://api.themoviedb.org/3'; 371 | const endpoint = mediaType === 'tv' ? 'tv' : 'movie'; 372 | const url = `${TMDB_BASE_URL}/${endpoint}/${tmdbId}?api_key=${TMDB_API_KEY}`; 373 | 374 | console.log(`[Mapple] Fetching TMDB details for ${mediaType} ID: ${tmdbId}`); 375 | 376 | return fetch(url, { 377 | method: 'GET', 378 | headers: { 379 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' 380 | } 381 | }).then(function(response) { 382 | if (!response.ok) { 383 | throw new Error(`TMDB API error: ${response.status}`); 384 | } 385 | return response.json(); 386 | }).then(function(data) { 387 | const title = mediaType === 'tv' ? data.name : data.title; 388 | const releaseDate = mediaType === 'tv' ? data.first_air_date : data.release_date; 389 | const year = releaseDate ? parseInt(releaseDate.split('-')[0]) : null; 390 | 391 | return { 392 | title: title, 393 | year: year 394 | }; 395 | }).catch(function(error) { 396 | console.error(`[Mapple] Failed to get TMDB details: ${error.message}`); 397 | return { title: 'Unknown Title', year: null }; 398 | }); 399 | } 400 | 401 | // Main scraping function - Promise-based approach 402 | function getStreams(tmdbId, mediaType, seasonNum, episodeNum) { 403 | console.log(`[Mapple] Starting scrape for TMDB ID: ${tmdbId}, Type: ${mediaType}${mediaType === 'tv' ? `, S:${seasonNum}E:${episodeNum}` : ''}`); 404 | 405 | // Validate parameters 406 | if (!tmdbId || !mediaType) { 407 | console.error('[Mapple] Missing required parameters: tmdbId and mediaType'); 408 | return Promise.resolve([]); 409 | } 410 | 411 | if (mediaType === 'tv' && (!seasonNum || !episodeNum)) { 412 | console.error('[Mapple] TV shows require seasonNum and episodeNum'); 413 | return Promise.resolve([]); 414 | } 415 | 416 | // Get TMDB details first 417 | return getTMDBDetails(tmdbId, mediaType).then(function(mediaInfo) { 418 | console.log(`[Mapple] TMDB Info: "${mediaInfo.title}" (${mediaInfo.year || 'N/A'})`); 419 | 420 | // Format title for streams 421 | let titleWithYear = mediaInfo.title; 422 | if (mediaInfo.year) { 423 | titleWithYear += ` (${mediaInfo.year})`; 424 | } 425 | if (mediaType === 'tv' && seasonNum && episodeNum) { 426 | titleWithYear = `${mediaInfo.title} S${String(seasonNum).padStart(2, '0')}E${String(episodeNum).padStart(2, '0')}`; 427 | } 428 | 429 | // Get session ID once for all sources (PARALLEL OPTIMIZATION) 430 | console.log('[Mapple] Getting shared session ID for all sources...'); 431 | return getSessionId().then(function(sharedSessionId) { 432 | console.log('[Mapple] Shared session ID obtained, processing sources in parallel...'); 433 | 434 | // Try multiple sources in parallel with shared session 435 | const sourcePromises = SOURCES.map(function(source) { 436 | return fetchStreamsForSource(tmdbId, mediaType, seasonNum, episodeNum, source, sharedSessionId); 437 | }); 438 | 439 | return Promise.allSettled(sourcePromises).then(function(results) { 440 | const allStreams = []; 441 | 442 | results.forEach(function(result, index) { 443 | if (result.status === 'fulfilled') { 444 | const streams = result.value; 445 | console.log(`[Mapple] Source ${SOURCES[index]} returned ${streams.length} streams`); 446 | 447 | // Add title to each stream 448 | streams.forEach(function(stream) { 449 | stream.title = titleWithYear; 450 | }); 451 | 452 | allStreams.push(...streams); 453 | } else { 454 | console.error(`[Mapple] Source ${SOURCES[index]} failed: ${result.reason.message}`); 455 | } 456 | }); 457 | 458 | // Sort streams by quality (highest first) 459 | const qualityOrder = ['Auto', '4K', '1440p', '1080p', '720p', '480p', '360p', '240p', 'Unknown']; 460 | allStreams.sort(function(a, b) { 461 | const aIndex = qualityOrder.indexOf(a.quality); 462 | const bIndex = qualityOrder.indexOf(b.quality); 463 | return aIndex - bIndex; 464 | }); 465 | 466 | console.log(`[Mapple] Total streams found: ${allStreams.length}`); 467 | return allStreams; 468 | }); 469 | }); 470 | }).catch(function(error) { 471 | console.error(`[Mapple] Scraping error: ${error.message}`); 472 | return []; 473 | }); 474 | } 475 | 476 | // Export for React Native compatibility 477 | if (typeof module !== 'undefined' && module.exports) { 478 | module.exports = { getStreams }; 479 | } else { 480 | global.getStreams = getStreams; 481 | } 482 | -------------------------------------------------------------------------------- /providers/vidnest.js: -------------------------------------------------------------------------------- 1 | // Vidnest Scraper for Nuvio Local Scrapers 2 | // React Native compatible version - Promise-based approach only 3 | // Extracts streaming links using TMDB ID for Vidnest servers with AES-GCM decryption 4 | 5 | // TMDB API Configuration 6 | const TMDB_API_KEY = '439c478a771f35c05022f9feabcca01c'; 7 | const TMDB_BASE_URL = 'https://api.themoviedb.org/3'; 8 | 9 | // Vidnest Configuration 10 | const VIDNEST_BASE_URL = 'https://backend.vidnest.fun'; 11 | const PASSPHRASE = 'T8c8PQlSQVU4mBuW4CbE/g57VBbM5009QHd+ym93aZZ5pEeVpToY6OdpYPvRMVYp'; 12 | const SERVERS = ['allmovies', 'hollymoviehd']; 13 | 14 | // Working headers for Vidnest API 15 | const WORKING_HEADERS = { 16 | 'User-Agent': 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Mobile Safari/537.36', 17 | 'Accept': 'application/json, text/plain, */*', 18 | 'Accept-Language': 'en-US,en;q=0.9', 19 | 'Accept-Encoding': 'gzip, deflate, br', 20 | 'Referer': 'https://vidnest.fun/', 21 | 'Origin': 'https://vidnest.fun', 22 | 'DNT': '1' 23 | }; 24 | 25 | // Headers for stream playback (separate from API headers) 26 | const PLAYBACK_HEADERS = { 27 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36', 28 | 'Accept': 'video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5', 29 | 'Accept-Language': 'en-US,en;q=0.9', 30 | 'Accept-Encoding': 'identity', 31 | 'Connection': 'keep-alive', 32 | 'Sec-Fetch-Dest': 'video', 33 | 'Sec-Fetch-Mode': 'no-cors', 34 | 'Sec-Fetch-Site': 'cross-site', 35 | 'DNT': '1' 36 | }; 37 | 38 | // React Native-safe Base64 utilities (no Buffer dependency) 39 | const BASE64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; 40 | 41 | function base64ToBytes(base64) { 42 | if (!base64) return new Uint8Array(0); 43 | 44 | // Remove padding 45 | let input = String(base64).replace(/=+$/, ''); 46 | let output = ''; 47 | let bc = 0, bs, buffer, idx = 0; 48 | 49 | while ((buffer = input.charAt(idx++))) { 50 | buffer = BASE64_CHARS.indexOf(buffer); 51 | if (~buffer) { 52 | bs = bc % 4 ? bs * 64 + buffer : buffer; 53 | if (bc++ % 4) { 54 | output += String.fromCharCode(255 & (bs >> ((-2 * bc) & 6))); 55 | } 56 | } 57 | } 58 | 59 | // Convert string to bytes 60 | const bytes = new Uint8Array(output.length); 61 | for (let i = 0; i < output.length; i++) { 62 | bytes[i] = output.charCodeAt(i); 63 | } 64 | return bytes; 65 | } 66 | 67 | function bytesToBase64(bytes) { 68 | if (!bytes || bytes.length === 0) return ''; 69 | 70 | let output = ''; 71 | let i = 0; 72 | const len = bytes.length; 73 | 74 | while (i < len) { 75 | const a = bytes[i++]; 76 | const b = i < len ? bytes[i++] : 0; 77 | const c = i < len ? bytes[i++] : 0; 78 | 79 | const bitmap = (a << 16) | (b << 8) | c; 80 | 81 | output += BASE64_CHARS.charAt((bitmap >> 18) & 63); 82 | output += BASE64_CHARS.charAt((bitmap >> 12) & 63); 83 | output += i - 2 < len ? BASE64_CHARS.charAt((bitmap >> 6) & 63) : '='; 84 | output += i - 1 < len ? BASE64_CHARS.charAt(bitmap & 63) : '='; 85 | } 86 | 87 | return output; 88 | } 89 | 90 | // Node.js compatible atob function 91 | function atob(str) { 92 | return base64ToBytes(str).map(byte => String.fromCharCode(byte)).join(''); 93 | } 94 | 95 | // AES-GCM Decryption using server (React Native compatible) 96 | function decryptAesGcm(encryptedB64, passphraseB64) { 97 | console.log('[Vidnest] Starting AES-GCM decryption via server...'); 98 | 99 | return fetch('https://aesdec.nuvioapp.space/decrypt', { 100 | method: 'POST', 101 | headers: { 'Content-Type': 'application/json' }, 102 | body: JSON.stringify({ 103 | encryptedData: encryptedB64, 104 | passphrase: passphraseB64 105 | }) 106 | }) 107 | .then(response => response.json()) 108 | .then(data => { 109 | if (data.error) throw new Error(data.error); 110 | console.log('[Vidnest] Server decryption successful'); 111 | return data.decrypted; 112 | }) 113 | .catch(error => { 114 | console.error(`[Vidnest] Server decryption failed: ${error.message}`); 115 | throw error; 116 | }); 117 | } 118 | 119 | // Validate stream URL accessibility 120 | function validateStreamUrl(url, headers) { 121 | console.log(`[Vidnest] Validating stream URL: ${url.substring(0, 60)}...`); 122 | 123 | return fetch(url, { 124 | method: 'HEAD', 125 | headers: headers, 126 | timeout: 5000 127 | }) 128 | .then(response => { 129 | // Accept 200 OK, 206 Partial Content, or 302 redirects 130 | const isValid = response.ok || response.status === 206 || response.status === 302; 131 | console.log(`[Vidnest] URL validation result: ${response.status} - ${isValid ? 'VALID' : 'INVALID'}`); 132 | return isValid; 133 | }) 134 | .catch(error => { 135 | console.log(`[Vidnest] URL validation failed: ${error.message}`); 136 | return false; 137 | }); 138 | } 139 | 140 | // Helper function to make HTTP requests 141 | function makeRequest(url, options = {}) { 142 | const defaultHeaders = { ...WORKING_HEADERS }; 143 | 144 | return fetch(url, { 145 | method: options.method || 'GET', 146 | headers: { ...defaultHeaders, ...options.headers }, 147 | ...options 148 | }).then(function(response) { 149 | if (!response.ok) { 150 | throw new Error(`HTTP ${response.status}: ${response.statusText}`); 151 | } 152 | return response; 153 | }).catch(function(error) { 154 | console.error(`[Vidnest] Request failed for ${url}: ${error.message}`); 155 | throw error; 156 | }); 157 | } 158 | 159 | // Get movie/TV show details from TMDB 160 | function getTMDBDetails(tmdbId, mediaType) { 161 | const endpoint = mediaType === 'tv' ? 'tv' : 'movie'; 162 | const url = `${TMDB_BASE_URL}/${endpoint}/${tmdbId}?api_key=${TMDB_API_KEY}&append_to_response=external_ids`; 163 | 164 | return makeRequest(url) 165 | .then(function(response) { 166 | return response.json(); 167 | }) 168 | .then(function(data) { 169 | const title = mediaType === 'tv' ? data.name : data.title; 170 | const releaseDate = mediaType === 'tv' ? data.first_air_date : data.release_date; 171 | const year = releaseDate ? parseInt(releaseDate.split('-')[0]) : null; 172 | 173 | return { 174 | title: title, 175 | year: year, 176 | imdbId: data.external_ids?.imdb_id || null 177 | }; 178 | }); 179 | } 180 | 181 | // Extract quality from URL or response 182 | function extractQuality(url) { 183 | if (!url) return 'Unknown'; 184 | 185 | // Try to extract quality from URL patterns 186 | const qualityPatterns = [ 187 | /(\d{3,4})p/i, // 1080p, 720p, etc. 188 | /(\d{3,4})k/i, // 1080k, 720k, etc. 189 | /quality[_-]?(\d{3,4})/i, // quality-1080, quality_720, etc. 190 | /res[_-]?(\d{3,4})/i, // res-1080, res_720, etc. 191 | /(\d{3,4})x\d{3,4}/i, // 1920x1080, 1280x720, etc. 192 | ]; 193 | 194 | for (const pattern of qualityPatterns) { 195 | const match = url.match(pattern); 196 | if (match) { 197 | const qualityNum = parseInt(match[1]); 198 | if (qualityNum >= 240 && qualityNum <= 4320) { 199 | return `${qualityNum}p`; 200 | } 201 | } 202 | } 203 | 204 | // Additional quality detection based on URL patterns 205 | if (url.includes('1080') || url.includes('1920')) return '1080p'; 206 | if (url.includes('720') || url.includes('1280')) return '720p'; 207 | if (url.includes('480') || url.includes('854')) return '480p'; 208 | if (url.includes('360') || url.includes('640')) return '360p'; 209 | if (url.includes('240') || url.includes('426')) return '240p'; 210 | 211 | return 'Unknown'; 212 | } 213 | 214 | // Process Vidnest API response 215 | function processVidnestResponse(data, serverName, mediaInfo, seasonNum, episodeNum) { 216 | const streams = []; 217 | 218 | try { 219 | console.log(`[Vidnest] Processing response from ${serverName}:`, JSON.stringify(data, null, 2)); 220 | 221 | // Check if response has success field and streams/sources 222 | if (!data.success && !data.streams && !data.sources) { 223 | console.log(`[Vidnest] ${serverName}: No valid streams found in response`); 224 | return streams; 225 | } 226 | 227 | // Extract sources or streams array 228 | const sources = data.sources || data.streams || []; 229 | 230 | if (!Array.isArray(sources) || sources.length === 0) { 231 | console.log(`[Vidnest] ${serverName}: No sources/streams array found`); 232 | return streams; 233 | } 234 | 235 | // Process each source 236 | sources.forEach((source, index) => { 237 | if (!source) return; 238 | 239 | // Extract video URL from various possible fields 240 | const videoUrl = source.file || source.url || source.src || source.link; 241 | 242 | if (!videoUrl) { 243 | console.log(`[Vidnest] ${serverName}: Source ${index} has no video URL`); 244 | return; 245 | } 246 | 247 | // Extract quality 248 | let quality = extractQuality(videoUrl); 249 | 250 | // Extract language information 251 | let languageInfo = ''; 252 | if (source.language) { 253 | languageInfo = ` [${source.language}]`; 254 | } 255 | 256 | // Extract label information for better naming 257 | let labelInfo = ''; 258 | if (source.label) { 259 | labelInfo = ` - ${source.label}`; 260 | } 261 | 262 | // Determine stream type 263 | let streamType = 'Unknown'; 264 | if (source.type === 'hls' || videoUrl.includes('.m3u8')) { 265 | streamType = 'HLS'; 266 | if (quality === 'Unknown') { 267 | quality = 'Adaptive'; // HLS streams are usually adaptive 268 | } 269 | } else if (videoUrl.includes('.mp4')) { 270 | streamType = 'MP4'; 271 | } else if (videoUrl.includes('.mkv')) { 272 | streamType = 'MKV'; 273 | } 274 | 275 | // Create media title 276 | let mediaTitle = mediaInfo.title || 'Unknown'; 277 | if (mediaInfo.year) { 278 | mediaTitle += ` (${mediaInfo.year})`; 279 | } 280 | if (seasonNum && episodeNum) { 281 | mediaTitle = `${mediaInfo.title} S${String(seasonNum).padStart(2, '0')}E${String(episodeNum).padStart(2, '0')}`; 282 | } 283 | 284 | streams.push({ 285 | name: `Vidnest ${serverName.charAt(0).toUpperCase() + serverName.slice(1)}${labelInfo}${languageInfo} - ${quality}`, 286 | title: mediaTitle, 287 | url: videoUrl, 288 | quality: quality, 289 | size: 'Unknown', 290 | provider: 'vidnest' 291 | }); 292 | 293 | console.log(`[Vidnest] ${serverName}: Added ${quality}${languageInfo} stream: ${videoUrl.substring(0, 60)}...`); 294 | }); 295 | 296 | } catch (error) { 297 | console.error(`[Vidnest] Error processing ${serverName} response: ${error.message}`); 298 | } 299 | 300 | return streams; 301 | } 302 | 303 | // Fetch streams from a single server 304 | function fetchFromServer(serverName, mediaType, tmdbId, mediaInfo, seasonNum, episodeNum) { 305 | console.log(`[Vidnest] Fetching from ${serverName}...`); 306 | 307 | // Build API URL 308 | let apiUrl; 309 | if (mediaType === 'tv' && seasonNum && episodeNum) { 310 | apiUrl = `${VIDNEST_BASE_URL}/${serverName}/${mediaType}/${tmdbId}/${seasonNum}/${episodeNum}`; 311 | } else { 312 | apiUrl = `${VIDNEST_BASE_URL}/${serverName}/${mediaType}/${tmdbId}`; 313 | } 314 | 315 | console.log(`[Vidnest] ${serverName} API URL: ${apiUrl}`); 316 | 317 | return makeRequest(apiUrl) 318 | .then(function(response) { 319 | return response.text(); 320 | }) 321 | .then(function(responseText) { 322 | console.log(`[Vidnest] ${serverName} response length: ${responseText.length} characters`); 323 | 324 | // Try to parse as JSON first 325 | try { 326 | const data = JSON.parse(responseText); 327 | 328 | // Check if response contains encrypted data 329 | if (data.encrypted && data.data) { 330 | console.log(`[Vidnest] ${serverName}: Detected encrypted response, decrypting...`); 331 | 332 | return decryptAesGcm(data.data, PASSPHRASE) 333 | .then(function(decryptedText) { 334 | console.log(`[Vidnest] ${serverName}: Decryption successful`); 335 | 336 | try { 337 | const decryptedData = JSON.parse(decryptedText); 338 | return processVidnestResponse(decryptedData, serverName, mediaInfo, seasonNum, episodeNum); 339 | } catch (parseError) { 340 | console.error(`[Vidnest] ${serverName}: JSON parse error after decryption: ${parseError.message}`); 341 | return []; 342 | } 343 | }); 344 | } else { 345 | // Process non-encrypted response 346 | return processVidnestResponse(data, serverName, mediaInfo, seasonNum, episodeNum); 347 | } 348 | } catch (parseError) { 349 | console.error(`[Vidnest] ${serverName}: Invalid JSON response: ${parseError.message}`); 350 | return []; 351 | } 352 | }) 353 | .catch(function(error) { 354 | console.error(`[Vidnest] ${serverName} error: ${error.message}`); 355 | return []; 356 | }); 357 | } 358 | 359 | // Main function to extract streaming links for Nuvio 360 | function getStreams(tmdbId, mediaType, seasonNum, episodeNum) { 361 | console.log(`[Vidnest] Starting extraction for TMDB ID: ${tmdbId}, Type: ${mediaType}${mediaType === 'tv' ? `, S:${seasonNum}E:${episodeNum}` : ''}`); 362 | 363 | return new Promise((resolve, reject) => { 364 | // First, fetch media details from TMDB 365 | getTMDBDetails(tmdbId, mediaType) 366 | .then(function(mediaInfo) { 367 | console.log(`[Vidnest] TMDB Info: "${mediaInfo.title}" (${mediaInfo.year || 'N/A'})`); 368 | 369 | // Process both servers in parallel 370 | const serverPromises = SERVERS.map(serverName => { 371 | return fetchFromServer(serverName, mediaType, tmdbId, mediaInfo, seasonNum, episodeNum); 372 | }); 373 | 374 | return Promise.all(serverPromises) 375 | .then(function(results) { 376 | // Combine all streams from all servers 377 | const allStreams = []; 378 | results.forEach(streams => { 379 | allStreams.push(...streams); 380 | }); 381 | 382 | // Remove duplicate streams by URL 383 | const uniqueStreams = []; 384 | const seenUrls = new Set(); 385 | allStreams.forEach(stream => { 386 | if (!seenUrls.has(stream.url)) { 387 | seenUrls.add(stream.url); 388 | uniqueStreams.push(stream); 389 | } 390 | }); 391 | 392 | // Validate all streams in parallel 393 | console.log(`[Vidnest] Validating ${uniqueStreams.length} streams...`); 394 | const validationPromises = uniqueStreams.map(stream => 395 | validateStreamUrl(stream.url, PLAYBACK_HEADERS) 396 | .then(isValid => ({ stream, isValid })) 397 | ); 398 | 399 | return Promise.all(validationPromises) 400 | .then(function(results) { 401 | const validStreams = results 402 | .filter(r => r.isValid) 403 | .map(r => r.stream); 404 | 405 | console.log(`[Vidnest] Filtered ${uniqueStreams.length - validStreams.length} broken links`); 406 | 407 | // Sort streams by quality (highest first) 408 | const getQualityValue = (quality) => { 409 | const q = quality.toLowerCase().replace(/p$/, ''); // Remove trailing 'p' 410 | 411 | // Handle specific quality names 412 | if (q === '4k' || q === '2160') return 2160; 413 | if (q === '1440') return 1440; 414 | if (q === '1080') return 1080; 415 | if (q === '720') return 720; 416 | if (q === '480') return 480; 417 | if (q === '360') return 360; 418 | if (q === '240') return 240; 419 | 420 | // Handle unknown quality (put at end) 421 | if (q === 'unknown') return 0; 422 | 423 | // Try to parse as number 424 | const numQuality = parseInt(q); 425 | if (!isNaN(numQuality) && numQuality > 0) { 426 | return numQuality; 427 | } 428 | 429 | // Default for unrecognized qualities 430 | return 1; 431 | }; 432 | 433 | validStreams.sort((a, b) => { 434 | const qualityA = getQualityValue(a.quality); 435 | const qualityB = getQualityValue(b.quality); 436 | return qualityB - qualityA; 437 | }); 438 | 439 | console.log(`[Vidnest] Total valid streams found: ${validStreams.length}`); 440 | resolve(validStreams); 441 | }); 442 | }); 443 | }) 444 | .catch(function(error) { 445 | console.error(`[Vidnest] Error fetching media details: ${error.message}`); 446 | resolve([]); // Return empty array on error for Nuvio compatibility 447 | }); 448 | }); 449 | } 450 | 451 | // Export for React Native compatibility 452 | if (typeof module !== 'undefined' && module.exports) { 453 | module.exports = { getStreams }; 454 | } else { 455 | global.getStreams = getStreams; 456 | } 457 | -------------------------------------------------------------------------------- /providers/myflixer-extractor.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const axios = require('axios'); 4 | const cheerio = require('cheerio'); 5 | const { URL } = require('url'); 6 | 7 | class MyFlixerExtractor { 8 | constructor() { 9 | this.mainUrl = 'https://watch32.sx'; 10 | this.videostrUrl = 'https://videostr.net'; 11 | } 12 | 13 | async search(query) { 14 | try { 15 | const searchUrl = `${this.mainUrl}/search/${query.replace(/\s+/g, '-')}`; 16 | console.log(`Searching: ${searchUrl}`); 17 | 18 | const response = await axios.get(searchUrl); 19 | const $ = cheerio.load(response.data); 20 | 21 | const results = []; 22 | $('.flw-item').each((i, element) => { 23 | const title = $(element).find('h2.film-name > a').attr('title'); 24 | const link = $(element).find('h2.film-name > a').attr('href'); 25 | const poster = $(element).find('img.film-poster-img').attr('data-src'); 26 | 27 | if (title && link) { 28 | results.push({ 29 | title, 30 | url: link.startsWith('http') ? link : `${this.mainUrl}${link}`, 31 | poster 32 | }); 33 | } 34 | }); 35 | 36 | console.log('Search results found:'); 37 | results.forEach((result, index) => { 38 | console.log(`${index + 1}. ${result.title}`); 39 | }); 40 | 41 | return results; 42 | } catch (error) { 43 | console.error('Search error:', error.message); 44 | return []; 45 | } 46 | } 47 | 48 | async getContentDetails(url) { 49 | try { 50 | console.log(`Getting content details: ${url}`); 51 | const response = await axios.get(url); 52 | const $ = cheerio.load(response.data); 53 | 54 | const contentId = $('.detail_page-watch').attr('data-id'); 55 | const name = $('.detail_page-infor h2.heading-name > a').text(); 56 | const isMovie = url.includes('movie'); 57 | 58 | if (isMovie) { 59 | return { 60 | type: 'movie', 61 | name, 62 | data: `list/${contentId}` 63 | }; 64 | } else { 65 | // Get TV series episodes 66 | const episodes = []; 67 | const seasonsResponse = await axios.get(`${this.mainUrl}/ajax/season/list/${contentId}`); 68 | const $seasons = cheerio.load(seasonsResponse.data); 69 | 70 | for (const season of $seasons('a.ss-item').toArray()) { 71 | const seasonId = $(season).attr('data-id'); 72 | const seasonNum = $(season).text().replace('Season ', ''); 73 | 74 | const episodesResponse = await axios.get(`${this.mainUrl}/ajax/season/episodes/${seasonId}`); 75 | const $episodes = cheerio.load(episodesResponse.data); 76 | 77 | $episodes('a.eps-item').each((i, episode) => { 78 | const epId = $(episode).attr('data-id'); 79 | const title = $(episode).attr('title'); 80 | const match = title.match(/Eps (\d+): (.+)/); 81 | 82 | if (match) { 83 | episodes.push({ 84 | id: epId, 85 | episode: parseInt(match[1]), 86 | name: match[2], 87 | season: parseInt(seasonNum.replace('Series', '').trim()), 88 | data: `servers/${epId}` 89 | }); 90 | } 91 | }); 92 | } 93 | 94 | return { 95 | type: 'series', 96 | name, 97 | episodes 98 | }; 99 | } 100 | } catch (error) { 101 | console.error('Content details error:', error.message); 102 | return null; 103 | } 104 | } 105 | 106 | async getServerLinks(data) { 107 | try { 108 | console.log(`Getting server links: ${data}`); 109 | const response = await axios.get(`${this.mainUrl}/ajax/episode/${data}`); 110 | const $ = cheerio.load(response.data); 111 | 112 | const servers = []; 113 | $('a.link-item').each((i, element) => { 114 | const linkId = $(element).attr('data-linkid') || $(element).attr('data-id'); 115 | if (linkId) { 116 | servers.push(linkId); 117 | } 118 | }); 119 | 120 | return servers; 121 | } catch (error) { 122 | console.error('Server links error:', error.message); 123 | return []; 124 | } 125 | } 126 | 127 | async getSourceUrl(linkId) { 128 | try { 129 | console.log(`Getting source URL for linkId: ${linkId}`); 130 | const response = await axios.get(`${this.mainUrl}/ajax/episode/sources/${linkId}`); 131 | return response.data.link; 132 | } catch (error) { 133 | console.error('Source URL error:', error.message); 134 | return null; 135 | } 136 | } 137 | 138 | async extractVideostrM3u8(url) { 139 | try { 140 | console.log(`Extracting from Videostr: ${url}`); 141 | 142 | const headers = { 143 | 'Accept': '*/*', 144 | 'X-Requested-With': 'XMLHttpRequest', 145 | 'Referer': this.videostrUrl, 146 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' 147 | }; 148 | 149 | // Extract ID from URL 150 | const id = url.split('/').pop().split('?')[0]; 151 | 152 | // Get nonce from embed page 153 | const embedResponse = await axios.get(url, { headers }); 154 | const embedHtml = embedResponse.data; 155 | 156 | // Try to find 48-character nonce 157 | let nonce = embedHtml.match(/\b[a-zA-Z0-9]{48}\b/); 158 | if (nonce) { 159 | nonce = nonce[0]; 160 | } else { 161 | // Try to find three 16-character segments 162 | 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/); 163 | if (matches) { 164 | nonce = matches[1] + matches[2] + matches[3]; 165 | } 166 | } 167 | 168 | if (!nonce) { 169 | throw new Error('Could not extract nonce'); 170 | } 171 | 172 | console.log(`Extracted nonce: ${nonce}`); 173 | 174 | // Get sources from API 175 | const apiUrl = `${this.videostrUrl}/embed-1/v3/e-1/getSources?id=${id}&_k=${nonce}`; 176 | console.log(`API URL: ${apiUrl}`); 177 | 178 | const sourcesResponse = await axios.get(apiUrl, { headers }); 179 | const sourcesData = sourcesResponse.data; 180 | 181 | if (!sourcesData.sources) { 182 | throw new Error('No sources found in response'); 183 | } 184 | 185 | let m3u8Url = sourcesData.sources; 186 | 187 | // Check if sources is already an M3U8 URL 188 | if (!m3u8Url.includes('.m3u8')) { 189 | console.log('Sources are encrypted, attempting to decrypt...'); 190 | 191 | // Get decryption key 192 | const keyResponse = await axios.get('https://raw.githubusercontent.com/yogesh-hacker/MegacloudKeys/refs/heads/main/keys.json'); 193 | const key = keyResponse.data.vidstr; 194 | 195 | if (!key) { 196 | throw new Error('Could not get decryption key'); 197 | } 198 | 199 | // Decrypt using Google Apps Script 200 | const decodeUrl = 'https://script.google.com/macros/s/AKfycbx-yHTwupis_JD0lNzoOnxYcEYeXmJZrg7JeMxYnEZnLBy5V0--UxEvP-y9txHyy1TX9Q/exec'; 201 | const fullUrl = `${decodeUrl}?encrypted_data=${encodeURIComponent(m3u8Url)}&nonce=${encodeURIComponent(nonce)}&secret=${encodeURIComponent(key)}`; 202 | 203 | const decryptResponse = await axios.get(fullUrl); 204 | const decryptedData = decryptResponse.data; 205 | 206 | // Extract file URL from decrypted response 207 | const fileMatch = decryptedData.match(/"file":"(.*?)"/); 208 | if (fileMatch) { 209 | m3u8Url = fileMatch[1]; 210 | } else { 211 | throw new Error('Could not extract video URL from decrypted response'); 212 | } 213 | } 214 | 215 | console.log(`Final M3U8 URL: ${m3u8Url}`); 216 | 217 | // Filter only megacdn links 218 | if (!m3u8Url.includes('megacdn.co')) { 219 | console.log('Skipping non-megacdn link'); 220 | return null; 221 | } 222 | 223 | // Parse master playlist to extract quality streams 224 | const qualities = await this.parseM3U8Qualities(m3u8Url); 225 | 226 | return { 227 | m3u8Url, 228 | qualities, 229 | headers: { 230 | 'Referer': 'https://videostr.net/', 231 | 'Origin': 'https://videostr.net/' 232 | } 233 | }; 234 | 235 | } catch (error) { 236 | console.error('Videostr extraction error:', error.message); 237 | return null; 238 | } 239 | } 240 | 241 | async parseM3U8Qualities(masterUrl) { 242 | try { 243 | const response = await axios.get(masterUrl, { 244 | headers: { 245 | 'Referer': 'https://videostr.net/', 246 | 'Origin': 'https://videostr.net/' 247 | } 248 | }); 249 | 250 | const playlist = response.data; 251 | const qualities = []; 252 | 253 | // Parse M3U8 master playlist 254 | const lines = playlist.split('\n'); 255 | for (let i = 0; i < lines.length; i++) { 256 | const line = lines[i].trim(); 257 | if (line.startsWith('#EXT-X-STREAM-INF:')) { 258 | const nextLine = lines[i + 1]?.trim(); 259 | if (nextLine && !nextLine.startsWith('#')) { 260 | // Extract resolution and bandwidth 261 | const resolutionMatch = line.match(/RESOLUTION=(\d+x\d+)/); 262 | const bandwidthMatch = line.match(/BANDWIDTH=(\d+)/); 263 | 264 | const resolution = resolutionMatch ? resolutionMatch[1] : 'Unknown'; 265 | const bandwidth = bandwidthMatch ? parseInt(bandwidthMatch[1]) : 0; 266 | 267 | // Determine quality label 268 | let quality = 'Unknown'; 269 | if (resolution.includes('1920x1080')) quality = '1080p'; 270 | else if (resolution.includes('1280x720')) quality = '720p'; 271 | else if (resolution.includes('640x360')) quality = '360p'; 272 | else if (resolution.includes('854x480')) quality = '480p'; 273 | 274 | qualities.push({ 275 | quality, 276 | resolution, 277 | bandwidth, 278 | url: nextLine.startsWith('http') ? nextLine : new URL(nextLine, masterUrl).href 279 | }); 280 | } 281 | } 282 | } 283 | 284 | // Sort by bandwidth (highest first) 285 | qualities.sort((a, b) => b.bandwidth - a.bandwidth); 286 | 287 | return qualities; 288 | } catch (error) { 289 | console.error('Error parsing M3U8 qualities:', error.message); 290 | return []; 291 | } 292 | } 293 | 294 | async extractM3u8Links(query, episodeNumber = null, seasonNumber = null) { 295 | try { 296 | // Search for content 297 | const searchResults = await this.search(query); 298 | if (searchResults.length === 0) { 299 | console.log('No search results found'); 300 | return []; 301 | } 302 | 303 | console.log(`Found ${searchResults.length} results`); 304 | 305 | // Try to find exact match first, then partial match 306 | let selectedResult = searchResults.find(result => 307 | result.title.toLowerCase() === query.toLowerCase() 308 | ); 309 | 310 | if (!selectedResult) { 311 | // Look for best partial match (contains all words from query) 312 | const queryWords = query.toLowerCase().split(' '); 313 | selectedResult = searchResults.find(result => { 314 | const titleLower = result.title.toLowerCase(); 315 | return queryWords.every(word => titleLower.includes(word)); 316 | }); 317 | } 318 | 319 | // Fallback to first result if no good match found 320 | if (!selectedResult) { 321 | selectedResult = searchResults[0]; 322 | } 323 | 324 | console.log(`Selected: ${selectedResult.title}`); 325 | 326 | // Get content details 327 | const contentDetails = await this.getContentDetails(selectedResult.url); 328 | if (!contentDetails) { 329 | console.log('Could not get content details'); 330 | return []; 331 | } 332 | 333 | let dataToProcess = []; 334 | 335 | if (contentDetails.type === 'movie') { 336 | dataToProcess.push(contentDetails.data); 337 | } else { 338 | // For TV series, filter by episode/season if specified 339 | let episodes = contentDetails.episodes; 340 | 341 | if (seasonNumber) { 342 | episodes = episodes.filter(ep => ep.season === seasonNumber); 343 | } 344 | 345 | if (episodeNumber) { 346 | episodes = episodes.filter(ep => ep.episode === episodeNumber); 347 | } 348 | 349 | if (episodes.length === 0) { 350 | console.log('No matching episodes found'); 351 | return []; 352 | } 353 | 354 | // Use first matching episode or all if no specific episode requested 355 | const targetEpisode = episodeNumber ? episodes[0] : episodes[0]; 356 | console.log(`Selected episode: S${targetEpisode.season}E${targetEpisode.episode} - ${targetEpisode.name}`); 357 | dataToProcess.push(targetEpisode.data); 358 | } 359 | 360 | const allM3u8Links = []; 361 | 362 | // Process all data in parallel 363 | const allPromises = []; 364 | 365 | for (const data of dataToProcess) { 366 | // Get server links 367 | const serverLinksPromise = this.getServerLinks(data).then(async (serverLinks) => { 368 | console.log(`Found ${serverLinks.length} servers`); 369 | 370 | // Process all server links in parallel 371 | const linkPromises = serverLinks.map(async (linkId) => { 372 | try { 373 | // Get source URL 374 | const sourceUrl = await this.getSourceUrl(linkId); 375 | if (!sourceUrl) return null; 376 | 377 | console.log(`Source URL: ${sourceUrl}`); 378 | 379 | // Check if it's a videostr URL 380 | if (sourceUrl.includes('videostr.net')) { 381 | const result = await this.extractVideostrM3u8(sourceUrl); 382 | if (result) { 383 | return { 384 | source: 'videostr', 385 | m3u8Url: result.m3u8Url, 386 | qualities: result.qualities, 387 | headers: result.headers 388 | }; 389 | } 390 | } 391 | return null; 392 | } catch (error) { 393 | console.error(`Error processing link ${linkId}:`, error.message); 394 | return null; 395 | } 396 | }); 397 | 398 | return Promise.all(linkPromises); 399 | }); 400 | 401 | allPromises.push(serverLinksPromise); 402 | } 403 | 404 | // Wait for all promises to complete 405 | const results = await Promise.all(allPromises); 406 | 407 | // Flatten and filter results 408 | for (const serverResults of results) { 409 | for (const result of serverResults) { 410 | if (result) { 411 | allM3u8Links.push(result); 412 | } 413 | } 414 | } 415 | 416 | return allM3u8Links; 417 | 418 | } catch (error) { 419 | console.error('Extraction error:', error.message); 420 | return []; 421 | } 422 | } 423 | } 424 | 425 | // CLI usage 426 | if (require.main === module) { 427 | const args = process.argv.slice(2); 428 | 429 | if (args.length === 0) { 430 | console.log('Usage: node myflixer-extractor.js "" [episode] [season]'); 431 | console.log('Examples:'); 432 | console.log(' node myflixer-extractor.js "Avengers Endgame"'); 433 | console.log(' node myflixer-extractor.js "Breaking Bad" 1 1 # Season 1, Episode 1'); 434 | process.exit(1); 435 | } 436 | 437 | const query = args[0]; 438 | const episode = args[1] ? parseInt(args[1]) : null; 439 | const season = args[2] ? parseInt(args[2]) : null; 440 | 441 | const extractor = new MyFlixerExtractor(); 442 | 443 | extractor.extractM3u8Links(query, episode, season) 444 | .then(links => { 445 | if (links.length === 0) { 446 | console.log('No M3U8 links found'); 447 | } else { 448 | console.log('\n=== EXTRACTED M3U8 LINKS ==='); 449 | links.forEach((link, index) => { 450 | console.log(`\nLink ${index + 1}:`); 451 | console.log(`Source: ${link.source}`); 452 | console.log(`Master M3U8 URL: ${link.m3u8Url}`); 453 | console.log(`Headers: ${JSON.stringify(link.headers, null, 2)}`); 454 | 455 | if (link.qualities && link.qualities.length > 0) { 456 | console.log('Available Qualities:'); 457 | link.qualities.forEach((quality, qIndex) => { 458 | console.log(` ${qIndex + 1}. ${quality.quality} (${quality.resolution}) - ${Math.round(quality.bandwidth/1000)}kbps`); 459 | console.log(` URL: ${quality.url}`); 460 | }); 461 | } 462 | }); 463 | } 464 | }) 465 | .catch(error => { 466 | console.error('Error:', error.message); 467 | process.exit(1); 468 | }); 469 | } 470 | 471 | module.exports = MyFlixerExtractor; --------------------------------------------------------------------------------