7 Commits
23 changed files with 1905 additions and 949 deletions
+3 -1
View File
@@ -1 +1,3 @@
/node_modules /node_modules
restart.trigger
.vscode/
+76
View File
@@ -2,6 +2,78 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [1.0.6] - 2025-09-20
### Removed
- MoviesClub provider (multi-server scraping complexities & Turnstile challenge; deprecated permanently)
- Xprime provider (upstream Xprime.tv offline due to security changes)
- 4khdhub provider: all `.zip` archive links are now omitted entirely (previous releases experimented with stripping the extension which produced non‑playable pseudo‑MKV links)
### Added
- Structured multi-server debug instrumentation (session summaries, per-fetch metrics, pattern counters)
- Turnstile challenge detection & bypass attempt scaffold (synthetic `/rcp_verify` token posting)
- Optional internal stream proxy (`enableProxy` flag): mounts `/m3u8-proxy`, `/ts-proxy`, `/sub-proxy` with playlist + segment + subtitle handling and segment prefetch cache.
- Proxy range management features:
- `clampOpen` (default on) – caps ambiguous open‑ended `bytes=0-` requests to a bounded initial window (`openChunkKB`, default 4096 KB)
- `progressiveOpen` (default on) – incremental expansion of the head range on successive `bytes=0-` requests instead of a single huge span
- `initChunkKB` (default 512 KB) – size of the synthetic initial 206 response when neither clamp/progressive produce a range and `noSynth` is not set
- `tailPrefetch` (default on) + `tailPrefetchKB` (default 256 KB) – asynchronous fetch & in‑memory cache of the file tail to satisfy rapid VLC tail probes
- `force200` (opt‑in) – normalizes upstream 206 responses to 200 for diagnostics
- `noSynth` (opt‑in) – disables synthetic initial partial response generation
- Tail prefetch TTL cleanup task (30 min window) and in‑memory maps for: segment cache, open range clamp, progressive growth, and tail buffers
- Host routing overrides: `pixeldrain.*` & `video-downloads.googleusercontent.com` are forced through `/ts-proxy` (extensionless or ambiguous content)
### Changed
- Centralized multi-server request headers with realistic `sec-ch-ua*` & `Sec-Fetch-*` values
- Added retry, rotating User-Agent, and cookie jar logic to multi-server fetch pipeline
- Showbox provider priority map updated after Xprime removal
- README/Docs trimmed to reflect current active providers only
- When `enableProxy` is active, stream response objects have their original `headers` field removed (proxy handles all required headers internally).
- 4khdhub provider now filters out archive endpoints instead of attempting extension normalization (prevents feeding ZIP files to players)
- Open‑ended range handling improved to reduce VLC negotiation loops by throttling first‑pass read size and growing progressively
- Synthetic initial partial response is automatically suppressed when `progressiveOpen` is active (real range growth preferred)
### Fixed
- Ensured multi-server fallback attempts (direct rcp player/m3u8 extraction) operate with improved diagnostics
- Eliminated repeated VLC tail probe stalls caused by archive masquerading as video content (root cause was filtered by dropping `.zip` URLs)
### Documentation
- Updated README version badge to 1.0.6 and provider list (removed MoviesClub & Xprime, clarified active providers list)
- Added proxy tuning parameter reference (clamp/progressive/tail prefetch, synthetic partial, force200) and host override notes
- Expanded explanation that per‑stream headers are stripped when proxying is enabled
## [1.0.5] - 2025-09-19
### Improved
- 4khdhub provider: Permanently block `r2.dev` FSL links (previous optional flag removed).
- Added Referer/Origin headers automatically for FSL Server links during validation (prior to block enforcement ensured proper behavior).
- Tightened URL validation: removed unconditional trust for `r2.dev`; validation logic now consistent across hosts.
- Host distribution instrumentation logs final hostname counts for easier diagnostics.
- Preserved HubCloud worker `.zip` links by stripping the `.zip` extension instead of discarding them (enables direct playback attempts).
### Notes
- `r2.dev` links are always removed from final output; no env flag required.
### Documentation
## [1.0.4] - 2025-09-18
### Added
### Changed
```json
{ "title": "…", "url": "…", "quality": "…", "provider": "…", "headers": { } }
```
## [1.0.3] - 2025-09-17
### Fixed
- Server now binds to `0.0.0.0` by default so Docker port publishing works correctly from the host. Added `BIND_HOST=0.0.0.0` in Dockerfile and compose.
### Notes
- If you were seeing `ERR_CONNECTION_REFUSED` on `http://localhost:8787`, pull the latest image or rebuild, then re-run with `-p 8787:8787`.
## [1.0.2] - 2025-09-17 ## [1.0.2] - 2025-09-17
### Fixed ### Fixed
@@ -37,6 +109,10 @@ All notable changes to this project will be documented in this file.
## [1.0.0] - 2025-09-16 ## [1.0.0] - 2025-09-16
- Initial stable release. - Initial stable release.
[1.0.6]: https://github.com/Inside4ndroid/TMDB-Embed-API/compare/v1.0.5...v1.0.6
[1.0.5]: https://github.com/Inside4ndroid/TMDB-Embed-API/compare/v1.0.4...v1.0.5
[1.0.3]: https://github.com/Inside4ndroid/TMDB-Embed-API/compare/v1.0.2...v1.0.3
[1.0.4]: https://github.com/Inside4ndroid/TMDB-Embed-API/compare/v1.0.3...v1.0.4
[1.0.2]: https://github.com/Inside4ndroid/TMDB-Embed-API/compare/v1.0.1...v1.0.2 [1.0.2]: https://github.com/Inside4ndroid/TMDB-Embed-API/compare/v1.0.1...v1.0.2
[1.0.1]: https://github.com/Inside4ndroid/TMDB-Embed-API/compare/v1.0.0...v1.0.1 [1.0.1]: https://github.com/Inside4ndroid/TMDB-Embed-API/compare/v1.0.0...v1.0.1
+1
View File
@@ -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
+99 -26
View File
@@ -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.2-informational?style=flat" /> <img src="https://img.shields.io/badge/Version-1.0.6-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
+6 -2
View File
@@ -374,8 +374,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');
+1
View File
@@ -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:
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "tmdb-embed-api", "name": "tmdb-embed-api",
"version": "1.0.2", "version": "1.0.5",
"description": "Standalone streaming embed source aggregation API (TMDB ID in, streams out)", "description": "Standalone streaming embed source aggregation API (TMDB ID in, streams out)",
"main": "apiServer.js", "main": "apiServer.js",
"scripts": { "scripts": {
+441 -344
View File
File diff suppressed because it is too large Load Diff
+88 -106
View File
@@ -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,
+138 -134
View File
@@ -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 {
@@ -2722,27 +2732,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 +2763,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 +2778,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 +2790,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 +2811,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 +2820,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 +2857,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 +2942,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 +2956,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 +2978,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 +3006,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 +3031,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 +3074,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;
}; };
+4 -8
View File
@@ -84,17 +84,13 @@ const getVidZeeStreams = async (tmdbId, mediaType, seasonNum, episodeNum) => {
const language = sourceItem.language || sourceItem.lang; const language = sourceItem.language || sourceItem.lang;
return { return {
title: `VidZee S${sr} - ${quality}`, name: `VidZee Server${sr} - ${quality} - ${language}`,
title: `VidZee Server${sr} - ${quality} - ${language}`,
url: sourceItem.link, // Use sourceItem.link for the URL url: sourceItem.link, // Use sourceItem.link for the URL
quality: quality, quality: quality,
language: language,
provider: "VidZee", provider: "VidZee",
size: "Unknown size", headers: {
behaviorHints: { 'Referer': 'https://core.vidzee.wtf/'
notWebReady: true,
headers: {
'Referer': 'https://core.vidzee.wtf/'
}
} }
}; };
}).filter(stream => stream.url); }).filter(stream => stream.url);
+651
View File
@@ -0,0 +1,651 @@
// MoviesClub Provider
// Extracts streams from moviesapi.club
const axios = require('axios');
const JsUnpacker = require('../utils/jsunpack');
const { config } = require('../utils/config');
function applyMoviesClubProxy(url) {
try {
const base = config.moviesclubProxy;
if (!base || typeof base !== 'string') return url;
// Ensure base ends with either = or a delimiter allowing raw append
const proxied = base + encodeURIComponent(url);
return proxied;
} catch {
return url;
}
}
async function getMoviesClubStreams(tmdbId, mediaType, season, episode) {
console.log(`[moviesclub] Fetching streams for ${mediaType} tmdbId=${tmdbId} season=${season} episode=${episode}`);
const streams = [];
try {
// Build the URL based on media type
let url;
if (mediaType === 'movie') {
url = `https://moviesapi.club/movie/${tmdbId}`;
} else if (mediaType === 'tv' && season && episode) {
url = `https://moviesapi.club/tv/${tmdbId}-${season}-${episode}`;
} else {
console.log('[moviesclub] Invalid parameters for TV show');
return [];
}
console.log(`[moviesclub] Fetching from: ${url}`);
// Fetch the main page
const response = await axios.get(url, {
timeout: 10000,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1'
}
});
const html = response.data;
console.log(`[moviesclub] Fetched ${html.length} characters`);
// Extract data-url from srvswitch-menu
const dataUrlMatch = html.match(/data-url="([^"]+)"/);
if (!dataUrlMatch) {
console.log('[moviesclub] No data-url found in HTML');
return [];
}
const embedUrl = dataUrlMatch[1];
console.log(`[moviesclub] Found embed URL: ${embedUrl}`);
// Check if this is a cdn.moviesapi.club embed URL with multiple servers
if (embedUrl.includes('cdn.moviesapi.club')) {
console.log('[moviesclub] Detected cdn.moviesapi.club embed URL, handling multiple servers');
return null;
}
// Handle regular embed URLs (existing logic)
return await handleSingleServer(embedUrl, mediaType, season, episode, tmdbId);
} catch (error) {
console.error(`[moviesclub] Error:`, error.message);
return [];
}
}
// Handle cdn.moviesapi.club URLs with multiple servers
async function handleMultipleServers(embedUrl, mediaType, season, episode, tmdbId) {
console.log(`[moviesclub] Handling multiple servers from: ${embedUrl}`);
const streams = [];
try {
// Fetch the embed page
const embedResponse = await axios.get(embedUrl, {
timeout: 10000,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate',
'Connection': 'keep-alive'
}
});
const embedHtml = embedResponse.data;
console.log(`[moviesclub] Fetched embed page: ${embedHtml.length} characters`);
// Extract server options from the HTML
const serverMatches = embedHtml.match(/<div class="server"[^>]*data-hash="([^"]+)"[^>]*>([^<]+)<\/div>/gi);
if (!serverMatches || serverMatches.length === 0) {
console.log('[moviesclub] No server options found, trying direct iframe extraction');
// Try to extract the main iframe src
const iframeMatch = embedHtml.match(/<iframe[^>]*src="([^"]*)"[^>]*>/i);
if (iframeMatch) {
const iframeSrc = iframeMatch[1];
console.log(`[moviesclub] Found iframe src: ${iframeSrc}`);
// Try to decode if it's base64
try {
const decodedSrc = Buffer.from(iframeSrc.split('/').pop(), 'base64').toString('utf-8');
console.log(`[moviesclub] Decoded iframe src: ${decodedSrc}`);
// If it's a URL, use it directly
if (decodedSrc.startsWith('http')) {
const rawUrl = decodedSrc;
const stream = {
name: `MoviesClub - Direct Stream`,
title: `${mediaType === 'movie' ? 'Movie' : `S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`} ${tmdbId} - MoviesClub Direct`,
url: applyMoviesClubProxy(rawUrl),
quality: extractQualityFromUrl(rawUrl) || '720p'
};
streams.push(stream);
console.log(`[moviesclub] Added direct stream from iframe`);
return streams;
}
} catch (decodeError) {
console.log(`[moviesclub] Could not decode iframe src: ${decodeError.message}`);
}
}
return streams;
}
console.log(`[moviesclub] Found ${serverMatches.length} server options`);
// Process each server
for (let i = 0; i < serverMatches.length; i++) {
const serverMatch = serverMatches[i];
const hashMatch = serverMatch.match(/data-hash="([^"]+)"/);
const nameMatch = serverMatch.match(/>([^<]+)<\/div>/);
if (!hashMatch || !nameMatch) {
console.log(`[moviesclub] Could not parse server ${i + 1}, skipping`);
continue;
}
const encodedHash = hashMatch[1];
const serverName = nameMatch[1].trim();
console.log(`[moviesclub] Processing server ${i + 1}: ${serverName}`);
try {
// Construct the rcp URL from the hash
const rcpUrl = `https://cloudnestra.com/rcp/${encodedHash}`;
console.log(`[moviesclub] RCP URL: ${rcpUrl}`);
// Fetch the rcp URL to get the JavaScript with prorcp URL
const rcpResponse = await axios.get(rcpUrl, {
timeout: 8000,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate',
'Connection': 'keep-alive',
'Referer': embedUrl
}
});
const rcpHtml = rcpResponse.data;
console.log(`[moviesclub] RCP response length: ${rcpHtml.length}`);
// Extract prorcp URL from the JavaScript
const prorcpMatch = rcpHtml.match(/src:\s*['"]([^'"]*prorcp[^'"]*)['"]/i);
if (!prorcpMatch) {
// Try alternative patterns
const altMatch = rcpHtml.match(/['"](\/prorcp\/[^'"]*)['"]/i);
if (altMatch) {
prorcpUrl = altMatch[1];
} else {
console.log(`[moviesclub] No prorcp URL found in rcp response`);
continue;
}
} else {
prorcpUrl = prorcpMatch[1];
}
console.log(`[moviesclub] Found prorcp URL: ${prorcpUrl}`);
// If it's a relative URL, make it absolute
if (prorcpUrl.startsWith('/')) {
prorcpUrl = `https://cloudnestra.com${prorcpUrl}`;
console.log(`[moviesclub] Converted to absolute URL: ${prorcpUrl}`);
}
// Fetch the prorcp URL to get the player HTML
const prorcpResponse = await axios.get(prorcpUrl, {
timeout: 8000,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate',
'Connection': 'keep-alive',
'Referer': rcpUrl
}
});
const prorcpHtml = prorcpResponse.data;
console.log(`[moviesclub] Prorcp response length: ${prorcpHtml.length}`);
// Extract stream URL from Playerjs configuration
const streamUrl = extractStreamFromPlayerjsUpdated(prorcpHtml);
if (!streamUrl) {
console.log(`[moviesclub] No stream URL found in prorcp response`);
continue;
}
console.log(`[moviesclub] Extracted stream URL: ${streamUrl.substring(0, 100)}...`);
// Create stream object
const rawUrl = streamUrl;
const stream = {
name: `MoviesClub - ${serverName}`,
title: `${mediaType === 'movie' ? 'Movie' : `S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`} ${tmdbId} - ${serverName}`,
url: applyMoviesClubProxy(rawUrl),
quality: extractQualityFromUrl(rawUrl) || '720p',
headers: {}
};
streams.push(stream);
console.log(`[moviesclub] Added stream from server: ${serverName}`);
} catch (serverError) {
console.error(`[moviesclub] Error processing server ${serverName}:`, serverError.message);
continue;
}
}
console.log(`[moviesclub] Total streams found: ${streams.length}`);
return streams;
} catch (error) {
console.error(`[moviesclub] Error in handleMultipleServers:`, error.message);
return [];
}
}
// Handle regular embed URLs (existing logic)
async function handleSingleServer(embedUrl, mediaType, season, episode, tmdbId) {
console.log(`[moviesclub] Handling single server from: ${embedUrl}`);
const streams = [];
try {
// Fetch the embed page
const embedResponse = await axios.get(embedUrl, {
timeout: 10000,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate',
'Connection': 'keep-alive'
}
});
const embedHtml = embedResponse.data;
console.log(`[moviesclub] Fetched embed page: ${embedHtml.length} characters`);
// Look for packed JavaScript using JsUnpacker.detect()
const scriptTags = embedHtml.match(/<script[^>]*>[\s\S]*?<\/script>/gi) || [];
let packedCode = null;
for (const scriptTag of scriptTags) {
// Extract script content
const scriptContent = scriptTag.replace(/<\/?script[^>]*>/gi, '').trim();
if (scriptContent) {
// Create unpacker instance and check if it's packed
const unpacker = new JsUnpacker(scriptContent);
if (unpacker.detect()) {
console.log(`[moviesclub] Found packed JavaScript in script tag, length: ${scriptContent.length}`);
packedCode = scriptContent;
break;
}
}
}
if (!packedCode) {
console.log('[moviesclub] No packed JavaScript found');
return [];
}
// Unpack using JsUnpacker
const unpacker = new JsUnpacker(packedCode);
const unpackedCode = unpacker.unpack();
if (!unpackedCode) {
console.log('[moviesclub] Failed to unpack JavaScript');
return [];
}
console.log(`[moviesclub] Successfully unpacked code, length: ${unpackedCode.length}`);
// Parse the player configuration from unpacked code
const playerConfig = parsePlayerConfig(unpackedCode);
if (!playerConfig) {
console.log('[moviesclub] Failed to parse player configuration');
return [];
}
console.log('[moviesclub] Parsed player config:', playerConfig);
// Extract streams from the configuration
if (playerConfig.file) {
const rawUrl = playerConfig.file;
const stream = {
name: `MoviesClub - ${playerConfig.title || 'Server 1'}`,
title: playerConfig.title || `${mediaType === 'movie' ? 'Movie' : `S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`} ${tmdbId} - MoviesClub`,
url: applyMoviesClubProxy(rawUrl),
quality: extractQualityFromUrl(rawUrl) || '720p',
// Always include vidora headers for single-server results to prevent 403 playback issues
headers: {
Origin: 'https://vidora.stream',
Referer: 'https://vidora.stream/'
}
};
// Add poster if available
if (playerConfig.poster) {
stream.poster = playerConfig.poster;
}
// Add subtitles if available
if (playerConfig.subtitle) {
stream.subtitles = parseSubtitles(playerConfig.subtitle);
}
streams.push(stream);
}
console.log(`[moviesclub] Found ${streams.length} streams`);
return streams;
} catch (error) {
console.error(`[moviesclub] Error in handleSingleServer:`, error.message);
return [];
}
}
// Extract stream URL from Playerjs configuration in HTML
function extractStreamFromPlayerjs(html) {
try {
// Look for the Playerjs constructor call
const playerMatch = html.match(/new Playerjs\(\s*(\{[^}]*\})\s*\)/);
if (!playerMatch) {
console.log('[moviesclub] No Playerjs constructor found in HTML');
return null;
}
const configStr = playerMatch[1];
console.log(`[moviesclub] Found Playerjs config: ${configStr.substring(0, 200)}...`);
// Extract the file URL from the configuration
const fileMatch = configStr.match(/file:\s*['"]([^'"]+)['"]/);
if (!fileMatch) {
console.log('[moviesclub] No file URL found in Playerjs config');
return null;
}
const streamUrl = fileMatch[1];
console.log(`[moviesclub] Extracted stream URL from Playerjs: ${streamUrl.substring(0, 100)}...`);
return streamUrl;
} catch (error) {
console.error('[moviesclub] Error extracting stream from Playerjs:', error.message);
return null;
}
}
// Extract streams from server response
function extractStreamsFromServer(serverData, serverName, mediaType, season, episode, tmdbId) {
const streams = [];
try {
// Handle different response formats
if (typeof serverData === 'string') {
// Try to parse as JSON
try {
serverData = JSON.parse(serverData);
} catch (parseError) {
console.log(`[moviesclub] Server response is not JSON, treating as direct URL: ${serverData.substring(0, 100)}`);
// Treat as direct stream URL
if (serverData.startsWith('http')) {
const rawUrl = serverData;
const stream = {
name: `MoviesClub - ${serverName}`,
title: `${mediaType === 'movie' ? 'Movie' : `S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`} ${tmdbId} - ${serverName}`,
url: applyMoviesClubProxy(rawUrl),
quality: extractQualityFromUrl(rawUrl) || '720p',
headers: serverData.includes('vidora.stream') ? { Origin: 'https://vidora.stream', Referer: 'https://vidora.stream/' } : {}
};
streams.push(stream);
}
return streams;
}
}
// Handle JSON response
if (serverData && typeof serverData === 'object') {
// Look for common stream URL patterns
const possibleKeys = ['file', 'url', 'src', 'stream', 'video', 'source'];
for (const key of possibleKeys) {
if (serverData[key] && typeof serverData[key] === 'string' && serverData[key].startsWith('http')) {
console.log(`[moviesclub] Found stream URL in key '${key}': ${serverData[key].substring(0, 100)}`);
const rawUrl = serverData[key];
const stream = {
name: `MoviesClub - ${serverName}`,
title: `${mediaType === 'movie' ? 'Movie' : `S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`} ${tmdbId} - ${serverName}`,
url: applyMoviesClubProxy(rawUrl),
quality: extractQualityFromUrl(rawUrl) || '720p',
headers: rawUrl.includes('vidora.stream') ? { Origin: 'https://vidora.stream', Referer: 'https://vidora.stream/' } : {}
};
// Add poster if available
if (serverData.poster) {
stream.poster = serverData.poster;
}
// Add subtitles if available
if (serverData.subtitle || serverData.subtitles) {
const subtitleData = serverData.subtitle || serverData.subtitles;
stream.subtitles = parseSubtitles(subtitleData);
}
streams.push(stream);
break; // Only take the first valid stream URL
}
}
// If no direct URL found, check for nested objects
if (streams.length === 0) {
for (const key of possibleKeys) {
if (serverData[key] && typeof serverData[key] === 'object') {
const nestedStreams = extractStreamsFromServer(serverData[key], serverName, mediaType, season, episode, tmdbId);
streams.push(...nestedStreams);
}
}
}
}
} catch (error) {
console.error(`[moviesclub] Error extracting streams from server ${serverName}:`, error.message);
}
return streams;
}
// Extract stream URL from Playerjs configuration in HTML
function extractStreamFromPlayerjs(html) {
try {
// Look for the Playerjs constructor call
const playerMatch = html.match(/new Playerjs\(\s*(\{[^}]*\})\s*\)/);
if (!playerMatch) {
console.log('[moviesclub] No Playerjs constructor found in HTML');
return null;
}
const configStr = playerMatch[1];
console.log(`[moviesclub] Found Playerjs config: ${configStr.substring(0, 200)}...`);
// Extract the file URL from the configuration
const fileMatch = configStr.match(/file:\s*['"]([^'"]+)['"]/);
if (!fileMatch) {
console.log('[moviesclub] No file URL found in Playerjs config');
return null;
}
const streamUrl = fileMatch[1];
console.log(`[moviesclub] Extracted stream URL from Playerjs: ${streamUrl.substring(0, 100)}...`);
return streamUrl;
} catch (error) {
console.error('[moviesclub] Error extracting stream from Playerjs:', error.message);
return null;
}
}
// Parse player configuration from unpacked JavaScript
function parsePlayerConfig(unpackedCode) {
try {
// Look for the player configuration object
const configMatch = unpackedCode.match(/new Playerjs\(\s*(\{[^}]*\})\s*\)/);
if (!configMatch) {
return null;
}
// Extract the configuration object
const configStr = configMatch[1];
// Parse the configuration (this is a simple parser for the expected format)
const config = {};
// Extract key-value pairs
const pairs = configStr.match(/(\w+):"([^"]*)"/g) || [];
pairs.forEach(pair => {
const [, key, value] = pair.match(/(\w+):"([^"]*)"/);
config[key] = value;
});
// Also handle non-quoted values
const numberPairs = configStr.match(/(\w+):(\d+(?:\.\d+)?)/g) || [];
numberPairs.forEach(pair => {
const [, key, value] = pair.match(/(\w+):(\d+(?:\.\d+)?)/);
config[key] = parseFloat(value);
});
return config;
} catch (error) {
console.error('[moviesclub] Error parsing player config:', error.message);
return null;
}
}
// Parse subtitles from the subtitle string
function parseSubtitles(subtitleStr) {
if (!subtitleStr) return [];
const subtitles = [];
const subtitleMatches = subtitleStr.match(/\[([^\]]+)\]([^,\[]*)/g);
if (subtitleMatches) {
subtitleMatches.forEach(match => {
const [, language, url] = match.match(/\[([^\]]+)\]([^,\[]*)/);
if (url && url.trim()) {
subtitles.push({
language: language.trim(),
url: url.trim(),
type: 'vtt'
});
}
});
}
return subtitles;
}
// Extract quality from URL
function extractQualityFromUrl(url) {
if (!url) return '720p';
const qualityPatterns = [
{ pattern: /1080p|1080/i, quality: '1080p' },
{ pattern: /720p|720/i, quality: '720p' },
{ pattern: /480p|480/i, quality: '480p' },
{ pattern: /360p|360/i, quality: '360p' },
{ pattern: /2160p|4k|uhd/i, quality: '2160p' }
];
for (const { pattern, quality } of qualityPatterns) {
if (pattern.test(url)) {
return quality;
}
}
return '720p';
}
// Extract stream URL from Playerjs configuration in HTML (updated version)
function extractStreamFromPlayerjsUpdated(html) {
try {
// Look for the Playerjs constructor call
const playerMatch = html.match(/new Playerjs\(\s*(\{[^}]*\})\s*\)/);
if (!playerMatch) {
console.log('[moviesclub] No Playerjs constructor found in HTML');
return null;
}
const configStr = playerMatch[1];
console.log(`[moviesclub] Found Playerjs config: ${configStr.substring(0, 200)}...`);
// Extract the file URL from the configuration
let streamUrl = null;
// Try multiple patterns to extract the file URL
const patterns = [
/file:\s*['"]([^'"]+)['"]/,
/file:\s*([^,\s}]+)/,
/file\s*:\s*['"]([^'"]+)['"]/,
/file\s*:\s*([^,\s}]+)/
];
for (const pattern of patterns) {
const match = configStr.match(pattern);
if (match && match[1] && match[1].trim() !== '' && match[1].trim() !== '""') {
streamUrl = match[1].replace(/['"]/g, '').trim();
console.log(`[moviesclub] Found stream URL with pattern: ${streamUrl.substring(0, 100)}...`);
break;
}
}
// If no URL found in config, look for streaming URLs in the HTML
if (!streamUrl) {
console.log('[moviesclub] No file URL in Playerjs config, searching HTML for streaming URLs...');
// Look for m3u8 URLs
const m3u8Match = html.match(/https?:\/\/[^'"\s]+\.m3u8[^'"\s]*/i);
if (m3u8Match) {
streamUrl = m3u8Match[0];
console.log(`[moviesclub] Found m3u8 URL in HTML: ${streamUrl.substring(0, 100)}...`);
}
// Look for other streaming URLs
if (!streamUrl) {
const streamMatch = html.match(/https?:\/\/[^'"\s]+(?:\.mp4|\.m3u8|\.ts)[^'"\s]*/i);
if (streamMatch) {
streamUrl = streamMatch[0];
console.log(`[moviesclub] Found streaming URL in HTML: ${streamUrl.substring(0, 100)}...`);
}
}
}
if (!streamUrl) {
console.log('[moviesclub] No file URL found in Playerjs config after trying all patterns');
console.log('[moviesclub] Config string:', configStr.substring(0, 500));
return null;
}
console.log(`[moviesclub] Extracted stream URL from Playerjs: ${streamUrl.substring(0, 100)}...`);
return streamUrl;
} catch (error) {
console.error('[moviesclub] Error extracting stream from Playerjs:', error.message);
return null;
}
}
module.exports = {
getMoviesClubStreams
};
+2
View File
@@ -1106,6 +1106,8 @@ async function getMoviesModStreams(tmdbId, mediaType, seasonNum = null, episodeN
title: `${cleanFileName}\n${sizeInfo || ''}${techDetailsString}`, title: `${cleanFileName}\n${sizeInfo || ''}${techDetailsString}`,
url: selectedResult.url, url: selectedResult.url,
quality: actualQuality, quality: actualQuality,
provider: 'moviesmod',
headers: {}
}; };
} else { } else {
console.warn(`[MoviesMod] Unsupported URL type for final processing: ${targetLink?.url}`); console.warn(`[MoviesMod] Unsupported URL type for final processing: ${targetLink?.url}`);
+157 -241
View File
@@ -1,12 +1,23 @@
const { config } = require('../utils/config'); const { config } = require('../utils/config');
// Lazy load showbox to avoid early side-effects; temporarily disabled until stabilized const fs = require('fs');
let showbox = null; const path = require('path');
let k4hdhub = null; // 4khdhub provider lazy ref
let moviesmod = null; // moviesmod provider lazy ref // Lazy load cache for providers
let mp4hydra = null; // MP4Hydra provider lazy ref const providerCache = new Map();
let vidzee = null; // VidZee provider lazy ref
let vixsrc = null; // vixsrc provider lazy ref // Function name mappings for existing providers
let xprime = null; // xprime provider lazy ref const providerFunctionMap = {
'Showbox.js': 'getStreamsFromTmdbId',
'4khdhub.js': 'get4KHDHubStreams',
'moviesmod.js': 'getMoviesModStreams',
'MP4Hydra.js': 'getMP4HydraStreams',
'VidZee.js': 'getVidZeeStreams',
'vixsrc.js': 'getVixsrcStreams',
'xprime.js': 'getXprimeStreams',
'uhdmovies.js': 'getUHDMoviesStreams',
'moviesclub.js': 'getMoviesClubStreams',
};
// Stats for debug endpoint // Stats for debug endpoint
let lastCookieStats = { selected: null, index: null, total: 0, remainingMB: null, timestamp: null }; let lastCookieStats = { selected: null, index: null, total: 0, remainingMB: null, timestamp: null };
@@ -14,248 +25,153 @@ async function getEffectiveCookies() {
return Array.isArray(config.febboxCookies) ? config.febboxCookies : []; return Array.isArray(config.febboxCookies) ? config.febboxCookies : [];
} }
// Adapter for Showbox provider using existing exported getStreamsFromTmdbId // Get all provider files
async function fetchShowbox(ctx) { function getProviderFiles() {
if (!showbox) { const providersDir = path.join(__dirname);
try { showbox = require('./Showbox.js'); } catch (e) { console.error('Failed to load Showbox provider:', e.message); return []; } return fs.readdirSync(providersDir)
} .filter(file => file.endsWith('.js') && file !== 'registry.js' && file !== 'vidsrcextractor.js')
const { getTmdbApiKey } = require('../utils/tmdbKey'); .map(file => ({
const tmdbApiKey = getTmdbApiKey(); name: path.parse(file).name.toLowerCase(),
if (!tmdbApiKey) { file: file,
console.warn('[registry] showbox skipped: TMDB API key missing'); functionName: providerFunctionMap[file]
return []; }));
}
const cookies = await getEffectiveCookies();
const previousConfig = global.currentRequestConfig;
global.currentRequestConfig = { ...(previousConfig || {}) };
let selected = null;
if (cookies.length > 0) {
const index = Math.floor(Math.random() * cookies.length);
selected = cookies[index];
global.currentRequestConfig.cookie = selected.startsWith('ui=') ? selected : `ui=${selected}`;
global.currentRequestConfig.cookies = cookies.map(c => c.startsWith('ui=') ? c : `ui=${c}`);
lastCookieStats = { selected: selected.slice(0, 16) + '...', index, total: cookies.length, remainingMB: null, timestamp: Date.now() };
console.log(`[registry] Cookie random pick index=${index} total=${cookies.length}`);
}
try {
const tmdbType = ctx.type === 'movie' ? 'movie' : 'tv';
console.log(`[registry] Fetching Showbox streams for ${tmdbType} tmdbId=${ctx.tmdbId} season=${ctx.season||''} episode=${ctx.episode||''}`);
const t0 = Date.now();
const streams = await showbox.getStreamsFromTmdbId(tmdbType, ctx.tmdbId, ctx.season || null, ctx.episode || null, null, selected);
const durationMs = Date.now() - t0;
console.log(`[registry] Provider showbox fetch duration ${durationMs}ms`);
if (global.currentRequestUserCookieRemainingMB != null) {
lastCookieStats.remainingMB = global.currentRequestUserCookieRemainingMB;
}
console.log(`[registry] Showbox returned ${Array.isArray(streams)?streams.length:0} streams`);
return Array.isArray(streams) ? streams : [];
} catch (e) {
console.error('[registry] Showbox fetch error:', e.message);
return [];
} finally {
global.currentRequestConfig = previousConfig || {};
}
} }
// Load a provider module
function loadProvider(providerFile) {
if (!providerCache.has(providerFile)) {
try {
providerCache.set(providerFile, require(path.join(__dirname, providerFile)));
} catch (e) {
console.error(`Failed to load provider ${providerFile}:`, e.message);
return null;
}
}
return providerCache.get(providerFile);
}
// Create fetch function for a provider
function createFetchFunction(providerInfo) {
return async function(ctx) {
const module = loadProvider(providerInfo.file);
if (!module) return [];
const funcName = providerInfo.functionName;
if (!module[funcName]) {
console.error(`Provider ${providerInfo.name} does not export ${funcName}`);
return [];
}
try {
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
const t0 = Date.now();
let result;
if (providerInfo.name === 'showbox') {
// Special case for Showbox with TMDB key and cookies
const { getTmdbApiKey } = require('../utils/tmdbKey');
const tmdbApiKey = getTmdbApiKey();
if (!tmdbApiKey) {
console.warn('[registry] showbox skipped: TMDB API key missing');
return [];
}
const cookies = await getEffectiveCookies();
const previousConfig = global.currentRequestConfig;
global.currentRequestConfig = { ...(previousConfig || {}) };
let selected = null;
if (cookies.length > 0) {
const index = Math.floor(Math.random() * cookies.length);
selected = cookies[index];
global.currentRequestConfig.cookie = selected.startsWith('ui=') ? selected : `ui=${selected}`;
global.currentRequestConfig.cookies = cookies.map(c => c.startsWith('ui=') ? c : `ui=${c}`);
lastCookieStats = { selected: selected.slice(0, 16) + '...', index, total: cookies.length, remainingMB: null, timestamp: Date.now() };
console.log(`[registry] Cookie random pick index=${index} total=${cookies.length}`);
}
result = await module[funcName](mediaType, ctx.tmdbId, ctx.season || null, ctx.episode || null, null, selected);
if (global.currentRequestUserCookieRemainingMB != null) {
lastCookieStats.remainingMB = global.currentRequestUserCookieRemainingMB;
}
global.currentRequestConfig = previousConfig || {};
} else if (providerInfo.name === 'xprime') {
// Special case for Xprime with TMDB metadata lookup
const { getTmdbApiKey } = require('../utils/tmdbKey');
const tmdbApiKey = getTmdbApiKey();
if (!tmdbApiKey) {
console.warn('[registry] xprime skipped: TMDB API key missing');
return [];
}
// Fetch minimal TMDB metadata for title + year
let title = null; let year = null;
try {
const axios = require('axios');
const metaUrl = `https://api.themoviedb.org/3/${mediaType}/${ctx.tmdbId}?api_key=${tmdbApiKey}`;
const { data: meta } = await axios.get(metaUrl, { timeout: 8000 });
title = mediaType === 'movie' ? (meta.title || meta.original_title) : (meta.name || meta.original_name);
const dateStr = mediaType === 'movie' ? meta.release_date : meta.first_air_date;
if (dateStr) year = dateStr.split('-')[0];
} catch (mErr) {
console.warn('[registry] xprime metadata fetch failed:', mErr.message);
return [];
}
if (!title || !year) {
console.log('[registry] xprime missing title/year after TMDB lookup');
return [];
}
result = await module[funcName](title, year, mediaType, ctx.season || null, ctx.episode || null);
// Normalize quality labels
if (Array.isArray(result)) {
result = result.map(s => {
let q = s.quality || 'Unknown';
if (!/(\d{3,4})p/.test(q.toLowerCase())) {
const qLower = q.toLowerCase();
if (qLower.includes('fhd') || qLower.includes('full')) q = '1080p';
else if (qLower === 'hd' || qLower.includes('720')) q = '720p';
else if (qLower.includes('sd') || qLower.includes('480')) q = '480p';
else if (qLower.includes('cam')) q = '360p';
}
return { ...s, quality: q, name: s.name || 'Xprime', provider: 'xprime' };
});
}
} else {
// Standard provider call
result = await module[funcName](ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null);
}
const durationMs = Date.now() - t0;
console.log(`[registry] ${providerInfo.name} fetch duration ${durationMs}ms`);
if (!Array.isArray(result)) return [];
// Add provider name if not present
return result.map(s => ({ ...s, provider: s.provider || providerInfo.name }));
} catch (e) {
console.error(`[registry] ${providerInfo.name} fetch error:`, e.message);
return [];
}
};
}
// Initialize providers
const providerFiles = getProviderFiles();
const providers = []; const providers = [];
if (config.enableShowboxProvider) { for (const providerInfo of providerFiles) {
providers.push({ name: 'showbox', enabled: true, fetch: fetchShowbox }); const enableFlag = `enable${providerInfo.name.charAt(0).toUpperCase() + providerInfo.name.slice(1)}Provider`;
} else { const enabled = config[enableFlag] !== false;
console.log('[registry] showbox disabled via config');
}
if (enabled) {
// 4KHDHub adapter providers.push({
async function fetch4khdhub(ctx) { name: providerInfo.name,
if (!k4hdhub) { enabled: true,
try { k4hdhub = require('./4khdhub.js'); } catch (e) { console.error('Failed to load 4khdhub provider:', e.message); return []; } fetch: createFetchFunction(providerInfo)
}
try {
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
const t0 = Date.now();
const result = await k4hdhub.get4KHDHubStreams(ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null);
const durationMs = Date.now() - t0;
console.log(`[registry] 4khdhub fetch duration ${durationMs}ms`);
if (!Array.isArray(result)) return [];
return result.map(s => ({ ...s, name: s.name || '4KHDHub', provider: '4khdhub' }));
} catch (e) {
console.error('[registry] 4khdhub fetch error:', e.message);
return [];
}
}
if (config.enable4khdhubProvider) {
providers.push({ name: '4khdhub', enabled: true, fetch: fetch4khdhub });
console.log('[registry] 4khdhub provider enabled');
} else {
console.log('[registry] 4khdhub disabled via config');
}
// MoviesMod adapter
async function fetchMoviesMod(ctx) {
if (!moviesmod) {
try { moviesmod = require('./moviesmod.js'); } catch (e) { console.error('Failed to load moviesmod provider:', e.message); return []; }
}
try {
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
const t0 = Date.now();
const result = await moviesmod.getMoviesModStreams(ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null);
const durationMs = Date.now() - t0;
console.log(`[registry] moviesmod fetch duration ${durationMs}ms`);
if (!Array.isArray(result)) return [];
return result.map(s => ({ ...s, name: s.name || 'MoviesMod', provider: 'moviesmod' }));
} catch (e) {
console.error('[registry] moviesmod fetch error:', e.message);
return [];
}
}
if (config.enableMoviesmodProvider) {
providers.push({ name: 'moviesmod', enabled: true, fetch: fetchMoviesMod });
console.log('[registry] moviesmod provider enabled');
} else {
console.log('[registry] moviesmod disabled via config');
}
// MP4Hydra adapter
async function fetchMp4hydra(ctx) {
if (!mp4hydra) {
try { mp4hydra = require('./MP4Hydra.js'); } catch (e) { console.error('Failed to load MP4Hydra provider:', e.message); return []; }
}
try {
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
const t0 = Date.now();
const result = await mp4hydra.getMP4HydraStreams(ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null);
const durationMs = Date.now() - t0;
console.log(`[registry] mp4hydra fetch duration ${durationMs}ms`);
if (!Array.isArray(result)) return [];
return result.map(s => ({ ...s, name: s.name || 'MP4Hydra', provider: 'mp4hydra' }));
} catch (e) {
console.error('[registry] mp4hydra fetch error:', e.message);
return [];
}
}
if (config.enableMp4hydraProvider) {
providers.push({ name: 'mp4hydra', enabled: true, fetch: fetchMp4hydra });
console.log('[registry] mp4hydra provider enabled');
} else {
console.log('[registry] mp4hydra disabled via config');
}
// VidZee adapter
async function fetchVidZee(ctx) {
if (!vidzee) {
try { vidzee = require('./VidZee.js'); } catch (e) { console.error('Failed to load VidZee provider:', e.message); return []; }
}
try {
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
const t0 = Date.now();
const result = await vidzee.getVidZeeStreams(ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null);
const durationMs = Date.now() - t0;
console.log(`[registry] vidzee fetch duration ${durationMs}ms`);
if (!Array.isArray(result)) return [];
return result.map(s => ({ ...s, name: s.name || 'VidZee', provider: 'vidzee' }));
} catch (e) {
console.error('[registry] vidzee fetch error:', e.message);
return [];
}
}
if (config.enableVidzeeProvider) {
providers.push({ name: 'vidzee', enabled: true, fetch: fetchVidZee });
console.log('[registry] vidzee provider enabled');
} else {
console.log('[registry] vidzee disabled via config');
}
// vixsrc adapter
async function fetchVixsrc(ctx) {
if (!vixsrc) {
try { vixsrc = require('./vixsrc.js'); } catch (e) { console.error('Failed to load vixsrc provider:', e.message); return []; }
}
try {
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
const t0 = Date.now();
const result = await vixsrc.getVixsrcStreams(ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null);
const durationMs = Date.now() - t0;
console.log(`[registry] vixsrc fetch duration ${durationMs}ms`);
if (!Array.isArray(result)) return [];
return result.map(s => ({ ...s, name: s.name || 'Vixsrc', provider: 'vixsrc' }));
} catch (e) {
console.error('[registry] vixsrc fetch error:', e.message);
return [];
}
}
if (config.enableVixsrcProvider) {
providers.push({ name: 'vixsrc', enabled: true, fetch: fetchVixsrc });
console.log('[registry] vixsrc provider enabled');
} else {
console.log('[registry] vixsrc disabled via config');
}
// xprime adapter (requires TMDB metadata lookup for title/year)
async function fetchXprime(ctx) {
if (!xprime) {
try { xprime = require('./xprime.js'); } catch (e) { console.error('Failed to load xprime provider:', e.message); return []; }
}
try {
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
const { getTmdbApiKey } = require('../utils/tmdbKey');
const tmdbApiKey = getTmdbApiKey();
if (!tmdbApiKey) {
console.warn('[registry] xprime skipped: TMDB API key missing');
return [];
}
// Fetch minimal TMDB metadata for title + year
let title = null; let year = null;
try {
const axios = require('axios');
const metaUrl = `https://api.themoviedb.org/3/${mediaType}/${ctx.tmdbId}?api_key=${tmdbApiKey}`;
const { data: meta } = await axios.get(metaUrl, { timeout: 8000 });
title = mediaType === 'movie' ? (meta.title || meta.original_title) : (meta.name || meta.original_name);
const dateStr = mediaType === 'movie' ? meta.release_date : meta.first_air_date;
if (dateStr) year = dateStr.split('-')[0];
} catch (mErr) {
console.warn('[registry] xprime metadata fetch failed:', mErr.message);
return [];
}
if (!title || !year) {
console.log('[registry] xprime missing title/year after TMDB lookup');
return [];
}
const t0 = Date.now();
const streams = await xprime.getXprimeStreams(title, year, mediaType, ctx.season || null, ctx.episode || null);
const durationMs = Date.now() - t0;
console.log(`[registry] xprime fetch duration ${durationMs}ms (title='${title}' year=${year}) returned ${Array.isArray(streams)?streams.length:0}`);
if (!Array.isArray(streams) || !streams.length) return [];
// Normalize quality labels that are non-numeric (e.g., FHD/HD/SD)
return streams.map(s => {
let q = s.quality || 'Unknown';
if (!/(\d{3,4})p/.test(q.toLowerCase())) {
const qLower = q.toLowerCase();
if (qLower.includes('fhd') || qLower.includes('full')) q = '1080p';
else if (qLower === 'hd' || qLower.includes('720')) q = '720p';
else if (qLower.includes('sd') || qLower.includes('480')) q = '480p';
else if (qLower.includes('cam')) q = '360p';
}
return { ...s, quality: q, name: s.name || 'Xprime', provider: 'xprime' };
}); });
} catch (e) { console.log(`[registry] ${providerInfo.name} provider enabled`);
console.error('[registry] xprime fetch error:', e.message); } else {
return []; console.log(`[registry] ${providerInfo.name} disabled via config`);
} }
} }
if (config.enableXprimeProvider) {
providers.push({ name: 'xprime', enabled: true, fetch: fetchXprime });
console.log('[registry] xprime provider enabled');
} else {
console.log('[registry] xprime disabled via config');
}
function listProviders() { return providers.map(p => ({ name: p.name, enabled: p.enabled })); } function listProviders() { return providers.map(p => ({ name: p.name, enabled: p.enabled })); }
function getProvider(name) { return providers.find(p => p.name === name.toLowerCase()) || null; } function getProvider(name) { return providers.find(p => p.name === name.toLowerCase()) || null; }
+37 -40
View File
@@ -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}` }
}; };
} }
} }
+1 -1
View File
@@ -195,7 +195,7 @@ function getVixsrcStreams(tmdbId, mediaType = 'movie', seasonNum = null, episode
title: "Auto Quality Stream", title: "Auto Quality Stream",
url: masterPlaylistUrl, url: masterPlaylistUrl,
quality: '1080p', quality: '1080p',
type: 'direct', provider: 'Vidsrc',
headers: { headers: {
'Referer': BASE_URL, 'Referer': BASE_URL,
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36' 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
+3 -3
View File
@@ -132,12 +132,12 @@ async function getXprimeStreams(title, year, type, seasonNum, episodeNum) {
Object.entries(item.streams).forEach(([quality, url]) => { Object.entries(item.streams).forEach(([quality, url]) => {
if (!url || typeof url !== 'string') return; if (!url || typeof url !== 'string') return;
streams.push({ streams.push({
name: title,
title: `${title} - ${type === 'tv' ? `S${String(seasonNum).padStart(2,'0')}E${String(episodeNum).padStart(2,'0')} ` : ''}${quality}`,
url, url,
quality: quality || 'Unknown', quality: quality || 'Unknown',
title: `${title} - ${type === 'tv' ? `S${String(seasonNum).padStart(2,'0')}E${String(episodeNum).padStart(2,'0')} ` : ''}${quality}`,
provider: 'Xprime.tv', provider: 'Xprime.tv',
codecs: [], headers: {},
size: 'Unknown size'
}); });
}); });
}; };
+1
View File
@@ -182,6 +182,7 @@
<p class="hint">Provider enable/disable & feature flags apply immediately. Click toggles to enable (highlighted) or disable (dim).</p> <p class="hint">Provider enable/disable & feature flags apply immediately. Click toggles to enable (highlighted) or disable (dim).</p>
<div class="adv-group"> <div class="adv-group">
<div class="field small"><label>Showbox Cache Dir <input type="text" name="adv_showboxCacheDir" placeholder="/data/showbox-cache"/></label></div> <div class="field small"><label>Showbox Cache Dir <input type="text" name="adv_showboxCacheDir" placeholder="/data/showbox-cache"/></label></div>
<div class="field small"><label>MoviesClub Proxy URL <p class="hint">Optional. Example: <code>https://winter-surf-e49f.inside4ndroid-techsup.workers.dev/?destination=</code><br/>Leave blank for direct links. The raw stream URL will be appended (URL encoded).</p><input type="text" name="adv_moviesclubProxy" placeholder="https://proxy/?destination=" /></label></div>
</div> </div>
</section> </section>
<section id="panel-live" class="panel"> <section id="panel-live" class="panel">
+21 -9
View File
@@ -174,6 +174,7 @@ function fillForm(data){
advBools.forEach(([cfgKey, elName])=>{ const el = f.elements[elName]; if(!el) return; const src = (override[cfgKey]!==undefined? override[cfgKey] : merged[cfgKey]); if(el.type==='checkbox'){ el.checked = !!src; } else { el.value = src? 'true':''; } }); advBools.forEach(([cfgKey, elName])=>{ const el = f.elements[elName]; if(!el) return; const src = (override[cfgKey]!==undefined? override[cfgKey] : merged[cfgKey]); if(el.type==='checkbox'){ el.checked = !!src; } else { el.value = src? 'true':''; } });
const advTexts = [ const advTexts = [
['showboxCacheDir','adv_showboxCacheDir'] ['showboxCacheDir','adv_showboxCacheDir']
,['moviesclubProxy','adv_moviesclubProxy']
]; ];
advTexts.forEach(([cfgKey, elName])=>{ const el = f.elements[elName]; if(!el) return; const val = override[cfgKey]!==undefined? override[cfgKey] : merged[cfgKey]; el.value = val || ''; }); advTexts.forEach(([cfgKey, elName])=>{ const el = f.elements[elName]; if(!el) return; const val = override[cfgKey]!==undefined? override[cfgKey] : merged[cfgKey]; el.value = val || ''; });
// Sync checkbox style advanced flags // Sync checkbox style advanced flags
@@ -257,6 +258,7 @@ async function save(){
}; };
Object.entries(boolMap).forEach(([formName,cfgKey])=>{ const el = f.elements[formName]; if(el) payload[cfgKey] = !!el.checked; }); Object.entries(boolMap).forEach(([formName,cfgKey])=>{ const el = f.elements[formName]; if(el) payload[cfgKey] = !!el.checked; });
const textMap = { adv_showboxCacheDir:'showboxCacheDir' }; const textMap = { adv_showboxCacheDir:'showboxCacheDir' };
textMap.adv_moviesclubProxy = 'moviesclubProxy';
Object.entries(textMap).forEach(([formName,cfgKey])=>{ const el = f.elements[formName]; if(!el) return; const v = el.value.trim(); payload[cfgKey] = v? v : null; }); Object.entries(textMap).forEach(([formName,cfgKey])=>{ const el = f.elements[formName]; if(!el) return; const v = el.value.trim(); payload[cfgKey] = v? v : null; });
const tmdbHidden = f.elements['tmdbApiKeysHidden']; const tmdbHidden = f.elements['tmdbApiKeysHidden'];
if (tmdbHidden){ if (tmdbHidden){
@@ -596,15 +598,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
View File
@@ -1 +1 @@
1758105049325 1758225404146
+51 -28
View File
@@ -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,25 +93,31 @@ function normalizeConfig(base) {
cfg.tmdbApiKeys = Array.from(new Set(cfg.tmdbApiKeys.map(k=>String(k).trim()).filter(Boolean))); cfg.tmdbApiKeys = Array.from(new Set(cfg.tmdbApiKeys.map(k=>String(k).trim()).filter(Boolean)));
// Do not expose legacy single-key field in merged config output // Do not expose legacy single-key field in merged config output
delete cfg.tmdbApiKey; delete cfg.tmdbApiKey;
// Booleans normalization
const boolKeys = [ // Dynamic provider enable flags
'enableShowboxProvider','enableXprimeProvider','enable4khdhubProvider','enableMoviesmodProvider','enableMp4hydraProvider','enableVidzeeProvider','enableVixsrcProvider', const providerNames = getProviderNames();
'disableCache','enablePStreamApi','disableUrlValidation','disable4khdhubUrlValidation' const boolKeys = providerNames.map(name => `enable${name.charAt(0).toUpperCase() + name.slice(1)}Provider`);
]; boolKeys.push('disableCache', 'enablePStreamApi', 'disableUrlValidation', 'disable4khdhubUrlValidation');
boolKeys.forEach(k=>{ if (cfg[k] === 'true') cfg[k] = true; else if (cfg[k] === 'false') cfg[k] = false; }); boolKeys.forEach(k=>{ if (cfg[k] === 'true') cfg[k] = true; else if (cfg[k] === 'false') cfg[k] = false; });
// Default values if undefined
if (cfg.enableShowboxProvider === undefined) cfg.enableShowboxProvider = true; // Set defaults for provider enable flags
if (cfg.enableXprimeProvider === undefined) cfg.enableXprimeProvider = true; providerNames.forEach(name => {
if (cfg.enable4khdhubProvider === undefined) cfg.enable4khdhubProvider = true; const flag = `enable${name.charAt(0).toUpperCase() + name.slice(1)}Provider`;
if (cfg.enableMoviesmodProvider === undefined) cfg.enableMoviesmodProvider = true; if (cfg[flag] === undefined) cfg[flag] = true; // Default to enabled
if (cfg.enableMp4hydraProvider === undefined) cfg.enableMp4hydraProvider = true; });
if (cfg.enableVidzeeProvider === undefined) cfg.enableVidzeeProvider = true;
if (cfg.enableVixsrcProvider === undefined) cfg.enableVixsrcProvider = true; // Default values for other flags
if (cfg.disableCache === undefined) cfg.disableCache = false; if (cfg.disableCache === undefined) cfg.disableCache = false;
if (cfg.enablePStreamApi === undefined) cfg.enablePStreamApi = true; if (cfg.enablePStreamApi === undefined) cfg.enablePStreamApi = true;
// Proxy features removed; always use direct connections // Proxy features removed; always use direct connections
if (cfg.disableUrlValidation === undefined) cfg.disableUrlValidation = false; if (cfg.disableUrlValidation === undefined) cfg.disableUrlValidation = false;
if (cfg.disable4khdhubUrlValidation === undefined) cfg.disable4khdhubUrlValidation = false; if (cfg.disable4khdhubUrlValidation === undefined) cfg.disable4khdhubUrlValidation = false;
// MoviesClub optional proxy base (string URL with trailing ?destination= or similar)
if (cfg.moviesclubProxy !== undefined && typeof cfg.moviesclubProxy === 'string') {
cfg.moviesclubProxy = cfg.moviesclubProxy.trim();
if (!cfg.moviesclubProxy) delete cfg.moviesclubProxy;
}
return cfg; return cfg;
} }
@@ -114,14 +133,15 @@ function applyConfigToEnv(cfg){
if (cfg.febboxCookies && cfg.febboxCookies.length) process.env.FEBBOX_COOKIES = cfg.febboxCookies.join(','); if (cfg.febboxCookies && cfg.febboxCookies.length) process.env.FEBBOX_COOKIES = cfg.febboxCookies.join(',');
else delete process.env.FEBBOX_COOKIES; else delete process.env.FEBBOX_COOKIES;
if (cfg.defaultRegion) process.env.DEFAULT_REGION = cfg.defaultRegion; else delete process.env.DEFAULT_REGION; if (cfg.defaultRegion) process.env.DEFAULT_REGION = cfg.defaultRegion; else delete process.env.DEFAULT_REGION;
// Provider enable flags
process.env.ENABLE_SHOWBOX_PROVIDER = cfg.enableShowboxProvider ? 'true':'false'; // Dynamic provider enable flags
process.env.ENABLE_XPRIME_PROVIDER = cfg.enableXprimeProvider ? 'true':'false'; const providerNames = getProviderNames();
process.env.ENABLE_4KHDHUB_PROVIDER = cfg.enable4khdhubProvider ? 'true':'false'; providerNames.forEach(name => {
process.env.ENABLE_MOVIESMOD_PROVIDER = cfg.enableMoviesmodProvider ? 'true':'false'; const flag = `enable${name.charAt(0).toUpperCase() + name.slice(1)}Provider`;
process.env.ENABLE_MP4HYDRA_PROVIDER = cfg.enableMp4hydraProvider ? 'true':'false'; const envName = `ENABLE_${name.toUpperCase()}_PROVIDER`;
process.env.ENABLE_VIDZEE_PROVIDER = cfg.enableVidzeeProvider ? 'true':'false'; process.env[envName] = cfg[flag] ? 'true' : 'false';
process.env.ENABLE_VIXSRC_PROVIDER = cfg.enableVixsrcProvider ? 'true':'false'; });
// Caching / validation / PStream // Caching / validation / PStream
process.env.DISABLE_CACHE = cfg.disableCache ? 'true':'false'; process.env.DISABLE_CACHE = cfg.disableCache ? 'true':'false';
process.env.ENABLE_PSTREAM_API = cfg.enablePStreamApi ? 'true':'false'; process.env.ENABLE_PSTREAM_API = cfg.enablePStreamApi ? 'true':'false';
@@ -137,6 +157,8 @@ function applyConfigToEnv(cfg){
delete process.env.VIDZEE_PROXY_URL; delete process.env.VIDZEE_PROXY_URL;
delete process.env.VIDSRC_PROXY_URL; delete process.env.VIDSRC_PROXY_URL;
delete process.env.MOVIESMOD_PROXY_URL; delete process.env.MOVIESMOD_PROXY_URL;
// Mirror moviesclub proxy for potential legacy usage
if (cfg.moviesclubProxy) process.env.MOVIESCLUB_PROXY_URL = cfg.moviesclubProxy; else delete process.env.MOVIESCLUB_PROXY_URL;
if (cfg.defaultRegion) process.env.FEBBOX_REGION = cfg.defaultRegion; // alias if (cfg.defaultRegion) process.env.FEBBOX_REGION = cfg.defaultRegion; // alias
} }
@@ -152,13 +174,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 +181,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);
+113
View File
@@ -0,0 +1,113 @@
class JsUnpacker {
static Unbase = class {
constructor(radix) {
this.ALPHABET_62 =
'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
this.ALPHABET_95 =
' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~';
this.alphabet = null;
this.dictionary = {};
this.radix = radix;
if (radix > 36) {
if (radix < 62) {
this.alphabet = this.ALPHABET_62.substring(0, radix);
} else if (radix > 62 && radix < 95) {
this.alphabet = this.ALPHABET_95.substring(0, radix);
} else if (radix === 62) {
this.alphabet = this.ALPHABET_62;
} else if (radix === 95) {
this.alphabet = this.ALPHABET_95;
}
for (let i = 0; i < this.alphabet.length; i++) {
this.dictionary[this.alphabet[i]] = i;
}
}
}
unbase(str) {
let ret = 0;
if (this.alphabet === null) {
ret = parseInt(str, this.radix);
} else {
const tmp = str.split('').reverse().join('');
for (let i = 0; i < tmp.length; i++) {
ret += Math.pow(this.radix, i) * this.dictionary[tmp[i]];
}
}
return ret;
}
};
constructor(packedJS) {
this.packedJS = packedJS;
}
detect() {
const js = this.packedJS.replace(/\s/g, '');
const regex = /eval\(function\(p,a,c,k,e,(?:r|d)/;
return regex.test(js);
}
unpack() {
let js = this.packedJS;
try {
const regex =
/}\s*\('(.*)',\s*(.*?),\s*(\d+),\s*'(.*?)'\.split\('\|'\)/s;
const match = js.match(regex);
if (match && match.length === 5) {
let payload = match[1].replace(/\\'/g, "'");
const radixStr = match[2];
const countStr = match[3];
const symtab = match[4].split('|');
let radix = 36;
let count = 0;
try {
radix = parseInt(radixStr);
} catch (e) {}
try {
count = parseInt(countStr);
} catch (e) {}
if (symtab.length !== count) {
throw new Error('Unknown p.a.c.k.e.r. encoding');
}
const unbase = new this.constructor.Unbase(radix);
const wordRegex = /\b\w+\b/g;
let decoded = payload;
let replaceOffset = 0;
let wordMatch;
while ((wordMatch = wordRegex.exec(payload)) !== null) {
const word = wordMatch[0];
const x = unbase.unbase(word);
let value = null;
if (x < symtab.length) {
value = symtab[x];
}
if (value && value.length > 0) {
const start = wordMatch.index + replaceOffset;
const end = start + word.length;
decoded =
decoded.substring(0, start) +
value +
decoded.substring(end);
replaceOffset += value.length - word.length;
}
}
return decoded;
}
} catch (e) {
return null;
}
return null;
}
}
// CommonJS exports
module.exports = JsUnpacker;
+9 -4
View File
@@ -1,19 +1,24 @@
{ {
"defaultProviders": [], "defaultProviders": [],
"configVersion": 1, "configVersion": 1,
"tmdbApiKeys": [], "tmdbApiKeys": [
"f3c627493095a7e40ceca68355c94c6d"
],
"enableShowboxProvider": true, "enableShowboxProvider": true,
"febboxCookies": [], "febboxCookies": [
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3NTc5NjYxMDAsIm5iZiI6MTc1Nzk2NjEwMCwiZXhwIjoxNzg5MDcwMTIwLCJkYXRhIjp7InVpZCI6ODc2NTEwLCJ0b2tlbiI6ImY0MDQxNWJkMTdiYjc1YmQ2ZWQ1NzQ1ZTFhZmNlOTM5In19.vs_pLXAJYhnEaHDfcR_dv9-3us2MJhMOMHUC4la9Rag"
],
"enableXprimeProvider": true, "enableXprimeProvider": true,
"enable4khdhubProvider": true, "enable4khdhubProvider": true,
"enableMoviesmodProvider": true, "enableMoviesmodProvider": true,
"enableMp4hydraProvider": true, "enableMp4hydraProvider": true,
"enableVidzeeProvider": true, "enableVidzeeProvider": true,
"enableVixsrcProvider": true, "enableVixsrcProvider": true,
"disableCache": false, "disableCache": true,
"enablePStreamApi": true, "enablePStreamApi": true,
"disableUrlValidation": false, "disableUrlValidation": false,
"disable4khdhubUrlValidation": false, "disable4khdhubUrlValidation": false,
"excludeCodecsRaw": "{\"excludeDV\":false,\"excludeHDR\":false}", "excludeCodecsRaw": "{\"excludeDV\":false,\"excludeHDR\":false}",
"minQualitiesRaw": "all" "minQualitiesRaw": "all",
"port": 8787
} }