1 Commits
5 changed files with 103 additions and 26 deletions
+19 -10
View File
@@ -2,22 +2,31 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [1.0.4] - 2025-09-18 ## [1.0.5] - 2025-09-19
### Added ### Improved
- New provider: `moviesclub` with registry mapping (`moviesclub.js` → `getMoviesClubStreams`). - 4khdhub provider: Permanently block `r2.dev` FSL links (previous optional flag removed).
- Added Referer/Origin headers automatically for FSL Server links during validation (prior to block enforcement ensured proper behavior).
- Tightened URL validation: removed unconditional trust for `r2.dev`; validation logic now consistent across hosts.
- Host distribution instrumentation logs final hostname counts for easier diagnostics.
- Preserved HubCloud worker `.zip` links by stripping the `.zip` extension instead of discarding them (enables direct playback attempts).
- MoviesClub provider: Added automatic `Origin`/`Referer` headers for `vidora.stream` sources to prevent 403 responses.
### Changed ### Notes
- Unified provider stream schema across all providers: - `r2.dev` links are always removed from final output; no env flag required.
```json
{ "title": "…", "url": "…", "quality": "…", "provider": "…", "headers": { } }
```
- Provider integration docs: When adding a provider, also add its exported function to `providerFunctionMap` in `providers/registry.js`.
- README Docker quick run snippet updated to: `docker run -it -p 8787:8787 inside4ndroid/tmdb-embed-api:latest`.
### Documentation ### Documentation
- README: Updated Providers section, unified schema example, MoviesClub listed, registry mapping instructions, and badge version. - README: Updated Providers section, unified schema example, MoviesClub listed, registry mapping instructions, and badge version.
## [1.0.4] - 2025-09-18
### Added
### Changed
```json
{ "title": "…", "url": "…", "quality": "…", "provider": "…", "headers": { } }
```
## [1.0.3] - 2025-09-17 ## [1.0.3] - 2025-09-17
### Fixed ### Fixed
+1 -1
View File
@@ -6,7 +6,7 @@
<img src="https://img.shields.io/badge/Node.js-18%2B-brightgreen?style=flat" /> <img src="https://img.shields.io/badge/Node.js-18%2B-brightgreen?style=flat" />
<img src="https://img.shields.io/badge/Status-Active-success?style=flat" /> <img src="https://img.shields.io/badge/Status-Active-success?style=flat" />
<img src="https://img.shields.io/badge/License-MIT-blue?style=flat" /> <img src="https://img.shields.io/badge/License-MIT-blue?style=flat" />
<img src="https://img.shields.io/badge/Version-1.0.4-informational?style=flat" /> <img src="https://img.shields.io/badge/Version-1.0.5-informational?style=flat" />
<img src="https://img.shields.io/docker/pulls/inside4ndroid/tmdb-embed-api?label=Docker%20Pulls&style=flat" /> <img src="https://img.shields.io/docker/pulls/inside4ndroid/tmdb-embed-api?label=Docker%20Pulls&style=flat" />
</p> </p>
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "tmdb-embed-api", "name": "tmdb-embed-api",
"version": "1.0.4", "version": "1.0.5",
"description": "Standalone streaming embed source aggregation API (TMDB ID in, streams out)", "description": "Standalone streaming embed source aggregation API (TMDB ID in, streams out)",
"main": "apiServer.js", "main": "apiServer.js",
"scripts": { "scripts": {
+70 -10
View File
@@ -9,6 +9,9 @@ const path = require('path');
const DOMAINS_URL = 'https://raw.githubusercontent.com/phisher98/TVVVV/refs/heads/main/domains.json'; const DOMAINS_URL = 'https://raw.githubusercontent.com/phisher98/TVVVV/refs/heads/main/domains.json';
let cachedDomains = null; let cachedDomains = null;
// Permanent policy: block r2.dev links (previously optional via env flag)
console.log('[4KHDHub] r2.dev links will be removed (permanent policy)');
// --- Caching Configuration --- // --- Caching Configuration ---
const CACHE_ENABLED = process.env.DISABLE_CACHE !== 'true'; const CACHE_ENABLED = process.env.DISABLE_CACHE !== 'true';
console.log(`[4KHDHub] Internal cache is ${CACHE_ENABLED ? 'enabled' : 'disabled'}.`); console.log(`[4KHDHub] Internal cache is ${CACHE_ENABLED ? 'enabled' : 'disabled'}.`);
@@ -61,8 +64,7 @@ function validateUrl(url) {
// Skip validation for known reliable hosting services // Skip validation for known reliable hosting services
const trustedHosts = [ const trustedHosts = [
'pixeldrain.dev', 'pixeldrain.dev'
'r2.dev'
]; ];
const isTrustedHost = trustedHosts.some(host => urlObj.hostname.includes(host)); const isTrustedHost = trustedHosts.some(host => urlObj.hostname.includes(host));
@@ -74,11 +76,21 @@ function validateUrl(url) {
const protocol = urlObj.protocol === 'https:' ? https : http; const protocol = urlObj.protocol === 'https:' ? https : http;
// Add referer/origin for r2.dev validation attempts
const extraHeaders = {};
if (/r2\.dev$/i.test(urlObj.hostname)) {
const origin = `${urlObj.protocol}//${urlObj.hostname}`;
extraHeaders['Referer'] = origin + '/';
extraHeaders['Origin'] = origin;
console.log(`[4KHDHub] Adding Referer/Origin headers for validation of r2.dev URL: ${url}`);
}
const options = { const options = {
method: 'HEAD', method: 'HEAD',
timeout: 15000, timeout: 15000,
headers: { headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
...extraHeaders
} }
}; };
@@ -751,6 +763,14 @@ function extractHubCloudLinks(url, referer) {
if (displayFilename) titleParts.push(displayFilename); if (displayFilename) titleParts.push(displayFilename);
if (size) titleParts.push(size); if (size) titleParts.push(size);
const finalTitle = titleParts.join('\n'); const finalTitle = titleParts.join('\n');
let headers = {};
try {
const host = new URL(link).hostname;
if (/r2\.dev$/i.test(host)) {
headers = { Referer: baseUrl + '/', Origin: baseUrl };
console.log('[4KHDHub] Added Referer/Origin headers for r2.dev FSL link');
}
} catch {}
resolve({ resolve({
name: `4KHDHub - FSL Server${qualityLabel}`, name: `4KHDHub - FSL Server${qualityLabel}`,
@@ -758,7 +778,7 @@ function extractHubCloudLinks(url, referer) {
url: link, url: link,
quality: quality, quality: quality,
provider: '4khdhub', provider: '4khdhub',
headers: {} headers
}); });
}) })
.catch(() => { .catch(() => {
@@ -767,6 +787,14 @@ function extractHubCloudLinks(url, referer) {
if (displayFilename) titleParts.push(displayFilename); if (displayFilename) titleParts.push(displayFilename);
if (size) titleParts.push(size); if (size) titleParts.push(size);
const finalTitle = titleParts.join('\n'); const finalTitle = titleParts.join('\n');
let headers = {};
try {
const host = new URL(link).hostname;
if (/r2\.dev$/i.test(host)) {
headers = { Referer: baseUrl + '/', Origin: baseUrl };
console.log('[4KHDHub] Added Referer/Origin headers for r2.dev FSL link (fallback)');
}
} catch {}
resolve({ resolve({
name: `4KHDHub - FSL Server${qualityLabel}`, name: `4KHDHub - FSL Server${qualityLabel}`,
@@ -774,7 +802,7 @@ function extractHubCloudLinks(url, referer) {
url: link, url: link,
quality: quality, quality: quality,
provider: '4khdhub', provider: '4khdhub',
headers: {} headers
}); });
}); });
} else if (text.includes('Download File')) { } else if (text.includes('Download File')) {
@@ -1306,12 +1334,22 @@ function extractStreamingLinks(downloadLinks) {
.then(results => { .then(results => {
const validResults = results.filter(result => result !== null); const validResults = results.filter(result => result !== null);
const flatResults = validResults.flat(); const flatResults = validResults.flat();
// Filter out .zip files // Keep .zip links but strip the trailing .zip extension (attempt direct file URL)
const filteredResults = flatResults.filter(link => { const transformedResults = flatResults
return link && link.url && !link.url.toLowerCase().endsWith('.zip'); .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 // Note: Link count will be logged after validation completes
return filteredResults; return transformedResults;
}); });
} }
@@ -1778,7 +1816,7 @@ async function get4KHDHubStreams(tmdbId, type, season = null, episode = null) {
} }
// Normalize to generic stream object format // Normalize to generic stream object format
const streams = validatedLinks.map(link => ({ let streams = validatedLinks.map(link => ({
name: link.name, // Don't add prefix since it's already included name: link.name, // Don't add prefix since it's already included
title: link.title || link.name, title: link.title || link.name,
url: link.url, url: link.url,
@@ -1787,6 +1825,28 @@ async function get4KHDHubStreams(tmdbId, type, season = null, episode = null) {
headers: {} headers: {}
})); }));
// Always remove r2.dev links
{
const before = streams.length;
streams = streams.filter(s => {
try { return !new URL(s.url).hostname.endsWith('r2.dev'); } catch { return true; }
});
const removed = before - streams.length;
if (removed > 0) console.log(`[4KHDHub] Removed ${removed} r2.dev streams (permanent policy)`);
}
// Host distribution instrumentation
try {
const hostCounts = streams.reduce((acc, s) => {
try {
const h = new URL(s.url).hostname;
acc[h] = (acc[h] || 0) + 1;
} catch {}
return acc;
}, {});
console.log('[4KHDHub] Final host distribution:', hostCounts);
} catch {}
console.log(`[4KHDHub] Returning ${streams.length} streams`); console.log(`[4KHDHub] Returning ${streams.length} streams`);
return streams; return streams;
+11 -3
View File
@@ -309,7 +309,15 @@ async function handleSingleServer(embedUrl, mediaType, season, episode, tmdbId)
title: playerConfig.title || `${mediaType === 'movie' ? 'Movie' : `S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`} ${tmdbId} - MoviesClub`, title: playerConfig.title || `${mediaType === 'movie' ? 'Movie' : `S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`} ${tmdbId} - MoviesClub`,
url: playerConfig.file, url: playerConfig.file,
quality: extractQualityFromUrl(playerConfig.file) || '720p', quality: extractQualityFromUrl(playerConfig.file) || '720p',
headers: {} headers: (() => {
if (playerConfig.file.includes('vidora.stream')) {
return {
Origin: 'https://vidora.stream',
Referer: 'https://vidora.stream/'
};
}
return {};
})()
}; };
// Add poster if available // Add poster if available
@@ -384,7 +392,7 @@ function extractStreamsFromServer(serverData, serverName, mediaType, season, epi
title: `${mediaType === 'movie' ? 'Movie' : `S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`} ${tmdbId} - ${serverName}`, title: `${mediaType === 'movie' ? 'Movie' : `S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`} ${tmdbId} - ${serverName}`,
url: serverData, url: serverData,
quality: extractQualityFromUrl(serverData) || '720p', quality: extractQualityFromUrl(serverData) || '720p',
headers: {} headers: serverData.includes('vidora.stream') ? { Origin: 'https://vidora.stream', Referer: 'https://vidora.stream/' } : {}
}; };
streams.push(stream); streams.push(stream);
} }
@@ -406,7 +414,7 @@ function extractStreamsFromServer(serverData, serverName, mediaType, season, epi
title: `${mediaType === 'movie' ? 'Movie' : `S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`} ${tmdbId} - ${serverName}`, title: `${mediaType === 'movie' ? 'Movie' : `S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`} ${tmdbId} - ${serverName}`,
url: serverData[key], url: serverData[key],
quality: extractQualityFromUrl(serverData[key]) || '720p', quality: extractQualityFromUrl(serverData[key]) || '720p',
headers: {} headers: serverData[key].includes('vidora.stream') ? { Origin: 'https://vidora.stream', Referer: 'https://vidora.stream/' } : {}
}; };
// Add poster if available // Add poster if available