mirror of
https://github.com/Inside4ndroid/TMDB-Embed-API.git
synced 2026-10-22 17:45:11 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ebb0d32d5 | ||
|
|
c8ef89398f | ||
|
|
9714ad51c8 | ||
|
|
17079e1646 | ||
|
|
25cc59659d | ||
|
|
ab18e06a94 | ||
|
|
25acff24a7 | ||
|
|
0e9f6d92e4 | ||
|
|
3433c093bd |
+3
-1
@@ -1 +1,3 @@
|
|||||||
/node_modules
|
/node_modules
|
||||||
|
restart.trigger
|
||||||
|
.vscode/
|
||||||
+129
-1
@@ -2,6 +2,127 @@
|
|||||||
|
|
||||||
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.8] - 2025-10-03
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Added `processStreamsForProxy` so the aggregate and provider-specific endpoints automatically rewrite stream URLs through the internal `/m3u8-proxy`/`/ts-proxy` layer whenever `enableProxy` is turned on, stripping provider headers safely.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Showbox provider overhaul: filesystem-only caching (Redis removed), smarter FebBox cookie selection with region fallbacks, TMDB title/image validation that recognises romanized names, plus cached HEAD size lookups for faster listings.
|
||||||
|
- Vixsrc provider rewritten to parse the `window.masterPlaylist` payload (token + expiry), returning a single master playlist with English subtitle lookup and correct Referer headers.
|
||||||
|
- Config loader now prefers `utils/user-config.json`, dedupes TMDB keys, defaults provider enable flags to `true`, and scrubs legacy proxy env values when mirroring overrides back to `process.env`.
|
||||||
|
- 4KHDHub provider permanently drops `.zip` archive links instead of trimming extensions and improves host distribution logging.
|
||||||
|
- Provider registry dynamically enumerates available modules (removing MoviesClub/Xprime remnants) while keeping per-request cookie stats for the admin debug panel.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- Deleted legacy `providers/moviesclub.js`, `providers/xprime.js`, and other unused proxy/auth remnants that were no longer referenced.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Aggregated responses now obey the proxy flag without client changes—streams returned from `/api/streams/...` are proxy-wrapped and omit upstream header hints when `enableProxy` is active.
|
||||||
|
|
||||||
|
## [1.0.7] - 2025-09-21
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- VidZee provider: definitive AES-256-CBC decoder (replaced heuristic token attempts). Decodes newly obfuscated `atob(token) => ivBase64:cipherBase64` structure using padded key `qrincywincyspider` and PKCS7.
|
||||||
|
- Tail segment acceleration: internal tail prefetch map (`tailPrefetchMap`) with TTL cleanup and configurable `tailPrefetchKB` (default 256 KB) now documented and instrumented.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Proxy range negotiation: clarified interaction between `progressiveOpen` and synthetic initial partial (auto-suppression when progressive active). Additional debug logging around tail cache hits and forced host overrides.
|
||||||
|
- VidZee streams now always return direct decrypted URLs (per-stream `originalToken` retained for debugging when `VIDZEE_DEBUG=1`).
|
||||||
|
- Updated README version badge to 1.0.7.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Eliminated edge cases where encoded VidZee tokens were leaking through without decoding.
|
||||||
|
- Reduced VLC initial loop behavior via tail prefetch serving last-byte probes from cache faster (combined with earlier 1.0.6 range logic).
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- README: implicit improvements carried forward (no separate section required) plus badge bump.
|
||||||
|
- CHANGELOG: recorded AES decoder details & tail prefetch instrumentation.
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
- Future proxy tuning items (size meta map, dynamic progressive growth) tracked but not part of this release.
|
||||||
|
|
||||||
|
|
||||||
|
## [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
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Standardized Docker port to `8787` everywhere (Dockerfile `EXPOSE`, compose `ports`, healthchecks, and README examples). Previous release notes mentioned 8787 but some environments weren't reset; this release ensures consistency.
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- Updated README version badge to 1.0.2 and verified Docker commands use `-p 8787:8787`.
|
||||||
|
|
||||||
## [1.0.1] - 2025-09-17
|
## [1.0.1] - 2025-09-17
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -29,7 +150,14 @@ 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.1]: https://github.com/Inside4ndroid/TMDB-Embed-API/compare/v1.0.0...v1.0.1# Changelog
|
[1.0.8]: https://github.com/Inside4ndroid/TMDB-Embed-API/compare/v1.0.7...v1.0.8
|
||||||
|
[1.0.7]: https://github.com/Inside4ndroid/TMDB-Embed-API/compare/v1.0.6...v1.0.7
|
||||||
|
[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.1]: https://github.com/Inside4ndroid/TMDB-Embed-API/compare/v1.0.0...v1.0.1
|
||||||
|
|
||||||
## [1.0.0] - 2025-09-16
|
## [1.0.0] - 2025-09-16
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ 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.1-informational?style=flat" />
|
<img src="https://img.shields.io/badge/Version-1.0.8-informational?style=flat" />
|
||||||
<img src="https://img.shields.io/docker/pulls/inside4ndroid/tmdb-embed-api?label=Docker%20Pulls&style=flat" />
|
<img src="https://img.shields.io/docker/pulls/inside4ndroid/tmdb-embed-api?label=Docker%20Pulls&style=flat" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -34,14 +34,32 @@
|
|||||||
|
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
- **Multi‑TMDB Key Rotation** – Supply multiple API keys; one is chosen randomly per request.
|
- **Multi‑TMDB Key Rotation** – Supply multiple API keys; one is chosen randomly per request.
|
||||||
- **Provider Aggregation** – Pluggable providers (Showbox, 4khdhub, MoviesMod, MP4Hydra, VidZee, Vixsrc, Xprime) with per‑provider enable toggles + default selection.
|
- **Provider Aggregation** – Pluggable providers (Showbox, 4khdhub, MoviesMod, MP4Hydra, VidZee, Vixsrc, UHDMovies) with per‑provider enable toggles + default selection.
|
||||||
|
- **🔥 Plugin System** – Drop new provider files in `providers/` and add its exported function to the registry map (`providers/registry.js` → `providerFunctionMap`).
|
||||||
- **Dynamic Filtering** – Minimum quality presets, custom JSON quality map, codec exclusion rules (presets + JSON).
|
- **Dynamic Filtering** – Minimum quality presets, custom JSON quality map, codec exclusion rules (presets + JSON).
|
||||||
- **Runtime Overrides UI** – Fully interactive web admin at `/` (login protected) writing to `utils/user-config.json`.
|
- **Runtime Overrides UI** – Fully interactive web admin at `/` (login protected) writing to `utils/user-config.json`.
|
||||||
- **Session Auth + Rate Limiting** – Login system with brute‑force lockouts, logout, password change.
|
- **Session Auth + Rate Limiting** – Login system with brute‑force lockouts, logout, password change.
|
||||||
- **Status & Health Panel** – Live metrics, provider status, endpoint list, functional provider checks.
|
- **Status & Health Panel** – Live metrics, provider status, endpoint list, functional provider checks.
|
||||||
- **Config Propagation** – Overrides mirrored to `process.env` for legacy compatibility (no `.env` required after first save).
|
- **Config Propagation** – Overrides mirrored to `process.env` for legacy compatibility (no `.env` required after first save).
|
||||||
- **Back‑Navigation Safe** – Cache-control + visibility/session revalidation.
|
- **Back‑Navigation Safe** – Cache-control + visibility/session revalidation.
|
||||||
- **Extensible** – Simple provider registry pattern.
|
- **Extensible** – Simple drop-in provider plugin system.
|
||||||
|
- **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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -79,6 +97,11 @@ docker run --name tmdb-embed-api -p 8787:8787 \
|
|||||||
inside4ndroid/tmdb-embed-api:latest
|
inside4ndroid/tmdb-embed-api:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Or the minimal quick-test run:
|
||||||
|
```bash
|
||||||
|
docker run -it -p 8787:8787 inside4ndroid/tmdb-embed-api:latest
|
||||||
|
```
|
||||||
|
|
||||||
Persist overrides (Windows PowerShell example) by mounting a local file:
|
Persist overrides (Windows PowerShell example) by mounting a local file:
|
||||||
```powershell
|
```powershell
|
||||||
New-Item -ItemType File -Path .\utils\user-config.json -Force | Out-Null
|
New-Item -ItemType File -Path .\utils\user-config.json -Force | Out-Null
|
||||||
@@ -202,22 +225,61 @@ Session is revalidated on visibility and back/forward navigation to prevent stal
|
|||||||
---
|
---
|
||||||
|
|
||||||
## 🔌 Providers
|
## 🔌 Providers
|
||||||
Current built‑in providers:
|
The API supports a plugin system. Drop a new provider file in the `providers/` folder and register its exported function in `providers/registry.js` under `providerFunctionMap`.
|
||||||
- `showbox`
|
|
||||||
- `4khdhub`
|
|
||||||
- `moviesmod`
|
|
||||||
- `mp4hydra`
|
|
||||||
- `vidzee`
|
|
||||||
- `vixsrc`
|
|
||||||
- `xprime`
|
|
||||||
|
|
||||||
You can enable/disable and mark defaults via the Provider Matrix in the UI. If **no defaults** are selected, all enabled providers participate in aggregation.
|
### Current Built-in Providers
|
||||||
|
- `showbox` - Showbox/PStream integration
|
||||||
|
- `4khdhub` - 4KHDHub streams
|
||||||
|
- `moviesmod` - MoviesMod streams
|
||||||
|
- `mp4hydra` - MP4Hydra streams
|
||||||
|
- `vidzee` - VidZee streams
|
||||||
|
- `vixsrc` - Vixsrc streams
|
||||||
|
- `uhdmovies` - UHD Movies streams
|
||||||
|
|
||||||
### Adding a New Provider (Outline)
|
|
||||||
1. Create `providers/myprovider.js` exporting an async fetch function (see existing files for patterns).
|
### Adding a New Provider
|
||||||
2. Lazy‑load it in `providers/registry.js` similar to others.
|
1. **Create** `providers/yourprovider.js` with your stream fetching logic
|
||||||
3. Add a config flag (`enableMyproviderProvider`) if you want toggle support.
|
2. **Export** a function like `getYourproviderStreams(tmdbId, mediaType, season, episode)`
|
||||||
4. Update the provider matrix arrays in `public/index.js`.
|
3. **Register** it in `providers/registry.js` → `providerFunctionMap`:
|
||||||
|
```js
|
||||||
|
// providers/registry.js
|
||||||
|
const providerFunctionMap = {
|
||||||
|
'Showbox.js': 'getStreamsFromTmdbId',
|
||||||
|
'4khdhub.js': 'get4KHDHubStreams',
|
||||||
|
'moviesmod.js': 'getMoviesModStreams',
|
||||||
|
'MP4Hydra.js': 'getMP4HydraStreams',
|
||||||
|
'VidZee.js': 'getVidZeeStreams',
|
||||||
|
'vixsrc.js': 'getVixsrcStreams',
|
||||||
|
'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!
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -238,23 +300,34 @@ Aggregate endpoint auto-resolves IMDb when needed and merges provider output bef
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🧪 Stream Object Shape (Typical)
|
## 🧪 Stream Object Schema (Unified)
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"title": "Example Title",
|
"title": "Fight Club - 1080p [MP4Hydra #2]",
|
||||||
"url": "https://...",
|
"url": "https://stream.url/video.mp4",
|
||||||
"quality": "1080p",
|
"quality": "1080p",
|
||||||
"provider": "showbox",
|
"provider": "mp4hydra",
|
||||||
"size": null,
|
"headers": { "User-Agent": "Mozilla/5.0" }
|
||||||
"languages": ["en"],
|
|
||||||
"subtitles": [],
|
|
||||||
"codecs": ["H264"],
|
|
||||||
"headers": { "Referer": "..." }
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
+27
-3
@@ -3,14 +3,23 @@ const express = require('express');
|
|||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
const { config, saveConfigPatch, OVERRIDE_PATH } = require('./utils/config');
|
const { config, saveConfigPatch, OVERRIDE_PATH } = require('./utils/config');
|
||||||
const { authenticate, issueSession, requireAuth, getSession, updatePassword, AUTH_FILE } = require('./utils/auth');
|
const { authenticate, issueSession, requireAuth, getSession, updatePassword } = require('./utils/auth');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { listProviders, getProvider, getCookieStats } = require('./providers/registry');
|
const { listProviders, getProvider, getCookieStats } = require('./providers/registry');
|
||||||
|
const { createProxyRoutes, processStreamsForProxy } = require('./proxy/proxyServer');
|
||||||
const { resolveImdbId } = require('./utils/tmdb');
|
const { resolveImdbId } = require('./utils/tmdb');
|
||||||
const { applyFilters } = require('./utils/streamFilters');
|
const { applyFilters } = require('./utils/streamFilters');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
|
// Conditionally mount proxy routes early so downstream handlers can use them
|
||||||
|
if (config.enableProxy) {
|
||||||
|
console.log('[startup] enableProxy flag active: mounting proxy routes');
|
||||||
|
createProxyRoutes(app);
|
||||||
|
} else {
|
||||||
|
console.log('[startup] enableProxy flag disabled: proxy routes not mounted');
|
||||||
|
}
|
||||||
|
|
||||||
// --- Simple In-Memory Rate Limiting for /auth/login ---
|
// --- Simple In-Memory Rate Limiting for /auth/login ---
|
||||||
const loginAttempts = new Map(); // key: ip, value: { count, first, last, lockedUntil }
|
const loginAttempts = new Map(); // key: ip, value: { count, first, last, lockedUntil }
|
||||||
const MAX_ATTEMPTS_WINDOW = 5; // attempts allowed
|
const MAX_ATTEMPTS_WINDOW = 5; // attempts allowed
|
||||||
@@ -340,6 +349,12 @@ app.get('/api/streams/:type/:tmdbId', async (req,res) => {
|
|||||||
let streams = results.flat();
|
let streams = results.flat();
|
||||||
streams = applyFilters(streams, 'aggregate', config.minQualities, config.excludeCodecs);
|
streams = applyFilters(streams, 'aggregate', config.minQualities, config.excludeCodecs);
|
||||||
metrics.streamsReturned += streams.length;
|
metrics.streamsReturned += streams.length;
|
||||||
|
if (config.enableProxy) {
|
||||||
|
const serverUrl = `${req.protocol}://${req.get('host')}`;
|
||||||
|
streams = processStreamsForProxy(streams, serverUrl);
|
||||||
|
// Omit original headers when proxying to avoid leaking upstream requirements
|
||||||
|
streams = streams.map(s => { if (s && typeof s === 'object') { const { headers, ...rest } = s; return rest; } return s; });
|
||||||
|
}
|
||||||
res.json({ success:true, tmdbId, imdbId, count: streams.length, providerTimings, streams });
|
res.json({ success:true, tmdbId, imdbId, count: streams.length, providerTimings, streams });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
metrics.lastError = e.message;
|
metrics.lastError = e.message;
|
||||||
@@ -366,6 +381,11 @@ app.get('/api/streams/:provider/:type/:tmdbId', async (req,res) => {
|
|||||||
const providerTimings = { [prov.name]: Date.now()-t0 };
|
const providerTimings = { [prov.name]: Date.now()-t0 };
|
||||||
streams = applyFilters(streams, prov.name, config.minQualities, config.excludeCodecs);
|
streams = applyFilters(streams, prov.name, config.minQualities, config.excludeCodecs);
|
||||||
metrics.streamsReturned += streams.length;
|
metrics.streamsReturned += streams.length;
|
||||||
|
if (config.enableProxy) {
|
||||||
|
const serverUrl = `${req.protocol}://${req.get('host')}`;
|
||||||
|
streams = processStreamsForProxy(streams, serverUrl);
|
||||||
|
streams = streams.map(s => { if (s && typeof s === 'object') { const { headers, ...rest } = s; return rest; } return s; });
|
||||||
|
}
|
||||||
res.json({ success:true, provider: prov.name, tmdbId, imdbId, count: streams.length, providerTimings, streams });
|
res.json({ success:true, provider: prov.name, tmdbId, imdbId, count: streams.length, providerTimings, streams });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
metrics.lastError = e.message;
|
metrics.lastError = e.message;
|
||||||
@@ -374,8 +394,12 @@ app.get('/api/streams/:provider/:type/:tmdbId', async (req,res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const PORT = config.port;
|
const PORT = config.port;
|
||||||
const server = app.listen(PORT, () => {
|
const HOST = process.env.BIND_HOST || '0.0.0.0';
|
||||||
console.log(`TMDB Embed REST API listening on http://localhost:${PORT}`);
|
const server = app.listen(PORT, HOST, () => {
|
||||||
|
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,6 +9,7 @@ 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:
|
||||||
|
|||||||
Generated
+13
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "tmdb-embed-api",
|
"name": "tmdb-embed-api",
|
||||||
"version": "1.0.0",
|
"version": "1.0.8",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "tmdb-embed-api",
|
"name": "tmdb-embed-api",
|
||||||
"version": "1.0.0",
|
"version": "1.0.8",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"aromanize": "^0.1.5",
|
"aromanize": "^0.1.5",
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
@@ -674,6 +674,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/axios-cookiejar-support/node_modules/undici": {
|
||||||
|
"version": "7.16.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz",
|
||||||
|
"integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.18.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/b4a": {
|
"node_modules/b4a": {
|
||||||
"version": "1.6.7",
|
"version": "1.6.7",
|
||||||
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
|
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "tmdb-embed-api",
|
"name": "tmdb-embed-api",
|
||||||
"version": "1.0.1",
|
"version": "1.0.8",
|
||||||
"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": {
|
||||||
|
|||||||
+445
-346
File diff suppressed because it is too large
Load Diff
+88
-106
@@ -16,7 +16,7 @@ function processEpisode(episode, baseServer, serverName, serverNumber) {
|
|||||||
label: sub.label,
|
label: sub.label,
|
||||||
url: `${baseServer}${sub.src}`
|
url: `${baseServer}${sub.src}`
|
||||||
})) : [];
|
})) : [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: episode.show_title || episode.title,
|
title: episode.show_title || episode.title,
|
||||||
episode: episode.title,
|
episode: episode.title,
|
||||||
@@ -33,23 +33,23 @@ function processEpisode(episode, baseServer, serverName, serverNumber) {
|
|||||||
async function getMP4HydraStreams(tmdbId, mediaType, seasonNum = null, episodeNum = null) {
|
async function getMP4HydraStreams(tmdbId, mediaType, seasonNum = null, episodeNum = null) {
|
||||||
try {
|
try {
|
||||||
console.log(`[MP4Hydra] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}, Season: ${seasonNum}, Episode: ${episodeNum}`);
|
console.log(`[MP4Hydra] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}, Season: ${seasonNum}, Episode: ${episodeNum}`);
|
||||||
|
|
||||||
// Get TMDB details
|
// Get TMDB details
|
||||||
const details = await getTMDBDetails(tmdbId, mediaType);
|
const details = await getTMDBDetails(tmdbId, mediaType);
|
||||||
if (!details) {
|
if (!details) {
|
||||||
console.log(`[MP4Hydra] Could not fetch details for TMDB ID: ${tmdbId}`);
|
console.log(`[MP4Hydra] Could not fetch details for TMDB ID: ${tmdbId}`);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[MP4Hydra] Found title: ${details.title} (${details.year})`);
|
console.log(`[MP4Hydra] Found title: ${details.title} (${details.year})`);
|
||||||
|
|
||||||
// Generate slug in the format "movie-name-year"
|
// Generate slug in the format "movie-name-year"
|
||||||
let slug = details.slug;
|
let slug = details.slug;
|
||||||
if (mediaType === 'movie' && details.year) {
|
if (mediaType === 'movie' && details.year) {
|
||||||
slug = `${details.slug}-${details.year}`;
|
slug = `${details.slug}-${details.year}`;
|
||||||
}
|
}
|
||||||
console.log(`[MP4Hydra] Using slug: ${slug}`);
|
console.log(`[MP4Hydra] Using slug: ${slug}`);
|
||||||
|
|
||||||
// Create form data for multipart/form-data request
|
// Create form data for multipart/form-data request
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('v', '8');
|
formData.append('v', '8');
|
||||||
@@ -59,7 +59,7 @@ async function getMP4HydraStreams(tmdbId, mediaType, seasonNum = null, episodeNu
|
|||||||
se: seasonNum,
|
se: seasonNum,
|
||||||
ep: episodeNum
|
ep: episodeNum
|
||||||
}]));
|
}]));
|
||||||
|
|
||||||
// Make request to MP4Hydra API
|
// Make request to MP4Hydra API
|
||||||
const response = await axios({
|
const response = await axios({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
@@ -78,64 +78,61 @@ async function getMP4HydraStreams(tmdbId, mediaType, seasonNum = null, episodeNu
|
|||||||
},
|
},
|
||||||
timeout: 10000
|
timeout: 10000
|
||||||
});
|
});
|
||||||
|
|
||||||
// Process response
|
// Process response
|
||||||
if (response.data && response.data.playlist && response.data.playlist.length > 0) {
|
if (response.data && response.data.playlist && response.data.playlist.length > 0) {
|
||||||
const playlist = response.data.playlist;
|
const playlist = response.data.playlist;
|
||||||
const servers = response.data.servers;
|
const servers = response.data.servers;
|
||||||
|
|
||||||
console.log(`[MP4Hydra] Found ${playlist.length} videos`);
|
console.log(`[MP4Hydra] Found ${playlist.length} videos`);
|
||||||
console.log(`[MP4Hydra] Available servers: ${Object.keys(servers).join(', ')}`);
|
console.log(`[MP4Hydra] Available servers: ${Object.keys(servers).join(', ')}`);
|
||||||
|
|
||||||
// For TV shows, find the specific episode
|
// For TV shows, find the specific episode
|
||||||
if (mediaType === 'tv' && seasonNum && episodeNum) {
|
if (mediaType === 'tv' && seasonNum && episodeNum) {
|
||||||
const paddedSeason = seasonNum.toString().padStart(2, '0');
|
const paddedSeason = seasonNum.toString().padStart(2, '0');
|
||||||
const paddedEpisode = episodeNum.toString().padStart(2, '0');
|
const paddedEpisode = episodeNum.toString().padStart(2, '0');
|
||||||
const seasonEpisode = `S${paddedSeason}E${paddedEpisode}`;
|
const seasonEpisode = `S${paddedSeason}E${paddedEpisode}`;
|
||||||
|
|
||||||
const targetEpisode = playlist.find(item =>
|
const targetEpisode = playlist.find(item =>
|
||||||
item.title && item.title.toUpperCase() === seasonEpisode.toUpperCase()
|
item.title && item.title.toUpperCase() === seasonEpisode.toUpperCase()
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!targetEpisode) {
|
if (!targetEpisode) {
|
||||||
console.log(`[MP4Hydra] Could not find ${seasonEpisode}`);
|
console.log(`[MP4Hydra] Could not find ${seasonEpisode}`);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[MP4Hydra] Found episode: ${targetEpisode.show_title || targetEpisode.title}`);
|
console.log(`[MP4Hydra] Found episode: ${targetEpisode.show_title || targetEpisode.title}`);
|
||||||
|
|
||||||
// Process streams from main servers
|
// Process streams from main servers
|
||||||
const streams = [];
|
const streams = [];
|
||||||
const serverConfig = [
|
const serverConfig = [
|
||||||
{ name: 'Beta', number: '#1' },
|
{ name: 'Beta', number: '#1' },
|
||||||
{ name: 'Beta#3', number: '#2' }
|
{ name: 'Beta#3', number: '#2' }
|
||||||
];
|
];
|
||||||
|
|
||||||
serverConfig.forEach(server => {
|
serverConfig.forEach(server => {
|
||||||
const serverName = server.name;
|
const serverName = server.name;
|
||||||
const serverNumber = server.number;
|
const serverNumber = server.number;
|
||||||
|
|
||||||
if (servers[serverName]) {
|
if (servers[serverName]) {
|
||||||
const baseServer = servers[serverName];
|
const baseServer = servers[serverName];
|
||||||
console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`);
|
console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`);
|
||||||
|
|
||||||
const processedEpisode = processEpisode(targetEpisode, baseServer, serverName, serverNumber);
|
const processedEpisode = processEpisode(targetEpisode, baseServer, serverName, serverNumber);
|
||||||
|
|
||||||
// Convert to standard stream format
|
// Convert to standard stream format
|
||||||
streams.push({
|
streams.push({
|
||||||
title: `${details.title} - ${seasonEpisode} - ${processedEpisode.quality} [MP4Hydra ${serverNumber}]`,
|
title: `${details.title} - ${seasonEpisode} - ${processedEpisode.quality} [MP4Hydra ${serverNumber}]`,
|
||||||
url: processedEpisode.videoUrl,
|
url: processedEpisode.videoUrl,
|
||||||
quality: processedEpisode.quality,
|
quality: processedEpisode.quality,
|
||||||
provider: "MP4Hydra",
|
provider: "mp4hydra",
|
||||||
size: "Unknown size",
|
headers: {
|
||||||
behaviorHints: {
|
'Referer': 'https://mp4hydra.org/'
|
||||||
notWebReady: true,
|
|
||||||
headers: {
|
|
||||||
'Referer': 'https://mp4hydra.org/'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add subtitle tracks if available
|
// Add subtitle tracks if available
|
||||||
if (processedEpisode.subtitles && processedEpisode.subtitles.length > 0) {
|
if (processedEpisode.subtitles && processedEpisode.subtitles.length > 0) {
|
||||||
streams[streams.length - 1].subtitles = processedEpisode.subtitles.map(sub => ({
|
streams[streams.length - 1].subtitles = processedEpisode.subtitles.map(sub => ({
|
||||||
@@ -145,43 +142,40 @@ async function getMP4HydraStreams(tmdbId, mediaType, seasonNum = null, episodeNu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return streams;
|
return streams;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For movies, process all videos
|
// For movies, process all videos
|
||||||
const streams = [];
|
const streams = [];
|
||||||
const serverConfig = [
|
const serverConfig = [
|
||||||
{ name: 'Beta', number: '#1' },
|
{ name: 'Beta', number: '#1' },
|
||||||
{ name: 'Beta#3', number: '#2' }
|
{ name: 'Beta#3', number: '#2' }
|
||||||
];
|
];
|
||||||
|
|
||||||
serverConfig.forEach(server => {
|
serverConfig.forEach(server => {
|
||||||
const serverName = server.name;
|
const serverName = server.name;
|
||||||
const serverNumber = server.number;
|
const serverNumber = server.number;
|
||||||
|
|
||||||
if (servers[serverName]) {
|
if (servers[serverName]) {
|
||||||
const baseServer = servers[serverName];
|
const baseServer = servers[serverName];
|
||||||
console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`);
|
console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`);
|
||||||
|
|
||||||
playlist.forEach(item => {
|
playlist.forEach(item => {
|
||||||
const processedItem = processEpisode(item, baseServer, serverName, serverNumber);
|
const processedItem = processEpisode(item, baseServer, serverName, serverNumber);
|
||||||
|
|
||||||
// Convert to standard stream format
|
// Convert to standard stream format
|
||||||
streams.push({
|
streams.push({
|
||||||
title: `${details.title} - ${processedItem.quality} [MP4Hydra ${serverNumber}]`,
|
title: `${details.title} - ${processedItem.quality} [MP4Hydra ${serverNumber}]`,
|
||||||
url: processedItem.videoUrl,
|
url: processedItem.videoUrl,
|
||||||
quality: processedItem.quality,
|
quality: processedItem.quality,
|
||||||
provider: "MP4Hydra",
|
provider: "mp4hydra",
|
||||||
size: "Unknown size",
|
headers: {
|
||||||
behaviorHints: {
|
'Referer': 'https://mp4hydra.org/'
|
||||||
notWebReady: true,
|
|
||||||
headers: {
|
|
||||||
'Referer': 'https://mp4hydra.org/'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add subtitle tracks if available
|
// Add subtitle tracks if available
|
||||||
if (processedItem.subtitles && processedItem.subtitles.length > 0) {
|
if (processedItem.subtitles && processedItem.subtitles.length > 0) {
|
||||||
streams[streams.length - 1].subtitles = processedItem.subtitles.map(sub => ({
|
streams[streams.length - 1].subtitles = processedItem.subtitles.map(sub => ({
|
||||||
@@ -192,19 +186,19 @@ async function getMP4HydraStreams(tmdbId, mediaType, seasonNum = null, episodeNu
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return streams;
|
return streams;
|
||||||
} else {
|
} else {
|
||||||
// Try with original title if the first attempt failed
|
// Try with original title if the first attempt failed
|
||||||
if (details.title !== details.original_title) {
|
if (details.title !== details.original_title) {
|
||||||
return await tryAlternativeTitle(details, mediaType, seasonNum, episodeNum);
|
return await tryAlternativeTitle(details, mediaType, seasonNum, episodeNum);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try without year for movies
|
// Try without year for movies
|
||||||
if (mediaType === 'movie' && details.year) {
|
if (mediaType === 'movie' && details.year) {
|
||||||
return await tryWithoutYear(details, mediaType, seasonNum, episodeNum);
|
return await tryWithoutYear(details, mediaType, seasonNum, episodeNum);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[MP4Hydra] No streaming links found');
|
console.log('[MP4Hydra] No streaming links found');
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -220,11 +214,11 @@ async function tryAlternativeTitle(details, mediaType, seasonNum, episodeNum) {
|
|||||||
console.log(`[MP4Hydra] Retrying with original title: ${details.original_title}`);
|
console.log(`[MP4Hydra] Retrying with original title: ${details.original_title}`);
|
||||||
const originalSlug = generateSlug(details.original_title);
|
const originalSlug = generateSlug(details.original_title);
|
||||||
let originalFullSlug = originalSlug;
|
let originalFullSlug = originalSlug;
|
||||||
|
|
||||||
if (mediaType === 'movie' && details.year) {
|
if (mediaType === 'movie' && details.year) {
|
||||||
originalFullSlug = `${originalSlug}-${details.year}`;
|
originalFullSlug = `${originalSlug}-${details.year}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new form data with original title
|
// Create new form data with original title
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('v', '8');
|
formData.append('v', '8');
|
||||||
@@ -234,7 +228,7 @@ async function tryAlternativeTitle(details, mediaType, seasonNum, episodeNum) {
|
|||||||
se: seasonNum,
|
se: seasonNum,
|
||||||
ep: episodeNum
|
ep: episodeNum
|
||||||
}]));
|
}]));
|
||||||
|
|
||||||
const response = await axios({
|
const response = await axios({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
url: 'https://mp4hydra.org/info2?v=8',
|
url: 'https://mp4hydra.org/info2?v=8',
|
||||||
@@ -252,61 +246,58 @@ async function tryAlternativeTitle(details, mediaType, seasonNum, episodeNum) {
|
|||||||
},
|
},
|
||||||
timeout: 10000
|
timeout: 10000
|
||||||
});
|
});
|
||||||
|
|
||||||
// Process response
|
// Process response
|
||||||
if (response.data && response.data.playlist && response.data.playlist.length > 0) {
|
if (response.data && response.data.playlist && response.data.playlist.length > 0) {
|
||||||
const playlist = response.data.playlist;
|
const playlist = response.data.playlist;
|
||||||
const servers = response.data.servers;
|
const servers = response.data.servers;
|
||||||
|
|
||||||
console.log(`[MP4Hydra] Found ${playlist.length} videos with original title`);
|
console.log(`[MP4Hydra] Found ${playlist.length} videos with original title`);
|
||||||
|
|
||||||
// For TV shows, find the specific episode
|
// For TV shows, find the specific episode
|
||||||
if (mediaType === 'tv' && seasonNum && episodeNum) {
|
if (mediaType === 'tv' && seasonNum && episodeNum) {
|
||||||
const paddedSeason = seasonNum.toString().padStart(2, '0');
|
const paddedSeason = seasonNum.toString().padStart(2, '0');
|
||||||
const paddedEpisode = episodeNum.toString().padStart(2, '0');
|
const paddedEpisode = episodeNum.toString().padStart(2, '0');
|
||||||
const seasonEpisode = `S${paddedSeason}E${paddedEpisode}`;
|
const seasonEpisode = `S${paddedSeason}E${paddedEpisode}`;
|
||||||
|
|
||||||
const targetEpisode = playlist.find(item =>
|
const targetEpisode = playlist.find(item =>
|
||||||
item.title && item.title.toUpperCase() === seasonEpisode.toUpperCase()
|
item.title && item.title.toUpperCase() === seasonEpisode.toUpperCase()
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!targetEpisode) {
|
if (!targetEpisode) {
|
||||||
console.log(`[MP4Hydra] Could not find ${seasonEpisode} with original title`);
|
console.log(`[MP4Hydra] Could not find ${seasonEpisode} with original title`);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process streams from main servers
|
// Process streams from main servers
|
||||||
const streams = [];
|
const streams = [];
|
||||||
const serverConfig = [
|
const serverConfig = [
|
||||||
{ name: 'Beta', number: '#1' },
|
{ name: 'Beta', number: '#1' },
|
||||||
{ name: 'Beta#3', number: '#2' }
|
{ name: 'Beta#3', number: '#2' }
|
||||||
];
|
];
|
||||||
|
|
||||||
serverConfig.forEach(server => {
|
serverConfig.forEach(server => {
|
||||||
const serverName = server.name;
|
const serverName = server.name;
|
||||||
const serverNumber = server.number;
|
const serverNumber = server.number;
|
||||||
|
|
||||||
if (servers[serverName]) {
|
if (servers[serverName]) {
|
||||||
const baseServer = servers[serverName];
|
const baseServer = servers[serverName];
|
||||||
console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`);
|
console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`);
|
||||||
|
|
||||||
const processedEpisode = processEpisode(targetEpisode, baseServer, serverName, serverNumber);
|
const processedEpisode = processEpisode(targetEpisode, baseServer, serverName, serverNumber);
|
||||||
|
|
||||||
// Convert to standard stream format
|
// Convert to standard stream format
|
||||||
streams.push({
|
streams.push({
|
||||||
title: `${details.original_title} - ${seasonEpisode} - ${processedEpisode.quality} [MP4Hydra ${serverNumber}]`,
|
title: `${details.original_title} - ${seasonEpisode} - ${processedEpisode.quality} [MP4Hydra ${serverNumber}]`,
|
||||||
url: processedEpisode.videoUrl,
|
url: processedEpisode.videoUrl,
|
||||||
quality: processedEpisode.quality,
|
quality: processedEpisode.quality,
|
||||||
provider: "MP4Hydra",
|
provider: "mp4hydra",
|
||||||
size: "Unknown size",
|
headers: {
|
||||||
behaviorHints: {
|
'Referer': 'https://mp4hydra.org/'
|
||||||
notWebReady: true,
|
|
||||||
headers: {
|
|
||||||
'Referer': 'https://mp4hydra.org/'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add subtitle tracks if available
|
// Add subtitle tracks if available
|
||||||
if (processedEpisode.subtitles && processedEpisode.subtitles.length > 0) {
|
if (processedEpisode.subtitles && processedEpisode.subtitles.length > 0) {
|
||||||
streams[streams.length - 1].subtitles = processedEpisode.subtitles.map(sub => ({
|
streams[streams.length - 1].subtitles = processedEpisode.subtitles.map(sub => ({
|
||||||
@@ -316,43 +307,36 @@ async function tryAlternativeTitle(details, mediaType, seasonNum, episodeNum) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return streams;
|
return streams;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For movies, process all videos
|
// For movies, process all videos
|
||||||
const streams = [];
|
const streams = [];
|
||||||
const serverConfig = [
|
const serverConfig = [
|
||||||
{ name: 'Beta', number: '#1' },
|
{ name: 'Beta', number: '#1' },
|
||||||
{ name: 'Beta#3', number: '#2' }
|
{ name: 'Beta#3', number: '#2' }
|
||||||
];
|
];
|
||||||
|
|
||||||
serverConfig.forEach(server => {
|
serverConfig.forEach(server => {
|
||||||
const serverName = server.name;
|
const serverName = server.name;
|
||||||
const serverNumber = server.number;
|
const serverNumber = server.number;
|
||||||
|
|
||||||
if (servers[serverName]) {
|
if (servers[serverName]) {
|
||||||
const baseServer = servers[serverName];
|
const baseServer = servers[serverName];
|
||||||
console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`);
|
console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`);
|
||||||
|
|
||||||
playlist.forEach(item => {
|
playlist.forEach(item => {
|
||||||
const processedItem = processEpisode(item, baseServer, serverName, serverNumber);
|
const processedItem = processEpisode(item, baseServer, serverName, serverNumber);
|
||||||
|
|
||||||
// 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
|
||||||
if (processedItem.subtitles && processedItem.subtitles.length > 0) {
|
if (processedItem.subtitles && processedItem.subtitles.length > 0) {
|
||||||
streams[streams.length - 1].subtitles = processedItem.subtitles.map(sub => ({
|
streams[streams.length - 1].subtitles = processedItem.subtitles.map(sub => ({
|
||||||
@@ -363,10 +347,10 @@ async function tryAlternativeTitle(details, mediaType, seasonNum, episodeNum) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return streams;
|
return streams;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[MP4Hydra] Error with alternative title:`, error.message);
|
console.error(`[MP4Hydra] Error with alternative title:`, error.message);
|
||||||
@@ -379,7 +363,7 @@ async function tryWithoutYear(details, mediaType, seasonNum, episodeNum) {
|
|||||||
try {
|
try {
|
||||||
console.log(`[MP4Hydra] Retrying with title only (without year): ${details.title}`);
|
console.log(`[MP4Hydra] Retrying with title only (without year): ${details.title}`);
|
||||||
const titleOnlySlug = details.slug;
|
const titleOnlySlug = details.slug;
|
||||||
|
|
||||||
// Create new form data with title only
|
// Create new form data with title only
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('v', '8');
|
formData.append('v', '8');
|
||||||
@@ -389,7 +373,7 @@ async function tryWithoutYear(details, mediaType, seasonNum, episodeNum) {
|
|||||||
se: seasonNum,
|
se: seasonNum,
|
||||||
ep: episodeNum
|
ep: episodeNum
|
||||||
}]));
|
}]));
|
||||||
|
|
||||||
const response = await axios({
|
const response = await axios({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
url: 'https://mp4hydra.org/info2?v=8',
|
url: 'https://mp4hydra.org/info2?v=8',
|
||||||
@@ -407,47 +391,45 @@ async function tryWithoutYear(details, mediaType, seasonNum, episodeNum) {
|
|||||||
},
|
},
|
||||||
timeout: 10000
|
timeout: 10000
|
||||||
});
|
});
|
||||||
|
|
||||||
// Process response
|
// Process response
|
||||||
if (response.data && response.data.playlist && response.data.playlist.length > 0) {
|
if (response.data && response.data.playlist && response.data.playlist.length > 0) {
|
||||||
const playlist = response.data.playlist;
|
const playlist = response.data.playlist;
|
||||||
const servers = response.data.servers;
|
const servers = response.data.servers;
|
||||||
|
|
||||||
console.log(`[MP4Hydra] Found ${playlist.length} videos with title only`);
|
console.log(`[MP4Hydra] Found ${playlist.length} videos with title only`);
|
||||||
|
|
||||||
// For movies, process all videos
|
// For movies, process all videos
|
||||||
const streams = [];
|
const streams = [];
|
||||||
const serverConfig = [
|
const serverConfig = [
|
||||||
{ name: 'Beta', number: '#1' },
|
{ name: 'Beta', number: '#1' },
|
||||||
{ name: 'Beta#3', number: '#2' }
|
{ name: 'Beta#3', number: '#2' }
|
||||||
];
|
];
|
||||||
|
|
||||||
serverConfig.forEach(server => {
|
serverConfig.forEach(server => {
|
||||||
const serverName = server.name;
|
const serverName = server.name;
|
||||||
const serverNumber = server.number;
|
const serverNumber = server.number;
|
||||||
|
|
||||||
if (servers[serverName]) {
|
if (servers[serverName]) {
|
||||||
const baseServer = servers[serverName];
|
const baseServer = servers[serverName];
|
||||||
console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`);
|
console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`);
|
||||||
|
|
||||||
playlist.forEach(item => {
|
playlist.forEach(item => {
|
||||||
const processedItem = processEpisode(item, baseServer, serverName, serverNumber);
|
const processedItem = processEpisode(item, baseServer, serverName, serverNumber);
|
||||||
|
|
||||||
// Convert to standard stream format
|
// Convert to standard stream format
|
||||||
streams.push({
|
streams.push({
|
||||||
|
name: `${details.title}`,
|
||||||
title: `${details.title} - ${processedItem.quality} [MP4Hydra ${serverNumber}]`,
|
title: `${details.title} - ${processedItem.quality} [MP4Hydra ${serverNumber}]`,
|
||||||
url: processedItem.videoUrl,
|
url: processedItem.videoUrl,
|
||||||
quality: processedItem.quality,
|
quality: processedItem.quality,
|
||||||
provider: "MP4Hydra",
|
provider: "mp4hydra",
|
||||||
size: "Unknown size",
|
headers: {
|
||||||
behaviorHints: {
|
'Referer': 'https://mp4hydra.org/'
|
||||||
notWebReady: true,
|
|
||||||
headers: {
|
|
||||||
'Referer': 'https://mp4hydra.org/'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add subtitle tracks if available
|
// Add subtitle tracks if available
|
||||||
if (processedItem.subtitles && processedItem.subtitles.length > 0) {
|
if (processedItem.subtitles && processedItem.subtitles.length > 0) {
|
||||||
streams[streams.length - 1].subtitles = processedItem.subtitles.map(sub => ({
|
streams[streams.length - 1].subtitles = processedItem.subtitles.map(sub => ({
|
||||||
@@ -458,10 +440,10 @@ async function tryWithoutYear(details, mediaType, seasonNum, episodeNum) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return streams;
|
return streams;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[MP4Hydra] Error with title-only:`, error.message);
|
console.error(`[MP4Hydra] Error with title-only:`, error.message);
|
||||||
@@ -473,15 +455,15 @@ async function tryWithoutYear(details, mediaType, seasonNum, episodeNum) {
|
|||||||
async function getTMDBDetails(tmdbId, mediaType) {
|
async function getTMDBDetails(tmdbId, mediaType) {
|
||||||
const { getTmdbApiKey } = require('../utils/tmdbKey');
|
const { getTmdbApiKey } = require('../utils/tmdbKey');
|
||||||
const TMDB_API_KEY = getTmdbApiKey();
|
const TMDB_API_KEY = getTmdbApiKey();
|
||||||
if(!TMDB_API_KEY){
|
if (!TMDB_API_KEY) {
|
||||||
console.warn('[MP4Hydra] TMDB API key missing; skipping TMDB metadata fetch');
|
console.warn('[MP4Hydra] TMDB API key missing; skipping TMDB metadata fetch');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(`[MP4Hydra] Fetching ${mediaType} details for TMDB ID: ${tmdbId}`);
|
console.log(`[MP4Hydra] Fetching ${mediaType} details for TMDB ID: ${tmdbId}`);
|
||||||
const response = await axios.get(`https://api.themoviedb.org/3/${mediaType}/${tmdbId}?api_key=${TMDB_API_KEY}`);
|
const response = await axios.get(`https://api.themoviedb.org/3/${mediaType}/${tmdbId}?api_key=${TMDB_API_KEY}`);
|
||||||
|
|
||||||
if (mediaType === 'movie') {
|
if (mediaType === 'movie') {
|
||||||
return {
|
return {
|
||||||
title: response.data.title,
|
title: response.data.title,
|
||||||
|
|||||||
+141
-138
@@ -86,7 +86,7 @@ const getCookieForRequest = async (regionPreference = null, userCookie = null) =
|
|||||||
console.log('[CookieManager] Using user-supplied cookie from global state (legacy mode) for this cycle.');
|
console.log('[CookieManager] Using user-supplied cookie from global state (legacy mode) for this cycle.');
|
||||||
baseCookieToUse = global.currentRequestUserCookie;
|
baseCookieToUse = global.currentRequestUserCookie;
|
||||||
}
|
}
|
||||||
// No file-based fallback rotation (env-only)
|
// No file-based fallback rotation (env-only)
|
||||||
else {
|
else {
|
||||||
console.log('[CookieManager] No user-supplied cookies available for this cycle.');
|
console.log('[CookieManager] No user-supplied cookies available for this cycle.');
|
||||||
baseCookieToUse = '';
|
baseCookieToUse = '';
|
||||||
@@ -156,9 +156,9 @@ const FEBBOX_FILE_SHARE_LIST_URL = "https://www.febbox.com/file/file_share_list"
|
|||||||
// const SCRAPER_API_URL = 'https://api.scraperapi.com/';
|
// const SCRAPER_API_URL = 'https://api.scraperapi.com/';
|
||||||
// TMDB key now sourced from config/env; no hardcoded public key
|
// TMDB key now sourced from config/env; no hardcoded public key
|
||||||
const { getTmdbApiKey } = require('../utils/tmdbKey');
|
const { getTmdbApiKey } = require('../utils/tmdbKey');
|
||||||
function obtainTmdbKey(){
|
function obtainTmdbKey() {
|
||||||
const k = getTmdbApiKey();
|
const k = getTmdbApiKey();
|
||||||
if(!k) console.warn('[Showbox] No TMDB API key available (multi-key set empty).');
|
if (!k) console.warn('[Showbox] No TMDB API key available (multi-key set empty).');
|
||||||
return k;
|
return k;
|
||||||
}
|
}
|
||||||
const TMDB_BASE_URL = 'https://api.themoviedb.org/3';
|
const TMDB_BASE_URL = 'https://api.themoviedb.org/3';
|
||||||
@@ -534,9 +534,9 @@ const _searchAndExtractShowboxUrl = async (searchTerm, originalTmdbTitle, mediaY
|
|||||||
if (cachedResult === 'NO_MATCH_FOUND') return { url: null, score: -1 };
|
if (cachedResult === 'NO_MATCH_FOUND') return { url: null, score: -1 };
|
||||||
return { url: cachedResult, score: 10 };
|
return { url: cachedResult, score: 10 };
|
||||||
} else if (cachedResult.result === 'NO_MATCH_FOUND') {
|
} else if (cachedResult.result === 'NO_MATCH_FOUND') {
|
||||||
// For stream searches, always retry immediately - content availability can change anytime
|
// For stream searches, always retry immediately - content availability can change anytime
|
||||||
console.log(` CACHE HIT: Previously no match found for ${originalTmdbTitle} ${mediaYear || ''}, but retrying immediately for fresh content.`);
|
console.log(` CACHE HIT: Previously no match found for ${originalTmdbTitle} ${mediaYear || ''}, but retrying immediately for fresh content.`);
|
||||||
// Continue to search logic below to retry immediately
|
// Continue to search logic below to retry immediately
|
||||||
} else {
|
} else {
|
||||||
console.log(` CACHE HIT for ShowBox search best match URL (${originalTmdbTitle} ${mediaYear || ''}): ${cachedResult}`);
|
console.log(` CACHE HIT for ShowBox search best match URL (${originalTmdbTitle} ${mediaYear || ''}): ${cachedResult}`);
|
||||||
return { url: cachedResult, score: 10 };
|
return { url: cachedResult, score: 10 };
|
||||||
@@ -819,7 +819,7 @@ const _searchAndExtractShowboxUrl = async (searchTerm, originalTmdbTitle, mediaY
|
|||||||
// Allow for very close matches (like plurals or slight variations)
|
// Allow for very close matches (like plurals or slight variations)
|
||||||
if (tmdbWord.length > 3 && itemWord.length > 3) {
|
if (tmdbWord.length > 3 && itemWord.length > 3) {
|
||||||
return (itemWord.includes(tmdbWord) && itemWord.length - tmdbWord.length <= 2) ||
|
return (itemWord.includes(tmdbWord) && itemWord.length - tmdbWord.length <= 2) ||
|
||||||
(tmdbWord.includes(itemWord) && tmdbWord.length - itemWord.length <= 2);
|
(tmdbWord.includes(itemWord) && tmdbWord.length - itemWord.length <= 2);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
})) {
|
})) {
|
||||||
@@ -988,8 +988,8 @@ const getShowboxUrlFromTmdbInfo = async (tmdbType, tmdbId, regionPreference = nu
|
|||||||
let tmdbData = await getFromCache(mainCacheKey, mainCacheSubDir);
|
let tmdbData = await getFromCache(mainCacheKey, mainCacheSubDir);
|
||||||
|
|
||||||
if (!tmdbData || process.env.DISABLE_CACHE === 'true') {
|
if (!tmdbData || process.env.DISABLE_CACHE === 'true') {
|
||||||
const TMDB_API_KEY = obtainTmdbKey();
|
const TMDB_API_KEY = obtainTmdbKey();
|
||||||
const tmdbApiUrl = `${TMDB_BASE_URL}/${tmdbType}/${tmdbId}?api_key=${TMDB_API_KEY}`;
|
const tmdbApiUrl = `${TMDB_BASE_URL}/${tmdbType}/${tmdbId}?api_key=${TMDB_API_KEY}`;
|
||||||
console.log(` Fetching TMDB data from: ${tmdbApiUrl}`);
|
console.log(` Fetching TMDB data from: ${tmdbApiUrl}`);
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(tmdbApiUrl, { timeout: 10000 });
|
const response = await axios.get(tmdbApiUrl, { timeout: 10000 });
|
||||||
@@ -1021,8 +1021,8 @@ const getShowboxUrlFromTmdbInfo = async (tmdbType, tmdbId, regionPreference = nu
|
|||||||
let tmdbAlternativeTitlesData = await getFromCache(altTitlesCacheKey, mainCacheSubDir);
|
let tmdbAlternativeTitlesData = await getFromCache(altTitlesCacheKey, mainCacheSubDir);
|
||||||
|
|
||||||
if (!tmdbAlternativeTitlesData || process.env.DISABLE_CACHE === 'true') {
|
if (!tmdbAlternativeTitlesData || process.env.DISABLE_CACHE === 'true') {
|
||||||
const TMDB_API_KEY = obtainTmdbKey();
|
const TMDB_API_KEY = obtainTmdbKey();
|
||||||
const altTitlesApiUrl = `${TMDB_BASE_URL}/${tmdbType}/${tmdbId}/alternative_titles?api_key=${TMDB_API_KEY}`;
|
const altTitlesApiUrl = `${TMDB_BASE_URL}/${tmdbType}/${tmdbId}/alternative_titles?api_key=${TMDB_API_KEY}`;
|
||||||
console.log(` Fetching TMDB alternative titles from: ${altTitlesApiUrl}`);
|
console.log(` Fetching TMDB alternative titles from: ${altTitlesApiUrl}`);
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(altTitlesApiUrl, { timeout: 10000 });
|
const response = await axios.get(altTitlesApiUrl, { timeout: 10000 });
|
||||||
@@ -1047,8 +1047,8 @@ const getShowboxUrlFromTmdbInfo = async (tmdbType, tmdbId, regionPreference = nu
|
|||||||
let tmdbBackdropPaths = [];
|
let tmdbBackdropPaths = [];
|
||||||
|
|
||||||
if (!tmdbImagesData || process.env.DISABLE_CACHE === 'true') {
|
if (!tmdbImagesData || process.env.DISABLE_CACHE === 'true') {
|
||||||
const TMDB_API_KEY = obtainTmdbKey();
|
const TMDB_API_KEY = obtainTmdbKey();
|
||||||
const imagesApiUrl = `${TMDB_BASE_URL}/${tmdbType}/${tmdbId}/images?api_key=${TMDB_API_KEY}`;
|
const imagesApiUrl = `${TMDB_BASE_URL}/${tmdbType}/${tmdbId}/images?api_key=${TMDB_API_KEY}`;
|
||||||
console.log(` Fetching TMDB images from: ${imagesApiUrl}`);
|
console.log(` Fetching TMDB images from: ${imagesApiUrl}`);
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(imagesApiUrl, { timeout: 10000 });
|
const response = await axios.get(imagesApiUrl, { timeout: 10000 });
|
||||||
@@ -1352,9 +1352,9 @@ class ShowBoxScraper {
|
|||||||
// Store the region preference and user cookie
|
// Store the region preference and user cookie
|
||||||
this.regionPreference = regionPreference;
|
this.regionPreference = regionPreference;
|
||||||
this.userCookie = userCookie;
|
this.userCookie = userCookie;
|
||||||
// ScraperAPI key support removed
|
// ScraperAPI key support removed
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
this.baseHeaders = {
|
this.baseHeaders = {
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||||||
@@ -1372,10 +1372,10 @@ class ShowBoxScraper {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async _makeRequest(url, isJsonExpected = false) {
|
async _makeRequest(url, isJsonExpected = false) {
|
||||||
// Removed unused timerLabel variable (profiling optional)
|
// Removed unused timerLabel variable (profiling optional)
|
||||||
|
|
||||||
// For JSON requests, we still cache normally since they're small
|
// For JSON requests, we still cache normally since they're small
|
||||||
if (isJsonExpected) {
|
if (isJsonExpected) {
|
||||||
@@ -1393,7 +1393,7 @@ class ShowBoxScraper {
|
|||||||
// Always make direct requests
|
// Always make direct requests
|
||||||
const requestUrl = url;
|
const requestUrl = url;
|
||||||
|
|
||||||
// Removed console.time call (timerLabel removed)
|
// Removed console.time call (timerLabel removed)
|
||||||
|
|
||||||
// Get the cookie with region preference if site requires it
|
// Get the cookie with region preference if site requires it
|
||||||
let cookieValue = null;
|
let cookieValue = null;
|
||||||
@@ -1807,7 +1807,7 @@ const convertImdbToTmdb = async (imdbId, regionPreference = null, expectedType =
|
|||||||
|
|
||||||
if (findResults) {
|
if (findResults) {
|
||||||
let result = null;
|
let result = null;
|
||||||
|
|
||||||
// Context-aware prioritization based on expected type
|
// Context-aware prioritization based on expected type
|
||||||
if (expectedType === 'tv' || expectedType === 'series') {
|
if (expectedType === 'tv' || expectedType === 'series') {
|
||||||
// For series requests, prioritize TV results
|
// For series requests, prioritize TV results
|
||||||
@@ -1835,7 +1835,7 @@ const convertImdbToTmdb = async (imdbId, regionPreference = null, expectedType =
|
|||||||
result = { tmdbId: String(findResults.tv_results[0].id), tmdbType: 'tv', title: findResults.tv_results[0].name || findResults.tv_results[0].original_name };
|
result = { tmdbId: String(findResults.tv_results[0].id), tmdbType: 'tv', title: findResults.tv_results[0].name || findResults.tv_results[0].original_name };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (findResults.person_results && findResults.person_results.length > 0 && !result) {
|
if (findResults.person_results && findResults.person_results.length > 0 && !result) {
|
||||||
// Could handle other types if necessary, e.g. person, but for streams, movie/tv are key
|
// Could handle other types if necessary, e.g. person, but for streams, movie/tv are key
|
||||||
console.log(` IMDb ID ${imdbId} resolved to a person, not a movie or TV show on TMDB.`);
|
console.log(` IMDb ID ${imdbId} resolved to a person, not a movie or TV show on TMDB.`);
|
||||||
@@ -1868,47 +1868,47 @@ const getStreamsFromTmdbId = async (tmdbType, tmdbId, seasonNum = null, episodeN
|
|||||||
console.log(`Getting streams for TMDB ${tmdbType}/${tmdbId}${seasonNum !== null ? `, Season ${seasonNum}` : ''}${episodeNum !== null ? `, Episode ${episodeNum}` : ''}`);
|
console.log(`Getting streams for TMDB ${tmdbType}/${tmdbId}${seasonNum !== null ? `, Season ${seasonNum}` : ''}${episodeNum !== null ? `, Episode ${episodeNum}` : ''}`);
|
||||||
|
|
||||||
// PRIMARY METHOD: Try PStream API first (if enabled)
|
// PRIMARY METHOD: Try PStream API first (if enabled)
|
||||||
|
|
||||||
try {
|
|
||||||
// Get TMDB data to extract IMDB ID
|
|
||||||
const tmdbData = await getTmdbDataForPStream(tmdbType, tmdbId);
|
|
||||||
if (tmdbData && tmdbData.imdbId) {
|
|
||||||
console.log(`[PStream Primary] Attempting PStream API for IMDB ID: ${tmdbData.imdbId}`);
|
|
||||||
|
|
||||||
const pstreamStreams = await getStreamsFromPStreamAPI(
|
try {
|
||||||
tmdbData.imdbId,
|
// Get TMDB data to extract IMDB ID
|
||||||
tmdbType,
|
const tmdbData = await getTmdbDataForPStream(tmdbType, tmdbId);
|
||||||
seasonNum,
|
if (tmdbData && tmdbData.imdbId) {
|
||||||
episodeNum,
|
console.log(`[PStream Primary] Attempting PStream API for IMDB ID: ${tmdbData.imdbId}`);
|
||||||
regionPreference,
|
|
||||||
userCookie
|
|
||||||
);
|
|
||||||
|
|
||||||
if (pstreamStreams && pstreamStreams.length > 0) {
|
const pstreamStreams = await getStreamsFromPStreamAPI(
|
||||||
console.log(`[PStream Primary] SUCCESS: Found ${pstreamStreams.length} streams from PStream API`);
|
tmdbData.imdbId,
|
||||||
|
tmdbType,
|
||||||
|
seasonNum,
|
||||||
|
episodeNum,
|
||||||
|
regionPreference,
|
||||||
|
userCookie
|
||||||
|
);
|
||||||
|
|
||||||
// Fetch sizes for PStream streams
|
if (pstreamStreams && pstreamStreams.length > 0) {
|
||||||
if (pstreamStreams.length > 0) {
|
console.log(`[PStream Primary] SUCCESS: Found ${pstreamStreams.length} streams from PStream API`);
|
||||||
const sizePromises = pstreamStreams.map(async (stream) => {
|
|
||||||
stream.size = await fetchStreamSize(stream.url);
|
|
||||||
return stream;
|
|
||||||
});
|
|
||||||
await Promise.all(sizePromises); // removed unused streamsWithSizes var
|
|
||||||
}
|
|
||||||
|
|
||||||
const sortedStreams = sortStreamsByQuality(pstreamStreams);
|
// Fetch sizes for PStream streams
|
||||||
console.timeEnd(mainTimerLabel);
|
if (pstreamStreams.length > 0) {
|
||||||
return sortedStreams;
|
const sizePromises = pstreamStreams.map(async (stream) => {
|
||||||
} else {
|
stream.size = await fetchStreamSize(stream.url);
|
||||||
console.log(`[PStream Primary] No streams found from PStream API, falling back to FebBox`);
|
return stream;
|
||||||
|
});
|
||||||
|
await Promise.all(sizePromises); // removed unused streamsWithSizes var
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sortedStreams = sortStreamsByQuality(pstreamStreams);
|
||||||
|
console.timeEnd(mainTimerLabel);
|
||||||
|
return sortedStreams;
|
||||||
} else {
|
} else {
|
||||||
console.log(`[PStream Primary] Could not get IMDB ID from TMDB data, falling back to FebBox`);
|
console.log(`[PStream Primary] No streams found from PStream API, falling back to FebBox`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} else {
|
||||||
console.error(`[PStream Primary] Error with PStream API: ${error.message}, falling back to FebBox`);
|
console.log(`[PStream Primary] Could not get IMDB ID from TMDB data, falling back to FebBox`);
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[PStream Primary] Error with PStream API: ${error.message}, falling back to FebBox`);
|
||||||
|
}
|
||||||
|
|
||||||
// FALLBACK METHOD: Use original FebBox/ShowBox logic
|
// FALLBACK METHOD: Use original FebBox/ShowBox logic
|
||||||
console.log(`[Fallback] Using FebBox/ShowBox method`);
|
console.log(`[Fallback] Using FebBox/ShowBox method`);
|
||||||
const tmdbInfo = await getShowboxUrlFromTmdbInfo(tmdbType, tmdbId, regionPreference);
|
const tmdbInfo = await getShowboxUrlFromTmdbInfo(tmdbType, tmdbId, regionPreference);
|
||||||
@@ -1924,19 +1924,19 @@ const getStreamsFromTmdbId = async (tmdbType, tmdbId, seasonNum = null, episodeN
|
|||||||
|
|
||||||
// Check if we have cached FebBox share links to avoid redundant ShowBox requests
|
// Check if we have cached FebBox share links to avoid redundant ShowBox requests
|
||||||
let febboxShareInfos = tmdbInfo.febboxShareInfos;
|
let febboxShareInfos = tmdbInfo.febboxShareInfos;
|
||||||
|
|
||||||
if (!febboxShareInfos) {
|
if (!febboxShareInfos) {
|
||||||
// Only make ShowBox request if FebBox links are not cached
|
// Only make ShowBox request if FebBox links are not cached
|
||||||
console.log(`No cached FebBox links found, extracting from ShowBox: ${showboxUrl}`);
|
console.log(`No cached FebBox links found, extracting from ShowBox: ${showboxUrl}`);
|
||||||
const showboxScraper = new ShowBoxScraper(regionPreference, userCookie);
|
const showboxScraper = new ShowBoxScraper(regionPreference, userCookie);
|
||||||
febboxShareInfos = await showboxScraper.extractFebboxShareLinks(showboxUrl);
|
febboxShareInfos = await showboxScraper.extractFebboxShareLinks(showboxUrl);
|
||||||
|
|
||||||
if (!febboxShareInfos || febboxShareInfos.length === 0) {
|
if (!febboxShareInfos || febboxShareInfos.length === 0) {
|
||||||
console.log(`No FebBox share links found for ${showboxUrl}`);
|
console.log(`No FebBox share links found for ${showboxUrl}`);
|
||||||
console.timeEnd(mainTimerLabel);
|
console.timeEnd(mainTimerLabel);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update cache with FebBox share links to avoid future ShowBox requests
|
// Update cache with FebBox share links to avoid future ShowBox requests
|
||||||
if (process.env.DISABLE_CACHE !== 'true') {
|
if (process.env.DISABLE_CACHE !== 'true') {
|
||||||
const updatedTmdbInfo = { ...tmdbInfo, febboxShareInfos };
|
const updatedTmdbInfo = { ...tmdbInfo, febboxShareInfos };
|
||||||
@@ -1980,10 +1980,12 @@ const getStreamsFromTmdbId = async (tmdbType, tmdbId, seasonNum = null, episodeN
|
|||||||
}
|
}
|
||||||
} catch (e) { /* ignore if URL parsing fails */ }
|
} catch (e) { /* ignore if URL parsing fails */ }
|
||||||
streamsFromThisShareInfo.push({
|
streamsFromThisShareInfo.push({
|
||||||
|
name: streamTitle,
|
||||||
title: streamTitle,
|
title: streamTitle,
|
||||||
url: source.url,
|
url: source.url,
|
||||||
quality: parseQualityFromLabel(source.label),
|
quality: parseQualityFromLabel(source.label),
|
||||||
codecs: extractCodecDetails(key5FromDirectSource || streamTitle)
|
provider: 'Febbox Direct',
|
||||||
|
headers: {}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// If direct sources are found, original code used 'continue',
|
// If direct sources are found, original code used 'continue',
|
||||||
@@ -2007,10 +2009,12 @@ const getStreamsFromTmdbId = async (tmdbType, tmdbId, seasonNum = null, episodeN
|
|||||||
|
|
||||||
const streamTitle = `${baseStreamTitle} - ${source.label}`;
|
const streamTitle = `${baseStreamTitle} - ${source.label}`;
|
||||||
streamsFromThisShareInfo.push({
|
streamsFromThisShareInfo.push({
|
||||||
|
name: streamTitle,
|
||||||
title: streamTitle,
|
title: streamTitle,
|
||||||
url: source.url,
|
url: source.url,
|
||||||
quality: parseQualityFromLabel(source.label),
|
quality: parseQualityFromLabel(source.label),
|
||||||
codecs: extractCodecDetails(source.detailedFilename || streamTitle)
|
provider: 'Febbox FID',
|
||||||
|
headers: {}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2034,7 +2038,7 @@ const getStreamsFromTmdbId = async (tmdbType, tmdbId, seasonNum = null, episodeN
|
|||||||
stream.size = await fetchStreamSize(stream.url);
|
stream.size = await fetchStreamSize(stream.url);
|
||||||
return stream;
|
return stream;
|
||||||
});
|
});
|
||||||
await Promise.all(sizePromises); // removed unused streamsWithSizes var
|
await Promise.all(sizePromises); // removed unused streamsWithSizes var
|
||||||
console.timeEnd(`getStreamsFromTmdbId_fetchStreamSizes_${tmdbType}_${tmdbId}`);
|
console.timeEnd(`getStreamsFromTmdbId_fetchStreamSizes_${tmdbType}_${tmdbId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2202,10 +2206,12 @@ const processShowWithSeasonsEpisodes = async (febboxUrl, showboxTitle, seasonNum
|
|||||||
for (const source of directSources) {
|
for (const source of directSources) {
|
||||||
const streamTitle = `${showboxTitle} - ${source.label}`;
|
const streamTitle = `${showboxTitle} - ${source.label}`;
|
||||||
streamsForThisCall.push({ // MODIFIED: Push to streamsForThisCall
|
streamsForThisCall.push({ // MODIFIED: Push to streamsForThisCall
|
||||||
|
name: streamTitle,
|
||||||
title: streamTitle,
|
title: streamTitle,
|
||||||
url: source.url,
|
url: source.url,
|
||||||
quality: parseQualityFromLabel(source.label),
|
quality: parseQualityFromLabel(source.label),
|
||||||
codecs: extractCodecDetails(source.detailedFilename || streamTitle)
|
provider: 'Febbox Direct',
|
||||||
|
headers: {}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
console.timeEnd(processTimerLabel);
|
console.timeEnd(processTimerLabel);
|
||||||
@@ -2222,10 +2228,12 @@ const processShowWithSeasonsEpisodes = async (febboxUrl, showboxTitle, seasonNum
|
|||||||
for (const source of sources) {
|
for (const source of sources) {
|
||||||
const streamTitle = `${showboxTitle} - ${source.label}`;
|
const streamTitle = `${showboxTitle} - ${source.label}`;
|
||||||
streamsForThisCall.push({ // MODIFIED: Push to streamsForThisCall
|
streamsForThisCall.push({ // MODIFIED: Push to streamsForThisCall
|
||||||
|
name: streamTitle,
|
||||||
title: streamTitle,
|
title: streamTitle,
|
||||||
url: source.url,
|
url: source.url,
|
||||||
quality: parseQualityFromLabel(source.label),
|
quality: parseQualityFromLabel(source.label),
|
||||||
codecs: extractCodecDetails(source.detailedFilename || streamTitle)
|
provider: 'Febbox FID',
|
||||||
|
headers: {}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2506,10 +2514,12 @@ const processShowWithSeasonsEpisodes = async (febboxUrl, showboxTitle, seasonNum
|
|||||||
|
|
||||||
const streamTitle = `${showboxTitle} - S${seasonNum}${episodeNum && selectedEpisode && fid === selectedEpisode.fid ? `E${episodeNum}` : (episodeDetail ? `E${episodeDetail.episodeNum}` : '')} - ${episodeName} - ${source.label}`;
|
const streamTitle = `${showboxTitle} - S${seasonNum}${episodeNum && selectedEpisode && fid === selectedEpisode.fid ? `E${episodeNum}` : (episodeDetail ? `E${episodeDetail.episodeNum}` : '')} - ${episodeName} - ${source.label}`;
|
||||||
streamsForThisCall.push({ // MODIFIED: Push to streamsForThisCall
|
streamsForThisCall.push({ // MODIFIED: Push to streamsForThisCall
|
||||||
|
name: streamTitle,
|
||||||
title: streamTitle,
|
title: streamTitle,
|
||||||
url: source.url,
|
url: source.url,
|
||||||
quality: parseQualityFromLabel(source.label),
|
quality: parseQualityFromLabel(source.label),
|
||||||
codecs: extractCodecDetails(source.detailedFilename || streamTitle)
|
provider: 'Febbox FID',
|
||||||
|
headers: {}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -2669,10 +2679,9 @@ const sortStreamsByQuality = (streams) => {
|
|||||||
|
|
||||||
// Provider priority ordering (lower number = higher priority)
|
// Provider priority ordering (lower number = higher priority)
|
||||||
const providerSortKeys = {
|
const providerSortKeys = {
|
||||||
'ShowBox': 1,
|
'ShowBox': 1,
|
||||||
'Xprime.tv': 2,
|
'HollyMovieHD': 2,
|
||||||
'HollyMovieHD': 3,
|
'Soaper TV': 3,
|
||||||
'Soaper TV': 4,
|
|
||||||
// Default for unknown providers
|
// Default for unknown providers
|
||||||
default: 99
|
default: 99
|
||||||
};
|
};
|
||||||
@@ -2722,27 +2731,27 @@ const PSTREAM_ACCEPT_HEADER = 'pstream.org';
|
|||||||
const PSTREAM_REGION_MAPPING = {
|
const PSTREAM_REGION_MAPPING = {
|
||||||
// US Regions
|
// US Regions
|
||||||
'USA7': 'East',
|
'USA7': 'East',
|
||||||
'USA6': 'West',
|
'USA6': 'West',
|
||||||
'USA5': 'East',
|
'USA5': 'East',
|
||||||
'USA4': 'West',
|
'USA4': 'West',
|
||||||
'USA3': 'West',
|
'USA3': 'West',
|
||||||
'USA2': 'West',
|
'USA2': 'West',
|
||||||
'USA1': 'West',
|
'USA1': 'West',
|
||||||
|
|
||||||
// European Regions
|
// European Regions
|
||||||
'UK3': 'Europe',
|
'UK3': 'Europe',
|
||||||
'FR1': 'Europe',
|
'FR1': 'Europe',
|
||||||
'DE2': 'Europe',
|
'DE2': 'Europe',
|
||||||
'EU1': 'Europe',
|
'EU1': 'Europe',
|
||||||
'EU2': 'Europe',
|
'EU2': 'Europe',
|
||||||
|
|
||||||
// Asian Regions
|
// Asian Regions
|
||||||
'HK1': 'Asia',
|
'HK1': 'Asia',
|
||||||
'IN1': 'Asia',
|
'IN1': 'Asia',
|
||||||
'SZ': 'Asia',
|
'SZ': 'Asia',
|
||||||
'ASIA1': 'Asia',
|
'ASIA1': 'Asia',
|
||||||
'ASIA2': 'Asia',
|
'ASIA2': 'Asia',
|
||||||
|
|
||||||
// Other Regions
|
// Other Regions
|
||||||
'CA1': 'East', // Canada maps to East (US East)
|
'CA1': 'East', // Canada maps to East (US East)
|
||||||
'AU1': 'Asia' // Australia maps to Asia
|
'AU1': 'Asia' // Australia maps to Asia
|
||||||
@@ -2753,12 +2762,12 @@ const mapRegionForPStream = (febboxRegion) => {
|
|||||||
if (!febboxRegion) {
|
if (!febboxRegion) {
|
||||||
return PSTREAM_REGION_MAPPING[DEFAULT_OSS_REGION] || 'East';
|
return PSTREAM_REGION_MAPPING[DEFAULT_OSS_REGION] || 'East';
|
||||||
}
|
}
|
||||||
|
|
||||||
const mappedRegion = PSTREAM_REGION_MAPPING[febboxRegion];
|
const mappedRegion = PSTREAM_REGION_MAPPING[febboxRegion];
|
||||||
if (mappedRegion) {
|
if (mappedRegion) {
|
||||||
return mappedRegion;
|
return mappedRegion;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to East if region not found
|
// Fallback to East if region not found
|
||||||
console.log(`[PStream] Region ${febboxRegion} not found in mapping, using East as fallback`);
|
console.log(`[PStream] Region ${febboxRegion} not found in mapping, using East as fallback`);
|
||||||
return 'East';
|
return 'East';
|
||||||
@@ -2768,7 +2777,7 @@ const mapRegionForPStream = (febboxRegion) => {
|
|||||||
const getStreamsFromPStreamAPI = async (imdbId, tmdbType, seasonNum = null, episodeNum = null, regionPreference = null, userCookie = null) => {
|
const getStreamsFromPStreamAPI = async (imdbId, tmdbType, seasonNum = null, episodeNum = null, regionPreference = null, userCookie = null) => {
|
||||||
const timerLabel = `getStreamsFromPStreamAPI_${imdbId}_${tmdbType}` + (seasonNum ? `_s${seasonNum}` : '') + (episodeNum ? `_e${episodeNum}` : '');
|
const timerLabel = `getStreamsFromPStreamAPI_${imdbId}_${tmdbType}` + (seasonNum ? `_s${seasonNum}` : '') + (episodeNum ? `_e${episodeNum}` : '');
|
||||||
console.time(timerLabel);
|
console.time(timerLabel);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Build the PStream API URL
|
// Build the PStream API URL
|
||||||
let apiUrl;
|
let apiUrl;
|
||||||
@@ -2780,12 +2789,12 @@ const getStreamsFromPStreamAPI = async (imdbId, tmdbType, seasonNum = null, epis
|
|||||||
console.log(`[PStream] Invalid parameters for ${tmdbType}: season=${seasonNum}, episode=${episodeNum}`);
|
console.log(`[PStream] Invalid parameters for ${tmdbType}: season=${seasonNum}, episode=${episodeNum}`);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[PStream] Making request to: ${apiUrl}`);
|
console.log(`[PStream] Making request to: ${apiUrl}`);
|
||||||
|
|
||||||
// Get the region for PStream API
|
// Get the region for PStream API
|
||||||
const pstreamRegion = mapRegionForPStream(regionPreference || global.lastRequestedRegion?.used);
|
const pstreamRegion = mapRegionForPStream(regionPreference || global.lastRequestedRegion?.used);
|
||||||
|
|
||||||
// Prepare headers for PStream API
|
// Prepare headers for PStream API
|
||||||
const headers = {
|
const headers = {
|
||||||
'accept': PSTREAM_ACCEPT_HEADER,
|
'accept': PSTREAM_ACCEPT_HEADER,
|
||||||
@@ -2801,7 +2810,7 @@ const getStreamsFromPStreamAPI = async (imdbId, tmdbType, seasonNum = null, epis
|
|||||||
'sec-fetch-site': 'same-site',
|
'sec-fetch-site': 'same-site',
|
||||||
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36'
|
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add ui-token if available
|
// Add ui-token if available
|
||||||
const uiToken = await getUiTokenForPStream(userCookie);
|
const uiToken = await getUiTokenForPStream(userCookie);
|
||||||
if (uiToken) {
|
if (uiToken) {
|
||||||
@@ -2810,30 +2819,30 @@ const getStreamsFromPStreamAPI = async (imdbId, tmdbType, seasonNum = null, epis
|
|||||||
} else {
|
} else {
|
||||||
console.log(`[PStream] No ui-token available, making unauthenticated request`);
|
console.log(`[PStream] No ui-token available, making unauthenticated request`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always use direct API URL
|
// Always use direct API URL
|
||||||
const finalApiUrl = apiUrl;
|
const finalApiUrl = apiUrl;
|
||||||
|
|
||||||
// Make the request
|
// Make the request
|
||||||
const response = await axios.get(finalApiUrl, {
|
const response = await axios.get(finalApiUrl, {
|
||||||
headers: headers,
|
headers: headers,
|
||||||
timeout: 30000
|
timeout: 30000
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.data) {
|
if (!response.data) {
|
||||||
console.log(`[PStream] No data received from API`);
|
console.log(`[PStream] No data received from API`);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[PStream] Response received, processing streams...`);
|
console.log(`[PStream] Response received, processing streams...`);
|
||||||
|
|
||||||
// Process the PStream API response
|
// Process the PStream API response
|
||||||
const streams = processPStreamResponse(response.data, tmdbType, seasonNum, episodeNum);
|
const streams = processPStreamResponse(response.data, tmdbType, seasonNum, episodeNum);
|
||||||
|
|
||||||
console.timeEnd(timerLabel);
|
console.timeEnd(timerLabel);
|
||||||
console.log(`[PStream] Found ${streams.length} streams`);
|
console.log(`[PStream] Found ${streams.length} streams`);
|
||||||
return streams;
|
return streams;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[PStream] Error fetching streams: ${error.message}`);
|
console.error(`[PStream] Error fetching streams: ${error.message}`);
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
@@ -2847,90 +2856,84 @@ const getStreamsFromPStreamAPI = async (imdbId, tmdbType, seasonNum = null, epis
|
|||||||
// Function to process PStream API response and convert to standard stream format
|
// Function to process PStream API response and convert to standard stream format
|
||||||
const processPStreamResponse = (apiResponse, tmdbType, seasonNum, episodeNum) => {
|
const processPStreamResponse = (apiResponse, tmdbType, seasonNum, episodeNum) => {
|
||||||
const streams = [];
|
const streams = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Log the actual response structure for debugging
|
// Log the actual response structure for debugging
|
||||||
console.log(`[PStream] API Response structure:`, JSON.stringify(apiResponse, null, 2).substring(0, 1000));
|
console.log(`[PStream] API Response structure:`, JSON.stringify(apiResponse, null, 2).substring(0, 1000));
|
||||||
console.log(`[PStream] API Response type:`, typeof apiResponse);
|
console.log(`[PStream] API Response type:`, typeof apiResponse);
|
||||||
console.log(`[PStream] API Response keys:`, apiResponse ? Object.keys(apiResponse) : 'null/undefined');
|
console.log(`[PStream] API Response keys:`, apiResponse ? Object.keys(apiResponse) : 'null/undefined');
|
||||||
|
|
||||||
// Check if response has streams object (PStream API format)
|
// Check if response has streams object (PStream API format)
|
||||||
if (apiResponse && apiResponse.streams && typeof apiResponse.streams === 'object') {
|
if (apiResponse && apiResponse.streams && typeof apiResponse.streams === 'object') {
|
||||||
console.log(`[PStream] Found streams object with ${Object.keys(apiResponse.streams).length} quality levels`);
|
console.log(`[PStream] Found streams object with ${Object.keys(apiResponse.streams).length} quality levels`);
|
||||||
|
|
||||||
// Process streams object format
|
// Process streams object format
|
||||||
for (const [quality, url] of Object.entries(apiResponse.streams)) {
|
for (const [quality, url] of Object.entries(apiResponse.streams)) {
|
||||||
if (url && typeof url === 'string') {
|
if (url && typeof url === 'string') {
|
||||||
// Use "Auto" for name and actual filename for title
|
// Use "Auto" for name and actual filename for title
|
||||||
const streamName = "Auto";
|
|
||||||
const streamTitle = apiResponse.name || `${tmdbType === 'movie' ? 'PStream' : `PStream - S${seasonNum}E${episodeNum}`} - ${quality}`;
|
const streamTitle = apiResponse.name || `${tmdbType === 'movie' ? 'PStream' : `PStream - S${seasonNum}E${episodeNum}`} - ${quality}`;
|
||||||
|
|
||||||
streams.push({
|
streams.push({
|
||||||
name: streamName,
|
name: streamTitle,
|
||||||
title: streamTitle,
|
title: streamTitle,
|
||||||
url: url,
|
url: url,
|
||||||
quality: parseQualityFromLabel(quality),
|
quality: parseQualityFromLabel(quality),
|
||||||
codecs: extractCodecDetails(apiResponse.name || quality), // Use filename for codec detection
|
provider: 'PStream',
|
||||||
size: apiResponse.size || 'Unknown size',
|
headers: {}
|
||||||
provider: 'PStream'
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[PStream] Successfully parsed ${streams.length} streams from streams object`);
|
console.log(`[PStream] Successfully parsed ${streams.length} streams from streams object`);
|
||||||
return streams;
|
return streams;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if response has sources array (alternative format)
|
// Check if response has sources array (alternative format)
|
||||||
if (apiResponse && Array.isArray(apiResponse.sources)) {
|
if (apiResponse && Array.isArray(apiResponse.sources)) {
|
||||||
console.log(`[PStream] Found sources array with ${apiResponse.sources.length} sources`);
|
console.log(`[PStream] Found sources array with ${apiResponse.sources.length} sources`);
|
||||||
|
|
||||||
for (const source of apiResponse.sources) {
|
for (const source of apiResponse.sources) {
|
||||||
if (!source.url || !source.quality) {
|
if (!source.url || !source.quality) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const streamName = "Auto";
|
|
||||||
const streamTitle = apiResponse.name || `${tmdbType === 'movie' ? 'PStream' : `PStream - S${seasonNum}E${episodeNum}`} - ${source.quality}`;
|
const streamTitle = apiResponse.name || `${tmdbType === 'movie' ? 'PStream' : `PStream - S${seasonNum}E${episodeNum}`} - ${source.quality}`;
|
||||||
|
|
||||||
streams.push({
|
streams.push({
|
||||||
name: streamName,
|
name: streamTitle,
|
||||||
title: streamTitle,
|
title: streamTitle,
|
||||||
url: source.url,
|
url: source.url,
|
||||||
quality: parseQualityFromLabel(source.quality),
|
quality: parseQualityFromLabel(source.quality),
|
||||||
codecs: source.codec ? [source.codec] : [],
|
provider: 'PStream',
|
||||||
size: source.size || 'Unknown size',
|
headers: {}
|
||||||
provider: 'PStream'
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return streams;
|
return streams;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if response has data array (another alternative format)
|
// Check if response has data array (another alternative format)
|
||||||
if (apiResponse && Array.isArray(apiResponse.data)) {
|
if (apiResponse && Array.isArray(apiResponse.data)) {
|
||||||
console.log(`[PStream] Found 'data' array instead of 'sources'`);
|
console.log(`[PStream] Found 'data' array instead of 'sources'`);
|
||||||
|
|
||||||
for (const item of apiResponse.data) {
|
for (const item of apiResponse.data) {
|
||||||
if (item.url || item.file) {
|
if (item.url || item.file) {
|
||||||
const streamName = "Auto";
|
|
||||||
const streamTitle = apiResponse.name || `${tmdbType === 'movie' ? 'PStream' : `PStream - S${seasonNum}E${episodeNum}`} - ${item.quality || item.label || 'Unknown'}`;
|
const streamTitle = apiResponse.name || `${tmdbType === 'movie' ? 'PStream' : `PStream - S${seasonNum}E${episodeNum}`} - ${item.quality || item.label || 'Unknown'}`;
|
||||||
|
|
||||||
streams.push({
|
streams.push({
|
||||||
name: streamName,
|
name: streamTitle,
|
||||||
title: streamTitle,
|
title: streamTitle,
|
||||||
url: item.url || item.file,
|
url: item.url || item.file,
|
||||||
quality: parseQualityFromLabel(item.quality || item.label),
|
quality: parseQualityFromLabel(item.quality || item.label),
|
||||||
codecs: item.codec ? [item.codec] : [],
|
provider: 'PStream',
|
||||||
size: item.size || 'Unknown size',
|
headers: {}
|
||||||
provider: 'PStream'
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return streams;
|
return streams;
|
||||||
}
|
}
|
||||||
|
|
||||||
// No recognized format found
|
// No recognized format found
|
||||||
console.log(`[PStream] No recognized stream format found in response`);
|
console.log(`[PStream] No recognized stream format found in response`);
|
||||||
if (apiResponse && apiResponse.message) {
|
if (apiResponse && apiResponse.message) {
|
||||||
@@ -2938,13 +2941,13 @@ const processPStreamResponse = (apiResponse, tmdbType, seasonNum, episodeNum) =>
|
|||||||
} else if (apiResponse && apiResponse.error) {
|
} else if (apiResponse && apiResponse.error) {
|
||||||
console.log(`[PStream] API returned error:`, apiResponse.error);
|
console.log(`[PStream] API returned error:`, apiResponse.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return streams;
|
return streams;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[PStream] Error processing API response: ${error.message}`);
|
console.error(`[PStream] Error processing API response: ${error.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return streams;
|
return streams;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2952,15 +2955,15 @@ const processPStreamResponse = (apiResponse, tmdbType, seasonNum, episodeNum) =>
|
|||||||
const getTmdbDataForPStream = async (tmdbType, tmdbId) => {
|
const getTmdbDataForPStream = async (tmdbType, tmdbId) => {
|
||||||
const cacheSubDir = 'tmdb_api';
|
const cacheSubDir = 'tmdb_api';
|
||||||
const cacheKey = `tmdb-${tmdbType}-${tmdbId}.json`;
|
const cacheKey = `tmdb-${tmdbType}-${tmdbId}.json`;
|
||||||
|
|
||||||
// Try cache first
|
// Try cache first
|
||||||
let tmdbData = await getFromCache(cacheKey, cacheSubDir);
|
let tmdbData = await getFromCache(cacheKey, cacheSubDir);
|
||||||
|
|
||||||
if (!tmdbData || process.env.DISABLE_CACHE === 'true') {
|
if (!tmdbData || process.env.DISABLE_CACHE === 'true') {
|
||||||
const TMDB_API_KEY = obtainTmdbKey();
|
const TMDB_API_KEY = obtainTmdbKey();
|
||||||
const tmdbApiUrl = `${TMDB_BASE_URL}/${tmdbType}/${tmdbId}?api_key=${TMDB_API_KEY}`;
|
const tmdbApiUrl = `${TMDB_BASE_URL}/${tmdbType}/${tmdbId}?api_key=${TMDB_API_KEY}`;
|
||||||
console.log(` [PStream] Fetching TMDB data from: ${tmdbApiUrl}`);
|
console.log(` [PStream] Fetching TMDB data from: ${tmdbApiUrl}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(tmdbApiUrl, { timeout: 10000 });
|
const response = await axios.get(tmdbApiUrl, { timeout: 10000 });
|
||||||
tmdbData = response.data;
|
tmdbData = response.data;
|
||||||
@@ -2974,22 +2977,22 @@ const getTmdbDataForPStream = async (tmdbType, tmdbId) => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tmdbData) {
|
if (!tmdbData) {
|
||||||
console.log(` [PStream] No TMDB data found for ${tmdbType}/${tmdbId}`);
|
console.log(` [PStream] No TMDB data found for ${tmdbType}/${tmdbId}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract IMDB ID
|
// Extract IMDB ID
|
||||||
let imdbId = null;
|
let imdbId = null;
|
||||||
const externalIdsCacheKey = `tmdb-${tmdbType}-${tmdbId}-external_ids.json`;
|
const externalIdsCacheKey = `tmdb-${tmdbType}-${tmdbId}-external_ids.json`;
|
||||||
let externalIdsData = await getFromCache(externalIdsCacheKey, cacheSubDir);
|
let externalIdsData = await getFromCache(externalIdsCacheKey, cacheSubDir);
|
||||||
|
|
||||||
if (!externalIdsData || process.env.DISABLE_CACHE === 'true') {
|
if (!externalIdsData || process.env.DISABLE_CACHE === 'true') {
|
||||||
const TMDB_API_KEY = obtainTmdbKey();
|
const TMDB_API_KEY = obtainTmdbKey();
|
||||||
const externalIdsApiUrl = `${TMDB_BASE_URL}/${tmdbType}/${tmdbId}/external_ids?api_key=${TMDB_API_KEY}`;
|
const externalIdsApiUrl = `${TMDB_BASE_URL}/${tmdbType}/${tmdbId}/external_ids?api_key=${TMDB_API_KEY}`;
|
||||||
console.log(` [PStream] Fetching TMDB external IDs from: ${externalIdsApiUrl}`);
|
console.log(` [PStream] Fetching TMDB external IDs from: ${externalIdsApiUrl}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(externalIdsApiUrl, { timeout: 10000 });
|
const response = await axios.get(externalIdsApiUrl, { timeout: 10000 });
|
||||||
externalIdsData = response.data;
|
externalIdsData = response.data;
|
||||||
@@ -3002,21 +3005,21 @@ const getTmdbDataForPStream = async (tmdbType, tmdbId) => {
|
|||||||
console.log(` [PStream] Error fetching TMDB external IDs: ${error.message}`);
|
console.log(` [PStream] Error fetching TMDB external IDs: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (externalIdsData && externalIdsData.imdb_id) {
|
if (externalIdsData && externalIdsData.imdb_id) {
|
||||||
imdbId = externalIdsData.imdb_id;
|
imdbId = externalIdsData.imdb_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!imdbId) {
|
if (!imdbId) {
|
||||||
console.log(` [PStream] No IMDB ID found for ${tmdbType}/${tmdbId}`);
|
console.log(` [PStream] No IMDB ID found for ${tmdbType}/${tmdbId}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
imdbId: imdbId,
|
imdbId: imdbId,
|
||||||
title: tmdbType === 'movie' ? tmdbData.title : tmdbData.name,
|
title: tmdbType === 'movie' ? tmdbData.title : tmdbData.name,
|
||||||
year: tmdbData.release_date ? new Date(tmdbData.release_date).getFullYear() :
|
year: tmdbData.release_date ? new Date(tmdbData.release_date).getFullYear() :
|
||||||
tmdbData.first_air_date ? new Date(tmdbData.first_air_date).getFullYear() : null
|
tmdbData.first_air_date ? new Date(tmdbData.first_air_date).getFullYear() : null
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -3027,20 +3030,20 @@ const getUiTokenForPStream = async (userCookie = null) => {
|
|||||||
console.log('[PStream CookieManager] Using user-provided JWT token as ui-token');
|
console.log('[PStream CookieManager] Using user-provided JWT token as ui-token');
|
||||||
return userCookie;
|
return userCookie;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If user provided a regular cookie, try to convert it to JWT format
|
// If user provided a regular cookie, try to convert it to JWT format
|
||||||
if (userCookie && !userCookie.startsWith('eyJ')) {
|
if (userCookie && !userCookie.startsWith('eyJ')) {
|
||||||
console.log('[PStream CookieManager] Regular cookie provided, attempting to use as ui-token');
|
console.log('[PStream CookieManager] Regular cookie provided, attempting to use as ui-token');
|
||||||
// For now, pass it through as-is - the API might handle conversion
|
// For now, pass it through as-is - the API might handle conversion
|
||||||
return userCookie;
|
return userCookie;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check global cookie
|
// Check global cookie
|
||||||
if (global.currentRequestUserCookie && global.currentRequestUserCookie.startsWith('eyJ')) {
|
if (global.currentRequestUserCookie && global.currentRequestUserCookie.startsWith('eyJ')) {
|
||||||
console.log('[PStream CookieManager] Using JWT token from global state');
|
console.log('[PStream CookieManager] Using JWT token from global state');
|
||||||
return global.currentRequestUserCookie;
|
return global.currentRequestUserCookie;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try intelligent selection from cookies array if available
|
// Try intelligent selection from cookies array if available
|
||||||
if (global.currentRequestConfig && Array.isArray(global.currentRequestConfig.cookies) && global.currentRequestConfig.cookies.length > 0) {
|
if (global.currentRequestConfig && Array.isArray(global.currentRequestConfig.cookies) && global.currentRequestConfig.cookies.length > 0) {
|
||||||
try {
|
try {
|
||||||
@@ -3070,7 +3073,7 @@ const getUiTokenForPStream = async (userCookie = null) => {
|
|||||||
console.warn(`[PStream CookieManager] Multi-cookie selection failed: ${e.message}`);
|
console.warn(`[PStream CookieManager] Multi-cookie selection failed: ${e.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[PStream CookieManager] No valid ui-token available');
|
console.log('[PStream CookieManager] No valid ui-token available');
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
+50
-13
@@ -1,5 +1,43 @@
|
|||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
|
|
||||||
|
// Confirmed AES-CBC decoder for VidZee encrypted link format.
|
||||||
|
// Format (after initial atob in site script): ivBase64:cipherBase64
|
||||||
|
// ivBase64 -> CryptoJS Base64 parsed to WordArray (IV)
|
||||||
|
// Key: UTF-8 bytes of "qrincywincyspider" padded with nulls to 32 bytes (AES-256-CBC)
|
||||||
|
// Cipher: base64 ciphertext, PKCS7 padding, mode CBC.
|
||||||
|
let cryptoJs; // lazy load to avoid cost if not needed
|
||||||
|
function decodeVidZeeToken(token, debug) {
|
||||||
|
try {
|
||||||
|
if (typeof token !== 'string') return null;
|
||||||
|
if (/^https?:\/\//i.test(token)) return null; // already a URL
|
||||||
|
// Expect pattern base64:base64 (iv:cipher)
|
||||||
|
const raw = Buffer.from(token, 'base64').toString('utf8');
|
||||||
|
if (!raw.includes(':')) return null;
|
||||||
|
const [ivB64, cipherB64] = raw.split(':');
|
||||||
|
if (!ivB64 || !cipherB64) return null;
|
||||||
|
if (!cryptoJs) cryptoJs = require('crypto-js');
|
||||||
|
const iv = cryptoJs.enc.Base64.parse(ivB64.trim());
|
||||||
|
const keyStr = 'qrincywincyspider';
|
||||||
|
// Pad key with nulls to 32 bytes
|
||||||
|
const keyUtf8 = cryptoJs.enc.Utf8.parse(keyStr.padEnd(32, '\0'));
|
||||||
|
const decrypted = cryptoJs.AES.decrypt(cipherB64.trim(), keyUtf8, {
|
||||||
|
iv,
|
||||||
|
mode: cryptoJs.mode.CBC,
|
||||||
|
padding: cryptoJs.pad.Pkcs7
|
||||||
|
}).toString(cryptoJs.enc.Utf8);
|
||||||
|
if (!decrypted) return null;
|
||||||
|
if (!/^https?:\/\//i.test(decrypted)) {
|
||||||
|
if (debug) console.log('[VidZee] decrypted but not a URL', decrypted.slice(0,80));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (debug) console.log('[VidZee] AES decoded token', { tokenSnippet: token.slice(0, 24)+'...', url: decrypted.slice(0,120) });
|
||||||
|
return decrypted.trim();
|
||||||
|
} catch (e) {
|
||||||
|
if (debug) console.log('[VidZee] AES decode error', e.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Removed unused parseArgs helper
|
// Removed unused parseArgs helper
|
||||||
|
|
||||||
const getVidZeeStreams = async (tmdbId, mediaType, seasonNum, episodeNum) => {
|
const getVidZeeStreams = async (tmdbId, mediaType, seasonNum, episodeNum) => {
|
||||||
@@ -73,28 +111,27 @@ const getVidZeeStreams = async (tmdbId, mediaType, seasonNum, episodeNum) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const streams = apiSources.map(sourceItem => {
|
const streams = apiSources.map(sourceItem => {
|
||||||
// Prefer sourceItem.name as label, fallback to sourceItem.type, then 'VidZee Stream'
|
|
||||||
const label = sourceItem.name || sourceItem.type || 'VidZee';
|
const label = sourceItem.name || sourceItem.type || 'VidZee';
|
||||||
// Ensure quality has 'p' if it's a resolution, or keep it as is
|
|
||||||
let quality = String(label).match(/^\d+$/) ? `${label}p` : label;
|
let quality = String(label).match(/^\d+$/) ? `${label}p` : label;
|
||||||
// Normalize non-numeric or ambiguous quality labels to a baseline so they survive minQuality filter
|
|
||||||
if (!/(\d{3,4})p/.test(quality.toLowerCase())) {
|
if (!/(\d{3,4})p/.test(quality.toLowerCase())) {
|
||||||
quality = '720p';
|
quality = '720p';
|
||||||
}
|
}
|
||||||
const language = sourceItem.language || sourceItem.lang;
|
const language = sourceItem.language || sourceItem.lang;
|
||||||
|
let rawLink = sourceItem.link;
|
||||||
|
const debug = process.env.VIDZEE_DEBUG === '1';
|
||||||
|
const decoded = decodeVidZeeToken(rawLink, debug);
|
||||||
|
if (decoded && /^https?:\/\//i.test(decoded)) {
|
||||||
|
if (debug) console.log('[VidZee] decoded link', { beforeSample: rawLink.slice(0,40)+'...', after: decoded.slice(0,80) });
|
||||||
|
rawLink = decoded;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
title: `VidZee S${sr} - ${quality}`,
|
name: `VidZee Server${sr} - ${quality} - ${language}`,
|
||||||
url: sourceItem.link, // Use sourceItem.link for the URL
|
title: `VidZee Server${sr} - ${quality} - ${language}`,
|
||||||
|
url: rawLink,
|
||||||
quality: quality,
|
quality: quality,
|
||||||
language: language,
|
|
||||||
provider: "VidZee",
|
provider: "VidZee",
|
||||||
size: "Unknown size",
|
headers: {
|
||||||
behaviorHints: {
|
'Referer': 'https://core.vidzee.wtf/'
|
||||||
notWebReady: true,
|
|
||||||
headers: {
|
|
||||||
'Referer': 'https://core.vidzee.wtf/'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}).filter(stream => stream.url);
|
}).filter(stream => stream.url);
|
||||||
|
|||||||
@@ -1106,6 +1106,8 @@ async function getMoviesModStreams(tmdbId, mediaType, seasonNum = null, episodeN
|
|||||||
title: `${cleanFileName}\n${sizeInfo || ''}${techDetailsString}`,
|
title: `${cleanFileName}\n${sizeInfo || ''}${techDetailsString}`,
|
||||||
url: selectedResult.url,
|
url: selectedResult.url,
|
||||||
quality: actualQuality,
|
quality: actualQuality,
|
||||||
|
provider: 'moviesmod',
|
||||||
|
headers: {}
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[MoviesMod] Unsupported URL type for final processing: ${targetLink?.url}`);
|
console.warn(`[MoviesMod] Unsupported URL type for final processing: ${targetLink?.url}`);
|
||||||
|
|||||||
+115
-241
@@ -1,12 +1,21 @@
|
|||||||
const { config } = require('../utils/config');
|
const { config } = require('../utils/config');
|
||||||
// Lazy load showbox to avoid early side-effects; temporarily disabled until stabilized
|
const fs = require('fs');
|
||||||
let showbox = null;
|
const path = require('path');
|
||||||
let k4hdhub = null; // 4khdhub provider lazy ref
|
|
||||||
let moviesmod = null; // moviesmod provider lazy ref
|
// Lazy load cache for providers
|
||||||
let mp4hydra = null; // MP4Hydra provider lazy ref
|
const providerCache = new Map();
|
||||||
let vidzee = null; // VidZee provider lazy ref
|
|
||||||
let vixsrc = null; // vixsrc provider lazy ref
|
// Function name mappings for existing providers
|
||||||
let xprime = null; // xprime provider lazy ref
|
const providerFunctionMap = {
|
||||||
|
'Showbox.js': 'getStreamsFromTmdbId',
|
||||||
|
'4khdhub.js': 'get4KHDHubStreams',
|
||||||
|
'moviesmod.js': 'getMoviesModStreams',
|
||||||
|
'MP4Hydra.js': 'getMP4HydraStreams',
|
||||||
|
'VidZee.js': 'getVidZeeStreams',
|
||||||
|
'vixsrc.js': 'getVixsrcStreams',
|
||||||
|
'uhdmovies.js': 'getUHDMoviesStreams',
|
||||||
|
};
|
||||||
|
|
||||||
// Stats for debug endpoint
|
// Stats for debug endpoint
|
||||||
let lastCookieStats = { selected: null, index: null, total: 0, remainingMB: null, timestamp: null };
|
let lastCookieStats = { selected: null, index: null, total: 0, remainingMB: null, timestamp: null };
|
||||||
|
|
||||||
@@ -14,248 +23,113 @@ async function getEffectiveCookies() {
|
|||||||
return Array.isArray(config.febboxCookies) ? config.febboxCookies : [];
|
return Array.isArray(config.febboxCookies) ? config.febboxCookies : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adapter for Showbox provider using existing exported getStreamsFromTmdbId
|
// Get all provider files
|
||||||
async function fetchShowbox(ctx) {
|
function getProviderFiles() {
|
||||||
if (!showbox) {
|
const providersDir = path.join(__dirname);
|
||||||
try { showbox = require('./Showbox.js'); } catch (e) { console.error('Failed to load Showbox provider:', e.message); return []; }
|
return fs.readdirSync(providersDir)
|
||||||
}
|
.filter(file => file.endsWith('.js') && file !== 'registry.js' && file !== 'vidsrcextractor.js')
|
||||||
const { getTmdbApiKey } = require('../utils/tmdbKey');
|
.map(file => ({
|
||||||
const tmdbApiKey = getTmdbApiKey();
|
name: path.parse(file).name.toLowerCase(),
|
||||||
if (!tmdbApiKey) {
|
file: file,
|
||||||
console.warn('[registry] showbox skipped: TMDB API key missing');
|
functionName: providerFunctionMap[file]
|
||||||
return [];
|
}));
|
||||||
}
|
|
||||||
const cookies = await getEffectiveCookies();
|
|
||||||
const previousConfig = global.currentRequestConfig;
|
|
||||||
global.currentRequestConfig = { ...(previousConfig || {}) };
|
|
||||||
let selected = null;
|
|
||||||
if (cookies.length > 0) {
|
|
||||||
const index = Math.floor(Math.random() * cookies.length);
|
|
||||||
selected = cookies[index];
|
|
||||||
global.currentRequestConfig.cookie = selected.startsWith('ui=') ? selected : `ui=${selected}`;
|
|
||||||
global.currentRequestConfig.cookies = cookies.map(c => c.startsWith('ui=') ? c : `ui=${c}`);
|
|
||||||
lastCookieStats = { selected: selected.slice(0, 16) + '...', index, total: cookies.length, remainingMB: null, timestamp: Date.now() };
|
|
||||||
console.log(`[registry] Cookie random pick index=${index} total=${cookies.length}`);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const tmdbType = ctx.type === 'movie' ? 'movie' : 'tv';
|
|
||||||
console.log(`[registry] Fetching Showbox streams for ${tmdbType} tmdbId=${ctx.tmdbId} season=${ctx.season||''} episode=${ctx.episode||''}`);
|
|
||||||
const t0 = Date.now();
|
|
||||||
const streams = await showbox.getStreamsFromTmdbId(tmdbType, ctx.tmdbId, ctx.season || null, ctx.episode || null, null, selected);
|
|
||||||
const durationMs = Date.now() - t0;
|
|
||||||
console.log(`[registry] Provider showbox fetch duration ${durationMs}ms`);
|
|
||||||
if (global.currentRequestUserCookieRemainingMB != null) {
|
|
||||||
lastCookieStats.remainingMB = global.currentRequestUserCookieRemainingMB;
|
|
||||||
}
|
|
||||||
console.log(`[registry] Showbox returned ${Array.isArray(streams)?streams.length:0} streams`);
|
|
||||||
return Array.isArray(streams) ? streams : [];
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[registry] Showbox fetch error:', e.message);
|
|
||||||
return [];
|
|
||||||
} finally {
|
|
||||||
global.currentRequestConfig = previousConfig || {};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load a provider module
|
||||||
|
function loadProvider(providerFile) {
|
||||||
|
if (!providerCache.has(providerFile)) {
|
||||||
|
try {
|
||||||
|
providerCache.set(providerFile, require(path.join(__dirname, providerFile)));
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to load provider ${providerFile}:`, e.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return providerCache.get(providerFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create fetch function for a provider
|
||||||
|
function createFetchFunction(providerInfo) {
|
||||||
|
return async function(ctx) {
|
||||||
|
const module = loadProvider(providerInfo.file);
|
||||||
|
if (!module) return [];
|
||||||
|
|
||||||
|
const funcName = providerInfo.functionName;
|
||||||
|
if (!module[funcName]) {
|
||||||
|
console.error(`Provider ${providerInfo.name} does not export ${funcName}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
|
||||||
|
const t0 = Date.now();
|
||||||
|
|
||||||
|
let result;
|
||||||
|
if (providerInfo.name === 'showbox') {
|
||||||
|
// Special case for Showbox with TMDB key and cookies
|
||||||
|
const { getTmdbApiKey } = require('../utils/tmdbKey');
|
||||||
|
const tmdbApiKey = getTmdbApiKey();
|
||||||
|
if (!tmdbApiKey) {
|
||||||
|
console.warn('[registry] showbox skipped: TMDB API key missing');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const cookies = await getEffectiveCookies();
|
||||||
|
const previousConfig = global.currentRequestConfig;
|
||||||
|
global.currentRequestConfig = { ...(previousConfig || {}) };
|
||||||
|
let selected = null;
|
||||||
|
if (cookies.length > 0) {
|
||||||
|
const index = Math.floor(Math.random() * cookies.length);
|
||||||
|
selected = cookies[index];
|
||||||
|
global.currentRequestConfig.cookie = selected.startsWith('ui=') ? selected : `ui=${selected}`;
|
||||||
|
global.currentRequestConfig.cookies = cookies.map(c => c.startsWith('ui=') ? c : `ui=${c}`);
|
||||||
|
lastCookieStats = { selected: selected.slice(0, 16) + '...', index, total: cookies.length, remainingMB: null, timestamp: Date.now() };
|
||||||
|
console.log(`[registry] Cookie random pick index=${index} total=${cookies.length}`);
|
||||||
|
}
|
||||||
|
result = await module[funcName](mediaType, ctx.tmdbId, ctx.season || null, ctx.episode || null, null, selected);
|
||||||
|
if (global.currentRequestUserCookieRemainingMB != null) {
|
||||||
|
lastCookieStats.remainingMB = global.currentRequestUserCookieRemainingMB;
|
||||||
|
}
|
||||||
|
global.currentRequestConfig = previousConfig || {};
|
||||||
|
} else {
|
||||||
|
// Standard provider call
|
||||||
|
result = await module[funcName](ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const durationMs = Date.now() - t0;
|
||||||
|
console.log(`[registry] ${providerInfo.name} fetch duration ${durationMs}ms`);
|
||||||
|
|
||||||
|
if (!Array.isArray(result)) return [];
|
||||||
|
|
||||||
|
// Add provider name if not present
|
||||||
|
return result.map(s => ({ ...s, provider: s.provider || providerInfo.name }));
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`[registry] ${providerInfo.name} fetch error:`, e.message);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize providers
|
||||||
|
const providerFiles = getProviderFiles();
|
||||||
const providers = [];
|
const providers = [];
|
||||||
|
|
||||||
if (config.enableShowboxProvider) {
|
for (const providerInfo of providerFiles) {
|
||||||
providers.push({ name: 'showbox', enabled: true, fetch: fetchShowbox });
|
const enableFlag = `enable${providerInfo.name.charAt(0).toUpperCase() + providerInfo.name.slice(1)}Provider`;
|
||||||
} else {
|
const enabled = config[enableFlag] !== false;
|
||||||
console.log('[registry] showbox disabled via config');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
// 4KHDHub adapter
|
providers.push({
|
||||||
async function fetch4khdhub(ctx) {
|
name: providerInfo.name,
|
||||||
if (!k4hdhub) {
|
enabled: true,
|
||||||
try { k4hdhub = require('./4khdhub.js'); } catch (e) { console.error('Failed to load 4khdhub provider:', e.message); return []; }
|
fetch: createFetchFunction(providerInfo)
|
||||||
}
|
|
||||||
try {
|
|
||||||
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
|
|
||||||
const t0 = Date.now();
|
|
||||||
const result = await k4hdhub.get4KHDHubStreams(ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null);
|
|
||||||
const durationMs = Date.now() - t0;
|
|
||||||
console.log(`[registry] 4khdhub fetch duration ${durationMs}ms`);
|
|
||||||
if (!Array.isArray(result)) return [];
|
|
||||||
return result.map(s => ({ ...s, name: s.name || '4KHDHub', provider: '4khdhub' }));
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[registry] 4khdhub fetch error:', e.message);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.enable4khdhubProvider) {
|
|
||||||
providers.push({ name: '4khdhub', enabled: true, fetch: fetch4khdhub });
|
|
||||||
console.log('[registry] 4khdhub provider enabled');
|
|
||||||
} else {
|
|
||||||
console.log('[registry] 4khdhub disabled via config');
|
|
||||||
}
|
|
||||||
|
|
||||||
// MoviesMod adapter
|
|
||||||
async function fetchMoviesMod(ctx) {
|
|
||||||
if (!moviesmod) {
|
|
||||||
try { moviesmod = require('./moviesmod.js'); } catch (e) { console.error('Failed to load moviesmod provider:', e.message); return []; }
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
|
|
||||||
const t0 = Date.now();
|
|
||||||
const result = await moviesmod.getMoviesModStreams(ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null);
|
|
||||||
const durationMs = Date.now() - t0;
|
|
||||||
console.log(`[registry] moviesmod fetch duration ${durationMs}ms`);
|
|
||||||
if (!Array.isArray(result)) return [];
|
|
||||||
return result.map(s => ({ ...s, name: s.name || 'MoviesMod', provider: 'moviesmod' }));
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[registry] moviesmod fetch error:', e.message);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.enableMoviesmodProvider) {
|
|
||||||
providers.push({ name: 'moviesmod', enabled: true, fetch: fetchMoviesMod });
|
|
||||||
console.log('[registry] moviesmod provider enabled');
|
|
||||||
} else {
|
|
||||||
console.log('[registry] moviesmod disabled via config');
|
|
||||||
}
|
|
||||||
|
|
||||||
// MP4Hydra adapter
|
|
||||||
async function fetchMp4hydra(ctx) {
|
|
||||||
if (!mp4hydra) {
|
|
||||||
try { mp4hydra = require('./MP4Hydra.js'); } catch (e) { console.error('Failed to load MP4Hydra provider:', e.message); return []; }
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
|
|
||||||
const t0 = Date.now();
|
|
||||||
const result = await mp4hydra.getMP4HydraStreams(ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null);
|
|
||||||
const durationMs = Date.now() - t0;
|
|
||||||
console.log(`[registry] mp4hydra fetch duration ${durationMs}ms`);
|
|
||||||
if (!Array.isArray(result)) return [];
|
|
||||||
return result.map(s => ({ ...s, name: s.name || 'MP4Hydra', provider: 'mp4hydra' }));
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[registry] mp4hydra fetch error:', e.message);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.enableMp4hydraProvider) {
|
|
||||||
providers.push({ name: 'mp4hydra', enabled: true, fetch: fetchMp4hydra });
|
|
||||||
console.log('[registry] mp4hydra provider enabled');
|
|
||||||
} else {
|
|
||||||
console.log('[registry] mp4hydra disabled via config');
|
|
||||||
}
|
|
||||||
|
|
||||||
// VidZee adapter
|
|
||||||
async function fetchVidZee(ctx) {
|
|
||||||
if (!vidzee) {
|
|
||||||
try { vidzee = require('./VidZee.js'); } catch (e) { console.error('Failed to load VidZee provider:', e.message); return []; }
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
|
|
||||||
const t0 = Date.now();
|
|
||||||
const result = await vidzee.getVidZeeStreams(ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null);
|
|
||||||
const durationMs = Date.now() - t0;
|
|
||||||
console.log(`[registry] vidzee fetch duration ${durationMs}ms`);
|
|
||||||
if (!Array.isArray(result)) return [];
|
|
||||||
return result.map(s => ({ ...s, name: s.name || 'VidZee', provider: 'vidzee' }));
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[registry] vidzee fetch error:', e.message);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.enableVidzeeProvider) {
|
|
||||||
providers.push({ name: 'vidzee', enabled: true, fetch: fetchVidZee });
|
|
||||||
console.log('[registry] vidzee provider enabled');
|
|
||||||
} else {
|
|
||||||
console.log('[registry] vidzee disabled via config');
|
|
||||||
}
|
|
||||||
|
|
||||||
// vixsrc adapter
|
|
||||||
async function fetchVixsrc(ctx) {
|
|
||||||
if (!vixsrc) {
|
|
||||||
try { vixsrc = require('./vixsrc.js'); } catch (e) { console.error('Failed to load vixsrc provider:', e.message); return []; }
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
|
|
||||||
const t0 = Date.now();
|
|
||||||
const result = await vixsrc.getVixsrcStreams(ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null);
|
|
||||||
const durationMs = Date.now() - t0;
|
|
||||||
console.log(`[registry] vixsrc fetch duration ${durationMs}ms`);
|
|
||||||
if (!Array.isArray(result)) return [];
|
|
||||||
return result.map(s => ({ ...s, name: s.name || 'Vixsrc', provider: 'vixsrc' }));
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[registry] vixsrc fetch error:', e.message);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.enableVixsrcProvider) {
|
|
||||||
providers.push({ name: 'vixsrc', enabled: true, fetch: fetchVixsrc });
|
|
||||||
console.log('[registry] vixsrc provider enabled');
|
|
||||||
} else {
|
|
||||||
console.log('[registry] vixsrc disabled via config');
|
|
||||||
}
|
|
||||||
|
|
||||||
// xprime adapter (requires TMDB metadata lookup for title/year)
|
|
||||||
async function fetchXprime(ctx) {
|
|
||||||
if (!xprime) {
|
|
||||||
try { xprime = require('./xprime.js'); } catch (e) { console.error('Failed to load xprime provider:', e.message); return []; }
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
|
|
||||||
const { getTmdbApiKey } = require('../utils/tmdbKey');
|
|
||||||
const tmdbApiKey = getTmdbApiKey();
|
|
||||||
if (!tmdbApiKey) {
|
|
||||||
console.warn('[registry] xprime skipped: TMDB API key missing');
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
// Fetch minimal TMDB metadata for title + year
|
|
||||||
let title = null; let year = null;
|
|
||||||
try {
|
|
||||||
const axios = require('axios');
|
|
||||||
const metaUrl = `https://api.themoviedb.org/3/${mediaType}/${ctx.tmdbId}?api_key=${tmdbApiKey}`;
|
|
||||||
const { data: meta } = await axios.get(metaUrl, { timeout: 8000 });
|
|
||||||
title = mediaType === 'movie' ? (meta.title || meta.original_title) : (meta.name || meta.original_name);
|
|
||||||
const dateStr = mediaType === 'movie' ? meta.release_date : meta.first_air_date;
|
|
||||||
if (dateStr) year = dateStr.split('-')[0];
|
|
||||||
} catch (mErr) {
|
|
||||||
console.warn('[registry] xprime metadata fetch failed:', mErr.message);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
if (!title || !year) {
|
|
||||||
console.log('[registry] xprime missing title/year after TMDB lookup');
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const t0 = Date.now();
|
|
||||||
const streams = await xprime.getXprimeStreams(title, year, mediaType, ctx.season || null, ctx.episode || null);
|
|
||||||
const durationMs = Date.now() - t0;
|
|
||||||
console.log(`[registry] xprime fetch duration ${durationMs}ms (title='${title}' year=${year}) returned ${Array.isArray(streams)?streams.length:0}`);
|
|
||||||
if (!Array.isArray(streams) || !streams.length) return [];
|
|
||||||
// Normalize quality labels that are non-numeric (e.g., FHD/HD/SD)
|
|
||||||
return streams.map(s => {
|
|
||||||
let q = s.quality || 'Unknown';
|
|
||||||
if (!/(\d{3,4})p/.test(q.toLowerCase())) {
|
|
||||||
const qLower = q.toLowerCase();
|
|
||||||
if (qLower.includes('fhd') || qLower.includes('full')) q = '1080p';
|
|
||||||
else if (qLower === 'hd' || qLower.includes('720')) q = '720p';
|
|
||||||
else if (qLower.includes('sd') || qLower.includes('480')) q = '480p';
|
|
||||||
else if (qLower.includes('cam')) q = '360p';
|
|
||||||
}
|
|
||||||
return { ...s, quality: q, name: s.name || 'Xprime', provider: 'xprime' };
|
|
||||||
});
|
});
|
||||||
} catch (e) {
|
console.log(`[registry] ${providerInfo.name} provider enabled`);
|
||||||
console.error('[registry] xprime fetch error:', e.message);
|
} else {
|
||||||
return [];
|
console.log(`[registry] ${providerInfo.name} disabled via config`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.enableXprimeProvider) {
|
|
||||||
providers.push({ name: 'xprime', enabled: true, fetch: fetchXprime });
|
|
||||||
console.log('[registry] xprime provider enabled');
|
|
||||||
} else {
|
|
||||||
console.log('[registry] xprime disabled via config');
|
|
||||||
}
|
|
||||||
|
|
||||||
function listProviders() { return providers.map(p => ({ name: p.name, enabled: p.enabled })); }
|
function listProviders() { return providers.map(p => ({ name: p.name, enabled: p.enabled })); }
|
||||||
function getProvider(name) { return providers.find(p => p.name === name.toLowerCase()) || null; }
|
function getProvider(name) { return providers.find(p => p.name === name.toLowerCase()) || null; }
|
||||||
|
|
||||||
|
|||||||
+37
-40
@@ -46,9 +46,9 @@ async function getUHDMoviesDomain() {
|
|||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
const { getTmdbApiKey } = require('../utils/tmdbKey');
|
const { getTmdbApiKey } = require('../utils/tmdbKey');
|
||||||
function getUhdMoviesKey(){
|
function getUhdMoviesKey() {
|
||||||
const k = getTmdbApiKey();
|
const k = getTmdbApiKey();
|
||||||
if(!k) console.warn('[UHDMovies] TMDB API key missing; certain metadata features skipped.');
|
if (!k) console.warn('[UHDMovies] TMDB API key missing; certain metadata features skipped.');
|
||||||
return k;
|
return k;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,8 +209,8 @@ function extractCleanQuality(fullQualityText) {
|
|||||||
// Fallback: try to extract a shorter version of the original text
|
// Fallback: try to extract a shorter version of the original text
|
||||||
// Look for patterns like "Movie Name (Year) Resolution ..."
|
// Look for patterns like "Movie Name (Year) Resolution ..."
|
||||||
const patterns = [
|
const patterns = [
|
||||||
/(\d{3,4}p.*?(?:x264|x265|hevc).*?)[[(]/i,
|
/(\d{3,4}p.*?(?:x264|x265|hevc).*?)[[(]/i,
|
||||||
/(\d{3,4}p.*?)[[(]/i,
|
/(\d{3,4}p.*?)[[(]/i,
|
||||||
/((?:720p|1080p|2160p|4k).*?)$/i
|
/((?:720p|1080p|2160p|4k).*?)$/i
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -251,7 +251,7 @@ async function extractTvShowDownloadLinks(showPageUrl, season, episode) {
|
|||||||
// Check if we are entering a new season block
|
// Check if we are entering a new season block
|
||||||
if (seasonMatch) {
|
if (seasonMatch) {
|
||||||
const currentSeasonNum = parseInt(seasonMatch[1], 10);
|
const currentSeasonNum = parseInt(seasonMatch[1], 10);
|
||||||
if (currentSeasonNum === season) {
|
if (currentSeasonNum === season) {
|
||||||
inTargetSeason = true;
|
inTargetSeason = true;
|
||||||
console.log(`[UHDMovies] Entering Season ${season} block.`);
|
console.log(`[UHDMovies] Entering Season ${season} block.`);
|
||||||
} else if (inTargetSeason) {
|
} else if (inTargetSeason) {
|
||||||
@@ -298,7 +298,7 @@ async function extractTvShowDownloadLinks(showPageUrl, season, episode) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- ENHANCED: Check for maxbutton-gdrive-episode structure ---
|
// --- ENHANCED: Check for maxbutton-gdrive-episode structure ---
|
||||||
if ($el.is('p') && $el.find('a.maxbutton-gdrive-episode').length > 0) {
|
if ($el.is('p') && $el.find('a.maxbutton-gdrive-episode').length > 0) {
|
||||||
const episodeRegex = new RegExp(`^Episode\\s+0*${episode}(?!\\d)`, 'i');
|
const episodeRegex = new RegExp(`^Episode\\s+0*${episode}(?!\\d)`, 'i');
|
||||||
@@ -326,24 +326,24 @@ async function extractTvShowDownloadLinks(showPageUrl, season, episode) {
|
|||||||
|
|
||||||
if (downloadLinks.length === 0) {
|
if (downloadLinks.length === 0) {
|
||||||
console.log('[UHDMovies] Main extraction logic failed. Checking if requested season exists on page before fallback.');
|
console.log('[UHDMovies] Main extraction logic failed. Checking if requested season exists on page before fallback.');
|
||||||
|
|
||||||
// Check if the requested season exists on the page at all
|
// Check if the requested season exists on the page at all
|
||||||
let seasonExists = false;
|
let seasonExists = false;
|
||||||
const actualSeasonsOnPage = new Set(); // Track what seasons actually have content
|
const actualSeasonsOnPage = new Set(); // Track what seasons actually have content
|
||||||
|
|
||||||
// First pass: Look for actual episode content to see what seasons are available
|
// First pass: Look for actual episode content to see what seasons are available
|
||||||
$('.entry-content').find('a[href*="tech.unblockedgames.world"], a[href*="tech.examzculture.in"], a.maxbutton-gdrive-episode').each((index, element) => {
|
$('.entry-content').find('a[href*="tech.unblockedgames.world"], a[href*="tech.examzculture.in"], a.maxbutton-gdrive-episode').each((index, element) => {
|
||||||
const $el = $(element);
|
const $el = $(element);
|
||||||
const linkText = $el.text().trim();
|
const linkText = $el.text().trim();
|
||||||
const episodeText = $el.find('.mb-text').text().trim() || linkText;
|
const episodeText = $el.find('.mb-text').text().trim() || linkText;
|
||||||
|
|
||||||
// Look for season indicators in episode links
|
// Look for season indicators in episode links
|
||||||
const seasonMatches = [
|
const seasonMatches = [
|
||||||
episodeText.match(/S(\d{1,2})/i), // S01, S02, etc.
|
episodeText.match(/S(\d{1,2})/i), // S01, S02, etc.
|
||||||
episodeText.match(/Season\s+(\d+)/i), // Season 1, Season 2, etc.
|
episodeText.match(/Season\s+(\d+)/i), // Season 1, Season 2, etc.
|
||||||
episodeText.match(/S(\d{1,2})E(\d{1,3})/i) // S01E01 format
|
episodeText.match(/S(\d{1,2})E(\d{1,3})/i) // S01E01 format
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const match of seasonMatches) {
|
for (const match of seasonMatches) {
|
||||||
if (match && match[1]) {
|
if (match && match[1]) {
|
||||||
const foundSeason = parseInt(match[1], 10);
|
const foundSeason = parseInt(match[1], 10);
|
||||||
@@ -351,9 +351,9 @@ async function extractTvShowDownloadLinks(showPageUrl, season, episode) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`[UHDMovies] Actual seasons found on page: ${Array.from(actualSeasonsOnPage).sort((a,b) => a-b).join(', ')}`);
|
console.log(`[UHDMovies] Actual seasons found on page: ${Array.from(actualSeasonsOnPage).sort((a, b) => a - b).join(', ')}`);
|
||||||
|
|
||||||
// Check if requested season is in the actual content
|
// Check if requested season is in the actual content
|
||||||
if (actualSeasonsOnPage.has(season)) {
|
if (actualSeasonsOnPage.has(season)) {
|
||||||
seasonExists = true;
|
seasonExists = true;
|
||||||
@@ -371,7 +371,7 @@ async function extractTvShowDownloadLinks(showPageUrl, season, episode) {
|
|||||||
text.match(/Season\s+\d+\s*[–-]\s*(\d+)/i), // Matches "Season 1-2"
|
text.match(/Season\s+\d+\s*[–-]\s*(\d+)/i), // Matches "Season 1-2"
|
||||||
text.match(/\bS(\d+)/i) // Matches "S2", "S02", etc.
|
text.match(/\bS(\d+)/i) // Matches "S2", "S02", etc.
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const match of seasonMatches) {
|
for (const match of seasonMatches) {
|
||||||
if (match) {
|
if (match) {
|
||||||
const currentSeasonNum = parseInt(match[1], 10);
|
const currentSeasonNum = parseInt(match[1], 10);
|
||||||
@@ -397,15 +397,15 @@ async function extractTvShowDownloadLinks(showPageUrl, season, episode) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!seasonExists) {
|
if (!seasonExists) {
|
||||||
console.log(`[UHDMovies] Season ${season} not found on page. Available seasons may not include the requested season.`);
|
console.log(`[UHDMovies] Season ${season} not found on page. Available seasons may not include the requested season.`);
|
||||||
// Don't use fallback if the season doesn't exist to avoid wrong episodes
|
// Don't use fallback if the season doesn't exist to avoid wrong episodes
|
||||||
return { title: showTitle, links: [], seasonNotFound: true };
|
return { title: showTitle, links: [], seasonNotFound: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[UHDMovies] Season ${season} exists on page but episode extraction failed. Trying fallback method with season filtering.`);
|
console.log(`[UHDMovies] Season ${season} exists on page but episode extraction failed. Trying fallback method with season filtering.`);
|
||||||
|
|
||||||
// --- ENHANCED FALLBACK LOGIC FOR NEW HTML STRUCTURE ---
|
// --- ENHANCED FALLBACK LOGIC FOR NEW HTML STRUCTURE ---
|
||||||
// Try the new maxbutton-gdrive-episode structure first
|
// Try the new maxbutton-gdrive-episode structure first
|
||||||
$('.entry-content').find('a.maxbutton-gdrive-episode').each((i, el) => {
|
$('.entry-content').find('a.maxbutton-gdrive-episode').each((i, el) => {
|
||||||
@@ -417,7 +417,7 @@ async function extractTvShowDownloadLinks(showPageUrl, season, episode) {
|
|||||||
const link = linkElement.attr('href');
|
const link = linkElement.attr('href');
|
||||||
if (link && !downloadLinks.some(item => item.link === link)) {
|
if (link && !downloadLinks.some(item => item.link === link)) {
|
||||||
let qualityText = 'Unknown Quality';
|
let qualityText = 'Unknown Quality';
|
||||||
|
|
||||||
// Look for quality info in the preceding paragraph or heading
|
// Look for quality info in the preceding paragraph or heading
|
||||||
const parentP = linkElement.closest('p, div');
|
const parentP = linkElement.closest('p, div');
|
||||||
const prevElement = parentP.prev();
|
const prevElement = parentP.prev();
|
||||||
@@ -437,7 +437,7 @@ async function extractTvShowDownloadLinks(showPageUrl, season, episode) {
|
|||||||
new RegExp(`Season\\s+0*${season}\\b`, 'i'), // Season 1
|
new RegExp(`Season\\s+0*${season}\\b`, 'i'), // Season 1
|
||||||
new RegExp(`S0*${season}`, 'i') // S01 anywhere
|
new RegExp(`S0*${season}`, 'i') // S01 anywhere
|
||||||
];
|
];
|
||||||
|
|
||||||
const seasonMatch = seasonCheckRegexes.some(regex => regex.test(qualityText));
|
const seasonMatch = seasonCheckRegexes.some(regex => regex.test(qualityText));
|
||||||
if (!seasonMatch) {
|
if (!seasonMatch) {
|
||||||
console.log(`[UHDMovies] Skipping episode from different season: Quality='${qualityText}'`);
|
console.log(`[UHDMovies] Skipping episode from different season: Quality='${qualityText}'`);
|
||||||
@@ -454,7 +454,7 @@ async function extractTvShowDownloadLinks(showPageUrl, season, episode) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// If still no results, try the original fallback logic
|
// If still no results, try the original fallback logic
|
||||||
if (downloadLinks.length === 0) {
|
if (downloadLinks.length === 0) {
|
||||||
console.log(`[UHDMovies] Enhanced fallback failed, trying original fallback logic.`);
|
console.log(`[UHDMovies] Enhanced fallback failed, trying original fallback logic.`);
|
||||||
@@ -484,7 +484,7 @@ async function extractTvShowDownloadLinks(showPageUrl, season, episode) {
|
|||||||
new RegExp(`Season\\s+0*${season}\\b`, 'i'), // Season 1
|
new RegExp(`Season\\s+0*${season}\\b`, 'i'), // Season 1
|
||||||
new RegExp(`S0*${season}`, 'i') // S01 anywhere
|
new RegExp(`S0*${season}`, 'i') // S01 anywhere
|
||||||
];
|
];
|
||||||
|
|
||||||
const seasonMatch = seasonCheckRegexes.some(regex => regex.test(qualityText));
|
const seasonMatch = seasonCheckRegexes.some(regex => regex.test(qualityText));
|
||||||
if (!seasonMatch) {
|
if (!seasonMatch) {
|
||||||
console.log(`[UHDMovies] Skipping episode from different season: Quality='${qualityText}'`);
|
console.log(`[UHDMovies] Skipping episode from different season: Quality='${qualityText}'`);
|
||||||
@@ -822,7 +822,7 @@ async function validateVideoUrl(url, timeout = 10000) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(`[UHDMovies] Validating URL: ${url.substring(0, 100)}...`);
|
console.log(`[UHDMovies] Validating URL: ${url.substring(0, 100)}...`);
|
||||||
|
|
||||||
const response = await axiosInstance.head(url, {
|
const response = await axiosInstance.head(url, {
|
||||||
timeout,
|
timeout,
|
||||||
headers: {
|
headers: {
|
||||||
@@ -951,7 +951,7 @@ function scoreResult(title, requestedSeason = null) {
|
|||||||
console.log(`[UHDMovies] Season range bonus (+50): ${startSeason}-${endSeason} includes requested season ${requestedSeason}`);
|
console.log(`[UHDMovies] Season range bonus (+50): ${startSeason}-${endSeason} includes requested season ${requestedSeason}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for specific season mentions
|
// Check for specific season mentions
|
||||||
const specificSeasonMatch = lowerTitle.match(/season\s+(\d+)/i);
|
const specificSeasonMatch = lowerTitle.match(/season\s+(\d+)/i);
|
||||||
if (specificSeasonMatch) {
|
if (specificSeasonMatch) {
|
||||||
@@ -964,7 +964,7 @@ function scoreResult(title, requestedSeason = null) {
|
|||||||
console.log(`[UHDMovies] Season penalty (-20): Season ${mentionedSeason} is older than requested season ${requestedSeason}`);
|
console.log(`[UHDMovies] Season penalty (-20): Season ${mentionedSeason} is older than requested season ${requestedSeason}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for "Season X Added" or similar indicators
|
// Check for "Season X Added" or similar indicators
|
||||||
if (lowerTitle.includes('season') && lowerTitle.includes('added')) {
|
if (lowerTitle.includes('season') && lowerTitle.includes('added')) {
|
||||||
const addedSeasonMatch = lowerTitle.match(/season\s+(\d+)\s+added/i);
|
const addedSeasonMatch = lowerTitle.match(/season\s+(\d+)\s+added/i);
|
||||||
@@ -1030,7 +1030,7 @@ const getCookiesForUrl = async (jar, url) => {
|
|||||||
// Helper function to create a session for SID resolution
|
// Helper function to create a session for SID resolution
|
||||||
const createSession = async (jar) => {
|
const createSession = async (jar) => {
|
||||||
const { wrapper } = await getAxiosCookieJarSupport();
|
const { wrapper } = await getAxiosCookieJarSupport();
|
||||||
|
|
||||||
const sessionConfig = {
|
const sessionConfig = {
|
||||||
jar,
|
jar,
|
||||||
headers: {
|
headers: {
|
||||||
@@ -1044,7 +1044,7 @@ const createSession = async (jar) => {
|
|||||||
|
|
||||||
const session = wrapper(axios.create(sessionConfig));
|
const session = wrapper(axios.create(sessionConfig));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return session;
|
return session;
|
||||||
};
|
};
|
||||||
@@ -1178,8 +1178,8 @@ async function getUHDMoviesStreams(tmdbId, mediaType = 'movie', season = null, e
|
|||||||
}
|
}
|
||||||
console.log(`[UHDMovies] Cache MISS for ${cacheKey}. Fetching from source.`);
|
console.log(`[UHDMovies] Cache MISS for ${cacheKey}. Fetching from source.`);
|
||||||
// 2. If cache miss, get TMDB info to perform search
|
// 2. If cache miss, get TMDB info to perform search
|
||||||
const TMDB_API_KEY_UHDMOVIES = getUhdMoviesKey();
|
const TMDB_API_KEY_UHDMOVIES = getUhdMoviesKey();
|
||||||
const tmdbUrl = `https://api.themoviedb.org/3/${mediaType === 'tv' ? 'tv' : 'movie'}/${tmdbId}?api_key=${TMDB_API_KEY_UHDMOVIES}`;
|
const tmdbUrl = `https://api.themoviedb.org/3/${mediaType === 'tv' ? 'tv' : 'movie'}/${tmdbId}?api_key=${TMDB_API_KEY_UHDMOVIES}`;
|
||||||
const tmdbResponse = await axios.get(tmdbUrl);
|
const tmdbResponse = await axios.get(tmdbUrl);
|
||||||
const tmdbData = tmdbResponse.data;
|
const tmdbData = tmdbResponse.data;
|
||||||
const mediaInfo = {
|
const mediaInfo = {
|
||||||
@@ -1249,25 +1249,25 @@ async function getUHDMoviesStreams(tmdbId, mediaType = 'movie', season = null, e
|
|||||||
|
|
||||||
// 5. Extract SID links from the movie/show page
|
// 5. Extract SID links from the movie/show page
|
||||||
let downloadInfo = await (mediaType === 'tv' ? extractTvShowDownloadLinks(matchingResult.link, season, episode) : extractDownloadLinks(matchingResult.link, mediaInfo.year));
|
let downloadInfo = await (mediaType === 'tv' ? extractTvShowDownloadLinks(matchingResult.link, season, episode) : extractDownloadLinks(matchingResult.link, mediaInfo.year));
|
||||||
|
|
||||||
// Check if season was not found or episode extraction failed, and we have multiple results to try
|
// Check if season was not found or episode extraction failed, and we have multiple results to try
|
||||||
if (downloadInfo.links.length === 0 && matchingResults.length > 1 && scoredResults &&
|
if (downloadInfo.links.length === 0 && matchingResults.length > 1 && scoredResults &&
|
||||||
(downloadInfo.seasonNotFound || (mediaType === 'tv' && downloadInfo.title))) {
|
(downloadInfo.seasonNotFound || (mediaType === 'tv' && downloadInfo.title))) {
|
||||||
console.log(`[UHDMovies] Season ${season} not found or episode extraction failed on best match. Trying next best match...`);
|
console.log(`[UHDMovies] Season ${season} not found or episode extraction failed on best match. Trying next best match...`);
|
||||||
|
|
||||||
// Try the next best match
|
// Try the next best match
|
||||||
const nextBestMatch = scoredResults[1];
|
const nextBestMatch = scoredResults[1];
|
||||||
console.log(`[UHDMovies] Trying next best match: "${nextBestMatch.title}"`);
|
console.log(`[UHDMovies] Trying next best match: "${nextBestMatch.title}"`);
|
||||||
|
|
||||||
downloadInfo = await (mediaType === 'tv' ? extractTvShowDownloadLinks(nextBestMatch.link, season, episode) : extractDownloadLinks(nextBestMatch.link, mediaInfo.year));
|
downloadInfo = await (mediaType === 'tv' ? extractTvShowDownloadLinks(nextBestMatch.link, season, episode) : extractDownloadLinks(nextBestMatch.link, mediaInfo.year));
|
||||||
|
|
||||||
if (downloadInfo.links.length > 0) {
|
if (downloadInfo.links.length > 0) {
|
||||||
console.log(`[UHDMovies] Successfully found links on next best match!`);
|
console.log(`[UHDMovies] Successfully found links on next best match!`);
|
||||||
} else {
|
} else {
|
||||||
console.log(`[UHDMovies] Next best match also failed. No download links found.`);
|
console.log(`[UHDMovies] Next best match also failed. No download links found.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (downloadInfo.links.length === 0) {
|
if (downloadInfo.links.length === 0) {
|
||||||
console.log('[UHDMovies] No download links found on page.');
|
console.log('[UHDMovies] No download links found on page.');
|
||||||
// Don't cache empty results to allow retrying later
|
// Don't cache empty results to allow retrying later
|
||||||
@@ -1380,11 +1380,8 @@ async function getUHDMoviesStreams(tmdbId, mediaType = 'movie', season = null, e
|
|||||||
title: `${cleanFileName}\n${sizeInfo}`,
|
title: `${cleanFileName}\n${sizeInfo}`,
|
||||||
url: finalUrl,
|
url: finalUrl,
|
||||||
quality: linkInfo.quality,
|
quality: linkInfo.quality,
|
||||||
size: sizeInfo,
|
provider: 'uhdmovies',
|
||||||
fileName: fileName,
|
headers: {}
|
||||||
fullTitle: rawQuality,
|
|
||||||
codecs: codecs,
|
|
||||||
behaviorHints: { bingeGroup: `uhdmovies-${linkInfo.quality}` }
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+123
-124
@@ -6,7 +6,8 @@
|
|||||||
// Constants
|
// Constants
|
||||||
const TMDB_API_KEY = "68e094699525b18a70bab2f86b1fa706";
|
const TMDB_API_KEY = "68e094699525b18a70bab2f86b1fa706";
|
||||||
const BASE_URL = 'https://vixsrc.to';
|
const BASE_URL = 'https://vixsrc.to';
|
||||||
|
let vixsrcUrl;
|
||||||
|
let subtitleApiUrl;
|
||||||
// Helper function to make HTTP requests with default headers
|
// Helper function to make HTTP requests with default headers
|
||||||
function makeRequest(url, options = {}) {
|
function makeRequest(url, options = {}) {
|
||||||
const defaultHeaders = {
|
const defaultHeaders = {
|
||||||
@@ -23,24 +24,24 @@ function makeRequest(url, options = {}) {
|
|||||||
headers: defaultHeaders,
|
headers: defaultHeaders,
|
||||||
...options
|
...options
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error(`[Vixsrc] Request failed for ${url}: ${error.message}`);
|
console.error(`[Vixsrc] Request failed for ${url}: ${error.message}`);
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to get TMDB info
|
// Helper function to get TMDB info
|
||||||
function getTmdbInfo(tmdbId, mediaType) {
|
function getTmdbInfo(tmdbId, mediaType) {
|
||||||
const url = `https://api.themoviedb.org/3/${mediaType === 'tv' ? 'tv' : 'movie'}/${tmdbId}?api_key=${TMDB_API_KEY}`;
|
const url = `https://api.themoviedb.org/3/${mediaType === 'tv' ? 'tv' : 'movie'}/${tmdbId}?api_key=${TMDB_API_KEY}`;
|
||||||
|
|
||||||
return makeRequest(url)
|
return makeRequest(url)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => ({ data }));
|
.then(data => ({ data }));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,8 +52,6 @@ function getTmdbInfo(tmdbId, mediaType) {
|
|||||||
|
|
||||||
// Helper function to extract stream URL from Vixsrc page
|
// Helper function to extract stream URL from Vixsrc page
|
||||||
function extractStreamFromPage(url, contentType, contentId, seasonNum, episodeNum) {
|
function extractStreamFromPage(url, contentType, contentId, seasonNum, episodeNum) {
|
||||||
let vixsrcUrl;
|
|
||||||
let subtitleApiUrl;
|
|
||||||
|
|
||||||
if (contentType === 'movie') {
|
if (contentType === 'movie') {
|
||||||
vixsrcUrl = `${BASE_URL}/movie/${contentId}`;
|
vixsrcUrl = `${BASE_URL}/movie/${contentId}`;
|
||||||
@@ -64,152 +63,152 @@ function extractStreamFromPage(url, contentType, contentId, seasonNum, episodeNu
|
|||||||
|
|
||||||
console.log(`[Vixsrc] Fetching: ${vixsrcUrl}`);
|
console.log(`[Vixsrc] Fetching: ${vixsrcUrl}`);
|
||||||
|
|
||||||
return makeRequest(vixsrcUrl, {
|
return makeRequest(vixsrcUrl, {
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(response => response.text())
|
.then(response => response.text())
|
||||||
.then(html => {
|
.then(html => {
|
||||||
console.log(`[Vixsrc] HTML length: ${html.length} characters`);
|
console.log(`[Vixsrc] HTML length: ${html.length} characters`);
|
||||||
|
|
||||||
let masterPlaylistUrl = null;
|
let masterPlaylistUrl = null;
|
||||||
|
|
||||||
// Method 1: Look for window.masterPlaylist (primary method)
|
// Method 1: Look for window.masterPlaylist (primary method)
|
||||||
if (html.includes('window.masterPlaylist')) {
|
if (html.includes('window.masterPlaylist')) {
|
||||||
console.log('[Vixsrc] Found window.masterPlaylist');
|
console.log('[Vixsrc] Found window.masterPlaylist');
|
||||||
|
|
||||||
const urlMatch = html.match(/url:\s*['"]([^'"]+)['"]/);
|
const urlMatch = html.match(/url:\s*['"]([^'"]+)['"]/);
|
||||||
const tokenMatch = html.match(/['"]?token['"]?\s*:\s*['"]([^'"]+)['"]/);
|
const tokenMatch = html.match(/['"]?token['"]?\s*:\s*['"]([^'"]+)['"]/);
|
||||||
const expiresMatch = html.match(/['"]?expires['"]?\s*:\s*['"]([^'"]+)['"]/);
|
const expiresMatch = html.match(/['"]?expires['"]?\s*:\s*['"]([^'"]+)['"]/);
|
||||||
|
|
||||||
if (urlMatch && tokenMatch && expiresMatch) {
|
if (urlMatch && tokenMatch && expiresMatch) {
|
||||||
const baseUrl = urlMatch[1];
|
const baseUrl = urlMatch[1];
|
||||||
const token = tokenMatch[1];
|
const token = tokenMatch[1];
|
||||||
const expires = expiresMatch[1];
|
const expires = expiresMatch[1];
|
||||||
|
|
||||||
console.log('[Vixsrc] Extracted tokens:');
|
console.log('[Vixsrc] Extracted tokens:');
|
||||||
console.log(` - Base URL: ${baseUrl}`);
|
console.log(` - Base URL: ${baseUrl}`);
|
||||||
console.log(` - Token: ${token.substring(0, 20)}...`);
|
console.log(` - Token: ${token.substring(0, 20)}...`);
|
||||||
console.log(` - Expires: ${expires}`);
|
console.log(` - Expires: ${expires}`);
|
||||||
|
|
||||||
// Construct the master playlist URL
|
// Construct the master playlist URL
|
||||||
if (baseUrl.includes('?b=1')) {
|
if (baseUrl.includes('?b=1')) {
|
||||||
masterPlaylistUrl = `${baseUrl}&token=${token}&expires=${expires}&h=1&lang=en`;
|
masterPlaylistUrl = `${baseUrl}&token=${token}&expires=${expires}&h=1&lang=en`;
|
||||||
} else {
|
} else {
|
||||||
masterPlaylistUrl = `${baseUrl}?token=${token}&expires=${expires}&h=1&lang=en`;
|
masterPlaylistUrl = `${baseUrl}?token=${token}&expires=${expires}&h=1&lang=en`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[Vixsrc] Constructed master playlist URL: ${masterPlaylistUrl}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[Vixsrc] Constructed master playlist URL: ${masterPlaylistUrl}`);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Method 2: Look for direct .m3u8 URLs
|
// Method 2: Look for direct .m3u8 URLs
|
||||||
if (!masterPlaylistUrl) {
|
if (!masterPlaylistUrl) {
|
||||||
const m3u8Match = html.match(/(https?:\/\/[^'"\s]+\.m3u8[^'"\s]*)/);
|
const m3u8Match = html.match(/(https?:\/\/[^'"\s]+\.m3u8[^'"\s]*)/);
|
||||||
if (m3u8Match) {
|
if (m3u8Match) {
|
||||||
masterPlaylistUrl = m3u8Match[1];
|
masterPlaylistUrl = m3u8Match[1];
|
||||||
console.log('[Vixsrc] Found direct .m3u8 URL:', masterPlaylistUrl);
|
console.log('[Vixsrc] Found direct .m3u8 URL:', masterPlaylistUrl);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Method 3: Look for stream URLs in script tags
|
// Method 3: Look for stream URLs in script tags
|
||||||
if (!masterPlaylistUrl) {
|
if (!masterPlaylistUrl) {
|
||||||
const scriptMatches = html.match(/<script[^>]*>(.*?)<\/script>/gs);
|
const scriptMatches = html.match(/<script[^>]*>(.*?)<\/script>/gs);
|
||||||
if (scriptMatches) {
|
if (scriptMatches) {
|
||||||
for (const script of scriptMatches) {
|
for (const script of scriptMatches) {
|
||||||
const streamMatch = script.match(/['"]?(https?:\/\/[^'"\s]+(?:\.m3u8|playlist)[^'"\s]*)/);
|
const streamMatch = script.match(/['"]?(https?:\/\/[^'"\s]+(?:\.m3u8|playlist)[^'"\s]*)/);
|
||||||
if (streamMatch) {
|
if (streamMatch) {
|
||||||
masterPlaylistUrl = streamMatch[1];
|
masterPlaylistUrl = streamMatch[1];
|
||||||
console.log('[Vixsrc] Found stream in script:', masterPlaylistUrl);
|
console.log('[Vixsrc] Found stream in script:', masterPlaylistUrl);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!masterPlaylistUrl) {
|
if (!masterPlaylistUrl) {
|
||||||
console.log('[Vixsrc] No master playlist URL found');
|
console.log('[Vixsrc] No master playlist URL found');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { masterPlaylistUrl, subtitleApiUrl };
|
return { masterPlaylistUrl, subtitleApiUrl };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to get subtitles
|
// Helper function to get subtitles
|
||||||
function getSubtitles(subtitleApiUrl) {
|
function getSubtitles(subtitleApiUrl) {
|
||||||
return makeRequest(subtitleApiUrl)
|
return makeRequest(subtitleApiUrl)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(subtitleData => {
|
.then(subtitleData => {
|
||||||
// Find English subtitle track (same logic as original)
|
// Find English subtitle track (same logic as original)
|
||||||
let subtitleTrack = subtitleData.find(track =>
|
let subtitleTrack = subtitleData.find(track =>
|
||||||
track.display.includes('English') && (track.encoding === 'ASCII' || track.encoding === 'UTF-8')
|
track.display.includes('English') && (track.encoding === 'ASCII' || track.encoding === 'UTF-8')
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!subtitleTrack) {
|
if (!subtitleTrack) {
|
||||||
subtitleTrack = subtitleData.find(track => track.display.includes('English') && track.encoding === 'CP1252');
|
subtitleTrack = subtitleData.find(track => track.display.includes('English') && track.encoding === 'CP1252');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!subtitleTrack) {
|
if (!subtitleTrack) {
|
||||||
subtitleTrack = subtitleData.find(track => track.display.includes('English') && track.encoding === 'CP1250');
|
subtitleTrack = subtitleData.find(track => track.display.includes('English') && track.encoding === 'CP1250');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!subtitleTrack) {
|
if (!subtitleTrack) {
|
||||||
subtitleTrack = subtitleData.find(track => track.display.includes('English') && track.encoding === 'CP850');
|
subtitleTrack = subtitleData.find(track => track.display.includes('English') && track.encoding === 'CP850');
|
||||||
}
|
}
|
||||||
|
|
||||||
const subtitles = subtitleTrack ? subtitleTrack.url : '';
|
const subtitles = subtitleTrack ? subtitleTrack.url : '';
|
||||||
console.log(subtitles ? `[Vixsrc] Found subtitles: ${subtitles}` : '[Vixsrc] No English subtitles found');
|
console.log(subtitles ? `[Vixsrc] Found subtitles: ${subtitles}` : '[Vixsrc] No English subtitles found');
|
||||||
return subtitles;
|
return subtitles;
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.log('[Vixsrc] Subtitle fetch failed:', error.message);
|
console.log('[Vixsrc] Subtitle fetch failed:', error.message);
|
||||||
return '';
|
return '';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main function to get streams
|
// Main function to get streams
|
||||||
function getVixsrcStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) {
|
function getVixsrcStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) {
|
||||||
console.log(`[Vixsrc] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}`);
|
console.log(`[Vixsrc] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}`);
|
||||||
|
|
||||||
return getTmdbInfo(tmdbId, mediaType)
|
return getTmdbInfo(tmdbId, mediaType)
|
||||||
.then(() => extractStreamFromPage(null, mediaType, tmdbId, seasonNum, episodeNum))
|
.then(() => extractStreamFromPage(null, mediaType, tmdbId, seasonNum, episodeNum))
|
||||||
.then(streamData => {
|
.then(streamData => {
|
||||||
if (!streamData) {
|
if (!streamData) {
|
||||||
console.log('[Vixsrc] No stream data found');
|
console.log('[Vixsrc] No stream data found');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { masterPlaylistUrl, subtitleApiUrl } = streamData;
|
||||||
|
|
||||||
|
// Return single master playlist with Auto quality
|
||||||
|
console.log('[Vixsrc] Returning master playlist with Auto quality...');
|
||||||
|
|
||||||
|
// Get subtitles
|
||||||
|
return getSubtitles(subtitleApiUrl)
|
||||||
|
.then(subtitles => {
|
||||||
|
// Return single stream with master playlist
|
||||||
|
const tmdbStreams = [{
|
||||||
|
name: "Vixsrc",
|
||||||
|
title: "Auto Quality Stream",
|
||||||
|
url: masterPlaylistUrl,
|
||||||
|
quality: '1080p',
|
||||||
|
provider: 'Vidsrc',
|
||||||
|
headers: {
|
||||||
|
'Referer': vixsrcUrl,
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
|
||||||
|
console.log('[Vixsrc] Successfully processed 1 stream with Auto quality');
|
||||||
|
return tmdbStreams;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error(`[Vixsrc] Error in getVixsrcStreams: ${error.message}`);
|
||||||
return [];
|
return [];
|
||||||
}
|
|
||||||
|
|
||||||
const { masterPlaylistUrl, subtitleApiUrl } = streamData;
|
|
||||||
|
|
||||||
// Return single master playlist with Auto quality
|
|
||||||
console.log('[Vixsrc] Returning master playlist with Auto quality...');
|
|
||||||
|
|
||||||
// Get subtitles
|
|
||||||
return getSubtitles(subtitleApiUrl)
|
|
||||||
.then(subtitles => {
|
|
||||||
// Return single stream with master playlist
|
|
||||||
const tmdbStreams = [{
|
|
||||||
name: "Vixsrc",
|
|
||||||
title: "Auto Quality Stream",
|
|
||||||
url: masterPlaylistUrl,
|
|
||||||
quality: '1080p',
|
|
||||||
type: 'direct',
|
|
||||||
headers: {
|
|
||||||
'Referer': BASE_URL,
|
|
||||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
|
|
||||||
}
|
|
||||||
}];
|
|
||||||
|
|
||||||
console.log('[Vixsrc] Successfully processed 1 stream with Auto quality');
|
|
||||||
return tmdbStreams;
|
|
||||||
});
|
});
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error(`[Vixsrc] Error in getVixsrcStreams: ${error.message}`);
|
|
||||||
return [];
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { getVixsrcStreams };
|
module.exports = { getVixsrcStreams };
|
||||||
|
|||||||
@@ -1,158 +0,0 @@
|
|||||||
// xprime.js (simplified & cleaned)
|
|
||||||
require('dotenv').config();
|
|
||||||
const fs = require('fs').promises;
|
|
||||||
const path = require('path');
|
|
||||||
const crypto = require('crypto');
|
|
||||||
|
|
||||||
const RETRY_DELAY_MS_XPRIME = 1000;
|
|
||||||
const MAX_RETRIES_XPRIME = 3;
|
|
||||||
|
|
||||||
// Cache directory (Vercel ephemeral vs local)
|
|
||||||
const CACHE_DIR = process.env.VERCEL ? path.join('/tmp', '.cache') : path.join(__dirname, '.cache');
|
|
||||||
|
|
||||||
async function ensureCacheDir(dirPath) {
|
|
||||||
try { await fs.mkdir(dirPath, { recursive: true }); } catch (_) {
|
|
||||||
// swallow errors creating cache dir (ephemeral env or permission)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getFromCache(cacheKey, subDir = '') {
|
|
||||||
if (process.env.DISABLE_CACHE === 'true') return null;
|
|
||||||
try {
|
|
||||||
const fullDir = path.join(CACHE_DIR, subDir);
|
|
||||||
const filePath = path.join(fullDir, cacheKey);
|
|
||||||
const data = await fs.readFile(filePath, 'utf8');
|
|
||||||
return data;
|
|
||||||
} catch (_) { return null; }
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveToCache(cacheKey, data, subDir = '') {
|
|
||||||
if (process.env.DISABLE_CACHE === 'true') return;
|
|
||||||
try {
|
|
||||||
const fullDir = path.join(CACHE_DIR, subDir);
|
|
||||||
await fs.mkdir(fullDir, { recursive: true });
|
|
||||||
const filePath = path.join(fullDir, cacheKey);
|
|
||||||
await fs.writeFile(filePath, data, 'utf8');
|
|
||||||
} catch (_) {
|
|
||||||
// ignore cache write errors (non-fatal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const BROWSER_HEADERS_XPRIME = {
|
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36',
|
|
||||||
'Accept': '*/*',
|
|
||||||
'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8',
|
|
||||||
'Cache-Control': 'no-cache',
|
|
||||||
'Pragma': 'no-cache',
|
|
||||||
'Sec-Ch-Ua': '"Chromium";v="136", "Google Chrome";v="136", "Not.A/Brand";v="99"',
|
|
||||||
'Sec-Ch-Ua-Mobile': '?0',
|
|
||||||
'Sec-Ch-Ua-Platform': '"Windows"',
|
|
||||||
'Connection': 'keep-alive'
|
|
||||||
};
|
|
||||||
|
|
||||||
async function fetchWithRetry(url, options, maxRetries = MAX_RETRIES_XPRIME) {
|
|
||||||
const { default: fetch } = await import('node-fetch');
|
|
||||||
let lastError;
|
|
||||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
||||||
try {
|
|
||||||
const res = await fetch(url, options);
|
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
||||||
return res;
|
|
||||||
} catch (err) {
|
|
||||||
lastError = err;
|
|
||||||
if (attempt < maxRetries) {
|
|
||||||
await new Promise(r => setTimeout(r, RETRY_DELAY_MS_XPRIME * Math.pow(2, attempt - 1)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw lastError;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchStreamSize(url) {
|
|
||||||
const subDir = 'xprime_stream_sizes';
|
|
||||||
const hash = crypto.createHash('md5').update(url).digest('hex');
|
|
||||||
const key = `${hash}.txt`;
|
|
||||||
const cached = await getFromCache(key, subDir);
|
|
||||||
if (cached) return cached;
|
|
||||||
if (url.toLowerCase().includes('.m3u8')) {
|
|
||||||
await saveToCache(key, 'Playlist (size N/A)', subDir);
|
|
||||||
return 'Playlist (size N/A)';
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const { default: fetch } = await import('node-fetch');
|
|
||||||
const controller = new AbortController();
|
|
||||||
const t = setTimeout(() => controller.abort(), 5000);
|
|
||||||
let size = 'Unknown size';
|
|
||||||
try {
|
|
||||||
const head = await fetch(url, { method: 'HEAD', signal: controller.signal });
|
|
||||||
const cl = head.headers.get('content-length');
|
|
||||||
if (cl) {
|
|
||||||
const bytes = parseInt(cl, 10);
|
|
||||||
if (!isNaN(bytes)) {
|
|
||||||
if (bytes < 1024) size = `${bytes} B`;
|
|
||||||
else if (bytes < 1024 * 1024) size = `${(bytes / 1024).toFixed(2)} KB`;
|
|
||||||
else if (bytes < 1024 * 1024 * 1024) size = `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
||||||
else size = `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally { clearTimeout(t); }
|
|
||||||
await saveToCache(key, size, subDir);
|
|
||||||
return size;
|
|
||||||
} catch (_) {
|
|
||||||
await saveToCache(key, 'Unknown size', subDir);
|
|
||||||
return 'Unknown size';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getXprimeStreams(title, year, type, seasonNum, episodeNum) {
|
|
||||||
if (!title || !year) return [];
|
|
||||||
const encoded = encodeURIComponent(title);
|
|
||||||
let apiUrl;
|
|
||||||
if (type === 'movie') {
|
|
||||||
apiUrl = `https://backend.xprime.tv/primebox?name=${encoded}&year=${year}&fallback_year=${year}`;
|
|
||||||
} else if (type === 'tv') {
|
|
||||||
if (seasonNum == null || episodeNum == null) return [];
|
|
||||||
apiUrl = `https://backend.xprime.tv/primebox?name=${encoded}&year=${year}&fallback_year=${year}&season=${seasonNum}&episode=${episodeNum}`;
|
|
||||||
} else return [];
|
|
||||||
|
|
||||||
console.log(`[Xprime.tv] Fetch attempt '${title}' (${year}) type=${type}`);
|
|
||||||
|
|
||||||
let resJson;
|
|
||||||
try {
|
|
||||||
const r = await fetchWithRetry(apiUrl, { headers: BROWSER_HEADERS_XPRIME });
|
|
||||||
resJson = await r.json();
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[Xprime.tv] Fetch failed:', e.message);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const streams = [];
|
|
||||||
const processItem = (item) => {
|
|
||||||
if (!item || item.error || !item.streams) return;
|
|
||||||
Object.entries(item.streams).forEach(([quality, url]) => {
|
|
||||||
if (!url || typeof url !== 'string') return;
|
|
||||||
streams.push({
|
|
||||||
url,
|
|
||||||
quality: quality || 'Unknown',
|
|
||||||
title: `${title} - ${type === 'tv' ? `S${String(seasonNum).padStart(2,'0')}E${String(episodeNum).padStart(2,'0')} ` : ''}${quality}`,
|
|
||||||
provider: 'Xprime.tv',
|
|
||||||
codecs: [],
|
|
||||||
size: 'Unknown size'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
if (Array.isArray(resJson)) resJson.forEach(processItem); else processItem(resJson);
|
|
||||||
|
|
||||||
if (streams.length) {
|
|
||||||
console.time('[Xprime.tv] Fetch stream sizes');
|
|
||||||
await Promise.all(streams.map(async s => { s.size = await fetchStreamSize(s.url); }));
|
|
||||||
console.timeEnd('[Xprime.tv] Fetch stream sizes');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[Xprime.tv] Returning ${streams.length} streams.`);
|
|
||||||
return streams;
|
|
||||||
}
|
|
||||||
|
|
||||||
ensureCacheDir(CACHE_DIR).catch(()=>{});
|
|
||||||
|
|
||||||
module.exports = { getXprimeStreams };
|
|
||||||
@@ -0,0 +1,451 @@
|
|||||||
|
// Lightweight proxy/m3u8/segment/subtitle rewriting layer. thanks to https://github.com/cinepro-org/ for the original code.
|
||||||
|
// Adapted from external project (user supplied) to fit CommonJS style and existing config system.
|
||||||
|
// Provides conditional route registration via enableProxy flag.
|
||||||
|
|
||||||
|
const cors = require('cors');
|
||||||
|
const fetch = require('node-fetch');
|
||||||
|
|
||||||
|
const CACHE_MAX_SIZE = 2000;
|
||||||
|
const CACHE_EXPIRY_MS = 2 * 60 * 60 * 1000; // 2 hours
|
||||||
|
const segmentCache = new Map();
|
||||||
|
// Track first open-ended (bytes=0-) range per target to clamp only once per TTL window
|
||||||
|
const openRangeClampMap = new Map();
|
||||||
|
const OPEN_RANGE_CLAMP_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
||||||
|
// Progressive open-ended growth tracking: keeps expanding bytes=0- served window.
|
||||||
|
const progressiveOpenMap = new Map(); // url -> { lastEnd }
|
||||||
|
// Tail prefetch cache: stores last N KB for quick tail range responses
|
||||||
|
const tailPrefetchMap = new Map(); // url -> { data: Buffer, start: number, end: number, size: number, ts: number }
|
||||||
|
const TAIL_PREFETCH_TTL_MS = 10 * 60 * 1000; // 10 minutes
|
||||||
|
|
||||||
|
function isCacheDisabled() {
|
||||||
|
return process.env.DISABLE_CACHE === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanupCache() {
|
||||||
|
const now = Date.now();
|
||||||
|
for (const [url, entry] of segmentCache.entries()) {
|
||||||
|
if (now - entry.timestamp > CACHE_EXPIRY_MS) segmentCache.delete(url);
|
||||||
|
}
|
||||||
|
if (segmentCache.size > CACHE_MAX_SIZE) {
|
||||||
|
const entries = Array.from(segmentCache.entries()).sort((a, b) => a[1].timestamp - b[1].timestamp);
|
||||||
|
const toRemove = entries.slice(0, segmentCache.size - CACHE_MAX_SIZE);
|
||||||
|
toRemove.forEach(([u]) => segmentCache.delete(u));
|
||||||
|
}
|
||||||
|
return segmentCache.size;
|
||||||
|
}
|
||||||
|
setInterval(cleanupCache, 30 * 60 * 1000).unref();
|
||||||
|
|
||||||
|
function cleanupClampMap() {
|
||||||
|
const now = Date.now();
|
||||||
|
for (const [url, ts] of openRangeClampMap.entries()) {
|
||||||
|
if (now - ts > OPEN_RANGE_CLAMP_TTL_MS) openRangeClampMap.delete(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setInterval(cleanupClampMap, 10 * 60 * 1000).unref();
|
||||||
|
|
||||||
|
function cleanupTailPrefetch() {
|
||||||
|
const now = Date.now();
|
||||||
|
for (const [url, entry] of tailPrefetchMap.entries()) {
|
||||||
|
if (now - entry.ts > TAIL_PREFETCH_TTL_MS) tailPrefetchMap.delete(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setInterval(cleanupTailPrefetch, 15 * 60 * 1000).unref();
|
||||||
|
|
||||||
|
function getCachedSegment(url) {
|
||||||
|
if (isCacheDisabled()) return null;
|
||||||
|
const e = segmentCache.get(url);
|
||||||
|
if (!e) return null;
|
||||||
|
if (Date.now() - e.timestamp > CACHE_EXPIRY_MS) { segmentCache.delete(url); return null; }
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function prefetchSegment(url, headers) {
|
||||||
|
if (isCacheDisabled() || segmentCache.size >= CACHE_MAX_SIZE) return;
|
||||||
|
const existing = segmentCache.get(url);
|
||||||
|
if (existing && Date.now() - existing.timestamp <= CACHE_EXPIRY_MS) return;
|
||||||
|
try {
|
||||||
|
const resp = await fetch(url, { headers: { 'User-Agent': DEFAULT_UA, ...headers } });
|
||||||
|
if (!resp.ok) return;
|
||||||
|
const data = new Uint8Array(await resp.arrayBuffer());
|
||||||
|
const responseHeaders = {};
|
||||||
|
resp.headers.forEach((v, k) => responseHeaders[k] = v);
|
||||||
|
segmentCache.set(url, { data, headers: responseHeaders, timestamp: Date.now() });
|
||||||
|
} catch (_e) { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
|
||||||
|
|
||||||
|
function inferContentType(upstreamCT, targetUrl) {
|
||||||
|
const bad = !upstreamCT || /application\/octet-stream/i.test(upstreamCT) || /application\/(x-)?zip/i.test(upstreamCT);
|
||||||
|
if (!bad) return upstreamCT;
|
||||||
|
if (/\.mkv(\?|$)/i.test(targetUrl)) return 'video/x-matroska';
|
||||||
|
if (/\.mp4(\?|$)/i.test(targetUrl)) return 'video/mp4';
|
||||||
|
if (/\.m3u8(\?|$)/i.test(targetUrl)) return 'application/vnd.apple.mpegurl';
|
||||||
|
if (/\.ts(\?|$)/i.test(targetUrl)) return 'video/mp2t';
|
||||||
|
return 'application/octet-stream';
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractOriginalUrl(proxyUrl) {
|
||||||
|
try {
|
||||||
|
const url = new URL(proxyUrl);
|
||||||
|
if (url.pathname.includes('/proxy/')) {
|
||||||
|
const m = url.pathname.match(/\/proxy\/(.+)$/);
|
||||||
|
if (m) {
|
||||||
|
let decoded = decodeURIComponent(m[1]);
|
||||||
|
while (decoded.includes('%2F')) {
|
||||||
|
try { decoded = decodeURIComponent(decoded); } catch { break; }
|
||||||
|
}
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (url.searchParams.has('url')) return decodeURIComponent(url.searchParams.get('url'));
|
||||||
|
const patterns = [
|
||||||
|
/\/api\/[^/]+\/proxy\?url=(.+)$/,
|
||||||
|
/\/proxy\?.*url=([^&]+)/,
|
||||||
|
/\/stream\/proxy\/(.+)$/,
|
||||||
|
/\/p\/(.+)$/
|
||||||
|
];
|
||||||
|
for (const p of patterns) {
|
||||||
|
const m = proxyUrl.match(p); if (m) return decodeURIComponent(m[1]);
|
||||||
|
}
|
||||||
|
return proxyUrl;
|
||||||
|
} catch { return proxyUrl; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function rewriteM3u8(content, targetUrl, baseProxyUrl, headers) {
|
||||||
|
const lines = content.split('\n');
|
||||||
|
const out = []; const segmentUrls = [];
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.startsWith('#')) {
|
||||||
|
if (line.startsWith('#EXT-X-KEY:')) {
|
||||||
|
const regex = /https?:\/\/[^""\s]+/g; const keyUrl = regex.exec(line)?.[0];
|
||||||
|
if (keyUrl) {
|
||||||
|
const proxyUrl = `${baseProxyUrl}/ts-proxy?url=${encodeURIComponent(keyUrl)}&headers=${encodeURIComponent(JSON.stringify(headers))}`;
|
||||||
|
out.push(line.replace(keyUrl, proxyUrl));
|
||||||
|
if (!isCacheDisabled()) prefetchSegment(keyUrl, headers);
|
||||||
|
} else out.push(line);
|
||||||
|
} else if (line.startsWith('#EXT-X-MEDIA:') || line.startsWith('#EXT-X-I-FRAME-STREAM-INF:')) {
|
||||||
|
const uriMatch = line.match(/URI="([^"]+)"/);
|
||||||
|
if (uriMatch) {
|
||||||
|
try {
|
||||||
|
const mediaUrl = new URL(uriMatch[1], targetUrl).href;
|
||||||
|
const proxyUrl = `${baseProxyUrl}/m3u8-proxy?url=${encodeURIComponent(mediaUrl)}&headers=${encodeURIComponent(JSON.stringify(headers))}`;
|
||||||
|
out.push(line.replace(uriMatch[1], proxyUrl));
|
||||||
|
} catch { out.push(line); }
|
||||||
|
} else out.push(line);
|
||||||
|
} else out.push(line);
|
||||||
|
} else if (line.trim()) {
|
||||||
|
try {
|
||||||
|
const abs = new URL(line, targetUrl).href;
|
||||||
|
if (/\.m3u8(\?|$)/i.test(abs)) {
|
||||||
|
out.push(`${baseProxyUrl}/m3u8-proxy?url=${encodeURIComponent(abs)}&headers=${encodeURIComponent(JSON.stringify(headers))}`);
|
||||||
|
} else if (/\.ts(\?|$)/i.test(abs)) {
|
||||||
|
segmentUrls.push(abs);
|
||||||
|
out.push(`${baseProxyUrl}/ts-proxy?url=${encodeURIComponent(abs)}&headers=${encodeURIComponent(JSON.stringify(headers))}`);
|
||||||
|
} else out.push(line);
|
||||||
|
} catch { out.push(line); }
|
||||||
|
} else out.push(line);
|
||||||
|
}
|
||||||
|
if (segmentUrls.length && !isCacheDisabled()) {
|
||||||
|
Promise.all(segmentUrls.map(u => prefetchSegment(u, headers))).catch(() => undefined);
|
||||||
|
}
|
||||||
|
return out.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function createProxyRoutes(app) {
|
||||||
|
// m3u8 playlist proxy
|
||||||
|
app.get('/m3u8-proxy', cors(), async (req, res) => {
|
||||||
|
const targetUrl = req.query.url; if (!targetUrl) return res.status(400).json({ error: 'URL parameter required' });
|
||||||
|
let headers = {};
|
||||||
|
try { headers = JSON.parse(req.query.headers || '{}'); } catch {
|
||||||
|
// Ignore URL parsing errors
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await fetch(targetUrl, { headers: { 'User-Agent': DEFAULT_UA, ...headers } });
|
||||||
|
if (!response.ok) return res.status(response.status).json({ error: `M3U8 fetch failed: ${response.status}` });
|
||||||
|
const text = await response.text();
|
||||||
|
const protocol = req.headers['x-forwarded-proto'] || req.protocol || 'http';
|
||||||
|
const baseProxyUrl = `${protocol}://${req.get('host')}`;
|
||||||
|
const rewritten = rewriteM3u8(text, targetUrl, baseProxyUrl, headers);
|
||||||
|
res.setHeader('Content-Type', 'application/vnd.apple.mpegurl');
|
||||||
|
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
res.send(rewritten);
|
||||||
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||||
|
});
|
||||||
|
|
||||||
|
// ts / key segment proxy
|
||||||
|
app.get('/ts-proxy', cors(), async (req, res) => {
|
||||||
|
const targetUrl = req.query.url; if (!targetUrl) return res.status(400).json({ error: 'URL parameter required' });
|
||||||
|
const debug = req.query.debug === '1';
|
||||||
|
const noSynth = req.query.noSynth === '1';
|
||||||
|
const force200 = req.query.force200 === '1';
|
||||||
|
const clampOpen = req.query.clampOpen !== '0';
|
||||||
|
const progressiveOpen = req.query.progressiveOpen !== '0';
|
||||||
|
const tailPrefetchEnabled = req.query.tailPrefetch !== '0';
|
||||||
|
let tailPrefetchKB = parseInt(req.query.tailPrefetchKB || '256', 10);
|
||||||
|
if (isNaN(tailPrefetchKB) || tailPrefetchKB < 64) tailPrefetchKB = 256;
|
||||||
|
if (tailPrefetchKB > 2048) tailPrefetchKB = 2048; // max 2MB tail window
|
||||||
|
let openChunkKB = parseInt(req.query.openChunkKB || '4096', 10);
|
||||||
|
if (isNaN(openChunkKB) || openChunkKB < 64) openChunkKB = 4096;
|
||||||
|
if (openChunkKB > 16384) openChunkKB = 16384; // cap at 16MB
|
||||||
|
let initChunkKB = parseInt(req.query.initChunkKB || '512', 10);
|
||||||
|
if (isNaN(initChunkKB) || initChunkKB < 64) initChunkKB = 512;
|
||||||
|
if (initChunkKB > 2048) initChunkKB = 2048; // hard cap 2MB
|
||||||
|
let headers = {}; try { headers = JSON.parse(req.query.headers || '{}'); } catch {
|
||||||
|
// Ignore URL parsing errors
|
||||||
|
}
|
||||||
|
// Pass through Range header for progressive playback / seeking
|
||||||
|
const range = req.headers['range'];
|
||||||
|
let appliedClamp = false;
|
||||||
|
let effectiveRange = range;
|
||||||
|
if (range) headers.Range = range;
|
||||||
|
// Clamp first open-ended bytes=0- request to a bounded span (once per TTL) to avoid gigantic initial 206
|
||||||
|
if (range && /^bytes=0-\s*$/i.test(range) && !force200) {
|
||||||
|
const now = Date.now();
|
||||||
|
// Progressive growth supersedes single-shot clamp if enabled
|
||||||
|
if (progressiveOpen) {
|
||||||
|
const prog = progressiveOpenMap.get(targetUrl) || { lastEnd: -1 };
|
||||||
|
const prevEnd = prog.lastEnd;
|
||||||
|
let nextEnd;
|
||||||
|
if (prog.lastEnd < 0) {
|
||||||
|
nextEnd = openChunkKB * 1024 - 1;
|
||||||
|
} else {
|
||||||
|
const increment = openChunkKB * 1024;
|
||||||
|
nextEnd = prog.lastEnd + increment;
|
||||||
|
const maxCap = 256 * 1024 * 1024 - 1; // 256MB
|
||||||
|
if (nextEnd > maxCap) nextEnd = maxCap;
|
||||||
|
}
|
||||||
|
const newRange = `bytes=0-${nextEnd}`;
|
||||||
|
headers.Range = newRange;
|
||||||
|
effectiveRange = newRange;
|
||||||
|
prog.lastEnd = nextEnd;
|
||||||
|
progressiveOpenMap.set(targetUrl, prog);
|
||||||
|
appliedClamp = true; // reuse flag for logging
|
||||||
|
if (debug) console.log('[ts-proxy] progressiveOpen expand', { prevEnd, nextEnd, increment: openChunkKB * 1024 });
|
||||||
|
} else if (clampOpen) {
|
||||||
|
const lastTs = openRangeClampMap.get(targetUrl);
|
||||||
|
if (!lastTs || (now - lastTs) > OPEN_RANGE_CLAMP_TTL_MS) {
|
||||||
|
const clampBytes = openChunkKB * 1024;
|
||||||
|
const newRange = `bytes=0-${clampBytes - 1}`;
|
||||||
|
headers.Range = newRange;
|
||||||
|
effectiveRange = newRange;
|
||||||
|
openRangeClampMap.set(targetUrl, now);
|
||||||
|
appliedClamp = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (debug) console.log('[ts-proxy] incoming', { url: targetUrl, range, effectiveRange, appliedClamp, clampOpen, progressiveOpen, openChunkKB, noSynth, initChunkKB, force200, tailPrefetchEnabled, tailPrefetchKB });
|
||||||
|
if (!isCacheDisabled()) {
|
||||||
|
const cached = getCachedSegment(targetUrl);
|
||||||
|
if (cached) {
|
||||||
|
const ct = inferContentType(cached.headers['content-type'], targetUrl);
|
||||||
|
res.setHeader('Content-Type', ct);
|
||||||
|
res.setHeader('Cache-Control', 'public, max-age=3600');
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
res.setHeader('Accept-Ranges', 'bytes');
|
||||||
|
if (debug) console.log('[ts-proxy] cache hit');
|
||||||
|
return res.send(Buffer.from(cached.data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// If we have a tail prefetch entry and the client requests a tail range fully inside it, serve from memory early.
|
||||||
|
if (effectiveRange && /^bytes=\d+-$/i.test(effectiveRange)) {
|
||||||
|
const tailEntry = tailPrefetchMap.get(targetUrl);
|
||||||
|
if (tailEntry) {
|
||||||
|
const m = effectiveRange.match(/bytes=(\d+)-/i);
|
||||||
|
if (m) {
|
||||||
|
const startReq = parseInt(m[1], 10);
|
||||||
|
if (!isNaN(startReq) && startReq >= tailEntry.start && startReq <= tailEntry.end) {
|
||||||
|
const offset = startReq - tailEntry.start;
|
||||||
|
const slice = tailEntry.data.subarray(offset);
|
||||||
|
const endByte = tailEntry.end;
|
||||||
|
res.status(206);
|
||||||
|
const ct = inferContentType(null, targetUrl);
|
||||||
|
res.setHeader('Content-Type', ct);
|
||||||
|
res.setHeader('Accept-Ranges', 'bytes');
|
||||||
|
res.setHeader('Content-Length', slice.length.toString());
|
||||||
|
res.setHeader('Content-Range', `bytes ${startReq}-${endByte}/${tailEntry.size}`);
|
||||||
|
res.setHeader('Cache-Control', 'public, max-age=3600');
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
if (debug) console.log('[ts-proxy] tail serve hit', { startReq, cachedStart: tailEntry.start, cachedEnd: tailEntry.end, size: tailEntry.size });
|
||||||
|
return res.end(slice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HEAD preflight (only when no explicit Range requested) to get size for synthesized partial response
|
||||||
|
let contentLength = null;
|
||||||
|
let upstreamAcceptRanges = null;
|
||||||
|
if (!effectiveRange) {
|
||||||
|
try {
|
||||||
|
const headResp = await fetch(targetUrl, { method: 'HEAD', headers: { 'User-Agent': DEFAULT_UA, ...headers } });
|
||||||
|
if (headResp.ok) {
|
||||||
|
contentLength = headResp.headers.get('content-length');
|
||||||
|
upstreamAcceptRanges = headResp.headers.get('accept-ranges');
|
||||||
|
if (debug) console.log('[ts-proxy] HEAD ok', { contentLength, upstreamAcceptRanges });
|
||||||
|
} else if (debug) {
|
||||||
|
console.log('[ts-proxy] HEAD status', headResp.status);
|
||||||
|
}
|
||||||
|
} catch (_e) { if (debug) console.log('[ts-proxy] HEAD failed'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback probe: if still no size, attempt a 0-0 range request to derive total size from Content-Range
|
||||||
|
if (!effectiveRange && !contentLength) {
|
||||||
|
try {
|
||||||
|
const probeRange = 'bytes=0-0';
|
||||||
|
if (debug) console.log('[ts-proxy] probe range');
|
||||||
|
const probeResp = await fetch(targetUrl, { headers: { 'User-Agent': DEFAULT_UA, ...headers, 'Range': probeRange } });
|
||||||
|
if (probeResp.status === 206) {
|
||||||
|
const cr = probeResp.headers.get('content-range');
|
||||||
|
if (cr) {
|
||||||
|
const m = cr.match(/\/(\d+)$/); if (m) contentLength = m[1];
|
||||||
|
}
|
||||||
|
try { await probeResp.arrayBuffer(); } catch (readErr) {
|
||||||
|
if (debug) console.log('[ts-proxy] probe body read failed', readErr.message);
|
||||||
|
}
|
||||||
|
if (debug) console.log('[ts-proxy] probe success', { contentLength });
|
||||||
|
} else if (debug) {
|
||||||
|
console.log('[ts-proxy] probe status', probeResp.status);
|
||||||
|
}
|
||||||
|
} catch (_e) { if (debug) console.log('[ts-proxy] probe failed'); }
|
||||||
|
}
|
||||||
|
if (debug && contentLength) console.log('[ts-proxy] size determined', contentLength);
|
||||||
|
|
||||||
|
// Initiate tail prefetch asynchronously (once) when size known and feature enabled
|
||||||
|
if (tailPrefetchEnabled && contentLength && !tailPrefetchMap.has(targetUrl)) {
|
||||||
|
const total = parseInt(contentLength, 10);
|
||||||
|
if (!isNaN(total) && total > 0) {
|
||||||
|
const tailBytes = Math.min(tailPrefetchKB * 1024, total);
|
||||||
|
const start = Math.max(0, total - tailBytes);
|
||||||
|
const tailRange = `bytes=${start}-`;
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
if (debug) console.log('[ts-proxy] tail prefetch start', { tailRange });
|
||||||
|
const tr = await fetch(targetUrl, { headers: { 'User-Agent': DEFAULT_UA, ...headers, Range: tailRange } });
|
||||||
|
if (tr.status === 206) {
|
||||||
|
const buf = Buffer.from(await tr.arrayBuffer());
|
||||||
|
const cr = tr.headers.get('content-range');
|
||||||
|
if (cr) {
|
||||||
|
const mm = cr.match(/bytes\s+(\d+)-(\d+)\/(\d+)/i);
|
||||||
|
if (mm) {
|
||||||
|
const s = parseInt(mm[1], 10); const e = parseInt(mm[2], 10); const sz = parseInt(mm[3], 10);
|
||||||
|
if (!isNaN(s) && !isNaN(e) && !isNaN(sz)) {
|
||||||
|
tailPrefetchMap.set(targetUrl, { data: buf, start: s, end: e, size: sz, ts: Date.now() });
|
||||||
|
if (debug) console.log('[ts-proxy] tail prefetch success', { start: s, end: e, bytes: buf.length });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (debug) {
|
||||||
|
console.log('[ts-proxy] tail prefetch status', tr.status);
|
||||||
|
}
|
||||||
|
} catch (e) { if (debug) console.log('[ts-proxy] tail prefetch error', e.message); }
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no range requested but we now know size (HEAD or probe), synthesize an initial small range fetch to accelerate playback.
|
||||||
|
if (!force200 && !effectiveRange && contentLength && !noSynth && req.query.progressiveOpen === '0') {
|
||||||
|
const total = parseInt(contentLength, 10);
|
||||||
|
const desired = initChunkKB * 1024;
|
||||||
|
const chunkSize = Math.min(desired, Math.max(0, total - 1));
|
||||||
|
const syntheticRange = `bytes=0-${chunkSize}`;
|
||||||
|
const resp = await fetch(targetUrl, { headers: { 'User-Agent': DEFAULT_UA, ...headers, 'Range': syntheticRange } });
|
||||||
|
if (resp.status === 206) {
|
||||||
|
if (debug) console.log('[ts-proxy] synthetic 206', syntheticRange);
|
||||||
|
const upstreamCT = inferContentType(resp.headers.get('content-type'), targetUrl);
|
||||||
|
res.status(206);
|
||||||
|
res.setHeader('Content-Type', upstreamCT);
|
||||||
|
res.setHeader('Accept-Ranges', upstreamAcceptRanges || 'bytes');
|
||||||
|
const contentRange = resp.headers.get('content-range');
|
||||||
|
if (contentRange) res.setHeader('Content-Range', contentRange);
|
||||||
|
const cl = resp.headers.get('content-length'); if (cl) res.setHeader('Content-Length', cl);
|
||||||
|
res.setHeader('Cache-Control', 'public, max-age=3600');
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
// Pipe first chunk then leave connection open? Simpler: end after chunk; player will request next range.
|
||||||
|
return resp.body.pipe(res);
|
||||||
|
}
|
||||||
|
// If server ignored range (e.g., returned 200), fall through to normal logic below.
|
||||||
|
}
|
||||||
|
|
||||||
|
const upstreamOptions = { headers: { 'User-Agent': DEFAULT_UA, ...headers } };
|
||||||
|
// If client supplied a suffix range like bytes=0- we pass it; force200 strips Range
|
||||||
|
if (force200 && headers.Range) delete headers.Range;
|
||||||
|
const resp = await fetch(targetUrl, upstreamOptions);
|
||||||
|
if (!resp.ok && resp.status !== 206) {
|
||||||
|
if (debug) console.log('[ts-proxy] upstream error', resp.status);
|
||||||
|
return res.status(resp.status).json({ error: `TS fetch failed: ${resp.status}` });
|
||||||
|
}
|
||||||
|
// Forward partial content status when range used
|
||||||
|
if (effectiveRange && resp.status === 206 && !force200) { res.status(206); if (debug) console.log('[ts-proxy] forwarding 206', { appliedClamp }); }
|
||||||
|
if (force200 && resp.status === 206) {
|
||||||
|
// Some origins still return 206 even without Range; normalize to 200 for players expecting full stream
|
||||||
|
if (debug) console.log('[ts-proxy] normalizing 206 -> 200 due to force200');
|
||||||
|
}
|
||||||
|
// Copy key streaming headers
|
||||||
|
const upstreamCT = inferContentType(resp.headers.get('content-type'), targetUrl);
|
||||||
|
res.setHeader('Content-Type', upstreamCT);
|
||||||
|
let cl = resp.headers.get('content-length');
|
||||||
|
const crHeader = resp.headers.get('content-range');
|
||||||
|
if (!cl && crHeader) {
|
||||||
|
// Try to derive length from range span
|
||||||
|
const m = crHeader.match(/bytes\s+(\d+)-(\d+)\/(\d+|\*)/i);
|
||||||
|
if (m) {
|
||||||
|
const start = parseInt(m[1], 10); const end = parseInt(m[2], 10);
|
||||||
|
if (!isNaN(start) && !isNaN(end)) cl = (end - start + 1).toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cl) res.setHeader('Content-Length', cl);
|
||||||
|
const acceptRanges = resp.headers.get('accept-ranges');
|
||||||
|
if (acceptRanges) res.setHeader('Accept-Ranges', acceptRanges); else res.setHeader('Accept-Ranges', 'bytes');
|
||||||
|
const contentRange = resp.headers.get('content-range'); if (contentRange && !force200) res.setHeader('Content-Range', contentRange);
|
||||||
|
res.setHeader('Cache-Control', 'public, max-age=3600');
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
if (debug) console.log('[ts-proxy] streaming body', { status: resp.status, ct: upstreamCT, cl, contentRange: force200 ? undefined : contentRange });
|
||||||
|
resp.body.pipe(res);
|
||||||
|
} catch (e) { if (debug) console.log('[ts-proxy] exception', e.message); res.status(500).json({ error: e.message }); }
|
||||||
|
});
|
||||||
|
|
||||||
|
// subtitle proxy
|
||||||
|
app.get('/sub-proxy', cors(), async (req, res) => {
|
||||||
|
const targetUrl = req.query.url; if (!targetUrl) return res.status(400).json({ error: 'url parameter required' });
|
||||||
|
let headers = {}; try { headers = JSON.parse(req.query.headers || '{}'); } catch {
|
||||||
|
// Ignore URL parsing errors
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const resp = await fetch(targetUrl, { headers: { 'User-Agent': DEFAULT_UA, ...headers } });
|
||||||
|
if (!resp.ok) return res.status(resp.status).json({ error: `subtitle fetch failed: ${resp.status}` });
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
res.setHeader('Cache-Control', 'public, max-age=3600');
|
||||||
|
res.setHeader('Content-Type', resp.headers.get('content-type') || 'text/vtt');
|
||||||
|
resp.body.pipe(res);
|
||||||
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function processStreamsForProxy(streams, serverUrl) {
|
||||||
|
if (!Array.isArray(streams)) return streams;
|
||||||
|
return streams.map(s => {
|
||||||
|
if (!s || !s.url || typeof s.url !== 'string') return s;
|
||||||
|
const original = extractOriginalUrl(s.url);
|
||||||
|
const headers = s.headers || {};
|
||||||
|
const hParam = Object.keys(headers).length ? `&headers=${encodeURIComponent(JSON.stringify(headers))}` : '';
|
||||||
|
let host = '';
|
||||||
|
try {
|
||||||
|
host = new URL(original).hostname.toLowerCase();
|
||||||
|
} catch {
|
||||||
|
// Ignore errors
|
||||||
|
}
|
||||||
|
// Force specific hosts through ts-proxy (direct file style) even without extension
|
||||||
|
if (host.includes('pixeldrain.') || host === 'video-downloads.googleusercontent.com') {
|
||||||
|
return { ...s, url: `${serverUrl}/ts-proxy?url=${encodeURIComponent(original)}${hParam}` };
|
||||||
|
}
|
||||||
|
if (/\.(mp4|mkv)(\?|$)/i.test(original)) {
|
||||||
|
return { ...s, url: `${serverUrl}/ts-proxy?url=${encodeURIComponent(original)}${hParam}` };
|
||||||
|
}
|
||||||
|
return { ...s, url: `${serverUrl}/m3u8-proxy?url=${encodeURIComponent(original)}${hParam}` };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { createProxyRoutes, processStreamsForProxy };
|
||||||
@@ -177,6 +177,7 @@
|
|||||||
<label class="adv-flag-item"><input type="checkbox" name="adv_disableCache" value="1" /> <span>Disable Cache</span></label>
|
<label class="adv-flag-item"><input type="checkbox" name="adv_disableCache" value="1" /> <span>Disable Cache</span></label>
|
||||||
<label class="adv-flag-item"><input type="checkbox" name="adv_disableUrlValidation" value="1" /> <span>Disable URL Validation</span></label>
|
<label class="adv-flag-item"><input type="checkbox" name="adv_disableUrlValidation" value="1" /> <span>Disable URL Validation</span></label>
|
||||||
<label class="adv-flag-item"><input type="checkbox" name="adv_disable4khdhubValidation" value="1" /> <span>Disable 4KHDHub Validation</span></label>
|
<label class="adv-flag-item"><input type="checkbox" name="adv_disable4khdhubValidation" value="1" /> <span>Disable 4KHDHub Validation</span></label>
|
||||||
|
<label class="adv-flag-item"><input type="checkbox" name="adv_enableProxy" value="1" /> <span>Enable Stream Proxy</span></label>
|
||||||
</div>
|
</div>
|
||||||
<h1>Paths</h1>
|
<h1>Paths</h1>
|
||||||
<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>
|
||||||
|
|||||||
+22
-12
@@ -160,12 +160,12 @@ function fillForm(data){
|
|||||||
tmdbBanner.style.display = (tmdbMissing && tmdbProvidersEnabled)? 'block':'none';
|
tmdbBanner.style.display = (tmdbMissing && tmdbProvidersEnabled)? 'block':'none';
|
||||||
}
|
}
|
||||||
const advBools = [
|
const advBools = [
|
||||||
['enableXprimeProvider','adv_enableXprime'],
|
|
||||||
['enable4khdhubProvider','adv_enable4khdhub'],
|
['enable4khdhubProvider','adv_enable4khdhub'],
|
||||||
['enableMoviesmodProvider','adv_enableMoviesmod'],
|
['enableMoviesmodProvider','adv_enableMoviesmod'],
|
||||||
['enableMp4hydraProvider','adv_enableMp4hydra'],
|
['enableMp4hydraProvider','adv_enableMp4hydra'],
|
||||||
['enableVidzeeProvider','adv_enableVidzee'],
|
['enableVidzeeProvider','adv_enableVidzee'],
|
||||||
['enableVixsrcProvider','adv_enableVixsrc'],
|
['enableVixsrcProvider','adv_enableVixsrc'],
|
||||||
|
['enableProxy','adv_enableProxy'],
|
||||||
['disableCache','adv_disableCache'],
|
['disableCache','adv_disableCache'],
|
||||||
['enablePStreamApi','adv_enablePStream'],
|
['enablePStreamApi','adv_enablePStream'],
|
||||||
['disableUrlValidation','adv_disableUrlValidation'],
|
['disableUrlValidation','adv_disableUrlValidation'],
|
||||||
@@ -244,12 +244,12 @@ async function save(){
|
|||||||
payload.febboxCookies = cookiesVal? cookiesVal.split(',').filter(Boolean) : [];
|
payload.febboxCookies = cookiesVal? cookiesVal.split(',').filter(Boolean) : [];
|
||||||
}
|
}
|
||||||
const boolMap = {
|
const boolMap = {
|
||||||
adv_enableXprime:'enableXprimeProvider',
|
|
||||||
adv_enable4khdhub:'enable4khdhubProvider',
|
adv_enable4khdhub:'enable4khdhubProvider',
|
||||||
adv_enableMoviesmod:'enableMoviesmodProvider',
|
adv_enableMoviesmod:'enableMoviesmodProvider',
|
||||||
adv_enableMp4hydra:'enableMp4hydraProvider',
|
adv_enableMp4hydra:'enableMp4hydraProvider',
|
||||||
adv_enableVidzee:'enableVidzeeProvider',
|
adv_enableVidzee:'enableVidzeeProvider',
|
||||||
adv_enableVixsrc:'enableVixsrcProvider',
|
adv_enableVixsrc:'enableVixsrcProvider',
|
||||||
|
adv_enableProxy:'enableProxy',
|
||||||
adv_disableCache:'disableCache',
|
adv_disableCache:'disableCache',
|
||||||
adv_enablePStream:'enablePStreamApi',
|
adv_enablePStream:'enablePStreamApi',
|
||||||
adv_disableUrlValidation:'disableUrlValidation',
|
adv_disableUrlValidation:'disableUrlValidation',
|
||||||
@@ -337,12 +337,12 @@ async function clearAll(){
|
|||||||
tmdbApiKey: null,
|
tmdbApiKey: null,
|
||||||
// Provider enable flags default to true
|
// Provider enable flags default to true
|
||||||
enableShowboxProvider: true,
|
enableShowboxProvider: true,
|
||||||
enableXprimeProvider: true,
|
|
||||||
enable4khdhubProvider: true,
|
enable4khdhubProvider: true,
|
||||||
enableMoviesmodProvider: true,
|
enableMoviesmodProvider: true,
|
||||||
enableMp4hydraProvider: true,
|
enableMp4hydraProvider: true,
|
||||||
enableVidzeeProvider: true,
|
enableVidzeeProvider: true,
|
||||||
enableVixsrcProvider: true,
|
enableVixsrcProvider: true,
|
||||||
|
enableProxy: false,
|
||||||
// Other defaults
|
// Other defaults
|
||||||
disableCache: false,
|
disableCache: false,
|
||||||
enablePStreamApi: true,
|
enablePStreamApi: true,
|
||||||
@@ -596,15 +596,25 @@ async function renderProviderMatrix(merged){
|
|||||||
if(!merged){ try { const data = await fetchConfig(); merged = data.merged||{}; } catch { return; } }
|
if(!merged){ try { const data = await fetchConfig(); merged = data.merged||{}; } catch { return; } }
|
||||||
const container = document.getElementById('providerMatrix'); if(!container) return;
|
const container = document.getElementById('providerMatrix'); if(!container) return;
|
||||||
const defaults = (merged.defaultProviders||[]).map(s=>s.toLowerCase());
|
const defaults = (merged.defaultProviders||[]).map(s=>s.toLowerCase());
|
||||||
const providers = [
|
|
||||||
['showbox','enableShowboxProvider'],
|
// Fetch available providers from API
|
||||||
['4khdhub','enable4khdhubProvider'],
|
let availableProviders = [];
|
||||||
['moviesmod','enableMoviesmodProvider'],
|
try {
|
||||||
['mp4hydra','enableMp4hydraProvider'],
|
const providersResponse = await fetch('/api/providers');
|
||||||
['vidzee','enableVidzeeProvider'],
|
if (providersResponse.ok) {
|
||||||
['vixsrc','enableVixsrcProvider'],
|
const providersData = await providersResponse.json();
|
||||||
['xprime','enableXprimeProvider']
|
availableProviders = providersData.providers || [];
|
||||||
];
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to fetch providers list:', e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create provider list with enable flags
|
||||||
|
const providers = availableProviders.map(p => {
|
||||||
|
const flag = `enable${p.name.charAt(0).toUpperCase() + p.name.slice(1)}Provider`;
|
||||||
|
return [p.name, flag];
|
||||||
|
});
|
||||||
|
|
||||||
container.innerHTML = providers.map(([name,flag])=>{
|
container.innerHTML = providers.map(([name,flag])=>{
|
||||||
const enabled = merged[flag] !== false;
|
const enabled = merged[flag] !== false;
|
||||||
const isDefault = defaults.includes(name);
|
const isDefault = defaults.includes(name);
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
1758105049325
|
1758225404146
|
||||||
+46
-29
@@ -52,6 +52,19 @@ function writeOverrideFile(obj) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getProviderNames() {
|
||||||
|
try {
|
||||||
|
const providersDir = path.join(process.cwd(), 'providers');
|
||||||
|
if (!fs.existsSync(providersDir)) return [];
|
||||||
|
return fs.readdirSync(providersDir)
|
||||||
|
.filter(file => file.endsWith('.js') && file !== 'registry.js' && file !== 'vidsrcextractor.js')
|
||||||
|
.map(file => path.parse(file).name.toLowerCase());
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[config] Failed to scan providers directory:', e.message);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeConfig(base) {
|
function normalizeConfig(base) {
|
||||||
const cfg = { ...base };
|
const cfg = { ...base };
|
||||||
// Marker: if user explicitly provided an empty array for tmdbApiKeys in override file we should not resurrect legacy key
|
// Marker: if user explicitly provided an empty array for tmdbApiKeys in override file we should not resurrect legacy key
|
||||||
@@ -80,22 +93,24 @@ function normalizeConfig(base) {
|
|||||||
cfg.tmdbApiKeys = Array.from(new Set(cfg.tmdbApiKeys.map(k=>String(k).trim()).filter(Boolean)));
|
cfg.tmdbApiKeys = Array.from(new Set(cfg.tmdbApiKeys.map(k=>String(k).trim()).filter(Boolean)));
|
||||||
// Do not expose legacy single-key field in merged config output
|
// Do not expose legacy single-key field in merged config output
|
||||||
delete cfg.tmdbApiKey;
|
delete cfg.tmdbApiKey;
|
||||||
// Booleans normalization
|
|
||||||
const boolKeys = [
|
// Dynamic provider enable flags
|
||||||
'enableShowboxProvider','enableXprimeProvider','enable4khdhubProvider','enableMoviesmodProvider','enableMp4hydraProvider','enableVidzeeProvider','enableVixsrcProvider',
|
const providerNames = getProviderNames();
|
||||||
'disableCache','enablePStreamApi','disableUrlValidation','disable4khdhubUrlValidation'
|
const boolKeys = providerNames.map(name => `enable${name.charAt(0).toUpperCase() + name.slice(1)}Provider`);
|
||||||
];
|
boolKeys.push('disableCache', 'enablePStreamApi', 'disableUrlValidation', 'disable4khdhubUrlValidation', 'enableProxy');
|
||||||
|
|
||||||
boolKeys.forEach(k=>{ if (cfg[k] === 'true') cfg[k] = true; else if (cfg[k] === 'false') cfg[k] = false; });
|
boolKeys.forEach(k=>{ if (cfg[k] === 'true') cfg[k] = true; else if (cfg[k] === 'false') cfg[k] = false; });
|
||||||
// Default values if undefined
|
|
||||||
if (cfg.enableShowboxProvider === undefined) cfg.enableShowboxProvider = true;
|
// Set defaults for provider enable flags
|
||||||
if (cfg.enableXprimeProvider === undefined) cfg.enableXprimeProvider = true;
|
providerNames.forEach(name => {
|
||||||
if (cfg.enable4khdhubProvider === undefined) cfg.enable4khdhubProvider = true;
|
const flag = `enable${name.charAt(0).toUpperCase() + name.slice(1)}Provider`;
|
||||||
if (cfg.enableMoviesmodProvider === undefined) cfg.enableMoviesmodProvider = true;
|
if (cfg[flag] === undefined) cfg[flag] = true; // Default to enabled
|
||||||
if (cfg.enableMp4hydraProvider === undefined) cfg.enableMp4hydraProvider = true;
|
});
|
||||||
if (cfg.enableVidzeeProvider === undefined) cfg.enableVidzeeProvider = true;
|
|
||||||
if (cfg.enableVixsrcProvider === undefined) cfg.enableVixsrcProvider = true;
|
// Default values for other flags
|
||||||
if (cfg.disableCache === undefined) cfg.disableCache = false;
|
if (cfg.disableCache === undefined) cfg.disableCache = false;
|
||||||
if (cfg.enablePStreamApi === undefined) cfg.enablePStreamApi = true;
|
if (cfg.enablePStreamApi === undefined) cfg.enablePStreamApi = true;
|
||||||
|
if (cfg.enableProxy === undefined) cfg.enableProxy = false; // default off
|
||||||
// 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;
|
||||||
@@ -114,26 +129,27 @@ function applyConfigToEnv(cfg){
|
|||||||
if (cfg.febboxCookies && cfg.febboxCookies.length) process.env.FEBBOX_COOKIES = cfg.febboxCookies.join(',');
|
if (cfg.febboxCookies && cfg.febboxCookies.length) process.env.FEBBOX_COOKIES = cfg.febboxCookies.join(',');
|
||||||
else delete process.env.FEBBOX_COOKIES;
|
else delete process.env.FEBBOX_COOKIES;
|
||||||
if (cfg.defaultRegion) process.env.DEFAULT_REGION = cfg.defaultRegion; else delete process.env.DEFAULT_REGION;
|
if (cfg.defaultRegion) process.env.DEFAULT_REGION = cfg.defaultRegion; else delete process.env.DEFAULT_REGION;
|
||||||
// Provider enable flags
|
|
||||||
process.env.ENABLE_SHOWBOX_PROVIDER = cfg.enableShowboxProvider ? 'true':'false';
|
// Dynamic provider enable flags
|
||||||
process.env.ENABLE_XPRIME_PROVIDER = cfg.enableXprimeProvider ? 'true':'false';
|
const providerNames = getProviderNames();
|
||||||
process.env.ENABLE_4KHDHUB_PROVIDER = cfg.enable4khdhubProvider ? 'true':'false';
|
providerNames.forEach(name => {
|
||||||
process.env.ENABLE_MOVIESMOD_PROVIDER = cfg.enableMoviesmodProvider ? 'true':'false';
|
const flag = `enable${name.charAt(0).toUpperCase() + name.slice(1)}Provider`;
|
||||||
process.env.ENABLE_MP4HYDRA_PROVIDER = cfg.enableMp4hydraProvider ? 'true':'false';
|
const envName = `ENABLE_${name.toUpperCase()}_PROVIDER`;
|
||||||
process.env.ENABLE_VIDZEE_PROVIDER = cfg.enableVidzeeProvider ? 'true':'false';
|
process.env[envName] = cfg[flag] ? 'true' : 'false';
|
||||||
process.env.ENABLE_VIXSRC_PROVIDER = cfg.enableVixsrcProvider ? 'true':'false';
|
});
|
||||||
|
|
||||||
// Caching / validation / PStream
|
// Caching / validation / PStream
|
||||||
process.env.DISABLE_CACHE = cfg.disableCache ? 'true':'false';
|
process.env.DISABLE_CACHE = cfg.disableCache ? 'true':'false';
|
||||||
process.env.ENABLE_PSTREAM_API = cfg.enablePStreamApi ? 'true':'false';
|
process.env.ENABLE_PSTREAM_API = cfg.enablePStreamApi ? 'true':'false';
|
||||||
process.env.DISABLE_URL_VALIDATION = cfg.disableUrlValidation ? 'true':'false';
|
process.env.DISABLE_URL_VALIDATION = cfg.disableUrlValidation ? 'true':'false';
|
||||||
process.env.DISABLE_4KHDHUB_URL_VALIDATION = cfg.disable4khdhubUrlValidation ? 'true':'false';
|
process.env.DISABLE_4KHDHUB_URL_VALIDATION = cfg.disable4khdhubUrlValidation ? 'true':'false';
|
||||||
|
process.env.ENABLE_PROXY = cfg.enableProxy ? 'true':'false';
|
||||||
// Showbox specific
|
// Showbox specific
|
||||||
if (cfg.showboxCacheDir) process.env.SHOWBOX_CACHE_DIR = cfg.showboxCacheDir; else delete process.env.SHOWBOX_CACHE_DIR;
|
if (cfg.showboxCacheDir) process.env.SHOWBOX_CACHE_DIR = cfg.showboxCacheDir; else delete process.env.SHOWBOX_CACHE_DIR;
|
||||||
// Proxy settings removed; ensure legacy envs are cleared
|
// Proxy settings removed; ensure legacy envs are cleared
|
||||||
delete process.env.SHOWBOX_USE_ROTATING_PROXY;
|
delete process.env.SHOWBOX_USE_ROTATING_PROXY;
|
||||||
delete process.env.SHOWBOX_PROXY_URL_VALUE;
|
delete process.env.SHOWBOX_PROXY_URL_VALUE;
|
||||||
delete process.env.SHOWBOX_PROXY_URL_ALTERNATE;
|
delete process.env.SHOWBOX_PROXY_URL_ALTERNATE;
|
||||||
delete process.env.XPRIME_PROXY_URL;
|
|
||||||
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;
|
||||||
@@ -152,13 +168,6 @@ function loadConfig() {
|
|||||||
tmdbApiKeys: parseJsonMaybe(process.env.TMDB_API_KEYS) || null,
|
tmdbApiKeys: parseJsonMaybe(process.env.TMDB_API_KEYS) || null,
|
||||||
febboxCookies: parseCookies(process.env.FEBBOX_COOKIES),
|
febboxCookies: parseCookies(process.env.FEBBOX_COOKIES),
|
||||||
// Extended advanced config (may not exist in env)
|
// Extended advanced config (may not exist in env)
|
||||||
enableShowboxProvider: process.env.ENABLE_SHOWBOX_PROVIDER,
|
|
||||||
enableXprimeProvider: process.env.ENABLE_XPRIME_PROVIDER,
|
|
||||||
enable4khdhubProvider: process.env.ENABLE_4KHDHUB_PROVIDER,
|
|
||||||
enableMoviesmodProvider: process.env.ENABLE_MOVIESMOD_PROVIDER,
|
|
||||||
enableMp4hydraProvider: process.env.ENABLE_MP4HYDRA_PROVIDER,
|
|
||||||
enableVidzeeProvider: process.env.ENABLE_VIDZEE_PROVIDER,
|
|
||||||
enableVixsrcProvider: process.env.ENABLE_VIXSRC_PROVIDER,
|
|
||||||
disableCache: process.env.DISABLE_CACHE,
|
disableCache: process.env.DISABLE_CACHE,
|
||||||
enablePStreamApi: process.env.ENABLE_PSTREAM_API,
|
enablePStreamApi: process.env.ENABLE_PSTREAM_API,
|
||||||
showboxCacheDir: process.env.SHOWBOX_CACHE_DIR || null,
|
showboxCacheDir: process.env.SHOWBOX_CACHE_DIR || null,
|
||||||
@@ -166,6 +175,14 @@ function loadConfig() {
|
|||||||
disableUrlValidation: process.env.DISABLE_URL_VALIDATION,
|
disableUrlValidation: process.env.DISABLE_URL_VALIDATION,
|
||||||
disable4khdhubUrlValidation: process.env.DISABLE_4KHDHUB_URL_VALIDATION
|
disable4khdhubUrlValidation: process.env.DISABLE_4KHDHUB_URL_VALIDATION
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Dynamic provider enable flags from env
|
||||||
|
const providerNames = getProviderNames();
|
||||||
|
providerNames.forEach(name => {
|
||||||
|
const envName = `ENABLE_${name.toUpperCase()}_PROVIDER`;
|
||||||
|
envCfg[`enable${name.charAt(0).toUpperCase() + name.slice(1)}Provider`] = process.env[envName];
|
||||||
|
});
|
||||||
|
|
||||||
const override = readOverrideFile();
|
const override = readOverrideFile();
|
||||||
const merged = { ...envCfg, ...override };
|
const merged = { ...envCfg, ...override };
|
||||||
const normalized = normalizeConfig(merged);
|
const normalized = normalizeConfig(merged);
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
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 {
|
||||||
|
// Ignore URL parsing errors
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
count = parseInt(countStr);
|
||||||
|
} catch {
|
||||||
|
// Ignore URL parsing errors
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
+5
-12
@@ -1,26 +1,19 @@
|
|||||||
{
|
{
|
||||||
"defaultProviders": [],
|
"defaultProviders": [],
|
||||||
"configVersion": 1,
|
"configVersion": 1,
|
||||||
"tmdbApiKeys": [
|
"tmdbApiKeys": [],
|
||||||
"qwfghfgh",
|
|
||||||
"fghsdfs",
|
|
||||||
"uygghmng"
|
|
||||||
],
|
|
||||||
"enableShowboxProvider": true,
|
"enableShowboxProvider": true,
|
||||||
"febboxCookies": [
|
"febboxCookies": [],
|
||||||
"gfdgfdgf"
|
|
||||||
],
|
|
||||||
"enableXprimeProvider": true,
|
|
||||||
"enable4khdhubProvider": true,
|
"enable4khdhubProvider": true,
|
||||||
"enableMoviesmodProvider": true,
|
"enableMoviesmodProvider": true,
|
||||||
"enableMp4hydraProvider": true,
|
"enableMp4hydraProvider": true,
|
||||||
"enableVidzeeProvider": true,
|
"enableVidzeeProvider": true,
|
||||||
"enableVixsrcProvider": true,
|
"enableVixsrcProvider": true,
|
||||||
|
"enableProxy": false,
|
||||||
"disableCache": false,
|
"disableCache": false,
|
||||||
"enablePStreamApi": true,
|
"enablePStreamApi": true,
|
||||||
"disableUrlValidation": false,
|
"disableUrlValidation": false,
|
||||||
"disable4khdhubUrlValidation": false,
|
"disable4khdhubUrlValidation": false,
|
||||||
"excludeCodecsRaw": "{\"excludeDV\":true,\"excludeHDR\":true}",
|
"excludeCodecsRaw": "{\"excludeDV\":false,\"excludeHDR\":false}",
|
||||||
"minQualitiesRaw": "1080p",
|
"minQualitiesRaw": "all"
|
||||||
"port": 3000
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user