From 6ebb0d32d54688c97d2102b2548919d7e3c86b74 Mon Sep 17 00:00:00 2001
From: Inside4ndroid
Date: Fri, 3 Oct 2025 16:27:54 +0100
Subject: [PATCH] chore(release): 1.0.8
---
CHANGELOG.md | 19 ++
README.md | 2 +-
apiServer.js | 22 +-
package-lock.json | 15 +-
package.json | 2 +-
providers/4khdhub.js | 46 +--
providers/Showbox.js | 7 +-
providers/VidZee.js | 51 +++-
providers/moviesclub.js | 651 ----------------------------------------
providers/registry.js | 42 ---
providers/vixsrc.js | 247 ++++++++-------
providers/xprime.js | 158 ----------
proxy/proxyServer.js | 451 ++++++++++++++++++++++++++++
public/config.html | 2 +-
public/index.js | 8 +-
utils/config.js | 12 +-
utils/jsunpack.js | 8 +-
utils/user-config.json | 15 +-
18 files changed, 720 insertions(+), 1038 deletions(-)
delete mode 100644 providers/moviesclub.js
delete mode 100644 providers/xprime.js
create mode 100644 proxy/proxyServer.js
diff --git a/CHANGELOG.md b/CHANGELOG.md
index dd7d191..f0efb16 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,24 @@
All notable changes to this project will be documented in this file.
+## [1.0.8] - 2025-10-03
+
+### Added
+- Added `processStreamsForProxy` so the aggregate and provider-specific endpoints automatically rewrite stream URLs through the internal `/m3u8-proxy`/`/ts-proxy` layer whenever `enableProxy` is turned on, stripping provider headers safely.
+
+### Changed
+- Showbox provider overhaul: filesystem-only caching (Redis removed), smarter FebBox cookie selection with region fallbacks, TMDB title/image validation that recognises romanized names, plus cached HEAD size lookups for faster listings.
+- Vixsrc provider rewritten to parse the `window.masterPlaylist` payload (token + expiry), returning a single master playlist with English subtitle lookup and correct Referer headers.
+- Config loader now prefers `utils/user-config.json`, dedupes TMDB keys, defaults provider enable flags to `true`, and scrubs legacy proxy env values when mirroring overrides back to `process.env`.
+- 4KHDHub provider permanently drops `.zip` archive links instead of trimming extensions and improves host distribution logging.
+- Provider registry dynamically enumerates available modules (removing MoviesClub/Xprime remnants) while keeping per-request cookie stats for the admin debug panel.
+
+### Removed
+- Deleted legacy `providers/moviesclub.js`, `providers/xprime.js`, and other unused proxy/auth remnants that were no longer referenced.
+
+### Fixed
+- Aggregated responses now obey the proxy flag without client changes—streams returned from `/api/streams/...` are proxy-wrapped and omit upstream header hints when `enableProxy` is active.
+
## [1.0.7] - 2025-09-21
### Added
@@ -132,6 +150,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
diff --git a/README.md b/README.md
index 21d5467..f88b63b 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
-
+
diff --git a/apiServer.js b/apiServer.js
index cfc70e8..61e849d 100644
--- a/apiServer.js
+++ b/apiServer.js
@@ -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;
diff --git a/package-lock.json b/package-lock.json
index 8d62993..d6531aa 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "tmdb-embed-api",
- "version": "1.0.0",
+ "version": "1.0.8",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "tmdb-embed-api",
- "version": "1.0.0",
+ "version": "1.0.8",
"dependencies": {
"aromanize": "^0.1.5",
"axios": "^1.9.0",
@@ -674,6 +674,17 @@
}
}
},
+ "node_modules/axios-cookiejar-support/node_modules/undici": {
+ "version": "7.16.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz",
+ "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==",
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "engines": {
+ "node": ">=20.18.1"
+ }
+ },
"node_modules/b4a": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
diff --git a/package.json b/package.json
index 36c522d..2ec6e66 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "tmdb-embed-api",
- "version": "1.0.7",
+ "version": "1.0.8",
"description": "Standalone streaming embed source aggregation API (TMDB ID in, streams out)",
"main": "apiServer.js",
"scripts": {
diff --git a/providers/4khdhub.js b/providers/4khdhub.js
index c711bf1..58c2aa8 100644
--- a/providers/4khdhub.js
+++ b/providers/4khdhub.js
@@ -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;
diff --git a/providers/Showbox.js b/providers/Showbox.js
index 7fa56f5..5a024c9 100644
--- a/providers/Showbox.js
+++ b/providers/Showbox.js
@@ -2679,10 +2679,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
};
diff --git a/providers/VidZee.js b/providers/VidZee.js
index 314110f..2d23524 100644
--- a/providers/VidZee.js
+++ b/providers/VidZee.js
@@ -1,5 +1,43 @@
const axios = require('axios');
+// Confirmed AES-CBC decoder for VidZee encrypted link format.
+// Format (after initial atob in site script): ivBase64:cipherBase64
+// ivBase64 -> CryptoJS Base64 parsed to WordArray (IV)
+// Key: UTF-8 bytes of "qrincywincyspider" padded with nulls to 32 bytes (AES-256-CBC)
+// Cipher: base64 ciphertext, PKCS7 padding, mode CBC.
+let cryptoJs; // lazy load to avoid cost if not needed
+function decodeVidZeeToken(token, debug) {
+ try {
+ if (typeof token !== 'string') return null;
+ if (/^https?:\/\//i.test(token)) return null; // already a URL
+ // Expect pattern base64:base64 (iv:cipher)
+ const raw = Buffer.from(token, 'base64').toString('utf8');
+ if (!raw.includes(':')) return null;
+ const [ivB64, cipherB64] = raw.split(':');
+ if (!ivB64 || !cipherB64) return null;
+ if (!cryptoJs) cryptoJs = require('crypto-js');
+ const iv = cryptoJs.enc.Base64.parse(ivB64.trim());
+ const keyStr = 'qrincywincyspider';
+ // Pad key with nulls to 32 bytes
+ const keyUtf8 = cryptoJs.enc.Utf8.parse(keyStr.padEnd(32, '\0'));
+ const decrypted = cryptoJs.AES.decrypt(cipherB64.trim(), keyUtf8, {
+ iv,
+ mode: cryptoJs.mode.CBC,
+ padding: cryptoJs.pad.Pkcs7
+ }).toString(cryptoJs.enc.Utf8);
+ if (!decrypted) return null;
+ if (!/^https?:\/\//i.test(decrypted)) {
+ if (debug) console.log('[VidZee] decrypted but not a URL', decrypted.slice(0,80));
+ return null;
+ }
+ if (debug) console.log('[VidZee] AES decoded token', { tokenSnippet: token.slice(0, 24)+'...', url: decrypted.slice(0,120) });
+ return decrypted.trim();
+ } catch (e) {
+ if (debug) console.log('[VidZee] AES decode error', e.message);
+ return null;
+ }
+}
+
// Removed unused parseArgs helper
const getVidZeeStreams = async (tmdbId, mediaType, seasonNum, episodeNum) => {
@@ -73,20 +111,23 @@ const getVidZeeStreams = async (tmdbId, mediaType, seasonNum, episodeNum) => {
}
const streams = apiSources.map(sourceItem => {
- // Prefer sourceItem.name as label, fallback to sourceItem.type, then 'VidZee Stream'
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';
}
const language = sourceItem.language || sourceItem.lang;
-
+ let rawLink = sourceItem.link;
+ const debug = process.env.VIDZEE_DEBUG === '1';
+ const decoded = decodeVidZeeToken(rawLink, debug);
+ if (decoded && /^https?:\/\//i.test(decoded)) {
+ if (debug) console.log('[VidZee] decoded link', { beforeSample: rawLink.slice(0,40)+'...', after: decoded.slice(0,80) });
+ rawLink = decoded;
+ }
return {
name: `VidZee Server${sr} - ${quality} - ${language}`,
title: `VidZee Server${sr} - ${quality} - ${language}`,
- url: sourceItem.link, // Use sourceItem.link for the URL
+ url: rawLink,
quality: quality,
provider: "VidZee",
headers: {
diff --git a/providers/moviesclub.js b/providers/moviesclub.js
deleted file mode 100644
index ce36a19..0000000
--- a/providers/moviesclub.js
+++ /dev/null
@@ -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(/]*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(/