3 Commits
29 changed files with 1897 additions and 4636 deletions
+65
View File
@@ -2,6 +2,70 @@
All notable changes to this project will be documented in this file.
## [1.1.0] - 2026-05-16
## Dependency Security
- **npm vulnerabilities fixed**: Upgraded `cheerio` (1.0.0 → 1.2.0) and `axios-cookiejar-support` (6.0.2 → 7.0.0) to eliminate 14 vulnerabilities (including `undici <=6.23.0` high/critical CVEs). Result: 0 vulnerabilities.
### Added
- **Videasy provider**: 10-server parallel scraper via enc-dec.app decrypt relay.
- **Vidlink provider**: Single-stream HLS source via enc-dec.app encode relay.
- **LordFlix provider**: 9-server scraper via enc-dec.app (replaces defunct Vidsync).
- **NoTorrent provider**: Stremio addon API bridge (`addon-osvh.onrender.com`).
- **DahmerMovies provider**: Open-directory direct file link scraper with proxy rewrite.
- **GitHub Sponsors button**: Official iframe embed in admin sidebar and README badge.
### Removed
- **Vidsync provider**: All 6 servers returned HTTP 401 — API locked. Replaced by LordFlix.
- **MP4Hydra provider**: Dead upstream, removed entirely.
- **UHDMovies provider**: Dead upstream, removed entirely.
- **vidsrcextractor.js**: Unused legacy file removed.
### Fixed
- **Provider enable/disable not working at runtime**: Removed the startup-time enabled/disabled filter. All providers are now always loaded into the registry array. `listProviders()` and `getProvider()` now call `isProviderEnabled(name)` which reads the live `config` object on every call. Changes take effect immediately without a server restart.
- **Showbox/FebBox provider**: Restored with correct `responseType:'text'`, AJAX headers, and double `ui=` cookie bug fix.
- **MoviesMod provider**: Updated class-based selectors and added support for `links.modpro.blog`, `cloud.unblockedgames.world`, `tech.examdegree.site`, and `driveleech.net` domains.
- **VixSrc provider**: Corrected to proper 2-step API flow.
---
## [1.0.9] - 2025-11-19
### Removed
- **Showbox/FebBox/PStream provider**: Permanently removed due to PStream API being protected by Cloudflare bot detection, making it inaccessible without complex proxy infrastructure
- Deleted `providers/Showbox.js` and backup files
- Removed FebBox cookie management from configuration panel
- Removed `FEBBOX_COOKIES`, `SHOWBOX_CACHE_DIR`, and related environment variables
- Cleaned up all Showbox-specific logic from provider registry
- Removed FebBox/PStream configuration UI panel and related functions
- **Unused dependencies**: Removed 121 packages including puppeteer, patchright, puppeteer-extra, puppeteer-extra-plugin-stealth, and vm2 (~200MB+ saved)
- **Unused utility files**: Removed `cloudflareBypasser.js` and `jsunpack.js`
### Fixed
- **VidZee provider**: Updated to support two-stage AES decryption with dynamic API keys. The provider now fetches an encrypted key from `https://core.vidzee.wtf/api-key`, decrypts it using AES-256-GCM with a hardcoded key, then uses the result to decrypt video URLs with AES-256-CBC. Includes 1-hour API key caching for performance.
- **4khdhub provider**: Added support for `links.modpro.blog` domain (site migrated from `modrefer.in`).
- **moviesmod provider**: Added support for `links.modpro.blog` domain alongside existing `modrefer.in`.
- **uhdmovies provider**: Fixed domain to use `uhdmovies.rip` instead of outdated `uhdmovies.mov`. Added automatic domain replacement for stale scraped URLs. Created new `utils/linkResolver.js` utility to handle driveseed/driveleech download button extraction (supports Instant Download, Resume Cloud, Resume Worker Bot, Direct Links CF Type 1).
- **Provider registry**: Fixed `listProviders()` to return all available providers with their enabled status, not just enabled ones. Config panel now shows all 6 providers correctly.
## [1.0.8] - 2025-10-03
### Added
- Added `processStreamsForProxy` so the aggregate and provider-specific endpoints automatically rewrite stream URLs through the internal `/m3u8-proxy`/`/ts-proxy` layer whenever `enableProxy` is turned on, stripping provider headers safely.
### Changed
- Showbox provider overhaul: filesystem-only caching (Redis removed), smarter FebBox cookie selection with region fallbacks, TMDB title/image validation that recognises romanized names, plus cached HEAD size lookups for faster listings.
- Vixsrc provider rewritten to parse the `window.masterPlaylist` payload (token + expiry), returning a single master playlist with English subtitle lookup and correct Referer headers.
- Config loader now prefers `utils/user-config.json`, dedupes TMDB keys, defaults provider enable flags to `true`, and scrubs legacy proxy env values when mirroring overrides back to `process.env`.
- 4KHDHub provider permanently drops `.zip` archive links instead of trimming extensions and improves host distribution logging.
- Provider registry dynamically enumerates available modules (removing MoviesClub/Xprime remnants) while keeping per-request cookie stats for the admin debug panel.
### Removed
- Deleted legacy `providers/moviesclub.js`, `providers/xprime.js`, and other unused proxy/auth remnants that were no longer referenced.
### Fixed
- Aggregated responses now obey the proxy flag without client changes—streams returned from `/api/streams/...` are proxy-wrapped and omit upstream header hints when `enableProxy` is active.
## [1.0.7] - 2025-09-21
### Added
@@ -132,6 +196,7 @@ All notable changes to this project will be documented in this file.
## [1.0.0] - 2025-09-16
- Initial stable release.
[1.0.8]: https://github.com/Inside4ndroid/TMDB-Embed-API/compare/v1.0.7...v1.0.8
[1.0.7]: https://github.com/Inside4ndroid/TMDB-Embed-API/compare/v1.0.6...v1.0.7
[1.0.6]: https://github.com/Inside4ndroid/TMDB-Embed-API/compare/v1.0.5...v1.0.6
[1.0.5]: https://github.com/Inside4ndroid/TMDB-Embed-API/compare/v1.0.4...v1.0.5
+24 -20
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.7-informational?style=flat" />
<img src="https://img.shields.io/badge/Version-1.1.0-informational?style=flat" />
<img src="https://img.shields.io/docker/pulls/inside4ndroid/tmdb-embed-api?label=Docker%20Pulls&style=flat" />
</p>
@@ -34,7 +34,7 @@
## ✨ 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, UHDMovies) with per‑provider enable toggles + default selection.
- **Provider Aggregation** – Pluggable providers (Showbox/FebBox, 4KHDHub, MoviesMod, VidZee, VixSrc, Videasy, Vidlink, LordFlix, NoTorrent, DahmerMovies) 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`.
@@ -130,7 +130,6 @@ docker compose up -d --build
Environment variables can be supplied via a `.env` file in the same directory (Compose automatically loads it). Example `.env`:
```
TMDB_API_KEY=first_key
FEBBOX_COOKIES=cookie1,cookie2
```
To stop & remove:
@@ -153,7 +152,6 @@ or add / remove keys inside the Admin UI (Keys panel) and save.
| `API_PORT` | Port the server listens on | Defaults to 8787 |
| `TMDB_API_KEY` | Single TMDB key (legacy) | Use if you only have one key |
| `TMDB_API_KEYS` | JSON array of keys | Overrides single key when present |
| `FEBBOX_COOKIES` | Comma separated FebBox cookies | Enables Showbox provider immediately |
| `PASSWORD_HASH` | Pre-seed admin password hash | Optional (UI can set) |
| `ADMIN_USERNAME` | Override default username | Default: `admin` |
@@ -201,9 +199,9 @@ Saving in the UI writes only changed keys. Setting a field to empty removes the
**Override File:** `utils/user-config.json`
```json
{
"defaultProviders": ["showbox","4khdhub"],
"defaultProviders": ["4khdhub","moviesmod"],
"tmdbApiKeys": ["KEY_A","KEY_B"],
"enableShowboxProvider": true
"enable4khdhubProvider": true
}
```
@@ -215,7 +213,6 @@ Saving in the UI writes only changed keys. Setting a field to empty removes the
| Core | Port, default providers, default region |
| Quality / Filters | Min quality presets & codec exclusion JSON |
| Keys | Add/remove TMDB API keys (rotated randomly) |
| FebBox / PStream | Manage FebBox cookies powering Showbox provider |
| Advanced | Provider toggles, cache & validation flags |
| Server Status | Live metrics, provider functional checks |
| Live Config | View merged + override JSON snapshots |
@@ -228,13 +225,16 @@ Session is revalidated on visibility and back/forward navigation to prevent stal
The API supports a plugin system. Drop a new provider file in the `providers/` folder and register its exported function in `providers/registry.js` under `providerFunctionMap`.
### Current Built-in Providers
- `showbox` - Showbox/PStream integration
- `showbox` - Showbox/FebBox streams (requires FebBox JWT cookie)
- `4khdhub` - 4KHDHub streams
- `moviesmod` - MoviesMod streams
- `mp4hydra` - MP4Hydra streams
- `moviesmod` - MoviesMod streams
- `vidzee` - VidZee streams
- `vixsrc` - Vixsrc streams
- `uhdmovies` - UHD Movies streams
- `vixsrc` - VixSrc streams
- `videasy` - Videasy streams (10 servers via enc-dec.app)
- `vidlink` - Vidlink streams (via enc-dec.app)
- `lordflix` - LordFlix streams (9 servers via enc-dec.app)
- `notorrent` - NoTorrent streams (Stremio addon API)
- `dahmermovies` - DahmerMovies streams (direct file links)
### Adding a New Provider
@@ -247,10 +247,13 @@ The API supports a plugin system. Drop a new provider file in the `providers/` f
'Showbox.js': 'getStreamsFromTmdbId',
'4khdhub.js': 'get4KHDHubStreams',
'moviesmod.js': 'getMoviesModStreams',
'MP4Hydra.js': 'getMP4HydraStreams',
'VidZee.js': 'getVidZeeStreams',
'vixsrc.js': 'getVixsrcStreams',
'uhdmovies.js': 'getUHDMoviesStreams',
'videasy.js': 'getVideasyStreams',
'vidlink.js': 'getVidlinkStreams',
'lordflix.js': 'getLordflixStreams',
'notorrent.js': 'getNotorrentStreams',
'dahmermovies.js': 'getDahmermoviesStreams',
'yourprovider.js': 'getYourproviderStreams'
};
```
@@ -320,9 +323,7 @@ Filtering passes through `applyFilters` to enforce min quality + codec exclusion
## ⚙️ 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 |
| disableCache | false | Disables internal caches |\n| 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 |
@@ -334,7 +335,7 @@ Toggle `enableProxy` to activate the internal proxy. This adds lightweight playl
- Presets: `all`, `480p`, `720p`, `1080p`, `1440p`, `2160p`.
- Custom quality JSON example:
```json
{ "default": "900p", "showbox": "1080p" }
{ "default": "900p", "4khdhub": "1080p" }
```
- Codec exclusion JSON example:
```json
@@ -367,7 +368,6 @@ For ephemeral platforms (e.g., Vercel) note that some providers use temporary di
## 💡 Troubleshooting
| Symptom | Cause / Fix |
|---------|------------|
| No streams from Showbox | Missing FebBox cookies |
| TMDB quota issues | Add more keys under Keys panel |
| Provider missing in matrix | Ensure its enable flag exists & UI updated |
| Empty merged config after restart | `user-config.json` deleted or unreadable |
@@ -386,7 +386,11 @@ PRs welcome. Keep changes focused and avoid unrelated formatting churn. For new
## ❤️ Sponsorship
If this project helps you, consider sponsoring to support continued development & maintenance:
➡️ **GitHub Sponsors:** https://github.com/sponsors/Inside4ndroid
<p align="center">
<a href="https://github.com/sponsors/Inside4ndroid">
<img src="https://img.shields.io/badge/Sponsor-GitHub%20Sponsors-ea4aaa?style=for-the-badge&logo=github-sponsors&logoColor=white" alt="Sponsor on GitHub" />
</a>
</p>
Every contribution accelerates feature delivery & sustainability.
+21 -1
View File
@@ -3,14 +3,23 @@ const express = require('express');
const cors = require('cors');
const os = require('os');
const { config, saveConfigPatch, OVERRIDE_PATH } = require('./utils/config');
const { authenticate, issueSession, requireAuth, getSession, updatePassword, AUTH_FILE } = require('./utils/auth');
const { authenticate, issueSession, requireAuth, getSession, updatePassword } = require('./utils/auth');
const path = require('path');
const { listProviders, getProvider, getCookieStats } = require('./providers/registry');
const { createProxyRoutes, processStreamsForProxy } = require('./proxy/proxyServer');
const { resolveImdbId } = require('./utils/tmdb');
const { applyFilters } = require('./utils/streamFilters');
const app = express();
// Conditionally mount proxy routes early so downstream handlers can use them
if (config.enableProxy) {
console.log('[startup] enableProxy flag active: mounting proxy routes');
createProxyRoutes(app);
} else {
console.log('[startup] enableProxy flag disabled: proxy routes not mounted');
}
// --- Simple In-Memory Rate Limiting for /auth/login ---
const loginAttempts = new Map(); // key: ip, value: { count, first, last, lockedUntil }
const MAX_ATTEMPTS_WINDOW = 5; // attempts allowed
@@ -340,6 +349,12 @@ app.get('/api/streams/:type/:tmdbId', async (req,res) => {
let streams = results.flat();
streams = applyFilters(streams, 'aggregate', config.minQualities, config.excludeCodecs);
metrics.streamsReturned += streams.length;
if (config.enableProxy) {
const serverUrl = `${req.protocol}://${req.get('host')}`;
streams = processStreamsForProxy(streams, serverUrl);
// Omit original headers when proxying to avoid leaking upstream requirements
streams = streams.map(s => { if (s && typeof s === 'object') { const { headers, ...rest } = s; return rest; } return s; });
}
res.json({ success:true, tmdbId, imdbId, count: streams.length, providerTimings, streams });
} catch (e) {
metrics.lastError = e.message;
@@ -366,6 +381,11 @@ app.get('/api/streams/:provider/:type/:tmdbId', async (req,res) => {
const providerTimings = { [prov.name]: Date.now()-t0 };
streams = applyFilters(streams, prov.name, config.minQualities, config.excludeCodecs);
metrics.streamsReturned += streams.length;
if (config.enableProxy) {
const serverUrl = `${req.protocol}://${req.get('host')}`;
streams = processStreamsForProxy(streams, serverUrl);
streams = streams.map(s => { if (s && typeof s === 'object') { const { headers, ...rest } = s; return rest; } return s; });
}
res.json({ success:true, provider: prov.name, tmdbId, imdbId, count: streams.length, providerTimings, streams });
} catch (e) {
metrics.lastError = e.message;
+320 -1070
View File
File diff suppressed because it is too large Load Diff
+7 -9
View File
@@ -1,40 +1,38 @@
{
"name": "tmdb-embed-api",
"version": "1.0.7",
"version": "1.1.0",
"description": "Standalone streaming embed source aggregation API (TMDB ID in, streams out)",
"main": "apiServer.js",
"scripts": {
"start": "nodemon --watch restart.trigger apiServer.js",
"start:dev": "nodemon --watch restart.trigger --ignore providers/.cache --ignore **/tmdb-embed-showbox-cache apiServer.js",
"start:dev": "nodemon --watch restart.trigger --ignore providers/.cache --ignore **/cache apiServer.js",
"lint": "eslint . --ext .js"
},
"dependencies": {
"aromanize": "^0.1.5",
"axios": "^1.9.0",
"axios-cookiejar-support": "^6.0.2",
"axios-cookiejar-support": "^7.0.0",
"body-parser": "^2.2.0",
"cheerio": "^1.0.0",
"cheerio": "^1.2.0",
"cors": "^2.8.5",
"crypto-js": "^4.2.0",
"dotenv": "^16.5.0",
"express": "^4.21.2",
"form-data": "^4.0.3",
"hls-parser": "^0.13.5",
"hls-parser": "^0.13.5",
"jsdom": "^26.1.0",
"node-fetch": "^2.7.0",
"p-limit": "^6.2.0",
"puppeteer": "^24.11.0",
"string-similarity": "^4.0.4",
"tough-cookie": "^5.1.2",
"ttl-cache": "^1.0.2",
"vm2": "^3.9.19",
"yargs": "^17.7.2"
},
"engines": {
"node": ">=14.0.0"
},
"devDependencies": {
"nodemon": "^3.1.10",
"eslint": "^8.57.0"
"eslint": "^8.57.0",
"nodemon": "^3.1.10"
}
}
+24 -22
View File
@@ -770,7 +770,9 @@ function extractHubCloudLinks(url, referer) {
headers = { Referer: baseUrl + '/', Origin: baseUrl };
console.log('[4KHDHub] Added Referer/Origin headers for r2.dev FSL link');
}
} catch {}
} catch {
// Ignore URL parsing errors
}
resolve({
name: `4KHDHub - FSL Server${qualityLabel}`,
@@ -794,7 +796,9 @@ function extractHubCloudLinks(url, referer) {
headers = { Referer: baseUrl + '/', Origin: baseUrl };
console.log('[4KHDHub] Added Referer/Origin headers for r2.dev FSL link (fallback)');
}
} catch {}
} catch {
// Ignore URL parsing errors
}
resolve({
name: `4KHDHub - FSL Server${qualityLabel}`,
@@ -1332,24 +1336,18 @@ function extractStreamingLinks(downloadLinks) {
return Promise.all(promises)
.then(results => {
const validResults = results.filter(result => result !== null);
const flatResults = validResults.flat();
// Keep .zip links but strip the trailing .zip extension (attempt direct file URL)
const transformedResults = flatResults
.filter(link => link && link.url)
.map(link => {
try {
const lower = link.url.toLowerCase();
if (lower.endsWith('.zip')) {
const newUrl = link.url.slice(0, -4); // remove .zip
console.log(`[4KHDHub] Converted zip URL to non-zip: ${link.url} -> ${newUrl}`);
return { ...link, url: newUrl };
}
} catch {}
return link;
});
// Note: Link count will be logged after validation completes
return transformedResults;
const validResults = results.filter(r => r !== null).flat();
const filtered = validResults.filter(link => {
if (!link || !link.url) return false;
const lower = link.url.toLowerCase();
if (lower.endsWith('.zip')) {
console.log(`[4KHDHub] Skipping archive (zip) link: ${link.url}`);
return false;
}
return true;
});
console.log(`[4KHDHub] Post-processing: kept ${filtered.length}, skipped ${validResults.length - filtered.length} zip archive links`);
return filtered;
});
}
@@ -1841,11 +1839,15 @@ async function get4KHDHubStreams(tmdbId, type, season = null, episode = null) {
try {
const h = new URL(s.url).hostname;
acc[h] = (acc[h] || 0) + 1;
} catch {}
} catch {
// Ignore URL parsing errors
}
return acc;
}, {});
console.log('[4KHDHub] Final host distribution:', hostCounts);
} catch {}
} catch {
// Ignore URL parsing errors
}
console.log(`[4KHDHub] Returning ${streams.length} streams`);
return streams;
-488
View File
@@ -1,488 +0,0 @@
const axios = require('axios');
const FormData = require('form-data');
// Helper function to generate slug from title
function generateSlug(title) {
return title.toLowerCase()
.replace(/[^\w\s-]/g, '') // Remove special characters
.replace(/\s+/g, '-') // Replace spaces with hyphens
.replace(/-+/g, '-'); // Replace multiple hyphens with a single hyphen
}
// Process episode data from MP4Hydra response
function processEpisode(episode, baseServer, serverName, serverNumber) {
const videoUrl = `${baseServer}${episode.src}`;
const subtitles = episode.subs ? episode.subs.map(sub => ({
label: sub.label,
url: `${baseServer}${sub.src}`
})) : [];
return {
title: episode.show_title || episode.title,
episode: episode.title,
type: episode.type,
quality: episode.quality || episode.label,
videoUrl: videoUrl,
server: serverName,
serverNumber: serverNumber,
subtitles: subtitles
};
}
// Main function to get streams from MP4Hydra
async function getMP4HydraStreams(tmdbId, mediaType, seasonNum = null, episodeNum = null) {
try {
console.log(`[MP4Hydra] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}, Season: ${seasonNum}, Episode: ${episodeNum}`);
// Get TMDB details
const details = await getTMDBDetails(tmdbId, mediaType);
if (!details) {
console.log(`[MP4Hydra] Could not fetch details for TMDB ID: ${tmdbId}`);
return [];
}
console.log(`[MP4Hydra] Found title: ${details.title} (${details.year})`);
// Generate slug in the format "movie-name-year"
let slug = details.slug;
if (mediaType === 'movie' && details.year) {
slug = `${details.slug}-${details.year}`;
}
console.log(`[MP4Hydra] Using slug: ${slug}`);
// Create form data for multipart/form-data request
const formData = new FormData();
formData.append('v', '8');
formData.append('z', JSON.stringify([{
s: slug,
t: mediaType,
se: seasonNum,
ep: episodeNum
}]));
// Make request to MP4Hydra API
const response = await axios({
method: 'post',
url: 'https://mp4hydra.org/info2?v=8',
data: formData,
headers: {
...formData.getHeaders(),
'User-Agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Mobile Safari/537.36',
'Accept': '*/*',
'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8',
'Origin': 'https://mp4hydra.org',
'Referer': `https://mp4hydra.org/${mediaType}/${slug}`,
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin'
},
timeout: 10000
});
// Process response
if (response.data && response.data.playlist && response.data.playlist.length > 0) {
const playlist = response.data.playlist;
const servers = response.data.servers;
console.log(`[MP4Hydra] Found ${playlist.length} videos`);
console.log(`[MP4Hydra] Available servers: ${Object.keys(servers).join(', ')}`);
// For TV shows, find the specific episode
if (mediaType === 'tv' && seasonNum && episodeNum) {
const paddedSeason = seasonNum.toString().padStart(2, '0');
const paddedEpisode = episodeNum.toString().padStart(2, '0');
const seasonEpisode = `S${paddedSeason}E${paddedEpisode}`;
const targetEpisode = playlist.find(item =>
item.title && item.title.toUpperCase() === seasonEpisode.toUpperCase()
);
if (!targetEpisode) {
console.log(`[MP4Hydra] Could not find ${seasonEpisode}`);
return [];
}
console.log(`[MP4Hydra] Found episode: ${targetEpisode.show_title || targetEpisode.title}`);
// Process streams from main servers
const streams = [];
const serverConfig = [
{ name: 'Beta', number: '#1' },
{ name: 'Beta#3', number: '#2' }
];
serverConfig.forEach(server => {
const serverName = server.name;
const serverNumber = server.number;
if (servers[serverName]) {
const baseServer = servers[serverName];
console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`);
const processedEpisode = processEpisode(targetEpisode, baseServer, serverName, serverNumber);
// Convert to standard stream format
streams.push({
title: `${details.title} - ${seasonEpisode} - ${processedEpisode.quality} [MP4Hydra ${serverNumber}]`,
url: processedEpisode.videoUrl,
quality: processedEpisode.quality,
provider: "mp4hydra",
headers: {
'Referer': 'https://mp4hydra.org/'
}
});
// Add subtitle tracks if available
if (processedEpisode.subtitles && processedEpisode.subtitles.length > 0) {
streams[streams.length - 1].subtitles = processedEpisode.subtitles.map(sub => ({
url: sub.url,
lang: sub.label
}));
}
}
});
return streams;
}
// For movies, process all videos
const streams = [];
const serverConfig = [
{ name: 'Beta', number: '#1' },
{ name: 'Beta#3', number: '#2' }
];
serverConfig.forEach(server => {
const serverName = server.name;
const serverNumber = server.number;
if (servers[serverName]) {
const baseServer = servers[serverName];
console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`);
playlist.forEach(item => {
const processedItem = processEpisode(item, baseServer, serverName, serverNumber);
// Convert to standard stream format
streams.push({
title: `${details.title} - ${processedItem.quality} [MP4Hydra ${serverNumber}]`,
url: processedItem.videoUrl,
quality: processedItem.quality,
provider: "mp4hydra",
headers: {
'Referer': 'https://mp4hydra.org/'
}
});
// Add subtitle tracks if available
if (processedItem.subtitles && processedItem.subtitles.length > 0) {
streams[streams.length - 1].subtitles = processedItem.subtitles.map(sub => ({
url: sub.url,
lang: sub.label
}));
}
});
}
});
return streams;
} else {
// Try with original title if the first attempt failed
if (details.title !== details.original_title) {
return await tryAlternativeTitle(details, mediaType, seasonNum, episodeNum);
}
// Try without year for movies
if (mediaType === 'movie' && details.year) {
return await tryWithoutYear(details, mediaType, seasonNum, episodeNum);
}
console.log('[MP4Hydra] No streaming links found');
return [];
}
} catch (error) {
console.error(`[MP4Hydra] Error fetching streams:`, error.message);
return [];
}
}
// Helper function to try with original title
async function tryAlternativeTitle(details, mediaType, seasonNum, episodeNum) {
try {
console.log(`[MP4Hydra] Retrying with original title: ${details.original_title}`);
const originalSlug = generateSlug(details.original_title);
let originalFullSlug = originalSlug;
if (mediaType === 'movie' && details.year) {
originalFullSlug = `${originalSlug}-${details.year}`;
}
// Create new form data with original title
const formData = new FormData();
formData.append('v', '8');
formData.append('z', JSON.stringify([{
s: originalFullSlug,
t: mediaType,
se: seasonNum,
ep: episodeNum
}]));
const response = await axios({
method: 'post',
url: 'https://mp4hydra.org/info2?v=8',
data: formData,
headers: {
...formData.getHeaders(),
'User-Agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Mobile Safari/537.36',
'Accept': '*/*',
'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8',
'Origin': 'https://mp4hydra.org',
'Referer': `https://mp4hydra.org/${mediaType}/${originalFullSlug}`,
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin'
},
timeout: 10000
});
// Process response
if (response.data && response.data.playlist && response.data.playlist.length > 0) {
const playlist = response.data.playlist;
const servers = response.data.servers;
console.log(`[MP4Hydra] Found ${playlist.length} videos with original title`);
// For TV shows, find the specific episode
if (mediaType === 'tv' && seasonNum && episodeNum) {
const paddedSeason = seasonNum.toString().padStart(2, '0');
const paddedEpisode = episodeNum.toString().padStart(2, '0');
const seasonEpisode = `S${paddedSeason}E${paddedEpisode}`;
const targetEpisode = playlist.find(item =>
item.title && item.title.toUpperCase() === seasonEpisode.toUpperCase()
);
if (!targetEpisode) {
console.log(`[MP4Hydra] Could not find ${seasonEpisode} with original title`);
return [];
}
// Process streams from main servers
const streams = [];
const serverConfig = [
{ name: 'Beta', number: '#1' },
{ name: 'Beta#3', number: '#2' }
];
serverConfig.forEach(server => {
const serverName = server.name;
const serverNumber = server.number;
if (servers[serverName]) {
const baseServer = servers[serverName];
console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`);
const processedEpisode = processEpisode(targetEpisode, baseServer, serverName, serverNumber);
// Convert to standard stream format
streams.push({
title: `${details.original_title} - ${seasonEpisode} - ${processedEpisode.quality} [MP4Hydra ${serverNumber}]`,
url: processedEpisode.videoUrl,
quality: processedEpisode.quality,
provider: "mp4hydra",
headers: {
'Referer': 'https://mp4hydra.org/'
}
});
// Add subtitle tracks if available
if (processedEpisode.subtitles && processedEpisode.subtitles.length > 0) {
streams[streams.length - 1].subtitles = processedEpisode.subtitles.map(sub => ({
url: sub.url,
lang: sub.label
}));
}
}
});
return streams;
}
// For movies, process all videos
const streams = [];
const serverConfig = [
{ name: 'Beta', number: '#1' },
{ name: 'Beta#3', number: '#2' }
];
serverConfig.forEach(server => {
const serverName = server.name;
const serverNumber = server.number;
if (servers[serverName]) {
const baseServer = servers[serverName];
console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`);
playlist.forEach(item => {
const processedItem = processEpisode(item, baseServer, serverName, serverNumber);
// 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
});
// Add subtitle tracks if available
if (processedItem.subtitles && processedItem.subtitles.length > 0) {
streams[streams.length - 1].subtitles = processedItem.subtitles.map(sub => ({
url: sub.url,
lang: sub.label
}));
}
});
}
});
return streams;
}
return [];
} catch (error) {
console.error(`[MP4Hydra] Error with alternative title:`, error.message);
return [];
}
}
// Helper function to try without year
async function tryWithoutYear(details, mediaType, seasonNum, episodeNum) {
try {
console.log(`[MP4Hydra] Retrying with title only (without year): ${details.title}`);
const titleOnlySlug = details.slug;
// Create new form data with title only
const formData = new FormData();
formData.append('v', '8');
formData.append('z', JSON.stringify([{
s: titleOnlySlug,
t: mediaType,
se: seasonNum,
ep: episodeNum
}]));
const response = await axios({
method: 'post',
url: 'https://mp4hydra.org/info2?v=8',
data: formData,
headers: {
...formData.getHeaders(),
'User-Agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Mobile Safari/537.36',
'Accept': '*/*',
'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8',
'Origin': 'https://mp4hydra.org',
'Referer': `https://mp4hydra.org/${mediaType}/${titleOnlySlug}`,
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin'
},
timeout: 10000
});
// Process response
if (response.data && response.data.playlist && response.data.playlist.length > 0) {
const playlist = response.data.playlist;
const servers = response.data.servers;
console.log(`[MP4Hydra] Found ${playlist.length} videos with title only`);
// For movies, process all videos
const streams = [];
const serverConfig = [
{ name: 'Beta', number: '#1' },
{ name: 'Beta#3', number: '#2' }
];
serverConfig.forEach(server => {
const serverName = server.name;
const serverNumber = server.number;
if (servers[serverName]) {
const baseServer = servers[serverName];
console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`);
playlist.forEach(item => {
const processedItem = processEpisode(item, baseServer, serverName, serverNumber);
// 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",
headers: {
'Referer': 'https://mp4hydra.org/'
}
});
// Add subtitle tracks if available
if (processedItem.subtitles && processedItem.subtitles.length > 0) {
streams[streams.length - 1].subtitles = processedItem.subtitles.map(sub => ({
url: sub.url,
lang: sub.label
}));
}
});
}
});
return streams;
}
return [];
} catch (error) {
console.error(`[MP4Hydra] Error with title-only:`, error.message);
return [];
}
}
// Helper function to get TMDB details
async function getTMDBDetails(tmdbId, mediaType) {
const { getTmdbApiKey } = require('../utils/tmdbKey');
const TMDB_API_KEY = getTmdbApiKey();
if (!TMDB_API_KEY) {
console.warn('[MP4Hydra] TMDB API key missing; skipping TMDB metadata fetch');
return null;
}
try {
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}`);
if (mediaType === 'movie') {
return {
title: response.data.title,
original_title: response.data.original_title,
year: response.data.release_date ? response.data.release_date.split('-')[0] : null,
slug: generateSlug(response.data.title)
};
} else {
return {
title: response.data.name,
original_title: response.data.original_name,
year: response.data.first_air_date ? response.data.first_air_date.split('-')[0] : null,
slug: generateSlug(response.data.name)
};
}
} catch (error) {
console.error(`[MP4Hydra] Error fetching details from TMDB:`, error.message);
return null;
}
}
module.exports = { getMP4HydraStreams };
+13 -8
View File
@@ -62,8 +62,9 @@ const getCookieForRequest = async (regionPreference = null, userCookie = null) =
try {
const array = Array.isArray(global.currentRequestConfig.cookies) ? global.currentRequestConfig.cookies.filter(Boolean) : [];
if (array.length > 0) {
// Just pick first for now (quota logic removed here if previously elsewhere)
baseCookieToUse = array[0];
// Strip 'ui=' prefix if present — callers always prepend 'ui=' themselves
const raw = array[0];
baseCookieToUse = raw.startsWith('ui=') ? raw.slice(3) : raw;
}
} catch (e) {
console.warn(`[CookieManager] Cookie array selection error: ${e.message}`);
@@ -1211,7 +1212,12 @@ const fetchSourcesForSingleFid = async (fidToProcess, shareKey, regionPreference
const baseHeaders = {
'Cookie': `ui=${cookieForRequest}`,
'Content-Type': 'application/x-www-form-urlencoded'
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest',
'Origin': 'https://www.febbox.com',
'Referer': `https://www.febbox.com/share/${shareKey}`,
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
'Accept': '*/*',
};
const finalPostUrl = FEBBOX_PLAYER_URL;
@@ -1221,7 +1227,7 @@ const fetchSourcesForSingleFid = async (fidToProcess, shareKey, regionPreference
console.log(` Fetching fresh player data for video FID: ${fidToProcess} (Share: ${shareKey}) directly to ${FEBBOX_PLAYER_URL}`);
try {
const response = await axios.post(finalPostUrl, postDataForAxios, axiosConfig);
const response = await axios.post(finalPostUrl, postDataForAxios, { ...axiosConfig, responseType: 'text' });
const playerContent = response.data;
// Mark the region as available if the request succeeded
@@ -2679,10 +2685,9 @@ const sortStreamsByQuality = (streams) => {
// Provider priority ordering (lower number = higher priority)
const providerSortKeys = {
'ShowBox': 1,
'Xprime.tv': 2,
'HollyMovieHD': 3,
'Soaper TV': 4,
'ShowBox': 1,
'HollyMovieHD': 2,
'Soaper TV': 3,
// Default for unknown providers
default: 99
};
+142 -61
View File
@@ -1,6 +1,100 @@
const crypto = require('crypto');
const axios = require('axios');
// Removed unused parseArgs helper
const BASE_URL = 'https://core.vidzee.wtf';
const PLAYER_URL = 'https://player.vidzee.wtf';
const VIDZEE_HEADERS = {
'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.7051.98 Safari/537.36',
'Accept': 'application/json, text/javascript, */*; q=0.01',
'Accept-Language': 'en-US,en;q=0.9',
'Referer': PLAYER_URL,
'Origin': PLAYER_URL
};
// Pad/truncate key string to 32 bytes (UTF-8, mirrors CryptoJS behaviour)
function getKeyBytes(key) {
const encoded = Buffer.from(key, 'utf8');
const result = Buffer.alloc(32);
encoded.copy(result, 0, 0, Math.min(encoded.length, 32));
return result;
}
// AES-256-CBC decrypt a VidZee stream link.
// encryptedData is outer-base64 of "ivBase64:cipherBase64"
async function decryptLink(encryptedData, decryptionKey) {
try {
if (!encryptedData || !decryptionKey) return '';
const decoded = Buffer.from(encryptedData, 'base64').toString('utf8');
const colonIdx = decoded.indexOf(':');
if (colonIdx === -1) return '';
const ivBase64 = decoded.slice(0, colonIdx);
const cipherBase64 = decoded.slice(colonIdx + 1);
if (!ivBase64 || !cipherBase64) return '';
const iv = Buffer.from(ivBase64, 'base64');
const cipherBytes = Buffer.from(cipherBase64, 'base64');
const keyBytes = getKeyBytes(decryptionKey);
const decipher = crypto.createDecipheriv('aes-256-cbc', keyBytes, iv);
const decrypted = Buffer.concat([decipher.update(cipherBytes), decipher.final()]);
return decrypted.toString('utf8');
} catch {
return '';
}
}
// Derive the real AES-CBC decryption key from VidZee's API-key blob via AES-GCM.
// Mirrors the TypeScript deriveKey() in the reference implementation.
async function deriveKey(e) {
try {
if (!e) return '';
const t = Buffer.from(e.replace(/\s+/g, ''), 'base64');
if (t.length <= 28) return '';
const n = t.slice(0, 12); // AES-GCM IV
const r = t.slice(12, 28);
const a = t.slice(28);
// i = a || r (reference: i.set(a, 0); i.set(r, a.length))
const i = Buffer.concat([a, r]);
// AES-256-GCM key = SHA-256 of fixed salt string
const l = crypto.createHash('sha256')
.update('4f2a9c7d1e8b3a6f0d5c2e9a7b1f4d8c', 'utf8')
.digest();
// WebCrypto passes ciphertext+tag as one buffer; tag is last 16 bytes
const ciphertext = i.slice(0, i.length - 16);
const authTag = i.slice(i.length - 16);
const decipher = crypto.createDecipheriv('aes-256-gcm', l, n);
decipher.setAuthTag(authTag);
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
return decrypted.toString('utf8');
} catch {
return '';
}
}
// Fetch and derive the current decryption key from VidZee's API
async function fetchDecryptionKey() {
try {
const response = await axios.get(`${BASE_URL}/api-key`, {
headers: VIDZEE_HEADERS,
responseType: 'text',
timeout: 10000
});
if (response.status === 200 && response.data) {
return await deriveKey(response.data);
}
return null;
} catch {
return null;
}
}
const getVidZeeStreams = async (tmdbId, mediaType, seasonNum, episodeNum) => {
if (!tmdbId) {
@@ -24,41 +118,31 @@ const getVidZeeStreams = async (tmdbId, mediaType, seasonNum, episodeNum) => {
}
}
const servers = [1,2,3,4,5,6,7,8,9,10]; // VidZee server identifiers
// Fetch and derive the decryption key before hitting servers
const decKey = await fetchDecryptionKey();
if (!decKey) {
console.error('[VidZee] Failed to fetch/derive decryption key.');
return [];
}
console.log('[VidZee] Decryption key derived successfully.');
// Servers 0–13 (matches reference implementation)
const servers = Array.from({ length: 14 }, (_, i) => i);
const streamPromises = servers.map(async (sr) => {
let targetApiUrl = `https://player.vidzee.wtf/api/server?id=${tmdbId}&sr=${sr}`;
let apiUrl = `${PLAYER_URL}/api/server?id=${tmdbId}&sr=${sr}`;
if (mediaType === 'tv') {
targetApiUrl += `&ss=${seasonNum}&ep=${episodeNum}`;
apiUrl += `&ss=${seasonNum}&ep=${episodeNum}`;
}
let finalApiUrl;
const headers = {
'Referer': `https://player.vidzee.wtf/embed/movie/${tmdbId}`
};
const timeout = 7000; // Reduced timeout
finalApiUrl = targetApiUrl;
console.log(`[VidZee] Fetching from server ${sr}: ${targetApiUrl}`);
try {
const response = await axios.get(finalApiUrl, {
headers: headers,
timeout: timeout
const response = await axios.get(apiUrl, {
headers: VIDZEE_HEADERS,
timeout: 7000
});
const responseData = response.data;
if (!responseData || typeof responseData !== 'object') {
console.error(`[VidZee S${sr}] Error: Invalid response data from API.`);
return [];
}
if (responseData.tracks) {
delete responseData.tracks;
}
if (!responseData || typeof responseData !== 'object') return [];
let apiSources = [];
if (responseData.url && Array.isArray(responseData.url)) {
@@ -67,54 +151,51 @@ const getVidZeeStreams = async (tmdbId, mediaType, seasonNum, episodeNum) => {
apiSources = [responseData];
}
if (!apiSources || apiSources.length === 0) {
console.log(`[VidZee S${sr}] No stream sources found in API response.`);
return [];
}
if (apiSources.length === 0) return [];
const streams = apiSources.map(sourceItem => {
// Prefer sourceItem.name as label, fallback to sourceItem.type, then 'VidZee Stream'
const streams = await Promise.all(apiSources.map(async (sourceItem) => {
const label = sourceItem.name || sourceItem.type || 'VidZee';
// Ensure quality has 'p' if it's a resolution, or keep it as is
let quality = String(label).match(/^\d+$/) ? `${label}p` : label;
// Normalize non-numeric or ambiguous quality labels to a baseline so they survive minQuality filter
if (!/(\d{3,4})p/.test(quality.toLowerCase())) {
quality = '720p';
if (!/\d{3,4}p/.test(quality)) quality = '720p';
const language = sourceItem.language || sourceItem.lang || 'Unknown';
let rawLink = sourceItem.link;
if (rawLink && !/^https?:\/\//i.test(rawLink)) {
const decoded = await decryptLink(rawLink, decKey);
if (decoded && /^https?:\/\//i.test(decoded)) {
rawLink = decoded;
} else {
console.log(`[VidZee S${sr}] Decryption yielded non-URL, skipping.`);
return null;
}
}
const language = sourceItem.language || sourceItem.lang;
if (!rawLink || !/^https?:\/\//i.test(rawLink)) return null;
return {
name: `VidZee Server${sr} - ${quality} - ${language}`,
title: `VidZee Server${sr} - ${quality} - ${language}`,
url: sourceItem.link, // Use sourceItem.link for the URL
quality: quality,
provider: "VidZee",
headers: {
'Referer': 'https://core.vidzee.wtf/'
}
url: rawLink,
quality,
provider: 'VidZee',
headers: { 'Referer': `${BASE_URL}/` }
};
}).filter(stream => stream.url);
}));
console.log(`[VidZee S${sr}] Successfully extracted ${streams.length} streams. Qualities: ${streams.map(s=>s.quality).join(', ')}`);
return streams;
} catch (error) {
if (error.response) {
console.error(`[VidZee S${sr}] Error fetching: ${error.response.status} ${error.response.statusText}`);
} else if (error.request) {
console.error(`[VidZee S${sr}] Error fetching: No response received.`);
} else {
console.error(`[VidZee S${sr}] Error fetching:`, error.message);
const valid = streams.filter(Boolean);
if (valid.length > 0) {
console.log(`[VidZee S${sr}] ${valid.length} stream(s) extracted.`);
}
return valid;
} catch (error) {
console.error(`[VidZee S${sr}] Error: ${error.message}`);
return [];
}
});
const allStreamsNested = await Promise.all(streamPromises);
const allStreams = allStreamsNested.flat();
console.log(`[VidZee] Found a total of ${allStreams.length} streams from servers ${servers.join(', ')}.`);
const allStreams = (await Promise.all(streamPromises)).flat();
console.log(`[VidZee] Total streams: ${allStreams.length}`);
return allStreams;
};
module.exports = { getVidZeeStreams };
module.exports = { getVidZeeStreams };
+152
View File
@@ -0,0 +1,152 @@
const axios = require('axios');
const { getTmdbApiKey } = require('../utils/tmdbKey');
const DAHMER_MOVIES_API = 'https://a.111477.xyz';
const DAHMER_WORKER_API = 'https://p.111477.xyz/bulk?u=';
const REQUEST_HEADERS = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
'Referer': `${DAHMER_MOVIES_API}/`
};
function parseLinks(html) {
const links = [];
const rowRegex = /<tr[^>]*>([\s\S]*?)<\/tr>/gi;
let match;
while ((match = rowRegex.exec(html)) !== null) {
const rowContent = match[1];
const linkMatch = rowContent.match(/<a[^>]*href=["']([^"']*)["'][^>]*>([^<]*)<\/a>/i);
const sizeMatch = rowContent.match(/<td[^>]*>(\d+(?:\.\d+)?\s?[KMGT]B)<\/td>/i);
if (linkMatch) {
const href = linkMatch[1];
const text = linkMatch[2].trim();
const size = sizeMatch ? sizeMatch[1].trim() : 'N/A';
if (text && href !== '../' && /\.(mkv|mp4|avi|webm|m3u8)$/i.test(text)) {
links.push({ text, href, size });
}
}
}
return links;
}
async function fetchDirectory(title, year, season) {
const cleanTitle = title.replace(/:/g, '');
const variants = season !== null ? [
`/tvs/${encodeURIComponent(cleanTitle)}/Season%20${season < 10 ? '0' + season : season}/`,
`/tvs/${encodeURIComponent(cleanTitle)}/Season%20${season}/`
] : [
`/movies/${encodeURIComponent(`${cleanTitle} (${year})`)}/`
];
for (const variant of variants) {
try {
const res = await axios.get(DAHMER_MOVIES_API + variant, {
headers: REQUEST_HEADERS,
timeout: 10000,
responseType: 'text'
});
if (res.data) return { html: res.data, dirUrl: DAHMER_MOVIES_API + variant };
} catch {
// try next variant
}
}
return null;
}
async function getDahmermoviesStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) {
console.log(`[DahmerMovies] Searching for ${mediaType} ${tmdbId}`);
const tmdbKey = getTmdbApiKey();
if (!tmdbKey) {
console.error('[DahmerMovies] No TMDB API key configured.');
return [];
}
let title, year;
try {
const type = mediaType === 'tv' ? 'tv' : 'movie';
const { data } = await axios.get(
`https://api.themoviedb.org/3/${type}/${tmdbId}?api_key=${tmdbKey}`,
{ timeout: 8000 }
);
title = data.title || data.name || '';
year = (data.release_date || data.first_air_date || '').substring(0, 4);
} catch (err) {
console.error(`[DahmerMovies] TMDB lookup failed: ${err.message}`);
return [];
}
if (!title) return [];
const dir = await fetchDirectory(title, year, seasonNum);
if (!dir) {
console.log(`[DahmerMovies] Directory not found for "${title}"`);
return [];
}
let paths = parseLinks(dir.html);
// For TV, filter to the specific episode when possible
if (seasonNum !== null && episodeNum !== null) {
const epStr = String(episodeNum).padStart(2, '0');
const seStr = String(seasonNum).padStart(2, '0');
const epFiltered = paths.filter(p => {
const name = p.text.toLowerCase();
return name.includes(`s${seStr}e${epStr}`) || name.includes(`e${epStr}`);
});
if (epFiltered.length > 0) paths = epFiltered;
}
// Sort 4K first
paths.sort((a, b) => (/2160p|4k/i.test(b.text) ? 1 : 0) - (/2160p|4k/i.test(a.text) ? 1 : 0));
const streams = [];
for (const path of paths.slice(0, 5)) {
let directUrl;
if (path.href.startsWith('http')) {
directUrl = path.href;
} else if (path.href.includes('/movies/') || path.href.includes('/tvs/')) {
directUrl = DAHMER_MOVIES_API + (path.href.startsWith('/') ? '' : '/') + path.href;
} else {
directUrl = dir.dirUrl + path.href;
}
directUrl = decodeURI(directUrl.replace(/([^:]\/)\/+/g, '$1'));
const fileName = path.text;
const isMulti = /\b(HIN|TAM|TEL|Multi|Dual|DUB|Multi-Audio|MULTI)\b/i.test(fileName);
const hasEngTag = /\b(Eng|English)\b/i.test(fileName);
const isEnglishTitle = /^[a-zA-Z0-9\s?!\-:]+$/.test(title);
let language = 'Original';
if (isMulti) language = 'Multi Audio';
else if (isEnglishTitle && hasEngTag) language = 'English';
const formatMatch = fileName.match(/\.(mkv|mp4|m3u8|avi|webm)$/i);
const fileFormat = formatMatch ? formatMatch[1].toUpperCase() : 'LINK';
const resolution = fileName.match(/\b(2160p|1080p|720p|4[Kk])\b/)?.[0] || '1080p';
const info = fileName
.replace(/\.(mkv|mp4|avi|webm|m3u8)$/i, '')
.replace(/[\[\]()._-]/g, ' ')
.replace(/\s+/g, ' ')
.trim();
streams.push({
name: 'DahmerMovies',
title: `${resolution} | ${language} | ${path.size} | ${fileFormat} | ${info}`,
url: DAHMER_WORKER_API + encodeURI(directUrl),
quality: resolution,
provider: 'DahmerMovies',
headers: {
'User-Agent': REQUEST_HEADERS['User-Agent'],
'Referer': `${DAHMER_MOVIES_API}/`,
'Accept': '*/*',
'Range': 'bytes=0-'
}
});
}
console.log(`[DahmerMovies] Total results found: ${streams.length}`);
return streams;
}
module.exports = { getDahmermoviesStreams };
+118
View File
@@ -0,0 +1,118 @@
const axios = require('axios');
const { getTmdbApiKey } = require('../utils/tmdbKey');
const LORDFLIX_HEADERS = {
'Accept': '*/*',
'Origin': 'https://lordflix.org',
'Referer': 'https://lordflix.org/',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'
};
const LORDFLIX_API = 'https://network.hasta-la-vista.site';
const MULTI_DECRYPT_API = 'https://enc-dec.app/api';
const SERVERS = ['Berlin', 'Tokyo', 'Bogota', 'Oslo', 'Luna', 'LordFlix', 'Sakura', 'Rio', 'Ativa'];
// Matches the reference project's encodeQuote: encodeURIComponent with + encoded as %20
function encodeQuote(str) {
return encodeURIComponent(str).replace(/%20/g, '+').replace(/\+/g, '%20');
}
async function getLordflixStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) {
console.log(`[Lordflix] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}`);
const tmdbKey = getTmdbApiKey();
if (!tmdbKey) {
console.error('[Lordflix] No TMDB API key configured.');
return [];
}
// Step 1: TMDB lookup for title, year, imdbId
let info;
try {
const type = mediaType === 'tv' ? 'tv' : 'movie';
const { data } = await axios.get(
`https://api.themoviedb.org/3/${type}/${tmdbId}?api_key=${tmdbKey}&append_to_response=external_ids`,
{ timeout: 8000 }
);
info = {
title: data.title || data.name || '',
year: (data.release_date || data.first_air_date || '').split('-')[0],
imdbId: (data.external_ids && data.external_ids.imdb_id) || ''
};
} catch (err) {
console.error(`[Lordflix] TMDB lookup failed: ${err.message}`);
return [];
}
if (!info.title || !info.imdbId) {
console.error('[Lordflix] Missing title or IMDb ID from TMDB.');
return [];
}
const typeParam = mediaType === 'tv' ? 'series' : 'movie';
const titleEnc = encodeQuote(info.title);
const streams = [];
// Step 2: Query each server in parallel
await Promise.all(SERVERS.map(async (server) => {
try {
let serverUrl = `${LORDFLIX_API}/?title=${titleEnc}&type=${typeParam}&year=${info.year || ''}`
+ `&imdb=${info.imdbId}&tmdb=${tmdbId}&server=${server}`;
if (mediaType === 'tv') {
serverUrl += `&season=${seasonNum}&episode=${episodeNum}`;
}
// Step 3: Get signed proxy URL from enc-dec.app
const encBridgeRes = await axios.get(
`${MULTI_DECRYPT_API}/enc-lordflix?url=${encodeQuote(serverUrl)}`,
{ timeout: 8000 }
);
const encBridgeJson = encBridgeRes.data;
if (!encBridgeJson || encBridgeJson.status !== 200 || !encBridgeJson.result) return;
const { url: proxyEncUrl, sign: signature } = encBridgeJson.result;
if (!proxyEncUrl || !signature) return;
// Step 4: Fetch encrypted stream data from proxy URL
const remoteEncRes = await axios.get(proxyEncUrl, {
headers: LORDFLIX_HEADERS,
timeout: 8000,
responseType: 'text'
});
const remoteEncData = typeof remoteEncRes.data === 'string'
? remoteEncRes.data
: JSON.stringify(remoteEncRes.data);
// Step 5: Decrypt via enc-dec.app
const decRes = await axios.post(`${MULTI_DECRYPT_API}/dec-lordflix`,
{ text: remoteEncData, sign: signature },
{ headers: { 'Content-Type': 'application/json' }, timeout: 8000 }
);
const finalJson = decRes.data;
if (!finalJson || finalJson.status !== 200 || !finalJson.result || finalJson.result.error) return;
const streamList = finalJson.result.stream;
if (!Array.isArray(streamList) || streamList.length === 0) return;
const topStream = streamList[0];
if (topStream.type === 'hls' && topStream.playlist) {
streams.push({
name: `Lordflix[${server}]`,
title: `Lordflix[${server}]`,
url: topStream.playlist,
quality: 'Auto',
provider: 'Lordflix',
headers: LORDFLIX_HEADERS
});
console.log(`[Lordflix] Server ${server}: got stream.`);
}
} catch (err) {
console.error(`[Lordflix] Server ${server} error: ${err.message}`);
}
}));
console.log(`[Lordflix] Total streams: ${streams.length}`);
return streams;
}
module.exports = { getLordflixStreams };
-651
View File
@@ -1,651 +0,0 @@
// MoviesClub Provider
// Extracts streams from moviesapi.club
const axios = require('axios');
const JsUnpacker = require('../utils/jsunpack');
const { config } = require('../utils/config');
function applyMoviesClubProxy(url) {
try {
const base = config.moviesclubProxy;
if (!base || typeof base !== 'string') return url;
// Ensure base ends with either = or a delimiter allowing raw append
const proxied = base + encodeURIComponent(url);
return proxied;
} catch {
return url;
}
}
async function getMoviesClubStreams(tmdbId, mediaType, season, episode) {
console.log(`[moviesclub] Fetching streams for ${mediaType} tmdbId=${tmdbId} season=${season} episode=${episode}`);
const streams = [];
try {
// Build the URL based on media type
let url;
if (mediaType === 'movie') {
url = `https://moviesapi.club/movie/${tmdbId}`;
} else if (mediaType === 'tv' && season && episode) {
url = `https://moviesapi.club/tv/${tmdbId}-${season}-${episode}`;
} else {
console.log('[moviesclub] Invalid parameters for TV show');
return [];
}
console.log(`[moviesclub] Fetching from: ${url}`);
// Fetch the main page
const response = await axios.get(url, {
timeout: 10000,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1'
}
});
const html = response.data;
console.log(`[moviesclub] Fetched ${html.length} characters`);
// Extract data-url from srvswitch-menu
const dataUrlMatch = html.match(/data-url="([^"]+)"/);
if (!dataUrlMatch) {
console.log('[moviesclub] No data-url found in HTML');
return [];
}
const embedUrl = dataUrlMatch[1];
console.log(`[moviesclub] Found embed URL: ${embedUrl}`);
// Check if this is a cdn.moviesapi.club embed URL with multiple servers
if (embedUrl.includes('cdn.moviesapi.club')) {
console.log('[moviesclub] Detected cdn.moviesapi.club embed URL, handling multiple servers');
return null;
}
// Handle regular embed URLs (existing logic)
return await handleSingleServer(embedUrl, mediaType, season, episode, tmdbId);
} catch (error) {
console.error(`[moviesclub] Error:`, error.message);
return [];
}
}
// Handle cdn.moviesapi.club URLs with multiple servers
async function handleMultipleServers(embedUrl, mediaType, season, episode, tmdbId) {
console.log(`[moviesclub] Handling multiple servers from: ${embedUrl}`);
const streams = [];
try {
// Fetch the embed page
const embedResponse = await axios.get(embedUrl, {
timeout: 10000,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate',
'Connection': 'keep-alive'
}
});
const embedHtml = embedResponse.data;
console.log(`[moviesclub] Fetched embed page: ${embedHtml.length} characters`);
// Extract server options from the HTML
const serverMatches = embedHtml.match(/<div class="server"[^>]*data-hash="([^"]+)"[^>]*>([^<]+)<\/div>/gi);
if (!serverMatches || serverMatches.length === 0) {
console.log('[moviesclub] No server options found, trying direct iframe extraction');
// Try to extract the main iframe src
const iframeMatch = embedHtml.match(/<iframe[^>]*src="([^"]*)"[^>]*>/i);
if (iframeMatch) {
const iframeSrc = iframeMatch[1];
console.log(`[moviesclub] Found iframe src: ${iframeSrc}`);
// Try to decode if it's base64
try {
const decodedSrc = Buffer.from(iframeSrc.split('/').pop(), 'base64').toString('utf-8');
console.log(`[moviesclub] Decoded iframe src: ${decodedSrc}`);
// If it's a URL, use it directly
if (decodedSrc.startsWith('http')) {
const rawUrl = decodedSrc;
const stream = {
name: `MoviesClub - Direct Stream`,
title: `${mediaType === 'movie' ? 'Movie' : `S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`} ${tmdbId} - MoviesClub Direct`,
url: applyMoviesClubProxy(rawUrl),
quality: extractQualityFromUrl(rawUrl) || '720p'
};
streams.push(stream);
console.log(`[moviesclub] Added direct stream from iframe`);
return streams;
}
} catch (decodeError) {
console.log(`[moviesclub] Could not decode iframe src: ${decodeError.message}`);
}
}
return streams;
}
console.log(`[moviesclub] Found ${serverMatches.length} server options`);
// Process each server
for (let i = 0; i < serverMatches.length; i++) {
const serverMatch = serverMatches[i];
const hashMatch = serverMatch.match(/data-hash="([^"]+)"/);
const nameMatch = serverMatch.match(/>([^<]+)<\/div>/);
if (!hashMatch || !nameMatch) {
console.log(`[moviesclub] Could not parse server ${i + 1}, skipping`);
continue;
}
const encodedHash = hashMatch[1];
const serverName = nameMatch[1].trim();
console.log(`[moviesclub] Processing server ${i + 1}: ${serverName}`);
try {
// Construct the rcp URL from the hash
const rcpUrl = `https://cloudnestra.com/rcp/${encodedHash}`;
console.log(`[moviesclub] RCP URL: ${rcpUrl}`);
// Fetch the rcp URL to get the JavaScript with prorcp URL
const rcpResponse = await axios.get(rcpUrl, {
timeout: 8000,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate',
'Connection': 'keep-alive',
'Referer': embedUrl
}
});
const rcpHtml = rcpResponse.data;
console.log(`[moviesclub] RCP response length: ${rcpHtml.length}`);
// Extract prorcp URL from the JavaScript
const prorcpMatch = rcpHtml.match(/src:\s*['"]([^'"]*prorcp[^'"]*)['"]/i);
if (!prorcpMatch) {
// Try alternative patterns
const altMatch = rcpHtml.match(/['"](\/prorcp\/[^'"]*)['"]/i);
if (altMatch) {
prorcpUrl = altMatch[1];
} else {
console.log(`[moviesclub] No prorcp URL found in rcp response`);
continue;
}
} else {
prorcpUrl = prorcpMatch[1];
}
console.log(`[moviesclub] Found prorcp URL: ${prorcpUrl}`);
// If it's a relative URL, make it absolute
if (prorcpUrl.startsWith('/')) {
prorcpUrl = `https://cloudnestra.com${prorcpUrl}`;
console.log(`[moviesclub] Converted to absolute URL: ${prorcpUrl}`);
}
// Fetch the prorcp URL to get the player HTML
const prorcpResponse = await axios.get(prorcpUrl, {
timeout: 8000,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate',
'Connection': 'keep-alive',
'Referer': rcpUrl
}
});
const prorcpHtml = prorcpResponse.data;
console.log(`[moviesclub] Prorcp response length: ${prorcpHtml.length}`);
// Extract stream URL from Playerjs configuration
const streamUrl = extractStreamFromPlayerjsUpdated(prorcpHtml);
if (!streamUrl) {
console.log(`[moviesclub] No stream URL found in prorcp response`);
continue;
}
console.log(`[moviesclub] Extracted stream URL: ${streamUrl.substring(0, 100)}...`);
// Create stream object
const rawUrl = streamUrl;
const stream = {
name: `MoviesClub - ${serverName}`,
title: `${mediaType === 'movie' ? 'Movie' : `S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`} ${tmdbId} - ${serverName}`,
url: applyMoviesClubProxy(rawUrl),
quality: extractQualityFromUrl(rawUrl) || '720p',
headers: {}
};
streams.push(stream);
console.log(`[moviesclub] Added stream from server: ${serverName}`);
} catch (serverError) {
console.error(`[moviesclub] Error processing server ${serverName}:`, serverError.message);
continue;
}
}
console.log(`[moviesclub] Total streams found: ${streams.length}`);
return streams;
} catch (error) {
console.error(`[moviesclub] Error in handleMultipleServers:`, error.message);
return [];
}
}
// Handle regular embed URLs (existing logic)
async function handleSingleServer(embedUrl, mediaType, season, episode, tmdbId) {
console.log(`[moviesclub] Handling single server from: ${embedUrl}`);
const streams = [];
try {
// Fetch the embed page
const embedResponse = await axios.get(embedUrl, {
timeout: 10000,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate',
'Connection': 'keep-alive'
}
});
const embedHtml = embedResponse.data;
console.log(`[moviesclub] Fetched embed page: ${embedHtml.length} characters`);
// Look for packed JavaScript using JsUnpacker.detect()
const scriptTags = embedHtml.match(/<script[^>]*>[\s\S]*?<\/script>/gi) || [];
let packedCode = null;
for (const scriptTag of scriptTags) {
// Extract script content
const scriptContent = scriptTag.replace(/<\/?script[^>]*>/gi, '').trim();
if (scriptContent) {
// Create unpacker instance and check if it's packed
const unpacker = new JsUnpacker(scriptContent);
if (unpacker.detect()) {
console.log(`[moviesclub] Found packed JavaScript in script tag, length: ${scriptContent.length}`);
packedCode = scriptContent;
break;
}
}
}
if (!packedCode) {
console.log('[moviesclub] No packed JavaScript found');
return [];
}
// Unpack using JsUnpacker
const unpacker = new JsUnpacker(packedCode);
const unpackedCode = unpacker.unpack();
if (!unpackedCode) {
console.log('[moviesclub] Failed to unpack JavaScript');
return [];
}
console.log(`[moviesclub] Successfully unpacked code, length: ${unpackedCode.length}`);
// Parse the player configuration from unpacked code
const playerConfig = parsePlayerConfig(unpackedCode);
if (!playerConfig) {
console.log('[moviesclub] Failed to parse player configuration');
return [];
}
console.log('[moviesclub] Parsed player config:', playerConfig);
// Extract streams from the configuration
if (playerConfig.file) {
const rawUrl = playerConfig.file;
const stream = {
name: `MoviesClub - ${playerConfig.title || 'Server 1'}`,
title: playerConfig.title || `${mediaType === 'movie' ? 'Movie' : `S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`} ${tmdbId} - MoviesClub`,
url: applyMoviesClubProxy(rawUrl),
quality: extractQualityFromUrl(rawUrl) || '720p',
// Always include vidora headers for single-server results to prevent 403 playback issues
headers: {
Origin: 'https://vidora.stream',
Referer: 'https://vidora.stream/'
}
};
// Add poster if available
if (playerConfig.poster) {
stream.poster = playerConfig.poster;
}
// Add subtitles if available
if (playerConfig.subtitle) {
stream.subtitles = parseSubtitles(playerConfig.subtitle);
}
streams.push(stream);
}
console.log(`[moviesclub] Found ${streams.length} streams`);
return streams;
} catch (error) {
console.error(`[moviesclub] Error in handleSingleServer:`, error.message);
return [];
}
}
// Extract stream URL from Playerjs configuration in HTML
function extractStreamFromPlayerjs(html) {
try {
// Look for the Playerjs constructor call
const playerMatch = html.match(/new Playerjs\(\s*(\{[^}]*\})\s*\)/);
if (!playerMatch) {
console.log('[moviesclub] No Playerjs constructor found in HTML');
return null;
}
const configStr = playerMatch[1];
console.log(`[moviesclub] Found Playerjs config: ${configStr.substring(0, 200)}...`);
// Extract the file URL from the configuration
const fileMatch = configStr.match(/file:\s*['"]([^'"]+)['"]/);
if (!fileMatch) {
console.log('[moviesclub] No file URL found in Playerjs config');
return null;
}
const streamUrl = fileMatch[1];
console.log(`[moviesclub] Extracted stream URL from Playerjs: ${streamUrl.substring(0, 100)}...`);
return streamUrl;
} catch (error) {
console.error('[moviesclub] Error extracting stream from Playerjs:', error.message);
return null;
}
}
// Extract streams from server response
function extractStreamsFromServer(serverData, serverName, mediaType, season, episode, tmdbId) {
const streams = [];
try {
// Handle different response formats
if (typeof serverData === 'string') {
// Try to parse as JSON
try {
serverData = JSON.parse(serverData);
} catch (parseError) {
console.log(`[moviesclub] Server response is not JSON, treating as direct URL: ${serverData.substring(0, 100)}`);
// Treat as direct stream URL
if (serverData.startsWith('http')) {
const rawUrl = serverData;
const stream = {
name: `MoviesClub - ${serverName}`,
title: `${mediaType === 'movie' ? 'Movie' : `S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`} ${tmdbId} - ${serverName}`,
url: applyMoviesClubProxy(rawUrl),
quality: extractQualityFromUrl(rawUrl) || '720p',
headers: serverData.includes('vidora.stream') ? { Origin: 'https://vidora.stream', Referer: 'https://vidora.stream/' } : {}
};
streams.push(stream);
}
return streams;
}
}
// Handle JSON response
if (serverData && typeof serverData === 'object') {
// Look for common stream URL patterns
const possibleKeys = ['file', 'url', 'src', 'stream', 'video', 'source'];
for (const key of possibleKeys) {
if (serverData[key] && typeof serverData[key] === 'string' && serverData[key].startsWith('http')) {
console.log(`[moviesclub] Found stream URL in key '${key}': ${serverData[key].substring(0, 100)}`);
const rawUrl = serverData[key];
const stream = {
name: `MoviesClub - ${serverName}`,
title: `${mediaType === 'movie' ? 'Movie' : `S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`} ${tmdbId} - ${serverName}`,
url: applyMoviesClubProxy(rawUrl),
quality: extractQualityFromUrl(rawUrl) || '720p',
headers: rawUrl.includes('vidora.stream') ? { Origin: 'https://vidora.stream', Referer: 'https://vidora.stream/' } : {}
};
// Add poster if available
if (serverData.poster) {
stream.poster = serverData.poster;
}
// Add subtitles if available
if (serverData.subtitle || serverData.subtitles) {
const subtitleData = serverData.subtitle || serverData.subtitles;
stream.subtitles = parseSubtitles(subtitleData);
}
streams.push(stream);
break; // Only take the first valid stream URL
}
}
// If no direct URL found, check for nested objects
if (streams.length === 0) {
for (const key of possibleKeys) {
if (serverData[key] && typeof serverData[key] === 'object') {
const nestedStreams = extractStreamsFromServer(serverData[key], serverName, mediaType, season, episode, tmdbId);
streams.push(...nestedStreams);
}
}
}
}
} catch (error) {
console.error(`[moviesclub] Error extracting streams from server ${serverName}:`, error.message);
}
return streams;
}
// Extract stream URL from Playerjs configuration in HTML
function extractStreamFromPlayerjs(html) {
try {
// Look for the Playerjs constructor call
const playerMatch = html.match(/new Playerjs\(\s*(\{[^}]*\})\s*\)/);
if (!playerMatch) {
console.log('[moviesclub] No Playerjs constructor found in HTML');
return null;
}
const configStr = playerMatch[1];
console.log(`[moviesclub] Found Playerjs config: ${configStr.substring(0, 200)}...`);
// Extract the file URL from the configuration
const fileMatch = configStr.match(/file:\s*['"]([^'"]+)['"]/);
if (!fileMatch) {
console.log('[moviesclub] No file URL found in Playerjs config');
return null;
}
const streamUrl = fileMatch[1];
console.log(`[moviesclub] Extracted stream URL from Playerjs: ${streamUrl.substring(0, 100)}...`);
return streamUrl;
} catch (error) {
console.error('[moviesclub] Error extracting stream from Playerjs:', error.message);
return null;
}
}
// Parse player configuration from unpacked JavaScript
function parsePlayerConfig(unpackedCode) {
try {
// Look for the player configuration object
const configMatch = unpackedCode.match(/new Playerjs\(\s*(\{[^}]*\})\s*\)/);
if (!configMatch) {
return null;
}
// Extract the configuration object
const configStr = configMatch[1];
// Parse the configuration (this is a simple parser for the expected format)
const config = {};
// Extract key-value pairs
const pairs = configStr.match(/(\w+):"([^"]*)"/g) || [];
pairs.forEach(pair => {
const [, key, value] = pair.match(/(\w+):"([^"]*)"/);
config[key] = value;
});
// Also handle non-quoted values
const numberPairs = configStr.match(/(\w+):(\d+(?:\.\d+)?)/g) || [];
numberPairs.forEach(pair => {
const [, key, value] = pair.match(/(\w+):(\d+(?:\.\d+)?)/);
config[key] = parseFloat(value);
});
return config;
} catch (error) {
console.error('[moviesclub] Error parsing player config:', error.message);
return null;
}
}
// Parse subtitles from the subtitle string
function parseSubtitles(subtitleStr) {
if (!subtitleStr) return [];
const subtitles = [];
const subtitleMatches = subtitleStr.match(/\[([^\]]+)\]([^,\[]*)/g);
if (subtitleMatches) {
subtitleMatches.forEach(match => {
const [, language, url] = match.match(/\[([^\]]+)\]([^,\[]*)/);
if (url && url.trim()) {
subtitles.push({
language: language.trim(),
url: url.trim(),
type: 'vtt'
});
}
});
}
return subtitles;
}
// Extract quality from URL
function extractQualityFromUrl(url) {
if (!url) return '720p';
const qualityPatterns = [
{ pattern: /1080p|1080/i, quality: '1080p' },
{ pattern: /720p|720/i, quality: '720p' },
{ pattern: /480p|480/i, quality: '480p' },
{ pattern: /360p|360/i, quality: '360p' },
{ pattern: /2160p|4k|uhd/i, quality: '2160p' }
];
for (const { pattern, quality } of qualityPatterns) {
if (pattern.test(url)) {
return quality;
}
}
return '720p';
}
// Extract stream URL from Playerjs configuration in HTML (updated version)
function extractStreamFromPlayerjsUpdated(html) {
try {
// Look for the Playerjs constructor call
const playerMatch = html.match(/new Playerjs\(\s*(\{[^}]*\})\s*\)/);
if (!playerMatch) {
console.log('[moviesclub] No Playerjs constructor found in HTML');
return null;
}
const configStr = playerMatch[1];
console.log(`[moviesclub] Found Playerjs config: ${configStr.substring(0, 200)}...`);
// Extract the file URL from the configuration
let streamUrl = null;
// Try multiple patterns to extract the file URL
const patterns = [
/file:\s*['"]([^'"]+)['"]/,
/file:\s*([^,\s}]+)/,
/file\s*:\s*['"]([^'"]+)['"]/,
/file\s*:\s*([^,\s}]+)/
];
for (const pattern of patterns) {
const match = configStr.match(pattern);
if (match && match[1] && match[1].trim() !== '' && match[1].trim() !== '""') {
streamUrl = match[1].replace(/['"]/g, '').trim();
console.log(`[moviesclub] Found stream URL with pattern: ${streamUrl.substring(0, 100)}...`);
break;
}
}
// If no URL found in config, look for streaming URLs in the HTML
if (!streamUrl) {
console.log('[moviesclub] No file URL in Playerjs config, searching HTML for streaming URLs...');
// Look for m3u8 URLs
const m3u8Match = html.match(/https?:\/\/[^'"\s]+\.m3u8[^'"\s]*/i);
if (m3u8Match) {
streamUrl = m3u8Match[0];
console.log(`[moviesclub] Found m3u8 URL in HTML: ${streamUrl.substring(0, 100)}...`);
}
// Look for other streaming URLs
if (!streamUrl) {
const streamMatch = html.match(/https?:\/\/[^'"\s]+(?:\.mp4|\.m3u8|\.ts)[^'"\s]*/i);
if (streamMatch) {
streamUrl = streamMatch[0];
console.log(`[moviesclub] Found streaming URL in HTML: ${streamUrl.substring(0, 100)}...`);
}
}
}
if (!streamUrl) {
console.log('[moviesclub] No file URL found in Playerjs config after trying all patterns');
console.log('[moviesclub] Config string:', configStr.substring(0, 500));
return null;
}
console.log(`[moviesclub] Extracted stream URL from Playerjs: ${streamUrl.substring(0, 100)}...`);
return streamUrl;
} catch (error) {
console.error('[moviesclub] Error extracting stream from Playerjs:', error.message);
return null;
}
}
module.exports = {
getMoviesClubStreams
};
+82 -23
View File
@@ -202,7 +202,7 @@ async function extractDownloadLinks(moviePageUrl) {
if (header.is('h3') && headerText.toLowerCase().includes('season')) {
// TV Show Logic
const linkElements = blockContent.find('a.maxbutton-episode-links, a.maxbutton-batch-zip');
const linkElements = blockContent.find('a.maxbutton-episode-links, a.maxbutton-batch-zip, a.maxbutton-download-links, a[href*="links.modpro.blog"], a[href*="posts.modpro.blog"]');
linkElements.each((j, linkEl) => {
const buttonText = $(linkEl).text().trim();
const linkUrl = $(linkEl).attr('href');
@@ -214,15 +214,17 @@ async function extractDownloadLinks(moviePageUrl) {
}
});
} else if (header.is('h4')) {
// Movie Logic
const linkElement = blockContent.find('a[href*="modrefer.in"]').first();
// Movie Logic - site changed from modrefer.in to links.modpro.blog
const linkElement = blockContent.find('a.maxbutton-download-links, a[href*="links.modpro.blog"], a[href*="posts.modpro.blog"], a[href*="modrefer.in"]').first();
if (linkElement.length > 0) {
const link = linkElement.attr('href');
const cleanQuality = extractQuality(headerText);
links.push({
quality: cleanQuality,
url: link
});
if (link && cleanQuality && cleanQuality !== 'Unknown') {
links.push({
quality: cleanQuality,
url: link
});
}
}
}
});
@@ -239,7 +241,35 @@ async function resolveIntermediateLink(initialUrl, refererUrl, quality) {
try {
const urlObject = new URL(initialUrl);
if (urlObject.hostname.includes('dramadrip.com')) {
if (urlObject.hostname.includes('links.modpro.blog') || urlObject.hostname.includes('posts.modpro.blog')) {
const { data } = await makeRequest(initialUrl, { headers: { 'Referer': refererUrl } });
const $ = cheerio.load(data);
const finalLinks = [];
// Primary: scoped to .entry-content
$('.entry-content a[href*="driveseed.org"], .entry-content a[href*="cloud.unblockedgames.world"], .entry-content a[href*="tech.creativeexpressionsblog.com"], .entry-content a[href*="tech.examzculture.in"], .entry-content a[href*="tech.unblockedgames.world"], .entry-content a[href*="tech.examdegree.site"]').each((i, el) => {
const link = $(el).attr('href');
const text = $(el).text().trim();
if (link && text && !text.toLowerCase().includes('batch')) {
finalLinks.push({ server: text.replace(/\s+/g, ' '), url: link });
}
});
// Fallback: page-wide search
if (finalLinks.length === 0) {
$('a[href*="driveseed.org"], a[href*="cloud.unblockedgames.world"], a[href*="tech.creativeexpressionsblog.com"], a[href*="tech.examzculture.in"], a[href*="tech.unblockedgames.world"], a[href*="tech.examdegree.site"]').each((i, el) => {
const link = $(el).attr('href');
const text = $(el).text().trim();
if (link && text && !text.toLowerCase().includes('batch')) {
finalLinks.push({ server: text.replace(/\s+/g, ' ') || 'Download Link', url: link });
}
});
}
console.log(`[MoviesMod] Found ${finalLinks.length} links from ${urlObject.hostname}`);
return finalLinks;
} else if (urlObject.hostname.includes('dramadrip.com')) {
const { data: dramaData } = await makeRequest(initialUrl, { headers: { 'Referer': refererUrl } });
const $$ = cheerio.load(dramaData);
@@ -779,18 +809,38 @@ async function getMoviesModStreams(tmdbId, mediaType, seasonNum = null, episodeN
}
const { default: fetch } = await import('node-fetch');
const tmdbUrl = `https://api.themoviedb.org/3/${mediaType === 'tv' ? 'tv' : 'movie'}/${tmdbId}?api_key=${TMDB_API_KEY}&language=en-US`;
const tmdbUrl = `https://api.themoviedb.org/3/${mediaType === 'tv' ? 'tv' : 'movie'}/${tmdbId}?api_key=${TMDB_API_KEY}&language=en-US&append_to_response=external_ids`;
const tmdbDetails = await (await fetch(tmdbUrl)).json();
const title = mediaType === 'tv' ? tmdbDetails.name : tmdbDetails.title;
const year = mediaType === 'tv' ? tmdbDetails.first_air_date?.substring(0, 4) : tmdbDetails.release_date?.substring(0, 4);
const imdbId = tmdbDetails.external_ids?.imdb_id || null;
if (!title) throw new Error('Could not get title from TMDB');
console.log(`[MoviesMod] Found metadata: ${title} (${year})`);
const searchResults = await searchMoviesMod(title);
console.log(`[MoviesMod] Found metadata: ${title} (${year})${imdbId ? ` [IMDB: ${imdbId}]` : ''}`);
// Try IMDb ID search first — more reliable than title matching
let searchResults = [];
if (imdbId) {
const imdbQuery = (mediaType === 'tv' && seasonNum) ? `${imdbId} Season ${seasonNum}` : imdbId;
console.log(`[MoviesMod] Trying IMDB ID search: ${imdbQuery}`);
searchResults = await searchMoviesMod(imdbQuery);
if (searchResults.length > 0) {
console.log(`[MoviesMod] Found ${searchResults.length} results via IMDB ID`);
}
}
if (searchResults.length === 0) {
console.log(`[MoviesMod] Falling back to title search: ${title}`);
searchResults = await searchMoviesMod(title);
if (searchResults.length === 0 && mediaType === 'tv' && seasonNum) {
searchResults = await searchMoviesMod(`${title} Season ${seasonNum}`);
}
}
if (searchResults.length === 0) throw new Error(`No search results found for "${title}"`);
// --- NEW: Use string similarity to find the best match ---
// --- Use string similarity to find the best match ---
const titles = searchResults.map(r => r.title);
const bestMatch = findBestMatch(title, titles);
@@ -853,21 +903,27 @@ async function getMoviesModStreams(tmdbId, mediaType, seasonNum = null, episodeN
const finalLinks = await resolveIntermediateLink(link.url, selectedResult.url, link.quality);
if (!finalLinks || finalLinks.length === 0) return null;
// Resolve to driveseed redirect URLs (intermediate step) for caching
// Resolve to host redirect URLs (intermediate step) for caching
const driveseedPromises = finalLinks.map(async (targetLink) => {
try {
let currentUrl = targetLink.url;
// Handle SID links if they appear
if (currentUrl.includes('tech.unblockedgames.world') || currentUrl.includes('tech.creativeexpressionsblog.com') || currentUrl.includes('tech.examzculture.in')) {
// Handle SID links (all known tech-unblock domains)
if (
currentUrl.includes('tech.unblockedgames.world') ||
currentUrl.includes('cloud.unblockedgames.world') ||
currentUrl.includes('tech.creativeexpressionsblog.com') ||
currentUrl.includes('tech.examzculture.in') ||
currentUrl.includes('tech.examdegree.site')
) {
const resolvedUrl = await resolveTechUnblockedLink(currentUrl);
if (!resolvedUrl) return null;
currentUrl = resolvedUrl;
}
// Only process if it's a driveseed URL
if (currentUrl && currentUrl.includes('driveseed.org')) {
console.log(`[MoviesMod] Caching driveseed redirect URL for ${targetLink.server}: ${currentUrl}`);
// Accept driveseed.org or driveleech.net (same operator, same page structure)
if (currentUrl && (currentUrl.includes('driveseed.org') || currentUrl.includes('driveleech.net'))) {
console.log(`[MoviesMod] Resolved host URL for ${targetLink.server}: ${currentUrl}`);
return { ...targetLink, driveseedRedirectUrl: currentUrl };
}
return null;
@@ -932,9 +988,11 @@ async function getMoviesModStreams(tmdbId, mediaType, seasonNum = null, episodeN
const { driveseedRedirectUrl } = targetLink;
if (!driveseedRedirectUrl) return null;
// Process the cached driveseed redirect URL
if (driveseedRedirectUrl.includes('driveseed.org')) {
// First, resolve the driveseed redirect URL to get the final file page URL
// Process the cached host redirect URL (driveseed.org or driveleech.net)
if (driveseedRedirectUrl.includes('driveseed.org') || driveseedRedirectUrl.includes('driveleech.net')) {
const hostOrigin = new URL(driveseedRedirectUrl).origin;
// Resolve the redirect URL to get the final file page URL
const response = await makeRequest(driveseedRedirectUrl, {
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',
@@ -949,7 +1007,8 @@ async function getMoviesModStreams(tmdbId, mediaType, seasonNum = null, episodeN
let $; // single declaration for cheerio instance
if (redirectMatch && redirectMatch[1]) {
const finalPath = redirectMatch[1];
finalFilePageUrl = `https://driveseed.org${finalPath}`;
// Use the dynamic host origin — works for both driveseed.org and driveleech.net
finalFilePageUrl = finalPath.startsWith('http') ? finalPath : `${hostOrigin}${finalPath}`;
console.log(`[MoviesMod] Resolved redirect to final file page: ${finalFilePageUrl}`);
// Load the final file page
@@ -996,7 +1055,7 @@ async function getMoviesModStreams(tmdbId, mediaType, seasonNum = null, episodeN
downloadOptions.push({
title: 'Resume Cloud',
type: 'resume',
url: `https://driveseed.org${resumeCloudLink}`,
url: resumeCloudLink.startsWith('http') ? resumeCloudLink : `${hostOrigin}${resumeCloudLink}`,
priority: 1
});
}
+92
View File
@@ -0,0 +1,92 @@
const axios = require('axios');
const { getTmdbApiKey } = require('../utils/tmdbKey');
const NOTORRENT_API = 'https://addon-osvh.onrender.com';
function cleanText(str) {
if (!str) return '';
return str.replace(/[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]/gu, '').trim();
}
function extractQuality(titleText) {
const raw = titleText || '';
const match = raw.match(/(\d{3,4}p)/);
if (match) return match[0];
if (raw.toUpperCase().includes('FREE')) return 'Auto';
return 'Unknown';
}
async function getNotorrentStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) {
console.log(`[NoTorrent] Searching for ${mediaType} ${tmdbId}`);
const tmdbKey = getTmdbApiKey();
if (!tmdbKey) {
console.error('[NoTorrent] No TMDB API key configured.');
return [];
}
// Step 1: TMDB → IMDB ID
let imdbId;
try {
const type = mediaType === 'tv' ? 'tv' : 'movie';
const { data } = await axios.get(
`https://api.themoviedb.org/3/${type}/${tmdbId}?api_key=${tmdbKey}&append_to_response=external_ids`,
{ timeout: 8000 }
);
imdbId = (data.external_ids && data.external_ids.imdb_id) || null;
} catch (err) {
console.error(`[NoTorrent] TMDB lookup failed: ${err.message}`);
return [];
}
if (!imdbId) {
console.warn('[NoTorrent] Failed to map IMDB ID from TMDB.');
return [];
}
// Step 2: Fetch streams from NoTorrent Stremio addon API
const apiUrl = (mediaType === 'tv' && seasonNum != null)
? `${NOTORRENT_API}/stream/series/${imdbId}:${seasonNum}:${episodeNum}.json`
: `${NOTORRENT_API}/stream/movie/${imdbId}.json`;
try {
const { data } = await axios.get(apiUrl, { timeout: 20000 });
const rawList = data.streams || [];
const streams = [];
for (const item of rawList) {
if (item.externalUrl || !item.url) continue;
if (item.url.includes('github.com') || item.url.includes('googleusercontent')) continue;
const cleanTitleStr = cleanText(item.title || '');
const quality = extractQuality(cleanTitleStr);
let language = 'Default';
const langMatch = cleanTitleStr.match(/\(([^)]+)\)/);
if (langMatch) {
language = langMatch[1].charAt(0).toUpperCase() + langMatch[1].slice(1).toLowerCase();
}
const proxyHeaders = (item.behaviorHints?.proxyHeaders?.request) || {};
const headers = { ...(item.behaviorHints?.headers || {}), ...proxyHeaders };
const nameParts = ['NoTorrent', language !== 'Default' ? language : ''].filter(p => p.trim() !== '');
streams.push({
name: nameParts.join(' \u2022 '),
title: quality,
url: item.url,
quality,
provider: 'NoTorrent',
headers: Object.keys(headers).length > 0 ? headers : undefined
});
}
console.log(`[NoTorrent] Total results found: ${streams.length}`);
return streams;
} catch (err) {
console.error(`[NoTorrent] Fetch failed: ${err.message}`);
return [];
}
}
module.exports = { getNotorrentStreams };
+23 -61
View File
@@ -10,12 +10,13 @@ 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',
'videasy.js': 'getVideasyStreams',
'vidlink.js': 'getVidlinkStreams',
'lordflix.js': 'getLordflixStreams',
'notorrent.js': 'getNotorrentStreams',
'dahmermovies.js': 'getDahmermoviesStreams',
};
// Stats for debug endpoint
@@ -29,7 +30,7 @@ async function getEffectiveCookies() {
function getProviderFiles() {
const providersDir = path.join(__dirname);
return fs.readdirSync(providersDir)
.filter(file => file.endsWith('.js') && file !== 'registry.js' && file !== 'vidsrcextractor.js')
.filter(file => file.endsWith('.js') && file !== 'registry.js')
.map(file => ({
name: path.parse(file).name.toLowerCase(),
file: file,
@@ -92,46 +93,6 @@ function createFetchFunction(providerInfo) {
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);
@@ -152,28 +113,29 @@ function createFetchFunction(providerInfo) {
};
}
// Initialize providers
// Initialize providers - always load all; enabled state is checked dynamically from live config
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] ${providerInfo.name} disabled via config`);
}
providers.push({
name: providerInfo.name,
fetch: createFetchFunction(providerInfo)
});
console.log(`[registry] ${providerInfo.name} provider loaded`);
}
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 isProviderEnabled(name) {
const flag = `enable${name.charAt(0).toUpperCase() + name.slice(1)}Provider`;
return config[flag] !== false;
}
function listProviders() { return providers.map(p => ({ name: p.name, enabled: isProviderEnabled(p.name) })); }
function getProvider(name) {
const p = providers.find(p => p.name === name.toLowerCase());
if (!p) return null;
return { ...p, enabled: isProviderEnabled(p.name) };
}
function getCookieStats() { return lastCookieStats; }
File diff suppressed because it is too large Load Diff
+116
View File
@@ -0,0 +1,116 @@
const axios = require('axios');
const { getTmdbApiKey } = require('../utils/tmdbKey');
const VIDEASY_HEADERS = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
'Accept': 'application/json, text/plain, */*',
'Origin': 'https://player.videasy.net',
'Referer': 'https://player.videasy.net/'
};
// Each server maps a name to its API URL. moviesOnly:true skips TV requests.
const SERVERS = {
'Neon': { url: 'https://api.videasy.net/myflixerzupcloud/sources-with-title' },
'Yoru': { url: 'https://api.videasy.net/cdn/sources-with-title', moviesOnly: true },
'Cypher': { url: 'https://api.videasy.net/moviebox/sources-with-title' },
'Reyna': { url: 'https://api.videasy.net/primewire/sources-with-title' },
'Omen': { url: 'https://api.videasy.net/onionplay/sources-with-title' },
'Breach': { url: 'https://api.videasy.net/m4uhd/sources-with-title' },
'Ghost': { url: 'https://api.videasy.net/primesrcme/sources-with-title' },
'Sage': { url: 'https://api.videasy.net/1movies/sources-with-title' },
'Vyse': { url: 'https://api.videasy.net/hdmovie/sources-with-title' },
'Raze': { url: 'https://api.videasy.net/superflix/sources-with-title' }
};
const DECRYPT_URL = 'https://enc-dec.app/api/dec-videasy';
async function getVideasyStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) {
console.log(`[Videasy] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}`);
const tmdbKey = getTmdbApiKey();
if (!tmdbKey) {
console.error('[Videasy] No TMDB API key configured.');
return [];
}
// Step 1: Resolve title/year/imdbId from TMDB
let details;
try {
const type = mediaType === 'tv' ? 'tv' : 'movie';
const { data } = await axios.get(
`https://api.themoviedb.org/3/${type}/${tmdbId}?api_key=${tmdbKey}&append_to_response=external_ids`,
{ timeout: 8000 }
);
details = {
title: data.title || data.name || '',
year: (data.release_date || data.first_air_date || '').split('-')[0],
imdbId: (data.external_ids && data.external_ids.imdb_id) || '',
type
};
} catch (err) {
console.error(`[Videasy] TMDB lookup failed: ${err.message}`);
return [];
}
if (!details.title) {
console.error('[Videasy] No title returned from TMDB.');
return [];
}
const allStreams = [];
const seen = new Set();
// Step 2: Query each server in parallel
await Promise.all(Object.entries(SERVERS).map(async ([name, server]) => {
if (server.moviesOnly && mediaType === 'tv') return;
let apiUrl = `${server.url}?title=${encodeURIComponent(details.title)}`
+ `&mediaType=${details.type}&year=${details.year}`
+ `&tmdbId=${tmdbId}&imdbId=${details.imdbId || ''}`;
if (mediaType === 'tv') apiUrl += `&seasonId=${seasonNum}&episodeId=${episodeNum}`;
try {
const encRes = await axios.get(apiUrl, {
headers: VIDEASY_HEADERS,
timeout: 8000,
responseType: 'text'
});
const encryptedText = typeof encRes.data === 'string' ? encRes.data : JSON.stringify(encRes.data);
if (!encryptedText || encryptedText.length < 20 || encryptedText.startsWith('<')) return;
// Step 3: Decrypt via enc-dec.app
const decRes = await axios.post(DECRYPT_URL,
{ text: encryptedText, id: String(tmdbId) },
{ headers: { 'Content-Type': 'application/json' }, timeout: 8000 }
);
const resData = (decRes.data && decRes.data.result) || decRes.data;
if (!resData || !Array.isArray(resData.sources)) return;
for (const s of resData.sources) {
if (!s.url || seen.has(s.url)) continue;
seen.add(s.url);
allStreams.push({
name: `Videasy ${name}`,
title: `Videasy ${name} - ${s.quality || 'Auto'}`,
url: s.url,
quality: s.quality || 'Auto',
provider: 'Videasy',
headers: {
'Referer': 'https://player.videasy.net/',
'Origin': 'https://player.videasy.net'
}
});
}
console.log(`[Videasy] Server ${name}: ${resData.sources.length} source(s)`);
} catch {
// server unreachable or returned no data — skip silently
}
}));
console.log(`[Videasy] Total streams: ${allStreams.length}`);
return allStreams;
}
module.exports = { getVideasyStreams };
+51
View File
@@ -0,0 +1,51 @@
const axios = require('axios');
const VIDLINK_HEADERS = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
'Referer': 'https://vidlink.pro'
};
async function getVidlinkStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) {
console.log(`[Vidlink] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}`);
try {
// Step 1: Encrypt the TMDB ID via enc-dec.app
const encRes = await axios.get(
`https://enc-dec.app/api/enc-vidlink?text=${encodeURIComponent(String(tmdbId))}`,
{ timeout: 8000 }
);
const encodedTmdb = encRes.data && encRes.data.result;
if (!encodedTmdb) {
console.log('[Vidlink] Encryption step returned no result.');
return [];
}
// Step 2: Fetch stream playlist from Vidlink API
const apiUrl = mediaType === 'tv'
? `https://vidlink.pro/api/b/tv/${encodedTmdb}/${seasonNum}/${episodeNum}?multiLang=0`
: `https://vidlink.pro/api/b/movie/${encodedTmdb}?multiLang=0`;
const apiRes = await axios.get(apiUrl, { headers: VIDLINK_HEADERS, timeout: 8000 });
const playlist = apiRes.data && apiRes.data.stream && apiRes.data.stream.playlist;
if (!playlist) {
console.log('[Vidlink] No playlist URL in response.');
return [];
}
console.log(`[Vidlink] Got stream.`);
return [{
name: 'Vidlink',
title: 'Vidlink',
url: playlist,
quality: 'Auto',
provider: 'Vidlink',
headers: { 'Referer': 'https://vidlink.pro' }
}];
} catch (err) {
console.error(`[Vidlink] Error: ${err.message}`);
return [];
}
}
module.exports = { getVidlinkStreams };
-411
View File
@@ -1,411 +0,0 @@
#!/usr/bin/env node
const cheerio = require('cheerio');
// --- Constants ---
let BASEDOM = "https://cloudnestra.com"; // This can be updated by serversLoad
const SOURCE_URL = "https://vidsrc.xyz/embed";
// --- Helper: Direct Fetch ---
async function fetchWrapper(url, options) {
console.log(`[VidSrc Direct] Fetching: ${url}`);
return fetch(url, options);
}
// --- Helper Functions (copied and adapted from src/extractor.ts) ---
async function serversLoad(html) {
const $ = cheerio.load(html);
const servers = [];
const title = $("title").text() ?? "";
const baseFrameSrc = $("iframe").attr("src") ?? "";
if (baseFrameSrc) {
try {
const fullUrl = baseFrameSrc.startsWith("//") ? "https:" + baseFrameSrc : baseFrameSrc;
BASEDOM = new URL(fullUrl).origin;
}
catch (e) {
console.warn(`(Attempt 1) Failed to parse base URL from iframe src: ${baseFrameSrc} using new URL(), error: ${e.message}`);
// Attempt 2: Regex fallback for origin
const originMatch = (baseFrameSrc.startsWith("//") ? "https:" + baseFrameSrc : baseFrameSrc).match(/^(https?:\/\/[^/]+)/);
if (originMatch && originMatch[1]) {
BASEDOM = originMatch[1];
console.log(`(Attempt 2) Successfully extracted origin using regex: ${BASEDOM}`);
} else {
console.error(`(Attempt 2) Failed to extract origin using regex from: ${baseFrameSrc}. Using default: ${BASEDOM}`);
// Keep the default BASEDOM = "https://cloudnestra.com" if all fails
}
}
}
$(".serversList .server").each((index, element) => {
const server = $(element);
servers.push({
name: server.text().trim(),
dataHash: server.attr("data-hash") ?? null,
});
});
return {
servers: servers,
title: title,
};
}
async function parseMasterM3U8(m3u8Content, masterM3U8Url) {
const lines = m3u8Content.split('\n').map(line => line.trim());
const streams = [];
for (let i = 0; i < lines.length; i++) {
if (lines[i].startsWith("#EXT-X-STREAM-INF:")) {
const infoLine = lines[i];
let quality = "unknown";
const resolutionMatch = infoLine.match(/RESOLUTION=(\d+x\d+)/);
if (resolutionMatch && resolutionMatch[1]) {
quality = resolutionMatch[1];
}
else {
const bandwidthMatch = infoLine.match(/BANDWIDTH=(\d+)/);
if (bandwidthMatch && bandwidthMatch[1]) {
quality = `${Math.round(parseInt(bandwidthMatch[1]) / 1000)}kbps`;
}
}
if (i + 1 < lines.length && lines[i + 1] && !lines[i + 1].startsWith("#")) {
const streamUrlPart = lines[i + 1];
try {
const fullStreamUrl = new URL(streamUrlPart, masterM3U8Url).href;
streams.push({ quality: quality, url: fullStreamUrl });
}
catch (e) {
console.error(`Error constructing URL for stream part: ${streamUrlPart} with base: ${masterM3U8Url}`, e);
streams.push({ quality: quality, url: streamUrlPart }); // Store partial URL as a fallback
}
i++;
}
}
}
// Sort streams by quality (highest first)
streams.sort((a, b) => {
// Extract resolution height from quality (e.g., "1280x720" -> 720)
const getHeight = (quality) => {
const match = quality.match(/(\d+)x(\d+)/);
return match ? parseInt(match[2], 10) : 0;
};
const heightA = getHeight(a.quality);
const heightB = getHeight(b.quality);
// Higher resolution comes first
return heightB - heightA;
});
return streams;
}
async function PRORCPhandler(prorcp) {
try {
const prorcpUrl = `${BASEDOM}/prorcp/${prorcp}`;
const prorcpFetch = await fetchWrapper(prorcpUrl, {
headers: {
"accept": "*/*", "accept-language": "en-US,en;q=0.9", "priority": "u=1",
"sec-ch-ua": "\"Chromium\";v=\"128\", \"Not;A=Brand\";v=\"24\", \"Google Chrome\";v=\"128\"",
"sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": "\"Windows\"",
"sec-fetch-dest": "script", "sec-fetch-mode": "no-cors", "sec-fetch-site": "same-origin",
'Sec-Fetch-Dest': 'iframe', "Referer": `${BASEDOM}/`, "Referrer-Policy": "origin",
},
timeout: 10000
});
if (!prorcpFetch.ok) {
console.error(`Failed to fetch prorcp: ${prorcpUrl}, status: ${prorcpFetch.status}`);
return null;
}
const prorcpResponse = await prorcpFetch.text();
const regex = /file:\s*'([^']*)'/gm;
const match = regex.exec(prorcpResponse);
if (match && match[1]) {
const masterM3U8Url = match[1];
const m3u8FileFetch = await fetchWrapper(masterM3U8Url, {
headers: { "Referer": prorcpUrl, "Accept": "*/*" },
timeout: 10000
});
if (!m3u8FileFetch.ok) {
console.error(`Failed to fetch master M3U8: ${masterM3U8Url}, status: ${m3u8FileFetch.status}`);
return null;
}
const m3u8Content = await m3u8FileFetch.text();
return parseMasterM3U8(m3u8Content, masterM3U8Url);
}
console.warn("No master M3U8 URL found in prorcp response for:", prorcpUrl);
return null;
}
catch (error) {
console.error(`Error in PRORCPhandler for ${BASEDOM}/prorcp/${prorcp}:`, error);
return null;
}
}
async function SRCRCPhandler(srcrcpPath, refererForSrcrcp) {
try {
const srcrcpUrl = BASEDOM + srcrcpPath;
console.log(`[VidSrc - SRCRCP] Fetching: ${srcrcpUrl} (Referer: ${refererForSrcrcp})`);
const response = await fetchWrapper(srcrcpUrl, {
headers: {
"accept": "*/*",
"accept-language": "en-US,en;q=0.9",
"sec-ch-ua": "\"Chromium\";v=\"128\", \"Not;A=Brand\";v=\"24\", \"Google Chrome\";v=\"128\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"Windows\"",
"sec-fetch-dest": "iframe",
"sec-fetch-mode": "navigate",
"sec-fetch-site": "same-origin", // This might need to be 'cross-site' if BASEDOM's origin differs from srcrcp link's origin
"Referer": refererForSrcrcp,
"Referrer-Policy": "origin",
},
timeout: 10000
});
if (!response.ok) {
console.error(`[VidSrc - SRCRCP] Failed to fetch ${srcrcpUrl}, status: ${response.status}`);
return null;
}
const responseText = await response.text();
console.log(`[VidSrc - SRCRCP] Response from ${srcrcpUrl} (first 500 chars): ${responseText.substring(0, 500)}`);
// Attempt 1: Check for "file: '...'" like in PRORCP
const fileRegex = /file:\s*'([^']*)'/gm;
const fileMatch = fileRegex.exec(responseText);
if (fileMatch && fileMatch[1]) {
const masterM3U8Url = fileMatch[1];
console.log(`[VidSrc - SRCRCP] Found M3U8 URL (via fileMatch): ${masterM3U8Url}`);
const m3u8FileFetch = await fetchWrapper(masterM3U8Url, {
headers: { "Referer": srcrcpUrl, "Accept": "*/*" },
timeout: 10000
});
if (!m3u8FileFetch.ok) {
console.error(`[VidSrc - SRCRCP] Failed to fetch master M3U8: ${masterM3U8Url}, status: ${m3u8FileFetch.status}`);
return null;
}
const m3u8Content = await m3u8FileFetch.text();
return parseMasterM3U8(m3u8Content, masterM3U8Url);
}
// Attempt 2: Check if the responseText itself is an M3U8 playlist
if (responseText.trim().startsWith("#EXTM3U")) {
console.log(`[VidSrc - SRCRCP] Response from ${srcrcpUrl} appears to be an M3U8 playlist directly.`);
return parseMasterM3U8(responseText, srcrcpUrl);
}
// Attempt 3: Look for sources = [...] or sources: [...] in script tags or in JSON-like structures
const $ = cheerio.load(responseText);
let sourcesFound = null;
$('script').each((i, script) => {
const scriptContent = $(script).html();
if (scriptContent) {
// Regex for various ways sources might be defined
const sourcesRegexes = [
/sources\s*[:=]\s*(\[[^\]]*\{(?:\s*|.*?)file\s*:\s*['"]([^'"]+)['"](?:\s*|.*?)\}[^\]]*\])/si, // extracts the URL from sources: [{file: "URL"}]
/playerInstance\.setup\s*\(\s*\{\s*sources\s*:\s*(\[[^\]]*\{(?:\s*|.*?)file\s*:\s*['"]([^'"]+)['"](?:\s*|.*?)\}[^\]]*\])/si, // for playerInstance.setup({sources: [{file: "URL"}]})
/file\s*:\s*['"]([^'"]+\.m3u8[^'"]*)['"]/i, // Direct M3U8 link in a var or object e.g. file: "URL.m3u8"
/src\s*:\s*['"]([^'"]+\.m3u8[^'"]*)['"]/i, // Direct M3U8 link e.g. src: "URL.m3u8"
/loadSource\(['"]([^'"]+\.m3u8[^'"]*)['"]\)/i, // For .loadSource("URL.m3u8")
/new\s+Player\([^)]*\{\s*src\s*:\s*['"]([^'"]+)['"]\s*\}\s*\)/i // For new Player({src: "URL"})
];
for (const regex of sourcesRegexes) {
const sourcesMatch = scriptContent.match(regex);
// For regexes that capture a JSON array-like structure in group 1 and then the URL in group 2 (first two regexes)
if (regex.source.includes('file') && regex.source.includes('sources')) { // Heuristic to identify complex sources regex capturing group 2
if (sourcesMatch && sourcesMatch[2]) { // URL is in group 2
console.log(`[VidSrc - SRCRCP] Found M3U8 URL (script complex): ${sourcesMatch[2]}`);
sourcesFound = [{ quality: 'default', url: sourcesMatch[2] }];
return false; // break cheerio loop
}
}
// For simpler regexes where URL is in group 1
else if (sourcesMatch && sourcesMatch[1]) {
console.log(`[VidSrc - SRCRCP] Found M3U8 URL (script simple): ${sourcesMatch[1]}`);
sourcesFound = [{ quality: 'default', url: sourcesMatch[1] }];
return false; // break cheerio loop
}
}
// Fallback: Look for any absolute .m3u8 URL within the script tag
if (!sourcesFound) {
const m3u8GenericMatch = scriptContent.match(/['"](https?:\/\/[^'"\s]+\.m3u8[^'"\s]*)['"]/i);
if (m3u8GenericMatch && m3u8GenericMatch[1]) {
console.log(`[VidSrc - SRCRCP] Found M3U8 URL (script generic fallback): ${m3u8GenericMatch[1]}`);
sourcesFound = [{ quality: 'default', url: m3u8GenericMatch[1] }];
return false; // break cheerio loop
}
}
}
});
if (sourcesFound && sourcesFound.length > 0) {
// Process the first valid M3U8 URL found, or return direct links
const m3u8Source = sourcesFound.find(s => s.url && s.url.includes('.m3u8'));
if (m3u8Source) {
console.log(`[VidSrc - SRCRCP] First M3U8 source from script: ${m3u8Source.url}`);
// Ensure URL is absolute
const absoluteM3u8Url = m3u8Source.url.startsWith('http') ? m3u8Source.url : new URL(m3u8Source.url, srcrcpUrl).href;
const m3u8FileFetch = await fetchWrapper(absoluteM3u8Url, {
headers: { "Referer": srcrcpUrl, "Accept": "*/*" },
timeout: 10000
});
if (!m3u8FileFetch.ok) {
console.error(`[VidSrc - SRCRCP] Failed to fetch M3U8 from script source: ${absoluteM3u8Url}, status: ${m3u8FileFetch.status}`);
return null;
}
const m3u8Content = await m3u8FileFetch.text();
return parseMasterM3U8(m3u8Content, absoluteM3u8Url);
} else {
// Assuming direct links if no .m3u8 found in the sources array
console.log(`[VidSrc - SRCRCP] Assuming direct links from script sources:`, sourcesFound);
return sourcesFound.map(s => ({
quality: s.quality || s.label || 'auto',
url: s.url.startsWith('http') ? s.url : new URL(s.url, srcrcpUrl).href
}));
}
}
console.warn(`[VidSrc - SRCRCP] No stream extraction method succeeded for ${srcrcpUrl}`);
return null;
} catch (error) {
console.error(`[VidSrc - SRCRCP] Error in SRCRCPhandler for ${srcrcpPath}:`, error);
return null;
}
}
async function rcpGrabber(html) {
const regex = /src:\s*'([^']*)'/;
const match = html.match(regex);
if (!match || !match[1])
return null;
return { metadata: { image: "" }, data: match[1] };
}
function getObject(id) {
const arr = id.split(':');
return { id: arr[0], season: arr[1], episode: arr[2] };
}
function getUrl(id, type) {
if (type === "movie") {
return `${SOURCE_URL}/movie/${id}`;
}
else {
const obj = getObject(id);
return `${SOURCE_URL}/tv/${obj.id}/${obj.season}-${obj.episode}`;
}
}
async function getStreamContent(id, type) {
const url = getUrl(id, type);
const embedRes = await fetchWrapper(url, { headers: { "Referer": SOURCE_URL } });
if (!embedRes.ok) {
console.error(`Failed to fetch embed page ${url}: ${embedRes.status}`);
return [];
}
const embedResp = await embedRes.text();
const { servers, title } = await serversLoad(embedResp);
const apiResponse = [];
// MODIFIED: Process servers in parallel
const serverPromises = servers.map(async (server) => {
if (!server.dataHash) return null; // Skip servers without dataHash
try {
const rcpUrl = `${BASEDOM}/rcp/${server.dataHash}`;
const rcpRes = await fetchWrapper(rcpUrl, {
headers: { 'Sec-Fetch-Dest': 'iframe', "Referer": url }
});
if (!rcpRes.ok) {
console.warn(`RCP fetch failed for server ${server.name}: ${rcpRes.status}`);
return null; // Failed to fetch RCP
}
const rcpHtml = await rcpRes.text();
const rcpData = await rcpGrabber(rcpHtml);
if (!rcpData || !rcpData.data) {
console.warn(`Skipping server ${server.name} due to missing rcp data.`);
return null; // Missing RCP data
}
let streamDetails = null;
if (rcpData.data.startsWith("/prorcp/")) {
streamDetails = await PRORCPhandler(rcpData.data.replace("/prorcp/", ""));
} else if (rcpData.data.startsWith("/srcrcp/")) {
if (server.name === "Superembed" || server.name === "2Embed") {
console.warn(`[VidSrc] Skipping SRCRCP for known problematic server: ${server.name}`);
return null;
}
streamDetails = await SRCRCPhandler(rcpData.data, rcpUrl); // Pass rcpUrl as referer
} else {
console.warn(`Unhandled rcp data type for server ${server.name}: ${rcpData.data.substring(0, 50)}`);
return null; // Unhandled type
}
if (streamDetails && streamDetails.length > 0) {
return {
name: title,
image: rcpData.metadata.image,
mediaId: id,
streams: streamDetails,
referer: BASEDOM,
};
} else {
console.warn(`No stream details from handler for server ${server.name} (${rcpData.data})`);
return null; // No stream details found by the handler
}
} catch (e) {
console.error(`Error processing server ${server.name} (${server.dataHash}):`, e);
return null; // Error during server processing
}
});
const results = await Promise.all(serverPromises);
// Filter out null results (failed servers or no streams) and add valid results to apiResponse
results.forEach(result => {
if (result) {
apiResponse.push(result);
}
});
// END MODIFICATION
return apiResponse;
}
// --- Main execution logic (conditional) ---
async function main() {
const args = process.argv.slice(2);
if (args.length < 2) {
console.error("Usage: node vidsrcextractor.js <id> <type>");
console.error("Example (movie): node vidsrcextractor.js tt0111161 movie");
console.error("Example (series): node vidsrcextractor.js tt0944947:1:1 series"); // Game of Thrones S1E1
return 1; // error code
}
const id = args[0];
const type = args[1];
if (type !== "movie" && type !== "series") {
console.error("Invalid type. Must be 'movie' or 'series'.");
return 1;
}
if (type === "series" && id.split(':').length < 3) {
console.error("Invalid series ID format. Expected 'ttID:season:episode' (e.g., tt0944947:1:1).");
return 1;
}
console.log(`Fetching streams for ID: ${id}, Type: ${type}`);
try {
const results = await getStreamContent(id, type);
if (results && results.length > 0) {
console.log("Extracted Data:");
console.log(JSON.stringify(results, null, 2));
} else {
console.log("No streams found or an error occurred.");
}
return 0;
} catch (error) {
console.error("Error during extraction process:", error);
return 1;
}
}
// Export the function for use in other modules
module.exports = { getStreamContent };
// Run main only if the script is executed directly
if (require.main === module) {
main()
.then(code => {
if (code !== 0) {
console.error(`[vidsrcextractor] Finished with non-zero code ${code}`);
}
})
.catch(error => {
console.error("Unhandled error in main execution:", error);
});
}
+160 -195
View File
@@ -1,215 +1,180 @@
/**
* Vixsrc streaming provider integration (standalone)
* React Native compatible variant - no external addon framework
*/
const axios = require('axios');
// Constants
const TMDB_API_KEY = "68e094699525b18a70bab2f86b1fa706";
const BASE_URL = 'https://vixsrc.to';
const VIXSRC_HEADERS = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150 Safari/537.36',
'Accept': 'application/json, text/javascript, */*; q=0.01',
'Accept-Language': 'en-US,en;q=0.9',
'Referer': BASE_URL,
'Origin': BASE_URL
};
// Helper function to make HTTP requests with default headers
function makeRequest(url, options = {}) {
const defaultHeaders = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept': 'application/json,*/*',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate',
'Connection': 'keep-alive',
...options.headers
};
return fetch(url, {
method: options.method || 'GET',
headers: defaultHeaders,
...options
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response;
})
.catch(error => {
console.error(`[Vixsrc] Request failed for ${url}: ${error.message}`);
throw error;
});
// Step 1: GET /api/movie/{id} or /api/tv/{id}/{s}/{e} → { src: "/embed/..." }
async function fetchApi(url) {
try {
const response = await axios.get(url, { headers: VIXSRC_HEADERS, timeout: 10000 });
if (response.status !== 200 || !response.data) return null;
return response.data;
} catch {
return null;
}
}
// Helper function to get TMDB info
function getTmdbInfo(tmdbId, mediaType) {
const url = `https://api.themoviedb.org/3/${mediaType === 'tv' ? 'tv' : 'movie'}/${tmdbId}?api_key=${TMDB_API_KEY}`;
return makeRequest(url)
.then(response => response.json())
.then(data => ({ data }));
// Step 2: GET BASE_URL + sublink.src → HTML embed page
async function fetchEmbedPage(suburl) {
try {
const response = await axios.get(BASE_URL + suburl, {
headers: { ...VIXSRC_HEADERS, Accept: 'text/html,application/xhtml+xml,*/*' },
timeout: 10000,
responseType: 'text'
});
if (response.status !== 200) return null;
return response.data;
} catch {
return null;
}
}
// Helper function to parse M3U8 playlist
// Removed obsolete parseM3U8Playlist implementation
// Step 3: Extract token, expires, playlist URL from embed HTML
function extractTokenData(html) {
const token = html.match(/token["']\s*:\s*["']([^"']+)/)?.[1];
const expires = html.match(/expires["']\s*:\s*["']([^"']+)/)?.[1];
const playlist = html.match(/url\s*:\s*["']([^"']+)/)?.[1];
// Removed unused helpers getQualityFromResolution and resolveUrl
if (!token || !expires || !playlist) return null;
// Helper function to extract stream URL from Vixsrc page
function extractStreamFromPage(url, contentType, contentId, seasonNum, episodeNum) {
let vixsrcUrl;
let subtitleApiUrl;
if (contentType === 'movie') {
vixsrcUrl = `${BASE_URL}/movie/${contentId}`;
subtitleApiUrl = `https://sub.wyzie.ru/search?id=${contentId}`;
} else {
vixsrcUrl = `${BASE_URL}/tv/${contentId}/${seasonNum}/${episodeNum}`;
subtitleApiUrl = `https://sub.wyzie.ru/search?id=${contentId}&season=${seasonNum}&episode=${episodeNum}`;
// Reject expired tokens (with 60s grace period)
if (parseInt(expires, 10) * 1000 - 60_000 < Date.now()) {
console.log('[Vixsrc] Token is expired');
return null;
}
console.log(`[Vixsrc] Fetching: ${vixsrcUrl}`);
return makeRequest(vixsrcUrl, {
headers: {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
}
})
.then(response => response.text())
.then(html => {
console.log(`[Vixsrc] HTML length: ${html.length} characters`);
let masterPlaylistUrl = null;
// Method 1: Look for window.masterPlaylist (primary method)
if (html.includes('window.masterPlaylist')) {
console.log('[Vixsrc] Found window.masterPlaylist');
const urlMatch = html.match(/url:\s*['"]([^'"]+)['"]/);
const tokenMatch = html.match(/['"]?token['"]?\s*:\s*['"]([^'"]+)['"]/);
const expiresMatch = html.match(/['"]?expires['"]?\s*:\s*['"]([^'"]+)['"]/);
if (urlMatch && tokenMatch && expiresMatch) {
const baseUrl = urlMatch[1];
const token = tokenMatch[1];
const expires = expiresMatch[1];
console.log('[Vixsrc] Extracted tokens:');
console.log(` - Base URL: ${baseUrl}`);
console.log(` - Token: ${token.substring(0, 20)}...`);
console.log(` - Expires: ${expires}`);
// Construct the master playlist URL
if (baseUrl.includes('?b=1')) {
masterPlaylistUrl = `${baseUrl}&token=${token}&expires=${expires}&h=1&lang=en`;
} else {
masterPlaylistUrl = `${baseUrl}?token=${token}&expires=${expires}&h=1&lang=en`;
}
console.log(`[Vixsrc] Constructed master playlist URL: ${masterPlaylistUrl}`);
}
}
// Method 2: Look for direct .m3u8 URLs
if (!masterPlaylistUrl) {
const m3u8Match = html.match(/(https?:\/\/[^'"\s]+\.m3u8[^'"\s]*)/);
if (m3u8Match) {
masterPlaylistUrl = m3u8Match[1];
console.log('[Vixsrc] Found direct .m3u8 URL:', masterPlaylistUrl);
}
}
// Method 3: Look for stream URLs in script tags
if (!masterPlaylistUrl) {
const scriptMatches = html.match(/<script[^>]*>(.*?)<\/script>/gs);
if (scriptMatches) {
for (const script of scriptMatches) {
const streamMatch = script.match(/['"]?(https?:\/\/[^'"\s]+(?:\.m3u8|playlist)[^'"\s]*)/);
if (streamMatch) {
masterPlaylistUrl = streamMatch[1];
console.log('[Vixsrc] Found stream in script:', masterPlaylistUrl);
break;
}
}
}
}
if (!masterPlaylistUrl) {
console.log('[Vixsrc] No master playlist URL found');
return null;
}
return { masterPlaylistUrl, subtitleApiUrl };
});
return { token, expires, playlist };
}
// Helper function to get subtitles
function getSubtitles(subtitleApiUrl) {
return makeRequest(subtitleApiUrl)
.then(response => response.json())
.then(subtitleData => {
// Find English subtitle track (same logic as original)
let subtitleTrack = subtitleData.find(track =>
track.display.includes('English') && (track.encoding === 'ASCII' || track.encoding === 'UTF-8')
);
if (!subtitleTrack) {
subtitleTrack = subtitleData.find(track => track.display.includes('English') && track.encoding === 'CP1252');
}
if (!subtitleTrack) {
subtitleTrack = subtitleData.find(track => track.display.includes('English') && track.encoding === 'CP1250');
}
if (!subtitleTrack) {
subtitleTrack = subtitleData.find(track => track.display.includes('English') && track.encoding === 'CP850');
}
const subtitles = subtitleTrack ? subtitleTrack.url : '';
console.log(subtitles ? `[Vixsrc] Found subtitles: ${subtitles}` : '[Vixsrc] No English subtitles found');
return subtitles;
})
.catch(error => {
console.log('[Vixsrc] Subtitle fetch failed:', error.message);
return '';
});
// Step 4: Append token params to master URL
function buildMasterUrl(tokenData) {
const { token, expires, playlist } = tokenData;
const sep = playlist.includes('?') ? '&' : '?';
return `${playlist}${sep}token=${token}&expires=${expires}&h=1`;
}
// Main function to get streams
function getVixsrcStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) {
console.log(`[Vixsrc] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}`);
return getTmdbInfo(tmdbId, mediaType)
.then(() => extractStreamFromPage(null, mediaType, tmdbId, seasonNum, episodeNum))
.then(streamData => {
if (!streamData) {
console.log('[Vixsrc] No stream data found');
return [];
}
const { masterPlaylistUrl, subtitleApiUrl } = streamData;
// Return single master playlist with Auto quality
console.log('[Vixsrc] Returning master playlist with Auto quality...');
// Get subtitles
return getSubtitles(subtitleApiUrl)
.then(subtitles => {
// Return single stream with master playlist
const tmdbStreams = [{
name: "Vixsrc",
title: "Auto Quality Stream",
url: masterPlaylistUrl,
quality: '1080p',
provider: 'Vidsrc',
headers: {
'Referer': BASE_URL,
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
}
}];
console.log('[Vixsrc] Successfully processed 1 stream with Auto quality');
return tmdbStreams;
// Step 5: Fetch the master HLS playlist
async function fetchPlaylist(masterUrl, pageApiUrl) {
try {
const response = await axios.get(masterUrl, {
headers: { ...VIXSRC_HEADERS, Referer: pageApiUrl },
timeout: 10000,
responseType: 'text'
});
})
.catch(error => {
console.error(`[Vixsrc] Error in getVixsrcStreams: ${error.message}`);
return [];
if (response.status !== 200) return null;
return response.data;
} catch {
return null;
}
}
// Step 6: Parse HLS manifest for quality variants, audio tracks, subtitles
function parsePlaylist(content, masterUrl, pageApiUrl) {
const sources = [];
const subtitles = [];
const audioTracks = [];
const lines = content.split('\n');
// Audio tracks
for (const line of lines) {
if (!line.startsWith('#EXT-X-MEDIA:TYPE=AUDIO')) continue;
const language = line.match(/LANGUAGE="([^"]+)"/)?.[1] ?? 'unknown';
const label = line.match(/NAME="([^"]+)"/)?.[1] ?? 'Audio';
audioTracks.push({ language, label });
}
// Subtitles
for (const line of lines) {
if (!line.startsWith('#EXT-X-MEDIA:TYPE=SUBTITLES')) continue;
const url = line.match(/URI="([^"]+)"/)?.[1];
if (!url) continue;
const label = line.match(/NAME="([^"]+)"/)?.[1] ?? 'unknown';
subtitles.push({ url, label, format: 'vtt' });
}
// Quality variants — find the highest resolution
const variantRegex = /#EXT-X-STREAM-INF:[^\n]*RESOLUTION=\d+x(\d+)[^\n]*\n([^\n]+)/g;
let match;
let bestResolution = 0;
while ((match = variantRegex.exec(content)) !== null) {
const res = parseInt(match[1], 10);
if (res > bestResolution) bestResolution = res;
}
if (bestResolution === 0) return { sources: [], subtitles: [] };
sources.push({
name: `Vixsrc - ${bestResolution}p`,
title: `Vixsrc - ${bestResolution}p`,
url: masterUrl,
quality: `${bestResolution}p`,
provider: 'Vixsrc',
headers: {
'Referer': pageApiUrl,
'User-Agent': VIXSRC_HEADERS['User-Agent']
}
});
return { sources, subtitles };
}
async function getVixsrcStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) {
console.log(`[Vixsrc] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}`);
// Build API URL
let apiUrl;
if (mediaType === 'movie') {
apiUrl = `${BASE_URL}/api/movie/${tmdbId}`;
} else {
apiUrl = `${BASE_URL}/api/tv/${tmdbId}/${seasonNum}/${episodeNum}`;
}
console.log(`[Vixsrc] Step 1 - Calling API: ${apiUrl}`);
const apiData = await fetchApi(apiUrl);
if (!apiData || !apiData.src) {
console.log('[Vixsrc] No src returned from API');
return [];
}
console.log(`[Vixsrc] Step 2 - Fetching embed page: ${BASE_URL}${apiData.src}`);
const html = await fetchEmbedPage(apiData.src);
if (!html) {
console.log('[Vixsrc] Failed to fetch embed page');
return [];
}
console.log(`[Vixsrc] Embed HTML length: ${html.length} characters`);
const tokenData = extractTokenData(html);
if (!tokenData) {
console.log('[Vixsrc] Could not extract token/expires/playlist from embed HTML');
return [];
}
const masterUrl = buildMasterUrl(tokenData);
console.log(`[Vixsrc] Step 3 - Master URL: ${masterUrl}`);
const playlistContent = await fetchPlaylist(masterUrl, apiUrl);
if (!playlistContent) {
console.log('[Vixsrc] Failed to fetch HLS playlist');
return [];
}
const { sources, subtitles } = parsePlaylist(playlistContent, masterUrl, apiUrl);
if (sources.length === 0) {
console.log('[Vixsrc] No streams found in HLS playlist');
return [];
}
console.log(`[Vixsrc] Successfully extracted ${sources.length} stream(s). Subtitles: ${subtitles.length}`);
return sources;
}
module.exports = { getVixsrcStreams };
-158
View File
@@ -1,158 +0,0 @@
// xprime.js (simplified & cleaned)
require('dotenv').config();
const fs = require('fs').promises;
const path = require('path');
const crypto = require('crypto');
const RETRY_DELAY_MS_XPRIME = 1000;
const MAX_RETRIES_XPRIME = 3;
// Cache directory (Vercel ephemeral vs local)
const CACHE_DIR = process.env.VERCEL ? path.join('/tmp', '.cache') : path.join(__dirname, '.cache');
async function ensureCacheDir(dirPath) {
try { await fs.mkdir(dirPath, { recursive: true }); } catch (_) {
// swallow errors creating cache dir (ephemeral env or permission)
}
}
async function getFromCache(cacheKey, subDir = '') {
if (process.env.DISABLE_CACHE === 'true') return null;
try {
const fullDir = path.join(CACHE_DIR, subDir);
const filePath = path.join(fullDir, cacheKey);
const data = await fs.readFile(filePath, 'utf8');
return data;
} catch (_) { return null; }
}
async function saveToCache(cacheKey, data, subDir = '') {
if (process.env.DISABLE_CACHE === 'true') return;
try {
const fullDir = path.join(CACHE_DIR, subDir);
await fs.mkdir(fullDir, { recursive: true });
const filePath = path.join(fullDir, cacheKey);
await fs.writeFile(filePath, data, 'utf8');
} catch (_) {
// ignore cache write errors (non-fatal)
}
}
const BROWSER_HEADERS_XPRIME = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36',
'Accept': '*/*',
'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8',
'Cache-Control': 'no-cache',
'Pragma': 'no-cache',
'Sec-Ch-Ua': '"Chromium";v="136", "Google Chrome";v="136", "Not.A/Brand";v="99"',
'Sec-Ch-Ua-Mobile': '?0',
'Sec-Ch-Ua-Platform': '"Windows"',
'Connection': 'keep-alive'
};
async function fetchWithRetry(url, options, maxRetries = MAX_RETRIES_XPRIME) {
const { default: fetch } = await import('node-fetch');
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const res = await fetch(url, options);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res;
} catch (err) {
lastError = err;
if (attempt < maxRetries) {
await new Promise(r => setTimeout(r, RETRY_DELAY_MS_XPRIME * Math.pow(2, attempt - 1)));
}
}
}
throw lastError;
}
async function fetchStreamSize(url) {
const subDir = 'xprime_stream_sizes';
const hash = crypto.createHash('md5').update(url).digest('hex');
const key = `${hash}.txt`;
const cached = await getFromCache(key, subDir);
if (cached) return cached;
if (url.toLowerCase().includes('.m3u8')) {
await saveToCache(key, 'Playlist (size N/A)', subDir);
return 'Playlist (size N/A)';
}
try {
const { default: fetch } = await import('node-fetch');
const controller = new AbortController();
const t = setTimeout(() => controller.abort(), 5000);
let size = 'Unknown size';
try {
const head = await fetch(url, { method: 'HEAD', signal: controller.signal });
const cl = head.headers.get('content-length');
if (cl) {
const bytes = parseInt(cl, 10);
if (!isNaN(bytes)) {
if (bytes < 1024) size = `${bytes} B`;
else if (bytes < 1024 * 1024) size = `${(bytes / 1024).toFixed(2)} KB`;
else if (bytes < 1024 * 1024 * 1024) size = `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
else size = `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
}
}
} finally { clearTimeout(t); }
await saveToCache(key, size, subDir);
return size;
} catch (_) {
await saveToCache(key, 'Unknown size', subDir);
return 'Unknown size';
}
}
async function getXprimeStreams(title, year, type, seasonNum, episodeNum) {
if (!title || !year) return [];
const encoded = encodeURIComponent(title);
let apiUrl;
if (type === 'movie') {
apiUrl = `https://backend.xprime.tv/primebox?name=${encoded}&year=${year}&fallback_year=${year}`;
} else if (type === 'tv') {
if (seasonNum == null || episodeNum == null) return [];
apiUrl = `https://backend.xprime.tv/primebox?name=${encoded}&year=${year}&fallback_year=${year}&season=${seasonNum}&episode=${episodeNum}`;
} else return [];
console.log(`[Xprime.tv] Fetch attempt '${title}' (${year}) type=${type}`);
let resJson;
try {
const r = await fetchWithRetry(apiUrl, { headers: BROWSER_HEADERS_XPRIME });
resJson = await r.json();
} catch (e) {
console.error('[Xprime.tv] Fetch failed:', e.message);
return [];
}
const streams = [];
const processItem = (item) => {
if (!item || item.error || !item.streams) return;
Object.entries(item.streams).forEach(([quality, url]) => {
if (!url || typeof url !== 'string') return;
streams.push({
name: title,
title: `${title} - ${type === 'tv' ? `S${String(seasonNum).padStart(2,'0')}E${String(episodeNum).padStart(2,'0')} ` : ''}${quality}`,
url,
quality: quality || 'Unknown',
provider: 'Xprime.tv',
headers: {},
});
});
};
if (Array.isArray(resJson)) resJson.forEach(processItem); else processItem(resJson);
if (streams.length) {
console.time('[Xprime.tv] Fetch stream sizes');
await Promise.all(streams.map(async s => { s.size = await fetchStreamSize(s.url); }));
console.timeEnd('[Xprime.tv] Fetch stream sizes');
}
console.log(`[Xprime.tv] Returning ${streams.length} streams.`);
return streams;
}
ensureCacheDir(CACHE_DIR).catch(()=>{});
module.exports = { getXprimeStreams };
+451
View File
@@ -0,0 +1,451 @@
// Lightweight proxy/m3u8/segment/subtitle rewriting layer. thanks to https://github.com/cinepro-org/ for the original code.
// Adapted from external project (user supplied) to fit CommonJS style and existing config system.
// Provides conditional route registration via enableProxy flag.
const cors = require('cors');
const fetch = require('node-fetch');
const CACHE_MAX_SIZE = 2000;
const CACHE_EXPIRY_MS = 2 * 60 * 60 * 1000; // 2 hours
const segmentCache = new Map();
// Track first open-ended (bytes=0-) range per target to clamp only once per TTL window
const openRangeClampMap = new Map();
const OPEN_RANGE_CLAMP_TTL_MS = 5 * 60 * 1000; // 5 minutes
// Progressive open-ended growth tracking: keeps expanding bytes=0- served window.
const progressiveOpenMap = new Map(); // url -> { lastEnd }
// Tail prefetch cache: stores last N KB for quick tail range responses
const tailPrefetchMap = new Map(); // url -> { data: Buffer, start: number, end: number, size: number, ts: number }
const TAIL_PREFETCH_TTL_MS = 10 * 60 * 1000; // 10 minutes
function isCacheDisabled() {
return process.env.DISABLE_CACHE === 'true';
}
function cleanupCache() {
const now = Date.now();
for (const [url, entry] of segmentCache.entries()) {
if (now - entry.timestamp > CACHE_EXPIRY_MS) segmentCache.delete(url);
}
if (segmentCache.size > CACHE_MAX_SIZE) {
const entries = Array.from(segmentCache.entries()).sort((a, b) => a[1].timestamp - b[1].timestamp);
const toRemove = entries.slice(0, segmentCache.size - CACHE_MAX_SIZE);
toRemove.forEach(([u]) => segmentCache.delete(u));
}
return segmentCache.size;
}
setInterval(cleanupCache, 30 * 60 * 1000).unref();
function cleanupClampMap() {
const now = Date.now();
for (const [url, ts] of openRangeClampMap.entries()) {
if (now - ts > OPEN_RANGE_CLAMP_TTL_MS) openRangeClampMap.delete(url);
}
}
setInterval(cleanupClampMap, 10 * 60 * 1000).unref();
function cleanupTailPrefetch() {
const now = Date.now();
for (const [url, entry] of tailPrefetchMap.entries()) {
if (now - entry.ts > TAIL_PREFETCH_TTL_MS) tailPrefetchMap.delete(url);
}
}
setInterval(cleanupTailPrefetch, 15 * 60 * 1000).unref();
function getCachedSegment(url) {
if (isCacheDisabled()) return null;
const e = segmentCache.get(url);
if (!e) return null;
if (Date.now() - e.timestamp > CACHE_EXPIRY_MS) { segmentCache.delete(url); return null; }
return e;
}
async function prefetchSegment(url, headers) {
if (isCacheDisabled() || segmentCache.size >= CACHE_MAX_SIZE) return;
const existing = segmentCache.get(url);
if (existing && Date.now() - existing.timestamp <= CACHE_EXPIRY_MS) return;
try {
const resp = await fetch(url, { headers: { 'User-Agent': DEFAULT_UA, ...headers } });
if (!resp.ok) return;
const data = new Uint8Array(await resp.arrayBuffer());
const responseHeaders = {};
resp.headers.forEach((v, k) => responseHeaders[k] = v);
segmentCache.set(url, { data, headers: responseHeaders, timestamp: Date.now() });
} catch (_e) { /* ignore */ }
}
const DEFAULT_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
function inferContentType(upstreamCT, targetUrl) {
const bad = !upstreamCT || /application\/octet-stream/i.test(upstreamCT) || /application\/(x-)?zip/i.test(upstreamCT);
if (!bad) return upstreamCT;
if (/\.mkv(\?|$)/i.test(targetUrl)) return 'video/x-matroska';
if (/\.mp4(\?|$)/i.test(targetUrl)) return 'video/mp4';
if (/\.m3u8(\?|$)/i.test(targetUrl)) return 'application/vnd.apple.mpegurl';
if (/\.ts(\?|$)/i.test(targetUrl)) return 'video/mp2t';
return 'application/octet-stream';
}
function extractOriginalUrl(proxyUrl) {
try {
const url = new URL(proxyUrl);
if (url.pathname.includes('/proxy/')) {
const m = url.pathname.match(/\/proxy\/(.+)$/);
if (m) {
let decoded = decodeURIComponent(m[1]);
while (decoded.includes('%2F')) {
try { decoded = decodeURIComponent(decoded); } catch { break; }
}
return decoded;
}
}
if (url.searchParams.has('url')) return decodeURIComponent(url.searchParams.get('url'));
const patterns = [
/\/api\/[^/]+\/proxy\?url=(.+)$/,
/\/proxy\?.*url=([^&]+)/,
/\/stream\/proxy\/(.+)$/,
/\/p\/(.+)$/
];
for (const p of patterns) {
const m = proxyUrl.match(p); if (m) return decodeURIComponent(m[1]);
}
return proxyUrl;
} catch { return proxyUrl; }
}
function rewriteM3u8(content, targetUrl, baseProxyUrl, headers) {
const lines = content.split('\n');
const out = []; const segmentUrls = [];
for (const line of lines) {
if (line.startsWith('#')) {
if (line.startsWith('#EXT-X-KEY:')) {
const regex = /https?:\/\/[^""\s]+/g; const keyUrl = regex.exec(line)?.[0];
if (keyUrl) {
const proxyUrl = `${baseProxyUrl}/ts-proxy?url=${encodeURIComponent(keyUrl)}&headers=${encodeURIComponent(JSON.stringify(headers))}`;
out.push(line.replace(keyUrl, proxyUrl));
if (!isCacheDisabled()) prefetchSegment(keyUrl, headers);
} else out.push(line);
} else if (line.startsWith('#EXT-X-MEDIA:') || line.startsWith('#EXT-X-I-FRAME-STREAM-INF:')) {
const uriMatch = line.match(/URI="([^"]+)"/);
if (uriMatch) {
try {
const mediaUrl = new URL(uriMatch[1], targetUrl).href;
const proxyUrl = `${baseProxyUrl}/m3u8-proxy?url=${encodeURIComponent(mediaUrl)}&headers=${encodeURIComponent(JSON.stringify(headers))}`;
out.push(line.replace(uriMatch[1], proxyUrl));
} catch { out.push(line); }
} else out.push(line);
} else out.push(line);
} else if (line.trim()) {
try {
const abs = new URL(line, targetUrl).href;
if (/\.m3u8(\?|$)/i.test(abs)) {
out.push(`${baseProxyUrl}/m3u8-proxy?url=${encodeURIComponent(abs)}&headers=${encodeURIComponent(JSON.stringify(headers))}`);
} else if (/\.ts(\?|$)/i.test(abs)) {
segmentUrls.push(abs);
out.push(`${baseProxyUrl}/ts-proxy?url=${encodeURIComponent(abs)}&headers=${encodeURIComponent(JSON.stringify(headers))}`);
} else out.push(line);
} catch { out.push(line); }
} else out.push(line);
}
if (segmentUrls.length && !isCacheDisabled()) {
Promise.all(segmentUrls.map(u => prefetchSegment(u, headers))).catch(() => undefined);
}
return out.join('\n');
}
function createProxyRoutes(app) {
// m3u8 playlist proxy
app.get('/m3u8-proxy', cors(), async (req, res) => {
const targetUrl = req.query.url; if (!targetUrl) return res.status(400).json({ error: 'URL parameter required' });
let headers = {};
try { headers = JSON.parse(req.query.headers || '{}'); } catch {
// Ignore URL parsing errors
}
try {
const response = await fetch(targetUrl, { headers: { 'User-Agent': DEFAULT_UA, ...headers } });
if (!response.ok) return res.status(response.status).json({ error: `M3U8 fetch failed: ${response.status}` });
const text = await response.text();
const protocol = req.headers['x-forwarded-proto'] || req.protocol || 'http';
const baseProxyUrl = `${protocol}://${req.get('host')}`;
const rewritten = rewriteM3u8(text, targetUrl, baseProxyUrl, headers);
res.setHeader('Content-Type', 'application/vnd.apple.mpegurl');
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
res.setHeader('Access-Control-Allow-Origin', '*');
res.send(rewritten);
} catch (e) { res.status(500).json({ error: e.message }); }
});
// ts / key segment proxy
app.get('/ts-proxy', cors(), async (req, res) => {
const targetUrl = req.query.url; if (!targetUrl) return res.status(400).json({ error: 'URL parameter required' });
const debug = req.query.debug === '1';
const noSynth = req.query.noSynth === '1';
const force200 = req.query.force200 === '1';
const clampOpen = req.query.clampOpen !== '0';
const progressiveOpen = req.query.progressiveOpen !== '0';
const tailPrefetchEnabled = req.query.tailPrefetch !== '0';
let tailPrefetchKB = parseInt(req.query.tailPrefetchKB || '256', 10);
if (isNaN(tailPrefetchKB) || tailPrefetchKB < 64) tailPrefetchKB = 256;
if (tailPrefetchKB > 2048) tailPrefetchKB = 2048; // max 2MB tail window
let openChunkKB = parseInt(req.query.openChunkKB || '4096', 10);
if (isNaN(openChunkKB) || openChunkKB < 64) openChunkKB = 4096;
if (openChunkKB > 16384) openChunkKB = 16384; // cap at 16MB
let initChunkKB = parseInt(req.query.initChunkKB || '512', 10);
if (isNaN(initChunkKB) || initChunkKB < 64) initChunkKB = 512;
if (initChunkKB > 2048) initChunkKB = 2048; // hard cap 2MB
let headers = {}; try { headers = JSON.parse(req.query.headers || '{}'); } catch {
// Ignore URL parsing errors
}
// Pass through Range header for progressive playback / seeking
const range = req.headers['range'];
let appliedClamp = false;
let effectiveRange = range;
if (range) headers.Range = range;
// Clamp first open-ended bytes=0- request to a bounded span (once per TTL) to avoid gigantic initial 206
if (range && /^bytes=0-\s*$/i.test(range) && !force200) {
const now = Date.now();
// Progressive growth supersedes single-shot clamp if enabled
if (progressiveOpen) {
const prog = progressiveOpenMap.get(targetUrl) || { lastEnd: -1 };
const prevEnd = prog.lastEnd;
let nextEnd;
if (prog.lastEnd < 0) {
nextEnd = openChunkKB * 1024 - 1;
} else {
const increment = openChunkKB * 1024;
nextEnd = prog.lastEnd + increment;
const maxCap = 256 * 1024 * 1024 - 1; // 256MB
if (nextEnd > maxCap) nextEnd = maxCap;
}
const newRange = `bytes=0-${nextEnd}`;
headers.Range = newRange;
effectiveRange = newRange;
prog.lastEnd = nextEnd;
progressiveOpenMap.set(targetUrl, prog);
appliedClamp = true; // reuse flag for logging
if (debug) console.log('[ts-proxy] progressiveOpen expand', { prevEnd, nextEnd, increment: openChunkKB * 1024 });
} else if (clampOpen) {
const lastTs = openRangeClampMap.get(targetUrl);
if (!lastTs || (now - lastTs) > OPEN_RANGE_CLAMP_TTL_MS) {
const clampBytes = openChunkKB * 1024;
const newRange = `bytes=0-${clampBytes - 1}`;
headers.Range = newRange;
effectiveRange = newRange;
openRangeClampMap.set(targetUrl, now);
appliedClamp = true;
}
}
}
if (debug) console.log('[ts-proxy] incoming', { url: targetUrl, range, effectiveRange, appliedClamp, clampOpen, progressiveOpen, openChunkKB, noSynth, initChunkKB, force200, tailPrefetchEnabled, tailPrefetchKB });
if (!isCacheDisabled()) {
const cached = getCachedSegment(targetUrl);
if (cached) {
const ct = inferContentType(cached.headers['content-type'], targetUrl);
res.setHeader('Content-Type', ct);
res.setHeader('Cache-Control', 'public, max-age=3600');
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Accept-Ranges', 'bytes');
if (debug) console.log('[ts-proxy] cache hit');
return res.send(Buffer.from(cached.data));
}
}
try {
// If we have a tail prefetch entry and the client requests a tail range fully inside it, serve from memory early.
if (effectiveRange && /^bytes=\d+-$/i.test(effectiveRange)) {
const tailEntry = tailPrefetchMap.get(targetUrl);
if (tailEntry) {
const m = effectiveRange.match(/bytes=(\d+)-/i);
if (m) {
const startReq = parseInt(m[1], 10);
if (!isNaN(startReq) && startReq >= tailEntry.start && startReq <= tailEntry.end) {
const offset = startReq - tailEntry.start;
const slice = tailEntry.data.subarray(offset);
const endByte = tailEntry.end;
res.status(206);
const ct = inferContentType(null, targetUrl);
res.setHeader('Content-Type', ct);
res.setHeader('Accept-Ranges', 'bytes');
res.setHeader('Content-Length', slice.length.toString());
res.setHeader('Content-Range', `bytes ${startReq}-${endByte}/${tailEntry.size}`);
res.setHeader('Cache-Control', 'public, max-age=3600');
res.setHeader('Access-Control-Allow-Origin', '*');
if (debug) console.log('[ts-proxy] tail serve hit', { startReq, cachedStart: tailEntry.start, cachedEnd: tailEntry.end, size: tailEntry.size });
return res.end(slice);
}
}
}
}
// HEAD preflight (only when no explicit Range requested) to get size for synthesized partial response
let contentLength = null;
let upstreamAcceptRanges = null;
if (!effectiveRange) {
try {
const headResp = await fetch(targetUrl, { method: 'HEAD', headers: { 'User-Agent': DEFAULT_UA, ...headers } });
if (headResp.ok) {
contentLength = headResp.headers.get('content-length');
upstreamAcceptRanges = headResp.headers.get('accept-ranges');
if (debug) console.log('[ts-proxy] HEAD ok', { contentLength, upstreamAcceptRanges });
} else if (debug) {
console.log('[ts-proxy] HEAD status', headResp.status);
}
} catch (_e) { if (debug) console.log('[ts-proxy] HEAD failed'); }
}
// Fallback probe: if still no size, attempt a 0-0 range request to derive total size from Content-Range
if (!effectiveRange && !contentLength) {
try {
const probeRange = 'bytes=0-0';
if (debug) console.log('[ts-proxy] probe range');
const probeResp = await fetch(targetUrl, { headers: { 'User-Agent': DEFAULT_UA, ...headers, 'Range': probeRange } });
if (probeResp.status === 206) {
const cr = probeResp.headers.get('content-range');
if (cr) {
const m = cr.match(/\/(\d+)$/); if (m) contentLength = m[1];
}
try { await probeResp.arrayBuffer(); } catch (readErr) {
if (debug) console.log('[ts-proxy] probe body read failed', readErr.message);
}
if (debug) console.log('[ts-proxy] probe success', { contentLength });
} else if (debug) {
console.log('[ts-proxy] probe status', probeResp.status);
}
} catch (_e) { if (debug) console.log('[ts-proxy] probe failed'); }
}
if (debug && contentLength) console.log('[ts-proxy] size determined', contentLength);
// Initiate tail prefetch asynchronously (once) when size known and feature enabled
if (tailPrefetchEnabled && contentLength && !tailPrefetchMap.has(targetUrl)) {
const total = parseInt(contentLength, 10);
if (!isNaN(total) && total > 0) {
const tailBytes = Math.min(tailPrefetchKB * 1024, total);
const start = Math.max(0, total - tailBytes);
const tailRange = `bytes=${start}-`;
(async () => {
try {
if (debug) console.log('[ts-proxy] tail prefetch start', { tailRange });
const tr = await fetch(targetUrl, { headers: { 'User-Agent': DEFAULT_UA, ...headers, Range: tailRange } });
if (tr.status === 206) {
const buf = Buffer.from(await tr.arrayBuffer());
const cr = tr.headers.get('content-range');
if (cr) {
const mm = cr.match(/bytes\s+(\d+)-(\d+)\/(\d+)/i);
if (mm) {
const s = parseInt(mm[1], 10); const e = parseInt(mm[2], 10); const sz = parseInt(mm[3], 10);
if (!isNaN(s) && !isNaN(e) && !isNaN(sz)) {
tailPrefetchMap.set(targetUrl, { data: buf, start: s, end: e, size: sz, ts: Date.now() });
if (debug) console.log('[ts-proxy] tail prefetch success', { start: s, end: e, bytes: buf.length });
}
}
}
} else if (debug) {
console.log('[ts-proxy] tail prefetch status', tr.status);
}
} catch (e) { if (debug) console.log('[ts-proxy] tail prefetch error', e.message); }
})();
}
}
// If no range requested but we now know size (HEAD or probe), synthesize an initial small range fetch to accelerate playback.
if (!force200 && !effectiveRange && contentLength && !noSynth && req.query.progressiveOpen === '0') {
const total = parseInt(contentLength, 10);
const desired = initChunkKB * 1024;
const chunkSize = Math.min(desired, Math.max(0, total - 1));
const syntheticRange = `bytes=0-${chunkSize}`;
const resp = await fetch(targetUrl, { headers: { 'User-Agent': DEFAULT_UA, ...headers, 'Range': syntheticRange } });
if (resp.status === 206) {
if (debug) console.log('[ts-proxy] synthetic 206', syntheticRange);
const upstreamCT = inferContentType(resp.headers.get('content-type'), targetUrl);
res.status(206);
res.setHeader('Content-Type', upstreamCT);
res.setHeader('Accept-Ranges', upstreamAcceptRanges || 'bytes');
const contentRange = resp.headers.get('content-range');
if (contentRange) res.setHeader('Content-Range', contentRange);
const cl = resp.headers.get('content-length'); if (cl) res.setHeader('Content-Length', cl);
res.setHeader('Cache-Control', 'public, max-age=3600');
res.setHeader('Access-Control-Allow-Origin', '*');
// Pipe first chunk then leave connection open? Simpler: end after chunk; player will request next range.
return resp.body.pipe(res);
}
// If server ignored range (e.g., returned 200), fall through to normal logic below.
}
const upstreamOptions = { headers: { 'User-Agent': DEFAULT_UA, ...headers } };
// If client supplied a suffix range like bytes=0- we pass it; force200 strips Range
if (force200 && headers.Range) delete headers.Range;
const resp = await fetch(targetUrl, upstreamOptions);
if (!resp.ok && resp.status !== 206) {
if (debug) console.log('[ts-proxy] upstream error', resp.status);
return res.status(resp.status).json({ error: `TS fetch failed: ${resp.status}` });
}
// Forward partial content status when range used
if (effectiveRange && resp.status === 206 && !force200) { res.status(206); if (debug) console.log('[ts-proxy] forwarding 206', { appliedClamp }); }
if (force200 && resp.status === 206) {
// Some origins still return 206 even without Range; normalize to 200 for players expecting full stream
if (debug) console.log('[ts-proxy] normalizing 206 -> 200 due to force200');
}
// Copy key streaming headers
const upstreamCT = inferContentType(resp.headers.get('content-type'), targetUrl);
res.setHeader('Content-Type', upstreamCT);
let cl = resp.headers.get('content-length');
const crHeader = resp.headers.get('content-range');
if (!cl && crHeader) {
// Try to derive length from range span
const m = crHeader.match(/bytes\s+(\d+)-(\d+)\/(\d+|\*)/i);
if (m) {
const start = parseInt(m[1], 10); const end = parseInt(m[2], 10);
if (!isNaN(start) && !isNaN(end)) cl = (end - start + 1).toString();
}
}
if (cl) res.setHeader('Content-Length', cl);
const acceptRanges = resp.headers.get('accept-ranges');
if (acceptRanges) res.setHeader('Accept-Ranges', acceptRanges); else res.setHeader('Accept-Ranges', 'bytes');
const contentRange = resp.headers.get('content-range'); if (contentRange && !force200) res.setHeader('Content-Range', contentRange);
res.setHeader('Cache-Control', 'public, max-age=3600');
res.setHeader('Access-Control-Allow-Origin', '*');
if (debug) console.log('[ts-proxy] streaming body', { status: resp.status, ct: upstreamCT, cl, contentRange: force200 ? undefined : contentRange });
resp.body.pipe(res);
} catch (e) { if (debug) console.log('[ts-proxy] exception', e.message); res.status(500).json({ error: e.message }); }
});
// subtitle proxy
app.get('/sub-proxy', cors(), async (req, res) => {
const targetUrl = req.query.url; if (!targetUrl) return res.status(400).json({ error: 'url parameter required' });
let headers = {}; try { headers = JSON.parse(req.query.headers || '{}'); } catch {
// Ignore URL parsing errors
}
try {
const resp = await fetch(targetUrl, { headers: { 'User-Agent': DEFAULT_UA, ...headers } });
if (!resp.ok) return res.status(resp.status).json({ error: `subtitle fetch failed: ${resp.status}` });
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Cache-Control', 'public, max-age=3600');
res.setHeader('Content-Type', resp.headers.get('content-type') || 'text/vtt');
resp.body.pipe(res);
} catch (e) { res.status(500).json({ error: e.message }); }
});
}
function processStreamsForProxy(streams, serverUrl) {
if (!Array.isArray(streams)) return streams;
return streams.map(s => {
if (!s || !s.url || typeof s.url !== 'string') return s;
const original = extractOriginalUrl(s.url);
const headers = s.headers || {};
const hParam = Object.keys(headers).length ? `&headers=${encodeURIComponent(JSON.stringify(headers))}` : '';
let host = '';
try {
host = new URL(original).hostname.toLowerCase();
} catch {
// Ignore errors
}
// Force specific hosts through ts-proxy (direct file style) even without extension
if (host.includes('pixeldrain.') || host === 'video-downloads.googleusercontent.com') {
return { ...s, url: `${serverUrl}/ts-proxy?url=${encodeURIComponent(original)}${hParam}` };
}
if (/\.(mp4|mkv)(\?|$)/i.test(original)) {
return { ...s, url: `${serverUrl}/ts-proxy?url=${encodeURIComponent(original)}${hParam}` };
}
return { ...s, url: `${serverUrl}/m3u8-proxy?url=${encodeURIComponent(original)}${hParam}` };
});
}
module.exports = { createProxyRoutes, processStreamsForProxy };
+4 -4
View File
@@ -29,15 +29,15 @@
</footer>
<footer class="side-footer">Credits to :<a class="hint" style="margin-top: .5rem; color: var(--primary-fg);" href="https://github.com/tapframe/NuvioStreamsAddon" target="_blank">NuvioStreamsAddon for Stremio</a></footer>
<div class="bmc-widget-container">
<script type="text/javascript" src="https://cdnjs.buymeacoffee.com/1.0.0/button.prod.min.js" data-name="bmc-button" data-slug="K3QSoR2" data-color="#5F7FFF" data-emoji="☕" data-font="Bree" data-text="Buy me a coffee" data-outline-color="#000000" data-font-color="#ffffff" data-coffee-color="#FFDD00" ></script>
<div class="bmc-widget-container" style="text-align: center;">
<iframe src="https://github.com/sponsors/Inside4ndroid/button" title="Sponsor Inside4ndroid" height="32" width="114" style="border:0;border-radius:6px;margin-top:.5rem;"></iframe>
</div>
<footer class="side-footer">Made with ❤️ by Inside4ndroid</footer>
</aside>
<main class="main">
<div id="bootWarn" class="alert warning" style="display:none;">No FEBBOX_COOKIES configured. Showbox / PStream streams may be unavailable.</div>
<div id="cacheDisabledBanner" class="alert info" style="display:none;">Cache is <strong>DISABLED</strong>; expect slower responses & higher source load.</div>
<div id="tmdbMissingBanner" class="alert warning" style="display:none;">No TMDB API Keys configured. Metadata-dependent providers (Showbox, 4khdhub, MoviesMod, MP4Hydra) may degrade.</div>
<div id="tmdbMissingBanner" class="alert warning" style="display:none;">No TMDB API Keys configured. Metadata-dependent providers (Showbox, 4khdhub, MoviesMod) may degrade.</div>
<form id="cfgForm" onsubmit="return false;" class="panels">
<section id="panel-status" class="panel">
<h1>Server Status<button style="margin-left: 1rem;" id="refreshStatusBtn" type="button">Refresh Status</button></h1>
@@ -177,12 +177,12 @@
<label class="adv-flag-item"><input type="checkbox" name="adv_disableCache" value="1" /> <span>Disable Cache</span></label>
<label class="adv-flag-item"><input type="checkbox" name="adv_disableUrlValidation" value="1" /> <span>Disable URL Validation</span></label>
<label class="adv-flag-item"><input type="checkbox" name="adv_disable4khdhubValidation" value="1" /> <span>Disable 4KHDHub Validation</span></label>
<label class="adv-flag-item"><input type="checkbox" name="adv_enableProxy" value="1" /> <span>Enable Stream Proxy</span></label>
</div>
<h1>Paths</h1>
<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="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>
</section>
<section id="panel-live" class="panel">
+5 -11
View File
@@ -154,18 +154,16 @@ function fillForm(data){
const tmdbProvidersEnabled = [
merged.enableShowboxProvider !== false,
merged.enable4khdhubProvider !== false,
merged.enableMoviesmodProvider !== false,
merged.enableMp4hydraProvider !== false
merged.enableMoviesmodProvider !== false
].some(Boolean);
tmdbBanner.style.display = (tmdbMissing && tmdbProvidersEnabled)? 'block':'none';
}
const advBools = [
['enableXprimeProvider','adv_enableXprime'],
['enable4khdhubProvider','adv_enable4khdhub'],
['enableMoviesmodProvider','adv_enableMoviesmod'],
['enableMp4hydraProvider','adv_enableMp4hydra'],
['enableVidzeeProvider','adv_enableVidzee'],
['enableVixsrcProvider','adv_enableVixsrc'],
['enableProxy','adv_enableProxy'],
['disableCache','adv_disableCache'],
['enablePStreamApi','adv_enablePStream'],
['disableUrlValidation','adv_disableUrlValidation'],
@@ -174,7 +172,6 @@ function fillForm(data){
advBools.forEach(([cfgKey, elName])=>{ const el = f.elements[elName]; if(!el) return; const src = (override[cfgKey]!==undefined? override[cfgKey] : merged[cfgKey]); if(el.type==='checkbox'){ el.checked = !!src; } else { el.value = src? 'true':''; } });
const advTexts = [
['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 || ''; });
// Sync checkbox style advanced flags
@@ -245,12 +242,11 @@ async function save(){
payload.febboxCookies = cookiesVal? cookiesVal.split(',').filter(Boolean) : [];
}
const boolMap = {
adv_enableXprime:'enableXprimeProvider',
adv_enable4khdhub:'enable4khdhubProvider',
adv_enableMoviesmod:'enableMoviesmodProvider',
adv_enableMp4hydra:'enableMp4hydraProvider',
adv_enableVidzee:'enableVidzeeProvider',
adv_enableVixsrc:'enableVixsrcProvider',
adv_enableProxy:'enableProxy',
adv_disableCache:'disableCache',
adv_enablePStream:'enablePStreamApi',
adv_disableUrlValidation:'disableUrlValidation',
@@ -258,7 +254,6 @@ async function save(){
};
Object.entries(boolMap).forEach(([formName,cfgKey])=>{ const el = f.elements[formName]; if(el) payload[cfgKey] = !!el.checked; });
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; });
const tmdbHidden = f.elements['tmdbApiKeysHidden'];
if (tmdbHidden){
@@ -339,12 +334,11 @@ async function clearAll(){
tmdbApiKey: null,
// Provider enable flags default to true
enableShowboxProvider: true,
enableXprimeProvider: true,
enable4khdhubProvider: true,
enableMoviesmodProvider: true,
enableMp4hydraProvider: true,
enableVidzeeProvider: true,
enableVixsrcProvider: true,
enableProxy: false,
// Other defaults
disableCache: false,
enablePStreamApi: true,
@@ -451,7 +445,7 @@ async function loadServerStatus(){
}
}
async function runProviderFunctionalChecks(){
const targetTmdb = '293660'; // TMDB ID provided
const targetTmdb = '550'; // TMDB ID provided
const provEnhanced = document.getElementById('statusProvidersEnhanced');
const summary = document.getElementById('providerCheckSummary');
if(!provEnhanced) return;
+1 -1
View File
@@ -1 +1 @@
1758225404146
1778930133625
+1 -1
View File
@@ -3,7 +3,7 @@
"users": [
{
"username": "admin",
"hash": "7fffd5f131a733035eaf443cd99905c0:f1cfa876f9b84fede40cd618a4352221f7ab36e4f8bead432ba5d866b06cb9fcc2903ce623d8286bb541f8d28490fc692d0bf3fadda08002265374915cdb27b5"
"hash": "3c2efa431b6de0bee19847236e2c296b:d88ccc2ee950eda254a03d073bc55bbc8d0efc263c93c4e20307154dc90fadbc3141dcc9556a150b39b52088bb8ad06b120bc8832d7d8dc35918e710803d207c"
}
]
}
+4 -10
View File
@@ -57,7 +57,7 @@ function getProviderNames() {
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')
.filter(file => file.endsWith('.js') && file !== 'registry.js')
.map(file => path.parse(file).name.toLowerCase());
} catch (e) {
console.warn('[config] Failed to scan providers directory:', e.message);
@@ -97,7 +97,7 @@ function normalizeConfig(base) {
// 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.push('disableCache', 'enablePStreamApi', 'disableUrlValidation', 'disable4khdhubUrlValidation', 'enableProxy');
boolKeys.forEach(k=>{ if (cfg[k] === 'true') cfg[k] = true; else if (cfg[k] === 'false') cfg[k] = false; });
@@ -110,14 +110,10 @@ function normalizeConfig(base) {
// Default values for other flags
if (cfg.disableCache === undefined) cfg.disableCache = false;
if (cfg.enablePStreamApi === undefined) cfg.enablePStreamApi = true;
if (cfg.enableProxy === undefined) cfg.enableProxy = false; // default off
// Proxy features removed; always use direct connections
if (cfg.disableUrlValidation === undefined) cfg.disableUrlValidation = 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;
}
@@ -147,18 +143,16 @@ function applyConfigToEnv(cfg){
process.env.ENABLE_PSTREAM_API = cfg.enablePStreamApi ? 'true':'false';
process.env.DISABLE_URL_VALIDATION = cfg.disableUrlValidation ? 'true':'false';
process.env.DISABLE_4KHDHUB_URL_VALIDATION = cfg.disable4khdhubUrlValidation ? 'true':'false';
process.env.ENABLE_PROXY = cfg.enableProxy ? 'true':'false';
// Showbox specific
if (cfg.showboxCacheDir) process.env.SHOWBOX_CACHE_DIR = cfg.showboxCacheDir; else delete process.env.SHOWBOX_CACHE_DIR;
// Proxy settings removed; ensure legacy envs are cleared
delete process.env.SHOWBOX_USE_ROTATING_PROXY;
delete process.env.SHOWBOX_PROXY_URL_VALUE;
delete process.env.SHOWBOX_PROXY_URL_ALTERNATE;
delete process.env.XPRIME_PROXY_URL;
delete process.env.VIDZEE_PROXY_URL;
delete process.env.VIDSRC_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
}
+6 -2
View File
@@ -67,10 +67,14 @@ class JsUnpacker {
let count = 0;
try {
radix = parseInt(radixStr);
} catch (e) {}
} catch {
// Ignore URL parsing errors
}
try {
count = parseInt(countStr);
} catch (e) {}
} catch {
// Ignore URL parsing errors
}
if (symtab.length !== count) {
throw new Error('Unknown p.a.c.k.e.r. encoding');
+15 -11
View File
@@ -2,23 +2,27 @@
"defaultProviders": [],
"configVersion": 1,
"tmdbApiKeys": [
"f3c627493095a7e40ceca68355c94c6d"
"338a47b75eab45d9e64e67088f910f93"
],
"enableShowboxProvider": true,
"enableShowboxProvider": false,
"febboxCookies": [
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3NTc5NjYxMDAsIm5iZiI6MTc1Nzk2NjEwMCwiZXhwIjoxNzg5MDcwMTIwLCJkYXRhIjp7InVpZCI6ODc2NTEwLCJ0b2tlbiI6ImY0MDQxNWJkMTdiYjc1YmQ2ZWQ1NzQ1ZTFhZmNlOTM5In19.vs_pLXAJYhnEaHDfcR_dv9-3us2MJhMOMHUC4la9Rag"
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3Nzg5MjM2NDksIm5iZiI6MTc3ODkyMzY0OSwiZXhwIjoxODEwMDI3NjY5LCJkYXRhIjp7InVpZCI6ODc2NTEwLCJ0b2tlbiI6IjYxNmZlZDdjZDRjMzY1OWJmNWMzNTg5MGUzOTQ4Y2MyIn19.ESauZ9IInk5Iafpt-zLI64u4hMzsby_Spy0tkTJ6KuY"
],
"enableXprimeProvider": true,
"enable4khdhubProvider": true,
"enableMoviesmodProvider": true,
"enableMp4hydraProvider": true,
"enableVidzeeProvider": true,
"enableVixsrcProvider": true,
"disableCache": true,
"enable4khdhubProvider": false,
"enableMoviesmodProvider": false,
"enableVidzeeProvider": false,
"enableVixsrcProvider": false,
"enableProxy": false,
"disableCache": false,
"enablePStreamApi": true,
"disableUrlValidation": false,
"disable4khdhubUrlValidation": false,
"excludeCodecsRaw": "{\"excludeDV\":false,\"excludeHDR\":false}",
"minQualitiesRaw": "all",
"port": 8787
"enableUhdmoviesProvider": false,
"enableVidlinkProvider": false,
"enableVideasyProvider": false,
"enableLordflixProvider": false,
"port": 8787,
"enableNotorrentProvider": false
}