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(/