mirror of
https://github.com/Inside4ndroid/TMDB-Embed-API.git
synced 2026-10-22 17:45:11 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ddd75bbddb |
@@ -1,3 +1 @@
|
|||||||
/node_modules
|
/node_modules
|
||||||
restart.trigger
|
|
||||||
.vscode/
|
|
||||||
@@ -2,78 +2,6 @@
|
|||||||
|
|
||||||
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.6] - 2025-09-20
|
|
||||||
|
|
||||||
### Removed
|
|
||||||
- MoviesClub provider (multi-server scraping complexities & Turnstile challenge; deprecated permanently)
|
|
||||||
- Xprime provider (upstream Xprime.tv offline due to security changes)
|
|
||||||
- 4khdhub provider: all `.zip` archive links are now omitted entirely (previous releases experimented with stripping the extension which produced non‑playable pseudo‑MKV links)
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Structured multi-server debug instrumentation (session summaries, per-fetch metrics, pattern counters)
|
|
||||||
- Turnstile challenge detection & bypass attempt scaffold (synthetic `/rcp_verify` token posting)
|
|
||||||
- Optional internal stream proxy (`enableProxy` flag): mounts `/m3u8-proxy`, `/ts-proxy`, `/sub-proxy` with playlist + segment + subtitle handling and segment prefetch cache.
|
|
||||||
- Proxy range management features:
|
|
||||||
- `clampOpen` (default on) – caps ambiguous open‑ended `bytes=0-` requests to a bounded initial window (`openChunkKB`, default 4096 KB)
|
|
||||||
- `progressiveOpen` (default on) – incremental expansion of the head range on successive `bytes=0-` requests instead of a single huge span
|
|
||||||
- `initChunkKB` (default 512 KB) – size of the synthetic initial 206 response when neither clamp/progressive produce a range and `noSynth` is not set
|
|
||||||
- `tailPrefetch` (default on) + `tailPrefetchKB` (default 256 KB) – asynchronous fetch & in‑memory cache of the file tail to satisfy rapid VLC tail probes
|
|
||||||
- `force200` (opt‑in) – normalizes upstream 206 responses to 200 for diagnostics
|
|
||||||
- `noSynth` (opt‑in) – disables synthetic initial partial response generation
|
|
||||||
- Tail prefetch TTL cleanup task (30 min window) and in‑memory maps for: segment cache, open range clamp, progressive growth, and tail buffers
|
|
||||||
- Host routing overrides: `pixeldrain.*` & `video-downloads.googleusercontent.com` are forced through `/ts-proxy` (extensionless or ambiguous content)
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Centralized multi-server request headers with realistic `sec-ch-ua*` & `Sec-Fetch-*` values
|
|
||||||
- Added retry, rotating User-Agent, and cookie jar logic to multi-server fetch pipeline
|
|
||||||
- Showbox provider priority map updated after Xprime removal
|
|
||||||
- README/Docs trimmed to reflect current active providers only
|
|
||||||
- When `enableProxy` is active, stream response objects have their original `headers` field removed (proxy handles all required headers internally).
|
|
||||||
- 4khdhub provider now filters out archive endpoints instead of attempting extension normalization (prevents feeding ZIP files to players)
|
|
||||||
- Open‑ended range handling improved to reduce VLC negotiation loops by throttling first‑pass read size and growing progressively
|
|
||||||
- Synthetic initial partial response is automatically suppressed when `progressiveOpen` is active (real range growth preferred)
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Ensured multi-server fallback attempts (direct rcp player/m3u8 extraction) operate with improved diagnostics
|
|
||||||
- Eliminated repeated VLC tail probe stalls caused by archive masquerading as video content (root cause was filtered by dropping `.zip` URLs)
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
- Updated README version badge to 1.0.6 and provider list (removed MoviesClub & Xprime, clarified active providers list)
|
|
||||||
- Added proxy tuning parameter reference (clamp/progressive/tail prefetch, synthetic partial, force200) and host override notes
|
|
||||||
- Expanded explanation that per‑stream headers are stripped when proxying is enabled
|
|
||||||
|
|
||||||
|
|
||||||
## [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).
|
|
||||||
|
|
||||||
### Notes
|
|
||||||
- `r2.dev` links are always removed from final output; no env flag required.
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
|
|
||||||
## [1.0.4] - 2025-09-18
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
```json
|
|
||||||
{ "title": "…", "url": "…", "quality": "…", "provider": "…", "headers": { } }
|
|
||||||
```
|
|
||||||
|
|
||||||
## [1.0.3] - 2025-09-17
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Server now binds to `0.0.0.0` by default so Docker port publishing works correctly from the host. Added `BIND_HOST=0.0.0.0` in Dockerfile and compose.
|
|
||||||
|
|
||||||
### Notes
|
|
||||||
- If you were seeing `ERR_CONNECTION_REFUSED` on `http://localhost:8787`, pull the latest image or rebuild, then re-run with `-p 8787:8787`.
|
|
||||||
|
|
||||||
## [1.0.2] - 2025-09-17
|
## [1.0.2] - 2025-09-17
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
@@ -109,10 +37,6 @@ All notable changes to this project will be documented in this file.
|
|||||||
## [1.0.0] - 2025-09-16
|
## [1.0.0] - 2025-09-16
|
||||||
- Initial stable release.
|
- Initial stable release.
|
||||||
|
|
||||||
[1.0.6]: https://github.com/Inside4ndroid/TMDB-Embed-API/compare/v1.0.5...v1.0.6
|
|
||||||
[1.0.5]: https://github.com/Inside4ndroid/TMDB-Embed-API/compare/v1.0.4...v1.0.5
|
|
||||||
[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
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ ARG VERSION=dev
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ENV NODE_ENV=production \
|
ENV NODE_ENV=production \
|
||||||
API_PORT=8787 \
|
API_PORT=8787 \
|
||||||
BIND_HOST=0.0.0.0 \
|
|
||||||
APP_VERSION=${VERSION}
|
APP_VERSION=${VERSION}
|
||||||
|
|
||||||
# Create non-root user
|
# Create non-root user
|
||||||
|
|||||||
@@ -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.6-informational?style=flat" />
|
<img src="https://img.shields.io/badge/Version-1.0.2-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,32 +34,14 @@
|
|||||||
|
|
||||||
## ✨ 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, UHDMovies) with per‑provider enable toggles + default selection.
|
- **Provider Aggregation** – Pluggable providers (Showbox, 4khdhub, MoviesMod, MP4Hydra, VidZee, Vixsrc, Xprime) 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 drop-in provider plugin system.
|
- **Extensible** – Simple provider registry pattern.
|
||||||
- **Optional Stream Proxy Layer** – When enabled, rewrites returned stream URLs so HLS playlists, TS segments, and subtitles are served through internal endpoints (`/m3u8-proxy`, `/ts-proxy`, `/sub-proxy`) allowing uniform headers, origin shielding, and optional segment caching.
|
|
||||||
When active the API omits per-stream `headers` objects from responses (they're no longer needed by clients) to avoid leaking upstream header requirements.
|
|
||||||
|
|
||||||
Proxy Tuning Parameters (query flags accepted by `/ts-proxy` – defaults shown):
|
|
||||||
- `clampOpen` (on) – If a client sends an ambiguous `Range: bytes=0-`, constrain it to an initial window of `openChunkKB` (default 4096 KB) to avoid huge first reads.
|
|
||||||
- `openChunkKB=4096` – Size (KB) used for both clamp window and each progressive expansion increment.
|
|
||||||
- `progressiveOpen` (on) – Grow successive ambiguous head requests (`bytes=0-`) incrementally instead of one large span. Maintains a per‑URL expansion map.
|
|
||||||
- `initChunkKB=512` – Size used for a synthetic initial partial (206) when no client range is provided and progressive growth is disabled. Capped 64–2048 KB.
|
|
||||||
- `noSynth=1` – Disable synthetic initial partial generation (forces pass‑through behavior).
|
|
||||||
- `force200=1` – Normalize upstream 206 responses to 200 (diagnostics / edge player testing).
|
|
||||||
- `tailPrefetch` (on) – Enable asynchronous tail fetch of the file’s last bytes to satisfy rapid player tail probes.
|
|
||||||
- `tailPrefetchKB=256` – Tail window size (64–2048 KB). Cached in memory with TTL cleanup.
|
|
||||||
Behavior Notes:
|
|
||||||
- Synthetic partials auto‑disable when `progressiveOpen` is active (real progressive ranges preferred).
|
|
||||||
- Player tail probes (e.g., VLC metadata scans) are accelerated by the cached tail window.
|
|
||||||
- Forced 200 mode strips `Content-Range` to emulate full responses for troubleshooting.
|
|
||||||
- Host Overrides: `pixeldrain.*` and `video-downloads.googleusercontent.com` URLs are routed through `/ts-proxy` regardless of extension to ensure correct range + MIME handling.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -97,11 +79,6 @@ 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
|
||||||
@@ -225,61 +202,22 @@ Session is revalidated on visibility and back/forward navigation to prevent stal
|
|||||||
---
|
---
|
||||||
|
|
||||||
## 🔌 Providers
|
## 🔌 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`.
|
Current built‑in providers:
|
||||||
|
- `showbox`
|
||||||
|
- `4khdhub`
|
||||||
|
- `moviesmod`
|
||||||
|
- `mp4hydra`
|
||||||
|
- `vidzee`
|
||||||
|
- `vixsrc`
|
||||||
|
- `xprime`
|
||||||
|
|
||||||
### Current Built-in Providers
|
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.
|
||||||
- `showbox` - Showbox/PStream integration
|
|
||||||
- `4khdhub` - 4KHDHub streams
|
|
||||||
- `moviesmod` - MoviesMod streams
|
|
||||||
- `mp4hydra` - MP4Hydra streams
|
|
||||||
- `vidzee` - VidZee streams
|
|
||||||
- `vixsrc` - Vixsrc streams
|
|
||||||
- `uhdmovies` - UHD Movies 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',
|
|
||||||
'uhdmovies.js': 'getUHDMoviesStreams',
|
|
||||||
'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!
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -300,34 +238,23 @@ Aggregate endpoint auto-resolves IMDb when needed and merges provider output bef
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🧪 Stream Object Schema (Unified)
|
## 🧪 Stream Object Shape (Typical)
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"title": "Fight Club - 1080p [MP4Hydra #2]",
|
"title": "Example Title",
|
||||||
"url": "https://stream.url/video.mp4",
|
"url": "https://...",
|
||||||
"quality": "1080p",
|
"quality": "1080p",
|
||||||
"provider": "mp4hydra",
|
"provider": "showbox",
|
||||||
"headers": { "User-Agent": "Mozilla/5.0" }
|
"size": null,
|
||||||
|
"languages": ["en"],
|
||||||
|
"subtitles": [],
|
||||||
|
"codecs": ["H264"],
|
||||||
|
"headers": { "Referer": "..." }
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Filtering passes through `applyFilters` to enforce min quality + codec exclusions.
|
Filtering passes through `applyFilters` to enforce min quality + codec exclusions.
|
||||||
|
|
||||||
> Note: When the `enableProxy` flag is turned on, provider-specific request headers are stripped from each stream object before responding. Clients should use the proxied URL directly without adding custom Referer/Origin headers.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚙️ Configuration Flags (Advanced Panel)
|
|
||||||
| Flag | Default | Purpose |
|
|
||||||
|------|---------|---------|
|
|
||||||
| disableCache | false | Disables internal caches (Showbox + proxy segment cache) |
|
|
||||||
| enablePStreamApi | true | Enables Showbox PStream-specific handling |
|
|
||||||
| disableUrlValidation | false | Skip general URL pattern validation checks |
|
|
||||||
| disable4khdhubUrlValidation | false | Skip 4khdhub-specific URL validation |
|
|
||||||
| enableProxy | false | Mounts proxy routes and rewrites stream URLs through them |
|
|
||||||
|
|
||||||
Toggle `enableProxy` to activate the internal proxy. This adds lightweight playlist/segment/subtitle rewriting without modifying provider code. Disable it to return direct upstream URLs.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🧩 Quality & Codec Filtering
|
## 🧩 Quality & Codec Filtering
|
||||||
|
|||||||
+2
-6
@@ -374,12 +374,8 @@ app.get('/api/streams/:provider/:type/:tmdbId', async (req,res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const PORT = config.port;
|
const PORT = config.port;
|
||||||
const HOST = process.env.BIND_HOST || '0.0.0.0';
|
const server = app.listen(PORT, () => {
|
||||||
const server = app.listen(PORT, HOST, () => {
|
console.log(`TMDB Embed REST API listening on http://localhost:${PORT}`);
|
||||||
console.log(`TMDB Embed REST API listening on http://${HOST}:${PORT}`);
|
|
||||||
if (HOST !== 'localhost') {
|
|
||||||
console.log(`Local access (if running on your machine): http://localhost:${PORT}`);
|
|
||||||
}
|
|
||||||
console.log('Endpoints:');
|
console.log('Endpoints:');
|
||||||
console.log(' GET /api/health');
|
console.log(' GET /api/health');
|
||||||
console.log(' GET /api/metrics');
|
console.log(' GET /api/metrics');
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ services:
|
|||||||
# Alternative: supply JSON array
|
# Alternative: supply JSON array
|
||||||
# - TMDB_API_KEYS=["key1","key2"]
|
# - TMDB_API_KEYS=["key1","key2"]
|
||||||
- API_PORT=8787
|
- API_PORT=8787
|
||||||
- BIND_HOST=0.0.0.0
|
|
||||||
# FebBox cookies (comma separated) if you want Showbox immediately functional
|
# FebBox cookies (comma separated) if you want Showbox immediately functional
|
||||||
- FEBBOX_COOKIES=${FEBBOX_COOKIES:-}
|
- FEBBOX_COOKIES=${FEBBOX_COOKIES:-}
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "tmdb-embed-api",
|
"name": "tmdb-embed-api",
|
||||||
"version": "1.0.5",
|
"version": "1.0.2",
|
||||||
"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": {
|
||||||
|
|||||||
+31
-128
@@ -9,9 +9,6 @@ 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'}.`);
|
||||||
@@ -64,7 +61,8 @@ function validateUrl(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));
|
||||||
@@ -76,21 +74,11 @@ 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
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -409,10 +397,8 @@ 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 = {
|
const romans = { 'I': '1', 'II': '2', 'III': '3', 'IV': '4', 'V': '5',
|
||||||
'I': '1', 'II': '2', 'III': '3', 'IV': '4', 'V': '5',
|
'VI': '6', 'VII': '7', 'VIII': '8', 'IX': '9', 'X': '10' };
|
||||||
'VI': '6', 'VII': '7', 'VIII': '8', 'IX': '9', 'X': '10'
|
|
||||||
};
|
|
||||||
return romans[match] || match;
|
return romans[match] || match;
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
@@ -763,22 +749,12 @@ 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(() => {
|
||||||
@@ -787,22 +763,12 @@ 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')) {
|
||||||
@@ -820,9 +786,7 @@ 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(() => {
|
||||||
@@ -836,9 +800,7 @@ 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')) {
|
||||||
@@ -867,9 +829,7 @@ function extractHubCloudLinks(url, referer) {
|
|||||||
name: `4KHDHub - BuzzServer${qualityLabel}`,
|
name: `4KHDHub - BuzzServer${qualityLabel}`,
|
||||||
title: finalTitle,
|
title: finalTitle,
|
||||||
url: finalUrl,
|
url: finalUrl,
|
||||||
quality: quality,
|
quality: quality
|
||||||
provider: '4khdhub',
|
|
||||||
headers: {}
|
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@@ -883,9 +843,7 @@ function extractHubCloudLinks(url, referer) {
|
|||||||
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 {
|
||||||
@@ -922,9 +880,7 @@ 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(() => {
|
||||||
@@ -938,9 +894,7 @@ 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')) {
|
||||||
@@ -958,9 +912,7 @@ 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(() => {
|
||||||
@@ -974,9 +926,7 @@ 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')) {
|
||||||
@@ -1016,9 +966,7 @@ function extractHubCloudLinks(url, referer) {
|
|||||||
name: `4KHDHub - 10Gbps Server${qualityLabel}`,
|
name: `4KHDHub - 10Gbps Server${qualityLabel}`,
|
||||||
title: finalTitle,
|
title: finalTitle,
|
||||||
url: decodedUrl,
|
url: decodedUrl,
|
||||||
quality: quality,
|
quality: quality
|
||||||
provider: '4khdhub',
|
|
||||||
headers: {}
|
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@@ -1032,9 +980,7 @@ function extractHubCloudLinks(url, referer) {
|
|||||||
name: `4KHDHub - 10Gbps Server${qualityLabel}`,
|
name: `4KHDHub - 10Gbps Server${qualityLabel}`,
|
||||||
title: finalTitle,
|
title: finalTitle,
|
||||||
url: decodedUrl,
|
url: decodedUrl,
|
||||||
quality: quality,
|
quality: quality
|
||||||
provider: '4khdhub',
|
|
||||||
headers: {}
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1070,9 +1016,7 @@ 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(() => {
|
||||||
@@ -1086,9 +1030,7 @@ 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: {}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1334,22 +1276,12 @@ 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();
|
||||||
// Keep .zip links but strip the trailing .zip extension (attempt direct file URL)
|
// Filter out .zip files
|
||||||
const transformedResults = flatResults
|
const filteredResults = flatResults.filter(link => {
|
||||||
.filter(link => link && link.url)
|
return link && link.url && !link.url.toLowerCase().endsWith('.zip');
|
||||||
.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 transformedResults;
|
return filteredResults;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1456,9 +1388,7 @@ 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(() => {
|
||||||
@@ -1472,9 +1402,7 @@ 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: {}
|
|
||||||
}];
|
}];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1535,9 +1463,7 @@ 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(() => {
|
||||||
@@ -1551,9 +1477,7 @@ 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 {
|
||||||
@@ -1816,36 +1740,15 @@ async function get4KHDHubStreams(tmdbId, type, season = null, episode = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Normalize to generic stream object format
|
// Normalize to generic stream object format
|
||||||
let streams = validatedLinks.map(link => ({
|
const 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`,
|
||||||
provider: "4khdhub",
|
behaviorHints: {
|
||||||
headers: {}
|
bingeGroup: '4khdhub-streams'
|
||||||
}));
|
|
||||||
|
|
||||||
// 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;
|
||||||
|
|||||||
+29
-11
@@ -126,11 +126,14 @@ 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",
|
||||||
|
behaviorHints: {
|
||||||
|
notWebReady: true,
|
||||||
headers: {
|
headers: {
|
||||||
'Referer': 'https://mp4hydra.org/'
|
'Referer': 'https://mp4hydra.org/'
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add subtitle tracks if available
|
// Add subtitle tracks if available
|
||||||
@@ -169,11 +172,14 @@ 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",
|
||||||
|
behaviorHints: {
|
||||||
|
notWebReady: true,
|
||||||
headers: {
|
headers: {
|
||||||
'Referer': 'https://mp4hydra.org/'
|
'Referer': 'https://mp4hydra.org/'
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add subtitle tracks if available
|
// Add subtitle tracks if available
|
||||||
@@ -291,11 +297,14 @@ 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",
|
||||||
|
behaviorHints: {
|
||||||
|
notWebReady: true,
|
||||||
headers: {
|
headers: {
|
||||||
'Referer': 'https://mp4hydra.org/'
|
'Referer': 'https://mp4hydra.org/'
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add subtitle tracks if available
|
// Add subtitle tracks if available
|
||||||
@@ -331,10 +340,17 @@ 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
|
||||||
@@ -419,15 +435,17 @@ 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",
|
||||||
|
behaviorHints: {
|
||||||
|
notWebReady: true,
|
||||||
headers: {
|
headers: {
|
||||||
'Referer': 'https://mp4hydra.org/'
|
'Referer': 'https://mp4hydra.org/'
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add subtitle tracks if available
|
// Add subtitle tracks if available
|
||||||
|
|||||||
+20
-24
@@ -1980,12 +1980,10 @@ 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),
|
||||||
provider: 'Febbox Direct',
|
codecs: extractCodecDetails(key5FromDirectSource || streamTitle)
|
||||||
headers: {}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// If direct sources are found, original code used 'continue',
|
// If direct sources are found, original code used 'continue',
|
||||||
@@ -2009,12 +2007,10 @@ 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),
|
||||||
provider: 'Febbox FID',
|
codecs: extractCodecDetails(source.detailedFilename || streamTitle)
|
||||||
headers: {}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2206,12 +2202,10 @@ 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),
|
||||||
provider: 'Febbox Direct',
|
codecs: extractCodecDetails(source.detailedFilename || streamTitle)
|
||||||
headers: {}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
console.timeEnd(processTimerLabel);
|
console.timeEnd(processTimerLabel);
|
||||||
@@ -2228,12 +2222,10 @@ 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),
|
||||||
provider: 'Febbox FID',
|
codecs: extractCodecDetails(source.detailedFilename || streamTitle)
|
||||||
headers: {}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2514,12 +2506,10 @@ 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),
|
||||||
provider: 'Febbox FID',
|
codecs: extractCodecDetails(source.detailedFilename || streamTitle)
|
||||||
headers: {}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -2872,15 +2862,17 @@ 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: streamTitle,
|
name: streamName,
|
||||||
title: streamTitle,
|
title: streamTitle,
|
||||||
url: url,
|
url: url,
|
||||||
quality: parseQualityFromLabel(quality),
|
quality: parseQualityFromLabel(quality),
|
||||||
provider: 'PStream',
|
codecs: extractCodecDetails(apiResponse.name || quality), // Use filename for codec detection
|
||||||
headers: {}
|
size: apiResponse.size || 'Unknown size',
|
||||||
|
provider: 'PStream'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2898,15 +2890,17 @@ 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: streamTitle,
|
name: streamName,
|
||||||
title: streamTitle,
|
title: streamTitle,
|
||||||
url: source.url,
|
url: source.url,
|
||||||
quality: parseQualityFromLabel(source.quality),
|
quality: parseQualityFromLabel(source.quality),
|
||||||
provider: 'PStream',
|
codecs: source.codec ? [source.codec] : [],
|
||||||
headers: {}
|
size: source.size || 'Unknown size',
|
||||||
|
provider: 'PStream'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2919,15 +2913,17 @@ 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: streamTitle,
|
name: streamName,
|
||||||
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),
|
||||||
provider: 'PStream',
|
codecs: item.codec ? [item.codec] : [],
|
||||||
headers: {}
|
size: item.size || 'Unknown size',
|
||||||
|
provider: 'PStream'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-2
@@ -84,14 +84,18 @@ const getVidZeeStreams = async (tmdbId, mediaType, seasonNum, episodeNum) => {
|
|||||||
const language = sourceItem.language || sourceItem.lang;
|
const language = sourceItem.language || sourceItem.lang;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: `VidZee Server${sr} - ${quality} - ${language}`,
|
title: `VidZee S${sr} - ${quality}`,
|
||||||
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",
|
||||||
|
behaviorHints: {
|
||||||
|
notWebReady: true,
|
||||||
headers: {
|
headers: {
|
||||||
'Referer': 'https://core.vidzee.wtf/'
|
'Referer': 'https://core.vidzee.wtf/'
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}).filter(stream => stream.url);
|
}).filter(stream => stream.url);
|
||||||
|
|
||||||
|
|||||||
@@ -1,651 +0,0 @@
|
|||||||
// MoviesClub Provider
|
|
||||||
// Extracts streams from moviesapi.club
|
|
||||||
|
|
||||||
const axios = require('axios');
|
|
||||||
const JsUnpacker = require('../utils/jsunpack');
|
|
||||||
const { config } = require('../utils/config');
|
|
||||||
|
|
||||||
function applyMoviesClubProxy(url) {
|
|
||||||
try {
|
|
||||||
const base = config.moviesclubProxy;
|
|
||||||
if (!base || typeof base !== 'string') return url;
|
|
||||||
// Ensure base ends with either = or a delimiter allowing raw append
|
|
||||||
const proxied = base + encodeURIComponent(url);
|
|
||||||
return proxied;
|
|
||||||
} catch {
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 rawUrl = decodedSrc;
|
|
||||||
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: applyMoviesClubProxy(rawUrl),
|
|
||||||
quality: extractQualityFromUrl(rawUrl) || '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 rawUrl = streamUrl;
|
|
||||||
const stream = {
|
|
||||||
name: `MoviesClub - ${serverName}`,
|
|
||||||
title: `${mediaType === 'movie' ? 'Movie' : `S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`} ${tmdbId} - ${serverName}`,
|
|
||||||
url: applyMoviesClubProxy(rawUrl),
|
|
||||||
quality: extractQualityFromUrl(rawUrl) || '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 rawUrl = 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: applyMoviesClubProxy(rawUrl),
|
|
||||||
quality: extractQualityFromUrl(rawUrl) || '720p',
|
|
||||||
// Always include vidora headers for single-server results to prevent 403 playback issues
|
|
||||||
headers: {
|
|
||||||
Origin: 'https://vidora.stream',
|
|
||||||
Referer: 'https://vidora.stream/'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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 rawUrl = serverData;
|
|
||||||
const stream = {
|
|
||||||
name: `MoviesClub - ${serverName}`,
|
|
||||||
title: `${mediaType === 'movie' ? 'Movie' : `S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`} ${tmdbId} - ${serverName}`,
|
|
||||||
url: applyMoviesClubProxy(rawUrl),
|
|
||||||
quality: extractQualityFromUrl(rawUrl) || '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 rawUrl = serverData[key];
|
|
||||||
const stream = {
|
|
||||||
name: `MoviesClub - ${serverName}`,
|
|
||||||
title: `${mediaType === 'movie' ? 'Movie' : `S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`} ${tmdbId} - ${serverName}`,
|
|
||||||
url: applyMoviesClubProxy(rawUrl),
|
|
||||||
quality: extractQualityFromUrl(rawUrl) || '720p',
|
|
||||||
headers: rawUrl.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,8 +1106,6 @@ 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}`);
|
||||||
|
|||||||
+186
-102
@@ -1,23 +1,12 @@
|
|||||||
const { config } = require('../utils/config');
|
const { config } = require('../utils/config');
|
||||||
const fs = require('fs');
|
// Lazy load showbox to avoid early side-effects; temporarily disabled until stabilized
|
||||||
const path = require('path');
|
let showbox = null;
|
||||||
|
let k4hdhub = null; // 4khdhub provider lazy ref
|
||||||
// Lazy load cache for providers
|
let moviesmod = null; // moviesmod provider lazy ref
|
||||||
const providerCache = new Map();
|
let mp4hydra = null; // MP4Hydra provider lazy ref
|
||||||
|
let vidzee = null; // VidZee provider lazy ref
|
||||||
// Function name mappings for existing providers
|
let vixsrc = null; // vixsrc provider lazy ref
|
||||||
const providerFunctionMap = {
|
let xprime = null; // xprime provider lazy ref
|
||||||
'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 };
|
||||||
|
|
||||||
@@ -25,50 +14,11 @@ async function getEffectiveCookies() {
|
|||||||
return Array.isArray(config.febboxCookies) ? config.febboxCookies : [];
|
return Array.isArray(config.febboxCookies) ? config.febboxCookies : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all provider files
|
// Adapter for Showbox provider using existing exported getStreamsFromTmdbId
|
||||||
function getProviderFiles() {
|
async function fetchShowbox(ctx) {
|
||||||
const providersDir = path.join(__dirname);
|
if (!showbox) {
|
||||||
return fs.readdirSync(providersDir)
|
try { showbox = require('./Showbox.js'); } catch (e) { console.error('Failed to load Showbox provider:', e.message); return []; }
|
||||||
.filter(file => file.endsWith('.js') && file !== 'registry.js' && file !== 'vidsrcextractor.js')
|
|
||||||
.map(file => ({
|
|
||||||
name: path.parse(file).name.toLowerCase(),
|
|
||||||
file: file,
|
|
||||||
functionName: providerFunctionMap[file]
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load a provider module
|
|
||||||
function loadProvider(providerFile) {
|
|
||||||
if (!providerCache.has(providerFile)) {
|
|
||||||
try {
|
|
||||||
providerCache.set(providerFile, require(path.join(__dirname, providerFile)));
|
|
||||||
} catch (e) {
|
|
||||||
console.error(`Failed to load provider ${providerFile}:`, e.message);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return providerCache.get(providerFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create fetch function for a provider
|
|
||||||
function createFetchFunction(providerInfo) {
|
|
||||||
return async function(ctx) {
|
|
||||||
const module = loadProvider(providerInfo.file);
|
|
||||||
if (!module) return [];
|
|
||||||
|
|
||||||
const funcName = providerInfo.functionName;
|
|
||||||
if (!module[funcName]) {
|
|
||||||
console.error(`Provider ${providerInfo.name} does not export ${funcName}`);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
|
|
||||||
const t0 = Date.now();
|
|
||||||
|
|
||||||
let result;
|
|
||||||
if (providerInfo.name === 'showbox') {
|
|
||||||
// Special case for Showbox with TMDB key and cookies
|
|
||||||
const { getTmdbApiKey } = require('../utils/tmdbKey');
|
const { getTmdbApiKey } = require('../utils/tmdbKey');
|
||||||
const tmdbApiKey = getTmdbApiKey();
|
const tmdbApiKey = getTmdbApiKey();
|
||||||
if (!tmdbApiKey) {
|
if (!tmdbApiKey) {
|
||||||
@@ -87,13 +37,172 @@ function createFetchFunction(providerInfo) {
|
|||||||
lastCookieStats = { selected: selected.slice(0, 16) + '...', index, total: cookies.length, remainingMB: null, timestamp: Date.now() };
|
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}`);
|
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);
|
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) {
|
if (global.currentRequestUserCookieRemainingMB != null) {
|
||||||
lastCookieStats.remainingMB = global.currentRequestUserCookieRemainingMB;
|
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 || {};
|
global.currentRequestConfig = previousConfig || {};
|
||||||
} else if (providerInfo.name === 'xprime') {
|
}
|
||||||
// Special case for Xprime with TMDB metadata lookup
|
}
|
||||||
|
|
||||||
|
const providers = [];
|
||||||
|
|
||||||
|
if (config.enableShowboxProvider) {
|
||||||
|
providers.push({ name: 'showbox', enabled: true, fetch: fetchShowbox });
|
||||||
|
} else {
|
||||||
|
console.log('[registry] showbox disabled via config');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 4KHDHub adapter
|
||||||
|
async function fetch4khdhub(ctx) {
|
||||||
|
if (!k4hdhub) {
|
||||||
|
try { k4hdhub = require('./4khdhub.js'); } catch (e) { console.error('Failed to load 4khdhub provider:', e.message); return []; }
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
|
||||||
|
const t0 = Date.now();
|
||||||
|
const result = await k4hdhub.get4KHDHubStreams(ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null);
|
||||||
|
const durationMs = Date.now() - t0;
|
||||||
|
console.log(`[registry] 4khdhub fetch duration ${durationMs}ms`);
|
||||||
|
if (!Array.isArray(result)) return [];
|
||||||
|
return result.map(s => ({ ...s, name: s.name || '4KHDHub', provider: '4khdhub' }));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[registry] 4khdhub fetch error:', e.message);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.enable4khdhubProvider) {
|
||||||
|
providers.push({ name: '4khdhub', enabled: true, fetch: fetch4khdhub });
|
||||||
|
console.log('[registry] 4khdhub provider enabled');
|
||||||
|
} else {
|
||||||
|
console.log('[registry] 4khdhub disabled via config');
|
||||||
|
}
|
||||||
|
|
||||||
|
// MoviesMod adapter
|
||||||
|
async function fetchMoviesMod(ctx) {
|
||||||
|
if (!moviesmod) {
|
||||||
|
try { moviesmod = require('./moviesmod.js'); } catch (e) { console.error('Failed to load moviesmod provider:', e.message); return []; }
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
|
||||||
|
const t0 = Date.now();
|
||||||
|
const result = await moviesmod.getMoviesModStreams(ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null);
|
||||||
|
const durationMs = Date.now() - t0;
|
||||||
|
console.log(`[registry] moviesmod fetch duration ${durationMs}ms`);
|
||||||
|
if (!Array.isArray(result)) return [];
|
||||||
|
return result.map(s => ({ ...s, name: s.name || 'MoviesMod', provider: 'moviesmod' }));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[registry] moviesmod fetch error:', e.message);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.enableMoviesmodProvider) {
|
||||||
|
providers.push({ name: 'moviesmod', enabled: true, fetch: fetchMoviesMod });
|
||||||
|
console.log('[registry] moviesmod provider enabled');
|
||||||
|
} else {
|
||||||
|
console.log('[registry] moviesmod disabled via config');
|
||||||
|
}
|
||||||
|
|
||||||
|
// MP4Hydra adapter
|
||||||
|
async function fetchMp4hydra(ctx) {
|
||||||
|
if (!mp4hydra) {
|
||||||
|
try { mp4hydra = require('./MP4Hydra.js'); } catch (e) { console.error('Failed to load MP4Hydra provider:', e.message); return []; }
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
|
||||||
|
const t0 = Date.now();
|
||||||
|
const result = await mp4hydra.getMP4HydraStreams(ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null);
|
||||||
|
const durationMs = Date.now() - t0;
|
||||||
|
console.log(`[registry] mp4hydra fetch duration ${durationMs}ms`);
|
||||||
|
if (!Array.isArray(result)) return [];
|
||||||
|
return result.map(s => ({ ...s, name: s.name || 'MP4Hydra', provider: 'mp4hydra' }));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[registry] mp4hydra fetch error:', e.message);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.enableMp4hydraProvider) {
|
||||||
|
providers.push({ name: 'mp4hydra', enabled: true, fetch: fetchMp4hydra });
|
||||||
|
console.log('[registry] mp4hydra provider enabled');
|
||||||
|
} else {
|
||||||
|
console.log('[registry] mp4hydra disabled via config');
|
||||||
|
}
|
||||||
|
|
||||||
|
// VidZee adapter
|
||||||
|
async function fetchVidZee(ctx) {
|
||||||
|
if (!vidzee) {
|
||||||
|
try { vidzee = require('./VidZee.js'); } catch (e) { console.error('Failed to load VidZee provider:', e.message); return []; }
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
|
||||||
|
const t0 = Date.now();
|
||||||
|
const result = await vidzee.getVidZeeStreams(ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null);
|
||||||
|
const durationMs = Date.now() - t0;
|
||||||
|
console.log(`[registry] vidzee fetch duration ${durationMs}ms`);
|
||||||
|
if (!Array.isArray(result)) return [];
|
||||||
|
return result.map(s => ({ ...s, name: s.name || 'VidZee', provider: 'vidzee' }));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[registry] vidzee fetch error:', e.message);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.enableVidzeeProvider) {
|
||||||
|
providers.push({ name: 'vidzee', enabled: true, fetch: fetchVidZee });
|
||||||
|
console.log('[registry] vidzee provider enabled');
|
||||||
|
} else {
|
||||||
|
console.log('[registry] vidzee disabled via config');
|
||||||
|
}
|
||||||
|
|
||||||
|
// vixsrc adapter
|
||||||
|
async function fetchVixsrc(ctx) {
|
||||||
|
if (!vixsrc) {
|
||||||
|
try { vixsrc = require('./vixsrc.js'); } catch (e) { console.error('Failed to load vixsrc provider:', e.message); return []; }
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
|
||||||
|
const t0 = Date.now();
|
||||||
|
const result = await vixsrc.getVixsrcStreams(ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null);
|
||||||
|
const durationMs = Date.now() - t0;
|
||||||
|
console.log(`[registry] vixsrc fetch duration ${durationMs}ms`);
|
||||||
|
if (!Array.isArray(result)) return [];
|
||||||
|
return result.map(s => ({ ...s, name: s.name || 'Vixsrc', provider: 'vixsrc' }));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[registry] vixsrc fetch error:', e.message);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.enableVixsrcProvider) {
|
||||||
|
providers.push({ name: 'vixsrc', enabled: true, fetch: fetchVixsrc });
|
||||||
|
console.log('[registry] vixsrc provider enabled');
|
||||||
|
} else {
|
||||||
|
console.log('[registry] vixsrc disabled via config');
|
||||||
|
}
|
||||||
|
|
||||||
|
// xprime adapter (requires TMDB metadata lookup for title/year)
|
||||||
|
async function fetchXprime(ctx) {
|
||||||
|
if (!xprime) {
|
||||||
|
try { xprime = require('./xprime.js'); } catch (e) { console.error('Failed to load xprime provider:', e.message); return []; }
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
|
||||||
const { getTmdbApiKey } = require('../utils/tmdbKey');
|
const { getTmdbApiKey } = require('../utils/tmdbKey');
|
||||||
const tmdbApiKey = getTmdbApiKey();
|
const tmdbApiKey = getTmdbApiKey();
|
||||||
if (!tmdbApiKey) {
|
if (!tmdbApiKey) {
|
||||||
@@ -117,10 +226,13 @@ function createFetchFunction(providerInfo) {
|
|||||||
console.log('[registry] xprime missing title/year after TMDB lookup');
|
console.log('[registry] xprime missing title/year after TMDB lookup');
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
result = await module[funcName](title, year, mediaType, ctx.season || null, ctx.episode || null);
|
const t0 = Date.now();
|
||||||
// Normalize quality labels
|
const streams = await xprime.getXprimeStreams(title, year, mediaType, ctx.season || null, ctx.episode || null);
|
||||||
if (Array.isArray(result)) {
|
const durationMs = Date.now() - t0;
|
||||||
result = result.map(s => {
|
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';
|
let q = s.quality || 'Unknown';
|
||||||
if (!/(\d{3,4})p/.test(q.toLowerCase())) {
|
if (!/(\d{3,4})p/.test(q.toLowerCase())) {
|
||||||
const qLower = q.toLowerCase();
|
const qLower = q.toLowerCase();
|
||||||
@@ -131,45 +243,17 @@ function createFetchFunction(providerInfo) {
|
|||||||
}
|
}
|
||||||
return { ...s, quality: q, name: s.name || 'Xprime', provider: 'xprime' };
|
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) {
|
} catch (e) {
|
||||||
console.error(`[registry] ${providerInfo.name} fetch error:`, e.message);
|
console.error('[registry] xprime fetch error:', e.message);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize providers
|
if (config.enableXprimeProvider) {
|
||||||
const providerFiles = getProviderFiles();
|
providers.push({ name: 'xprime', enabled: true, fetch: fetchXprime });
|
||||||
const providers = [];
|
console.log('[registry] xprime provider enabled');
|
||||||
|
|
||||||
for (const providerInfo of providerFiles) {
|
|
||||||
const enableFlag = `enable${providerInfo.name.charAt(0).toUpperCase() + providerInfo.name.slice(1)}Provider`;
|
|
||||||
const enabled = config[enableFlag] !== false;
|
|
||||||
|
|
||||||
if (enabled) {
|
|
||||||
providers.push({
|
|
||||||
name: providerInfo.name,
|
|
||||||
enabled: true,
|
|
||||||
fetch: createFetchFunction(providerInfo)
|
|
||||||
});
|
|
||||||
console.log(`[registry] ${providerInfo.name} provider enabled`);
|
|
||||||
} else {
|
} else {
|
||||||
console.log(`[registry] ${providerInfo.name} disabled via config`);
|
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 })); }
|
||||||
|
|||||||
@@ -1380,8 +1380,11 @@ 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,
|
||||||
provider: 'uhdmovies',
|
size: sizeInfo,
|
||||||
headers: {}
|
fileName: fileName,
|
||||||
|
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',
|
||||||
provider: 'Vidsrc',
|
type: 'direct',
|
||||||
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',
|
||||||
headers: {},
|
codecs: [],
|
||||||
|
size: 'Unknown size'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -182,7 +182,6 @@
|
|||||||
<p class="hint">Provider enable/disable & feature flags apply immediately. Click toggles to enable (highlighted) or disable (dim).</p>
|
<p class="hint">Provider enable/disable & feature flags apply immediately. Click toggles to enable (highlighted) or disable (dim).</p>
|
||||||
<div class="adv-group">
|
<div class="adv-group">
|
||||||
<div class="field small"><label>Showbox Cache Dir <input type="text" name="adv_showboxCacheDir" placeholder="/data/showbox-cache"/></label></div>
|
<div class="field small"><label>Showbox Cache Dir <input type="text" name="adv_showboxCacheDir" placeholder="/data/showbox-cache"/></label></div>
|
||||||
<div class="field small"><label>MoviesClub Proxy URL <p class="hint">Optional. Example: <code>https://winter-surf-e49f.inside4ndroid-techsup.workers.dev/?destination=</code><br/>Leave blank for direct links. The raw stream URL will be appended (URL encoded).</p><input type="text" name="adv_moviesclubProxy" placeholder="https://proxy/?destination=" /></label></div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section id="panel-live" class="panel">
|
<section id="panel-live" class="panel">
|
||||||
|
|||||||
+9
-21
@@ -174,7 +174,6 @@ function fillForm(data){
|
|||||||
advBools.forEach(([cfgKey, elName])=>{ const el = f.elements[elName]; if(!el) return; const src = (override[cfgKey]!==undefined? override[cfgKey] : merged[cfgKey]); if(el.type==='checkbox'){ el.checked = !!src; } else { el.value = src? 'true':''; } });
|
advBools.forEach(([cfgKey, elName])=>{ const el = f.elements[elName]; if(!el) return; const src = (override[cfgKey]!==undefined? override[cfgKey] : merged[cfgKey]); if(el.type==='checkbox'){ el.checked = !!src; } else { el.value = src? 'true':''; } });
|
||||||
const advTexts = [
|
const advTexts = [
|
||||||
['showboxCacheDir','adv_showboxCacheDir']
|
['showboxCacheDir','adv_showboxCacheDir']
|
||||||
,['moviesclubProxy','adv_moviesclubProxy']
|
|
||||||
];
|
];
|
||||||
advTexts.forEach(([cfgKey, elName])=>{ const el = f.elements[elName]; if(!el) return; const val = override[cfgKey]!==undefined? override[cfgKey] : merged[cfgKey]; el.value = val || ''; });
|
advTexts.forEach(([cfgKey, elName])=>{ const el = f.elements[elName]; if(!el) return; const val = override[cfgKey]!==undefined? override[cfgKey] : merged[cfgKey]; el.value = val || ''; });
|
||||||
// Sync checkbox style advanced flags
|
// Sync checkbox style advanced flags
|
||||||
@@ -258,7 +257,6 @@ async function save(){
|
|||||||
};
|
};
|
||||||
Object.entries(boolMap).forEach(([formName,cfgKey])=>{ const el = f.elements[formName]; if(el) payload[cfgKey] = !!el.checked; });
|
Object.entries(boolMap).forEach(([formName,cfgKey])=>{ const el = f.elements[formName]; if(el) payload[cfgKey] = !!el.checked; });
|
||||||
const textMap = { adv_showboxCacheDir:'showboxCacheDir' };
|
const textMap = { adv_showboxCacheDir:'showboxCacheDir' };
|
||||||
textMap.adv_moviesclubProxy = 'moviesclubProxy';
|
|
||||||
Object.entries(textMap).forEach(([formName,cfgKey])=>{ const el = f.elements[formName]; if(!el) return; const v = el.value.trim(); payload[cfgKey] = v? v : null; });
|
Object.entries(textMap).forEach(([formName,cfgKey])=>{ const el = f.elements[formName]; if(!el) return; const v = el.value.trim(); payload[cfgKey] = v? v : null; });
|
||||||
const tmdbHidden = f.elements['tmdbApiKeysHidden'];
|
const tmdbHidden = f.elements['tmdbApiKeysHidden'];
|
||||||
if (tmdbHidden){
|
if (tmdbHidden){
|
||||||
@@ -598,25 +596,15 @@ 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 = [
|
||||||
// Fetch available providers from API
|
['showbox','enableShowboxProvider'],
|
||||||
let availableProviders = [];
|
['4khdhub','enable4khdhubProvider'],
|
||||||
try {
|
['moviesmod','enableMoviesmodProvider'],
|
||||||
const providersResponse = await fetch('/api/providers');
|
['mp4hydra','enableMp4hydraProvider'],
|
||||||
if (providersResponse.ok) {
|
['vidzee','enableVidzeeProvider'],
|
||||||
const providersData = await providersResponse.json();
|
['vixsrc','enableVixsrcProvider'],
|
||||||
availableProviders = providersData.providers || [];
|
['xprime','enableXprimeProvider']
|
||||||
}
|
];
|
||||||
} 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 @@
|
|||||||
1758225404146
|
1758105049325
|
||||||
+28
-51
@@ -52,19 +52,6 @@ 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
|
||||||
@@ -93,31 +80,25 @@ 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
|
||||||
// Dynamic provider enable flags
|
const boolKeys = [
|
||||||
const providerNames = getProviderNames();
|
'enableShowboxProvider','enableXprimeProvider','enable4khdhubProvider','enableMoviesmodProvider','enableMp4hydraProvider','enableVidzeeProvider','enableVixsrcProvider',
|
||||||
const boolKeys = providerNames.map(name => `enable${name.charAt(0).toUpperCase() + name.slice(1)}Provider`);
|
'disableCache','enablePStreamApi','disableUrlValidation','disable4khdhubUrlValidation'
|
||||||
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
|
||||||
// Set defaults for provider enable flags
|
if (cfg.enableShowboxProvider === undefined) cfg.enableShowboxProvider = true;
|
||||||
providerNames.forEach(name => {
|
if (cfg.enableXprimeProvider === undefined) cfg.enableXprimeProvider = true;
|
||||||
const flag = `enable${name.charAt(0).toUpperCase() + name.slice(1)}Provider`;
|
if (cfg.enable4khdhubProvider === undefined) cfg.enable4khdhubProvider = true;
|
||||||
if (cfg[flag] === undefined) cfg[flag] = true; // Default to enabled
|
if (cfg.enableMoviesmodProvider === undefined) cfg.enableMoviesmodProvider = true;
|
||||||
});
|
if (cfg.enableMp4hydraProvider === undefined) cfg.enableMp4hydraProvider = true;
|
||||||
|
if (cfg.enableVidzeeProvider === undefined) cfg.enableVidzeeProvider = true;
|
||||||
// Default values for other flags
|
if (cfg.enableVixsrcProvider === undefined) cfg.enableVixsrcProvider = true;
|
||||||
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
|
||||||
if (cfg.disableUrlValidation === undefined) cfg.disableUrlValidation = false;
|
if (cfg.disableUrlValidation === undefined) cfg.disableUrlValidation = false;
|
||||||
if (cfg.disable4khdhubUrlValidation === undefined) cfg.disable4khdhubUrlValidation = false;
|
if (cfg.disable4khdhubUrlValidation === undefined) cfg.disable4khdhubUrlValidation = false;
|
||||||
// MoviesClub optional proxy base (string URL with trailing ?destination= or similar)
|
|
||||||
if (cfg.moviesclubProxy !== undefined && typeof cfg.moviesclubProxy === 'string') {
|
|
||||||
cfg.moviesclubProxy = cfg.moviesclubProxy.trim();
|
|
||||||
if (!cfg.moviesclubProxy) delete cfg.moviesclubProxy;
|
|
||||||
}
|
|
||||||
return cfg;
|
return cfg;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,15 +114,14 @@ 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
|
||||||
// Dynamic provider enable flags
|
process.env.ENABLE_SHOWBOX_PROVIDER = cfg.enableShowboxProvider ? 'true':'false';
|
||||||
const providerNames = getProviderNames();
|
process.env.ENABLE_XPRIME_PROVIDER = cfg.enableXprimeProvider ? 'true':'false';
|
||||||
providerNames.forEach(name => {
|
process.env.ENABLE_4KHDHUB_PROVIDER = cfg.enable4khdhubProvider ? 'true':'false';
|
||||||
const flag = `enable${name.charAt(0).toUpperCase() + name.slice(1)}Provider`;
|
process.env.ENABLE_MOVIESMOD_PROVIDER = cfg.enableMoviesmodProvider ? 'true':'false';
|
||||||
const envName = `ENABLE_${name.toUpperCase()}_PROVIDER`;
|
process.env.ENABLE_MP4HYDRA_PROVIDER = cfg.enableMp4hydraProvider ? 'true':'false';
|
||||||
process.env[envName] = cfg[flag] ? 'true' : 'false';
|
process.env.ENABLE_VIDZEE_PROVIDER = cfg.enableVidzeeProvider ? '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';
|
||||||
@@ -157,8 +137,6 @@ function applyConfigToEnv(cfg){
|
|||||||
delete process.env.VIDZEE_PROXY_URL;
|
delete process.env.VIDZEE_PROXY_URL;
|
||||||
delete process.env.VIDSRC_PROXY_URL;
|
delete process.env.VIDSRC_PROXY_URL;
|
||||||
delete process.env.MOVIESMOD_PROXY_URL;
|
delete process.env.MOVIESMOD_PROXY_URL;
|
||||||
// Mirror moviesclub proxy for potential legacy usage
|
|
||||||
if (cfg.moviesclubProxy) process.env.MOVIESCLUB_PROXY_URL = cfg.moviesclubProxy; else delete process.env.MOVIESCLUB_PROXY_URL;
|
|
||||||
if (cfg.defaultRegion) process.env.FEBBOX_REGION = cfg.defaultRegion; // alias
|
if (cfg.defaultRegion) process.env.FEBBOX_REGION = cfg.defaultRegion; // alias
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,6 +152,13 @@ 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,
|
||||||
@@ -181,14 +166,6 @@ 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);
|
||||||
|
|||||||
@@ -1,113 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,24 +1,19 @@
|
|||||||
{
|
{
|
||||||
"defaultProviders": [],
|
"defaultProviders": [],
|
||||||
"configVersion": 1,
|
"configVersion": 1,
|
||||||
"tmdbApiKeys": [
|
"tmdbApiKeys": [],
|
||||||
"f3c627493095a7e40ceca68355c94c6d"
|
|
||||||
],
|
|
||||||
"enableShowboxProvider": true,
|
"enableShowboxProvider": true,
|
||||||
"febboxCookies": [
|
"febboxCookies": [],
|
||||||
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3NTc5NjYxMDAsIm5iZiI6MTc1Nzk2NjEwMCwiZXhwIjoxNzg5MDcwMTIwLCJkYXRhIjp7InVpZCI6ODc2NTEwLCJ0b2tlbiI6ImY0MDQxNWJkMTdiYjc1YmQ2ZWQ1NzQ1ZTFhZmNlOTM5In19.vs_pLXAJYhnEaHDfcR_dv9-3us2MJhMOMHUC4la9Rag"
|
|
||||||
],
|
|
||||||
"enableXprimeProvider": true,
|
"enableXprimeProvider": true,
|
||||||
"enable4khdhubProvider": true,
|
"enable4khdhubProvider": true,
|
||||||
"enableMoviesmodProvider": true,
|
"enableMoviesmodProvider": true,
|
||||||
"enableMp4hydraProvider": true,
|
"enableMp4hydraProvider": true,
|
||||||
"enableVidzeeProvider": true,
|
"enableVidzeeProvider": true,
|
||||||
"enableVixsrcProvider": true,
|
"enableVixsrcProvider": true,
|
||||||
"disableCache": true,
|
"disableCache": false,
|
||||||
"enablePStreamApi": true,
|
"enablePStreamApi": true,
|
||||||
"disableUrlValidation": false,
|
"disableUrlValidation": false,
|
||||||
"disable4khdhubUrlValidation": false,
|
"disable4khdhubUrlValidation": false,
|
||||||
"excludeCodecsRaw": "{\"excludeDV\":false,\"excludeHDR\":false}",
|
"excludeCodecsRaw": "{\"excludeDV\":false,\"excludeHDR\":false}",
|
||||||
"minQualitiesRaw": "all",
|
"minQualitiesRaw": "all"
|
||||||
"port": 8787
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user