mirror of
https://github.com/Inside4ndroid/TMDB-Embed-API.git
synced 2026-10-22 09:35:11 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17079e1646 | ||
|
|
25cc59659d |
@@ -1 +1,3 @@
|
|||||||
/node_modules
|
/node_modules
|
||||||
|
restart.trigger
|
||||||
|
.vscode/
|
||||||
@@ -2,6 +2,31 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [1.0.5] - 2025-09-19
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
- 4khdhub provider: Permanently block `r2.dev` FSL links (previous optional flag removed).
|
||||||
|
- Added Referer/Origin headers automatically for FSL Server links during validation (prior to block enforcement ensured proper behavior).
|
||||||
|
- Tightened URL validation: removed unconditional trust for `r2.dev`; validation logic now consistent across hosts.
|
||||||
|
- Host distribution instrumentation logs final hostname counts for easier diagnostics.
|
||||||
|
- Preserved HubCloud worker `.zip` links by stripping the `.zip` extension instead of discarding them (enables direct playback attempts).
|
||||||
|
- MoviesClub provider: Added automatic `Origin`/`Referer` headers for `vidora.stream` sources to prevent 403 responses.
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
- `r2.dev` links are always removed from final output; no env flag required.
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- README: Updated Providers section, unified schema example, MoviesClub listed, registry mapping instructions, and badge version.
|
||||||
|
|
||||||
|
## [1.0.4] - 2025-09-18
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
```json
|
||||||
|
{ "title": "…", "url": "…", "quality": "…", "provider": "…", "headers": { } }
|
||||||
|
```
|
||||||
|
|
||||||
## [1.0.3] - 2025-09-17
|
## [1.0.3] - 2025-09-17
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
@@ -46,6 +71,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
- Initial stable release.
|
- Initial stable release.
|
||||||
|
|
||||||
[1.0.3]: https://github.com/Inside4ndroid/TMDB-Embed-API/compare/v1.0.2...v1.0.3
|
[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.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
|
[1.0.1]: https://github.com/Inside4ndroid/TMDB-Embed-API/compare/v1.0.0...v1.0.1
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<img src="https://img.shields.io/badge/Node.js-18%2B-brightgreen?style=flat" />
|
<img src="https://img.shields.io/badge/Node.js-18%2B-brightgreen?style=flat" />
|
||||||
<img src="https://img.shields.io/badge/Status-Active-success?style=flat" />
|
<img src="https://img.shields.io/badge/Status-Active-success?style=flat" />
|
||||||
<img src="https://img.shields.io/badge/License-MIT-blue?style=flat" />
|
<img src="https://img.shields.io/badge/License-MIT-blue?style=flat" />
|
||||||
<img src="https://img.shields.io/badge/Version-1.0.3-informational?style=flat" />
|
<img src="https://img.shields.io/badge/Version-1.0.5-informational?style=flat" />
|
||||||
<img src="https://img.shields.io/docker/pulls/inside4ndroid/tmdb-embed-api?label=Docker%20Pulls&style=flat" />
|
<img src="https://img.shields.io/docker/pulls/inside4ndroid/tmdb-embed-api?label=Docker%20Pulls&style=flat" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -34,14 +34,15 @@
|
|||||||
|
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
- **Multi‑TMDB Key Rotation** – Supply multiple API keys; one is chosen randomly per request.
|
- **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).
|
- **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`.
|
- **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.
|
- **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.
|
- **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).
|
- **Config Propagation** – Overrides mirrored to `process.env` for legacy compatibility (no `.env` required after first save).
|
||||||
- **Back‑Navigation Safe** – Cache-control + visibility/session revalidation.
|
- **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
|
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:
|
Persist overrides (Windows PowerShell example) by mounting a local file:
|
||||||
```powershell
|
```powershell
|
||||||
New-Item -ItemType File -Path .\utils\user-config.json -Force | Out-Null
|
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
|
## 🔌 Providers
|
||||||
Current built‑in providers:
|
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`.
|
||||||
- `showbox`
|
|
||||||
- `4khdhub`
|
|
||||||
- `moviesmod`
|
|
||||||
- `mp4hydra`
|
|
||||||
- `vidzee`
|
|
||||||
- `vixsrc`
|
|
||||||
- `xprime`
|
|
||||||
|
|
||||||
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)
|
### Adding a New Provider
|
||||||
1. Create `providers/myprovider.js` exporting an async fetch function (see existing files for patterns).
|
1. **Create** `providers/yourprovider.js` with your stream fetching logic
|
||||||
2. Lazy‑load it in `providers/registry.js` similar to others.
|
2. **Export** a function like `getYourproviderStreams(tmdbId, mediaType, season, episode)`
|
||||||
3. Add a config flag (`enableMyproviderProvider`) if you want toggle support.
|
3. **Register** it in `providers/registry.js` → `providerFunctionMap`:
|
||||||
4. Update the provider matrix arrays in `public/index.js`.
|
```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
|
```json
|
||||||
{
|
{
|
||||||
"title": "Example Title",
|
"title": "Fight Club - 1080p [MP4Hydra #2]",
|
||||||
"url": "https://...",
|
"url": "https://stream.url/video.mp4",
|
||||||
"quality": "1080p",
|
"quality": "1080p",
|
||||||
"provider": "showbox",
|
"provider": "mp4hydra",
|
||||||
"size": null,
|
"headers": { "User-Agent": "Mozilla/5.0" }
|
||||||
"languages": ["en"],
|
|
||||||
"subtitles": [],
|
|
||||||
"codecs": ["H264"],
|
|
||||||
"headers": { "Referer": "..." }
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "tmdb-embed-api",
|
"name": "tmdb-embed-api",
|
||||||
"version": "1.0.3",
|
"version": "1.0.5",
|
||||||
"description": "Standalone streaming embed source aggregation API (TMDB ID in, streams out)",
|
"description": "Standalone streaming embed source aggregation API (TMDB ID in, streams out)",
|
||||||
"main": "apiServer.js",
|
"main": "apiServer.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+232
-135
@@ -9,6 +9,9 @@ const path = require('path');
|
|||||||
const DOMAINS_URL = 'https://raw.githubusercontent.com/phisher98/TVVVV/refs/heads/main/domains.json';
|
const DOMAINS_URL = 'https://raw.githubusercontent.com/phisher98/TVVVV/refs/heads/main/domains.json';
|
||||||
let cachedDomains = null;
|
let cachedDomains = null;
|
||||||
|
|
||||||
|
// Permanent policy: block r2.dev links (previously optional via env flag)
|
||||||
|
console.log('[4KHDHub] r2.dev links will be removed (permanent policy)');
|
||||||
|
|
||||||
// --- Caching Configuration ---
|
// --- Caching Configuration ---
|
||||||
const CACHE_ENABLED = process.env.DISABLE_CACHE !== 'true';
|
const CACHE_ENABLED = process.env.DISABLE_CACHE !== 'true';
|
||||||
console.log(`[4KHDHub] Internal cache is ${CACHE_ENABLED ? 'enabled' : 'disabled'}.`);
|
console.log(`[4KHDHub] Internal cache is ${CACHE_ENABLED ? 'enabled' : 'disabled'}.`);
|
||||||
@@ -18,14 +21,14 @@ const CACHE_DIR = process.env.VERCEL ? path.join('/tmp', '.4khdhub_cache') : pat
|
|||||||
|
|
||||||
// --- Caching Helper Functions ---
|
// --- Caching Helper Functions ---
|
||||||
const ensureCacheDir = async () => {
|
const ensureCacheDir = async () => {
|
||||||
if (!CACHE_ENABLED) return;
|
if (!CACHE_ENABLED) return;
|
||||||
try {
|
try {
|
||||||
await fs.mkdir(CACHE_DIR, { recursive: true });
|
await fs.mkdir(CACHE_DIR, { recursive: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code !== 'EEXIST') {
|
if (error.code !== 'EEXIST') {
|
||||||
console.error(`[4KHDHub Cache] Error creating cache directory: ${error.message}`);
|
console.error(`[4KHDHub Cache] Error creating cache directory: ${error.message}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFromCache = async (key) => {
|
const getFromCache = async (key) => {
|
||||||
@@ -48,7 +51,7 @@ function base64Decode(str) {
|
|||||||
|
|
||||||
|
|
||||||
function rot13(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;
|
const start = char <= 'Z' ? 65 : 97;
|
||||||
return String.fromCharCode(((char.charCodeAt(0) - start + 13) % 26) + start);
|
return String.fromCharCode(((char.charCodeAt(0) - start + 13) % 26) + start);
|
||||||
});
|
});
|
||||||
@@ -60,10 +63,9 @@ function validateUrl(url) {
|
|||||||
const urlObj = new URL(url);
|
const urlObj = new URL(url);
|
||||||
|
|
||||||
// Skip validation for known reliable hosting services
|
// Skip validation for known reliable hosting services
|
||||||
const trustedHosts = [
|
const trustedHosts = [
|
||||||
'pixeldrain.dev',
|
'pixeldrain.dev'
|
||||||
'r2.dev'
|
];
|
||||||
];
|
|
||||||
|
|
||||||
const isTrustedHost = trustedHosts.some(host => urlObj.hostname.includes(host));
|
const isTrustedHost = trustedHosts.some(host => urlObj.hostname.includes(host));
|
||||||
if (isTrustedHost) {
|
if (isTrustedHost) {
|
||||||
@@ -74,11 +76,21 @@ function validateUrl(url) {
|
|||||||
|
|
||||||
const protocol = urlObj.protocol === 'https:' ? https : http;
|
const protocol = urlObj.protocol === 'https:' ? https : http;
|
||||||
|
|
||||||
|
// Add referer/origin for r2.dev validation attempts
|
||||||
|
const extraHeaders = {};
|
||||||
|
if (/r2\.dev$/i.test(urlObj.hostname)) {
|
||||||
|
const origin = `${urlObj.protocol}//${urlObj.hostname}`;
|
||||||
|
extraHeaders['Referer'] = origin + '/';
|
||||||
|
extraHeaders['Origin'] = origin;
|
||||||
|
console.log(`[4KHDHub] Adding Referer/Origin headers for validation of r2.dev URL: ${url}`);
|
||||||
|
}
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
method: 'HEAD',
|
method: 'HEAD',
|
||||||
timeout: 15000,
|
timeout: 15000,
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||||
|
...extraHeaders
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -397,8 +409,10 @@ function generateAlternativeQueries(title, originalTitle = null) {
|
|||||||
return romans[num] || match;
|
return romans[num] || match;
|
||||||
}),
|
}),
|
||||||
title.replace(/\b(I{1,3}|IV|V|VI{0,3}|IX|X)\b/g, 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',
|
const romans = {
|
||||||
'VI': '6', 'VII': '7', 'VIII': '8', 'IX': '9', 'X': '10' };
|
'I': '1', 'II': '2', 'III': '3', 'IV': '4', 'V': '5',
|
||||||
|
'VI': '6', 'VII': '7', 'VIII': '8', 'IX': '9', 'X': '10'
|
||||||
|
};
|
||||||
return romans[match] || match;
|
return romans[match] || match;
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
@@ -749,12 +763,22 @@ function extractHubCloudLinks(url, referer) {
|
|||||||
if (displayFilename) titleParts.push(displayFilename);
|
if (displayFilename) titleParts.push(displayFilename);
|
||||||
if (size) titleParts.push(size);
|
if (size) titleParts.push(size);
|
||||||
const finalTitle = titleParts.join('\n');
|
const finalTitle = titleParts.join('\n');
|
||||||
|
let headers = {};
|
||||||
|
try {
|
||||||
|
const host = new URL(link).hostname;
|
||||||
|
if (/r2\.dev$/i.test(host)) {
|
||||||
|
headers = { Referer: baseUrl + '/', Origin: baseUrl };
|
||||||
|
console.log('[4KHDHub] Added Referer/Origin headers for r2.dev FSL link');
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
resolve({
|
resolve({
|
||||||
name: `4KHDHub - FSL Server${qualityLabel}`,
|
name: `4KHDHub - FSL Server${qualityLabel}`,
|
||||||
title: finalTitle,
|
title: finalTitle,
|
||||||
url: link,
|
url: link,
|
||||||
quality: quality
|
quality: quality,
|
||||||
|
provider: '4khdhub',
|
||||||
|
headers
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@@ -763,12 +787,22 @@ function extractHubCloudLinks(url, referer) {
|
|||||||
if (displayFilename) titleParts.push(displayFilename);
|
if (displayFilename) titleParts.push(displayFilename);
|
||||||
if (size) titleParts.push(size);
|
if (size) titleParts.push(size);
|
||||||
const finalTitle = titleParts.join('\n');
|
const finalTitle = titleParts.join('\n');
|
||||||
|
let headers = {};
|
||||||
|
try {
|
||||||
|
const host = new URL(link).hostname;
|
||||||
|
if (/r2\.dev$/i.test(host)) {
|
||||||
|
headers = { Referer: baseUrl + '/', Origin: baseUrl };
|
||||||
|
console.log('[4KHDHub] Added Referer/Origin headers for r2.dev FSL link (fallback)');
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
resolve({
|
resolve({
|
||||||
name: `4KHDHub - FSL Server${qualityLabel}`,
|
name: `4KHDHub - FSL Server${qualityLabel}`,
|
||||||
title: finalTitle,
|
title: finalTitle,
|
||||||
url: link,
|
url: link,
|
||||||
quality: quality
|
quality: quality,
|
||||||
|
provider: '4khdhub',
|
||||||
|
headers
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else if (text.includes('Download File')) {
|
} else if (text.includes('Download File')) {
|
||||||
@@ -786,7 +820,9 @@ function extractHubCloudLinks(url, referer) {
|
|||||||
name: `4KHDHub - HubCloud${qualityLabel}`,
|
name: `4KHDHub - HubCloud${qualityLabel}`,
|
||||||
title: finalTitle,
|
title: finalTitle,
|
||||||
url: link,
|
url: link,
|
||||||
quality: quality
|
quality: quality,
|
||||||
|
provider: '4khdhub',
|
||||||
|
headers: {}
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@@ -800,7 +836,9 @@ function extractHubCloudLinks(url, referer) {
|
|||||||
name: `4KHDHub - HubCloud${qualityLabel}`,
|
name: `4KHDHub - HubCloud${qualityLabel}`,
|
||||||
title: finalTitle,
|
title: finalTitle,
|
||||||
url: link,
|
url: link,
|
||||||
quality: quality
|
quality: quality,
|
||||||
|
provider: '4khdhub',
|
||||||
|
headers: {}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else if (text.includes('BuzzServer')) {
|
} else if (text.includes('BuzzServer')) {
|
||||||
@@ -811,50 +849,54 @@ function extractHubCloudLinks(url, referer) {
|
|||||||
allowRedirects: false,
|
allowRedirects: false,
|
||||||
headers: { 'Referer': link }
|
headers: { 'Referer': link }
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
const redirectUrl = response.headers['hx-redirect'] || response.headers['location'];
|
const redirectUrl = response.headers['hx-redirect'] || response.headers['location'];
|
||||||
if (redirectUrl) {
|
if (redirectUrl) {
|
||||||
console.log(`[4KHDHub] BuzzServer redirect found: ${redirectUrl}`);
|
console.log(`[4KHDHub] BuzzServer redirect found: ${redirectUrl}`);
|
||||||
const finalUrl = buttonBaseUrl + redirectUrl;
|
const finalUrl = buttonBaseUrl + redirectUrl;
|
||||||
// Get actual filename from HEAD request
|
// Get actual filename from HEAD request
|
||||||
getFilenameFromUrl(finalUrl)
|
getFilenameFromUrl(finalUrl)
|
||||||
.then(actualFilename => {
|
.then(actualFilename => {
|
||||||
const displayFilename = actualFilename || headerDetails || 'Unknown';
|
const displayFilename = actualFilename || headerDetails || 'Unknown';
|
||||||
const titleParts = [];
|
const titleParts = [];
|
||||||
if (displayFilename) titleParts.push(displayFilename);
|
if (displayFilename) titleParts.push(displayFilename);
|
||||||
if (size) titleParts.push(size);
|
if (size) titleParts.push(size);
|
||||||
const finalTitle = titleParts.join('\n');
|
const finalTitle = titleParts.join('\n');
|
||||||
|
|
||||||
resolve({
|
resolve({
|
||||||
name: `4KHDHub - BuzzServer${qualityLabel}`,
|
name: `4KHDHub - BuzzServer${qualityLabel}`,
|
||||||
title: finalTitle,
|
title: finalTitle,
|
||||||
url: finalUrl,
|
url: finalUrl,
|
||||||
quality: quality
|
quality: quality,
|
||||||
});
|
provider: '4khdhub',
|
||||||
})
|
headers: {}
|
||||||
.catch(() => {
|
});
|
||||||
const displayFilename = headerDetails || 'Unknown';
|
})
|
||||||
const titleParts = [];
|
.catch(() => {
|
||||||
if (displayFilename) titleParts.push(displayFilename);
|
const displayFilename = headerDetails || 'Unknown';
|
||||||
if (size) titleParts.push(size);
|
const titleParts = [];
|
||||||
const finalTitle = titleParts.join('\n');
|
if (displayFilename) titleParts.push(displayFilename);
|
||||||
|
if (size) titleParts.push(size);
|
||||||
|
const finalTitle = titleParts.join('\n');
|
||||||
|
|
||||||
resolve({
|
resolve({
|
||||||
name: `4KHDHub - BuzzServer${qualityLabel}`,
|
name: `4KHDHub - BuzzServer${qualityLabel}`,
|
||||||
title: finalTitle,
|
title: finalTitle,
|
||||||
url: finalUrl,
|
url: finalUrl,
|
||||||
quality: quality
|
quality: quality,
|
||||||
|
provider: '4khdhub',
|
||||||
|
headers: {}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
} else {
|
||||||
} else {
|
console.log(`[4KHDHub] BuzzServer redirect not found`);
|
||||||
console.log(`[4KHDHub] BuzzServer redirect not found`);
|
resolve(null);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.log(`[4KHDHub] BuzzServer redirect failed: ${err.message}`);
|
||||||
resolve(null);
|
resolve(null);
|
||||||
}
|
});
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.log(`[4KHDHub] BuzzServer redirect failed: ${err.message}`);
|
|
||||||
resolve(null);
|
|
||||||
});
|
|
||||||
} else if (link.includes('pixeldra')) {
|
} else if (link.includes('pixeldra')) {
|
||||||
console.log(`[4KHDHub] Button ${index + 1} is Pixeldrain`);
|
console.log(`[4KHDHub] Button ${index + 1} is Pixeldrain`);
|
||||||
|
|
||||||
@@ -880,7 +922,9 @@ function extractHubCloudLinks(url, referer) {
|
|||||||
name: `4KHDHub - Pixeldrain${qualityLabel}`,
|
name: `4KHDHub - Pixeldrain${qualityLabel}`,
|
||||||
title: finalTitle,
|
title: finalTitle,
|
||||||
url: convertedLink,
|
url: convertedLink,
|
||||||
quality: quality
|
quality: quality,
|
||||||
|
provider: '4khdhub',
|
||||||
|
headers: {}
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@@ -894,7 +938,9 @@ function extractHubCloudLinks(url, referer) {
|
|||||||
name: `4KHDHub - Pixeldrain${qualityLabel}`,
|
name: `4KHDHub - Pixeldrain${qualityLabel}`,
|
||||||
title: finalTitle,
|
title: finalTitle,
|
||||||
url: convertedLink,
|
url: convertedLink,
|
||||||
quality: quality
|
quality: quality,
|
||||||
|
provider: '4khdhub',
|
||||||
|
headers: {}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else if (text.includes('S3 Server')) {
|
} else if (text.includes('S3 Server')) {
|
||||||
@@ -912,7 +958,9 @@ function extractHubCloudLinks(url, referer) {
|
|||||||
name: `4KHDHub - S3 Server${qualityLabel}`,
|
name: `4KHDHub - S3 Server${qualityLabel}`,
|
||||||
title: finalTitle,
|
title: finalTitle,
|
||||||
url: link,
|
url: link,
|
||||||
quality: quality
|
quality: quality,
|
||||||
|
provider: '4khdhub',
|
||||||
|
headers: {}
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@@ -926,7 +974,9 @@ function extractHubCloudLinks(url, referer) {
|
|||||||
name: `4KHDHub - S3 Server${qualityLabel}`,
|
name: `4KHDHub - S3 Server${qualityLabel}`,
|
||||||
title: finalTitle,
|
title: finalTitle,
|
||||||
url: link,
|
url: link,
|
||||||
quality: quality
|
quality: quality,
|
||||||
|
provider: '4khdhub',
|
||||||
|
headers: {}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else if (text.includes('10Gbps')) {
|
} else if (text.includes('10Gbps')) {
|
||||||
@@ -939,57 +989,61 @@ function extractHubCloudLinks(url, referer) {
|
|||||||
parseHTML: false,
|
parseHTML: false,
|
||||||
allowRedirects: false
|
allowRedirects: false
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
const redirectUrl = response.headers['location'];
|
const redirectUrl = response.headers['location'];
|
||||||
if (!redirectUrl) {
|
if (!redirectUrl) {
|
||||||
throw new Error('No redirect found');
|
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
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
throw new Error('Final link not found');
|
|
||||||
} else {
|
console.log(`[4KHDHub] 10Gbps redirect: ${redirectUrl}`);
|
||||||
currentLink = redirectUrl;
|
|
||||||
return followRedirects();
|
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()
|
followRedirects()
|
||||||
@@ -1016,7 +1070,9 @@ function extractHubCloudLinks(url, referer) {
|
|||||||
name: `4KHDHub - HubCloud${qualityLabel}`,
|
name: `4KHDHub - HubCloud${qualityLabel}`,
|
||||||
title: finalTitle,
|
title: finalTitle,
|
||||||
url: link,
|
url: link,
|
||||||
quality: quality
|
quality: quality,
|
||||||
|
provider: '4khdhub',
|
||||||
|
headers: {}
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@@ -1030,7 +1086,9 @@ function extractHubCloudLinks(url, referer) {
|
|||||||
name: `4KHDHub - HubCloud${qualityLabel}`,
|
name: `4KHDHub - HubCloud${qualityLabel}`,
|
||||||
title: finalTitle,
|
title: finalTitle,
|
||||||
url: link,
|
url: link,
|
||||||
quality: quality
|
quality: quality,
|
||||||
|
provider: '4khdhub',
|
||||||
|
headers: {}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1276,12 +1334,22 @@ function extractStreamingLinks(downloadLinks) {
|
|||||||
.then(results => {
|
.then(results => {
|
||||||
const validResults = results.filter(result => result !== null);
|
const validResults = results.filter(result => result !== null);
|
||||||
const flatResults = validResults.flat();
|
const flatResults = validResults.flat();
|
||||||
// Filter out .zip files
|
// Keep .zip links but strip the trailing .zip extension (attempt direct file URL)
|
||||||
const filteredResults = flatResults.filter(link => {
|
const transformedResults = flatResults
|
||||||
return link && link.url && !link.url.toLowerCase().endsWith('.zip');
|
.filter(link => link && link.url)
|
||||||
});
|
.map(link => {
|
||||||
|
try {
|
||||||
|
const lower = link.url.toLowerCase();
|
||||||
|
if (lower.endsWith('.zip')) {
|
||||||
|
const newUrl = link.url.slice(0, -4); // remove .zip
|
||||||
|
console.log(`[4KHDHub] Converted zip URL to non-zip: ${link.url} -> ${newUrl}`);
|
||||||
|
return { ...link, url: newUrl };
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
return link;
|
||||||
|
});
|
||||||
// Note: Link count will be logged after validation completes
|
// Note: Link count will be logged after validation completes
|
||||||
return filteredResults;
|
return transformedResults;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1306,9 +1374,9 @@ function extractHubDriveLinks(url, referer) {
|
|||||||
let filename = headerDetails || header || 'Unknown';
|
let filename = headerDetails || header || 'Unknown';
|
||||||
// Clean up the filename by removing common prefixes and file extensions
|
// Clean up the filename by removing common prefixes and file extensions
|
||||||
filename = filename.replace(/^4kHDHub\.com\s*[-_]?\s*/i, '')
|
filename = filename.replace(/^4kHDHub\.com\s*[-_]?\s*/i, '')
|
||||||
.replace(/\.[a-z0-9]{2,4}$/i, '')
|
.replace(/\.[a-z0-9]{2,4}$/i, '')
|
||||||
.replace(/[._]/g, ' ')
|
.replace(/[._]/g, ' ')
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
// Use the exact selector from Kotlin code
|
// Use the exact selector from Kotlin code
|
||||||
const downloadBtn = $('.btn.btn-primary.btn-user.btn-success1.m-1').first();
|
const downloadBtn = $('.btn.btn-primary.btn-user.btn-success1.m-1').first();
|
||||||
@@ -1373,7 +1441,7 @@ function processHubDriveLink(href, referer, filename = 'Unknown', size = '', qua
|
|||||||
const labelExtras = [];
|
const labelExtras = [];
|
||||||
if (filename && filename !== 'Unknown') labelExtras.push(`[${filename}]`);
|
if (filename && filename !== 'Unknown') labelExtras.push(`[${filename}]`);
|
||||||
if (size) labelExtras.push(`[${size}]`);
|
if (size) labelExtras.push(`[${size}]`);
|
||||||
// labelExtra removed (not used)
|
// labelExtra removed (not used)
|
||||||
|
|
||||||
// Get actual filename from HEAD request
|
// Get actual filename from HEAD request
|
||||||
return getFilenameFromUrl(href)
|
return getFilenameFromUrl(href)
|
||||||
@@ -1388,7 +1456,9 @@ function processHubDriveLink(href, referer, filename = 'Unknown', size = '', qua
|
|||||||
name: `4KHDHub - HubDrive${qualityLabel}`,
|
name: `4KHDHub - HubDrive${qualityLabel}`,
|
||||||
title: finalTitle,
|
title: finalTitle,
|
||||||
url: href,
|
url: href,
|
||||||
quality: quality
|
quality: quality,
|
||||||
|
provider: '4khdhub',
|
||||||
|
headers: {}
|
||||||
}];
|
}];
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@@ -1402,7 +1472,9 @@ function processHubDriveLink(href, referer, filename = 'Unknown', size = '', qua
|
|||||||
name: `4KHDHub - HubDrive${qualityLabel}`,
|
name: `4KHDHub - HubDrive${qualityLabel}`,
|
||||||
title: finalTitle,
|
title: finalTitle,
|
||||||
url: href,
|
url: href,
|
||||||
quality: quality
|
quality: quality,
|
||||||
|
provider: '4khdhub',
|
||||||
|
headers: {}
|
||||||
}];
|
}];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1463,7 +1535,9 @@ function processExtractorLink(link, resolve, linkNumber) {
|
|||||||
name: '4KHDHub Direct Link',
|
name: '4KHDHub Direct Link',
|
||||||
title: finalTitle,
|
title: finalTitle,
|
||||||
url: link,
|
url: link,
|
||||||
quality: 1080
|
quality: 1080,
|
||||||
|
provider: "4khdhub",
|
||||||
|
headers: {}
|
||||||
}]);
|
}]);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@@ -1477,7 +1551,9 @@ function processExtractorLink(link, resolve, linkNumber) {
|
|||||||
name: '4KHDHub Direct Link',
|
name: '4KHDHub Direct Link',
|
||||||
title: finalTitle,
|
title: finalTitle,
|
||||||
url: link,
|
url: link,
|
||||||
quality: 1080
|
quality: 1080,
|
||||||
|
provider: "4khdhub",
|
||||||
|
headers: {}
|
||||||
}]);
|
}]);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -1490,14 +1566,14 @@ function processExtractorLink(link, resolve, linkNumber) {
|
|||||||
async function getTMDBDetails(tmdbId, mediaType) {
|
async function getTMDBDetails(tmdbId, mediaType) {
|
||||||
const { getTmdbApiKey } = require('../utils/tmdbKey');
|
const { getTmdbApiKey } = require('../utils/tmdbKey');
|
||||||
const TMDB_API_KEY = getTmdbApiKey();
|
const TMDB_API_KEY = getTmdbApiKey();
|
||||||
if(!TMDB_API_KEY){
|
if (!TMDB_API_KEY) {
|
||||||
console.warn('[4KHDHub] TMDB API key missing; skipping TMDB metadata fetch');
|
console.warn('[4KHDHub] TMDB API key missing; skipping TMDB metadata fetch');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(`[4KHDHub] Fetching ${mediaType} details for TMDB ID: ${tmdbId}`);
|
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);
|
const data = JSON.parse(response.body);
|
||||||
|
|
||||||
if (mediaType === 'movie') {
|
if (mediaType === 'movie') {
|
||||||
@@ -1739,17 +1815,38 @@ async function get4KHDHubStreams(tmdbId, type, season = null, episode = null) {
|
|||||||
console.log(`[4KHDHub] URL validation disabled, skipping validation`);
|
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 => ({
|
let streams = validatedLinks.map(link => ({
|
||||||
name: link.name, // Don't add prefix since it's already included
|
name: link.name, // Don't add prefix since it's already included
|
||||||
title: link.title || link.name,
|
title: link.title || link.name,
|
||||||
url: link.url,
|
url: link.url,
|
||||||
quality: `${link.quality}p`,
|
quality: `${link.quality}p`,
|
||||||
behaviorHints: {
|
provider: "4khdhub",
|
||||||
bingeGroup: '4khdhub-streams'
|
headers: {}
|
||||||
}
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Always remove r2.dev links
|
||||||
|
{
|
||||||
|
const before = streams.length;
|
||||||
|
streams = streams.filter(s => {
|
||||||
|
try { return !new URL(s.url).hostname.endsWith('r2.dev'); } catch { return true; }
|
||||||
|
});
|
||||||
|
const removed = before - streams.length;
|
||||||
|
if (removed > 0) console.log(`[4KHDHub] Removed ${removed} r2.dev streams (permanent policy)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Host distribution instrumentation
|
||||||
|
try {
|
||||||
|
const hostCounts = streams.reduce((acc, s) => {
|
||||||
|
try {
|
||||||
|
const h = new URL(s.url).hostname;
|
||||||
|
acc[h] = (acc[h] || 0) + 1;
|
||||||
|
} catch {}
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
console.log('[4KHDHub] Final host distribution:', hostCounts);
|
||||||
|
} catch {}
|
||||||
|
|
||||||
console.log(`[4KHDHub] Returning ${streams.length} streams`);
|
console.log(`[4KHDHub] Returning ${streams.length} streams`);
|
||||||
return streams;
|
return streams;
|
||||||
|
|
||||||
|
|||||||
+21
-39
@@ -126,14 +126,11 @@ async function getMP4HydraStreams(tmdbId, mediaType, seasonNum = null, episodeNu
|
|||||||
title: `${details.title} - ${seasonEpisode} - ${processedEpisode.quality} [MP4Hydra ${serverNumber}]`,
|
title: `${details.title} - ${seasonEpisode} - ${processedEpisode.quality} [MP4Hydra ${serverNumber}]`,
|
||||||
url: processedEpisode.videoUrl,
|
url: processedEpisode.videoUrl,
|
||||||
quality: processedEpisode.quality,
|
quality: processedEpisode.quality,
|
||||||
provider: "MP4Hydra",
|
provider: "mp4hydra",
|
||||||
size: "Unknown size",
|
headers: {
|
||||||
behaviorHints: {
|
'Referer': 'https://mp4hydra.org/'
|
||||||
notWebReady: true,
|
|
||||||
headers: {
|
|
||||||
'Referer': 'https://mp4hydra.org/'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add subtitle tracks if available
|
// Add subtitle tracks if available
|
||||||
@@ -172,14 +169,11 @@ async function getMP4HydraStreams(tmdbId, mediaType, seasonNum = null, episodeNu
|
|||||||
title: `${details.title} - ${processedItem.quality} [MP4Hydra ${serverNumber}]`,
|
title: `${details.title} - ${processedItem.quality} [MP4Hydra ${serverNumber}]`,
|
||||||
url: processedItem.videoUrl,
|
url: processedItem.videoUrl,
|
||||||
quality: processedItem.quality,
|
quality: processedItem.quality,
|
||||||
provider: "MP4Hydra",
|
provider: "mp4hydra",
|
||||||
size: "Unknown size",
|
headers: {
|
||||||
behaviorHints: {
|
'Referer': 'https://mp4hydra.org/'
|
||||||
notWebReady: true,
|
|
||||||
headers: {
|
|
||||||
'Referer': 'https://mp4hydra.org/'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add subtitle tracks if available
|
// Add subtitle tracks if available
|
||||||
@@ -297,14 +291,11 @@ async function tryAlternativeTitle(details, mediaType, seasonNum, episodeNum) {
|
|||||||
title: `${details.original_title} - ${seasonEpisode} - ${processedEpisode.quality} [MP4Hydra ${serverNumber}]`,
|
title: `${details.original_title} - ${seasonEpisode} - ${processedEpisode.quality} [MP4Hydra ${serverNumber}]`,
|
||||||
url: processedEpisode.videoUrl,
|
url: processedEpisode.videoUrl,
|
||||||
quality: processedEpisode.quality,
|
quality: processedEpisode.quality,
|
||||||
provider: "MP4Hydra",
|
provider: "mp4hydra",
|
||||||
size: "Unknown size",
|
headers: {
|
||||||
behaviorHints: {
|
'Referer': 'https://mp4hydra.org/'
|
||||||
notWebReady: true,
|
|
||||||
headers: {
|
|
||||||
'Referer': 'https://mp4hydra.org/'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add subtitle tracks if available
|
// Add subtitle tracks if available
|
||||||
@@ -340,17 +331,10 @@ async function tryAlternativeTitle(details, mediaType, seasonNum, episodeNum) {
|
|||||||
|
|
||||||
// Convert to standard stream format
|
// Convert to standard stream format
|
||||||
streams.push({
|
streams.push({
|
||||||
|
name: `${details.original_title}`,
|
||||||
title: `${details.original_title} - ${processedItem.quality} [MP4Hydra ${serverNumber}]`,
|
title: `${details.original_title} - ${processedItem.quality} [MP4Hydra ${serverNumber}]`,
|
||||||
url: processedItem.videoUrl,
|
url: processedItem.videoUrl,
|
||||||
quality: processedItem.quality,
|
quality: processedItem.quality
|
||||||
provider: "MP4Hydra",
|
|
||||||
size: "Unknown size",
|
|
||||||
behaviorHints: {
|
|
||||||
notWebReady: true,
|
|
||||||
headers: {
|
|
||||||
'Referer': 'https://mp4hydra.org/'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add subtitle tracks if available
|
// Add subtitle tracks if available
|
||||||
@@ -435,17 +419,15 @@ async function tryWithoutYear(details, mediaType, seasonNum, episodeNum) {
|
|||||||
|
|
||||||
// Convert to standard stream format
|
// Convert to standard stream format
|
||||||
streams.push({
|
streams.push({
|
||||||
|
name: `${details.title}`,
|
||||||
title: `${details.title} - ${processedItem.quality} [MP4Hydra ${serverNumber}]`,
|
title: `${details.title} - ${processedItem.quality} [MP4Hydra ${serverNumber}]`,
|
||||||
url: processedItem.videoUrl,
|
url: processedItem.videoUrl,
|
||||||
quality: processedItem.quality,
|
quality: processedItem.quality,
|
||||||
provider: "MP4Hydra",
|
provider: "mp4hydra",
|
||||||
size: "Unknown size",
|
headers: {
|
||||||
behaviorHints: {
|
'Referer': 'https://mp4hydra.org/'
|
||||||
notWebReady: true,
|
|
||||||
headers: {
|
|
||||||
'Referer': 'https://mp4hydra.org/'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add subtitle tracks if available
|
// Add subtitle tracks if available
|
||||||
@@ -473,14 +455,14 @@ async function tryWithoutYear(details, mediaType, seasonNum, episodeNum) {
|
|||||||
async function getTMDBDetails(tmdbId, mediaType) {
|
async function getTMDBDetails(tmdbId, mediaType) {
|
||||||
const { getTmdbApiKey } = require('../utils/tmdbKey');
|
const { getTmdbApiKey } = require('../utils/tmdbKey');
|
||||||
const TMDB_API_KEY = getTmdbApiKey();
|
const TMDB_API_KEY = getTmdbApiKey();
|
||||||
if(!TMDB_API_KEY){
|
if (!TMDB_API_KEY) {
|
||||||
console.warn('[MP4Hydra] TMDB API key missing; skipping TMDB metadata fetch');
|
console.warn('[MP4Hydra] TMDB API key missing; skipping TMDB metadata fetch');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(`[MP4Hydra] Fetching ${mediaType} details for TMDB ID: ${tmdbId}`);
|
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') {
|
if (mediaType === 'movie') {
|
||||||
return {
|
return {
|
||||||
|
|||||||
+79
-75
@@ -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.');
|
console.log('[CookieManager] Using user-supplied cookie from global state (legacy mode) for this cycle.');
|
||||||
baseCookieToUse = global.currentRequestUserCookie;
|
baseCookieToUse = global.currentRequestUserCookie;
|
||||||
}
|
}
|
||||||
// No file-based fallback rotation (env-only)
|
// No file-based fallback rotation (env-only)
|
||||||
else {
|
else {
|
||||||
console.log('[CookieManager] No user-supplied cookies available for this cycle.');
|
console.log('[CookieManager] No user-supplied cookies available for this cycle.');
|
||||||
baseCookieToUse = '';
|
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/';
|
// const SCRAPER_API_URL = 'https://api.scraperapi.com/';
|
||||||
// TMDB key now sourced from config/env; no hardcoded public key
|
// TMDB key now sourced from config/env; no hardcoded public key
|
||||||
const { getTmdbApiKey } = require('../utils/tmdbKey');
|
const { getTmdbApiKey } = require('../utils/tmdbKey');
|
||||||
function obtainTmdbKey(){
|
function obtainTmdbKey() {
|
||||||
const k = getTmdbApiKey();
|
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;
|
return k;
|
||||||
}
|
}
|
||||||
const TMDB_BASE_URL = 'https://api.themoviedb.org/3';
|
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 };
|
if (cachedResult === 'NO_MATCH_FOUND') return { url: null, score: -1 };
|
||||||
return { url: cachedResult, score: 10 };
|
return { url: cachedResult, score: 10 };
|
||||||
} else if (cachedResult.result === 'NO_MATCH_FOUND') {
|
} else if (cachedResult.result === 'NO_MATCH_FOUND') {
|
||||||
// For stream searches, always retry immediately - content availability can change anytime
|
// 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.`);
|
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
|
// Continue to search logic below to retry immediately
|
||||||
} else {
|
} else {
|
||||||
console.log(` CACHE HIT for ShowBox search best match URL (${originalTmdbTitle} ${mediaYear || ''}): ${cachedResult}`);
|
console.log(` CACHE HIT for ShowBox search best match URL (${originalTmdbTitle} ${mediaYear || ''}): ${cachedResult}`);
|
||||||
return { url: cachedResult, score: 10 };
|
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)
|
// Allow for very close matches (like plurals or slight variations)
|
||||||
if (tmdbWord.length > 3 && itemWord.length > 3) {
|
if (tmdbWord.length > 3 && itemWord.length > 3) {
|
||||||
return (itemWord.includes(tmdbWord) && itemWord.length - tmdbWord.length <= 2) ||
|
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;
|
return false;
|
||||||
})) {
|
})) {
|
||||||
@@ -988,8 +988,8 @@ const getShowboxUrlFromTmdbInfo = async (tmdbType, tmdbId, regionPreference = nu
|
|||||||
let tmdbData = await getFromCache(mainCacheKey, mainCacheSubDir);
|
let tmdbData = await getFromCache(mainCacheKey, mainCacheSubDir);
|
||||||
|
|
||||||
if (!tmdbData || process.env.DISABLE_CACHE === 'true') {
|
if (!tmdbData || process.env.DISABLE_CACHE === 'true') {
|
||||||
const TMDB_API_KEY = obtainTmdbKey();
|
const TMDB_API_KEY = obtainTmdbKey();
|
||||||
const tmdbApiUrl = `${TMDB_BASE_URL}/${tmdbType}/${tmdbId}?api_key=${TMDB_API_KEY}`;
|
const tmdbApiUrl = `${TMDB_BASE_URL}/${tmdbType}/${tmdbId}?api_key=${TMDB_API_KEY}`;
|
||||||
console.log(` Fetching TMDB data from: ${tmdbApiUrl}`);
|
console.log(` Fetching TMDB data from: ${tmdbApiUrl}`);
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(tmdbApiUrl, { timeout: 10000 });
|
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);
|
let tmdbAlternativeTitlesData = await getFromCache(altTitlesCacheKey, mainCacheSubDir);
|
||||||
|
|
||||||
if (!tmdbAlternativeTitlesData || process.env.DISABLE_CACHE === 'true') {
|
if (!tmdbAlternativeTitlesData || process.env.DISABLE_CACHE === 'true') {
|
||||||
const TMDB_API_KEY = obtainTmdbKey();
|
const TMDB_API_KEY = obtainTmdbKey();
|
||||||
const altTitlesApiUrl = `${TMDB_BASE_URL}/${tmdbType}/${tmdbId}/alternative_titles?api_key=${TMDB_API_KEY}`;
|
const altTitlesApiUrl = `${TMDB_BASE_URL}/${tmdbType}/${tmdbId}/alternative_titles?api_key=${TMDB_API_KEY}`;
|
||||||
console.log(` Fetching TMDB alternative titles from: ${altTitlesApiUrl}`);
|
console.log(` Fetching TMDB alternative titles from: ${altTitlesApiUrl}`);
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(altTitlesApiUrl, { timeout: 10000 });
|
const response = await axios.get(altTitlesApiUrl, { timeout: 10000 });
|
||||||
@@ -1047,8 +1047,8 @@ const getShowboxUrlFromTmdbInfo = async (tmdbType, tmdbId, regionPreference = nu
|
|||||||
let tmdbBackdropPaths = [];
|
let tmdbBackdropPaths = [];
|
||||||
|
|
||||||
if (!tmdbImagesData || process.env.DISABLE_CACHE === 'true') {
|
if (!tmdbImagesData || process.env.DISABLE_CACHE === 'true') {
|
||||||
const TMDB_API_KEY = obtainTmdbKey();
|
const TMDB_API_KEY = obtainTmdbKey();
|
||||||
const imagesApiUrl = `${TMDB_BASE_URL}/${tmdbType}/${tmdbId}/images?api_key=${TMDB_API_KEY}`;
|
const imagesApiUrl = `${TMDB_BASE_URL}/${tmdbType}/${tmdbId}/images?api_key=${TMDB_API_KEY}`;
|
||||||
console.log(` Fetching TMDB images from: ${imagesApiUrl}`);
|
console.log(` Fetching TMDB images from: ${imagesApiUrl}`);
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(imagesApiUrl, { timeout: 10000 });
|
const response = await axios.get(imagesApiUrl, { timeout: 10000 });
|
||||||
@@ -1352,7 +1352,7 @@ class ShowBoxScraper {
|
|||||||
// Store the region preference and user cookie
|
// Store the region preference and user cookie
|
||||||
this.regionPreference = regionPreference;
|
this.regionPreference = regionPreference;
|
||||||
this.userCookie = userCookie;
|
this.userCookie = userCookie;
|
||||||
// ScraperAPI key support removed
|
// ScraperAPI key support removed
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1375,7 +1375,7 @@ class ShowBoxScraper {
|
|||||||
|
|
||||||
|
|
||||||
async _makeRequest(url, isJsonExpected = false) {
|
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
|
// For JSON requests, we still cache normally since they're small
|
||||||
if (isJsonExpected) {
|
if (isJsonExpected) {
|
||||||
@@ -1393,7 +1393,7 @@ class ShowBoxScraper {
|
|||||||
// Always make direct requests
|
// Always make direct requests
|
||||||
const requestUrl = url;
|
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
|
// Get the cookie with region preference if site requires it
|
||||||
let cookieValue = null;
|
let cookieValue = null;
|
||||||
@@ -1869,45 +1869,45 @@ const getStreamsFromTmdbId = async (tmdbType, tmdbId, seasonNum = null, episodeN
|
|||||||
|
|
||||||
// PRIMARY METHOD: Try PStream API first (if enabled)
|
// PRIMARY METHOD: Try PStream API first (if enabled)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get TMDB data to extract IMDB ID
|
// Get TMDB data to extract IMDB ID
|
||||||
const tmdbData = await getTmdbDataForPStream(tmdbType, tmdbId);
|
const tmdbData = await getTmdbDataForPStream(tmdbType, tmdbId);
|
||||||
if (tmdbData && tmdbData.imdbId) {
|
if (tmdbData && tmdbData.imdbId) {
|
||||||
console.log(`[PStream Primary] Attempting PStream API for IMDB ID: ${tmdbData.imdbId}`);
|
console.log(`[PStream Primary] Attempting PStream API for IMDB ID: ${tmdbData.imdbId}`);
|
||||||
|
|
||||||
const pstreamStreams = await getStreamsFromPStreamAPI(
|
const pstreamStreams = await getStreamsFromPStreamAPI(
|
||||||
tmdbData.imdbId,
|
tmdbData.imdbId,
|
||||||
tmdbType,
|
tmdbType,
|
||||||
seasonNum,
|
seasonNum,
|
||||||
episodeNum,
|
episodeNum,
|
||||||
regionPreference,
|
regionPreference,
|
||||||
userCookie
|
userCookie
|
||||||
);
|
);
|
||||||
|
|
||||||
if (pstreamStreams && pstreamStreams.length > 0) {
|
if (pstreamStreams && pstreamStreams.length > 0) {
|
||||||
console.log(`[PStream Primary] SUCCESS: Found ${pstreamStreams.length} streams from PStream API`);
|
console.log(`[PStream Primary] SUCCESS: Found ${pstreamStreams.length} streams from PStream API`);
|
||||||
|
|
||||||
// Fetch sizes for PStream streams
|
// Fetch sizes for PStream streams
|
||||||
if (pstreamStreams.length > 0) {
|
if (pstreamStreams.length > 0) {
|
||||||
const sizePromises = pstreamStreams.map(async (stream) => {
|
const sizePromises = pstreamStreams.map(async (stream) => {
|
||||||
stream.size = await fetchStreamSize(stream.url);
|
stream.size = await fetchStreamSize(stream.url);
|
||||||
return stream;
|
return stream;
|
||||||
});
|
});
|
||||||
await Promise.all(sizePromises); // removed unused streamsWithSizes var
|
await Promise.all(sizePromises); // removed unused streamsWithSizes var
|
||||||
}
|
|
||||||
|
|
||||||
const sortedStreams = sortStreamsByQuality(pstreamStreams);
|
|
||||||
console.timeEnd(mainTimerLabel);
|
|
||||||
return sortedStreams;
|
|
||||||
} else {
|
|
||||||
console.log(`[PStream Primary] No streams found from PStream API, falling back to FebBox`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sortedStreams = sortStreamsByQuality(pstreamStreams);
|
||||||
|
console.timeEnd(mainTimerLabel);
|
||||||
|
return sortedStreams;
|
||||||
} else {
|
} 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) {
|
} else {
|
||||||
console.error(`[PStream Primary] Error with PStream API: ${error.message}, falling back to FebBox`);
|
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
|
// FALLBACK METHOD: Use original FebBox/ShowBox logic
|
||||||
console.log(`[Fallback] Using FebBox/ShowBox method`);
|
console.log(`[Fallback] Using FebBox/ShowBox method`);
|
||||||
@@ -1928,7 +1928,7 @@ const getStreamsFromTmdbId = async (tmdbType, tmdbId, seasonNum = null, episodeN
|
|||||||
if (!febboxShareInfos) {
|
if (!febboxShareInfos) {
|
||||||
// Only make ShowBox request if FebBox links are not cached
|
// Only make ShowBox request if FebBox links are not cached
|
||||||
console.log(`No cached FebBox links found, extracting from ShowBox: ${showboxUrl}`);
|
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);
|
febboxShareInfos = await showboxScraper.extractFebboxShareLinks(showboxUrl);
|
||||||
|
|
||||||
if (!febboxShareInfos || febboxShareInfos.length === 0) {
|
if (!febboxShareInfos || febboxShareInfos.length === 0) {
|
||||||
@@ -1980,10 +1980,12 @@ const getStreamsFromTmdbId = async (tmdbType, tmdbId, seasonNum = null, episodeN
|
|||||||
}
|
}
|
||||||
} catch (e) { /* ignore if URL parsing fails */ }
|
} catch (e) { /* ignore if URL parsing fails */ }
|
||||||
streamsFromThisShareInfo.push({
|
streamsFromThisShareInfo.push({
|
||||||
|
name: streamTitle,
|
||||||
title: streamTitle,
|
title: streamTitle,
|
||||||
url: source.url,
|
url: source.url,
|
||||||
quality: parseQualityFromLabel(source.label),
|
quality: parseQualityFromLabel(source.label),
|
||||||
codecs: extractCodecDetails(key5FromDirectSource || streamTitle)
|
provider: 'Febbox Direct',
|
||||||
|
headers: {}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// If direct sources are found, original code used 'continue',
|
// 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}`;
|
const streamTitle = `${baseStreamTitle} - ${source.label}`;
|
||||||
streamsFromThisShareInfo.push({
|
streamsFromThisShareInfo.push({
|
||||||
|
name: streamTitle,
|
||||||
title: streamTitle,
|
title: streamTitle,
|
||||||
url: source.url,
|
url: source.url,
|
||||||
quality: parseQualityFromLabel(source.label),
|
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);
|
stream.size = await fetchStreamSize(stream.url);
|
||||||
return stream;
|
return stream;
|
||||||
});
|
});
|
||||||
await Promise.all(sizePromises); // removed unused streamsWithSizes var
|
await Promise.all(sizePromises); // removed unused streamsWithSizes var
|
||||||
console.timeEnd(`getStreamsFromTmdbId_fetchStreamSizes_${tmdbType}_${tmdbId}`);
|
console.timeEnd(`getStreamsFromTmdbId_fetchStreamSizes_${tmdbType}_${tmdbId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2202,10 +2206,12 @@ const processShowWithSeasonsEpisodes = async (febboxUrl, showboxTitle, seasonNum
|
|||||||
for (const source of directSources) {
|
for (const source of directSources) {
|
||||||
const streamTitle = `${showboxTitle} - ${source.label}`;
|
const streamTitle = `${showboxTitle} - ${source.label}`;
|
||||||
streamsForThisCall.push({ // MODIFIED: Push to streamsForThisCall
|
streamsForThisCall.push({ // MODIFIED: Push to streamsForThisCall
|
||||||
|
name: streamTitle,
|
||||||
title: streamTitle,
|
title: streamTitle,
|
||||||
url: source.url,
|
url: source.url,
|
||||||
quality: parseQualityFromLabel(source.label),
|
quality: parseQualityFromLabel(source.label),
|
||||||
codecs: extractCodecDetails(source.detailedFilename || streamTitle)
|
provider: 'Febbox Direct',
|
||||||
|
headers: {}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
console.timeEnd(processTimerLabel);
|
console.timeEnd(processTimerLabel);
|
||||||
@@ -2222,10 +2228,12 @@ const processShowWithSeasonsEpisodes = async (febboxUrl, showboxTitle, seasonNum
|
|||||||
for (const source of sources) {
|
for (const source of sources) {
|
||||||
const streamTitle = `${showboxTitle} - ${source.label}`;
|
const streamTitle = `${showboxTitle} - ${source.label}`;
|
||||||
streamsForThisCall.push({ // MODIFIED: Push to streamsForThisCall
|
streamsForThisCall.push({ // MODIFIED: Push to streamsForThisCall
|
||||||
|
name: streamTitle,
|
||||||
title: streamTitle,
|
title: streamTitle,
|
||||||
url: source.url,
|
url: source.url,
|
||||||
quality: parseQualityFromLabel(source.label),
|
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}`;
|
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
|
streamsForThisCall.push({ // MODIFIED: Push to streamsForThisCall
|
||||||
|
name: streamTitle,
|
||||||
title: streamTitle,
|
title: streamTitle,
|
||||||
url: source.url,
|
url: source.url,
|
||||||
quality: parseQualityFromLabel(source.label),
|
quality: parseQualityFromLabel(source.label),
|
||||||
codecs: extractCodecDetails(source.detailedFilename || streamTitle)
|
provider: 'Febbox FID',
|
||||||
|
headers: {}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -2862,17 +2872,15 @@ const processPStreamResponse = (apiResponse, tmdbType, seasonNum, episodeNum) =>
|
|||||||
for (const [quality, url] of Object.entries(apiResponse.streams)) {
|
for (const [quality, url] of Object.entries(apiResponse.streams)) {
|
||||||
if (url && typeof url === 'string') {
|
if (url && typeof url === 'string') {
|
||||||
// Use "Auto" for name and actual filename for title
|
// 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}`;
|
const streamTitle = apiResponse.name || `${tmdbType === 'movie' ? 'PStream' : `PStream - S${seasonNum}E${episodeNum}`} - ${quality}`;
|
||||||
|
|
||||||
streams.push({
|
streams.push({
|
||||||
name: streamName,
|
name: streamTitle,
|
||||||
title: streamTitle,
|
title: streamTitle,
|
||||||
url: url,
|
url: url,
|
||||||
quality: parseQualityFromLabel(quality),
|
quality: parseQualityFromLabel(quality),
|
||||||
codecs: extractCodecDetails(apiResponse.name || quality), // Use filename for codec detection
|
provider: 'PStream',
|
||||||
size: apiResponse.size || 'Unknown size',
|
headers: {}
|
||||||
provider: 'PStream'
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2890,17 +2898,15 @@ const processPStreamResponse = (apiResponse, tmdbType, seasonNum, episodeNum) =>
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const streamName = "Auto";
|
|
||||||
const streamTitle = apiResponse.name || `${tmdbType === 'movie' ? 'PStream' : `PStream - S${seasonNum}E${episodeNum}`} - ${source.quality}`;
|
const streamTitle = apiResponse.name || `${tmdbType === 'movie' ? 'PStream' : `PStream - S${seasonNum}E${episodeNum}`} - ${source.quality}`;
|
||||||
|
|
||||||
streams.push({
|
streams.push({
|
||||||
name: streamName,
|
name: streamTitle,
|
||||||
title: streamTitle,
|
title: streamTitle,
|
||||||
url: source.url,
|
url: source.url,
|
||||||
quality: parseQualityFromLabel(source.quality),
|
quality: parseQualityFromLabel(source.quality),
|
||||||
codecs: source.codec ? [source.codec] : [],
|
provider: 'PStream',
|
||||||
size: source.size || 'Unknown size',
|
headers: {}
|
||||||
provider: 'PStream'
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2913,17 +2919,15 @@ const processPStreamResponse = (apiResponse, tmdbType, seasonNum, episodeNum) =>
|
|||||||
|
|
||||||
for (const item of apiResponse.data) {
|
for (const item of apiResponse.data) {
|
||||||
if (item.url || item.file) {
|
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'}`;
|
const streamTitle = apiResponse.name || `${tmdbType === 'movie' ? 'PStream' : `PStream - S${seasonNum}E${episodeNum}`} - ${item.quality || item.label || 'Unknown'}`;
|
||||||
|
|
||||||
streams.push({
|
streams.push({
|
||||||
name: streamName,
|
name: streamTitle,
|
||||||
title: streamTitle,
|
title: streamTitle,
|
||||||
url: item.url || item.file,
|
url: item.url || item.file,
|
||||||
quality: parseQualityFromLabel(item.quality || item.label),
|
quality: parseQualityFromLabel(item.quality || item.label),
|
||||||
codecs: item.codec ? [item.codec] : [],
|
provider: 'PStream',
|
||||||
size: item.size || 'Unknown size',
|
headers: {}
|
||||||
provider: 'PStream'
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2957,8 +2961,8 @@ const getTmdbDataForPStream = async (tmdbType, tmdbId) => {
|
|||||||
let tmdbData = await getFromCache(cacheKey, cacheSubDir);
|
let tmdbData = await getFromCache(cacheKey, cacheSubDir);
|
||||||
|
|
||||||
if (!tmdbData || process.env.DISABLE_CACHE === 'true') {
|
if (!tmdbData || process.env.DISABLE_CACHE === 'true') {
|
||||||
const TMDB_API_KEY = obtainTmdbKey();
|
const TMDB_API_KEY = obtainTmdbKey();
|
||||||
const tmdbApiUrl = `${TMDB_BASE_URL}/${tmdbType}/${tmdbId}?api_key=${TMDB_API_KEY}`;
|
const tmdbApiUrl = `${TMDB_BASE_URL}/${tmdbType}/${tmdbId}?api_key=${TMDB_API_KEY}`;
|
||||||
console.log(` [PStream] Fetching TMDB data from: ${tmdbApiUrl}`);
|
console.log(` [PStream] Fetching TMDB data from: ${tmdbApiUrl}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -2986,8 +2990,8 @@ const getTmdbDataForPStream = async (tmdbType, tmdbId) => {
|
|||||||
let externalIdsData = await getFromCache(externalIdsCacheKey, cacheSubDir);
|
let externalIdsData = await getFromCache(externalIdsCacheKey, cacheSubDir);
|
||||||
|
|
||||||
if (!externalIdsData || process.env.DISABLE_CACHE === 'true') {
|
if (!externalIdsData || process.env.DISABLE_CACHE === 'true') {
|
||||||
const TMDB_API_KEY = obtainTmdbKey();
|
const TMDB_API_KEY = obtainTmdbKey();
|
||||||
const externalIdsApiUrl = `${TMDB_BASE_URL}/${tmdbType}/${tmdbId}/external_ids?api_key=${TMDB_API_KEY}`;
|
const externalIdsApiUrl = `${TMDB_BASE_URL}/${tmdbType}/${tmdbId}/external_ids?api_key=${TMDB_API_KEY}`;
|
||||||
console.log(` [PStream] Fetching TMDB external IDs from: ${externalIdsApiUrl}`);
|
console.log(` [PStream] Fetching TMDB external IDs from: ${externalIdsApiUrl}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -3016,7 +3020,7 @@ const getTmdbDataForPStream = async (tmdbType, tmdbId) => {
|
|||||||
imdbId: imdbId,
|
imdbId: imdbId,
|
||||||
title: tmdbType === 'movie' ? tmdbData.title : tmdbData.name,
|
title: tmdbType === 'movie' ? tmdbData.title : tmdbData.name,
|
||||||
year: tmdbData.release_date ? new Date(tmdbData.release_date).getFullYear() :
|
year: tmdbData.release_date ? new Date(tmdbData.release_date).getFullYear() :
|
||||||
tmdbData.first_air_date ? new Date(tmdbData.first_air_date).getFullYear() : null
|
tmdbData.first_air_date ? new Date(tmdbData.first_air_date).getFullYear() : null
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+4
-8
@@ -84,17 +84,13 @@ const getVidZeeStreams = async (tmdbId, mediaType, seasonNum, episodeNum) => {
|
|||||||
const language = sourceItem.language || sourceItem.lang;
|
const language = sourceItem.language || sourceItem.lang;
|
||||||
|
|
||||||
return {
|
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
|
url: sourceItem.link, // Use sourceItem.link for the URL
|
||||||
quality: quality,
|
quality: quality,
|
||||||
language: language,
|
|
||||||
provider: "VidZee",
|
provider: "VidZee",
|
||||||
size: "Unknown size",
|
headers: {
|
||||||
behaviorHints: {
|
'Referer': 'https://core.vidzee.wtf/'
|
||||||
notWebReady: true,
|
|
||||||
headers: {
|
|
||||||
'Referer': 'https://core.vidzee.wtf/'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}).filter(stream => stream.url);
|
}).filter(stream => stream.url);
|
||||||
|
|||||||
@@ -0,0 +1,637 @@
|
|||||||
|
// 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(/<div class="server"[^>]*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(/<iframe[^>]*src="([^"]*)"[^>]*>/i);
|
||||||
|
if (iframeMatch) {
|
||||||
|
const iframeSrc = iframeMatch[1];
|
||||||
|
console.log(`[moviesclub] Found iframe src: ${iframeSrc}`);
|
||||||
|
|
||||||
|
// Try to decode if it's base64
|
||||||
|
try {
|
||||||
|
const decodedSrc = Buffer.from(iframeSrc.split('/').pop(), 'base64').toString('utf-8');
|
||||||
|
console.log(`[moviesclub] Decoded iframe src: ${decodedSrc}`);
|
||||||
|
|
||||||
|
// If it's a URL, use it directly
|
||||||
|
if (decodedSrc.startsWith('http')) {
|
||||||
|
const stream = {
|
||||||
|
name: `MoviesClub - Direct Stream`,
|
||||||
|
title: `${mediaType === 'movie' ? 'Movie' : `S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`} ${tmdbId} - MoviesClub Direct`,
|
||||||
|
url: decodedSrc,
|
||||||
|
quality: extractQualityFromUrl(decodedSrc) || '720p'
|
||||||
|
};
|
||||||
|
streams.push(stream);
|
||||||
|
console.log(`[moviesclub] Added direct stream from iframe`);
|
||||||
|
return streams;
|
||||||
|
}
|
||||||
|
} catch (decodeError) {
|
||||||
|
console.log(`[moviesclub] Could not decode iframe src: ${decodeError.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return streams;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[moviesclub] Found ${serverMatches.length} server options`);
|
||||||
|
|
||||||
|
// Process each server
|
||||||
|
for (let i = 0; i < serverMatches.length; i++) {
|
||||||
|
const serverMatch = serverMatches[i];
|
||||||
|
const hashMatch = serverMatch.match(/data-hash="([^"]+)"/);
|
||||||
|
const nameMatch = serverMatch.match(/>([^<]+)<\/div>/);
|
||||||
|
|
||||||
|
if (!hashMatch || !nameMatch) {
|
||||||
|
console.log(`[moviesclub] Could not parse server ${i + 1}, skipping`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const encodedHash = hashMatch[1];
|
||||||
|
const serverName = nameMatch[1].trim();
|
||||||
|
|
||||||
|
console.log(`[moviesclub] Processing server ${i + 1}: ${serverName}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Construct the rcp URL from the hash
|
||||||
|
const rcpUrl = `https://cloudnestra.com/rcp/${encodedHash}`;
|
||||||
|
console.log(`[moviesclub] RCP URL: ${rcpUrl}`);
|
||||||
|
|
||||||
|
// Fetch the rcp URL to get the JavaScript with prorcp URL
|
||||||
|
const rcpResponse = await axios.get(rcpUrl, {
|
||||||
|
timeout: 8000,
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||||||
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||||
|
'Accept-Language': 'en-US,en;q=0.5',
|
||||||
|
'Accept-Encoding': 'gzip, deflate',
|
||||||
|
'Connection': 'keep-alive',
|
||||||
|
'Referer': embedUrl
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const rcpHtml = rcpResponse.data;
|
||||||
|
console.log(`[moviesclub] RCP response length: ${rcpHtml.length}`);
|
||||||
|
|
||||||
|
// Extract prorcp URL from the JavaScript
|
||||||
|
const prorcpMatch = rcpHtml.match(/src:\s*['"]([^'"]*prorcp[^'"]*)['"]/i);
|
||||||
|
if (!prorcpMatch) {
|
||||||
|
// Try alternative patterns
|
||||||
|
const altMatch = rcpHtml.match(/['"](\/prorcp\/[^'"]*)['"]/i);
|
||||||
|
if (altMatch) {
|
||||||
|
prorcpUrl = altMatch[1];
|
||||||
|
} else {
|
||||||
|
console.log(`[moviesclub] No prorcp URL found in rcp response`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
prorcpUrl = prorcpMatch[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[moviesclub] Found prorcp URL: ${prorcpUrl}`);
|
||||||
|
|
||||||
|
// If it's a relative URL, make it absolute
|
||||||
|
if (prorcpUrl.startsWith('/')) {
|
||||||
|
prorcpUrl = `https://cloudnestra.com${prorcpUrl}`;
|
||||||
|
console.log(`[moviesclub] Converted to absolute URL: ${prorcpUrl}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the prorcp URL to get the player HTML
|
||||||
|
const prorcpResponse = await axios.get(prorcpUrl, {
|
||||||
|
timeout: 8000,
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||||||
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||||
|
'Accept-Language': 'en-US,en;q=0.5',
|
||||||
|
'Accept-Encoding': 'gzip, deflate',
|
||||||
|
'Connection': 'keep-alive',
|
||||||
|
'Referer': rcpUrl
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const prorcpHtml = prorcpResponse.data;
|
||||||
|
console.log(`[moviesclub] Prorcp response length: ${prorcpHtml.length}`);
|
||||||
|
|
||||||
|
// Extract stream URL from Playerjs configuration
|
||||||
|
const streamUrl = extractStreamFromPlayerjsUpdated(prorcpHtml);
|
||||||
|
if (!streamUrl) {
|
||||||
|
console.log(`[moviesclub] No stream URL found in prorcp response`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[moviesclub] Extracted stream URL: ${streamUrl.substring(0, 100)}...`);
|
||||||
|
|
||||||
|
// Create stream object
|
||||||
|
const stream = {
|
||||||
|
name: `MoviesClub - ${serverName}`,
|
||||||
|
title: `${mediaType === 'movie' ? 'Movie' : `S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`} ${tmdbId} - ${serverName}`,
|
||||||
|
url: streamUrl,
|
||||||
|
quality: extractQualityFromUrl(streamUrl) || '720p',
|
||||||
|
headers: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
streams.push(stream);
|
||||||
|
console.log(`[moviesclub] Added stream from server: ${serverName}`);
|
||||||
|
|
||||||
|
} catch (serverError) {
|
||||||
|
console.error(`[moviesclub] Error processing server ${serverName}:`, serverError.message);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[moviesclub] Total streams found: ${streams.length}`);
|
||||||
|
return streams;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[moviesclub] Error in handleMultipleServers:`, error.message);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle regular embed URLs (existing logic)
|
||||||
|
async function handleSingleServer(embedUrl, mediaType, season, episode, tmdbId) {
|
||||||
|
console.log(`[moviesclub] Handling single server from: ${embedUrl}`);
|
||||||
|
|
||||||
|
const streams = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch the embed page
|
||||||
|
const embedResponse = await axios.get(embedUrl, {
|
||||||
|
timeout: 10000,
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||||||
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||||
|
'Accept-Language': 'en-US,en;q=0.5',
|
||||||
|
'Accept-Encoding': 'gzip, deflate',
|
||||||
|
'Connection': 'keep-alive'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const embedHtml = embedResponse.data;
|
||||||
|
console.log(`[moviesclub] Fetched embed page: ${embedHtml.length} characters`);
|
||||||
|
|
||||||
|
// Look for packed JavaScript using JsUnpacker.detect()
|
||||||
|
const scriptTags = embedHtml.match(/<script[^>]*>[\s\S]*?<\/script>/gi) || [];
|
||||||
|
let packedCode = null;
|
||||||
|
|
||||||
|
for (const scriptTag of scriptTags) {
|
||||||
|
// Extract script content
|
||||||
|
const scriptContent = scriptTag.replace(/<\/?script[^>]*>/gi, '').trim();
|
||||||
|
|
||||||
|
if (scriptContent) {
|
||||||
|
// Create unpacker instance and check if it's packed
|
||||||
|
const unpacker = new JsUnpacker(scriptContent);
|
||||||
|
|
||||||
|
if (unpacker.detect()) {
|
||||||
|
console.log(`[moviesclub] Found packed JavaScript in script tag, length: ${scriptContent.length}`);
|
||||||
|
packedCode = scriptContent;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!packedCode) {
|
||||||
|
console.log('[moviesclub] No packed JavaScript found');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unpack using JsUnpacker
|
||||||
|
const unpacker = new JsUnpacker(packedCode);
|
||||||
|
const unpackedCode = unpacker.unpack();
|
||||||
|
|
||||||
|
if (!unpackedCode) {
|
||||||
|
console.log('[moviesclub] Failed to unpack JavaScript');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[moviesclub] Successfully unpacked code, length: ${unpackedCode.length}`);
|
||||||
|
|
||||||
|
// Parse the player configuration from unpacked code
|
||||||
|
const playerConfig = parsePlayerConfig(unpackedCode);
|
||||||
|
if (!playerConfig) {
|
||||||
|
console.log('[moviesclub] Failed to parse player configuration');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[moviesclub] Parsed player config:', playerConfig);
|
||||||
|
|
||||||
|
// Extract streams from the configuration
|
||||||
|
if (playerConfig.file) {
|
||||||
|
const stream = {
|
||||||
|
name: `MoviesClub - ${playerConfig.title || 'Server 1'}`,
|
||||||
|
title: playerConfig.title || `${mediaType === 'movie' ? 'Movie' : `S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`} ${tmdbId} - MoviesClub`,
|
||||||
|
url: playerConfig.file,
|
||||||
|
quality: extractQualityFromUrl(playerConfig.file) || '720p',
|
||||||
|
headers: (() => {
|
||||||
|
if (playerConfig.file.includes('vidora.stream')) {
|
||||||
|
return {
|
||||||
|
Origin: 'https://vidora.stream',
|
||||||
|
Referer: 'https://vidora.stream/'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
})()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add poster if available
|
||||||
|
if (playerConfig.poster) {
|
||||||
|
stream.poster = playerConfig.poster;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add subtitles if available
|
||||||
|
if (playerConfig.subtitle) {
|
||||||
|
stream.subtitles = parseSubtitles(playerConfig.subtitle);
|
||||||
|
}
|
||||||
|
|
||||||
|
streams.push(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[moviesclub] Found ${streams.length} streams`);
|
||||||
|
return streams;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[moviesclub] Error in handleSingleServer:`, error.message);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract stream URL from Playerjs configuration in HTML
|
||||||
|
function extractStreamFromPlayerjs(html) {
|
||||||
|
try {
|
||||||
|
// Look for the Playerjs constructor call
|
||||||
|
const playerMatch = html.match(/new Playerjs\(\s*(\{[^}]*\})\s*\)/);
|
||||||
|
if (!playerMatch) {
|
||||||
|
console.log('[moviesclub] No Playerjs constructor found in HTML');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const configStr = playerMatch[1];
|
||||||
|
console.log(`[moviesclub] Found Playerjs config: ${configStr.substring(0, 200)}...`);
|
||||||
|
|
||||||
|
// Extract the file URL from the configuration
|
||||||
|
const fileMatch = configStr.match(/file:\s*['"]([^'"]+)['"]/);
|
||||||
|
if (!fileMatch) {
|
||||||
|
console.log('[moviesclub] No file URL found in Playerjs config');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const streamUrl = fileMatch[1];
|
||||||
|
console.log(`[moviesclub] Extracted stream URL from Playerjs: ${streamUrl.substring(0, 100)}...`);
|
||||||
|
|
||||||
|
return streamUrl;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[moviesclub] Error extracting stream from Playerjs:', error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract streams from server response
|
||||||
|
function extractStreamsFromServer(serverData, serverName, mediaType, season, episode, tmdbId) {
|
||||||
|
const streams = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Handle different response formats
|
||||||
|
if (typeof serverData === 'string') {
|
||||||
|
// Try to parse as JSON
|
||||||
|
try {
|
||||||
|
serverData = JSON.parse(serverData);
|
||||||
|
} catch (parseError) {
|
||||||
|
console.log(`[moviesclub] Server response is not JSON, treating as direct URL: ${serverData.substring(0, 100)}`);
|
||||||
|
// Treat as direct stream URL
|
||||||
|
if (serverData.startsWith('http')) {
|
||||||
|
const stream = {
|
||||||
|
name: `MoviesClub - ${serverName}`,
|
||||||
|
title: `${mediaType === 'movie' ? 'Movie' : `S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`} ${tmdbId} - ${serverName}`,
|
||||||
|
url: serverData,
|
||||||
|
quality: extractQualityFromUrl(serverData) || '720p',
|
||||||
|
headers: serverData.includes('vidora.stream') ? { Origin: 'https://vidora.stream', Referer: 'https://vidora.stream/' } : {}
|
||||||
|
};
|
||||||
|
streams.push(stream);
|
||||||
|
}
|
||||||
|
return streams;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle JSON response
|
||||||
|
if (serverData && typeof serverData === 'object') {
|
||||||
|
// Look for common stream URL patterns
|
||||||
|
const possibleKeys = ['file', 'url', 'src', 'stream', 'video', 'source'];
|
||||||
|
|
||||||
|
for (const key of possibleKeys) {
|
||||||
|
if (serverData[key] && typeof serverData[key] === 'string' && serverData[key].startsWith('http')) {
|
||||||
|
console.log(`[moviesclub] Found stream URL in key '${key}': ${serverData[key].substring(0, 100)}`);
|
||||||
|
|
||||||
|
const stream = {
|
||||||
|
name: `MoviesClub - ${serverName}`,
|
||||||
|
title: `${mediaType === 'movie' ? 'Movie' : `S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`} ${tmdbId} - ${serverName}`,
|
||||||
|
url: serverData[key],
|
||||||
|
quality: extractQualityFromUrl(serverData[key]) || '720p',
|
||||||
|
headers: serverData[key].includes('vidora.stream') ? { Origin: 'https://vidora.stream', Referer: 'https://vidora.stream/' } : {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add poster if available
|
||||||
|
if (serverData.poster) {
|
||||||
|
stream.poster = serverData.poster;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add subtitles if available
|
||||||
|
if (serverData.subtitle || serverData.subtitles) {
|
||||||
|
const subtitleData = serverData.subtitle || serverData.subtitles;
|
||||||
|
stream.subtitles = parseSubtitles(subtitleData);
|
||||||
|
}
|
||||||
|
|
||||||
|
streams.push(stream);
|
||||||
|
break; // Only take the first valid stream URL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no direct URL found, check for nested objects
|
||||||
|
if (streams.length === 0) {
|
||||||
|
for (const key of possibleKeys) {
|
||||||
|
if (serverData[key] && typeof serverData[key] === 'object') {
|
||||||
|
const nestedStreams = extractStreamsFromServer(serverData[key], serverName, mediaType, season, episode, tmdbId);
|
||||||
|
streams.push(...nestedStreams);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[moviesclub] Error extracting streams from server ${serverName}:`, error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return streams;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract stream URL from Playerjs configuration in HTML
|
||||||
|
function extractStreamFromPlayerjs(html) {
|
||||||
|
try {
|
||||||
|
// Look for the Playerjs constructor call
|
||||||
|
const playerMatch = html.match(/new Playerjs\(\s*(\{[^}]*\})\s*\)/);
|
||||||
|
if (!playerMatch) {
|
||||||
|
console.log('[moviesclub] No Playerjs constructor found in HTML');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const configStr = playerMatch[1];
|
||||||
|
console.log(`[moviesclub] Found Playerjs config: ${configStr.substring(0, 200)}...`);
|
||||||
|
|
||||||
|
// Extract the file URL from the configuration
|
||||||
|
const fileMatch = configStr.match(/file:\s*['"]([^'"]+)['"]/);
|
||||||
|
if (!fileMatch) {
|
||||||
|
console.log('[moviesclub] No file URL found in Playerjs config');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const streamUrl = fileMatch[1];
|
||||||
|
console.log(`[moviesclub] Extracted stream URL from Playerjs: ${streamUrl.substring(0, 100)}...`);
|
||||||
|
|
||||||
|
return streamUrl;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[moviesclub] Error extracting stream from Playerjs:', error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse player configuration from unpacked JavaScript
|
||||||
|
function parsePlayerConfig(unpackedCode) {
|
||||||
|
try {
|
||||||
|
// Look for the player configuration object
|
||||||
|
const configMatch = unpackedCode.match(/new Playerjs\(\s*(\{[^}]*\})\s*\)/);
|
||||||
|
if (!configMatch) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the configuration object
|
||||||
|
const configStr = configMatch[1];
|
||||||
|
|
||||||
|
// Parse the configuration (this is a simple parser for the expected format)
|
||||||
|
const config = {};
|
||||||
|
|
||||||
|
// Extract key-value pairs
|
||||||
|
const pairs = configStr.match(/(\w+):"([^"]*)"/g) || [];
|
||||||
|
pairs.forEach(pair => {
|
||||||
|
const [, key, value] = pair.match(/(\w+):"([^"]*)"/);
|
||||||
|
config[key] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also handle non-quoted values
|
||||||
|
const numberPairs = configStr.match(/(\w+):(\d+(?:\.\d+)?)/g) || [];
|
||||||
|
numberPairs.forEach(pair => {
|
||||||
|
const [, key, value] = pair.match(/(\w+):(\d+(?:\.\d+)?)/);
|
||||||
|
config[key] = parseFloat(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
return config;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[moviesclub] Error parsing player config:', error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse subtitles from the subtitle string
|
||||||
|
function parseSubtitles(subtitleStr) {
|
||||||
|
if (!subtitleStr) return [];
|
||||||
|
|
||||||
|
const subtitles = [];
|
||||||
|
const subtitleMatches = subtitleStr.match(/\[([^\]]+)\]([^,\[]*)/g);
|
||||||
|
|
||||||
|
if (subtitleMatches) {
|
||||||
|
subtitleMatches.forEach(match => {
|
||||||
|
const [, language, url] = match.match(/\[([^\]]+)\]([^,\[]*)/);
|
||||||
|
if (url && url.trim()) {
|
||||||
|
subtitles.push({
|
||||||
|
language: language.trim(),
|
||||||
|
url: url.trim(),
|
||||||
|
type: 'vtt'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return subtitles;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract quality from URL
|
||||||
|
function extractQualityFromUrl(url) {
|
||||||
|
if (!url) return '720p';
|
||||||
|
|
||||||
|
const qualityPatterns = [
|
||||||
|
{ pattern: /1080p|1080/i, quality: '1080p' },
|
||||||
|
{ pattern: /720p|720/i, quality: '720p' },
|
||||||
|
{ pattern: /480p|480/i, quality: '480p' },
|
||||||
|
{ pattern: /360p|360/i, quality: '360p' },
|
||||||
|
{ pattern: /2160p|4k|uhd/i, quality: '2160p' }
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { pattern, quality } of qualityPatterns) {
|
||||||
|
if (pattern.test(url)) {
|
||||||
|
return quality;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '720p';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract stream URL from Playerjs configuration in HTML (updated version)
|
||||||
|
function extractStreamFromPlayerjsUpdated(html) {
|
||||||
|
try {
|
||||||
|
// Look for the Playerjs constructor call
|
||||||
|
const playerMatch = html.match(/new Playerjs\(\s*(\{[^}]*\})\s*\)/);
|
||||||
|
if (!playerMatch) {
|
||||||
|
console.log('[moviesclub] No Playerjs constructor found in HTML');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const configStr = playerMatch[1];
|
||||||
|
console.log(`[moviesclub] Found Playerjs config: ${configStr.substring(0, 200)}...`);
|
||||||
|
|
||||||
|
// Extract the file URL from the configuration
|
||||||
|
let streamUrl = null;
|
||||||
|
|
||||||
|
// Try multiple patterns to extract the file URL
|
||||||
|
const patterns = [
|
||||||
|
/file:\s*['"]([^'"]+)['"]/,
|
||||||
|
/file:\s*([^,\s}]+)/,
|
||||||
|
/file\s*:\s*['"]([^'"]+)['"]/,
|
||||||
|
/file\s*:\s*([^,\s}]+)/
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
const match = configStr.match(pattern);
|
||||||
|
if (match && match[1] && match[1].trim() !== '' && match[1].trim() !== '""') {
|
||||||
|
streamUrl = match[1].replace(/['"]/g, '').trim();
|
||||||
|
console.log(`[moviesclub] Found stream URL with pattern: ${streamUrl.substring(0, 100)}...`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no URL found in config, look for streaming URLs in the HTML
|
||||||
|
if (!streamUrl) {
|
||||||
|
console.log('[moviesclub] No file URL in Playerjs config, searching HTML for streaming URLs...');
|
||||||
|
|
||||||
|
// Look for m3u8 URLs
|
||||||
|
const m3u8Match = html.match(/https?:\/\/[^'"\s]+\.m3u8[^'"\s]*/i);
|
||||||
|
if (m3u8Match) {
|
||||||
|
streamUrl = m3u8Match[0];
|
||||||
|
console.log(`[moviesclub] Found m3u8 URL in HTML: ${streamUrl.substring(0, 100)}...`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for other streaming URLs
|
||||||
|
if (!streamUrl) {
|
||||||
|
const streamMatch = html.match(/https?:\/\/[^'"\s]+(?:\.mp4|\.m3u8|\.ts)[^'"\s]*/i);
|
||||||
|
if (streamMatch) {
|
||||||
|
streamUrl = streamMatch[0];
|
||||||
|
console.log(`[moviesclub] Found streaming URL in HTML: ${streamUrl.substring(0, 100)}...`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!streamUrl) {
|
||||||
|
console.log('[moviesclub] No file URL found in Playerjs config after trying all patterns');
|
||||||
|
console.log('[moviesclub] Config string:', configStr.substring(0, 500));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[moviesclub] Extracted stream URL from Playerjs: ${streamUrl.substring(0, 100)}...`);
|
||||||
|
|
||||||
|
return streamUrl;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[moviesclub] Error extracting stream from Playerjs:', error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getMoviesClubStreams
|
||||||
|
};
|
||||||
@@ -1106,6 +1106,8 @@ async function getMoviesModStreams(tmdbId, mediaType, seasonNum = null, episodeN
|
|||||||
title: `${cleanFileName}\n${sizeInfo || ''}${techDetailsString}`,
|
title: `${cleanFileName}\n${sizeInfo || ''}${techDetailsString}`,
|
||||||
url: selectedResult.url,
|
url: selectedResult.url,
|
||||||
quality: actualQuality,
|
quality: actualQuality,
|
||||||
|
provider: 'moviesmod',
|
||||||
|
headers: {}
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[MoviesMod] Unsupported URL type for final processing: ${targetLink?.url}`);
|
console.warn(`[MoviesMod] Unsupported URL type for final processing: ${targetLink?.url}`);
|
||||||
|
|||||||
+157
-241
@@ -1,12 +1,23 @@
|
|||||||
const { config } = require('../utils/config');
|
const { config } = require('../utils/config');
|
||||||
// Lazy load showbox to avoid early side-effects; temporarily disabled until stabilized
|
const fs = require('fs');
|
||||||
let showbox = null;
|
const path = require('path');
|
||||||
let k4hdhub = null; // 4khdhub provider lazy ref
|
|
||||||
let moviesmod = null; // moviesmod provider lazy ref
|
// Lazy load cache for providers
|
||||||
let mp4hydra = null; // MP4Hydra provider lazy ref
|
const providerCache = new Map();
|
||||||
let vidzee = null; // VidZee provider lazy ref
|
|
||||||
let vixsrc = null; // vixsrc provider lazy ref
|
// Function name mappings for existing providers
|
||||||
let xprime = null; // xprime provider lazy ref
|
const providerFunctionMap = {
|
||||||
|
'Showbox.js': 'getStreamsFromTmdbId',
|
||||||
|
'4khdhub.js': 'get4KHDHubStreams',
|
||||||
|
'moviesmod.js': 'getMoviesModStreams',
|
||||||
|
'MP4Hydra.js': 'getMP4HydraStreams',
|
||||||
|
'VidZee.js': 'getVidZeeStreams',
|
||||||
|
'vixsrc.js': 'getVixsrcStreams',
|
||||||
|
'xprime.js': 'getXprimeStreams',
|
||||||
|
'uhdmovies.js': 'getUHDMoviesStreams',
|
||||||
|
'moviesclub.js': 'getMoviesClubStreams',
|
||||||
|
};
|
||||||
|
|
||||||
// Stats for debug endpoint
|
// Stats for debug endpoint
|
||||||
let lastCookieStats = { selected: null, index: null, total: 0, remainingMB: null, timestamp: null };
|
let lastCookieStats = { selected: null, index: null, total: 0, remainingMB: null, timestamp: null };
|
||||||
|
|
||||||
@@ -14,248 +25,153 @@ async function getEffectiveCookies() {
|
|||||||
return Array.isArray(config.febboxCookies) ? config.febboxCookies : [];
|
return Array.isArray(config.febboxCookies) ? config.febboxCookies : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adapter for Showbox provider using existing exported getStreamsFromTmdbId
|
// Get all provider files
|
||||||
async function fetchShowbox(ctx) {
|
function getProviderFiles() {
|
||||||
if (!showbox) {
|
const providersDir = path.join(__dirname);
|
||||||
try { showbox = require('./Showbox.js'); } catch (e) { console.error('Failed to load Showbox provider:', e.message); return []; }
|
return fs.readdirSync(providersDir)
|
||||||
}
|
.filter(file => file.endsWith('.js') && file !== 'registry.js' && file !== 'vidsrcextractor.js')
|
||||||
const { getTmdbApiKey } = require('../utils/tmdbKey');
|
.map(file => ({
|
||||||
const tmdbApiKey = getTmdbApiKey();
|
name: path.parse(file).name.toLowerCase(),
|
||||||
if (!tmdbApiKey) {
|
file: file,
|
||||||
console.warn('[registry] showbox skipped: TMDB API key missing');
|
functionName: providerFunctionMap[file]
|
||||||
return [];
|
}));
|
||||||
}
|
|
||||||
const cookies = await getEffectiveCookies();
|
|
||||||
const previousConfig = global.currentRequestConfig;
|
|
||||||
global.currentRequestConfig = { ...(previousConfig || {}) };
|
|
||||||
let selected = null;
|
|
||||||
if (cookies.length > 0) {
|
|
||||||
const index = Math.floor(Math.random() * cookies.length);
|
|
||||||
selected = cookies[index];
|
|
||||||
global.currentRequestConfig.cookie = selected.startsWith('ui=') ? selected : `ui=${selected}`;
|
|
||||||
global.currentRequestConfig.cookies = cookies.map(c => c.startsWith('ui=') ? c : `ui=${c}`);
|
|
||||||
lastCookieStats = { selected: selected.slice(0, 16) + '...', index, total: cookies.length, remainingMB: null, timestamp: Date.now() };
|
|
||||||
console.log(`[registry] Cookie random pick index=${index} total=${cookies.length}`);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const tmdbType = ctx.type === 'movie' ? 'movie' : 'tv';
|
|
||||||
console.log(`[registry] Fetching Showbox streams for ${tmdbType} tmdbId=${ctx.tmdbId} season=${ctx.season||''} episode=${ctx.episode||''}`);
|
|
||||||
const t0 = Date.now();
|
|
||||||
const streams = await showbox.getStreamsFromTmdbId(tmdbType, ctx.tmdbId, ctx.season || null, ctx.episode || null, null, selected);
|
|
||||||
const durationMs = Date.now() - t0;
|
|
||||||
console.log(`[registry] Provider showbox fetch duration ${durationMs}ms`);
|
|
||||||
if (global.currentRequestUserCookieRemainingMB != null) {
|
|
||||||
lastCookieStats.remainingMB = global.currentRequestUserCookieRemainingMB;
|
|
||||||
}
|
|
||||||
console.log(`[registry] Showbox returned ${Array.isArray(streams)?streams.length:0} streams`);
|
|
||||||
return Array.isArray(streams) ? streams : [];
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[registry] Showbox fetch error:', e.message);
|
|
||||||
return [];
|
|
||||||
} finally {
|
|
||||||
global.currentRequestConfig = previousConfig || {};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load a provider module
|
||||||
|
function loadProvider(providerFile) {
|
||||||
|
if (!providerCache.has(providerFile)) {
|
||||||
|
try {
|
||||||
|
providerCache.set(providerFile, require(path.join(__dirname, providerFile)));
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to load provider ${providerFile}:`, e.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return providerCache.get(providerFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create fetch function for a provider
|
||||||
|
function createFetchFunction(providerInfo) {
|
||||||
|
return async function(ctx) {
|
||||||
|
const module = loadProvider(providerInfo.file);
|
||||||
|
if (!module) return [];
|
||||||
|
|
||||||
|
const funcName = providerInfo.functionName;
|
||||||
|
if (!module[funcName]) {
|
||||||
|
console.error(`Provider ${providerInfo.name} does not export ${funcName}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
|
||||||
|
const t0 = Date.now();
|
||||||
|
|
||||||
|
let result;
|
||||||
|
if (providerInfo.name === 'showbox') {
|
||||||
|
// Special case for Showbox with TMDB key and cookies
|
||||||
|
const { getTmdbApiKey } = require('../utils/tmdbKey');
|
||||||
|
const tmdbApiKey = getTmdbApiKey();
|
||||||
|
if (!tmdbApiKey) {
|
||||||
|
console.warn('[registry] showbox skipped: TMDB API key missing');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const cookies = await getEffectiveCookies();
|
||||||
|
const previousConfig = global.currentRequestConfig;
|
||||||
|
global.currentRequestConfig = { ...(previousConfig || {}) };
|
||||||
|
let selected = null;
|
||||||
|
if (cookies.length > 0) {
|
||||||
|
const index = Math.floor(Math.random() * cookies.length);
|
||||||
|
selected = cookies[index];
|
||||||
|
global.currentRequestConfig.cookie = selected.startsWith('ui=') ? selected : `ui=${selected}`;
|
||||||
|
global.currentRequestConfig.cookies = cookies.map(c => c.startsWith('ui=') ? c : `ui=${c}`);
|
||||||
|
lastCookieStats = { selected: selected.slice(0, 16) + '...', index, total: cookies.length, remainingMB: null, timestamp: Date.now() };
|
||||||
|
console.log(`[registry] Cookie random pick index=${index} total=${cookies.length}`);
|
||||||
|
}
|
||||||
|
result = await module[funcName](mediaType, ctx.tmdbId, ctx.season || null, ctx.episode || null, null, selected);
|
||||||
|
if (global.currentRequestUserCookieRemainingMB != null) {
|
||||||
|
lastCookieStats.remainingMB = global.currentRequestUserCookieRemainingMB;
|
||||||
|
}
|
||||||
|
global.currentRequestConfig = previousConfig || {};
|
||||||
|
} else if (providerInfo.name === 'xprime') {
|
||||||
|
// Special case for Xprime with TMDB metadata lookup
|
||||||
|
const { getTmdbApiKey } = require('../utils/tmdbKey');
|
||||||
|
const tmdbApiKey = getTmdbApiKey();
|
||||||
|
if (!tmdbApiKey) {
|
||||||
|
console.warn('[registry] xprime skipped: TMDB API key missing');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
// Fetch minimal TMDB metadata for title + year
|
||||||
|
let title = null; let year = null;
|
||||||
|
try {
|
||||||
|
const axios = require('axios');
|
||||||
|
const metaUrl = `https://api.themoviedb.org/3/${mediaType}/${ctx.tmdbId}?api_key=${tmdbApiKey}`;
|
||||||
|
const { data: meta } = await axios.get(metaUrl, { timeout: 8000 });
|
||||||
|
title = mediaType === 'movie' ? (meta.title || meta.original_title) : (meta.name || meta.original_name);
|
||||||
|
const dateStr = mediaType === 'movie' ? meta.release_date : meta.first_air_date;
|
||||||
|
if (dateStr) year = dateStr.split('-')[0];
|
||||||
|
} catch (mErr) {
|
||||||
|
console.warn('[registry] xprime metadata fetch failed:', mErr.message);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (!title || !year) {
|
||||||
|
console.log('[registry] xprime missing title/year after TMDB lookup');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
result = await module[funcName](title, year, mediaType, ctx.season || null, ctx.episode || null);
|
||||||
|
// Normalize quality labels
|
||||||
|
if (Array.isArray(result)) {
|
||||||
|
result = result.map(s => {
|
||||||
|
let q = s.quality || 'Unknown';
|
||||||
|
if (!/(\d{3,4})p/.test(q.toLowerCase())) {
|
||||||
|
const qLower = q.toLowerCase();
|
||||||
|
if (qLower.includes('fhd') || qLower.includes('full')) q = '1080p';
|
||||||
|
else if (qLower === 'hd' || qLower.includes('720')) q = '720p';
|
||||||
|
else if (qLower.includes('sd') || qLower.includes('480')) q = '480p';
|
||||||
|
else if (qLower.includes('cam')) q = '360p';
|
||||||
|
}
|
||||||
|
return { ...s, quality: q, name: s.name || 'Xprime', provider: 'xprime' };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Standard provider call
|
||||||
|
result = await module[funcName](ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const durationMs = Date.now() - t0;
|
||||||
|
console.log(`[registry] ${providerInfo.name} fetch duration ${durationMs}ms`);
|
||||||
|
|
||||||
|
if (!Array.isArray(result)) return [];
|
||||||
|
|
||||||
|
// Add provider name if not present
|
||||||
|
return result.map(s => ({ ...s, provider: s.provider || providerInfo.name }));
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`[registry] ${providerInfo.name} fetch error:`, e.message);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize providers
|
||||||
|
const providerFiles = getProviderFiles();
|
||||||
const providers = [];
|
const providers = [];
|
||||||
|
|
||||||
if (config.enableShowboxProvider) {
|
for (const providerInfo of providerFiles) {
|
||||||
providers.push({ name: 'showbox', enabled: true, fetch: fetchShowbox });
|
const enableFlag = `enable${providerInfo.name.charAt(0).toUpperCase() + providerInfo.name.slice(1)}Provider`;
|
||||||
} else {
|
const enabled = config[enableFlag] !== false;
|
||||||
console.log('[registry] showbox disabled via config');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
// 4KHDHub adapter
|
providers.push({
|
||||||
async function fetch4khdhub(ctx) {
|
name: providerInfo.name,
|
||||||
if (!k4hdhub) {
|
enabled: true,
|
||||||
try { k4hdhub = require('./4khdhub.js'); } catch (e) { console.error('Failed to load 4khdhub provider:', e.message); return []; }
|
fetch: createFetchFunction(providerInfo)
|
||||||
}
|
|
||||||
try {
|
|
||||||
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
|
|
||||||
const t0 = Date.now();
|
|
||||||
const result = await k4hdhub.get4KHDHubStreams(ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null);
|
|
||||||
const durationMs = Date.now() - t0;
|
|
||||||
console.log(`[registry] 4khdhub fetch duration ${durationMs}ms`);
|
|
||||||
if (!Array.isArray(result)) return [];
|
|
||||||
return result.map(s => ({ ...s, name: s.name || '4KHDHub', provider: '4khdhub' }));
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[registry] 4khdhub fetch error:', e.message);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.enable4khdhubProvider) {
|
|
||||||
providers.push({ name: '4khdhub', enabled: true, fetch: fetch4khdhub });
|
|
||||||
console.log('[registry] 4khdhub provider enabled');
|
|
||||||
} else {
|
|
||||||
console.log('[registry] 4khdhub disabled via config');
|
|
||||||
}
|
|
||||||
|
|
||||||
// MoviesMod adapter
|
|
||||||
async function fetchMoviesMod(ctx) {
|
|
||||||
if (!moviesmod) {
|
|
||||||
try { moviesmod = require('./moviesmod.js'); } catch (e) { console.error('Failed to load moviesmod provider:', e.message); return []; }
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
|
|
||||||
const t0 = Date.now();
|
|
||||||
const result = await moviesmod.getMoviesModStreams(ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null);
|
|
||||||
const durationMs = Date.now() - t0;
|
|
||||||
console.log(`[registry] moviesmod fetch duration ${durationMs}ms`);
|
|
||||||
if (!Array.isArray(result)) return [];
|
|
||||||
return result.map(s => ({ ...s, name: s.name || 'MoviesMod', provider: 'moviesmod' }));
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[registry] moviesmod fetch error:', e.message);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.enableMoviesmodProvider) {
|
|
||||||
providers.push({ name: 'moviesmod', enabled: true, fetch: fetchMoviesMod });
|
|
||||||
console.log('[registry] moviesmod provider enabled');
|
|
||||||
} else {
|
|
||||||
console.log('[registry] moviesmod disabled via config');
|
|
||||||
}
|
|
||||||
|
|
||||||
// MP4Hydra adapter
|
|
||||||
async function fetchMp4hydra(ctx) {
|
|
||||||
if (!mp4hydra) {
|
|
||||||
try { mp4hydra = require('./MP4Hydra.js'); } catch (e) { console.error('Failed to load MP4Hydra provider:', e.message); return []; }
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
|
|
||||||
const t0 = Date.now();
|
|
||||||
const result = await mp4hydra.getMP4HydraStreams(ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null);
|
|
||||||
const durationMs = Date.now() - t0;
|
|
||||||
console.log(`[registry] mp4hydra fetch duration ${durationMs}ms`);
|
|
||||||
if (!Array.isArray(result)) return [];
|
|
||||||
return result.map(s => ({ ...s, name: s.name || 'MP4Hydra', provider: 'mp4hydra' }));
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[registry] mp4hydra fetch error:', e.message);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.enableMp4hydraProvider) {
|
|
||||||
providers.push({ name: 'mp4hydra', enabled: true, fetch: fetchMp4hydra });
|
|
||||||
console.log('[registry] mp4hydra provider enabled');
|
|
||||||
} else {
|
|
||||||
console.log('[registry] mp4hydra disabled via config');
|
|
||||||
}
|
|
||||||
|
|
||||||
// VidZee adapter
|
|
||||||
async function fetchVidZee(ctx) {
|
|
||||||
if (!vidzee) {
|
|
||||||
try { vidzee = require('./VidZee.js'); } catch (e) { console.error('Failed to load VidZee provider:', e.message); return []; }
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
|
|
||||||
const t0 = Date.now();
|
|
||||||
const result = await vidzee.getVidZeeStreams(ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null);
|
|
||||||
const durationMs = Date.now() - t0;
|
|
||||||
console.log(`[registry] vidzee fetch duration ${durationMs}ms`);
|
|
||||||
if (!Array.isArray(result)) return [];
|
|
||||||
return result.map(s => ({ ...s, name: s.name || 'VidZee', provider: 'vidzee' }));
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[registry] vidzee fetch error:', e.message);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.enableVidzeeProvider) {
|
|
||||||
providers.push({ name: 'vidzee', enabled: true, fetch: fetchVidZee });
|
|
||||||
console.log('[registry] vidzee provider enabled');
|
|
||||||
} else {
|
|
||||||
console.log('[registry] vidzee disabled via config');
|
|
||||||
}
|
|
||||||
|
|
||||||
// vixsrc adapter
|
|
||||||
async function fetchVixsrc(ctx) {
|
|
||||||
if (!vixsrc) {
|
|
||||||
try { vixsrc = require('./vixsrc.js'); } catch (e) { console.error('Failed to load vixsrc provider:', e.message); return []; }
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
|
|
||||||
const t0 = Date.now();
|
|
||||||
const result = await vixsrc.getVixsrcStreams(ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null);
|
|
||||||
const durationMs = Date.now() - t0;
|
|
||||||
console.log(`[registry] vixsrc fetch duration ${durationMs}ms`);
|
|
||||||
if (!Array.isArray(result)) return [];
|
|
||||||
return result.map(s => ({ ...s, name: s.name || 'Vixsrc', provider: 'vixsrc' }));
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[registry] vixsrc fetch error:', e.message);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.enableVixsrcProvider) {
|
|
||||||
providers.push({ name: 'vixsrc', enabled: true, fetch: fetchVixsrc });
|
|
||||||
console.log('[registry] vixsrc provider enabled');
|
|
||||||
} else {
|
|
||||||
console.log('[registry] vixsrc disabled via config');
|
|
||||||
}
|
|
||||||
|
|
||||||
// xprime adapter (requires TMDB metadata lookup for title/year)
|
|
||||||
async function fetchXprime(ctx) {
|
|
||||||
if (!xprime) {
|
|
||||||
try { xprime = require('./xprime.js'); } catch (e) { console.error('Failed to load xprime provider:', e.message); return []; }
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
|
|
||||||
const { getTmdbApiKey } = require('../utils/tmdbKey');
|
|
||||||
const tmdbApiKey = getTmdbApiKey();
|
|
||||||
if (!tmdbApiKey) {
|
|
||||||
console.warn('[registry] xprime skipped: TMDB API key missing');
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
// Fetch minimal TMDB metadata for title + year
|
|
||||||
let title = null; let year = null;
|
|
||||||
try {
|
|
||||||
const axios = require('axios');
|
|
||||||
const metaUrl = `https://api.themoviedb.org/3/${mediaType}/${ctx.tmdbId}?api_key=${tmdbApiKey}`;
|
|
||||||
const { data: meta } = await axios.get(metaUrl, { timeout: 8000 });
|
|
||||||
title = mediaType === 'movie' ? (meta.title || meta.original_title) : (meta.name || meta.original_name);
|
|
||||||
const dateStr = mediaType === 'movie' ? meta.release_date : meta.first_air_date;
|
|
||||||
if (dateStr) year = dateStr.split('-')[0];
|
|
||||||
} catch (mErr) {
|
|
||||||
console.warn('[registry] xprime metadata fetch failed:', mErr.message);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
if (!title || !year) {
|
|
||||||
console.log('[registry] xprime missing title/year after TMDB lookup');
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const t0 = Date.now();
|
|
||||||
const streams = await xprime.getXprimeStreams(title, year, mediaType, ctx.season || null, ctx.episode || null);
|
|
||||||
const durationMs = Date.now() - t0;
|
|
||||||
console.log(`[registry] xprime fetch duration ${durationMs}ms (title='${title}' year=${year}) returned ${Array.isArray(streams)?streams.length:0}`);
|
|
||||||
if (!Array.isArray(streams) || !streams.length) return [];
|
|
||||||
// Normalize quality labels that are non-numeric (e.g., FHD/HD/SD)
|
|
||||||
return streams.map(s => {
|
|
||||||
let q = s.quality || 'Unknown';
|
|
||||||
if (!/(\d{3,4})p/.test(q.toLowerCase())) {
|
|
||||||
const qLower = q.toLowerCase();
|
|
||||||
if (qLower.includes('fhd') || qLower.includes('full')) q = '1080p';
|
|
||||||
else if (qLower === 'hd' || qLower.includes('720')) q = '720p';
|
|
||||||
else if (qLower.includes('sd') || qLower.includes('480')) q = '480p';
|
|
||||||
else if (qLower.includes('cam')) q = '360p';
|
|
||||||
}
|
|
||||||
return { ...s, quality: q, name: s.name || 'Xprime', provider: 'xprime' };
|
|
||||||
});
|
});
|
||||||
} catch (e) {
|
console.log(`[registry] ${providerInfo.name} provider enabled`);
|
||||||
console.error('[registry] xprime fetch error:', e.message);
|
} else {
|
||||||
return [];
|
console.log(`[registry] ${providerInfo.name} disabled via config`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.enableXprimeProvider) {
|
|
||||||
providers.push({ name: 'xprime', enabled: true, fetch: fetchXprime });
|
|
||||||
console.log('[registry] xprime provider enabled');
|
|
||||||
} else {
|
|
||||||
console.log('[registry] xprime disabled via config');
|
|
||||||
}
|
|
||||||
|
|
||||||
function listProviders() { return providers.map(p => ({ name: p.name, enabled: p.enabled })); }
|
function listProviders() { return providers.map(p => ({ name: p.name, enabled: p.enabled })); }
|
||||||
function getProvider(name) { return providers.find(p => p.name === name.toLowerCase()) || null; }
|
function getProvider(name) { return providers.find(p => p.name === name.toLowerCase()) || null; }
|
||||||
|
|
||||||
|
|||||||
+11
-14
@@ -46,9 +46,9 @@ async function getUHDMoviesDomain() {
|
|||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
const { getTmdbApiKey } = require('../utils/tmdbKey');
|
const { getTmdbApiKey } = require('../utils/tmdbKey');
|
||||||
function getUhdMoviesKey(){
|
function getUhdMoviesKey() {
|
||||||
const k = getTmdbApiKey();
|
const k = getTmdbApiKey();
|
||||||
if(!k) console.warn('[UHDMovies] TMDB API key missing; certain metadata features skipped.');
|
if (!k) console.warn('[UHDMovies] TMDB API key missing; certain metadata features skipped.');
|
||||||
return k;
|
return k;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,8 +209,8 @@ function extractCleanQuality(fullQualityText) {
|
|||||||
// Fallback: try to extract a shorter version of the original text
|
// Fallback: try to extract a shorter version of the original text
|
||||||
// Look for patterns like "Movie Name (Year) Resolution ..."
|
// Look for patterns like "Movie Name (Year) Resolution ..."
|
||||||
const patterns = [
|
const patterns = [
|
||||||
/(\d{3,4}p.*?(?:x264|x265|hevc).*?)[[(]/i,
|
/(\d{3,4}p.*?(?:x264|x265|hevc).*?)[[(]/i,
|
||||||
/(\d{3,4}p.*?)[[(]/i,
|
/(\d{3,4}p.*?)[[(]/i,
|
||||||
/((?:720p|1080p|2160p|4k).*?)$/i
|
/((?:720p|1080p|2160p|4k).*?)$/i
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -251,7 +251,7 @@ async function extractTvShowDownloadLinks(showPageUrl, season, episode) {
|
|||||||
// Check if we are entering a new season block
|
// Check if we are entering a new season block
|
||||||
if (seasonMatch) {
|
if (seasonMatch) {
|
||||||
const currentSeasonNum = parseInt(seasonMatch[1], 10);
|
const currentSeasonNum = parseInt(seasonMatch[1], 10);
|
||||||
if (currentSeasonNum === season) {
|
if (currentSeasonNum === season) {
|
||||||
inTargetSeason = true;
|
inTargetSeason = true;
|
||||||
console.log(`[UHDMovies] Entering Season ${season} block.`);
|
console.log(`[UHDMovies] Entering Season ${season} block.`);
|
||||||
} else if (inTargetSeason) {
|
} else if (inTargetSeason) {
|
||||||
@@ -352,7 +352,7 @@ async function extractTvShowDownloadLinks(showPageUrl, season, episode) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`[UHDMovies] Actual seasons found on page: ${Array.from(actualSeasonsOnPage).sort((a,b) => a-b).join(', ')}`);
|
console.log(`[UHDMovies] Actual seasons found on page: ${Array.from(actualSeasonsOnPage).sort((a, b) => a - b).join(', ')}`);
|
||||||
|
|
||||||
// Check if requested season is in the actual content
|
// Check if requested season is in the actual content
|
||||||
if (actualSeasonsOnPage.has(season)) {
|
if (actualSeasonsOnPage.has(season)) {
|
||||||
@@ -1178,8 +1178,8 @@ async function getUHDMoviesStreams(tmdbId, mediaType = 'movie', season = null, e
|
|||||||
}
|
}
|
||||||
console.log(`[UHDMovies] Cache MISS for ${cacheKey}. Fetching from source.`);
|
console.log(`[UHDMovies] Cache MISS for ${cacheKey}. Fetching from source.`);
|
||||||
// 2. If cache miss, get TMDB info to perform search
|
// 2. If cache miss, get TMDB info to perform search
|
||||||
const TMDB_API_KEY_UHDMOVIES = getUhdMoviesKey();
|
const TMDB_API_KEY_UHDMOVIES = getUhdMoviesKey();
|
||||||
const tmdbUrl = `https://api.themoviedb.org/3/${mediaType === 'tv' ? 'tv' : 'movie'}/${tmdbId}?api_key=${TMDB_API_KEY_UHDMOVIES}`;
|
const tmdbUrl = `https://api.themoviedb.org/3/${mediaType === 'tv' ? 'tv' : 'movie'}/${tmdbId}?api_key=${TMDB_API_KEY_UHDMOVIES}`;
|
||||||
const tmdbResponse = await axios.get(tmdbUrl);
|
const tmdbResponse = await axios.get(tmdbUrl);
|
||||||
const tmdbData = tmdbResponse.data;
|
const tmdbData = tmdbResponse.data;
|
||||||
const mediaInfo = {
|
const mediaInfo = {
|
||||||
@@ -1252,7 +1252,7 @@ async function getUHDMoviesStreams(tmdbId, mediaType = 'movie', season = null, e
|
|||||||
|
|
||||||
// Check if season was not found or episode extraction failed, and we have multiple results to try
|
// Check if season was not found or episode extraction failed, and we have multiple results to try
|
||||||
if (downloadInfo.links.length === 0 && matchingResults.length > 1 && scoredResults &&
|
if (downloadInfo.links.length === 0 && matchingResults.length > 1 && scoredResults &&
|
||||||
(downloadInfo.seasonNotFound || (mediaType === 'tv' && downloadInfo.title))) {
|
(downloadInfo.seasonNotFound || (mediaType === 'tv' && downloadInfo.title))) {
|
||||||
console.log(`[UHDMovies] Season ${season} not found or episode extraction failed on best match. Trying next best match...`);
|
console.log(`[UHDMovies] Season ${season} not found or episode extraction failed on best match. Trying next best match...`);
|
||||||
|
|
||||||
// Try the next best match
|
// Try the next best match
|
||||||
@@ -1380,11 +1380,8 @@ async function getUHDMoviesStreams(tmdbId, mediaType = 'movie', season = null, e
|
|||||||
title: `${cleanFileName}\n${sizeInfo}`,
|
title: `${cleanFileName}\n${sizeInfo}`,
|
||||||
url: finalUrl,
|
url: finalUrl,
|
||||||
quality: linkInfo.quality,
|
quality: linkInfo.quality,
|
||||||
size: sizeInfo,
|
provider: 'uhdmovies',
|
||||||
fileName: fileName,
|
headers: {}
|
||||||
fullTitle: rawQuality,
|
|
||||||
codecs: codecs,
|
|
||||||
behaviorHints: { bingeGroup: `uhdmovies-${linkInfo.quality}` }
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -195,7 +195,7 @@ function getVixsrcStreams(tmdbId, mediaType = 'movie', seasonNum = null, episode
|
|||||||
title: "Auto Quality Stream",
|
title: "Auto Quality Stream",
|
||||||
url: masterPlaylistUrl,
|
url: masterPlaylistUrl,
|
||||||
quality: '1080p',
|
quality: '1080p',
|
||||||
type: 'direct',
|
provider: 'Vidsrc',
|
||||||
headers: {
|
headers: {
|
||||||
'Referer': BASE_URL,
|
'Referer': BASE_URL,
|
||||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
|
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
|
||||||
|
|||||||
+3
-3
@@ -132,12 +132,12 @@ async function getXprimeStreams(title, year, type, seasonNum, episodeNum) {
|
|||||||
Object.entries(item.streams).forEach(([quality, url]) => {
|
Object.entries(item.streams).forEach(([quality, url]) => {
|
||||||
if (!url || typeof url !== 'string') return;
|
if (!url || typeof url !== 'string') return;
|
||||||
streams.push({
|
streams.push({
|
||||||
|
name: title,
|
||||||
|
title: `${title} - ${type === 'tv' ? `S${String(seasonNum).padStart(2,'0')}E${String(episodeNum).padStart(2,'0')} ` : ''}${quality}`,
|
||||||
url,
|
url,
|
||||||
quality: quality || 'Unknown',
|
quality: quality || 'Unknown',
|
||||||
title: `${title} - ${type === 'tv' ? `S${String(seasonNum).padStart(2,'0')}E${String(episodeNum).padStart(2,'0')} ` : ''}${quality}`,
|
|
||||||
provider: 'Xprime.tv',
|
provider: 'Xprime.tv',
|
||||||
codecs: [],
|
headers: {},
|
||||||
size: 'Unknown size'
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
+19
-9
@@ -596,15 +596,25 @@ async function renderProviderMatrix(merged){
|
|||||||
if(!merged){ try { const data = await fetchConfig(); merged = data.merged||{}; } catch { return; } }
|
if(!merged){ try { const data = await fetchConfig(); merged = data.merged||{}; } catch { return; } }
|
||||||
const container = document.getElementById('providerMatrix'); if(!container) return;
|
const container = document.getElementById('providerMatrix'); if(!container) return;
|
||||||
const defaults = (merged.defaultProviders||[]).map(s=>s.toLowerCase());
|
const defaults = (merged.defaultProviders||[]).map(s=>s.toLowerCase());
|
||||||
const providers = [
|
|
||||||
['showbox','enableShowboxProvider'],
|
// Fetch available providers from API
|
||||||
['4khdhub','enable4khdhubProvider'],
|
let availableProviders = [];
|
||||||
['moviesmod','enableMoviesmodProvider'],
|
try {
|
||||||
['mp4hydra','enableMp4hydraProvider'],
|
const providersResponse = await fetch('/api/providers');
|
||||||
['vidzee','enableVidzeeProvider'],
|
if (providersResponse.ok) {
|
||||||
['vixsrc','enableVixsrcProvider'],
|
const providersData = await providersResponse.json();
|
||||||
['xprime','enableXprimeProvider']
|
availableProviders = providersData.providers || [];
|
||||||
];
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to fetch providers list:', e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create provider list with enable flags
|
||||||
|
const providers = availableProviders.map(p => {
|
||||||
|
const flag = `enable${p.name.charAt(0).toUpperCase() + p.name.slice(1)}Provider`;
|
||||||
|
return [p.name, flag];
|
||||||
|
});
|
||||||
|
|
||||||
container.innerHTML = providers.map(([name,flag])=>{
|
container.innerHTML = providers.map(([name,flag])=>{
|
||||||
const enabled = merged[flag] !== false;
|
const enabled = merged[flag] !== false;
|
||||||
const isDefault = defaults.includes(name);
|
const isDefault = defaults.includes(name);
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
1758105049325
|
1758225404146
|
||||||
+44
-28
@@ -52,6 +52,19 @@ function writeOverrideFile(obj) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getProviderNames() {
|
||||||
|
try {
|
||||||
|
const providersDir = path.join(process.cwd(), 'providers');
|
||||||
|
if (!fs.existsSync(providersDir)) return [];
|
||||||
|
return fs.readdirSync(providersDir)
|
||||||
|
.filter(file => file.endsWith('.js') && file !== 'registry.js' && file !== 'vidsrcextractor.js')
|
||||||
|
.map(file => path.parse(file).name.toLowerCase());
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[config] Failed to scan providers directory:', e.message);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeConfig(base) {
|
function normalizeConfig(base) {
|
||||||
const cfg = { ...base };
|
const cfg = { ...base };
|
||||||
// Marker: if user explicitly provided an empty array for tmdbApiKeys in override file we should not resurrect legacy key
|
// Marker: if user explicitly provided an empty array for tmdbApiKeys in override file we should not resurrect legacy key
|
||||||
@@ -80,20 +93,21 @@ function normalizeConfig(base) {
|
|||||||
cfg.tmdbApiKeys = Array.from(new Set(cfg.tmdbApiKeys.map(k=>String(k).trim()).filter(Boolean)));
|
cfg.tmdbApiKeys = Array.from(new Set(cfg.tmdbApiKeys.map(k=>String(k).trim()).filter(Boolean)));
|
||||||
// Do not expose legacy single-key field in merged config output
|
// Do not expose legacy single-key field in merged config output
|
||||||
delete cfg.tmdbApiKey;
|
delete cfg.tmdbApiKey;
|
||||||
// Booleans normalization
|
|
||||||
const boolKeys = [
|
// Dynamic provider enable flags
|
||||||
'enableShowboxProvider','enableXprimeProvider','enable4khdhubProvider','enableMoviesmodProvider','enableMp4hydraProvider','enableVidzeeProvider','enableVixsrcProvider',
|
const providerNames = getProviderNames();
|
||||||
'disableCache','enablePStreamApi','disableUrlValidation','disable4khdhubUrlValidation'
|
const boolKeys = providerNames.map(name => `enable${name.charAt(0).toUpperCase() + name.slice(1)}Provider`);
|
||||||
];
|
boolKeys.push('disableCache', 'enablePStreamApi', 'disableUrlValidation', 'disable4khdhubUrlValidation');
|
||||||
|
|
||||||
boolKeys.forEach(k=>{ if (cfg[k] === 'true') cfg[k] = true; else if (cfg[k] === 'false') cfg[k] = false; });
|
boolKeys.forEach(k=>{ if (cfg[k] === 'true') cfg[k] = true; else if (cfg[k] === 'false') cfg[k] = false; });
|
||||||
// Default values if undefined
|
|
||||||
if (cfg.enableShowboxProvider === undefined) cfg.enableShowboxProvider = true;
|
// Set defaults for provider enable flags
|
||||||
if (cfg.enableXprimeProvider === undefined) cfg.enableXprimeProvider = true;
|
providerNames.forEach(name => {
|
||||||
if (cfg.enable4khdhubProvider === undefined) cfg.enable4khdhubProvider = true;
|
const flag = `enable${name.charAt(0).toUpperCase() + name.slice(1)}Provider`;
|
||||||
if (cfg.enableMoviesmodProvider === undefined) cfg.enableMoviesmodProvider = true;
|
if (cfg[flag] === undefined) cfg[flag] = true; // Default to enabled
|
||||||
if (cfg.enableMp4hydraProvider === undefined) cfg.enableMp4hydraProvider = true;
|
});
|
||||||
if (cfg.enableVidzeeProvider === undefined) cfg.enableVidzeeProvider = true;
|
|
||||||
if (cfg.enableVixsrcProvider === undefined) cfg.enableVixsrcProvider = true;
|
// Default values for other flags
|
||||||
if (cfg.disableCache === undefined) cfg.disableCache = false;
|
if (cfg.disableCache === undefined) cfg.disableCache = false;
|
||||||
if (cfg.enablePStreamApi === undefined) cfg.enablePStreamApi = true;
|
if (cfg.enablePStreamApi === undefined) cfg.enablePStreamApi = true;
|
||||||
// Proxy features removed; always use direct connections
|
// Proxy features removed; always use direct connections
|
||||||
@@ -114,14 +128,15 @@ function applyConfigToEnv(cfg){
|
|||||||
if (cfg.febboxCookies && cfg.febboxCookies.length) process.env.FEBBOX_COOKIES = cfg.febboxCookies.join(',');
|
if (cfg.febboxCookies && cfg.febboxCookies.length) process.env.FEBBOX_COOKIES = cfg.febboxCookies.join(',');
|
||||||
else delete process.env.FEBBOX_COOKIES;
|
else delete process.env.FEBBOX_COOKIES;
|
||||||
if (cfg.defaultRegion) process.env.DEFAULT_REGION = cfg.defaultRegion; else delete process.env.DEFAULT_REGION;
|
if (cfg.defaultRegion) process.env.DEFAULT_REGION = cfg.defaultRegion; else delete process.env.DEFAULT_REGION;
|
||||||
// Provider enable flags
|
|
||||||
process.env.ENABLE_SHOWBOX_PROVIDER = cfg.enableShowboxProvider ? 'true':'false';
|
// Dynamic provider enable flags
|
||||||
process.env.ENABLE_XPRIME_PROVIDER = cfg.enableXprimeProvider ? 'true':'false';
|
const providerNames = getProviderNames();
|
||||||
process.env.ENABLE_4KHDHUB_PROVIDER = cfg.enable4khdhubProvider ? 'true':'false';
|
providerNames.forEach(name => {
|
||||||
process.env.ENABLE_MOVIESMOD_PROVIDER = cfg.enableMoviesmodProvider ? 'true':'false';
|
const flag = `enable${name.charAt(0).toUpperCase() + name.slice(1)}Provider`;
|
||||||
process.env.ENABLE_MP4HYDRA_PROVIDER = cfg.enableMp4hydraProvider ? 'true':'false';
|
const envName = `ENABLE_${name.toUpperCase()}_PROVIDER`;
|
||||||
process.env.ENABLE_VIDZEE_PROVIDER = cfg.enableVidzeeProvider ? 'true':'false';
|
process.env[envName] = cfg[flag] ? 'true' : 'false';
|
||||||
process.env.ENABLE_VIXSRC_PROVIDER = cfg.enableVixsrcProvider ? 'true':'false';
|
});
|
||||||
|
|
||||||
// Caching / validation / PStream
|
// Caching / validation / PStream
|
||||||
process.env.DISABLE_CACHE = cfg.disableCache ? 'true':'false';
|
process.env.DISABLE_CACHE = cfg.disableCache ? 'true':'false';
|
||||||
process.env.ENABLE_PSTREAM_API = cfg.enablePStreamApi ? 'true':'false';
|
process.env.ENABLE_PSTREAM_API = cfg.enablePStreamApi ? 'true':'false';
|
||||||
@@ -152,13 +167,6 @@ function loadConfig() {
|
|||||||
tmdbApiKeys: parseJsonMaybe(process.env.TMDB_API_KEYS) || null,
|
tmdbApiKeys: parseJsonMaybe(process.env.TMDB_API_KEYS) || null,
|
||||||
febboxCookies: parseCookies(process.env.FEBBOX_COOKIES),
|
febboxCookies: parseCookies(process.env.FEBBOX_COOKIES),
|
||||||
// Extended advanced config (may not exist in env)
|
// Extended advanced config (may not exist in env)
|
||||||
enableShowboxProvider: process.env.ENABLE_SHOWBOX_PROVIDER,
|
|
||||||
enableXprimeProvider: process.env.ENABLE_XPRIME_PROVIDER,
|
|
||||||
enable4khdhubProvider: process.env.ENABLE_4KHDHUB_PROVIDER,
|
|
||||||
enableMoviesmodProvider: process.env.ENABLE_MOVIESMOD_PROVIDER,
|
|
||||||
enableMp4hydraProvider: process.env.ENABLE_MP4HYDRA_PROVIDER,
|
|
||||||
enableVidzeeProvider: process.env.ENABLE_VIDZEE_PROVIDER,
|
|
||||||
enableVixsrcProvider: process.env.ENABLE_VIXSRC_PROVIDER,
|
|
||||||
disableCache: process.env.DISABLE_CACHE,
|
disableCache: process.env.DISABLE_CACHE,
|
||||||
enablePStreamApi: process.env.ENABLE_PSTREAM_API,
|
enablePStreamApi: process.env.ENABLE_PSTREAM_API,
|
||||||
showboxCacheDir: process.env.SHOWBOX_CACHE_DIR || null,
|
showboxCacheDir: process.env.SHOWBOX_CACHE_DIR || null,
|
||||||
@@ -166,6 +174,14 @@ function loadConfig() {
|
|||||||
disableUrlValidation: process.env.DISABLE_URL_VALIDATION,
|
disableUrlValidation: process.env.DISABLE_URL_VALIDATION,
|
||||||
disable4khdhubUrlValidation: process.env.DISABLE_4KHDHUB_URL_VALIDATION
|
disable4khdhubUrlValidation: process.env.DISABLE_4KHDHUB_URL_VALIDATION
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Dynamic provider enable flags from env
|
||||||
|
const providerNames = getProviderNames();
|
||||||
|
providerNames.forEach(name => {
|
||||||
|
const envName = `ENABLE_${name.toUpperCase()}_PROVIDER`;
|
||||||
|
envCfg[`enable${name.charAt(0).toUpperCase() + name.slice(1)}Provider`] = process.env[envName];
|
||||||
|
});
|
||||||
|
|
||||||
const override = readOverrideFile();
|
const override = readOverrideFile();
|
||||||
const merged = { ...envCfg, ...override };
|
const merged = { ...envCfg, ...override };
|
||||||
const normalized = normalizeConfig(merged);
|
const normalized = normalizeConfig(merged);
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
class JsUnpacker {
|
||||||
|
static Unbase = class {
|
||||||
|
constructor(radix) {
|
||||||
|
this.ALPHABET_62 =
|
||||||
|
'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||||
|
this.ALPHABET_95 =
|
||||||
|
' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~';
|
||||||
|
this.alphabet = null;
|
||||||
|
this.dictionary = {};
|
||||||
|
this.radix = radix;
|
||||||
|
|
||||||
|
if (radix > 36) {
|
||||||
|
if (radix < 62) {
|
||||||
|
this.alphabet = this.ALPHABET_62.substring(0, radix);
|
||||||
|
} else if (radix > 62 && radix < 95) {
|
||||||
|
this.alphabet = this.ALPHABET_95.substring(0, radix);
|
||||||
|
} else if (radix === 62) {
|
||||||
|
this.alphabet = this.ALPHABET_62;
|
||||||
|
} else if (radix === 95) {
|
||||||
|
this.alphabet = this.ALPHABET_95;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < this.alphabet.length; i++) {
|
||||||
|
this.dictionary[this.alphabet[i]] = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unbase(str) {
|
||||||
|
let ret = 0;
|
||||||
|
|
||||||
|
if (this.alphabet === null) {
|
||||||
|
ret = parseInt(str, this.radix);
|
||||||
|
} else {
|
||||||
|
const tmp = str.split('').reverse().join('');
|
||||||
|
for (let i = 0; i < tmp.length; i++) {
|
||||||
|
ret += Math.pow(this.radix, i) * this.dictionary[tmp[i]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(packedJS) {
|
||||||
|
this.packedJS = packedJS;
|
||||||
|
}
|
||||||
|
|
||||||
|
detect() {
|
||||||
|
const js = this.packedJS.replace(/\s/g, '');
|
||||||
|
const regex = /eval\(function\(p,a,c,k,e,(?:r|d)/;
|
||||||
|
return regex.test(js);
|
||||||
|
}
|
||||||
|
|
||||||
|
unpack() {
|
||||||
|
let js = this.packedJS;
|
||||||
|
try {
|
||||||
|
const regex =
|
||||||
|
/}\s*\('(.*)',\s*(.*?),\s*(\d+),\s*'(.*?)'\.split\('\|'\)/s;
|
||||||
|
const match = js.match(regex);
|
||||||
|
if (match && match.length === 5) {
|
||||||
|
let payload = match[1].replace(/\\'/g, "'");
|
||||||
|
const radixStr = match[2];
|
||||||
|
const countStr = match[3];
|
||||||
|
const symtab = match[4].split('|');
|
||||||
|
|
||||||
|
let radix = 36;
|
||||||
|
let count = 0;
|
||||||
|
try {
|
||||||
|
radix = parseInt(radixStr);
|
||||||
|
} catch (e) {}
|
||||||
|
try {
|
||||||
|
count = parseInt(countStr);
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
if (symtab.length !== count) {
|
||||||
|
throw new Error('Unknown p.a.c.k.e.r. encoding');
|
||||||
|
}
|
||||||
|
|
||||||
|
const unbase = new this.constructor.Unbase(radix);
|
||||||
|
const wordRegex = /\b\w+\b/g;
|
||||||
|
let decoded = payload;
|
||||||
|
let replaceOffset = 0;
|
||||||
|
let wordMatch;
|
||||||
|
|
||||||
|
while ((wordMatch = wordRegex.exec(payload)) !== null) {
|
||||||
|
const word = wordMatch[0];
|
||||||
|
const x = unbase.unbase(word);
|
||||||
|
let value = null;
|
||||||
|
if (x < symtab.length) {
|
||||||
|
value = symtab[x];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value && value.length > 0) {
|
||||||
|
const start = wordMatch.index + replaceOffset;
|
||||||
|
const end = start + word.length;
|
||||||
|
decoded =
|
||||||
|
decoded.substring(0, start) +
|
||||||
|
value +
|
||||||
|
decoded.substring(end);
|
||||||
|
replaceOffset += value.length - word.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommonJS exports
|
||||||
|
module.exports = JsUnpacker;
|
||||||
Reference in New Issue
Block a user