From 25cc59659dff6602af521eeee622f6c75338e1e6 Mon Sep 17 00:00:00 2001 From: Inside4ndroid Date: Thu, 18 Sep 2025 21:00:21 +0100 Subject: [PATCH] docs: update README (unified stream schema, MoviesClub, registry mapping, docker run)\nchore(release): 1.0.4 tag prep\nchore: ignore restart.trigger and .vscode --- .gitignore | 4 +- CHANGELOG.md | 17 + README.md | 96 ++++-- package.json | 2 +- providers/4khdhub.js | 711 +++++++++++++++++++++------------------- providers/MP4Hydra.js | 194 +++++------ providers/Showbox.js | 272 +++++++-------- providers/VidZee.js | 12 +- providers/moviesclub.js | 629 +++++++++++++++++++++++++++++++++++ providers/moviesmod.js | 2 + providers/registry.js | 398 +++++++++------------- providers/uhdmovies.js | 77 +++-- providers/vixsrc.js | 2 +- providers/xprime.js | 6 +- public/index.js | 28 +- restart.trigger | 2 +- utils/config.js | 72 ++-- utils/jsunpack.js | 113 +++++++ utils/user-config.json | 2 +- 19 files changed, 1702 insertions(+), 937 deletions(-) create mode 100644 providers/moviesclub.js create mode 100644 utils/jsunpack.js diff --git a/.gitignore b/.gitignore index 30bc162..edb9dad 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -/node_modules \ No newline at end of file +/node_modules +restart.trigger +.vscode/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 9aaa633..dce2ce5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,22 @@ All notable changes to this project will be documented in this file. +## [1.0.4] - 2025-09-18 + +### Added +- New provider: `moviesclub` with registry mapping (`moviesclub.js` → `getMoviesClubStreams`). + +### Changed +- Unified provider stream schema across all providers: + ```json + { "title": "…", "url": "…", "quality": "…", "provider": "…", "headers": { } } + ``` +- Provider integration docs: When adding a provider, also add its exported function to `providerFunctionMap` in `providers/registry.js`. +- README Docker quick run snippet updated to: `docker run -it -p 8787:8787 inside4ndroid/tmdb-embed-api:latest`. + +### Documentation +- README: Updated Providers section, unified schema example, MoviesClub listed, registry mapping instructions, and badge version. + ## [1.0.3] - 2025-09-17 ### Fixed @@ -46,6 +62,7 @@ All notable changes to this project will be documented in this file. - Initial stable release. [1.0.3]: https://github.com/Inside4ndroid/TMDB-Embed-API/compare/v1.0.2...v1.0.3 +[1.0.4]: https://github.com/Inside4ndroid/TMDB-Embed-API/compare/v1.0.3...v1.0.4 [1.0.2]: https://github.com/Inside4ndroid/TMDB-Embed-API/compare/v1.0.1...v1.0.2 [1.0.1]: https://github.com/Inside4ndroid/TMDB-Embed-API/compare/v1.0.0...v1.0.1 diff --git a/README.md b/README.md index aff629b..152ff66 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ - +

@@ -34,14 +34,15 @@ ## ✨ Features - **Multi‑TMDB Key Rotation** – Supply multiple API keys; one is chosen randomly per request. -- **Provider Aggregation** – Pluggable providers (Showbox, 4khdhub, MoviesMod, MP4Hydra, VidZee, Vixsrc, Xprime) with per‑provider enable toggles + default selection. +- **Provider Aggregation** – Pluggable providers (Showbox, 4khdhub, MoviesMod, MP4Hydra, VidZee, Vixsrc, Xprime, MoviesClub) with per‑provider enable toggles + default selection. +- **🔥 Plugin System** – Drop new provider files in `providers/` and add its exported function to the registry map (`providers/registry.js` → `providerFunctionMap`). - **Dynamic Filtering** – Minimum quality presets, custom JSON quality map, codec exclusion rules (presets + JSON). - **Runtime Overrides UI** – Fully interactive web admin at `/` (login protected) writing to `utils/user-config.json`. - **Session Auth + Rate Limiting** – Login system with brute‑force lockouts, logout, password change. - **Status & Health Panel** – Live metrics, provider status, endpoint list, functional provider checks. - **Config Propagation** – Overrides mirrored to `process.env` for legacy compatibility (no `.env` required after first save). - **Back‑Navigation Safe** – Cache-control + visibility/session revalidation. -- **Extensible** – Simple provider registry pattern. +- **Extensible** – Simple drop-in provider plugin system. --- @@ -79,6 +80,11 @@ docker run --name tmdb-embed-api -p 8787:8787 \ inside4ndroid/tmdb-embed-api:latest ``` +Or the minimal quick-test run: +```bash +docker run -it -p 8787:8787 inside4ndroid/tmdb-embed-api:latest +``` + Persist overrides (Windows PowerShell example) by mounting a local file: ```powershell New-Item -ItemType File -Path .\utils\user-config.json -Force | Out-Null @@ -202,22 +208,64 @@ Session is revalidated on visibility and back/forward navigation to prevent stal --- ## 🔌 Providers -Current built‑in providers: -- `showbox` -- `4khdhub` -- `moviesmod` -- `mp4hydra` -- `vidzee` -- `vixsrc` -- `xprime` +The API supports a plugin system. Drop a new provider file in the `providers/` folder and register its exported function in `providers/registry.js` under `providerFunctionMap`. -You can enable/disable and mark defaults via the Provider Matrix in the UI. If **no defaults** are selected, all enabled providers participate in aggregation. +### Current Built-in Providers +- `showbox` - Showbox/PStream integration +- `4khdhub` - 4KHDHub streams +- `moviesmod` - MoviesMod streams +- `mp4hydra` - MP4Hydra streams +- `vidzee` - VidZee streams +- `vixsrc` - Vixsrc streams +- `xprime` - Xprime streams +- `uhdmovies` - UHD Movies streams +- `moviesclub` - MoviesClub streams -### Adding a New Provider (Outline) -1. Create `providers/myprovider.js` exporting an async fetch function (see existing files for patterns). -2. Lazy‑load it in `providers/registry.js` similar to others. -3. Add a config flag (`enableMyproviderProvider`) if you want toggle support. -4. Update the provider matrix arrays in `public/index.js`. +### Adding a New Provider +1. **Create** `providers/yourprovider.js` with your stream fetching logic +2. **Export** a function like `getYourproviderStreams(tmdbId, mediaType, season, episode)` +3. **Register** it in `providers/registry.js` → `providerFunctionMap`: + ```js + // providers/registry.js + const providerFunctionMap = { + 'Showbox.js': 'getStreamsFromTmdbId', + '4khdhub.js': 'get4KHDHubStreams', + 'moviesmod.js': 'getMoviesModStreams', + 'MP4Hydra.js': 'getMP4HydraStreams', + 'VidZee.js': 'getVidZeeStreams', + 'vixsrc.js': 'getVixsrcStreams', + 'xprime.js': 'getXprimeStreams', + 'uhdmovies.js': 'getUHDMoviesStreams', + 'moviesclub.js': 'getMoviesClubStreams', + 'yourprovider.js': 'getYourproviderStreams' + }; + ``` +4. The provider will appear in the admin UI with an enable/disable toggle. + +**Example Provider (Unified Output):** +```javascript +async function getYourproviderStreams(tmdbId, mediaType, season, episode) { + // Your scraping/API logic here + return [{ + title: "Fight Club - 1080p [YourProvider #1]", + url: "https://stream.url/video.mp4", + quality: "1080p", + provider: "yourprovider", + headers: { "User-Agent": "Mozilla/5.0" } + }]; +} + +module.exports = { getYourproviderStreams }; +``` + +> **⚠️ Important**: All providers must return streams in the unified JSON format to ensure compatibility with filtering and aggregation. + +The system automatically: +- ✅ Detects new provider files +- ✅ Adds enable/disable toggles in the admin UI +- ✅ Includes them in stream aggregation +- ✅ Applies filtering and quality controls +- ✅ No core file edits required! --- @@ -238,18 +286,14 @@ Aggregate endpoint auto-resolves IMDb when needed and merges provider output bef --- -## 🧪 Stream Object Shape (Typical) +## 🧪 Stream Object Schema (Unified) ```json { - "title": "Example Title", - "url": "https://...", + "title": "Fight Club - 1080p [MP4Hydra #2]", + "url": "https://stream.url/video.mp4", "quality": "1080p", - "provider": "showbox", - "size": null, - "languages": ["en"], - "subtitles": [], - "codecs": ["H264"], - "headers": { "Referer": "..." } + "provider": "mp4hydra", + "headers": { "User-Agent": "Mozilla/5.0" } } ``` diff --git a/package.json b/package.json index 510076c..847e162 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tmdb-embed-api", - "version": "1.0.3", + "version": "1.0.4", "description": "Standalone streaming embed source aggregation API (TMDB ID in, streams out)", "main": "apiServer.js", "scripts": { diff --git a/providers/4khdhub.js b/providers/4khdhub.js index 4ccf07c..b5f1404 100644 --- a/providers/4khdhub.js +++ b/providers/4khdhub.js @@ -18,14 +18,14 @@ const CACHE_DIR = process.env.VERCEL ? path.join('/tmp', '.4khdhub_cache') : pat // --- Caching Helper Functions --- const ensureCacheDir = async () => { - if (!CACHE_ENABLED) return; - try { - await fs.mkdir(CACHE_DIR, { recursive: true }); - } catch (error) { - if (error.code !== 'EEXIST') { - console.error(`[4KHDHub Cache] Error creating cache directory: ${error.message}`); + if (!CACHE_ENABLED) return; + try { + await fs.mkdir(CACHE_DIR, { recursive: true }); + } catch (error) { + if (error.code !== 'EEXIST') { + console.error(`[4KHDHub Cache] Error creating cache directory: ${error.message}`); + } } - } }; const getFromCache = async (key) => { @@ -48,7 +48,7 @@ function base64Decode(str) { function rot13(str) { - return str.replace(/[A-Za-z]/g, function(char) { + return str.replace(/[A-Za-z]/g, function (char) { const start = char <= 'Z' ? 65 : 97; return String.fromCharCode(((char.charCodeAt(0) - start + 13) % 26) + start); }); @@ -58,22 +58,22 @@ function validateUrl(url) { return new Promise((resolve) => { try { const urlObj = new URL(url); - + // Skip validation for known reliable hosting services - const trustedHosts = [ - 'pixeldrain.dev', - 'r2.dev' - ]; - + const trustedHosts = [ + 'pixeldrain.dev', + 'r2.dev' + ]; + const isTrustedHost = trustedHosts.some(host => urlObj.hostname.includes(host)); if (isTrustedHost) { console.log(`[4KHDHub] Skipping validation for trusted host: ${urlObj.hostname}`); resolve(true); return; } - + const protocol = urlObj.protocol === 'https:' ? https : http; - + const options = { method: 'HEAD', timeout: 15000, @@ -81,7 +81,7 @@ function validateUrl(url) { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' } }; - + const req = protocol.request(url, options, (res) => { // Consider 2xx and 3xx status codes as valid, including 206 (Partial Content) const isValid = res.statusCode >= 200 && res.statusCode < 400; @@ -89,18 +89,18 @@ function validateUrl(url) { res.destroy(); // Close connection immediately resolve(isValid); }); - + req.on('error', (err) => { console.log(`[4KHDHub] URL validation error for ${url}: ${err.message}`); resolve(false); }); - + req.on('timeout', () => { console.log(`[4KHDHub] URL validation timeout for ${url}`); req.destroy(); resolve(false); }); - + req.setTimeout(15000); req.end(); } catch (error) { @@ -115,7 +115,7 @@ function makeRequest(url, options = {}) { const urlObj = new URL(url); const isHttps = urlObj.protocol === 'https:'; const httpModule = isHttps ? https : http; - + const requestOptions = { hostname: urlObj.hostname, port: urlObj.port || (isHttps ? 443 : 80), @@ -127,13 +127,13 @@ function makeRequest(url, options = {}) { }, timeout: 30000 }; - + const req = httpModule.request(requestOptions, (res) => { if (options.allowRedirects === false && (res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 303 || res.statusCode === 307 || res.statusCode === 308)) { resolve({ statusCode: res.statusCode, headers: res.headers }); return; } - + let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => { @@ -145,7 +145,7 @@ function makeRequest(url, options = {}) { } }); }); - + req.on('error', reject); req.on('timeout', () => reject(new Error('Request timeout'))); req.end(); @@ -155,19 +155,19 @@ function makeRequest(url, options = {}) { // Helper function to decode URL-encoded filenames and make them human-readable function decodeFilename(filename) { if (!filename) return filename; - + try { // Handle UTF-8 prefix and decode URL encoding let decoded = filename; - + // Remove UTF-8 prefix if present if (decoded.startsWith('UTF-8')) { decoded = decoded.substring(5); } - + // Decode URL encoding (%20 -> space, etc.) decoded = decodeURIComponent(decoded); - + return decoded; } catch (error) { console.log(`[4KHDHub] Error decoding filename: ${error.message}`); @@ -181,7 +181,7 @@ function getFilenameFromUrl(url) { const urlObj = new URL(url); const isHttps = urlObj.protocol === 'https:'; const httpModule = isHttps ? https : http; - + const requestOptions = { hostname: urlObj.hostname, port: urlObj.port || (isHttps ? 443 : 80), @@ -192,18 +192,18 @@ function getFilenameFromUrl(url) { }, timeout: 10000 }; - + const req = httpModule.request(requestOptions, (res) => { const contentDisposition = res.headers['content-disposition']; let filename = null; - + if (contentDisposition) { const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/i); if (filenameMatch && filenameMatch[1]) { filename = filenameMatch[1].replace(/["']/g, ''); } } - + if (!filename) { // Extract from URL path const pathParts = urlObj.pathname.split('/'); @@ -212,12 +212,12 @@ function getFilenameFromUrl(url) { filename = filename.replace(/\.[^.]+$/, ''); // Remove extension } } - + // Decode the filename to make it human-readable const decodedFilename = decodeFilename(filename); resolve(decodedFilename || null); }); - + req.on('error', () => resolve(null)); req.on('timeout', () => resolve(null)); req.end(); @@ -231,7 +231,7 @@ function getDomains() { if (cachedDomains) { return Promise.resolve(cachedDomains); } - + return makeRequest(DOMAINS_URL) .then(response => { cachedDomains = JSON.parse(response.body); @@ -250,31 +250,31 @@ function getRedirectLinks(url) { const regex = /s\('o','([A-Za-z0-9+/=]+)'|ck\('_wp_http_\d+','([^']+)'/g; let combinedString = ''; let match; - + while ((match = regex.exec(doc)) !== null) { const extractedValue = match[1] || match[2]; if (extractedValue) { combinedString += extractedValue; } } - + try { const decodedString = base64Decode(rot13(base64Decode(base64Decode(combinedString)))); const jsonObject = JSON.parse(decodedString); const encodedurl = base64Decode(jsonObject.o || '').trim(); const data = base64Decode(jsonObject.data || '').trim(); const wphttp1 = (jsonObject.blog_url || '').trim(); - + if (encodedurl) { return Promise.resolve(encodedurl); } - + if (wphttp1 && data) { return makeRequest(`${wphttp1}?re=${data}`, { parseHTML: true }) .then(resp => resp.document.body.textContent.trim()) .catch(() => ''); } - + return Promise.resolve(''); } catch (e) { console.error('[4KHDHub] Error processing links:', e.message); @@ -305,23 +305,23 @@ function cleanTitle(title) { // Decode URL-encoded title first const decodedTitle = decodeFilename(title); const parts = decodedTitle.split(/[.\-_]/); - + const qualityTags = ['WEBRip', 'WEB-DL', 'WEB', 'BluRay', 'HDRip', 'DVDRip', 'HDTV', 'CAM', 'TS', 'R5', 'DVDScr', 'BRRip', 'BDRip', 'DVD', 'PDTV', 'HD']; const audioTags = ['AAC', 'AC3', 'DTS', 'MP3', 'FLAC', 'DD5', 'EAC3', 'Atmos']; const subTags = ['ESub', 'ESubs', 'Subs', 'MultiSub', 'NoSub', 'EnglishSub', 'HindiSub']; const codecTags = ['x264', 'x265', 'H264', 'HEVC', 'AVC']; - - const startIndex = parts.findIndex(part => + + const startIndex = parts.findIndex(part => qualityTags.some(tag => part.toLowerCase().includes(tag.toLowerCase())) ); - + const endIndex = parts.map((part, index) => { - const hasTag = [...subTags, ...audioTags, ...codecTags].some(tag => + const hasTag = [...subTags, ...audioTags, ...codecTags].some(tag => part.toLowerCase().includes(tag.toLowerCase()) ); return hasTag ? index : -1; }).filter(index => index !== -1).pop() || -1; - + if (startIndex !== -1 && endIndex !== -1 && endIndex >= startIndex) { return parts.slice(startIndex, endIndex + 1).join('.'); } else if (startIndex !== -1) { @@ -368,25 +368,25 @@ function removeYear(title) { // Generate alternative search queries for better matching function generateAlternativeQueries(title, originalTitle = null) { const queries = new Set(); - + // Add the original title queries.add(title); - + // Add original title if different if (originalTitle && originalTitle !== title) { queries.add(originalTitle); } - + // Remove year and try again const titleWithoutYear = removeYear(title); if (titleWithoutYear !== title) { queries.add(titleWithoutYear); } - + // Remove colons and other punctuation queries.add(title.replace(/:/g, '')); queries.add(title.replace(/[^a-zA-Z0-9\s]/g, ' ').replace(/\s+/g, ' ').trim()); - + // Handle common title variations const variations = [ title.replace(/\bPart\s+(\d+)\b/gi, 'Part $1'), @@ -397,16 +397,18 @@ function generateAlternativeQueries(title, originalTitle = null) { return romans[num] || match; }), title.replace(/\b(I{1,3}|IV|V|VI{0,3}|IX|X)\b/g, match => { - const romans = { 'I': '1', 'II': '2', 'III': '3', 'IV': '4', 'V': '5', - 'VI': '6', 'VII': '7', 'VIII': '8', 'IX': '9', 'X': '10' }; + const romans = { + 'I': '1', 'II': '2', 'III': '3', 'IV': '4', 'V': '5', + 'VI': '6', 'VII': '7', 'VIII': '8', 'IX': '9', 'X': '10' + }; return romans[match] || match; }) ]; - + variations.forEach(v => { if (v && v !== title) queries.add(v); }); - + // Remove duplicates and filter out empty strings return Array.from(queries).filter(q => q && q.trim().length > 0); } @@ -415,18 +417,18 @@ function generateAlternativeQueries(title, originalTitle = null) { function calculateSimilarity(str1, str2) { const s1 = normalizeTitle(str1); const s2 = normalizeTitle(str2); - + if (s1 === s2) return 1.0; - + // Levenshtein distance const levenshtein = calculateLevenshteinSimilarity(s1, s2); - + // Jaccard similarity (word-based) const jaccard = calculateJaccardSimilarity(s1, s2); - + // Longest common subsequence const lcs = calculateLCSSimilarity(s1, s2); - + // Weighted combination of different similarity measures return (levenshtein * 0.4) + (jaccard * 0.4) + (lcs * 0.2); } @@ -435,15 +437,15 @@ function calculateSimilarity(str1, str2) { function calculateLevenshteinSimilarity(s1, s2) { const len1 = s1.length; const len2 = s2.length; - + if (len1 === 0) return len2 === 0 ? 1.0 : 0.0; if (len2 === 0) return 0.0; - + const matrix = Array(len1 + 1).fill().map(() => Array(len2 + 1).fill(0)); - + for (let i = 0; i <= len1; i++) matrix[i][0] = i; for (let j = 0; j <= len2; j++) matrix[0][j] = j; - + for (let i = 1; i <= len1; i++) { for (let j = 1; j <= len2; j++) { const cost = s1[i - 1] === s2[j - 1] ? 0 : 1; @@ -454,7 +456,7 @@ function calculateLevenshteinSimilarity(s1, s2) { ); } } - + const maxLen = Math.max(len1, len2); return (maxLen - matrix[len1][len2]) / maxLen; } @@ -463,10 +465,10 @@ function calculateLevenshteinSimilarity(s1, s2) { function calculateJaccardSimilarity(s1, s2) { const words1 = new Set(s1.split(' ').filter(w => w.length > 0)); const words2 = new Set(s2.split(' ').filter(w => w.length > 0)); - + const intersection = new Set([...words1].filter(w => words2.has(w))); const union = new Set([...words1, ...words2]); - + return union.size === 0 ? 0 : intersection.size / union.size; } @@ -474,11 +476,11 @@ function calculateJaccardSimilarity(s1, s2) { function calculateLCSSimilarity(s1, s2) { const len1 = s1.length; const len2 = s2.length; - + if (len1 === 0 || len2 === 0) return 0; - + const dp = Array(len1 + 1).fill().map(() => Array(len2 + 1).fill(0)); - + for (let i = 1; i <= len1; i++) { for (let j = 1; j <= len2; j++) { if (s1[i - 1] === s2[j - 1]) { @@ -488,7 +490,7 @@ function calculateLCSSimilarity(s1, s2) { } } } - + const maxLen = Math.max(len1, len2); return dp[len1][len2] / maxLen; } @@ -497,29 +499,29 @@ function calculateLCSSimilarity(s1, s2) { function containsWords(title, query) { const titleWords = normalizeTitle(title).split(' ').filter(w => w.length > 1); const queryWords = normalizeTitle(query).split(' ').filter(w => w.length > 1); - + let matchedWords = 0; - + for (const queryWord of queryWords) { const found = titleWords.some(titleWord => { // Exact match if (titleWord === queryWord) return true; - + // Substring match if (titleWord.includes(queryWord) || queryWord.includes(titleWord)) return true; - + // Fuzzy match for longer words (allow 1 character difference) if (queryWord.length > 3 && titleWord.length > 3) { const similarity = calculateLevenshteinSimilarity(titleWord, queryWord); return similarity > 0.8; } - + return false; }); - + if (found) matchedWords++; } - + // Require at least 70% of query words to be matched return matchedWords / queryWords.length >= 0.7; } @@ -528,9 +530,9 @@ function containsWords(title, query) { function findBestMatch(results, query, tmdbYear = null) { if (results.length === 0) return null; if (results.length === 1) return results[0]; - + console.log(`[4KHDHub] Finding best match for: "${query}" (Year: ${tmdbYear || 'N/A'})`); - + // Strict year gating: if TMDB year is known, only consider exact matches first, // then allow unknown-year entries as fallback. Reject clear mismatches. let candidateResults = results; @@ -557,7 +559,7 @@ function findBestMatch(results, query, tmdbYear = null) { } } } - + // Score each result const scoredResults = candidateResults.map(result => { let score = 0; @@ -565,21 +567,21 @@ function findBestMatch(results, query, tmdbYear = null) { const resultYear = result.year || extractYear(result.title); const queryWithoutYear = removeYear(query); const resultWithoutYear = removeYear(result.title); - + // Exact match gets highest score (without year) if (normalizeTitle(resultWithoutYear) === normalizeTitle(queryWithoutYear)) { score += 100; } - + // Enhanced similarity score (0-60 points) const similarity = calculateSimilarity(resultWithoutYear, queryWithoutYear); score += similarity * 60; - + // Word containment bonus (0-25 points) if (containsWords(result.title, query)) { score += 25; } - + // Year matching bonus (strict): only reward exact match when TMDB year is known if (tmdbYear && resultYear) { if (parseInt(tmdbYear) === parseInt(resultYear)) { @@ -588,34 +590,34 @@ function findBestMatch(results, query, tmdbYear = null) { } else if (resultYear && !tmdbYear) { score += 5; // Slight bonus for having year info when TMDB year unknown } - + // Length similarity bonus (0-10 points) const lengthDiff = Math.abs(resultWithoutYear.length - queryWithoutYear.length); score += Math.max(0, 10 - lengthDiff / 3); - + // Prefer results with quality indicators if (result.title.match(/\b(1080p|720p|4K|2160p|BluRay|WEB-DL)\b/i)) { score += 3; } - + // Penalty for results with too many extra words const queryWordCount = queryWithoutYear.split(' ').filter(w => w.length > 0).length; const resultWordCount = resultWithoutYear.split(' ').filter(w => w.length > 0).length; if (resultWordCount > queryWordCount * 2) { score -= 10; } - + return { ...result, score, similarity, resultYear }; }); - + // Sort by score (highest first) scoredResults.sort((a, b) => b.score - a.score); - + console.log('[4KHDHub] Title matching scores:'); scoredResults.slice(0, 5).forEach((result, index) => { console.log(`${index + 1}. ${result.title} (Score: ${result.score.toFixed(1)}, Similarity: ${(result.similarity * 100).toFixed(1)}%, Year: ${result.resultYear || 'N/A'})`); }); - + // Additional validation: ensure the best match has a reasonable score const bestResult = scoredResults[0]; // Final guard: if TMDB year is known and best has a conflicting year, reject @@ -627,19 +629,19 @@ function findBestMatch(results, query, tmdbYear = null) { console.log(`[4KHDHub] Best match score too low (${bestResult.score.toFixed(1)}), rejecting`); return null; } - + return bestResult; } function extractHubCloudLinks(url, referer) { console.log(`[4KHDHub] Starting HubCloud extraction for: ${url}`); const baseUrl = getBaseUrl(url); - + return makeRequest(url, { parseHTML: true }) .then(response => { const $ = response.$; console.log(`[4KHDHub] Got HubCloud page, looking for download element...`); - + // Check if this is already a hubcloud.php URL let href; if (url.includes('hubcloud.php')) { @@ -652,7 +654,7 @@ function extractHubCloudLinks(url, referer) { // Try alternative selectors const alternatives = ['a[href*="hubcloud.php"]', '.download-btn', 'a[href*="download"]']; let found = false; - + for (const selector of alternatives) { const altElement = $(selector).first(); if (altElement.length > 0) { @@ -665,7 +667,7 @@ function extractHubCloudLinks(url, referer) { } } } - + if (!found) { throw new Error('Download element not found with any selector'); } @@ -674,37 +676,37 @@ function extractHubCloudLinks(url, referer) { if (!rawHref) { throw new Error('Download href not found'); } - + href = rawHref.startsWith('http') ? rawHref : `${baseUrl.replace(/\/$/, '')}/${rawHref.replace(/^\//, '')}`; console.log(`[4KHDHub] Found download href: ${href}`); } } - + console.log(`[4KHDHub] Making request to HubCloud download page: ${href}`); return makeRequest(href, { parseHTML: true }); }) .then(response => { const $ = response.$; - + console.log(`[4KHDHub] Processing HubCloud download page...`); - + // Extract quality and size information const size = $('i#size').text() || ''; const header = $('div.card-header').text() || ''; const quality = getIndexQuality(header); const headerDetails = cleanTitle(header); - + console.log(`[4KHDHub] Extracted info - Size: ${size}, Header: ${header}, Quality: ${quality}, HeaderDetails: ${headerDetails}`); - + // Extract just the quality for clean naming const qualityLabel = quality ? ` - ${quality}p` : ''; - + // We'll build the title format later after getting actual filename from HEAD request - + // Find download buttons const downloadButtons = $('div.card-body h2 a.btn'); console.log(`[4KHDHub] Found ${downloadButtons.length} download buttons`); - + if (downloadButtons.length === 0) { // Try alternative selectors for download buttons const altSelectors = ['a.btn', '.btn', 'a[href]']; @@ -722,23 +724,23 @@ function extractHubCloudLinks(url, referer) { } } } - + const promises = downloadButtons.get().map((button, index) => { return new Promise((resolve) => { const $button = $(button); const link = $button.attr('href'); const text = $button.text(); - + console.log(`[4KHDHub] Processing button ${index + 1}: "${text}" -> ${link}`); - + if (!link) { console.log(`[4KHDHub] Button ${index + 1} has no link`); resolve(null); return; } - + const buttonBaseUrl = getBaseUrl(link); - + if (text.includes('FSL Server')) { console.log(`[4KHDHub] Button ${index + 1} is FSL Server`); // Get actual filename from HEAD request @@ -749,12 +751,14 @@ function extractHubCloudLinks(url, referer) { if (displayFilename) titleParts.push(displayFilename); if (size) titleParts.push(size); const finalTitle = titleParts.join('\n'); - + resolve({ name: `4KHDHub - FSL Server${qualityLabel}`, title: finalTitle, url: link, - quality: quality + quality: quality, + provider: '4khdhub', + headers: {} }); }) .catch(() => { @@ -763,12 +767,14 @@ function extractHubCloudLinks(url, referer) { if (displayFilename) titleParts.push(displayFilename); if (size) titleParts.push(size); const finalTitle = titleParts.join('\n'); - + resolve({ name: `4KHDHub - FSL Server${qualityLabel}`, title: finalTitle, url: link, - quality: quality + quality: quality, + provider: '4khdhub', + headers: {} }); }); } else if (text.includes('Download File')) { @@ -781,12 +787,14 @@ function extractHubCloudLinks(url, referer) { if (displayFilename) titleParts.push(displayFilename); if (size) titleParts.push(size); const finalTitle = titleParts.join('\n'); - + resolve({ name: `4KHDHub - HubCloud${qualityLabel}`, title: finalTitle, url: link, - quality: quality + quality: quality, + provider: '4khdhub', + headers: {} }); }) .catch(() => { @@ -795,69 +803,75 @@ function extractHubCloudLinks(url, referer) { if (displayFilename) titleParts.push(displayFilename); if (size) titleParts.push(size); const finalTitle = titleParts.join('\n'); - + resolve({ name: `4KHDHub - HubCloud${qualityLabel}`, title: finalTitle, url: link, - quality: quality + quality: quality, + provider: '4khdhub', + headers: {} }); }); } else if (text.includes('BuzzServer')) { console.log(`[4KHDHub] Button ${index + 1} is BuzzServer, following redirect...`); // Handle BuzzServer redirect - makeRequest(`${link}/download`, { + makeRequest(`${link}/download`, { parseHTML: false, allowRedirects: false, headers: { 'Referer': link } }) - .then(response => { - const redirectUrl = response.headers['hx-redirect'] || response.headers['location']; - if (redirectUrl) { - console.log(`[4KHDHub] BuzzServer redirect found: ${redirectUrl}`); - const finalUrl = buttonBaseUrl + redirectUrl; - // Get actual filename from HEAD request - getFilenameFromUrl(finalUrl) - .then(actualFilename => { - const displayFilename = actualFilename || headerDetails || 'Unknown'; - const titleParts = []; - if (displayFilename) titleParts.push(displayFilename); - if (size) titleParts.push(size); - const finalTitle = titleParts.join('\n'); - - resolve({ - name: `4KHDHub - BuzzServer${qualityLabel}`, - title: finalTitle, - url: finalUrl, - quality: quality + .then(response => { + const redirectUrl = response.headers['hx-redirect'] || response.headers['location']; + if (redirectUrl) { + console.log(`[4KHDHub] BuzzServer redirect found: ${redirectUrl}`); + const finalUrl = buttonBaseUrl + redirectUrl; + // Get actual filename from HEAD request + getFilenameFromUrl(finalUrl) + .then(actualFilename => { + const displayFilename = actualFilename || headerDetails || 'Unknown'; + const titleParts = []; + if (displayFilename) titleParts.push(displayFilename); + if (size) titleParts.push(size); + const finalTitle = titleParts.join('\n'); + + resolve({ + name: `4KHDHub - BuzzServer${qualityLabel}`, + title: finalTitle, + url: finalUrl, + quality: quality, + provider: '4khdhub', + headers: {} + }); + }) + .catch(() => { + const displayFilename = headerDetails || 'Unknown'; + const titleParts = []; + if (displayFilename) titleParts.push(displayFilename); + if (size) titleParts.push(size); + const finalTitle = titleParts.join('\n'); + + resolve({ + name: `4KHDHub - BuzzServer${qualityLabel}`, + title: finalTitle, + url: finalUrl, + quality: quality, + provider: '4khdhub', + headers: {} + }); }); - }) - .catch(() => { - const displayFilename = headerDetails || 'Unknown'; - const titleParts = []; - if (displayFilename) titleParts.push(displayFilename); - if (size) titleParts.push(size); - const finalTitle = titleParts.join('\n'); - - resolve({ - name: `4KHDHub - BuzzServer${qualityLabel}`, - title: finalTitle, - url: finalUrl, - quality: quality - }); - }); - } else { - console.log(`[4KHDHub] BuzzServer redirect not found`); + } else { + console.log(`[4KHDHub] BuzzServer redirect not found`); + resolve(null); + } + }) + .catch(err => { + console.log(`[4KHDHub] BuzzServer redirect failed: ${err.message}`); resolve(null); - } - }) - .catch(err => { - console.log(`[4KHDHub] BuzzServer redirect failed: ${err.message}`); - resolve(null); - }); + }); } else if (link.includes('pixeldra')) { console.log(`[4KHDHub] Button ${index + 1} is Pixeldrain`); - + // Convert pixeldrain.net/u/ID format to pixeldrain.net/api/file/ID format let convertedLink = link; const pixeldrainMatch = link.match(/pixeldrain\.net\/u\/([a-zA-Z0-9]+)/); @@ -866,7 +880,7 @@ function extractHubCloudLinks(url, referer) { convertedLink = `https://pixeldrain.net/api/file/${fileId}`; console.log(`[4KHDHub] Converted Pixeldrain URL from ${link} to ${convertedLink}`); } - + // Get actual filename from HEAD request getFilenameFromUrl(convertedLink) .then(actualFilename => { @@ -875,12 +889,14 @@ function extractHubCloudLinks(url, referer) { if (displayFilename) titleParts.push(displayFilename); if (size) titleParts.push(size); const finalTitle = titleParts.join('\n'); - + resolve({ name: `4KHDHub - Pixeldrain${qualityLabel}`, title: finalTitle, url: convertedLink, - quality: quality + quality: quality, + provider: '4khdhub', + headers: {} }); }) .catch(() => { @@ -889,12 +905,14 @@ function extractHubCloudLinks(url, referer) { if (displayFilename) titleParts.push(displayFilename); if (size) titleParts.push(size); const finalTitle = titleParts.join('\n'); - + resolve({ name: `4KHDHub - Pixeldrain${qualityLabel}`, title: finalTitle, url: convertedLink, - quality: quality + quality: quality, + provider: '4khdhub', + headers: {} }); }); } else if (text.includes('S3 Server')) { @@ -907,12 +925,14 @@ function extractHubCloudLinks(url, referer) { if (displayFilename) titleParts.push(displayFilename); if (size) titleParts.push(size); const finalTitle = titleParts.join('\n'); - + resolve({ name: `4KHDHub - S3 Server${qualityLabel}`, title: finalTitle, url: link, - quality: quality + quality: quality, + provider: '4khdhub', + headers: {} }); }) .catch(() => { @@ -921,77 +941,83 @@ function extractHubCloudLinks(url, referer) { if (displayFilename) titleParts.push(displayFilename); if (size) titleParts.push(size); const finalTitle = titleParts.join('\n'); - + resolve({ name: `4KHDHub - S3 Server${qualityLabel}`, title: finalTitle, url: link, - quality: quality + quality: quality, + provider: '4khdhub', + headers: {} }); }); } else if (text.includes('10Gbps')) { console.log(`[4KHDHub] Button ${index + 1} is 10Gbps server, following redirects...`); // Handle 10Gbps server with multiple redirects let currentLink = link; - + const followRedirects = () => { - return makeRequest(currentLink, { + return makeRequest(currentLink, { parseHTML: false, - allowRedirects: false + allowRedirects: false }) - .then(response => { - const redirectUrl = response.headers['location']; - if (!redirectUrl) { - throw new Error('No redirect found'); - } - - console.log(`[4KHDHub] 10Gbps redirect: ${redirectUrl}`); - - if (redirectUrl.includes('id=')) { - // Final redirect, extract the link parameter - const finalLink = redirectUrl.split('link=')[1]; - if (finalLink) { - console.log(`[4KHDHub] 10Gbps final link: ${finalLink}`); - const decodedUrl = decodeURIComponent(finalLink); - // Get actual filename from HEAD request - return getFilenameFromUrl(decodedUrl) - .then(actualFilename => { - const displayFilename = actualFilename || headerDetails || 'Unknown'; - const titleParts = []; - if (displayFilename) titleParts.push(displayFilename); - if (size) titleParts.push(size); - const finalTitle = titleParts.join('\n'); - - return { - name: `4KHDHub - 10Gbps Server${qualityLabel}`, - title: finalTitle, - url: decodedUrl, - quality: quality - }; - }) - .catch(() => { - const displayFilename = headerDetails || 'Unknown'; - const titleParts = []; - if (displayFilename) titleParts.push(displayFilename); - if (size) titleParts.push(size); - const finalTitle = titleParts.join('\n'); - - return { - name: `4KHDHub - 10Gbps Server${qualityLabel}`, - title: finalTitle, - url: decodedUrl, - quality: quality - }; - }); + .then(response => { + const redirectUrl = response.headers['location']; + if (!redirectUrl) { + throw new Error('No redirect found'); } - throw new Error('Final link not found'); - } else { - currentLink = redirectUrl; - return followRedirects(); - } - }); + + console.log(`[4KHDHub] 10Gbps redirect: ${redirectUrl}`); + + if (redirectUrl.includes('id=')) { + // Final redirect, extract the link parameter + const finalLink = redirectUrl.split('link=')[1]; + if (finalLink) { + console.log(`[4KHDHub] 10Gbps final link: ${finalLink}`); + const decodedUrl = decodeURIComponent(finalLink); + // Get actual filename from HEAD request + return getFilenameFromUrl(decodedUrl) + .then(actualFilename => { + const displayFilename = actualFilename || headerDetails || 'Unknown'; + const titleParts = []; + if (displayFilename) titleParts.push(displayFilename); + if (size) titleParts.push(size); + const finalTitle = titleParts.join('\n'); + + return { + name: `4KHDHub - 10Gbps Server${qualityLabel}`, + title: finalTitle, + url: decodedUrl, + quality: quality, + provider: '4khdhub', + headers: {} + }; + }) + .catch(() => { + const displayFilename = headerDetails || 'Unknown'; + const titleParts = []; + if (displayFilename) titleParts.push(displayFilename); + if (size) titleParts.push(size); + const finalTitle = titleParts.join('\n'); + + return { + name: `4KHDHub - 10Gbps Server${qualityLabel}`, + title: finalTitle, + url: decodedUrl, + quality: quality, + provider: '4khdhub', + headers: {} + }; + }); + } + throw new Error('Final link not found'); + } else { + currentLink = redirectUrl; + return followRedirects(); + } + }); }; - + followRedirects() .then(result => { console.log(`[4KHDHub] 10Gbps processing completed`); @@ -1011,12 +1037,14 @@ function extractHubCloudLinks(url, referer) { if (displayFilename) titleParts.push(displayFilename); if (size) titleParts.push(size); const finalTitle = titleParts.join('\n'); - + resolve({ name: `4KHDHub - HubCloud${qualityLabel}`, title: finalTitle, url: link, - quality: quality + quality: quality, + provider: '4khdhub', + headers: {} }); }) .catch(() => { @@ -1025,18 +1053,20 @@ function extractHubCloudLinks(url, referer) { if (displayFilename) titleParts.push(displayFilename); if (size) titleParts.push(size); const finalTitle = titleParts.join('\n'); - + resolve({ name: `4KHDHub - HubCloud${qualityLabel}`, title: finalTitle, url: link, - quality: quality + quality: quality, + provider: '4khdhub', + headers: {} }); }); } }); }); - + return Promise.all(promises) .then(results => { const validResults = results.filter(result => result !== null); @@ -1056,7 +1086,7 @@ function searchContent(query) { if (!domains || !domains['4khdhub']) { throw new Error('Failed to get domain information'); } - + const baseUrl = domains['4khdhub']; const searchUrl = `${baseUrl}/?s=${encodeURIComponent(query)}`; return makeRequest(searchUrl, { parseHTML: true }) @@ -1065,13 +1095,13 @@ function searchContent(query) { .then(({ response, baseUrl }) => { const $ = response.$; const results = []; - + $('div.card-grid a').each((index, card) => { const $card = $(card); const title = $card.find('h3').text(); const href = $card.attr('href'); const posterUrl = $card.find('img').attr('src'); - + // Extract year from movie-card-meta element const metaElement = $card.find('.movie-card-meta'); let year = null; @@ -1082,7 +1112,7 @@ function searchContent(query) { year = parseInt(yearMatch[0]); } } - + if (title && href) { // Convert relative URLs to absolute URLs const absoluteUrl = href.startsWith('http') ? href : `${baseUrl}${href.startsWith('/') ? '' : '/'}${href}`; @@ -1094,7 +1124,7 @@ function searchContent(query) { }); } }); - + return results; }); } @@ -1109,9 +1139,9 @@ function loadContent(url) { const year = parseInt($('div.mt-2 span').text()) || null; const description = $('div.content-section p.mt-4').text().trim() || ''; const trailer = $('#trailer-btn').attr('data-trailer-url') || ''; - + const isMovie = tags.includes('Movies'); - + // Try multiple selectors to find download links let hrefs = []; const selectors = [ @@ -1123,7 +1153,7 @@ function loadContent(url) { '.btn[href]', 'a.btn' ]; - + for (const selector of selectors) { const links = $(selector) .map((i, a) => $(a).attr('href')) @@ -1135,7 +1165,7 @@ function loadContent(url) { break; } } - + if (hrefs.length === 0) { console.log('[4KHDHub] No download links found. Available links on page:'); const allLinks = $('a[href]') @@ -1145,7 +1175,7 @@ function loadContent(url) { .slice(0, 10); // Show first 10 links console.log(allLinks); } - + const content = { title, poster, @@ -1155,18 +1185,18 @@ function loadContent(url) { trailer, type: isMovie ? 'movie' : 'series' }; - + if (isMovie) { content.downloadLinks = hrefs; return Promise.resolve(content); } else { // Handle TV series episodes const episodesMap = new Map(); // used to dedupe episodes below - + console.log(`[4KHDHub] Looking for episode structure...`); const seasonItems = $('div.episodes-list div.season-item'); console.log(`[4KHDHub] Found ${seasonItems.length} season items`); - + if (seasonItems.length === 0) { // Try alternative episode structure selectors const altSelectors = [ @@ -1176,7 +1206,7 @@ function loadContent(url) { 'div[class*="episode"]', 'div[class*="season"]' ]; - + for (const selector of altSelectors) { const items = $(selector); if (items.length > 0) { @@ -1184,7 +1214,7 @@ function loadContent(url) { break; } } - + // If no episode structure found, treat all found links as general series links if (hrefs.length > 0) { console.log(`[4KHDHub] No episode structure found, using general links for series`); @@ -1201,21 +1231,21 @@ function loadContent(url) { seasonItems.each((i, seasonElement) => { const $seasonElement = $(seasonElement); const seasonText = $seasonElement.find('div.episode-number').text() || ''; - const seasonMatch = seasonText.match(/S?([1-9][0-9]*)/); + const seasonMatch = seasonText.match(/S?([1-9][0-9]*)/); const season = seasonMatch ? parseInt(seasonMatch[1]) : null; - + const episodeItems = $seasonElement.find('div.episode-download-item'); episodeItems.each((j, episodeItem) => { const $episodeItem = $(episodeItem); const episodeText = $episodeItem.find('div.episode-file-info span.badge-psa').text() || ''; - const episodeMatch = episodeText.match(/Episode-0*([1-9][0-9]*)/); + const episodeMatch = episodeText.match(/Episode-0*([1-9][0-9]*)/); const episode = episodeMatch ? parseInt(episodeMatch[1]) : null; - + const episodeHrefs = $episodeItem.find('a') .map((k, a) => $(a).attr('href')) .get() .filter(href => href && href.trim()); - + if (season && episode && episodeHrefs.length > 0) { const key = `${season}-${episode}`; if (!episodesMap.has(key)) { @@ -1229,13 +1259,13 @@ function loadContent(url) { } }); }); - + content.episodes = Array.from(episodesMap.values()).map(ep => ({ ...ep, downloadLinks: [...new Set(ep.downloadLinks)] // Remove duplicates })); } - + console.log(`[4KHDHub] Found ${content.episodes.length} episodes with links`); return Promise.resolve(content); } @@ -1244,11 +1274,11 @@ function loadContent(url) { function extractStreamingLinks(downloadLinks) { console.log(`[4KHDHub] Processing ${downloadLinks.length} download links...`); - + const promises = downloadLinks.map((link, index) => { return new Promise((resolve) => { console.log(`[4KHDHub] Processing link ${index + 1}: ${link}`); - + // Check if link needs redirect processing if (link.toLowerCase().includes('id=')) { console.log(`[4KHDHub] Link ${index + 1} needs redirect processing`); @@ -1271,7 +1301,7 @@ function extractStreamingLinks(downloadLinks) { } }); }); - + return Promise.all(promises) .then(results => { const validResults = results.filter(result => result !== null); @@ -1287,32 +1317,32 @@ function extractStreamingLinks(downloadLinks) { function extractHubDriveLinks(url, referer) { console.log(`[4KHDHub] Starting HubDrive extraction for: ${url}`); - + return makeRequest(url, { parseHTML: true }) .then(response => { const $ = response.$; - + console.log(`[4KHDHub] Got HubDrive page, looking for download button...`); - + // Extract filename and size information const size = $('i#size').text() || ''; const header = $('div.card-header').text() || ''; const quality = getIndexQuality(header); const headerDetails = cleanTitle(header); - + console.log(`[4KHDHub] HubDrive extracted info - Size: ${size}, Header: ${header}, Quality: ${quality}, HeaderDetails: ${headerDetails}`); - + // Extract filename from header for title display let filename = headerDetails || header || 'Unknown'; // Clean up the filename by removing common prefixes and file extensions filename = filename.replace(/^4kHDHub\.com\s*[-_]?\s*/i, '') - .replace(/\.[a-z0-9]{2,4}$/i, '') - .replace(/[._]/g, ' ') - .trim(); - + .replace(/\.[a-z0-9]{2,4}$/i, '') + .replace(/[._]/g, ' ') + .trim(); + // Use the exact selector from Kotlin code const downloadBtn = $('.btn.btn-primary.btn-user.btn-success1.m-1').first(); - + if (downloadBtn.length === 0) { console.log('[4KHDHub] Primary download button not found, trying alternative selectors...'); // Try alternative selectors @@ -1322,7 +1352,7 @@ function extractHubDriveLinks(url, referer) { 'a[href*="download"]', 'a.btn' ]; - + let foundBtn = null; for (const selector of alternatives) { foundBtn = $(selector).first(); @@ -1331,25 +1361,25 @@ function extractHubDriveLinks(url, referer) { break; } } - + if (!foundBtn || foundBtn.length === 0) { throw new Error('Download button not found with any selector'); } - + const href = foundBtn.attr('href'); if (!href) { throw new Error('Download link not found'); } - + console.log(`[4KHDHub] Found HubDrive download link: ${href}`); return processHubDriveLink(href, referer, filename, size, quality); } - + const href = downloadBtn.attr('href'); if (!href) { throw new Error('Download link not found'); } - + console.log(`[4KHDHub] Found HubDrive download link: ${href}`); return processHubDriveLink(href, referer, filename, size, quality); }) @@ -1368,13 +1398,13 @@ function processHubDriveLink(href, referer, filename = 'Unknown', size = '', qua console.log('[4KHDHub] HubDrive direct link found'); // Direct link or other extractor const qualityLabel = quality ? ` - ${quality}p` : ''; - + // Build labelExtras like the original extractor const labelExtras = []; if (filename && filename !== 'Unknown') labelExtras.push(`[${filename}]`); if (size) labelExtras.push(`[${size}]`); - // labelExtra removed (not used) - + // labelExtra removed (not used) + // Get actual filename from HEAD request return getFilenameFromUrl(href) .then(actualFilename => { @@ -1383,12 +1413,14 @@ function processHubDriveLink(href, referer, filename = 'Unknown', size = '', qua if (displayFilename) titleParts.push(displayFilename); if (size) titleParts.push(size); const finalTitle = titleParts.join('\n'); - + return [{ name: `4KHDHub - HubDrive${qualityLabel}`, title: finalTitle, url: href, - quality: quality + quality: quality, + provider: '4khdhub', + headers: {} }]; }) .catch(() => { @@ -1397,12 +1429,14 @@ function processHubDriveLink(href, referer, filename = 'Unknown', size = '', qua if (displayFilename) titleParts.push(displayFilename); if (size) titleParts.push(size); const finalTitle = titleParts.join('\n'); - + return [{ name: `4KHDHub - HubDrive${qualityLabel}`, title: finalTitle, url: href, - quality: quality + quality: quality, + provider: '4khdhub', + headers: {} }]; }); } @@ -1410,9 +1444,9 @@ function processHubDriveLink(href, referer, filename = 'Unknown', size = '', qua function processExtractorLink(link, resolve, linkNumber) { const linkLower = link.toLowerCase(); - + console.log(`[4KHDHub] Checking extractors for link ${linkNumber}: ${link}`); - + if (linkLower.includes('hubdrive')) { console.log(`[4KHDHub] Link ${linkNumber} matched HubDrive extractor`); extractHubDriveLinks(link, '4KHDHub') @@ -1443,13 +1477,13 @@ function processExtractorLink(link, resolve, linkNumber) { // Extract filename from URL const urlParts = link.split('/'); const filename = urlParts[urlParts.length - 1].replace(/\.[^/.]+$/, '').replace(/[._]/g, ' '); - + // Build labelExtras like the original extractor const labelExtras = []; if (filename) labelExtras.push(`[${filename}]`); labelExtras.push('[Direct Link]'); // labelExtra removed (not used) - + // Get actual filename from HEAD request getFilenameFromUrl(link) .then(actualFilename => { @@ -1458,12 +1492,14 @@ function processExtractorLink(link, resolve, linkNumber) { if (displayFilename) titleParts.push(displayFilename); titleParts.push('[Direct Link]'); const finalTitle = titleParts.join('\n'); - + resolve([{ name: '4KHDHub Direct Link', title: finalTitle, url: link, - quality: 1080 + quality: 1080, + provider: "4khdhub", + headers: {} }]); }) .catch(() => { @@ -1472,12 +1508,14 @@ function processExtractorLink(link, resolve, linkNumber) { if (displayFilename) titleParts.push(displayFilename); titleParts.push('[Direct Link]'); const finalTitle = titleParts.join('\n'); - + resolve([{ name: '4KHDHub Direct Link', title: finalTitle, url: link, - quality: 1080 + quality: 1080, + provider: "4khdhub", + headers: {} }]); }); } else { @@ -1490,16 +1528,16 @@ function processExtractorLink(link, resolve, linkNumber) { async function getTMDBDetails(tmdbId, mediaType) { const { getTmdbApiKey } = require('../utils/tmdbKey'); const TMDB_API_KEY = getTmdbApiKey(); - if(!TMDB_API_KEY){ + if (!TMDB_API_KEY) { console.warn('[4KHDHub] TMDB API key missing; skipping TMDB metadata fetch'); return null; } - + try { console.log(`[4KHDHub] Fetching ${mediaType} details for TMDB ID: ${tmdbId}`); - const response = await makeRequest(`https://api.themoviedb.org/3/${mediaType}/${tmdbId}?api_key=${TMDB_API_KEY}`); + const response = await makeRequest(`https://api.themoviedb.org/3/${mediaType}/${tmdbId}?api_key=${TMDB_API_KEY}`); const data = JSON.parse(response.body); - + if (mediaType === 'movie') { return { title: data.title, @@ -1523,12 +1561,12 @@ async function getTMDBDetails(tmdbId, mediaType) { async function get4KHDHubStreams(tmdbId, type, season = null, episode = null) { try { console.log(`[4KHDHub] Starting search for TMDB ID: ${tmdbId}, Type: ${type}${season ? `, Season: ${season}` : ''}${episode ? `, Episode: ${episode}` : ''}`); - + // Create cache key for resolved file hosting URLs const cacheKey = `4khdhub_resolved_urls_v3_${tmdbId}_${type}${season ? `_s${season}e${episode}` : ''}`; - + let streamingLinks = []; - + // 1. Check cache for resolved file hosting URLs first const cachedResolvedUrls = await getFromCache(cacheKey); if (cachedResolvedUrls && cachedResolvedUrls.length > 0) { @@ -1542,36 +1580,36 @@ async function get4KHDHubStreams(tmdbId, type, season = null, episode = null) { } else { console.log(`[4KHDHub] Cache MISS for ${cacheKey}. Fetching from source.`); } - + // Map type to TMDB API format const tmdbType = type === 'series' ? 'tv' : type; - + // Get TMDB details to get the actual title const tmdbDetails = await getTMDBDetails(tmdbId, tmdbType); if (!tmdbDetails || !tmdbDetails.title) { console.log(`[4KHDHub] Could not fetch TMDB details for ID: ${tmdbId}`); return []; } - + console.log(`[4KHDHub] TMDB Details: ${tmdbDetails.title} (${tmdbDetails.year || 'N/A'})`); - + // Enhanced search with fallback strategies let searchResults = []; let bestMatch = null; - + // Primary search using the actual title const searchQuery = tmdbDetails.title; searchResults = await searchContent(searchQuery); console.log(`[4KHDHub] Primary search found ${searchResults.length} results`); - + if (searchResults.length > 0) { bestMatch = findBestMatch(searchResults, tmdbDetails.title, tmdbDetails.year); } - + // Fallback search strategies if no good match found if (!bestMatch && searchResults.length > 0) { console.log(`[4KHDHub] No good match from primary search, trying fallback strategies...`); - + // Try search without year const titleWithoutYear = removeYear(tmdbDetails.title); if (titleWithoutYear !== tmdbDetails.title) { @@ -1585,14 +1623,14 @@ async function get4KHDHubStreams(tmdbId, type, season = null, episode = null) { } } } - + // Try search with comprehensive alternative title formats if (!bestMatch) { const alternativeQueries = generateAlternativeQueries( - tmdbDetails.title, + tmdbDetails.title, tmdbDetails.original_title ).filter(query => query !== tmdbDetails.title); // Exclude the original title we already tried - + for (const altQuery of alternativeQueries) { console.log(`[4KHDHub] Trying alternative search: "${altQuery}"`); const altResults = await searchContent(altQuery); @@ -1608,33 +1646,33 @@ async function get4KHDHubStreams(tmdbId, type, season = null, episode = null) { } } } - + if (searchResults.length === 0) { console.log(`[4KHDHub] No search results found for any query variation`); return []; } - + if (!bestMatch) { console.log(`[4KHDHub] No suitable match found for: ${tmdbDetails.title}`); return []; } - + console.log(`[4KHDHub] Using best match: ${bestMatch.title}`); - + const content = await loadContent(bestMatch.url); - + let downloadLinks = []; - + if (type === 'movie') { downloadLinks = content.downloadLinks || []; } else if ((type === 'series' || type === 'tv') && season && episode) { console.log(`[4KHDHub] Looking for Season ${season}, Episode ${episode}`); console.log(`[4KHDHub] Available episodes:`, content.episodes?.map(ep => `S${ep.season}E${ep.episode} (${ep.downloadLinks?.length || 0} links)`)); - - const targetEpisode = content.episodes?.find(ep => + + const targetEpisode = content.episodes?.find(ep => ep.season === parseInt(season) && ep.episode === parseInt(episode) ); - + if (targetEpisode) { console.log(`[4KHDHub] Found target episode S${targetEpisode.season}E${targetEpisode.episode} with ${targetEpisode.downloadLinks?.length || 0} links`); downloadLinks = targetEpisode.downloadLinks || []; @@ -1642,20 +1680,20 @@ async function get4KHDHubStreams(tmdbId, type, season = null, episode = null) { console.log(`[4KHDHub] Target episode S${season}E${episode} not found`); } } - + if (downloadLinks.length === 0) { console.log(`[4KHDHub] No download links found`); return []; } - + // Resolve redirect URLs to actual file hosting URLs console.log(`[4KHDHub] Resolving ${downloadLinks.length} redirect URLs to file hosting URLs...`); const resolvedUrls = []; - + for (let i = 0; i < downloadLinks.length; i++) { const link = downloadLinks[i]; console.log(`[4KHDHub] Resolving link ${i + 1}/${downloadLinks.length}: ${link}`); - + try { if (link.toLowerCase().includes('id=')) { // This is a redirect URL, resolve it @@ -1675,21 +1713,21 @@ async function get4KHDHubStreams(tmdbId, type, season = null, episode = null) { console.error(`[4KHDHub] Error resolving link ${i + 1} (${link}):`, error.message); } } - + if (resolvedUrls.length === 0) { console.log(`[4KHDHub] No URLs resolved successfully`); return []; } - + // Cache the resolved file hosting URLs console.log(`[4KHDHub] Caching ${resolvedUrls.length} resolved URLs for key: ${cacheKey}`); await saveToCache(cacheKey, resolvedUrls); - + // Process resolved URLs to get final streaming links console.log(`[4KHDHub] Processing ${resolvedUrls.length} resolved URLs to get streaming links.`); streamingLinks = await extractStreamingLinks(resolvedUrls); } - + // Filter out suspicious AMP/redirect URLs const filteredLinks = streamingLinks.filter(link => { const url = link.url.toLowerCase(); @@ -1698,7 +1736,7 @@ async function get4KHDHubStreams(tmdbId, type, season = null, episode = null) { 'bloggingvector.shop', 'cdn.ampproject.org' ]; - + const isSuspicious = suspiciousPatterns.some(pattern => url.includes(pattern)); if (isSuspicious) { console.log(`[4KHDHub] Filtered out suspicious URL: ${link.url}`); @@ -1706,53 +1744,52 @@ async function get4KHDHubStreams(tmdbId, type, season = null, episode = null) { } return true; }); - + // Remove duplicates based on URL const uniqueLinks = []; const seenUrls = new Set(); - + for (const link of filteredLinks) { if (!seenUrls.has(link.url)) { seenUrls.add(link.url); uniqueLinks.push(link); } } - + console.log(`[4KHDHub] Processing ${uniqueLinks.length} unique links (${streamingLinks.length - filteredLinks.length} suspicious URLs filtered, ${filteredLinks.length - uniqueLinks.length} duplicates removed)`); - + // Validate URLs if DISABLE_4KHDHUB_URL_VALIDATION is false let validatedLinks = uniqueLinks; const disableValidation = process.env.DISABLE_4KHDHUB_URL_VALIDATION === 'true'; - + if (!disableValidation) { console.log(`[4KHDHub] URL validation enabled, validating ${uniqueLinks.length} links...`); const validationPromises = uniqueLinks.map(async (link) => { const isValid = await validateUrl(link.url); return isValid ? link : null; }); - + const validationResults = await Promise.all(validationPromises); validatedLinks = validationResults.filter(link => link !== null); - + console.log(`[4KHDHub] URL validation complete: ${validatedLinks.length}/${uniqueLinks.length} links are valid`); } else { console.log(`[4KHDHub] URL validation disabled, skipping validation`); } - - // Normalize to generic stream object format + + // Normalize to generic stream object format const streams = validatedLinks.map(link => ({ name: link.name, // Don't add prefix since it's already included title: link.title || link.name, url: link.url, quality: `${link.quality}p`, - behaviorHints: { - bingeGroup: '4khdhub-streams' - } + provider: "4khdhub", + headers: {} })); - + console.log(`[4KHDHub] Returning ${streams.length} streams`); return streams; - + } catch (error) { console.error(`[4KHDHub] Error getting streams:`, error.message); return []; diff --git a/providers/MP4Hydra.js b/providers/MP4Hydra.js index 3081eb7..080867e 100644 --- a/providers/MP4Hydra.js +++ b/providers/MP4Hydra.js @@ -16,7 +16,7 @@ function processEpisode(episode, baseServer, serverName, serverNumber) { label: sub.label, url: `${baseServer}${sub.src}` })) : []; - + return { title: episode.show_title || episode.title, episode: episode.title, @@ -33,23 +33,23 @@ function processEpisode(episode, baseServer, serverName, serverNumber) { async function getMP4HydraStreams(tmdbId, mediaType, seasonNum = null, episodeNum = null) { try { console.log(`[MP4Hydra] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}, Season: ${seasonNum}, Episode: ${episodeNum}`); - + // Get TMDB details const details = await getTMDBDetails(tmdbId, mediaType); if (!details) { console.log(`[MP4Hydra] Could not fetch details for TMDB ID: ${tmdbId}`); return []; } - + console.log(`[MP4Hydra] Found title: ${details.title} (${details.year})`); - + // Generate slug in the format "movie-name-year" let slug = details.slug; if (mediaType === 'movie' && details.year) { slug = `${details.slug}-${details.year}`; } console.log(`[MP4Hydra] Using slug: ${slug}`); - + // Create form data for multipart/form-data request const formData = new FormData(); formData.append('v', '8'); @@ -59,7 +59,7 @@ async function getMP4HydraStreams(tmdbId, mediaType, seasonNum = null, episodeNu se: seasonNum, ep: episodeNum }])); - + // Make request to MP4Hydra API const response = await axios({ method: 'post', @@ -78,64 +78,61 @@ async function getMP4HydraStreams(tmdbId, mediaType, seasonNum = null, episodeNu }, timeout: 10000 }); - + // Process response if (response.data && response.data.playlist && response.data.playlist.length > 0) { const playlist = response.data.playlist; const servers = response.data.servers; - + console.log(`[MP4Hydra] Found ${playlist.length} videos`); console.log(`[MP4Hydra] Available servers: ${Object.keys(servers).join(', ')}`); - + // For TV shows, find the specific episode if (mediaType === 'tv' && seasonNum && episodeNum) { const paddedSeason = seasonNum.toString().padStart(2, '0'); const paddedEpisode = episodeNum.toString().padStart(2, '0'); const seasonEpisode = `S${paddedSeason}E${paddedEpisode}`; - - const targetEpisode = playlist.find(item => + + const targetEpisode = playlist.find(item => item.title && item.title.toUpperCase() === seasonEpisode.toUpperCase() ); - + if (!targetEpisode) { console.log(`[MP4Hydra] Could not find ${seasonEpisode}`); return []; } - + console.log(`[MP4Hydra] Found episode: ${targetEpisode.show_title || targetEpisode.title}`); - + // Process streams from main servers const streams = []; const serverConfig = [ { name: 'Beta', number: '#1' }, { name: 'Beta#3', number: '#2' } ]; - + serverConfig.forEach(server => { const serverName = server.name; const serverNumber = server.number; - + if (servers[serverName]) { const baseServer = servers[serverName]; console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`); - + const processedEpisode = processEpisode(targetEpisode, baseServer, serverName, serverNumber); - + // Convert to standard stream format streams.push({ title: `${details.title} - ${seasonEpisode} - ${processedEpisode.quality} [MP4Hydra ${serverNumber}]`, url: processedEpisode.videoUrl, quality: processedEpisode.quality, - provider: "MP4Hydra", - size: "Unknown size", - behaviorHints: { - notWebReady: true, - headers: { - 'Referer': 'https://mp4hydra.org/' - } + provider: "mp4hydra", + headers: { + 'Referer': 'https://mp4hydra.org/' } + }); - + // Add subtitle tracks if available if (processedEpisode.subtitles && processedEpisode.subtitles.length > 0) { streams[streams.length - 1].subtitles = processedEpisode.subtitles.map(sub => ({ @@ -145,43 +142,40 @@ async function getMP4HydraStreams(tmdbId, mediaType, seasonNum = null, episodeNu } } }); - + return streams; } - + // For movies, process all videos const streams = []; const serverConfig = [ { name: 'Beta', number: '#1' }, { name: 'Beta#3', number: '#2' } ]; - + serverConfig.forEach(server => { const serverName = server.name; const serverNumber = server.number; - + if (servers[serverName]) { const baseServer = servers[serverName]; console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`); - + playlist.forEach(item => { const processedItem = processEpisode(item, baseServer, serverName, serverNumber); - + // Convert to standard stream format streams.push({ title: `${details.title} - ${processedItem.quality} [MP4Hydra ${serverNumber}]`, url: processedItem.videoUrl, quality: processedItem.quality, - provider: "MP4Hydra", - size: "Unknown size", - behaviorHints: { - notWebReady: true, - headers: { - 'Referer': 'https://mp4hydra.org/' - } + provider: "mp4hydra", + headers: { + 'Referer': 'https://mp4hydra.org/' } + }); - + // Add subtitle tracks if available if (processedItem.subtitles && processedItem.subtitles.length > 0) { streams[streams.length - 1].subtitles = processedItem.subtitles.map(sub => ({ @@ -192,19 +186,19 @@ async function getMP4HydraStreams(tmdbId, mediaType, seasonNum = null, episodeNu }); } }); - + return streams; } else { // Try with original title if the first attempt failed if (details.title !== details.original_title) { return await tryAlternativeTitle(details, mediaType, seasonNum, episodeNum); } - + // Try without year for movies if (mediaType === 'movie' && details.year) { return await tryWithoutYear(details, mediaType, seasonNum, episodeNum); } - + console.log('[MP4Hydra] No streaming links found'); return []; } @@ -220,11 +214,11 @@ async function tryAlternativeTitle(details, mediaType, seasonNum, episodeNum) { console.log(`[MP4Hydra] Retrying with original title: ${details.original_title}`); const originalSlug = generateSlug(details.original_title); let originalFullSlug = originalSlug; - + if (mediaType === 'movie' && details.year) { originalFullSlug = `${originalSlug}-${details.year}`; } - + // Create new form data with original title const formData = new FormData(); formData.append('v', '8'); @@ -234,7 +228,7 @@ async function tryAlternativeTitle(details, mediaType, seasonNum, episodeNum) { se: seasonNum, ep: episodeNum }])); - + const response = await axios({ method: 'post', url: 'https://mp4hydra.org/info2?v=8', @@ -252,61 +246,58 @@ async function tryAlternativeTitle(details, mediaType, seasonNum, episodeNum) { }, timeout: 10000 }); - + // Process response if (response.data && response.data.playlist && response.data.playlist.length > 0) { const playlist = response.data.playlist; const servers = response.data.servers; - + console.log(`[MP4Hydra] Found ${playlist.length} videos with original title`); - + // For TV shows, find the specific episode if (mediaType === 'tv' && seasonNum && episodeNum) { const paddedSeason = seasonNum.toString().padStart(2, '0'); const paddedEpisode = episodeNum.toString().padStart(2, '0'); const seasonEpisode = `S${paddedSeason}E${paddedEpisode}`; - - const targetEpisode = playlist.find(item => + + const targetEpisode = playlist.find(item => item.title && item.title.toUpperCase() === seasonEpisode.toUpperCase() ); - + if (!targetEpisode) { console.log(`[MP4Hydra] Could not find ${seasonEpisode} with original title`); return []; } - + // Process streams from main servers const streams = []; const serverConfig = [ { name: 'Beta', number: '#1' }, { name: 'Beta#3', number: '#2' } ]; - + serverConfig.forEach(server => { const serverName = server.name; const serverNumber = server.number; - + if (servers[serverName]) { const baseServer = servers[serverName]; console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`); - + const processedEpisode = processEpisode(targetEpisode, baseServer, serverName, serverNumber); - + // Convert to standard stream format streams.push({ title: `${details.original_title} - ${seasonEpisode} - ${processedEpisode.quality} [MP4Hydra ${serverNumber}]`, url: processedEpisode.videoUrl, quality: processedEpisode.quality, - provider: "MP4Hydra", - size: "Unknown size", - behaviorHints: { - notWebReady: true, - headers: { - 'Referer': 'https://mp4hydra.org/' - } + provider: "mp4hydra", + headers: { + 'Referer': 'https://mp4hydra.org/' } + }); - + // Add subtitle tracks if available if (processedEpisode.subtitles && processedEpisode.subtitles.length > 0) { streams[streams.length - 1].subtitles = processedEpisode.subtitles.map(sub => ({ @@ -316,43 +307,36 @@ async function tryAlternativeTitle(details, mediaType, seasonNum, episodeNum) { } } }); - + return streams; } - + // For movies, process all videos const streams = []; const serverConfig = [ { name: 'Beta', number: '#1' }, { name: 'Beta#3', number: '#2' } ]; - + serverConfig.forEach(server => { const serverName = server.name; const serverNumber = server.number; - + if (servers[serverName]) { const baseServer = servers[serverName]; console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`); - + playlist.forEach(item => { const processedItem = processEpisode(item, baseServer, serverName, serverNumber); - + // Convert to standard stream format streams.push({ + name: `${details.original_title}`, title: `${details.original_title} - ${processedItem.quality} [MP4Hydra ${serverNumber}]`, url: processedItem.videoUrl, - quality: processedItem.quality, - provider: "MP4Hydra", - size: "Unknown size", - behaviorHints: { - notWebReady: true, - headers: { - 'Referer': 'https://mp4hydra.org/' - } - } + quality: processedItem.quality }); - + // Add subtitle tracks if available if (processedItem.subtitles && processedItem.subtitles.length > 0) { streams[streams.length - 1].subtitles = processedItem.subtitles.map(sub => ({ @@ -363,10 +347,10 @@ async function tryAlternativeTitle(details, mediaType, seasonNum, episodeNum) { }); } }); - + return streams; } - + return []; } catch (error) { console.error(`[MP4Hydra] Error with alternative title:`, error.message); @@ -379,7 +363,7 @@ async function tryWithoutYear(details, mediaType, seasonNum, episodeNum) { try { console.log(`[MP4Hydra] Retrying with title only (without year): ${details.title}`); const titleOnlySlug = details.slug; - + // Create new form data with title only const formData = new FormData(); formData.append('v', '8'); @@ -389,7 +373,7 @@ async function tryWithoutYear(details, mediaType, seasonNum, episodeNum) { se: seasonNum, ep: episodeNum }])); - + const response = await axios({ method: 'post', url: 'https://mp4hydra.org/info2?v=8', @@ -407,47 +391,45 @@ async function tryWithoutYear(details, mediaType, seasonNum, episodeNum) { }, timeout: 10000 }); - + // Process response if (response.data && response.data.playlist && response.data.playlist.length > 0) { const playlist = response.data.playlist; const servers = response.data.servers; - + console.log(`[MP4Hydra] Found ${playlist.length} videos with title only`); - + // For movies, process all videos const streams = []; const serverConfig = [ { name: 'Beta', number: '#1' }, { name: 'Beta#3', number: '#2' } ]; - + serverConfig.forEach(server => { const serverName = server.name; const serverNumber = server.number; - + if (servers[serverName]) { const baseServer = servers[serverName]; console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`); - + playlist.forEach(item => { const processedItem = processEpisode(item, baseServer, serverName, serverNumber); - + // Convert to standard stream format streams.push({ + name: `${details.title}`, title: `${details.title} - ${processedItem.quality} [MP4Hydra ${serverNumber}]`, url: processedItem.videoUrl, quality: processedItem.quality, - provider: "MP4Hydra", - size: "Unknown size", - behaviorHints: { - notWebReady: true, - headers: { - 'Referer': 'https://mp4hydra.org/' - } + provider: "mp4hydra", + headers: { + 'Referer': 'https://mp4hydra.org/' } + }); - + // Add subtitle tracks if available if (processedItem.subtitles && processedItem.subtitles.length > 0) { streams[streams.length - 1].subtitles = processedItem.subtitles.map(sub => ({ @@ -458,10 +440,10 @@ async function tryWithoutYear(details, mediaType, seasonNum, episodeNum) { }); } }); - + return streams; } - + return []; } catch (error) { console.error(`[MP4Hydra] Error with title-only:`, error.message); @@ -473,15 +455,15 @@ async function tryWithoutYear(details, mediaType, seasonNum, episodeNum) { async function getTMDBDetails(tmdbId, mediaType) { const { getTmdbApiKey } = require('../utils/tmdbKey'); const TMDB_API_KEY = getTmdbApiKey(); - if(!TMDB_API_KEY){ + if (!TMDB_API_KEY) { console.warn('[MP4Hydra] TMDB API key missing; skipping TMDB metadata fetch'); return null; } - + try { console.log(`[MP4Hydra] Fetching ${mediaType} details for TMDB ID: ${tmdbId}`); - const response = await axios.get(`https://api.themoviedb.org/3/${mediaType}/${tmdbId}?api_key=${TMDB_API_KEY}`); - + const response = await axios.get(`https://api.themoviedb.org/3/${mediaType}/${tmdbId}?api_key=${TMDB_API_KEY}`); + if (mediaType === 'movie') { return { title: response.data.title, diff --git a/providers/Showbox.js b/providers/Showbox.js index e33e567..7fa56f5 100644 --- a/providers/Showbox.js +++ b/providers/Showbox.js @@ -86,7 +86,7 @@ const getCookieForRequest = async (regionPreference = null, userCookie = null) = console.log('[CookieManager] Using user-supplied cookie from global state (legacy mode) for this cycle.'); baseCookieToUse = global.currentRequestUserCookie; } - // No file-based fallback rotation (env-only) + // No file-based fallback rotation (env-only) else { console.log('[CookieManager] No user-supplied cookies available for this cycle.'); baseCookieToUse = ''; @@ -156,9 +156,9 @@ const FEBBOX_FILE_SHARE_LIST_URL = "https://www.febbox.com/file/file_share_list" // const SCRAPER_API_URL = 'https://api.scraperapi.com/'; // TMDB key now sourced from config/env; no hardcoded public key const { getTmdbApiKey } = require('../utils/tmdbKey'); -function obtainTmdbKey(){ +function obtainTmdbKey() { const k = getTmdbApiKey(); - if(!k) console.warn('[Showbox] No TMDB API key available (multi-key set empty).'); + if (!k) console.warn('[Showbox] No TMDB API key available (multi-key set empty).'); return k; } const TMDB_BASE_URL = 'https://api.themoviedb.org/3'; @@ -534,9 +534,9 @@ const _searchAndExtractShowboxUrl = async (searchTerm, originalTmdbTitle, mediaY if (cachedResult === 'NO_MATCH_FOUND') return { url: null, score: -1 }; return { url: cachedResult, score: 10 }; } else if (cachedResult.result === 'NO_MATCH_FOUND') { - // For stream searches, always retry immediately - content availability can change anytime - console.log(` CACHE HIT: Previously no match found for ${originalTmdbTitle} ${mediaYear || ''}, but retrying immediately for fresh content.`); - // Continue to search logic below to retry immediately + // For stream searches, always retry immediately - content availability can change anytime + console.log(` CACHE HIT: Previously no match found for ${originalTmdbTitle} ${mediaYear || ''}, but retrying immediately for fresh content.`); + // Continue to search logic below to retry immediately } else { console.log(` CACHE HIT for ShowBox search best match URL (${originalTmdbTitle} ${mediaYear || ''}): ${cachedResult}`); return { url: cachedResult, score: 10 }; @@ -819,7 +819,7 @@ const _searchAndExtractShowboxUrl = async (searchTerm, originalTmdbTitle, mediaY // Allow for very close matches (like plurals or slight variations) if (tmdbWord.length > 3 && itemWord.length > 3) { return (itemWord.includes(tmdbWord) && itemWord.length - tmdbWord.length <= 2) || - (tmdbWord.includes(itemWord) && tmdbWord.length - itemWord.length <= 2); + (tmdbWord.includes(itemWord) && tmdbWord.length - itemWord.length <= 2); } return false; })) { @@ -988,8 +988,8 @@ const getShowboxUrlFromTmdbInfo = async (tmdbType, tmdbId, regionPreference = nu let tmdbData = await getFromCache(mainCacheKey, mainCacheSubDir); if (!tmdbData || process.env.DISABLE_CACHE === 'true') { - const TMDB_API_KEY = obtainTmdbKey(); - const tmdbApiUrl = `${TMDB_BASE_URL}/${tmdbType}/${tmdbId}?api_key=${TMDB_API_KEY}`; + const TMDB_API_KEY = obtainTmdbKey(); + const tmdbApiUrl = `${TMDB_BASE_URL}/${tmdbType}/${tmdbId}?api_key=${TMDB_API_KEY}`; console.log(` Fetching TMDB data from: ${tmdbApiUrl}`); try { const response = await axios.get(tmdbApiUrl, { timeout: 10000 }); @@ -1021,8 +1021,8 @@ const getShowboxUrlFromTmdbInfo = async (tmdbType, tmdbId, regionPreference = nu let tmdbAlternativeTitlesData = await getFromCache(altTitlesCacheKey, mainCacheSubDir); if (!tmdbAlternativeTitlesData || process.env.DISABLE_CACHE === 'true') { - const TMDB_API_KEY = obtainTmdbKey(); - const altTitlesApiUrl = `${TMDB_BASE_URL}/${tmdbType}/${tmdbId}/alternative_titles?api_key=${TMDB_API_KEY}`; + const TMDB_API_KEY = obtainTmdbKey(); + const altTitlesApiUrl = `${TMDB_BASE_URL}/${tmdbType}/${tmdbId}/alternative_titles?api_key=${TMDB_API_KEY}`; console.log(` Fetching TMDB alternative titles from: ${altTitlesApiUrl}`); try { const response = await axios.get(altTitlesApiUrl, { timeout: 10000 }); @@ -1047,8 +1047,8 @@ const getShowboxUrlFromTmdbInfo = async (tmdbType, tmdbId, regionPreference = nu let tmdbBackdropPaths = []; if (!tmdbImagesData || process.env.DISABLE_CACHE === 'true') { - const TMDB_API_KEY = obtainTmdbKey(); - const imagesApiUrl = `${TMDB_BASE_URL}/${tmdbType}/${tmdbId}/images?api_key=${TMDB_API_KEY}`; + const TMDB_API_KEY = obtainTmdbKey(); + const imagesApiUrl = `${TMDB_BASE_URL}/${tmdbType}/${tmdbId}/images?api_key=${TMDB_API_KEY}`; console.log(` Fetching TMDB images from: ${imagesApiUrl}`); try { const response = await axios.get(imagesApiUrl, { timeout: 10000 }); @@ -1352,9 +1352,9 @@ class ShowBoxScraper { // Store the region preference and user cookie this.regionPreference = regionPreference; this.userCookie = userCookie; - // ScraperAPI key support removed + // ScraperAPI key support removed + - this.baseHeaders = { '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', @@ -1372,10 +1372,10 @@ class ShowBoxScraper { }; } - + async _makeRequest(url, isJsonExpected = false) { - // Removed unused timerLabel variable (profiling optional) + // Removed unused timerLabel variable (profiling optional) // For JSON requests, we still cache normally since they're small if (isJsonExpected) { @@ -1393,7 +1393,7 @@ class ShowBoxScraper { // Always make direct requests const requestUrl = url; - // Removed console.time call (timerLabel removed) + // Removed console.time call (timerLabel removed) // Get the cookie with region preference if site requires it let cookieValue = null; @@ -1807,7 +1807,7 @@ const convertImdbToTmdb = async (imdbId, regionPreference = null, expectedType = if (findResults) { let result = null; - + // Context-aware prioritization based on expected type if (expectedType === 'tv' || expectedType === 'series') { // For series requests, prioritize TV results @@ -1835,7 +1835,7 @@ const convertImdbToTmdb = async (imdbId, regionPreference = null, expectedType = result = { tmdbId: String(findResults.tv_results[0].id), tmdbType: 'tv', title: findResults.tv_results[0].name || findResults.tv_results[0].original_name }; } } - + if (findResults.person_results && findResults.person_results.length > 0 && !result) { // Could handle other types if necessary, e.g. person, but for streams, movie/tv are key console.log(` IMDb ID ${imdbId} resolved to a person, not a movie or TV show on TMDB.`); @@ -1868,47 +1868,47 @@ const getStreamsFromTmdbId = async (tmdbType, tmdbId, seasonNum = null, episodeN console.log(`Getting streams for TMDB ${tmdbType}/${tmdbId}${seasonNum !== null ? `, Season ${seasonNum}` : ''}${episodeNum !== null ? `, Episode ${episodeNum}` : ''}`); // PRIMARY METHOD: Try PStream API first (if enabled) - - try { - // Get TMDB data to extract IMDB ID - const tmdbData = await getTmdbDataForPStream(tmdbType, tmdbId); - if (tmdbData && tmdbData.imdbId) { - console.log(`[PStream Primary] Attempting PStream API for IMDB ID: ${tmdbData.imdbId}`); - const pstreamStreams = await getStreamsFromPStreamAPI( - tmdbData.imdbId, - tmdbType, - seasonNum, - episodeNum, - regionPreference, - userCookie - ); + try { + // Get TMDB data to extract IMDB ID + const tmdbData = await getTmdbDataForPStream(tmdbType, tmdbId); + if (tmdbData && tmdbData.imdbId) { + console.log(`[PStream Primary] Attempting PStream API for IMDB ID: ${tmdbData.imdbId}`); - if (pstreamStreams && pstreamStreams.length > 0) { - console.log(`[PStream Primary] SUCCESS: Found ${pstreamStreams.length} streams from PStream API`); + const pstreamStreams = await getStreamsFromPStreamAPI( + tmdbData.imdbId, + tmdbType, + seasonNum, + episodeNum, + regionPreference, + userCookie + ); - // Fetch sizes for PStream streams - if (pstreamStreams.length > 0) { - const sizePromises = pstreamStreams.map(async (stream) => { - stream.size = await fetchStreamSize(stream.url); - return stream; - }); - await Promise.all(sizePromises); // removed unused streamsWithSizes var - } + if (pstreamStreams && pstreamStreams.length > 0) { + console.log(`[PStream Primary] SUCCESS: Found ${pstreamStreams.length} streams from PStream API`); - const sortedStreams = sortStreamsByQuality(pstreamStreams); - console.timeEnd(mainTimerLabel); - return sortedStreams; - } else { - console.log(`[PStream Primary] No streams found from PStream API, falling back to FebBox`); + // Fetch sizes for PStream streams + if (pstreamStreams.length > 0) { + const sizePromises = pstreamStreams.map(async (stream) => { + stream.size = await fetchStreamSize(stream.url); + return stream; + }); + await Promise.all(sizePromises); // removed unused streamsWithSizes var } + + const sortedStreams = sortStreamsByQuality(pstreamStreams); + console.timeEnd(mainTimerLabel); + return sortedStreams; } else { - console.log(`[PStream Primary] Could not get IMDB ID from TMDB data, falling back to FebBox`); + console.log(`[PStream Primary] No streams found from PStream API, falling back to FebBox`); } - } catch (error) { - console.error(`[PStream Primary] Error with PStream API: ${error.message}, falling back to FebBox`); + } else { + console.log(`[PStream Primary] Could not get IMDB ID from TMDB data, falling back to FebBox`); } - + } catch (error) { + console.error(`[PStream Primary] Error with PStream API: ${error.message}, falling back to FebBox`); + } + // FALLBACK METHOD: Use original FebBox/ShowBox logic console.log(`[Fallback] Using FebBox/ShowBox method`); const tmdbInfo = await getShowboxUrlFromTmdbInfo(tmdbType, tmdbId, regionPreference); @@ -1924,19 +1924,19 @@ const getStreamsFromTmdbId = async (tmdbType, tmdbId, seasonNum = null, episodeN // Check if we have cached FebBox share links to avoid redundant ShowBox requests let febboxShareInfos = tmdbInfo.febboxShareInfos; - + if (!febboxShareInfos) { // Only make ShowBox request if FebBox links are not cached console.log(`No cached FebBox links found, extracting from ShowBox: ${showboxUrl}`); - const showboxScraper = new ShowBoxScraper(regionPreference, userCookie); + const showboxScraper = new ShowBoxScraper(regionPreference, userCookie); febboxShareInfos = await showboxScraper.extractFebboxShareLinks(showboxUrl); - + if (!febboxShareInfos || febboxShareInfos.length === 0) { console.log(`No FebBox share links found for ${showboxUrl}`); console.timeEnd(mainTimerLabel); return []; } - + // Update cache with FebBox share links to avoid future ShowBox requests if (process.env.DISABLE_CACHE !== 'true') { const updatedTmdbInfo = { ...tmdbInfo, febboxShareInfos }; @@ -1980,10 +1980,12 @@ const getStreamsFromTmdbId = async (tmdbType, tmdbId, seasonNum = null, episodeN } } catch (e) { /* ignore if URL parsing fails */ } streamsFromThisShareInfo.push({ + name: streamTitle, title: streamTitle, url: source.url, quality: parseQualityFromLabel(source.label), - codecs: extractCodecDetails(key5FromDirectSource || streamTitle) + provider: 'Febbox Direct', + headers: {} }); } // If direct sources are found, original code used 'continue', @@ -2007,10 +2009,12 @@ const getStreamsFromTmdbId = async (tmdbType, tmdbId, seasonNum = null, episodeN const streamTitle = `${baseStreamTitle} - ${source.label}`; streamsFromThisShareInfo.push({ + name: streamTitle, title: streamTitle, url: source.url, quality: parseQualityFromLabel(source.label), - codecs: extractCodecDetails(source.detailedFilename || streamTitle) + provider: 'Febbox FID', + headers: {} }); } } @@ -2034,7 +2038,7 @@ const getStreamsFromTmdbId = async (tmdbType, tmdbId, seasonNum = null, episodeN stream.size = await fetchStreamSize(stream.url); return stream; }); - await Promise.all(sizePromises); // removed unused streamsWithSizes var + await Promise.all(sizePromises); // removed unused streamsWithSizes var console.timeEnd(`getStreamsFromTmdbId_fetchStreamSizes_${tmdbType}_${tmdbId}`); } @@ -2202,10 +2206,12 @@ const processShowWithSeasonsEpisodes = async (febboxUrl, showboxTitle, seasonNum for (const source of directSources) { const streamTitle = `${showboxTitle} - ${source.label}`; streamsForThisCall.push({ // MODIFIED: Push to streamsForThisCall + name: streamTitle, title: streamTitle, url: source.url, quality: parseQualityFromLabel(source.label), - codecs: extractCodecDetails(source.detailedFilename || streamTitle) + provider: 'Febbox Direct', + headers: {} }); } console.timeEnd(processTimerLabel); @@ -2222,10 +2228,12 @@ const processShowWithSeasonsEpisodes = async (febboxUrl, showboxTitle, seasonNum for (const source of sources) { const streamTitle = `${showboxTitle} - ${source.label}`; streamsForThisCall.push({ // MODIFIED: Push to streamsForThisCall + name: streamTitle, title: streamTitle, url: source.url, quality: parseQualityFromLabel(source.label), - codecs: extractCodecDetails(source.detailedFilename || streamTitle) + provider: 'Febbox FID', + headers: {} }); } } @@ -2506,10 +2514,12 @@ const processShowWithSeasonsEpisodes = async (febboxUrl, showboxTitle, seasonNum const streamTitle = `${showboxTitle} - S${seasonNum}${episodeNum && selectedEpisode && fid === selectedEpisode.fid ? `E${episodeNum}` : (episodeDetail ? `E${episodeDetail.episodeNum}` : '')} - ${episodeName} - ${source.label}`; streamsForThisCall.push({ // MODIFIED: Push to streamsForThisCall + name: streamTitle, title: streamTitle, url: source.url, quality: parseQualityFromLabel(source.label), - codecs: extractCodecDetails(source.detailedFilename || streamTitle) + provider: 'Febbox FID', + headers: {} }); } } else { @@ -2722,27 +2732,27 @@ const PSTREAM_ACCEPT_HEADER = 'pstream.org'; const PSTREAM_REGION_MAPPING = { // US Regions 'USA7': 'East', - 'USA6': 'West', + 'USA6': 'West', 'USA5': 'East', 'USA4': 'West', 'USA3': 'West', 'USA2': 'West', 'USA1': 'West', - + // European Regions 'UK3': 'Europe', 'FR1': 'Europe', 'DE2': 'Europe', 'EU1': 'Europe', 'EU2': 'Europe', - + // Asian Regions 'HK1': 'Asia', 'IN1': 'Asia', 'SZ': 'Asia', 'ASIA1': 'Asia', 'ASIA2': 'Asia', - + // Other Regions 'CA1': 'East', // Canada maps to East (US East) 'AU1': 'Asia' // Australia maps to Asia @@ -2753,12 +2763,12 @@ const mapRegionForPStream = (febboxRegion) => { if (!febboxRegion) { return PSTREAM_REGION_MAPPING[DEFAULT_OSS_REGION] || 'East'; } - + const mappedRegion = PSTREAM_REGION_MAPPING[febboxRegion]; if (mappedRegion) { return mappedRegion; } - + // Fallback to East if region not found console.log(`[PStream] Region ${febboxRegion} not found in mapping, using East as fallback`); return 'East'; @@ -2768,7 +2778,7 @@ const mapRegionForPStream = (febboxRegion) => { const getStreamsFromPStreamAPI = async (imdbId, tmdbType, seasonNum = null, episodeNum = null, regionPreference = null, userCookie = null) => { const timerLabel = `getStreamsFromPStreamAPI_${imdbId}_${tmdbType}` + (seasonNum ? `_s${seasonNum}` : '') + (episodeNum ? `_e${episodeNum}` : ''); console.time(timerLabel); - + try { // Build the PStream API URL let apiUrl; @@ -2780,12 +2790,12 @@ const getStreamsFromPStreamAPI = async (imdbId, tmdbType, seasonNum = null, epis console.log(`[PStream] Invalid parameters for ${tmdbType}: season=${seasonNum}, episode=${episodeNum}`); return []; } - + console.log(`[PStream] Making request to: ${apiUrl}`); - + // Get the region for PStream API const pstreamRegion = mapRegionForPStream(regionPreference || global.lastRequestedRegion?.used); - + // Prepare headers for PStream API const headers = { 'accept': PSTREAM_ACCEPT_HEADER, @@ -2801,7 +2811,7 @@ const getStreamsFromPStreamAPI = async (imdbId, tmdbType, seasonNum = null, epis 'sec-fetch-site': 'same-site', 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36' }; - + // Add ui-token if available const uiToken = await getUiTokenForPStream(userCookie); if (uiToken) { @@ -2810,30 +2820,30 @@ const getStreamsFromPStreamAPI = async (imdbId, tmdbType, seasonNum = null, epis } else { console.log(`[PStream] No ui-token available, making unauthenticated request`); } - + // Always use direct API URL const finalApiUrl = apiUrl; - + // Make the request const response = await axios.get(finalApiUrl, { headers: headers, timeout: 30000 }); - + if (!response.data) { console.log(`[PStream] No data received from API`); return []; } - + console.log(`[PStream] Response received, processing streams...`); - + // Process the PStream API response const streams = processPStreamResponse(response.data, tmdbType, seasonNum, episodeNum); - + console.timeEnd(timerLabel); console.log(`[PStream] Found ${streams.length} streams`); return streams; - + } catch (error) { console.error(`[PStream] Error fetching streams: ${error.message}`); if (error.response) { @@ -2847,90 +2857,84 @@ const getStreamsFromPStreamAPI = async (imdbId, tmdbType, seasonNum = null, epis // Function to process PStream API response and convert to standard stream format const processPStreamResponse = (apiResponse, tmdbType, seasonNum, episodeNum) => { const streams = []; - + try { // Log the actual response structure for debugging console.log(`[PStream] API Response structure:`, JSON.stringify(apiResponse, null, 2).substring(0, 1000)); console.log(`[PStream] API Response type:`, typeof apiResponse); console.log(`[PStream] API Response keys:`, apiResponse ? Object.keys(apiResponse) : 'null/undefined'); - + // Check if response has streams object (PStream API format) if (apiResponse && apiResponse.streams && typeof apiResponse.streams === 'object') { console.log(`[PStream] Found streams object with ${Object.keys(apiResponse.streams).length} quality levels`); - + // Process streams object format for (const [quality, url] of Object.entries(apiResponse.streams)) { if (url && typeof url === 'string') { // Use "Auto" for name and actual filename for title - const streamName = "Auto"; const streamTitle = apiResponse.name || `${tmdbType === 'movie' ? 'PStream' : `PStream - S${seasonNum}E${episodeNum}`} - ${quality}`; - + streams.push({ - name: streamName, + name: streamTitle, title: streamTitle, url: url, quality: parseQualityFromLabel(quality), - codecs: extractCodecDetails(apiResponse.name || quality), // Use filename for codec detection - size: apiResponse.size || 'Unknown size', - provider: 'PStream' + provider: 'PStream', + headers: {} }); } } - + console.log(`[PStream] Successfully parsed ${streams.length} streams from streams object`); return streams; } - + // Check if response has sources array (alternative format) if (apiResponse && Array.isArray(apiResponse.sources)) { console.log(`[PStream] Found sources array with ${apiResponse.sources.length} sources`); - + for (const source of apiResponse.sources) { if (!source.url || !source.quality) { continue; } - - const streamName = "Auto"; + const streamTitle = apiResponse.name || `${tmdbType === 'movie' ? 'PStream' : `PStream - S${seasonNum}E${episodeNum}`} - ${source.quality}`; - + streams.push({ - name: streamName, + name: streamTitle, title: streamTitle, url: source.url, quality: parseQualityFromLabel(source.quality), - codecs: source.codec ? [source.codec] : [], - size: source.size || 'Unknown size', - provider: 'PStream' + provider: 'PStream', + headers: {} }); } - + return streams; } - + // Check if response has data array (another alternative format) if (apiResponse && Array.isArray(apiResponse.data)) { console.log(`[PStream] Found 'data' array instead of 'sources'`); - + for (const item of apiResponse.data) { if (item.url || item.file) { - const streamName = "Auto"; const streamTitle = apiResponse.name || `${tmdbType === 'movie' ? 'PStream' : `PStream - S${seasonNum}E${episodeNum}`} - ${item.quality || item.label || 'Unknown'}`; - + streams.push({ - name: streamName, + name: streamTitle, title: streamTitle, url: item.url || item.file, quality: parseQualityFromLabel(item.quality || item.label), - codecs: item.codec ? [item.codec] : [], - size: item.size || 'Unknown size', - provider: 'PStream' + provider: 'PStream', + headers: {} }); } } - + return streams; } - + // No recognized format found console.log(`[PStream] No recognized stream format found in response`); if (apiResponse && apiResponse.message) { @@ -2938,13 +2942,13 @@ const processPStreamResponse = (apiResponse, tmdbType, seasonNum, episodeNum) => } else if (apiResponse && apiResponse.error) { console.log(`[PStream] API returned error:`, apiResponse.error); } - + return streams; - + } catch (error) { console.error(`[PStream] Error processing API response: ${error.message}`); } - + return streams; }; @@ -2952,15 +2956,15 @@ const processPStreamResponse = (apiResponse, tmdbType, seasonNum, episodeNum) => const getTmdbDataForPStream = async (tmdbType, tmdbId) => { const cacheSubDir = 'tmdb_api'; const cacheKey = `tmdb-${tmdbType}-${tmdbId}.json`; - + // Try cache first let tmdbData = await getFromCache(cacheKey, cacheSubDir); - + if (!tmdbData || process.env.DISABLE_CACHE === 'true') { - const TMDB_API_KEY = obtainTmdbKey(); - const tmdbApiUrl = `${TMDB_BASE_URL}/${tmdbType}/${tmdbId}?api_key=${TMDB_API_KEY}`; + const TMDB_API_KEY = obtainTmdbKey(); + const tmdbApiUrl = `${TMDB_BASE_URL}/${tmdbType}/${tmdbId}?api_key=${TMDB_API_KEY}`; console.log(` [PStream] Fetching TMDB data from: ${tmdbApiUrl}`); - + try { const response = await axios.get(tmdbApiUrl, { timeout: 10000 }); tmdbData = response.data; @@ -2974,22 +2978,22 @@ const getTmdbDataForPStream = async (tmdbType, tmdbId) => { return null; } } - + if (!tmdbData) { console.log(` [PStream] No TMDB data found for ${tmdbType}/${tmdbId}`); return null; } - + // Extract IMDB ID let imdbId = null; const externalIdsCacheKey = `tmdb-${tmdbType}-${tmdbId}-external_ids.json`; let externalIdsData = await getFromCache(externalIdsCacheKey, cacheSubDir); - + if (!externalIdsData || process.env.DISABLE_CACHE === 'true') { - const TMDB_API_KEY = obtainTmdbKey(); - const externalIdsApiUrl = `${TMDB_BASE_URL}/${tmdbType}/${tmdbId}/external_ids?api_key=${TMDB_API_KEY}`; + const TMDB_API_KEY = obtainTmdbKey(); + const externalIdsApiUrl = `${TMDB_BASE_URL}/${tmdbType}/${tmdbId}/external_ids?api_key=${TMDB_API_KEY}`; console.log(` [PStream] Fetching TMDB external IDs from: ${externalIdsApiUrl}`); - + try { const response = await axios.get(externalIdsApiUrl, { timeout: 10000 }); externalIdsData = response.data; @@ -3002,21 +3006,21 @@ const getTmdbDataForPStream = async (tmdbType, tmdbId) => { console.log(` [PStream] Error fetching TMDB external IDs: ${error.message}`); } } - + if (externalIdsData && externalIdsData.imdb_id) { imdbId = externalIdsData.imdb_id; } - + if (!imdbId) { console.log(` [PStream] No IMDB ID found for ${tmdbType}/${tmdbId}`); return null; } - + return { imdbId: imdbId, title: tmdbType === 'movie' ? tmdbData.title : tmdbData.name, - year: tmdbData.release_date ? new Date(tmdbData.release_date).getFullYear() : - tmdbData.first_air_date ? new Date(tmdbData.first_air_date).getFullYear() : null + year: tmdbData.release_date ? new Date(tmdbData.release_date).getFullYear() : + tmdbData.first_air_date ? new Date(tmdbData.first_air_date).getFullYear() : null }; }; @@ -3027,20 +3031,20 @@ const getUiTokenForPStream = async (userCookie = null) => { console.log('[PStream CookieManager] Using user-provided JWT token as ui-token'); return userCookie; } - + // If user provided a regular cookie, try to convert it to JWT format if (userCookie && !userCookie.startsWith('eyJ')) { console.log('[PStream CookieManager] Regular cookie provided, attempting to use as ui-token'); // For now, pass it through as-is - the API might handle conversion return userCookie; } - + // Check global cookie if (global.currentRequestUserCookie && global.currentRequestUserCookie.startsWith('eyJ')) { console.log('[PStream CookieManager] Using JWT token from global state'); return global.currentRequestUserCookie; } - + // Try intelligent selection from cookies array if available if (global.currentRequestConfig && Array.isArray(global.currentRequestConfig.cookies) && global.currentRequestConfig.cookies.length > 0) { try { @@ -3070,7 +3074,7 @@ const getUiTokenForPStream = async (userCookie = null) => { console.warn(`[PStream CookieManager] Multi-cookie selection failed: ${e.message}`); } } - + console.log('[PStream CookieManager] No valid ui-token available'); return null; }; diff --git a/providers/VidZee.js b/providers/VidZee.js index 5ef7d35..314110f 100644 --- a/providers/VidZee.js +++ b/providers/VidZee.js @@ -84,17 +84,13 @@ const getVidZeeStreams = async (tmdbId, mediaType, seasonNum, episodeNum) => { const language = sourceItem.language || sourceItem.lang; return { - title: `VidZee S${sr} - ${quality}`, + name: `VidZee Server${sr} - ${quality} - ${language}`, + title: `VidZee Server${sr} - ${quality} - ${language}`, url: sourceItem.link, // Use sourceItem.link for the URL quality: quality, - language: language, provider: "VidZee", - size: "Unknown size", - behaviorHints: { - notWebReady: true, - headers: { - 'Referer': 'https://core.vidzee.wtf/' - } + headers: { + 'Referer': 'https://core.vidzee.wtf/' } }; }).filter(stream => stream.url); diff --git a/providers/moviesclub.js b/providers/moviesclub.js new file mode 100644 index 0000000..27c105b --- /dev/null +++ b/providers/moviesclub.js @@ -0,0 +1,629 @@ +// MoviesClub Provider +// Extracts streams from moviesapi.club + +const axios = require('axios'); +const JsUnpacker = require('../utils/jsunpack'); + +async function getMoviesClubStreams(tmdbId, mediaType, season, episode) { + console.log(`[moviesclub] Fetching streams for ${mediaType} tmdbId=${tmdbId} season=${season} episode=${episode}`); + + const streams = []; + + try { + // Build the URL based on media type + let url; + if (mediaType === 'movie') { + url = `https://moviesapi.club/movie/${tmdbId}`; + } else if (mediaType === 'tv' && season && episode) { + url = `https://moviesapi.club/tv/${tmdbId}-${season}-${episode}`; + } else { + console.log('[moviesclub] Invalid parameters for TV show'); + return []; + } + + console.log(`[moviesclub] Fetching from: ${url}`); + + // Fetch the main page + const response = await axios.get(url, { + timeout: 10000, + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', + 'Accept-Language': 'en-US,en;q=0.5', + 'Accept-Encoding': 'gzip, deflate', + 'Connection': 'keep-alive', + 'Upgrade-Insecure-Requests': '1' + } + }); + + const html = response.data; + console.log(`[moviesclub] Fetched ${html.length} characters`); + + // Extract data-url from srvswitch-menu + const dataUrlMatch = html.match(/data-url="([^"]+)"/); + if (!dataUrlMatch) { + console.log('[moviesclub] No data-url found in HTML'); + return []; + } + + const embedUrl = dataUrlMatch[1]; + console.log(`[moviesclub] Found embed URL: ${embedUrl}`); + + // Check if this is a cdn.moviesapi.club embed URL with multiple servers + if (embedUrl.includes('cdn.moviesapi.club')) { + console.log('[moviesclub] Detected cdn.moviesapi.club embed URL, handling multiple servers'); + return await handleMultipleServers(embedUrl, mediaType, season, episode, tmdbId); + } + + // Handle regular embed URLs (existing logic) + return await handleSingleServer(embedUrl, mediaType, season, episode, tmdbId); + + } catch (error) { + console.error(`[moviesclub] Error:`, error.message); + return []; + } +} + +// Handle cdn.moviesapi.club URLs with multiple servers +async function handleMultipleServers(embedUrl, mediaType, season, episode, tmdbId) { + console.log(`[moviesclub] Handling multiple servers from: ${embedUrl}`); + + const streams = []; + + try { + // Fetch the embed page + const embedResponse = await axios.get(embedUrl, { + timeout: 10000, + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', + 'Accept-Language': 'en-US,en;q=0.5', + 'Accept-Encoding': 'gzip, deflate', + 'Connection': 'keep-alive' + } + }); + + const embedHtml = embedResponse.data; + console.log(`[moviesclub] Fetched embed page: ${embedHtml.length} characters`); + + // Extract server options from the HTML + const serverMatches = embedHtml.match(/
]*data-hash="([^"]+)"[^>]*>([^<]+)<\/div>/gi); + + if (!serverMatches || serverMatches.length === 0) { + console.log('[moviesclub] No server options found, trying direct iframe extraction'); + // Try to extract the main iframe src + const iframeMatch = embedHtml.match(/]*src="([^"]*)"[^>]*>/i); + if (iframeMatch) { + const iframeSrc = iframeMatch[1]; + console.log(`[moviesclub] Found iframe src: ${iframeSrc}`); + + // Try to decode if it's base64 + try { + const decodedSrc = Buffer.from(iframeSrc.split('/').pop(), 'base64').toString('utf-8'); + console.log(`[moviesclub] Decoded iframe src: ${decodedSrc}`); + + // If it's a URL, use it directly + if (decodedSrc.startsWith('http')) { + const stream = { + name: `MoviesClub - Direct Stream`, + title: `${mediaType === 'movie' ? 'Movie' : `S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`} ${tmdbId} - MoviesClub Direct`, + url: decodedSrc, + quality: extractQualityFromUrl(decodedSrc) || '720p' + }; + streams.push(stream); + console.log(`[moviesclub] Added direct stream from iframe`); + return streams; + } + } catch (decodeError) { + console.log(`[moviesclub] Could not decode iframe src: ${decodeError.message}`); + } + } + return streams; + } + + console.log(`[moviesclub] Found ${serverMatches.length} server options`); + + // Process each server + for (let i = 0; i < serverMatches.length; i++) { + const serverMatch = serverMatches[i]; + const hashMatch = serverMatch.match(/data-hash="([^"]+)"/); + const nameMatch = serverMatch.match(/>([^<]+)<\/div>/); + + if (!hashMatch || !nameMatch) { + console.log(`[moviesclub] Could not parse server ${i + 1}, skipping`); + continue; + } + + const encodedHash = hashMatch[1]; + const serverName = nameMatch[1].trim(); + + console.log(`[moviesclub] Processing server ${i + 1}: ${serverName}`); + + try { + // Construct the rcp URL from the hash + const rcpUrl = `https://cloudnestra.com/rcp/${encodedHash}`; + console.log(`[moviesclub] RCP URL: ${rcpUrl}`); + + // Fetch the rcp URL to get the JavaScript with prorcp URL + const rcpResponse = await axios.get(rcpUrl, { + timeout: 8000, + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'Accept-Language': 'en-US,en;q=0.5', + 'Accept-Encoding': 'gzip, deflate', + 'Connection': 'keep-alive', + 'Referer': embedUrl + } + }); + + const rcpHtml = rcpResponse.data; + console.log(`[moviesclub] RCP response length: ${rcpHtml.length}`); + + // Extract prorcp URL from the JavaScript + const prorcpMatch = rcpHtml.match(/src:\s*['"]([^'"]*prorcp[^'"]*)['"]/i); + if (!prorcpMatch) { + // Try alternative patterns + const altMatch = rcpHtml.match(/['"](\/prorcp\/[^'"]*)['"]/i); + if (altMatch) { + prorcpUrl = altMatch[1]; + } else { + console.log(`[moviesclub] No prorcp URL found in rcp response`); + continue; + } + } else { + prorcpUrl = prorcpMatch[1]; + } + + console.log(`[moviesclub] Found prorcp URL: ${prorcpUrl}`); + + // If it's a relative URL, make it absolute + if (prorcpUrl.startsWith('/')) { + prorcpUrl = `https://cloudnestra.com${prorcpUrl}`; + console.log(`[moviesclub] Converted to absolute URL: ${prorcpUrl}`); + } + + // Fetch the prorcp URL to get the player HTML + const prorcpResponse = await axios.get(prorcpUrl, { + timeout: 8000, + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'Accept-Language': 'en-US,en;q=0.5', + 'Accept-Encoding': 'gzip, deflate', + 'Connection': 'keep-alive', + 'Referer': rcpUrl + } + }); + + const prorcpHtml = prorcpResponse.data; + console.log(`[moviesclub] Prorcp response length: ${prorcpHtml.length}`); + + // Extract stream URL from Playerjs configuration + const streamUrl = extractStreamFromPlayerjsUpdated(prorcpHtml); + if (!streamUrl) { + console.log(`[moviesclub] No stream URL found in prorcp response`); + continue; + } + + console.log(`[moviesclub] Extracted stream URL: ${streamUrl.substring(0, 100)}...`); + + // Create stream object + const stream = { + name: `MoviesClub - ${serverName}`, + title: `${mediaType === 'movie' ? 'Movie' : `S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`} ${tmdbId} - ${serverName}`, + url: streamUrl, + quality: extractQualityFromUrl(streamUrl) || '720p', + headers: {} + }; + + streams.push(stream); + console.log(`[moviesclub] Added stream from server: ${serverName}`); + + } catch (serverError) { + console.error(`[moviesclub] Error processing server ${serverName}:`, serverError.message); + continue; + } + } + + console.log(`[moviesclub] Total streams found: ${streams.length}`); + return streams; + + } catch (error) { + console.error(`[moviesclub] Error in handleMultipleServers:`, error.message); + return []; + } +} + +// Handle regular embed URLs (existing logic) +async function handleSingleServer(embedUrl, mediaType, season, episode, tmdbId) { + console.log(`[moviesclub] Handling single server from: ${embedUrl}`); + + const streams = []; + + try { + // Fetch the embed page + const embedResponse = await axios.get(embedUrl, { + timeout: 10000, + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', + 'Accept-Language': 'en-US,en;q=0.5', + 'Accept-Encoding': 'gzip, deflate', + 'Connection': 'keep-alive' + } + }); + + const embedHtml = embedResponse.data; + console.log(`[moviesclub] Fetched embed page: ${embedHtml.length} characters`); + + // Look for packed JavaScript using JsUnpacker.detect() + const scriptTags = embedHtml.match(/]*>[\s\S]*?<\/script>/gi) || []; + let packedCode = null; + + for (const scriptTag of scriptTags) { + // Extract script content + const scriptContent = scriptTag.replace(/<\/?script[^>]*>/gi, '').trim(); + + if (scriptContent) { + // Create unpacker instance and check if it's packed + const unpacker = new JsUnpacker(scriptContent); + + if (unpacker.detect()) { + console.log(`[moviesclub] Found packed JavaScript in script tag, length: ${scriptContent.length}`); + packedCode = scriptContent; + break; + } + } + } + + if (!packedCode) { + console.log('[moviesclub] No packed JavaScript found'); + return []; + } + + // Unpack using JsUnpacker + const unpacker = new JsUnpacker(packedCode); + const unpackedCode = unpacker.unpack(); + + if (!unpackedCode) { + console.log('[moviesclub] Failed to unpack JavaScript'); + return []; + } + + console.log(`[moviesclub] Successfully unpacked code, length: ${unpackedCode.length}`); + + // Parse the player configuration from unpacked code + const playerConfig = parsePlayerConfig(unpackedCode); + if (!playerConfig) { + console.log('[moviesclub] Failed to parse player configuration'); + return []; + } + + console.log('[moviesclub] Parsed player config:', playerConfig); + + // Extract streams from the configuration + if (playerConfig.file) { + const stream = { + name: `MoviesClub - ${playerConfig.title || 'Server 1'}`, + title: playerConfig.title || `${mediaType === 'movie' ? 'Movie' : `S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`} ${tmdbId} - MoviesClub`, + url: playerConfig.file, + quality: extractQualityFromUrl(playerConfig.file) || '720p', + headers: {} + }; + + // Add poster if available + if (playerConfig.poster) { + stream.poster = playerConfig.poster; + } + + // Add subtitles if available + if (playerConfig.subtitle) { + stream.subtitles = parseSubtitles(playerConfig.subtitle); + } + + streams.push(stream); + } + + console.log(`[moviesclub] Found ${streams.length} streams`); + return streams; + + } catch (error) { + console.error(`[moviesclub] Error in handleSingleServer:`, error.message); + return []; + } +} + +// Extract stream URL from Playerjs configuration in HTML +function extractStreamFromPlayerjs(html) { + try { + // Look for the Playerjs constructor call + const playerMatch = html.match(/new Playerjs\(\s*(\{[^}]*\})\s*\)/); + if (!playerMatch) { + console.log('[moviesclub] No Playerjs constructor found in HTML'); + return null; + } + + const configStr = playerMatch[1]; + console.log(`[moviesclub] Found Playerjs config: ${configStr.substring(0, 200)}...`); + + // Extract the file URL from the configuration + const fileMatch = configStr.match(/file:\s*['"]([^'"]+)['"]/); + if (!fileMatch) { + console.log('[moviesclub] No file URL found in Playerjs config'); + return null; + } + + const streamUrl = fileMatch[1]; + console.log(`[moviesclub] Extracted stream URL from Playerjs: ${streamUrl.substring(0, 100)}...`); + + return streamUrl; + + } catch (error) { + console.error('[moviesclub] Error extracting stream from Playerjs:', error.message); + return null; + } +} + +// Extract streams from server response +function extractStreamsFromServer(serverData, serverName, mediaType, season, episode, tmdbId) { + const streams = []; + + try { + // Handle different response formats + if (typeof serverData === 'string') { + // Try to parse as JSON + try { + serverData = JSON.parse(serverData); + } catch (parseError) { + console.log(`[moviesclub] Server response is not JSON, treating as direct URL: ${serverData.substring(0, 100)}`); + // Treat as direct stream URL + if (serverData.startsWith('http')) { + const stream = { + name: `MoviesClub - ${serverName}`, + title: `${mediaType === 'movie' ? 'Movie' : `S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`} ${tmdbId} - ${serverName}`, + url: serverData, + quality: extractQualityFromUrl(serverData) || '720p', + headers: {} + }; + streams.push(stream); + } + return streams; + } + } + + // Handle JSON response + if (serverData && typeof serverData === 'object') { + // Look for common stream URL patterns + const possibleKeys = ['file', 'url', 'src', 'stream', 'video', 'source']; + + for (const key of possibleKeys) { + if (serverData[key] && typeof serverData[key] === 'string' && serverData[key].startsWith('http')) { + console.log(`[moviesclub] Found stream URL in key '${key}': ${serverData[key].substring(0, 100)}`); + + const stream = { + name: `MoviesClub - ${serverName}`, + title: `${mediaType === 'movie' ? 'Movie' : `S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`} ${tmdbId} - ${serverName}`, + url: serverData[key], + quality: extractQualityFromUrl(serverData[key]) || '720p', + headers: {} + }; + + // Add poster if available + if (serverData.poster) { + stream.poster = serverData.poster; + } + + // Add subtitles if available + if (serverData.subtitle || serverData.subtitles) { + const subtitleData = serverData.subtitle || serverData.subtitles; + stream.subtitles = parseSubtitles(subtitleData); + } + + streams.push(stream); + break; // Only take the first valid stream URL + } + } + + // If no direct URL found, check for nested objects + if (streams.length === 0) { + for (const key of possibleKeys) { + if (serverData[key] && typeof serverData[key] === 'object') { + const nestedStreams = extractStreamsFromServer(serverData[key], serverName, mediaType, season, episode, tmdbId); + streams.push(...nestedStreams); + } + } + } + } + + } catch (error) { + console.error(`[moviesclub] Error extracting streams from server ${serverName}:`, error.message); + } + + return streams; +} + +// Extract stream URL from Playerjs configuration in HTML +function extractStreamFromPlayerjs(html) { + try { + // Look for the Playerjs constructor call + const playerMatch = html.match(/new Playerjs\(\s*(\{[^}]*\})\s*\)/); + if (!playerMatch) { + console.log('[moviesclub] No Playerjs constructor found in HTML'); + return null; + } + + const configStr = playerMatch[1]; + console.log(`[moviesclub] Found Playerjs config: ${configStr.substring(0, 200)}...`); + + // Extract the file URL from the configuration + const fileMatch = configStr.match(/file:\s*['"]([^'"]+)['"]/); + if (!fileMatch) { + console.log('[moviesclub] No file URL found in Playerjs config'); + return null; + } + + const streamUrl = fileMatch[1]; + console.log(`[moviesclub] Extracted stream URL from Playerjs: ${streamUrl.substring(0, 100)}...`); + + return streamUrl; + + } catch (error) { + console.error('[moviesclub] Error extracting stream from Playerjs:', error.message); + return null; + } +} + +// Parse player configuration from unpacked JavaScript +function parsePlayerConfig(unpackedCode) { + try { + // Look for the player configuration object + const configMatch = unpackedCode.match(/new Playerjs\(\s*(\{[^}]*\})\s*\)/); + if (!configMatch) { + return null; + } + + // Extract the configuration object + const configStr = configMatch[1]; + + // Parse the configuration (this is a simple parser for the expected format) + const config = {}; + + // Extract key-value pairs + const pairs = configStr.match(/(\w+):"([^"]*)"/g) || []; + pairs.forEach(pair => { + const [, key, value] = pair.match(/(\w+):"([^"]*)"/); + config[key] = value; + }); + + // Also handle non-quoted values + const numberPairs = configStr.match(/(\w+):(\d+(?:\.\d+)?)/g) || []; + numberPairs.forEach(pair => { + const [, key, value] = pair.match(/(\w+):(\d+(?:\.\d+)?)/); + config[key] = parseFloat(value); + }); + + return config; + } catch (error) { + console.error('[moviesclub] Error parsing player config:', error.message); + return null; + } +} + +// Parse subtitles from the subtitle string +function parseSubtitles(subtitleStr) { + if (!subtitleStr) return []; + + const subtitles = []; + const subtitleMatches = subtitleStr.match(/\[([^\]]+)\]([^,\[]*)/g); + + if (subtitleMatches) { + subtitleMatches.forEach(match => { + const [, language, url] = match.match(/\[([^\]]+)\]([^,\[]*)/); + if (url && url.trim()) { + subtitles.push({ + language: language.trim(), + url: url.trim(), + type: 'vtt' + }); + } + }); + } + + return subtitles; +} + +// Extract quality from URL +function extractQualityFromUrl(url) { + if (!url) return '720p'; + + const qualityPatterns = [ + { pattern: /1080p|1080/i, quality: '1080p' }, + { pattern: /720p|720/i, quality: '720p' }, + { pattern: /480p|480/i, quality: '480p' }, + { pattern: /360p|360/i, quality: '360p' }, + { pattern: /2160p|4k|uhd/i, quality: '2160p' } + ]; + + for (const { pattern, quality } of qualityPatterns) { + if (pattern.test(url)) { + return quality; + } + } + + return '720p'; +} + +// Extract stream URL from Playerjs configuration in HTML (updated version) +function extractStreamFromPlayerjsUpdated(html) { + try { + // Look for the Playerjs constructor call + const playerMatch = html.match(/new Playerjs\(\s*(\{[^}]*\})\s*\)/); + if (!playerMatch) { + console.log('[moviesclub] No Playerjs constructor found in HTML'); + return null; + } + + const configStr = playerMatch[1]; + console.log(`[moviesclub] Found Playerjs config: ${configStr.substring(0, 200)}...`); + + // Extract the file URL from the configuration + let streamUrl = null; + + // Try multiple patterns to extract the file URL + const patterns = [ + /file:\s*['"]([^'"]+)['"]/, + /file:\s*([^,\s}]+)/, + /file\s*:\s*['"]([^'"]+)['"]/, + /file\s*:\s*([^,\s}]+)/ + ]; + + for (const pattern of patterns) { + const match = configStr.match(pattern); + if (match && match[1] && match[1].trim() !== '' && match[1].trim() !== '""') { + streamUrl = match[1].replace(/['"]/g, '').trim(); + console.log(`[moviesclub] Found stream URL with pattern: ${streamUrl.substring(0, 100)}...`); + break; + } + } + + // If no URL found in config, look for streaming URLs in the HTML + if (!streamUrl) { + console.log('[moviesclub] No file URL in Playerjs config, searching HTML for streaming URLs...'); + + // Look for m3u8 URLs + const m3u8Match = html.match(/https?:\/\/[^'"\s]+\.m3u8[^'"\s]*/i); + if (m3u8Match) { + streamUrl = m3u8Match[0]; + console.log(`[moviesclub] Found m3u8 URL in HTML: ${streamUrl.substring(0, 100)}...`); + } + + // Look for other streaming URLs + if (!streamUrl) { + const streamMatch = html.match(/https?:\/\/[^'"\s]+(?:\.mp4|\.m3u8|\.ts)[^'"\s]*/i); + if (streamMatch) { + streamUrl = streamMatch[0]; + console.log(`[moviesclub] Found streaming URL in HTML: ${streamUrl.substring(0, 100)}...`); + } + } + } + + if (!streamUrl) { + console.log('[moviesclub] No file URL found in Playerjs config after trying all patterns'); + console.log('[moviesclub] Config string:', configStr.substring(0, 500)); + return null; + } + + console.log(`[moviesclub] Extracted stream URL from Playerjs: ${streamUrl.substring(0, 100)}...`); + + return streamUrl; + + } catch (error) { + console.error('[moviesclub] Error extracting stream from Playerjs:', error.message); + return null; + } +} + +module.exports = { + getMoviesClubStreams +}; \ No newline at end of file diff --git a/providers/moviesmod.js b/providers/moviesmod.js index 9d0e53f..a0cb631 100644 --- a/providers/moviesmod.js +++ b/providers/moviesmod.js @@ -1106,6 +1106,8 @@ async function getMoviesModStreams(tmdbId, mediaType, seasonNum = null, episodeN title: `${cleanFileName}\n${sizeInfo || ''}${techDetailsString}`, url: selectedResult.url, quality: actualQuality, + provider: 'moviesmod', + headers: {} }; } else { console.warn(`[MoviesMod] Unsupported URL type for final processing: ${targetLink?.url}`); diff --git a/providers/registry.js b/providers/registry.js index 0cca1ed..d03c9a0 100644 --- a/providers/registry.js +++ b/providers/registry.js @@ -1,12 +1,23 @@ const { config } = require('../utils/config'); -// Lazy load showbox to avoid early side-effects; temporarily disabled until stabilized -let showbox = null; -let k4hdhub = null; // 4khdhub provider lazy ref -let moviesmod = null; // moviesmod provider lazy ref -let mp4hydra = null; // MP4Hydra provider lazy ref -let vidzee = null; // VidZee provider lazy ref -let vixsrc = null; // vixsrc provider lazy ref -let xprime = null; // xprime provider lazy ref +const fs = require('fs'); +const path = require('path'); + +// Lazy load cache for providers +const providerCache = new Map(); + +// Function name mappings for existing providers +const providerFunctionMap = { + 'Showbox.js': 'getStreamsFromTmdbId', + '4khdhub.js': 'get4KHDHubStreams', + 'moviesmod.js': 'getMoviesModStreams', + 'MP4Hydra.js': 'getMP4HydraStreams', + 'VidZee.js': 'getVidZeeStreams', + 'vixsrc.js': 'getVixsrcStreams', + 'xprime.js': 'getXprimeStreams', + 'uhdmovies.js': 'getUHDMoviesStreams', + 'moviesclub.js': 'getMoviesClubStreams', +}; + // Stats for debug endpoint let lastCookieStats = { selected: null, index: null, total: 0, remainingMB: null, timestamp: null }; @@ -14,248 +25,153 @@ async function getEffectiveCookies() { return Array.isArray(config.febboxCookies) ? config.febboxCookies : []; } -// Adapter for Showbox provider using existing exported getStreamsFromTmdbId -async function fetchShowbox(ctx) { - if (!showbox) { - try { showbox = require('./Showbox.js'); } catch (e) { console.error('Failed to load Showbox provider:', e.message); return []; } - } - const { getTmdbApiKey } = require('../utils/tmdbKey'); - const tmdbApiKey = getTmdbApiKey(); - if (!tmdbApiKey) { - console.warn('[registry] showbox skipped: TMDB API key missing'); - return []; - } - const cookies = await getEffectiveCookies(); - const previousConfig = global.currentRequestConfig; - global.currentRequestConfig = { ...(previousConfig || {}) }; - let selected = null; - if (cookies.length > 0) { - const index = Math.floor(Math.random() * cookies.length); - selected = cookies[index]; - global.currentRequestConfig.cookie = selected.startsWith('ui=') ? selected : `ui=${selected}`; - global.currentRequestConfig.cookies = cookies.map(c => c.startsWith('ui=') ? c : `ui=${c}`); - lastCookieStats = { selected: selected.slice(0, 16) + '...', index, total: cookies.length, remainingMB: null, timestamp: Date.now() }; - console.log(`[registry] Cookie random pick index=${index} total=${cookies.length}`); - } - try { - const tmdbType = ctx.type === 'movie' ? 'movie' : 'tv'; - console.log(`[registry] Fetching Showbox streams for ${tmdbType} tmdbId=${ctx.tmdbId} season=${ctx.season||''} episode=${ctx.episode||''}`); - const t0 = Date.now(); - const streams = await showbox.getStreamsFromTmdbId(tmdbType, ctx.tmdbId, ctx.season || null, ctx.episode || null, null, selected); - const durationMs = Date.now() - t0; - console.log(`[registry] Provider showbox fetch duration ${durationMs}ms`); - if (global.currentRequestUserCookieRemainingMB != null) { - lastCookieStats.remainingMB = global.currentRequestUserCookieRemainingMB; - } - console.log(`[registry] Showbox returned ${Array.isArray(streams)?streams.length:0} streams`); - return Array.isArray(streams) ? streams : []; - } catch (e) { - console.error('[registry] Showbox fetch error:', e.message); - return []; - } finally { - global.currentRequestConfig = previousConfig || {}; - } +// Get all provider files +function getProviderFiles() { + const providersDir = path.join(__dirname); + return fs.readdirSync(providersDir) + .filter(file => file.endsWith('.js') && file !== 'registry.js' && file !== 'vidsrcextractor.js') + .map(file => ({ + name: path.parse(file).name.toLowerCase(), + file: file, + functionName: providerFunctionMap[file] + })); } +// Load a provider module +function loadProvider(providerFile) { + if (!providerCache.has(providerFile)) { + try { + providerCache.set(providerFile, require(path.join(__dirname, providerFile))); + } catch (e) { + console.error(`Failed to load provider ${providerFile}:`, e.message); + return null; + } + } + return providerCache.get(providerFile); +} + +// Create fetch function for a provider +function createFetchFunction(providerInfo) { + return async function(ctx) { + const module = loadProvider(providerInfo.file); + if (!module) return []; + + const funcName = providerInfo.functionName; + if (!module[funcName]) { + console.error(`Provider ${providerInfo.name} does not export ${funcName}`); + return []; + } + + try { + const mediaType = ctx.type === 'movie' ? 'movie' : 'tv'; + const t0 = Date.now(); + + let result; + if (providerInfo.name === 'showbox') { + // Special case for Showbox with TMDB key and cookies + const { getTmdbApiKey } = require('../utils/tmdbKey'); + const tmdbApiKey = getTmdbApiKey(); + if (!tmdbApiKey) { + console.warn('[registry] showbox skipped: TMDB API key missing'); + return []; + } + const cookies = await getEffectiveCookies(); + const previousConfig = global.currentRequestConfig; + global.currentRequestConfig = { ...(previousConfig || {}) }; + let selected = null; + if (cookies.length > 0) { + const index = Math.floor(Math.random() * cookies.length); + selected = cookies[index]; + global.currentRequestConfig.cookie = selected.startsWith('ui=') ? selected : `ui=${selected}`; + global.currentRequestConfig.cookies = cookies.map(c => c.startsWith('ui=') ? c : `ui=${c}`); + lastCookieStats = { selected: selected.slice(0, 16) + '...', index, total: cookies.length, remainingMB: null, timestamp: Date.now() }; + console.log(`[registry] Cookie random pick index=${index} total=${cookies.length}`); + } + result = await module[funcName](mediaType, ctx.tmdbId, ctx.season || null, ctx.episode || null, null, selected); + if (global.currentRequestUserCookieRemainingMB != null) { + lastCookieStats.remainingMB = global.currentRequestUserCookieRemainingMB; + } + global.currentRequestConfig = previousConfig || {}; + } else if (providerInfo.name === 'xprime') { + // Special case for Xprime with TMDB metadata lookup + const { getTmdbApiKey } = require('../utils/tmdbKey'); + const tmdbApiKey = getTmdbApiKey(); + if (!tmdbApiKey) { + console.warn('[registry] xprime skipped: TMDB API key missing'); + return []; + } + // Fetch minimal TMDB metadata for title + year + let title = null; let year = null; + try { + const axios = require('axios'); + const metaUrl = `https://api.themoviedb.org/3/${mediaType}/${ctx.tmdbId}?api_key=${tmdbApiKey}`; + const { data: meta } = await axios.get(metaUrl, { timeout: 8000 }); + title = mediaType === 'movie' ? (meta.title || meta.original_title) : (meta.name || meta.original_name); + const dateStr = mediaType === 'movie' ? meta.release_date : meta.first_air_date; + if (dateStr) year = dateStr.split('-')[0]; + } catch (mErr) { + console.warn('[registry] xprime metadata fetch failed:', mErr.message); + return []; + } + if (!title || !year) { + console.log('[registry] xprime missing title/year after TMDB lookup'); + return []; + } + result = await module[funcName](title, year, mediaType, ctx.season || null, ctx.episode || null); + // Normalize quality labels + if (Array.isArray(result)) { + result = result.map(s => { + let q = s.quality || 'Unknown'; + if (!/(\d{3,4})p/.test(q.toLowerCase())) { + const qLower = q.toLowerCase(); + if (qLower.includes('fhd') || qLower.includes('full')) q = '1080p'; + else if (qLower === 'hd' || qLower.includes('720')) q = '720p'; + else if (qLower.includes('sd') || qLower.includes('480')) q = '480p'; + else if (qLower.includes('cam')) q = '360p'; + } + return { ...s, quality: q, name: s.name || 'Xprime', provider: 'xprime' }; + }); + } + } else { + // Standard provider call + result = await module[funcName](ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null); + } + + const durationMs = Date.now() - t0; + console.log(`[registry] ${providerInfo.name} fetch duration ${durationMs}ms`); + + if (!Array.isArray(result)) return []; + + // Add provider name if not present + return result.map(s => ({ ...s, provider: s.provider || providerInfo.name })); + + } catch (e) { + console.error(`[registry] ${providerInfo.name} fetch error:`, e.message); + return []; + } + }; +} + +// Initialize providers +const providerFiles = getProviderFiles(); const providers = []; -if (config.enableShowboxProvider) { - providers.push({ name: 'showbox', enabled: true, fetch: fetchShowbox }); -} else { - console.log('[registry] showbox disabled via config'); -} +for (const providerInfo of providerFiles) { + const enableFlag = `enable${providerInfo.name.charAt(0).toUpperCase() + providerInfo.name.slice(1)}Provider`; + const enabled = config[enableFlag] !== false; - -// 4KHDHub adapter -async function fetch4khdhub(ctx) { - if (!k4hdhub) { - try { k4hdhub = require('./4khdhub.js'); } catch (e) { console.error('Failed to load 4khdhub provider:', e.message); return []; } - } - try { - const mediaType = ctx.type === 'movie' ? 'movie' : 'tv'; - const t0 = Date.now(); - const result = await k4hdhub.get4KHDHubStreams(ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null); - const durationMs = Date.now() - t0; - console.log(`[registry] 4khdhub fetch duration ${durationMs}ms`); - if (!Array.isArray(result)) return []; - return result.map(s => ({ ...s, name: s.name || '4KHDHub', provider: '4khdhub' })); - } catch (e) { - console.error('[registry] 4khdhub fetch error:', e.message); - return []; - } -} - -if (config.enable4khdhubProvider) { - providers.push({ name: '4khdhub', enabled: true, fetch: fetch4khdhub }); - console.log('[registry] 4khdhub provider enabled'); -} else { - console.log('[registry] 4khdhub disabled via config'); -} - -// MoviesMod adapter -async function fetchMoviesMod(ctx) { - if (!moviesmod) { - try { moviesmod = require('./moviesmod.js'); } catch (e) { console.error('Failed to load moviesmod provider:', e.message); return []; } - } - try { - const mediaType = ctx.type === 'movie' ? 'movie' : 'tv'; - const t0 = Date.now(); - const result = await moviesmod.getMoviesModStreams(ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null); - const durationMs = Date.now() - t0; - console.log(`[registry] moviesmod fetch duration ${durationMs}ms`); - if (!Array.isArray(result)) return []; - return result.map(s => ({ ...s, name: s.name || 'MoviesMod', provider: 'moviesmod' })); - } catch (e) { - console.error('[registry] moviesmod fetch error:', e.message); - return []; - } -} - -if (config.enableMoviesmodProvider) { - providers.push({ name: 'moviesmod', enabled: true, fetch: fetchMoviesMod }); - console.log('[registry] moviesmod provider enabled'); -} else { - console.log('[registry] moviesmod disabled via config'); -} - -// MP4Hydra adapter -async function fetchMp4hydra(ctx) { - if (!mp4hydra) { - try { mp4hydra = require('./MP4Hydra.js'); } catch (e) { console.error('Failed to load MP4Hydra provider:', e.message); return []; } - } - try { - const mediaType = ctx.type === 'movie' ? 'movie' : 'tv'; - const t0 = Date.now(); - const result = await mp4hydra.getMP4HydraStreams(ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null); - const durationMs = Date.now() - t0; - console.log(`[registry] mp4hydra fetch duration ${durationMs}ms`); - if (!Array.isArray(result)) return []; - return result.map(s => ({ ...s, name: s.name || 'MP4Hydra', provider: 'mp4hydra' })); - } catch (e) { - console.error('[registry] mp4hydra fetch error:', e.message); - return []; - } -} - -if (config.enableMp4hydraProvider) { - providers.push({ name: 'mp4hydra', enabled: true, fetch: fetchMp4hydra }); - console.log('[registry] mp4hydra provider enabled'); -} else { - console.log('[registry] mp4hydra disabled via config'); -} - -// VidZee adapter -async function fetchVidZee(ctx) { - if (!vidzee) { - try { vidzee = require('./VidZee.js'); } catch (e) { console.error('Failed to load VidZee provider:', e.message); return []; } - } - try { - const mediaType = ctx.type === 'movie' ? 'movie' : 'tv'; - const t0 = Date.now(); - const result = await vidzee.getVidZeeStreams(ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null); - const durationMs = Date.now() - t0; - console.log(`[registry] vidzee fetch duration ${durationMs}ms`); - if (!Array.isArray(result)) return []; - return result.map(s => ({ ...s, name: s.name || 'VidZee', provider: 'vidzee' })); - } catch (e) { - console.error('[registry] vidzee fetch error:', e.message); - return []; - } -} - -if (config.enableVidzeeProvider) { - providers.push({ name: 'vidzee', enabled: true, fetch: fetchVidZee }); - console.log('[registry] vidzee provider enabled'); -} else { - console.log('[registry] vidzee disabled via config'); -} - -// vixsrc adapter -async function fetchVixsrc(ctx) { - if (!vixsrc) { - try { vixsrc = require('./vixsrc.js'); } catch (e) { console.error('Failed to load vixsrc provider:', e.message); return []; } - } - try { - const mediaType = ctx.type === 'movie' ? 'movie' : 'tv'; - const t0 = Date.now(); - const result = await vixsrc.getVixsrcStreams(ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null); - const durationMs = Date.now() - t0; - console.log(`[registry] vixsrc fetch duration ${durationMs}ms`); - if (!Array.isArray(result)) return []; - return result.map(s => ({ ...s, name: s.name || 'Vixsrc', provider: 'vixsrc' })); - } catch (e) { - console.error('[registry] vixsrc fetch error:', e.message); - return []; - } -} - -if (config.enableVixsrcProvider) { - providers.push({ name: 'vixsrc', enabled: true, fetch: fetchVixsrc }); - console.log('[registry] vixsrc provider enabled'); -} else { - console.log('[registry] vixsrc disabled via config'); -} - -// xprime adapter (requires TMDB metadata lookup for title/year) -async function fetchXprime(ctx) { - if (!xprime) { - try { xprime = require('./xprime.js'); } catch (e) { console.error('Failed to load xprime provider:', e.message); return []; } - } - try { - const mediaType = ctx.type === 'movie' ? 'movie' : 'tv'; - const { getTmdbApiKey } = require('../utils/tmdbKey'); - const tmdbApiKey = getTmdbApiKey(); - if (!tmdbApiKey) { - console.warn('[registry] xprime skipped: TMDB API key missing'); - return []; - } - // Fetch minimal TMDB metadata for title + year - let title = null; let year = null; - try { - const axios = require('axios'); - const metaUrl = `https://api.themoviedb.org/3/${mediaType}/${ctx.tmdbId}?api_key=${tmdbApiKey}`; - const { data: meta } = await axios.get(metaUrl, { timeout: 8000 }); - title = mediaType === 'movie' ? (meta.title || meta.original_title) : (meta.name || meta.original_name); - const dateStr = mediaType === 'movie' ? meta.release_date : meta.first_air_date; - if (dateStr) year = dateStr.split('-')[0]; - } catch (mErr) { - console.warn('[registry] xprime metadata fetch failed:', mErr.message); - return []; - } - if (!title || !year) { - console.log('[registry] xprime missing title/year after TMDB lookup'); - return []; - } - const t0 = Date.now(); - const streams = await xprime.getXprimeStreams(title, year, mediaType, ctx.season || null, ctx.episode || null); - const durationMs = Date.now() - t0; - console.log(`[registry] xprime fetch duration ${durationMs}ms (title='${title}' year=${year}) returned ${Array.isArray(streams)?streams.length:0}`); - if (!Array.isArray(streams) || !streams.length) return []; - // Normalize quality labels that are non-numeric (e.g., FHD/HD/SD) - return streams.map(s => { - let q = s.quality || 'Unknown'; - if (!/(\d{3,4})p/.test(q.toLowerCase())) { - const qLower = q.toLowerCase(); - if (qLower.includes('fhd') || qLower.includes('full')) q = '1080p'; - else if (qLower === 'hd' || qLower.includes('720')) q = '720p'; - else if (qLower.includes('sd') || qLower.includes('480')) q = '480p'; - else if (qLower.includes('cam')) q = '360p'; - } - return { ...s, quality: q, name: s.name || 'Xprime', provider: 'xprime' }; + if (enabled) { + providers.push({ + name: providerInfo.name, + enabled: true, + fetch: createFetchFunction(providerInfo) }); - } catch (e) { - console.error('[registry] xprime fetch error:', e.message); - return []; + console.log(`[registry] ${providerInfo.name} provider enabled`); + } else { + console.log(`[registry] ${providerInfo.name} disabled via config`); } } -if (config.enableXprimeProvider) { - providers.push({ name: 'xprime', enabled: true, fetch: fetchXprime }); - console.log('[registry] xprime provider enabled'); -} else { - console.log('[registry] xprime disabled via config'); -} - function listProviders() { return providers.map(p => ({ name: p.name, enabled: p.enabled })); } function getProvider(name) { return providers.find(p => p.name === name.toLowerCase()) || null; } diff --git a/providers/uhdmovies.js b/providers/uhdmovies.js index de735a1..d2c7ff2 100644 --- a/providers/uhdmovies.js +++ b/providers/uhdmovies.js @@ -46,9 +46,9 @@ async function getUHDMoviesDomain() { // Constants const { getTmdbApiKey } = require('../utils/tmdbKey'); -function getUhdMoviesKey(){ +function getUhdMoviesKey() { const k = getTmdbApiKey(); - if(!k) console.warn('[UHDMovies] TMDB API key missing; certain metadata features skipped.'); + if (!k) console.warn('[UHDMovies] TMDB API key missing; certain metadata features skipped.'); return k; } @@ -209,8 +209,8 @@ function extractCleanQuality(fullQualityText) { // Fallback: try to extract a shorter version of the original text // Look for patterns like "Movie Name (Year) Resolution ..." const patterns = [ - /(\d{3,4}p.*?(?:x264|x265|hevc).*?)[[(]/i, - /(\d{3,4}p.*?)[[(]/i, + /(\d{3,4}p.*?(?:x264|x265|hevc).*?)[[(]/i, + /(\d{3,4}p.*?)[[(]/i, /((?:720p|1080p|2160p|4k).*?)$/i ]; @@ -251,7 +251,7 @@ async function extractTvShowDownloadLinks(showPageUrl, season, episode) { // Check if we are entering a new season block if (seasonMatch) { const currentSeasonNum = parseInt(seasonMatch[1], 10); - if (currentSeasonNum === season) { + if (currentSeasonNum === season) { inTargetSeason = true; console.log(`[UHDMovies] Entering Season ${season} block.`); } else if (inTargetSeason) { @@ -298,7 +298,7 @@ async function extractTvShowDownloadLinks(showPageUrl, season, episode) { } } } - + // --- ENHANCED: Check for maxbutton-gdrive-episode structure --- if ($el.is('p') && $el.find('a.maxbutton-gdrive-episode').length > 0) { const episodeRegex = new RegExp(`^Episode\\s+0*${episode}(?!\\d)`, 'i'); @@ -326,24 +326,24 @@ async function extractTvShowDownloadLinks(showPageUrl, season, episode) { if (downloadLinks.length === 0) { console.log('[UHDMovies] Main extraction logic failed. Checking if requested season exists on page before fallback.'); - + // Check if the requested season exists on the page at all let seasonExists = false; const actualSeasonsOnPage = new Set(); // Track what seasons actually have content - + // First pass: Look for actual episode content to see what seasons are available $('.entry-content').find('a[href*="tech.unblockedgames.world"], a[href*="tech.examzculture.in"], a.maxbutton-gdrive-episode').each((index, element) => { const $el = $(element); const linkText = $el.text().trim(); const episodeText = $el.find('.mb-text').text().trim() || linkText; - + // Look for season indicators in episode links const seasonMatches = [ episodeText.match(/S(\d{1,2})/i), // S01, S02, etc. episodeText.match(/Season\s+(\d+)/i), // Season 1, Season 2, etc. episodeText.match(/S(\d{1,2})E(\d{1,3})/i) // S01E01 format ]; - + for (const match of seasonMatches) { if (match && match[1]) { const foundSeason = parseInt(match[1], 10); @@ -351,9 +351,9 @@ async function extractTvShowDownloadLinks(showPageUrl, season, episode) { } } }); - - console.log(`[UHDMovies] Actual seasons found on page: ${Array.from(actualSeasonsOnPage).sort((a,b) => a-b).join(', ')}`); - + + console.log(`[UHDMovies] Actual seasons found on page: ${Array.from(actualSeasonsOnPage).sort((a, b) => a - b).join(', ')}`); + // Check if requested season is in the actual content if (actualSeasonsOnPage.has(season)) { seasonExists = true; @@ -371,7 +371,7 @@ async function extractTvShowDownloadLinks(showPageUrl, season, episode) { text.match(/Season\s+\d+\s*[–-]\s*(\d+)/i), // Matches "Season 1-2" text.match(/\bS(\d+)/i) // Matches "S2", "S02", etc. ]; - + for (const match of seasonMatches) { if (match) { const currentSeasonNum = parseInt(match[1], 10); @@ -397,15 +397,15 @@ async function extractTvShowDownloadLinks(showPageUrl, season, episode) { } }); } - + if (!seasonExists) { console.log(`[UHDMovies] Season ${season} not found on page. Available seasons may not include the requested season.`); // Don't use fallback if the season doesn't exist to avoid wrong episodes return { title: showTitle, links: [], seasonNotFound: true }; } - + console.log(`[UHDMovies] Season ${season} exists on page but episode extraction failed. Trying fallback method with season filtering.`); - + // --- ENHANCED FALLBACK LOGIC FOR NEW HTML STRUCTURE --- // Try the new maxbutton-gdrive-episode structure first $('.entry-content').find('a.maxbutton-gdrive-episode').each((i, el) => { @@ -417,7 +417,7 @@ async function extractTvShowDownloadLinks(showPageUrl, season, episode) { const link = linkElement.attr('href'); if (link && !downloadLinks.some(item => item.link === link)) { let qualityText = 'Unknown Quality'; - + // Look for quality info in the preceding paragraph or heading const parentP = linkElement.closest('p, div'); const prevElement = parentP.prev(); @@ -437,7 +437,7 @@ async function extractTvShowDownloadLinks(showPageUrl, season, episode) { new RegExp(`Season\\s+0*${season}\\b`, 'i'), // Season 1 new RegExp(`S0*${season}`, 'i') // S01 anywhere ]; - + const seasonMatch = seasonCheckRegexes.some(regex => regex.test(qualityText)); if (!seasonMatch) { console.log(`[UHDMovies] Skipping episode from different season: Quality='${qualityText}'`); @@ -454,7 +454,7 @@ async function extractTvShowDownloadLinks(showPageUrl, season, episode) { } } }); - + // If still no results, try the original fallback logic if (downloadLinks.length === 0) { console.log(`[UHDMovies] Enhanced fallback failed, trying original fallback logic.`); @@ -484,7 +484,7 @@ async function extractTvShowDownloadLinks(showPageUrl, season, episode) { new RegExp(`Season\\s+0*${season}\\b`, 'i'), // Season 1 new RegExp(`S0*${season}`, 'i') // S01 anywhere ]; - + const seasonMatch = seasonCheckRegexes.some(regex => regex.test(qualityText)); if (!seasonMatch) { console.log(`[UHDMovies] Skipping episode from different season: Quality='${qualityText}'`); @@ -822,7 +822,7 @@ async function validateVideoUrl(url, timeout = 10000) { try { console.log(`[UHDMovies] Validating URL: ${url.substring(0, 100)}...`); - + const response = await axiosInstance.head(url, { timeout, headers: { @@ -951,7 +951,7 @@ function scoreResult(title, requestedSeason = null) { console.log(`[UHDMovies] Season range bonus (+50): ${startSeason}-${endSeason} includes requested season ${requestedSeason}`); } } - + // Check for specific season mentions const specificSeasonMatch = lowerTitle.match(/season\s+(\d+)/i); if (specificSeasonMatch) { @@ -964,7 +964,7 @@ function scoreResult(title, requestedSeason = null) { console.log(`[UHDMovies] Season penalty (-20): Season ${mentionedSeason} is older than requested season ${requestedSeason}`); } } - + // Check for "Season X Added" or similar indicators if (lowerTitle.includes('season') && lowerTitle.includes('added')) { const addedSeasonMatch = lowerTitle.match(/season\s+(\d+)\s+added/i); @@ -1030,7 +1030,7 @@ const getCookiesForUrl = async (jar, url) => { // Helper function to create a session for SID resolution const createSession = async (jar) => { const { wrapper } = await getAxiosCookieJarSupport(); - + const sessionConfig = { jar, headers: { @@ -1044,7 +1044,7 @@ const createSession = async (jar) => { const session = wrapper(axios.create(sessionConfig)); - + return session; }; @@ -1178,8 +1178,8 @@ async function getUHDMoviesStreams(tmdbId, mediaType = 'movie', season = null, e } console.log(`[UHDMovies] Cache MISS for ${cacheKey}. Fetching from source.`); // 2. If cache miss, get TMDB info to perform search - const TMDB_API_KEY_UHDMOVIES = getUhdMoviesKey(); - const tmdbUrl = `https://api.themoviedb.org/3/${mediaType === 'tv' ? 'tv' : 'movie'}/${tmdbId}?api_key=${TMDB_API_KEY_UHDMOVIES}`; + const TMDB_API_KEY_UHDMOVIES = getUhdMoviesKey(); + const tmdbUrl = `https://api.themoviedb.org/3/${mediaType === 'tv' ? 'tv' : 'movie'}/${tmdbId}?api_key=${TMDB_API_KEY_UHDMOVIES}`; const tmdbResponse = await axios.get(tmdbUrl); const tmdbData = tmdbResponse.data; const mediaInfo = { @@ -1249,25 +1249,25 @@ async function getUHDMoviesStreams(tmdbId, mediaType = 'movie', season = null, e // 5. Extract SID links from the movie/show page let downloadInfo = await (mediaType === 'tv' ? extractTvShowDownloadLinks(matchingResult.link, season, episode) : extractDownloadLinks(matchingResult.link, mediaInfo.year)); - + // Check if season was not found or episode extraction failed, and we have multiple results to try - if (downloadInfo.links.length === 0 && matchingResults.length > 1 && scoredResults && - (downloadInfo.seasonNotFound || (mediaType === 'tv' && downloadInfo.title))) { + if (downloadInfo.links.length === 0 && matchingResults.length > 1 && scoredResults && + (downloadInfo.seasonNotFound || (mediaType === 'tv' && downloadInfo.title))) { console.log(`[UHDMovies] Season ${season} not found or episode extraction failed on best match. Trying next best match...`); - + // Try the next best match const nextBestMatch = scoredResults[1]; console.log(`[UHDMovies] Trying next best match: "${nextBestMatch.title}"`); - + downloadInfo = await (mediaType === 'tv' ? extractTvShowDownloadLinks(nextBestMatch.link, season, episode) : extractDownloadLinks(nextBestMatch.link, mediaInfo.year)); - + if (downloadInfo.links.length > 0) { console.log(`[UHDMovies] Successfully found links on next best match!`); } else { console.log(`[UHDMovies] Next best match also failed. No download links found.`); } } - + if (downloadInfo.links.length === 0) { console.log('[UHDMovies] No download links found on page.'); // Don't cache empty results to allow retrying later @@ -1380,11 +1380,8 @@ async function getUHDMoviesStreams(tmdbId, mediaType = 'movie', season = null, e title: `${cleanFileName}\n${sizeInfo}`, url: finalUrl, quality: linkInfo.quality, - size: sizeInfo, - fileName: fileName, - fullTitle: rawQuality, - codecs: codecs, - behaviorHints: { bingeGroup: `uhdmovies-${linkInfo.quality}` } + provider: 'uhdmovies', + headers: {} }; } } diff --git a/providers/vixsrc.js b/providers/vixsrc.js index cc044e2..571c136 100644 --- a/providers/vixsrc.js +++ b/providers/vixsrc.js @@ -195,7 +195,7 @@ function getVixsrcStreams(tmdbId, mediaType = 'movie', seasonNum = null, episode title: "Auto Quality Stream", url: masterPlaylistUrl, quality: '1080p', - type: 'direct', + provider: 'Vidsrc', headers: { 'Referer': BASE_URL, 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36' diff --git a/providers/xprime.js b/providers/xprime.js index c32e8fd..466c12c 100644 --- a/providers/xprime.js +++ b/providers/xprime.js @@ -132,12 +132,12 @@ async function getXprimeStreams(title, year, type, seasonNum, episodeNum) { Object.entries(item.streams).forEach(([quality, url]) => { if (!url || typeof url !== 'string') return; streams.push({ + name: title, + title: `${title} - ${type === 'tv' ? `S${String(seasonNum).padStart(2,'0')}E${String(episodeNum).padStart(2,'0')} ` : ''}${quality}`, url, quality: quality || 'Unknown', - title: `${title} - ${type === 'tv' ? `S${String(seasonNum).padStart(2,'0')}E${String(episodeNum).padStart(2,'0')} ` : ''}${quality}`, provider: 'Xprime.tv', - codecs: [], - size: 'Unknown size' + headers: {}, }); }); }; diff --git a/public/index.js b/public/index.js index ba28b23..21134f3 100644 --- a/public/index.js +++ b/public/index.js @@ -596,15 +596,25 @@ async function renderProviderMatrix(merged){ if(!merged){ try { const data = await fetchConfig(); merged = data.merged||{}; } catch { return; } } const container = document.getElementById('providerMatrix'); if(!container) return; const defaults = (merged.defaultProviders||[]).map(s=>s.toLowerCase()); - const providers = [ - ['showbox','enableShowboxProvider'], - ['4khdhub','enable4khdhubProvider'], - ['moviesmod','enableMoviesmodProvider'], - ['mp4hydra','enableMp4hydraProvider'], - ['vidzee','enableVidzeeProvider'], - ['vixsrc','enableVixsrcProvider'], - ['xprime','enableXprimeProvider'] - ]; + + // Fetch available providers from API + let availableProviders = []; + try { + const providersResponse = await fetch('/api/providers'); + if (providersResponse.ok) { + const providersData = await providersResponse.json(); + availableProviders = providersData.providers || []; + } + } catch (e) { + console.warn('Failed to fetch providers list:', e.message); + } + + // Create provider list with enable flags + const providers = availableProviders.map(p => { + const flag = `enable${p.name.charAt(0).toUpperCase() + p.name.slice(1)}Provider`; + return [p.name, flag]; + }); + container.innerHTML = providers.map(([name,flag])=>{ const enabled = merged[flag] !== false; const isDefault = defaults.includes(name); diff --git a/restart.trigger b/restart.trigger index 45009f4..204b0a8 100644 --- a/restart.trigger +++ b/restart.trigger @@ -1 +1 @@ -1758105049325 \ No newline at end of file +1758225404146 \ No newline at end of file diff --git a/utils/config.js b/utils/config.js index bd52a63..441657b 100644 --- a/utils/config.js +++ b/utils/config.js @@ -52,6 +52,19 @@ function writeOverrideFile(obj) { } } +function getProviderNames() { + try { + const providersDir = path.join(process.cwd(), 'providers'); + if (!fs.existsSync(providersDir)) return []; + return fs.readdirSync(providersDir) + .filter(file => file.endsWith('.js') && file !== 'registry.js' && file !== 'vidsrcextractor.js') + .map(file => path.parse(file).name.toLowerCase()); + } catch (e) { + console.warn('[config] Failed to scan providers directory:', e.message); + return []; + } +} + function normalizeConfig(base) { const cfg = { ...base }; // Marker: if user explicitly provided an empty array for tmdbApiKeys in override file we should not resurrect legacy key @@ -80,20 +93,21 @@ function normalizeConfig(base) { cfg.tmdbApiKeys = Array.from(new Set(cfg.tmdbApiKeys.map(k=>String(k).trim()).filter(Boolean))); // Do not expose legacy single-key field in merged config output delete cfg.tmdbApiKey; - // Booleans normalization - const boolKeys = [ - 'enableShowboxProvider','enableXprimeProvider','enable4khdhubProvider','enableMoviesmodProvider','enableMp4hydraProvider','enableVidzeeProvider','enableVixsrcProvider', - 'disableCache','enablePStreamApi','disableUrlValidation','disable4khdhubUrlValidation' - ]; + + // Dynamic provider enable flags + const providerNames = getProviderNames(); + const boolKeys = providerNames.map(name => `enable${name.charAt(0).toUpperCase() + name.slice(1)}Provider`); + boolKeys.push('disableCache', 'enablePStreamApi', 'disableUrlValidation', 'disable4khdhubUrlValidation'); + boolKeys.forEach(k=>{ if (cfg[k] === 'true') cfg[k] = true; else if (cfg[k] === 'false') cfg[k] = false; }); - // Default values if undefined - if (cfg.enableShowboxProvider === undefined) cfg.enableShowboxProvider = true; - if (cfg.enableXprimeProvider === undefined) cfg.enableXprimeProvider = true; - if (cfg.enable4khdhubProvider === undefined) cfg.enable4khdhubProvider = true; - if (cfg.enableMoviesmodProvider === undefined) cfg.enableMoviesmodProvider = true; - if (cfg.enableMp4hydraProvider === undefined) cfg.enableMp4hydraProvider = true; - if (cfg.enableVidzeeProvider === undefined) cfg.enableVidzeeProvider = true; - if (cfg.enableVixsrcProvider === undefined) cfg.enableVixsrcProvider = true; + + // Set defaults for provider enable flags + providerNames.forEach(name => { + const flag = `enable${name.charAt(0).toUpperCase() + name.slice(1)}Provider`; + if (cfg[flag] === undefined) cfg[flag] = true; // Default to enabled + }); + + // Default values for other flags if (cfg.disableCache === undefined) cfg.disableCache = false; if (cfg.enablePStreamApi === undefined) cfg.enablePStreamApi = true; // Proxy features removed; always use direct connections @@ -114,14 +128,15 @@ function applyConfigToEnv(cfg){ if (cfg.febboxCookies && cfg.febboxCookies.length) process.env.FEBBOX_COOKIES = cfg.febboxCookies.join(','); else delete process.env.FEBBOX_COOKIES; if (cfg.defaultRegion) process.env.DEFAULT_REGION = cfg.defaultRegion; else delete process.env.DEFAULT_REGION; - // Provider enable flags - process.env.ENABLE_SHOWBOX_PROVIDER = cfg.enableShowboxProvider ? 'true':'false'; - process.env.ENABLE_XPRIME_PROVIDER = cfg.enableXprimeProvider ? 'true':'false'; - process.env.ENABLE_4KHDHUB_PROVIDER = cfg.enable4khdhubProvider ? 'true':'false'; - process.env.ENABLE_MOVIESMOD_PROVIDER = cfg.enableMoviesmodProvider ? 'true':'false'; - process.env.ENABLE_MP4HYDRA_PROVIDER = cfg.enableMp4hydraProvider ? 'true':'false'; - process.env.ENABLE_VIDZEE_PROVIDER = cfg.enableVidzeeProvider ? 'true':'false'; - process.env.ENABLE_VIXSRC_PROVIDER = cfg.enableVixsrcProvider ? 'true':'false'; + + // Dynamic provider enable flags + const providerNames = getProviderNames(); + providerNames.forEach(name => { + const flag = `enable${name.charAt(0).toUpperCase() + name.slice(1)}Provider`; + const envName = `ENABLE_${name.toUpperCase()}_PROVIDER`; + process.env[envName] = cfg[flag] ? 'true' : 'false'; + }); + // Caching / validation / PStream process.env.DISABLE_CACHE = cfg.disableCache ? 'true':'false'; process.env.ENABLE_PSTREAM_API = cfg.enablePStreamApi ? 'true':'false'; @@ -152,13 +167,6 @@ function loadConfig() { tmdbApiKeys: parseJsonMaybe(process.env.TMDB_API_KEYS) || null, febboxCookies: parseCookies(process.env.FEBBOX_COOKIES), // Extended advanced config (may not exist in env) - enableShowboxProvider: process.env.ENABLE_SHOWBOX_PROVIDER, - enableXprimeProvider: process.env.ENABLE_XPRIME_PROVIDER, - enable4khdhubProvider: process.env.ENABLE_4KHDHUB_PROVIDER, - enableMoviesmodProvider: process.env.ENABLE_MOVIESMOD_PROVIDER, - enableMp4hydraProvider: process.env.ENABLE_MP4HYDRA_PROVIDER, - enableVidzeeProvider: process.env.ENABLE_VIDZEE_PROVIDER, - enableVixsrcProvider: process.env.ENABLE_VIXSRC_PROVIDER, disableCache: process.env.DISABLE_CACHE, enablePStreamApi: process.env.ENABLE_PSTREAM_API, showboxCacheDir: process.env.SHOWBOX_CACHE_DIR || null, @@ -166,6 +174,14 @@ function loadConfig() { disableUrlValidation: process.env.DISABLE_URL_VALIDATION, disable4khdhubUrlValidation: process.env.DISABLE_4KHDHUB_URL_VALIDATION }; + + // Dynamic provider enable flags from env + const providerNames = getProviderNames(); + providerNames.forEach(name => { + const envName = `ENABLE_${name.toUpperCase()}_PROVIDER`; + envCfg[`enable${name.charAt(0).toUpperCase() + name.slice(1)}Provider`] = process.env[envName]; + }); + const override = readOverrideFile(); const merged = { ...envCfg, ...override }; const normalized = normalizeConfig(merged); diff --git a/utils/jsunpack.js b/utils/jsunpack.js new file mode 100644 index 0000000..a99d572 --- /dev/null +++ b/utils/jsunpack.js @@ -0,0 +1,113 @@ +class JsUnpacker { + static Unbase = class { + constructor(radix) { + this.ALPHABET_62 = + '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + this.ALPHABET_95 = + ' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~'; + this.alphabet = null; + this.dictionary = {}; + this.radix = radix; + + if (radix > 36) { + if (radix < 62) { + this.alphabet = this.ALPHABET_62.substring(0, radix); + } else if (radix > 62 && radix < 95) { + this.alphabet = this.ALPHABET_95.substring(0, radix); + } else if (radix === 62) { + this.alphabet = this.ALPHABET_62; + } else if (radix === 95) { + this.alphabet = this.ALPHABET_95; + } + + for (let i = 0; i < this.alphabet.length; i++) { + this.dictionary[this.alphabet[i]] = i; + } + } + } + + unbase(str) { + let ret = 0; + + if (this.alphabet === null) { + ret = parseInt(str, this.radix); + } else { + const tmp = str.split('').reverse().join(''); + for (let i = 0; i < tmp.length; i++) { + ret += Math.pow(this.radix, i) * this.dictionary[tmp[i]]; + } + } + return ret; + } + }; + + constructor(packedJS) { + this.packedJS = packedJS; + } + + detect() { + const js = this.packedJS.replace(/\s/g, ''); + const regex = /eval\(function\(p,a,c,k,e,(?:r|d)/; + return regex.test(js); + } + + unpack() { + let js = this.packedJS; + try { + const regex = + /}\s*\('(.*)',\s*(.*?),\s*(\d+),\s*'(.*?)'\.split\('\|'\)/s; + const match = js.match(regex); + if (match && match.length === 5) { + let payload = match[1].replace(/\\'/g, "'"); + const radixStr = match[2]; + const countStr = match[3]; + const symtab = match[4].split('|'); + + let radix = 36; + let count = 0; + try { + radix = parseInt(radixStr); + } catch (e) {} + try { + count = parseInt(countStr); + } catch (e) {} + + if (symtab.length !== count) { + throw new Error('Unknown p.a.c.k.e.r. encoding'); + } + + const unbase = new this.constructor.Unbase(radix); + const wordRegex = /\b\w+\b/g; + let decoded = payload; + let replaceOffset = 0; + let wordMatch; + + while ((wordMatch = wordRegex.exec(payload)) !== null) { + const word = wordMatch[0]; + const x = unbase.unbase(word); + let value = null; + if (x < symtab.length) { + value = symtab[x]; + } + + if (value && value.length > 0) { + const start = wordMatch.index + replaceOffset; + const end = start + word.length; + decoded = + decoded.substring(0, start) + + value + + decoded.substring(end); + replaceOffset += value.length - word.length; + } + } + return decoded; + } + } catch (e) { + return null; + } + return null; + } +} + +// CommonJS exports +module.exports = JsUnpacker; diff --git a/utils/user-config.json b/utils/user-config.json index 9fca9f7..6a234f8 100644 --- a/utils/user-config.json +++ b/utils/user-config.json @@ -16,4 +16,4 @@ "disable4khdhubUrlValidation": false, "excludeCodecsRaw": "{\"excludeDV\":false,\"excludeHDR\":false}", "minQualitiesRaw": "all" -} +} \ No newline at end of file