5 Commits
22 changed files with 1732 additions and 950 deletions
+2
View File
@@ -1 +1,3 @@
/node_modules
restart.trigger
.vscode/
+36 -1
View File
@@ -2,6 +2,38 @@
All notable changes to this project will be documented in this file.
## [1.0.4] - 2025-09-18
### Added
- New provider: `moviesclub` with registry mapping (`moviesclub.js` → `getMoviesClubStreams`).
### Changed
- Unified provider stream schema across all providers:
```json
{ "title": "…", "url": "…", "quality": "…", "provider": "…", "headers": { } }
```
- Provider integration docs: When adding a provider, also add its exported function to `providerFunctionMap` in `providers/registry.js`.
- README Docker quick run snippet updated to: `docker run -it -p 8787:8787 inside4ndroid/tmdb-embed-api:latest`.
### Documentation
- README: Updated Providers section, unified schema example, MoviesClub listed, registry mapping instructions, and badge version.
## [1.0.3] - 2025-09-17
### Fixed
- Server now binds to `0.0.0.0` by default so Docker port publishing works correctly from the host. Added `BIND_HOST=0.0.0.0` in Dockerfile and compose.
### Notes
- If you were seeing `ERR_CONNECTION_REFUSED` on `http://localhost:8787`, pull the latest image or rebuild, then re-run with `-p 8787:8787`.
## [1.0.2] - 2025-09-17
### Fixed
- Standardized Docker port to `8787` everywhere (Dockerfile `EXPOSE`, compose `ports`, healthchecks, and README examples). Previous release notes mentioned 8787 but some environments weren't reset; this release ensures consistency.
### Documentation
- Updated README version badge to 1.0.2 and verified Docker commands use `-p 8787:8787`.
## [1.0.1] - 2025-09-17
### Added
@@ -29,7 +61,10 @@ All notable changes to this project will be documented in this file.
## [1.0.0] - 2025-09-16
- Initial stable release.
[1.0.1]: https://github.com/Inside4ndroid/TMDB-Embed-API/compare/v1.0.0...v1.0.1# Changelog
[1.0.3]: https://github.com/Inside4ndroid/TMDB-Embed-API/compare/v1.0.2...v1.0.3
[1.0.4]: https://github.com/Inside4ndroid/TMDB-Embed-API/compare/v1.0.3...v1.0.4
[1.0.2]: https://github.com/Inside4ndroid/TMDB-Embed-API/compare/v1.0.1...v1.0.2
[1.0.1]: https://github.com/Inside4ndroid/TMDB-Embed-API/compare/v1.0.0...v1.0.1
## [1.0.0] - 2025-09-16
+1
View File
@@ -22,6 +22,7 @@ ARG VERSION=dev
WORKDIR /app
ENV NODE_ENV=production \
API_PORT=8787 \
BIND_HOST=0.0.0.0 \
APP_VERSION=${VERSION}
# Create non-root user
+70 -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/Status-Active-success?style=flat" />
<img src="https://img.shields.io/badge/License-MIT-blue?style=flat" />
<img src="https://img.shields.io/badge/Version-1.0.1-informational?style=flat" />
<img src="https://img.shields.io/badge/Version-1.0.4-informational?style=flat" />
<img src="https://img.shields.io/docker/pulls/inside4ndroid/tmdb-embed-api?label=Docker%20Pulls&style=flat" />
</p>
@@ -34,14 +34,15 @@
## ✨ Features
- **Multi‑TMDB Key Rotation** – Supply multiple API keys; one is chosen randomly per request.
- **Provider Aggregation** – Pluggable providers (Showbox, 4khdhub, MoviesMod, MP4Hydra, VidZee, Vixsrc, Xprime) with per‑provider enable toggles + default selection.
- **Provider Aggregation** – Pluggable providers (Showbox, 4khdhub, MoviesMod, MP4Hydra, VidZee, Vixsrc, Xprime, MoviesClub) with per‑provider enable toggles + default selection.
- **🔥 Plugin System** – Drop new provider files in `providers/` and add its exported function to the registry map (`providers/registry.js` → `providerFunctionMap`).
- **Dynamic Filtering** – Minimum quality presets, custom JSON quality map, codec exclusion rules (presets + JSON).
- **Runtime Overrides UI** – Fully interactive web admin at `/` (login protected) writing to `utils/user-config.json`.
- **Session Auth + Rate Limiting** – Login system with brute‑force lockouts, logout, password change.
- **Status & Health Panel** – Live metrics, provider status, endpoint list, functional provider checks.
- **Config Propagation** – Overrides mirrored to `process.env` for legacy compatibility (no `.env` required after first save).
- **Back‑Navigation Safe** – Cache-control + visibility/session revalidation.
- **Extensible** – Simple provider registry pattern.
- **Extensible** – Simple drop-in provider plugin system.
---
@@ -79,6 +80,11 @@ docker run --name tmdb-embed-api -p 8787:8787 \
inside4ndroid/tmdb-embed-api:latest
```
Or the minimal quick-test run:
```bash
docker run -it -p 8787:8787 inside4ndroid/tmdb-embed-api:latest
```
Persist overrides (Windows PowerShell example) by mounting a local file:
```powershell
New-Item -ItemType File -Path .\utils\user-config.json -Force | Out-Null
@@ -202,22 +208,64 @@ Session is revalidated on visibility and back/forward navigation to prevent stal
---
## 🔌 Providers
Current built‑in providers:
- `showbox`
- `4khdhub`
- `moviesmod`
- `mp4hydra`
- `vidzee`
- `vixsrc`
- `xprime`
The API supports a plugin system. Drop a new provider file in the `providers/` folder and register its exported function in `providers/registry.js` under `providerFunctionMap`.
You can enable/disable and mark defaults via the Provider Matrix in the UI. If **no defaults** are selected, all enabled providers participate in aggregation.
### Current Built-in Providers
- `showbox` - Showbox/PStream integration
- `4khdhub` - 4KHDHub streams
- `moviesmod` - MoviesMod streams
- `mp4hydra` - MP4Hydra streams
- `vidzee` - VidZee streams
- `vixsrc` - Vixsrc streams
- `xprime` - Xprime streams
- `uhdmovies` - UHD Movies streams
- `moviesclub` - MoviesClub streams
### Adding a New Provider (Outline)
1. Create `providers/myprovider.js` exporting an async fetch function (see existing files for patterns).
2. Lazy‑load it in `providers/registry.js` similar to others.
3. Add a config flag (`enableMyproviderProvider`) if you want toggle support.
4. Update the provider matrix arrays in `public/index.js`.
### Adding a New Provider
1. **Create** `providers/yourprovider.js` with your stream fetching logic
2. **Export** a function like `getYourproviderStreams(tmdbId, mediaType, season, episode)`
3. **Register** it in `providers/registry.js` → `providerFunctionMap`:
```js
// providers/registry.js
const providerFunctionMap = {
'Showbox.js': 'getStreamsFromTmdbId',
'4khdhub.js': 'get4KHDHubStreams',
'moviesmod.js': 'getMoviesModStreams',
'MP4Hydra.js': 'getMP4HydraStreams',
'VidZee.js': 'getVidZeeStreams',
'vixsrc.js': 'getVixsrcStreams',
'xprime.js': 'getXprimeStreams',
'uhdmovies.js': 'getUHDMoviesStreams',
'moviesclub.js': 'getMoviesClubStreams',
'yourprovider.js': 'getYourproviderStreams'
};
```
4. The provider will appear in the admin UI with an enable/disable toggle.
**Example Provider (Unified Output):**
```javascript
async function getYourproviderStreams(tmdbId, mediaType, season, episode) {
// Your scraping/API logic here
return [{
title: "Fight Club - 1080p [YourProvider #1]",
url: "https://stream.url/video.mp4",
quality: "1080p",
provider: "yourprovider",
headers: { "User-Agent": "Mozilla/5.0" }
}];
}
module.exports = { getYourproviderStreams };
```
> **⚠️ Important**: All providers must return streams in the unified JSON format to ensure compatibility with filtering and aggregation.
The system automatically:
- ✅ Detects new provider files
- ✅ Adds enable/disable toggles in the admin UI
- ✅ Includes them in stream aggregation
- ✅ Applies filtering and quality controls
- ✅ No core file edits required!
---
@@ -238,18 +286,14 @@ Aggregate endpoint auto-resolves IMDb when needed and merges provider output bef
---
## 🧪 Stream Object Shape (Typical)
## 🧪 Stream Object Schema (Unified)
```json
{
"title": "Example Title",
"url": "https://...",
"title": "Fight Club - 1080p [MP4Hydra #2]",
"url": "https://stream.url/video.mp4",
"quality": "1080p",
"provider": "showbox",
"size": null,
"languages": ["en"],
"subtitles": [],
"codecs": ["H264"],
"headers": { "Referer": "..." }
"provider": "mp4hydra",
"headers": { "User-Agent": "Mozilla/5.0" }
}
```
+6 -2
View File
@@ -374,8 +374,12 @@ app.get('/api/streams/:provider/:type/:tmdbId', async (req,res) => {
});
const PORT = config.port;
const server = app.listen(PORT, () => {
console.log(`TMDB Embed REST API listening on http://localhost:${PORT}`);
const HOST = process.env.BIND_HOST || '0.0.0.0';
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(' GET /api/health');
console.log(' GET /api/metrics');
+1
View File
@@ -9,6 +9,7 @@ services:
# Alternative: supply JSON array
# - TMDB_API_KEYS=["key1","key2"]
- API_PORT=8787
- BIND_HOST=0.0.0.0
# FebBox cookies (comma separated) if you want Showbox immediately functional
- FEBBOX_COOKIES=${FEBBOX_COOKIES:-}
ports:
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "tmdb-embed-api",
"version": "1.0.1",
"version": "1.0.4",
"description": "Standalone streaming embed source aggregation API (TMDB ID in, streams out)",
"main": "apiServer.js",
"scripts": {
+60 -23
View File
@@ -397,8 +397,10 @@ function generateAlternativeQueries(title, originalTitle = null) {
return romans[num] || match;
}),
title.replace(/\b(I{1,3}|IV|V|VI{0,3}|IX|X)\b/g, match => {
const romans = { 'I': '1', 'II': '2', 'III': '3', 'IV': '4', 'V': '5',
'VI': '6', 'VII': '7', 'VIII': '8', 'IX': '9', 'X': '10' };
const romans = {
'I': '1', 'II': '2', 'III': '3', 'IV': '4', 'V': '5',
'VI': '6', 'VII': '7', 'VIII': '8', 'IX': '9', 'X': '10'
};
return romans[match] || match;
})
];
@@ -754,7 +756,9 @@ function extractHubCloudLinks(url, referer) {
name: `4KHDHub - FSL Server${qualityLabel}`,
title: finalTitle,
url: link,
quality: quality
quality: quality,
provider: '4khdhub',
headers: {}
});
})
.catch(() => {
@@ -768,7 +772,9 @@ function extractHubCloudLinks(url, referer) {
name: `4KHDHub - FSL Server${qualityLabel}`,
title: finalTitle,
url: link,
quality: quality
quality: quality,
provider: '4khdhub',
headers: {}
});
});
} else if (text.includes('Download File')) {
@@ -786,7 +792,9 @@ function extractHubCloudLinks(url, referer) {
name: `4KHDHub - HubCloud${qualityLabel}`,
title: finalTitle,
url: link,
quality: quality
quality: quality,
provider: '4khdhub',
headers: {}
});
})
.catch(() => {
@@ -800,7 +808,9 @@ function extractHubCloudLinks(url, referer) {
name: `4KHDHub - HubCloud${qualityLabel}`,
title: finalTitle,
url: link,
quality: quality
quality: quality,
provider: '4khdhub',
headers: {}
});
});
} else if (text.includes('BuzzServer')) {
@@ -829,7 +839,9 @@ function extractHubCloudLinks(url, referer) {
name: `4KHDHub - BuzzServer${qualityLabel}`,
title: finalTitle,
url: finalUrl,
quality: quality
quality: quality,
provider: '4khdhub',
headers: {}
});
})
.catch(() => {
@@ -843,7 +855,9 @@ function extractHubCloudLinks(url, referer) {
name: `4KHDHub - BuzzServer${qualityLabel}`,
title: finalTitle,
url: finalUrl,
quality: quality
quality: quality,
provider: '4khdhub',
headers: {}
});
});
} else {
@@ -880,7 +894,9 @@ function extractHubCloudLinks(url, referer) {
name: `4KHDHub - Pixeldrain${qualityLabel}`,
title: finalTitle,
url: convertedLink,
quality: quality
quality: quality,
provider: '4khdhub',
headers: {}
});
})
.catch(() => {
@@ -894,7 +910,9 @@ function extractHubCloudLinks(url, referer) {
name: `4KHDHub - Pixeldrain${qualityLabel}`,
title: finalTitle,
url: convertedLink,
quality: quality
quality: quality,
provider: '4khdhub',
headers: {}
});
});
} else if (text.includes('S3 Server')) {
@@ -912,7 +930,9 @@ function extractHubCloudLinks(url, referer) {
name: `4KHDHub - S3 Server${qualityLabel}`,
title: finalTitle,
url: link,
quality: quality
quality: quality,
provider: '4khdhub',
headers: {}
});
})
.catch(() => {
@@ -926,7 +946,9 @@ function extractHubCloudLinks(url, referer) {
name: `4KHDHub - S3 Server${qualityLabel}`,
title: finalTitle,
url: link,
quality: quality
quality: quality,
provider: '4khdhub',
headers: {}
});
});
} else if (text.includes('10Gbps')) {
@@ -966,7 +988,9 @@ function extractHubCloudLinks(url, referer) {
name: `4KHDHub - 10Gbps Server${qualityLabel}`,
title: finalTitle,
url: decodedUrl,
quality: quality
quality: quality,
provider: '4khdhub',
headers: {}
};
})
.catch(() => {
@@ -980,7 +1004,9 @@ function extractHubCloudLinks(url, referer) {
name: `4KHDHub - 10Gbps Server${qualityLabel}`,
title: finalTitle,
url: decodedUrl,
quality: quality
quality: quality,
provider: '4khdhub',
headers: {}
};
});
}
@@ -1016,7 +1042,9 @@ function extractHubCloudLinks(url, referer) {
name: `4KHDHub - HubCloud${qualityLabel}`,
title: finalTitle,
url: link,
quality: quality
quality: quality,
provider: '4khdhub',
headers: {}
});
})
.catch(() => {
@@ -1030,7 +1058,9 @@ function extractHubCloudLinks(url, referer) {
name: `4KHDHub - HubCloud${qualityLabel}`,
title: finalTitle,
url: link,
quality: quality
quality: quality,
provider: '4khdhub',
headers: {}
});
});
}
@@ -1388,7 +1418,9 @@ function processHubDriveLink(href, referer, filename = 'Unknown', size = '', qua
name: `4KHDHub - HubDrive${qualityLabel}`,
title: finalTitle,
url: href,
quality: quality
quality: quality,
provider: '4khdhub',
headers: {}
}];
})
.catch(() => {
@@ -1402,7 +1434,9 @@ function processHubDriveLink(href, referer, filename = 'Unknown', size = '', qua
name: `4KHDHub - HubDrive${qualityLabel}`,
title: finalTitle,
url: href,
quality: quality
quality: quality,
provider: '4khdhub',
headers: {}
}];
});
}
@@ -1463,7 +1497,9 @@ function processExtractorLink(link, resolve, linkNumber) {
name: '4KHDHub Direct Link',
title: finalTitle,
url: link,
quality: 1080
quality: 1080,
provider: "4khdhub",
headers: {}
}]);
})
.catch(() => {
@@ -1477,7 +1513,9 @@ function processExtractorLink(link, resolve, linkNumber) {
name: '4KHDHub Direct Link',
title: finalTitle,
url: link,
quality: 1080
quality: 1080,
provider: "4khdhub",
headers: {}
}]);
});
} else {
@@ -1745,9 +1783,8 @@ async function get4KHDHubStreams(tmdbId, type, season = null, episode = null) {
title: link.title || link.name,
url: link.url,
quality: `${link.quality}p`,
behaviorHints: {
bingeGroup: '4khdhub-streams'
}
provider: "4khdhub",
headers: {}
}));
console.log(`[4KHDHub] Returning ${streams.length} streams`);
+11 -29
View File
@@ -126,14 +126,11 @@ async function getMP4HydraStreams(tmdbId, mediaType, seasonNum = null, episodeNu
title: `${details.title} - ${seasonEpisode} - ${processedEpisode.quality} [MP4Hydra ${serverNumber}]`,
url: processedEpisode.videoUrl,
quality: processedEpisode.quality,
provider: "MP4Hydra",
size: "Unknown size",
behaviorHints: {
notWebReady: true,
provider: "mp4hydra",
headers: {
'Referer': 'https://mp4hydra.org/'
}
}
});
// Add subtitle tracks if available
@@ -172,14 +169,11 @@ async function getMP4HydraStreams(tmdbId, mediaType, seasonNum = null, episodeNu
title: `${details.title} - ${processedItem.quality} [MP4Hydra ${serverNumber}]`,
url: processedItem.videoUrl,
quality: processedItem.quality,
provider: "MP4Hydra",
size: "Unknown size",
behaviorHints: {
notWebReady: true,
provider: "mp4hydra",
headers: {
'Referer': 'https://mp4hydra.org/'
}
}
});
// Add subtitle tracks if available
@@ -297,14 +291,11 @@ async function tryAlternativeTitle(details, mediaType, seasonNum, episodeNum) {
title: `${details.original_title} - ${seasonEpisode} - ${processedEpisode.quality} [MP4Hydra ${serverNumber}]`,
url: processedEpisode.videoUrl,
quality: processedEpisode.quality,
provider: "MP4Hydra",
size: "Unknown size",
behaviorHints: {
notWebReady: true,
provider: "mp4hydra",
headers: {
'Referer': 'https://mp4hydra.org/'
}
}
});
// Add subtitle tracks if available
@@ -340,17 +331,10 @@ async function tryAlternativeTitle(details, mediaType, seasonNum, episodeNum) {
// Convert to standard stream format
streams.push({
name: `${details.original_title}`,
title: `${details.original_title} - ${processedItem.quality} [MP4Hydra ${serverNumber}]`,
url: processedItem.videoUrl,
quality: processedItem.quality,
provider: "MP4Hydra",
size: "Unknown size",
behaviorHints: {
notWebReady: true,
headers: {
'Referer': 'https://mp4hydra.org/'
}
}
quality: processedItem.quality
});
// Add subtitle tracks if available
@@ -435,17 +419,15 @@ async function tryWithoutYear(details, mediaType, seasonNum, episodeNum) {
// Convert to standard stream format
streams.push({
name: `${details.title}`,
title: `${details.title} - ${processedItem.quality} [MP4Hydra ${serverNumber}]`,
url: processedItem.videoUrl,
quality: processedItem.quality,
provider: "MP4Hydra",
size: "Unknown size",
behaviorHints: {
notWebReady: true,
provider: "mp4hydra",
headers: {
'Referer': 'https://mp4hydra.org/'
}
}
});
// Add subtitle tracks if available
+24 -20
View File
@@ -1980,10 +1980,12 @@ const getStreamsFromTmdbId = async (tmdbType, tmdbId, seasonNum = null, episodeN
}
} catch (e) { /* ignore if URL parsing fails */ }
streamsFromThisShareInfo.push({
name: streamTitle,
title: streamTitle,
url: source.url,
quality: parseQualityFromLabel(source.label),
codecs: extractCodecDetails(key5FromDirectSource || streamTitle)
provider: 'Febbox Direct',
headers: {}
});
}
// If direct sources are found, original code used 'continue',
@@ -2007,10 +2009,12 @@ const getStreamsFromTmdbId = async (tmdbType, tmdbId, seasonNum = null, episodeN
const streamTitle = `${baseStreamTitle} - ${source.label}`;
streamsFromThisShareInfo.push({
name: streamTitle,
title: streamTitle,
url: source.url,
quality: parseQualityFromLabel(source.label),
codecs: extractCodecDetails(source.detailedFilename || streamTitle)
provider: 'Febbox FID',
headers: {}
});
}
}
@@ -2202,10 +2206,12 @@ const processShowWithSeasonsEpisodes = async (febboxUrl, showboxTitle, seasonNum
for (const source of directSources) {
const streamTitle = `${showboxTitle} - ${source.label}`;
streamsForThisCall.push({ // MODIFIED: Push to streamsForThisCall
name: streamTitle,
title: streamTitle,
url: source.url,
quality: parseQualityFromLabel(source.label),
codecs: extractCodecDetails(source.detailedFilename || streamTitle)
provider: 'Febbox Direct',
headers: {}
});
}
console.timeEnd(processTimerLabel);
@@ -2222,10 +2228,12 @@ const processShowWithSeasonsEpisodes = async (febboxUrl, showboxTitle, seasonNum
for (const source of sources) {
const streamTitle = `${showboxTitle} - ${source.label}`;
streamsForThisCall.push({ // MODIFIED: Push to streamsForThisCall
name: streamTitle,
title: streamTitle,
url: source.url,
quality: parseQualityFromLabel(source.label),
codecs: extractCodecDetails(source.detailedFilename || streamTitle)
provider: 'Febbox FID',
headers: {}
});
}
}
@@ -2506,10 +2514,12 @@ const processShowWithSeasonsEpisodes = async (febboxUrl, showboxTitle, seasonNum
const streamTitle = `${showboxTitle} - S${seasonNum}${episodeNum && selectedEpisode && fid === selectedEpisode.fid ? `E${episodeNum}` : (episodeDetail ? `E${episodeDetail.episodeNum}` : '')} - ${episodeName} - ${source.label}`;
streamsForThisCall.push({ // MODIFIED: Push to streamsForThisCall
name: streamTitle,
title: streamTitle,
url: source.url,
quality: parseQualityFromLabel(source.label),
codecs: extractCodecDetails(source.detailedFilename || streamTitle)
provider: 'Febbox FID',
headers: {}
});
}
} else {
@@ -2862,17 +2872,15 @@ const processPStreamResponse = (apiResponse, tmdbType, seasonNum, episodeNum) =>
for (const [quality, url] of Object.entries(apiResponse.streams)) {
if (url && typeof url === 'string') {
// Use "Auto" for name and actual filename for title
const streamName = "Auto";
const streamTitle = apiResponse.name || `${tmdbType === 'movie' ? 'PStream' : `PStream - S${seasonNum}E${episodeNum}`} - ${quality}`;
streams.push({
name: streamName,
name: streamTitle,
title: streamTitle,
url: url,
quality: parseQualityFromLabel(quality),
codecs: extractCodecDetails(apiResponse.name || quality), // Use filename for codec detection
size: apiResponse.size || 'Unknown size',
provider: 'PStream'
provider: 'PStream',
headers: {}
});
}
}
@@ -2890,17 +2898,15 @@ const processPStreamResponse = (apiResponse, tmdbType, seasonNum, episodeNum) =>
continue;
}
const streamName = "Auto";
const streamTitle = apiResponse.name || `${tmdbType === 'movie' ? 'PStream' : `PStream - S${seasonNum}E${episodeNum}`} - ${source.quality}`;
streams.push({
name: streamName,
name: streamTitle,
title: streamTitle,
url: source.url,
quality: parseQualityFromLabel(source.quality),
codecs: source.codec ? [source.codec] : [],
size: source.size || 'Unknown size',
provider: 'PStream'
provider: 'PStream',
headers: {}
});
}
@@ -2913,17 +2919,15 @@ const processPStreamResponse = (apiResponse, tmdbType, seasonNum, episodeNum) =>
for (const item of apiResponse.data) {
if (item.url || item.file) {
const streamName = "Auto";
const streamTitle = apiResponse.name || `${tmdbType === 'movie' ? 'PStream' : `PStream - S${seasonNum}E${episodeNum}`} - ${item.quality || item.label || 'Unknown'}`;
streams.push({
name: streamName,
name: streamTitle,
title: streamTitle,
url: item.url || item.file,
quality: parseQualityFromLabel(item.quality || item.label),
codecs: item.codec ? [item.codec] : [],
size: item.size || 'Unknown size',
provider: 'PStream'
provider: 'PStream',
headers: {}
});
}
}
+2 -6
View File
@@ -84,18 +84,14 @@ const getVidZeeStreams = async (tmdbId, mediaType, seasonNum, episodeNum) => {
const language = sourceItem.language || sourceItem.lang;
return {
title: `VidZee S${sr} - ${quality}`,
name: `VidZee Server${sr} - ${quality} - ${language}`,
title: `VidZee Server${sr} - ${quality} - ${language}`,
url: sourceItem.link, // Use sourceItem.link for the URL
quality: quality,
language: language,
provider: "VidZee",
size: "Unknown size",
behaviorHints: {
notWebReady: true,
headers: {
'Referer': 'https://core.vidzee.wtf/'
}
}
};
}).filter(stream => stream.url);
+629
View File
@@ -0,0 +1,629 @@
// MoviesClub Provider
// Extracts streams from moviesapi.club
const axios = require('axios');
const JsUnpacker = require('../utils/jsunpack');
async function getMoviesClubStreams(tmdbId, mediaType, season, episode) {
console.log(`[moviesclub] Fetching streams for ${mediaType} tmdbId=${tmdbId} season=${season} episode=${episode}`);
const streams = [];
try {
// Build the URL based on media type
let url;
if (mediaType === 'movie') {
url = `https://moviesapi.club/movie/${tmdbId}`;
} else if (mediaType === 'tv' && season && episode) {
url = `https://moviesapi.club/tv/${tmdbId}-${season}-${episode}`;
} else {
console.log('[moviesclub] Invalid parameters for TV show');
return [];
}
console.log(`[moviesclub] Fetching from: ${url}`);
// Fetch the main page
const response = await axios.get(url, {
timeout: 10000,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1'
}
});
const html = response.data;
console.log(`[moviesclub] Fetched ${html.length} characters`);
// Extract data-url from srvswitch-menu
const dataUrlMatch = html.match(/data-url="([^"]+)"/);
if (!dataUrlMatch) {
console.log('[moviesclub] No data-url found in HTML');
return [];
}
const embedUrl = dataUrlMatch[1];
console.log(`[moviesclub] Found embed URL: ${embedUrl}`);
// Check if this is a cdn.moviesapi.club embed URL with multiple servers
if (embedUrl.includes('cdn.moviesapi.club')) {
console.log('[moviesclub] Detected cdn.moviesapi.club embed URL, handling multiple servers');
return await handleMultipleServers(embedUrl, mediaType, season, episode, tmdbId);
}
// Handle regular embed URLs (existing logic)
return await handleSingleServer(embedUrl, mediaType, season, episode, tmdbId);
} catch (error) {
console.error(`[moviesclub] Error:`, error.message);
return [];
}
}
// Handle cdn.moviesapi.club URLs with multiple servers
async function handleMultipleServers(embedUrl, mediaType, season, episode, tmdbId) {
console.log(`[moviesclub] Handling multiple servers from: ${embedUrl}`);
const streams = [];
try {
// Fetch the embed page
const embedResponse = await axios.get(embedUrl, {
timeout: 10000,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate',
'Connection': 'keep-alive'
}
});
const embedHtml = embedResponse.data;
console.log(`[moviesclub] Fetched embed page: ${embedHtml.length} characters`);
// Extract server options from the HTML
const serverMatches = embedHtml.match(/<div class="server"[^>]*data-hash="([^"]+)"[^>]*>([^<]+)<\/div>/gi);
if (!serverMatches || serverMatches.length === 0) {
console.log('[moviesclub] No server options found, trying direct iframe extraction');
// Try to extract the main iframe src
const iframeMatch = embedHtml.match(/<iframe[^>]*src="([^"]*)"[^>]*>/i);
if (iframeMatch) {
const iframeSrc = iframeMatch[1];
console.log(`[moviesclub] Found iframe src: ${iframeSrc}`);
// Try to decode if it's base64
try {
const decodedSrc = Buffer.from(iframeSrc.split('/').pop(), 'base64').toString('utf-8');
console.log(`[moviesclub] Decoded iframe src: ${decodedSrc}`);
// If it's a URL, use it directly
if (decodedSrc.startsWith('http')) {
const stream = {
name: `MoviesClub - Direct Stream`,
title: `${mediaType === 'movie' ? 'Movie' : `S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`} ${tmdbId} - MoviesClub Direct`,
url: decodedSrc,
quality: extractQualityFromUrl(decodedSrc) || '720p'
};
streams.push(stream);
console.log(`[moviesclub] Added direct stream from iframe`);
return streams;
}
} catch (decodeError) {
console.log(`[moviesclub] Could not decode iframe src: ${decodeError.message}`);
}
}
return streams;
}
console.log(`[moviesclub] Found ${serverMatches.length} server options`);
// Process each server
for (let i = 0; i < serverMatches.length; i++) {
const serverMatch = serverMatches[i];
const hashMatch = serverMatch.match(/data-hash="([^"]+)"/);
const nameMatch = serverMatch.match(/>([^<]+)<\/div>/);
if (!hashMatch || !nameMatch) {
console.log(`[moviesclub] Could not parse server ${i + 1}, skipping`);
continue;
}
const encodedHash = hashMatch[1];
const serverName = nameMatch[1].trim();
console.log(`[moviesclub] Processing server ${i + 1}: ${serverName}`);
try {
// Construct the rcp URL from the hash
const rcpUrl = `https://cloudnestra.com/rcp/${encodedHash}`;
console.log(`[moviesclub] RCP URL: ${rcpUrl}`);
// Fetch the rcp URL to get the JavaScript with prorcp URL
const rcpResponse = await axios.get(rcpUrl, {
timeout: 8000,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate',
'Connection': 'keep-alive',
'Referer': embedUrl
}
});
const rcpHtml = rcpResponse.data;
console.log(`[moviesclub] RCP response length: ${rcpHtml.length}`);
// Extract prorcp URL from the JavaScript
const prorcpMatch = rcpHtml.match(/src:\s*['"]([^'"]*prorcp[^'"]*)['"]/i);
if (!prorcpMatch) {
// Try alternative patterns
const altMatch = rcpHtml.match(/['"](\/prorcp\/[^'"]*)['"]/i);
if (altMatch) {
prorcpUrl = altMatch[1];
} else {
console.log(`[moviesclub] No prorcp URL found in rcp response`);
continue;
}
} else {
prorcpUrl = prorcpMatch[1];
}
console.log(`[moviesclub] Found prorcp URL: ${prorcpUrl}`);
// If it's a relative URL, make it absolute
if (prorcpUrl.startsWith('/')) {
prorcpUrl = `https://cloudnestra.com${prorcpUrl}`;
console.log(`[moviesclub] Converted to absolute URL: ${prorcpUrl}`);
}
// Fetch the prorcp URL to get the player HTML
const prorcpResponse = await axios.get(prorcpUrl, {
timeout: 8000,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate',
'Connection': 'keep-alive',
'Referer': rcpUrl
}
});
const prorcpHtml = prorcpResponse.data;
console.log(`[moviesclub] Prorcp response length: ${prorcpHtml.length}`);
// Extract stream URL from Playerjs configuration
const streamUrl = extractStreamFromPlayerjsUpdated(prorcpHtml);
if (!streamUrl) {
console.log(`[moviesclub] No stream URL found in prorcp response`);
continue;
}
console.log(`[moviesclub] Extracted stream URL: ${streamUrl.substring(0, 100)}...`);
// Create stream object
const stream = {
name: `MoviesClub - ${serverName}`,
title: `${mediaType === 'movie' ? 'Movie' : `S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`} ${tmdbId} - ${serverName}`,
url: streamUrl,
quality: extractQualityFromUrl(streamUrl) || '720p',
headers: {}
};
streams.push(stream);
console.log(`[moviesclub] Added stream from server: ${serverName}`);
} catch (serverError) {
console.error(`[moviesclub] Error processing server ${serverName}:`, serverError.message);
continue;
}
}
console.log(`[moviesclub] Total streams found: ${streams.length}`);
return streams;
} catch (error) {
console.error(`[moviesclub] Error in handleMultipleServers:`, error.message);
return [];
}
}
// Handle regular embed URLs (existing logic)
async function handleSingleServer(embedUrl, mediaType, season, episode, tmdbId) {
console.log(`[moviesclub] Handling single server from: ${embedUrl}`);
const streams = [];
try {
// Fetch the embed page
const embedResponse = await axios.get(embedUrl, {
timeout: 10000,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate',
'Connection': 'keep-alive'
}
});
const embedHtml = embedResponse.data;
console.log(`[moviesclub] Fetched embed page: ${embedHtml.length} characters`);
// Look for packed JavaScript using JsUnpacker.detect()
const scriptTags = embedHtml.match(/<script[^>]*>[\s\S]*?<\/script>/gi) || [];
let packedCode = null;
for (const scriptTag of scriptTags) {
// Extract script content
const scriptContent = scriptTag.replace(/<\/?script[^>]*>/gi, '').trim();
if (scriptContent) {
// Create unpacker instance and check if it's packed
const unpacker = new JsUnpacker(scriptContent);
if (unpacker.detect()) {
console.log(`[moviesclub] Found packed JavaScript in script tag, length: ${scriptContent.length}`);
packedCode = scriptContent;
break;
}
}
}
if (!packedCode) {
console.log('[moviesclub] No packed JavaScript found');
return [];
}
// Unpack using JsUnpacker
const unpacker = new JsUnpacker(packedCode);
const unpackedCode = unpacker.unpack();
if (!unpackedCode) {
console.log('[moviesclub] Failed to unpack JavaScript');
return [];
}
console.log(`[moviesclub] Successfully unpacked code, length: ${unpackedCode.length}`);
// Parse the player configuration from unpacked code
const playerConfig = parsePlayerConfig(unpackedCode);
if (!playerConfig) {
console.log('[moviesclub] Failed to parse player configuration');
return [];
}
console.log('[moviesclub] Parsed player config:', playerConfig);
// Extract streams from the configuration
if (playerConfig.file) {
const stream = {
name: `MoviesClub - ${playerConfig.title || 'Server 1'}`,
title: playerConfig.title || `${mediaType === 'movie' ? 'Movie' : `S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`} ${tmdbId} - MoviesClub`,
url: playerConfig.file,
quality: extractQualityFromUrl(playerConfig.file) || '720p',
headers: {}
};
// Add poster if available
if (playerConfig.poster) {
stream.poster = playerConfig.poster;
}
// Add subtitles if available
if (playerConfig.subtitle) {
stream.subtitles = parseSubtitles(playerConfig.subtitle);
}
streams.push(stream);
}
console.log(`[moviesclub] Found ${streams.length} streams`);
return streams;
} catch (error) {
console.error(`[moviesclub] Error in handleSingleServer:`, error.message);
return [];
}
}
// Extract stream URL from Playerjs configuration in HTML
function extractStreamFromPlayerjs(html) {
try {
// Look for the Playerjs constructor call
const playerMatch = html.match(/new Playerjs\(\s*(\{[^}]*\})\s*\)/);
if (!playerMatch) {
console.log('[moviesclub] No Playerjs constructor found in HTML');
return null;
}
const configStr = playerMatch[1];
console.log(`[moviesclub] Found Playerjs config: ${configStr.substring(0, 200)}...`);
// Extract the file URL from the configuration
const fileMatch = configStr.match(/file:\s*['"]([^'"]+)['"]/);
if (!fileMatch) {
console.log('[moviesclub] No file URL found in Playerjs config');
return null;
}
const streamUrl = fileMatch[1];
console.log(`[moviesclub] Extracted stream URL from Playerjs: ${streamUrl.substring(0, 100)}...`);
return streamUrl;
} catch (error) {
console.error('[moviesclub] Error extracting stream from Playerjs:', error.message);
return null;
}
}
// Extract streams from server response
function extractStreamsFromServer(serverData, serverName, mediaType, season, episode, tmdbId) {
const streams = [];
try {
// Handle different response formats
if (typeof serverData === 'string') {
// Try to parse as JSON
try {
serverData = JSON.parse(serverData);
} catch (parseError) {
console.log(`[moviesclub] Server response is not JSON, treating as direct URL: ${serverData.substring(0, 100)}`);
// Treat as direct stream URL
if (serverData.startsWith('http')) {
const stream = {
name: `MoviesClub - ${serverName}`,
title: `${mediaType === 'movie' ? 'Movie' : `S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`} ${tmdbId} - ${serverName}`,
url: serverData,
quality: extractQualityFromUrl(serverData) || '720p',
headers: {}
};
streams.push(stream);
}
return streams;
}
}
// Handle JSON response
if (serverData && typeof serverData === 'object') {
// Look for common stream URL patterns
const possibleKeys = ['file', 'url', 'src', 'stream', 'video', 'source'];
for (const key of possibleKeys) {
if (serverData[key] && typeof serverData[key] === 'string' && serverData[key].startsWith('http')) {
console.log(`[moviesclub] Found stream URL in key '${key}': ${serverData[key].substring(0, 100)}`);
const stream = {
name: `MoviesClub - ${serverName}`,
title: `${mediaType === 'movie' ? 'Movie' : `S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`} ${tmdbId} - ${serverName}`,
url: serverData[key],
quality: extractQualityFromUrl(serverData[key]) || '720p',
headers: {}
};
// 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}`,
url: selectedResult.url,
quality: actualQuality,
provider: 'moviesmod',
headers: {}
};
} else {
console.warn(`[MoviesMod] Unsupported URL type for final processing: ${targetLink?.url}`);
+104 -188
View File
@@ -1,12 +1,23 @@
const { config } = require('../utils/config');
// Lazy load showbox to avoid early side-effects; temporarily disabled until stabilized
let showbox = null;
let k4hdhub = null; // 4khdhub provider lazy ref
let moviesmod = null; // moviesmod provider lazy ref
let mp4hydra = null; // MP4Hydra provider lazy ref
let vidzee = null; // VidZee provider lazy ref
let vixsrc = null; // vixsrc provider lazy ref
let xprime = null; // xprime provider lazy ref
const fs = require('fs');
const path = require('path');
// Lazy load cache for providers
const providerCache = new Map();
// Function name mappings for existing providers
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
let lastCookieStats = { selected: null, index: null, total: 0, remainingMB: null, timestamp: null };
@@ -14,11 +25,50 @@ async function getEffectiveCookies() {
return Array.isArray(config.febboxCookies) ? config.febboxCookies : [];
}
// Adapter for Showbox provider using existing exported getStreamsFromTmdbId
async function fetchShowbox(ctx) {
if (!showbox) {
try { showbox = require('./Showbox.js'); } catch (e) { console.error('Failed to load Showbox provider:', e.message); return []; }
// Get all provider files
function getProviderFiles() {
const providersDir = path.join(__dirname);
return fs.readdirSync(providersDir)
.filter(file => file.endsWith('.js') && file !== 'registry.js' && file !== 'vidsrcextractor.js')
.map(file => ({
name: path.parse(file).name.toLowerCase(),
file: file,
functionName: providerFunctionMap[file]
}));
}
// Load a provider module
function loadProvider(providerFile) {
if (!providerCache.has(providerFile)) {
try {
providerCache.set(providerFile, require(path.join(__dirname, providerFile)));
} catch (e) {
console.error(`Failed to load provider ${providerFile}:`, e.message);
return null;
}
}
return providerCache.get(providerFile);
}
// Create fetch function for a provider
function createFetchFunction(providerInfo) {
return async function(ctx) {
const module = loadProvider(providerInfo.file);
if (!module) return [];
const funcName = providerInfo.functionName;
if (!module[funcName]) {
console.error(`Provider ${providerInfo.name} does not export ${funcName}`);
return [];
}
try {
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
const t0 = Date.now();
let result;
if (providerInfo.name === 'showbox') {
// Special case for Showbox with TMDB key and cookies
const { getTmdbApiKey } = require('../utils/tmdbKey');
const tmdbApiKey = getTmdbApiKey();
if (!tmdbApiKey) {
@@ -37,172 +87,13 @@ async function fetchShowbox(ctx) {
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`);
result = await module[funcName](mediaType, ctx.tmdbId, ctx.season || null, ctx.episode || null, null, selected);
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 || {};
}
}
const providers = [];
if (config.enableShowboxProvider) {
providers.push({ name: 'showbox', enabled: true, fetch: fetchShowbox });
} else {
console.log('[registry] showbox disabled via config');
}
// 4KHDHub adapter
async function fetch4khdhub(ctx) {
if (!k4hdhub) {
try { k4hdhub = require('./4khdhub.js'); } catch (e) { console.error('Failed to load 4khdhub provider:', e.message); return []; }
}
try {
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
const t0 = Date.now();
const result = await k4hdhub.get4KHDHubStreams(ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null);
const durationMs = Date.now() - t0;
console.log(`[registry] 4khdhub fetch duration ${durationMs}ms`);
if (!Array.isArray(result)) return [];
return result.map(s => ({ ...s, name: s.name || '4KHDHub', provider: '4khdhub' }));
} catch (e) {
console.error('[registry] 4khdhub fetch error:', e.message);
return [];
}
}
if (config.enable4khdhubProvider) {
providers.push({ name: '4khdhub', enabled: true, fetch: fetch4khdhub });
console.log('[registry] 4khdhub provider enabled');
} else {
console.log('[registry] 4khdhub disabled via config');
}
// MoviesMod adapter
async function fetchMoviesMod(ctx) {
if (!moviesmod) {
try { moviesmod = require('./moviesmod.js'); } catch (e) { console.error('Failed to load moviesmod provider:', e.message); return []; }
}
try {
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
const t0 = Date.now();
const result = await moviesmod.getMoviesModStreams(ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null);
const durationMs = Date.now() - t0;
console.log(`[registry] moviesmod fetch duration ${durationMs}ms`);
if (!Array.isArray(result)) return [];
return result.map(s => ({ ...s, name: s.name || 'MoviesMod', provider: 'moviesmod' }));
} catch (e) {
console.error('[registry] moviesmod fetch error:', e.message);
return [];
}
}
if (config.enableMoviesmodProvider) {
providers.push({ name: 'moviesmod', enabled: true, fetch: fetchMoviesMod });
console.log('[registry] moviesmod provider enabled');
} else {
console.log('[registry] moviesmod disabled via config');
}
// MP4Hydra adapter
async function fetchMp4hydra(ctx) {
if (!mp4hydra) {
try { mp4hydra = require('./MP4Hydra.js'); } catch (e) { console.error('Failed to load MP4Hydra provider:', e.message); return []; }
}
try {
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
const t0 = Date.now();
const result = await mp4hydra.getMP4HydraStreams(ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null);
const durationMs = Date.now() - t0;
console.log(`[registry] mp4hydra fetch duration ${durationMs}ms`);
if (!Array.isArray(result)) return [];
return result.map(s => ({ ...s, name: s.name || 'MP4Hydra', provider: 'mp4hydra' }));
} catch (e) {
console.error('[registry] mp4hydra fetch error:', e.message);
return [];
}
}
if (config.enableMp4hydraProvider) {
providers.push({ name: 'mp4hydra', enabled: true, fetch: fetchMp4hydra });
console.log('[registry] mp4hydra provider enabled');
} else {
console.log('[registry] mp4hydra disabled via config');
}
// VidZee adapter
async function fetchVidZee(ctx) {
if (!vidzee) {
try { vidzee = require('./VidZee.js'); } catch (e) { console.error('Failed to load VidZee provider:', e.message); return []; }
}
try {
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
const t0 = Date.now();
const result = await vidzee.getVidZeeStreams(ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null);
const durationMs = Date.now() - t0;
console.log(`[registry] vidzee fetch duration ${durationMs}ms`);
if (!Array.isArray(result)) return [];
return result.map(s => ({ ...s, name: s.name || 'VidZee', provider: 'vidzee' }));
} catch (e) {
console.error('[registry] vidzee fetch error:', e.message);
return [];
}
}
if (config.enableVidzeeProvider) {
providers.push({ name: 'vidzee', enabled: true, fetch: fetchVidZee });
console.log('[registry] vidzee provider enabled');
} else {
console.log('[registry] vidzee disabled via config');
}
// vixsrc adapter
async function fetchVixsrc(ctx) {
if (!vixsrc) {
try { vixsrc = require('./vixsrc.js'); } catch (e) { console.error('Failed to load vixsrc provider:', e.message); return []; }
}
try {
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
const t0 = Date.now();
const result = await vixsrc.getVixsrcStreams(ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null);
const durationMs = Date.now() - t0;
console.log(`[registry] vixsrc fetch duration ${durationMs}ms`);
if (!Array.isArray(result)) return [];
return result.map(s => ({ ...s, name: s.name || 'Vixsrc', provider: 'vixsrc' }));
} catch (e) {
console.error('[registry] vixsrc fetch error:', e.message);
return [];
}
}
if (config.enableVixsrcProvider) {
providers.push({ name: 'vixsrc', enabled: true, fetch: fetchVixsrc });
console.log('[registry] vixsrc provider enabled');
} else {
console.log('[registry] vixsrc disabled via config');
}
// xprime adapter (requires TMDB metadata lookup for title/year)
async function fetchXprime(ctx) {
if (!xprime) {
try { xprime = require('./xprime.js'); } catch (e) { console.error('Failed to load xprime provider:', e.message); return []; }
}
try {
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
} else if (providerInfo.name === 'xprime') {
// Special case for Xprime with TMDB metadata lookup
const { getTmdbApiKey } = require('../utils/tmdbKey');
const tmdbApiKey = getTmdbApiKey();
if (!tmdbApiKey) {
@@ -226,13 +117,10 @@ async function fetchXprime(ctx) {
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 => {
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();
@@ -243,17 +131,45 @@ async function fetchXprime(ctx) {
}
return { ...s, quality: q, name: s.name || 'Xprime', provider: 'xprime' };
});
} catch (e) {
console.error('[registry] xprime fetch error:', e.message);
return [];
}
} else {
// Standard provider call
result = await module[funcName](ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null);
}
if (config.enableXprimeProvider) {
providers.push({ name: 'xprime', enabled: true, fetch: fetchXprime });
console.log('[registry] xprime provider enabled');
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 = [];
for (const providerInfo of providerFiles) {
const enableFlag = `enable${providerInfo.name.charAt(0).toUpperCase() + providerInfo.name.slice(1)}Provider`;
const enabled = config[enableFlag] !== false;
if (enabled) {
providers.push({
name: providerInfo.name,
enabled: true,
fetch: createFetchFunction(providerInfo)
});
console.log(`[registry] ${providerInfo.name} provider enabled`);
} else {
console.log('[registry] xprime disabled via config');
console.log(`[registry] ${providerInfo.name} disabled via config`);
}
}
function listProviders() { return providers.map(p => ({ name: p.name, enabled: p.enabled })); }
+2 -5
View File
@@ -1380,11 +1380,8 @@ async function getUHDMoviesStreams(tmdbId, mediaType = 'movie', season = null, e
title: `${cleanFileName}\n${sizeInfo}`,
url: finalUrl,
quality: linkInfo.quality,
size: sizeInfo,
fileName: fileName,
fullTitle: rawQuality,
codecs: codecs,
behaviorHints: { bingeGroup: `uhdmovies-${linkInfo.quality}` }
provider: 'uhdmovies',
headers: {}
};
}
}
+1 -1
View File
@@ -195,7 +195,7 @@ function getVixsrcStreams(tmdbId, mediaType = 'movie', seasonNum = null, episode
title: "Auto Quality Stream",
url: masterPlaylistUrl,
quality: '1080p',
type: 'direct',
provider: 'Vidsrc',
headers: {
'Referer': BASE_URL,
'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]) => {
if (!url || typeof url !== 'string') return;
streams.push({
name: title,
title: `${title} - ${type === 'tv' ? `S${String(seasonNum).padStart(2,'0')}E${String(episodeNum).padStart(2,'0')} ` : ''}${quality}`,
url,
quality: quality || 'Unknown',
title: `${title} - ${type === 'tv' ? `S${String(seasonNum).padStart(2,'0')}E${String(episodeNum).padStart(2,'0')} ` : ''}${quality}`,
provider: 'Xprime.tv',
codecs: [],
size: 'Unknown size'
headers: {},
});
});
};
+19 -9
View File
@@ -596,15 +596,25 @@ async function renderProviderMatrix(merged){
if(!merged){ try { const data = await fetchConfig(); merged = data.merged||{}; } catch { return; } }
const container = document.getElementById('providerMatrix'); if(!container) return;
const defaults = (merged.defaultProviders||[]).map(s=>s.toLowerCase());
const providers = [
['showbox','enableShowboxProvider'],
['4khdhub','enable4khdhubProvider'],
['moviesmod','enableMoviesmodProvider'],
['mp4hydra','enableMp4hydraProvider'],
['vidzee','enableVidzeeProvider'],
['vixsrc','enableVixsrcProvider'],
['xprime','enableXprimeProvider']
];
// Fetch available providers from API
let availableProviders = [];
try {
const providersResponse = await fetch('/api/providers');
if (providersResponse.ok) {
const providersData = await providersResponse.json();
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])=>{
const enabled = merged[flag] !== false;
const isDefault = defaults.includes(name);
+1 -1
View File
@@ -1 +1 @@
1758105049325
1758225404146
+44 -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) {
const cfg = { ...base };
// Marker: if user explicitly provided an empty array for tmdbApiKeys in override file we should not resurrect legacy key
@@ -80,20 +93,21 @@ function normalizeConfig(base) {
cfg.tmdbApiKeys = Array.from(new Set(cfg.tmdbApiKeys.map(k=>String(k).trim()).filter(Boolean)));
// Do not expose legacy single-key field in merged config output
delete cfg.tmdbApiKey;
// Booleans normalization
const boolKeys = [
'enableShowboxProvider','enableXprimeProvider','enable4khdhubProvider','enableMoviesmodProvider','enableMp4hydraProvider','enableVidzeeProvider','enableVixsrcProvider',
'disableCache','enablePStreamApi','disableUrlValidation','disable4khdhubUrlValidation'
];
// Dynamic provider enable flags
const providerNames = getProviderNames();
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; });
// Default values if undefined
if (cfg.enableShowboxProvider === undefined) cfg.enableShowboxProvider = true;
if (cfg.enableXprimeProvider === undefined) cfg.enableXprimeProvider = true;
if (cfg.enable4khdhubProvider === undefined) cfg.enable4khdhubProvider = true;
if (cfg.enableMoviesmodProvider === undefined) cfg.enableMoviesmodProvider = true;
if (cfg.enableMp4hydraProvider === undefined) cfg.enableMp4hydraProvider = true;
if (cfg.enableVidzeeProvider === undefined) cfg.enableVidzeeProvider = true;
if (cfg.enableVixsrcProvider === undefined) cfg.enableVixsrcProvider = true;
// Set defaults for provider enable flags
providerNames.forEach(name => {
const flag = `enable${name.charAt(0).toUpperCase() + name.slice(1)}Provider`;
if (cfg[flag] === undefined) cfg[flag] = true; // Default to enabled
});
// Default values for other flags
if (cfg.disableCache === undefined) cfg.disableCache = false;
if (cfg.enablePStreamApi === undefined) cfg.enablePStreamApi = true;
// Proxy features removed; always use direct connections
@@ -114,14 +128,15 @@ function applyConfigToEnv(cfg){
if (cfg.febboxCookies && cfg.febboxCookies.length) process.env.FEBBOX_COOKIES = cfg.febboxCookies.join(',');
else delete process.env.FEBBOX_COOKIES;
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';
process.env.ENABLE_XPRIME_PROVIDER = cfg.enableXprimeProvider ? 'true':'false';
process.env.ENABLE_4KHDHUB_PROVIDER = cfg.enable4khdhubProvider ? 'true':'false';
process.env.ENABLE_MOVIESMOD_PROVIDER = cfg.enableMoviesmodProvider ? 'true':'false';
process.env.ENABLE_MP4HYDRA_PROVIDER = cfg.enableMp4hydraProvider ? 'true':'false';
process.env.ENABLE_VIDZEE_PROVIDER = cfg.enableVidzeeProvider ? 'true':'false';
process.env.ENABLE_VIXSRC_PROVIDER = cfg.enableVixsrcProvider ? 'true':'false';
// Dynamic provider enable flags
const providerNames = getProviderNames();
providerNames.forEach(name => {
const flag = `enable${name.charAt(0).toUpperCase() + name.slice(1)}Provider`;
const envName = `ENABLE_${name.toUpperCase()}_PROVIDER`;
process.env[envName] = cfg[flag] ? 'true' : 'false';
});
// Caching / validation / PStream
process.env.DISABLE_CACHE = cfg.disableCache ? 'true':'false';
process.env.ENABLE_PSTREAM_API = cfg.enablePStreamApi ? 'true':'false';
@@ -152,13 +167,6 @@ function loadConfig() {
tmdbApiKeys: parseJsonMaybe(process.env.TMDB_API_KEYS) || null,
febboxCookies: parseCookies(process.env.FEBBOX_COOKIES),
// 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,
enablePStreamApi: process.env.ENABLE_PSTREAM_API,
showboxCacheDir: process.env.SHOWBOX_CACHE_DIR || null,
@@ -166,6 +174,14 @@ function loadConfig() {
disableUrlValidation: process.env.DISABLE_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 merged = { ...envCfg, ...override };
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;
+4 -11
View File
@@ -1,15 +1,9 @@
{
"defaultProviders": [],
"configVersion": 1,
"tmdbApiKeys": [
"qwfghfgh",
"fghsdfs",
"uygghmng"
],
"tmdbApiKeys": [],
"enableShowboxProvider": true,
"febboxCookies": [
"gfdgfdgf"
],
"febboxCookies": [],
"enableXprimeProvider": true,
"enable4khdhubProvider": true,
"enableMoviesmodProvider": true,
@@ -20,7 +14,6 @@
"enablePStreamApi": true,
"disableUrlValidation": false,
"disable4khdhubUrlValidation": false,
"excludeCodecsRaw": "{\"excludeDV\":true,\"excludeHDR\":true}",
"minQualitiesRaw": "1080p",
"port": 3000
"excludeCodecsRaw": "{\"excludeDV\":false,\"excludeHDR\":false}",
"minQualitiesRaw": "all"
}