Add new providers for streaming services and utility functions

- Implement StreamFlix provider for fetching movie and episode streams.
- Add VaPlayer provider to retrieve streaming URLs for movies and TV shows.
- Introduce ZXCStreams provider for accessing various streaming links.
- Create utility functions for fetching episode counts from Cinemeta.
- Develop a title matching system for improved search results.
- Implement TMDB title to IMDB ID resolution for better integration with TMDB API.
This commit is contained in:
Inside4ndroid
2026-08-19 23:35:13 +01:00
parent 01f06f807e
commit 2c9a2cabbf
30 changed files with 2791 additions and 516 deletions
+162 -106
View File
@@ -124,43 +124,69 @@ function validateUrl(url) {
function makeRequest(url, options = {}) {
return new Promise((resolve, reject) => {
const urlObj = new URL(url);
const isHttps = urlObj.protocol === 'https:';
const httpModule = isHttps ? https : http;
const maxRedirects = options.maxRedirects || 10;
let redirectCount = 0;
const requestOptions = {
hostname: urlObj.hostname,
port: urlObj.port || (isHttps ? 443 : 80),
path: urlObj.pathname + urlObj.search,
method: options.method || 'GET',
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',
...options.headers
},
timeout: 30000
const doRequest = (currentUrl) => {
const urlObj = new URL(currentUrl);
const isHttps = urlObj.protocol === 'https:';
const httpModule = isHttps ? https : http;
const requestOptions = {
hostname: urlObj.hostname,
port: urlObj.port || (isHttps ? 443 : 80),
path: urlObj.pathname + urlObj.search,
method: options.method || 'GET',
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',
...options.headers
},
timeout: 30000
};
const req = httpModule.request(requestOptions, (res) => {
const redirectCodes = [301, 302, 303, 307, 308];
const isRedirect = redirectCodes.includes(res.statusCode);
const location = res.headers['location'];
if (options.allowRedirects === false && isRedirect) {
resolve({ statusCode: res.statusCode, headers: res.headers, url: currentUrl });
return;
}
if (isRedirect && location && redirectCount < maxRedirects) {
redirectCount++;
res.resume(); // Consume and discard the redirect body
let nextUrl;
try {
nextUrl = new URL(location, currentUrl).toString();
} catch (error) {
resolve({ statusCode: res.statusCode, headers: res.headers, body: '', url: currentUrl });
return;
}
console.log(`[4KHDHub] Following redirect (${redirectCount}/${maxRedirects}): ${currentUrl} -> ${nextUrl}`);
doRequest(nextUrl);
return;
}
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
if (options.parseHTML && data) {
const $ = cheerio.load(data);
resolve({ $: $, body: data, statusCode: res.statusCode, headers: res.headers, url: currentUrl });
} else {
resolve({ body: data, statusCode: res.statusCode, headers: res.headers, url: currentUrl });
}
});
});
req.on('error', reject);
req.on('timeout', () => reject(new Error('Request timeout')));
req.end();
};
const req = httpModule.request(requestOptions, (res) => {
if (options.allowRedirects === false && (res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 303 || res.statusCode === 307 || res.statusCode === 308)) {
resolve({ statusCode: res.statusCode, headers: res.headers });
return;
}
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
if (options.parseHTML && data) {
const $ = cheerio.load(data);
resolve({ $: $, body: data, statusCode: res.statusCode, headers: res.headers });
} else {
resolve({ body: data, statusCode: res.statusCode, headers: res.headers });
}
});
});
req.on('error', reject);
req.on('timeout', () => reject(new Error('Request timeout')));
req.end();
doRequest(url);
});
}
@@ -671,8 +697,8 @@ function extractHubCloudLinks(url, referer) {
const altElement = $(selector).first();
if (altElement.length > 0) {
const rawHref = altElement.attr('href');
if (rawHref) {
href = rawHref.startsWith('http') ? rawHref : `${baseUrl.replace(/\/$/, '')}/${rawHref.replace(/^\//, '')}`;
if (rawHref && /^https?:\/\//i.test(rawHref)) {
href = rawHref;
console.log(`[4KHDHub] Found download link with selector ${selector}: ${href}`);
found = true;
break;
@@ -719,6 +745,22 @@ function extractHubCloudLinks(url, referer) {
const downloadButtons = $('div.card-body h2 a.btn');
console.log(`[4KHDHub] Found ${downloadButtons.length} download buttons`);
// Apply JS-assigned pixel server hrefs (var pxl = "https://...") — the page
// overwrites the pixeldrain button href at runtime, so prefer the JS value
const pxlMatch = (response.body || '').match(/var\s+pxl\s*=\s*["']([^"']+)["']/);
if (pxlMatch && pxlMatch[1]) {
const pxlUrl = pxlMatch[1];
let applied = false;
$('a.btn[href*="pixeldrain"]').each((i, el) => {
$(el).attr('href', pxlUrl);
applied = true;
});
if (!applied) {
$('div.card-body h2').append(`<a href="${pxlUrl}" class="btn">Download [PixelServer]</a>`);
}
console.log(`[4KHDHub] Applied JS-assigned pixel server URL: ${pxlUrl}`);
}
if (downloadButtons.length === 0) {
// Try alternative selectors for download buttons
const altSelectors = ['a.btn', '.btn', 'a[href]'];
@@ -904,12 +946,12 @@ function extractHubCloudLinks(url, referer) {
} else if (link.includes('pixeldra')) {
console.log(`[4KHDHub] Button ${index + 1} is Pixeldrain`);
// Convert pixeldrain.net/u/ID format to pixeldrain.net/api/file/ID format
// Convert pixeldrain /u/ID format to /api/file/ID format (any pixeldrain host)
let convertedLink = link;
const pixeldrainMatch = link.match(/pixeldrain\.net\/u\/([a-zA-Z0-9]+)/);
const pixeldrainMatch = link.match(/pixeldrain\.(?:dev|net|com)\/u\/([a-zA-Z0-9]+)/);
if (pixeldrainMatch) {
const fileId = pixeldrainMatch[1];
convertedLink = `https://pixeldrain.net/api/file/${fileId}`;
convertedLink = link.replace(/\/u\/([a-zA-Z0-9]+)/, `/api/file/${fileId}`);
console.log(`[4KHDHub] Converted Pixeldrain URL from ${link} to ${convertedLink}`);
}
@@ -983,74 +1025,76 @@ function extractHubCloudLinks(url, referer) {
headers: {}
});
});
} else if (text.includes('10Gbps')) {
console.log(`[4KHDHub] Button ${index + 1} is 10Gbps server, following redirects...`);
// Handle 10Gbps server with multiple redirects
let currentLink = link;
} else if (text.includes('10Gbps') || /pixel\.hubcloud/i.test(link) || link.includes('dl.php')) {
console.log(`[4KHDHub] Button ${index + 1} is 10Gbps/pixel server, resolving final link...`);
// Follow redirects (pixel.hubcloud.cx -> worker -> gamerxyt.com/dl.php?link=<file>)
// and extract the 'link' query parameter from the final effective URL
makeRequest(link, { parseHTML: false })
.then(response => {
const finalUrl = response.url || link;
let decodedUrl = '';
const followRedirects = () => {
return makeRequest(currentLink, {
parseHTML: false,
allowRedirects: false
})
.then(response => {
const redirectUrl = response.headers['location'];
if (!redirectUrl) {
throw new Error('No redirect found');
}
console.log(`[4KHDHub] 10Gbps redirect: ${redirectUrl}`);
if (redirectUrl.includes('id=')) {
// Final redirect, extract the link parameter
const finalLink = redirectUrl.split('link=')[1];
if (finalLink) {
console.log(`[4KHDHub] 10Gbps final link: ${finalLink}`);
const decodedUrl = decodeURIComponent(finalLink);
// Get actual filename from HEAD request
return getFilenameFromUrl(decodedUrl)
.then(actualFilename => {
const displayFilename = actualFilename || headerDetails || 'Unknown';
const titleParts = [];
if (displayFilename) titleParts.push(displayFilename);
if (size) titleParts.push(size);
const finalTitle = titleParts.join('\n');
return {
name: `4KHDHub - 10Gbps Server${qualityLabel}`,
title: finalTitle,
url: decodedUrl,
quality: quality,
provider: '4khdhub',
headers: {}
};
})
.catch(() => {
const displayFilename = headerDetails || 'Unknown';
const titleParts = [];
if (displayFilename) titleParts.push(displayFilename);
if (size) titleParts.push(size);
const finalTitle = titleParts.join('\n');
return {
name: `4KHDHub - 10Gbps Server${qualityLabel}`,
title: finalTitle,
url: decodedUrl,
quality: quality,
provider: '4khdhub',
headers: {}
};
});
try {
const urlObj = new URL(finalUrl);
const linkParam = urlObj.searchParams.get('link');
if (linkParam) {
decodedUrl = linkParam;
if (decodedUrl.includes('%')) {
try { decodedUrl = decodeURIComponent(decodedUrl); } catch { /* keep as-is */ }
}
throw new Error('Final link not found');
} else {
currentLink = redirectUrl;
return followRedirects();
}
});
};
} catch { /* ignore */ }
followRedirects()
if (!decodedUrl) {
// Old-style fallback: look for link= in the redirect URL
const oldMatch = finalUrl.match(/[?&]link=([^&]+)/);
if (oldMatch) {
decodedUrl = oldMatch[1];
try { decodedUrl = decodeURIComponent(decodedUrl); } catch { /* keep as-is */ }
}
}
if (!decodedUrl) {
throw new Error('Final link not found');
}
console.log(`[4KHDHub] 10Gbps final link: ${decodedUrl}`);
// Get actual filename from HEAD request
return getFilenameFromUrl(decodedUrl)
.then(actualFilename => {
const displayFilename = actualFilename || headerDetails || 'Unknown';
const titleParts = [];
if (displayFilename) titleParts.push(displayFilename);
if (size) titleParts.push(size);
const finalTitle = titleParts.join('\n');
return {
name: `4KHDHub - 10Gbps Server${qualityLabel}`,
title: finalTitle,
url: decodedUrl,
quality: quality,
provider: '4khdhub',
headers: {}
};
})
.catch(() => {
const displayFilename = headerDetails || 'Unknown';
const titleParts = [];
if (displayFilename) titleParts.push(displayFilename);
if (size) titleParts.push(size);
const finalTitle = titleParts.join('\n');
return {
name: `4KHDHub - 10Gbps Server${qualityLabel}`,
title: finalTitle,
url: decodedUrl,
quality: quality,
provider: '4khdhub',
headers: {}
};
});
})
.then(result => {
console.log(`[4KHDHub] 10Gbps processing completed`);
resolve(result);
@@ -1368,6 +1412,12 @@ function extractHubDriveLinks(url, referer) {
console.log(`[4KHDHub] HubDrive extracted info - Size: ${size}, Header: ${header}, Quality: ${quality}, HeaderDetails: ${headerDetails}`);
// Guard against dead/expired HubDrive files ("File not found" page)
if (/file not found/i.test(header)) {
console.log('[4KHDHub] HubDrive file not found, skipping');
return [];
}
// Extract filename from header for title display
let filename = headerDetails || header || 'Unknown';
// Clean up the filename by removing common prefixes and file extensions
@@ -1393,8 +1443,12 @@ function extractHubDriveLinks(url, referer) {
for (const selector of alternatives) {
foundBtn = $(selector).first();
if (foundBtn.length > 0) {
console.log(`[4KHDHub] Found download button with selector: ${selector}`);
break;
const altHref = foundBtn.attr('href');
if (altHref && /^https?:\/\//i.test(altHref)) {
console.log(`[4KHDHub] Found download button with selector: ${selector}`);
break;
}
foundBtn = null;
}
}
@@ -1770,7 +1824,9 @@ async function get4KHDHubStreams(tmdbId, type, season = null, episode = null) {
const suspiciousPatterns = [
'www-google-com.cdn.ampproject.org',
'bloggingvector.shop',
'cdn.ampproject.org'
'cdn.ampproject.org',
'hubcloud.cx/tg', // Telegram share redirects, not playable streams
'/tg/go'
];
const isSuspicious = suspiciousPatterns.some(pattern => url.includes(pattern));
+417
View File
@@ -0,0 +1,417 @@
const { createDecipheriv } = require('crypto');
const { getDetails } = require('../utils/tmdb');
const CASTLE_BASE = 'https://api.hlowb.com';
const PKG = 'com.external.castle';
const CHANNEL = 'IndiaA';
const CLIENT = '1';
const LANG = 'en-US';
const API_HEADERS = {
'User-Agent': 'okhttp/4.9.3',
'Accept': 'application/json',
'Accept-Language': 'en-US,en;q=0.9',
'Connection': 'Keep-Alive',
'Referer': CASTLE_BASE
};
const PLAYBACK_HEADERS = {
'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',
'Accept': 'video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5',
'Accept-Language': 'en-US,en;q=0.9',
'Accept-Encoding': 'identity',
'Connection': 'keep-alive',
'Sec-Fetch-Dest': 'video',
'Sec-Fetch-Mode': 'no-cors',
'Sec-Fetch-Site': 'cross-site',
'DNT': '1'
};
function castleSafeParse(text) {
const safe = text.replace(/([:{[,]\s*)(\d{16,})/g, '$1"$2"');
return JSON.parse(safe);
}
function deriveKey(securityKey) {
const keyBytes = Buffer.from(securityKey, 'base64');
const suffix = Buffer.from('T!BgJB', 'utf8');
const combined = Buffer.concat([keyBytes, suffix]);
if (combined.length < 16) {
return Buffer.concat([combined, Buffer.alloc(16 - combined.length, 0)]);
}
return combined.subarray(0, 16);
}
function decryptCastle(cipherText, securityKey) {
const key = deriveKey(securityKey);
const decipher = createDecipheriv('aes-128-cbc', key, key);
decipher.setAutoPadding(true);
const decrypted = Buffer.concat([
decipher.update(Buffer.from(cipherText, 'base64')),
decipher.final()
]);
return decrypted.toString('utf8');
}
async function castleRequest(url, options = {}) {
const res = await fetch(url, {
...options,
headers: { ...API_HEADERS, ...(options.headers || {}) },
signal: AbortSignal.timeout(12000)
});
if (!res.ok) {
throw new Error(`[CastleTV] HTTP ${res.status}: ${res.statusText}`);
}
return res;
}
async function extractCipher(res) {
const text = await res.text();
const trimmed = text.trim();
if (!trimmed) throw new Error('[CastleTV] Empty response body');
try {
const parsed = JSON.parse(trimmed);
if (parsed.data && typeof parsed.data === 'string') {
return parsed.data.trim();
}
} catch {
// Not JSON — raw cipher text
}
return trimmed;
}
function unwrap(obj) {
if (obj && obj.data && typeof obj.data === 'object' && !Array.isArray(obj.data)) {
return obj.data;
}
return obj;
}
async function getSecurityKey() {
const url = `${CASTLE_BASE}/v0.1/system/getSecurityKey/1?channel=${CHANNEL}&clientType=${CLIENT}&lang=${LANG}`;
const res = await castleRequest(url);
const json = await res.json();
if (json.code !== 200 || !json.data) {
throw new Error(`[CastleTV] Security key error: ${JSON.stringify(json)}`);
}
return json.data;
}
async function searchCastle(secKey, keyword) {
const params = new URLSearchParams({
channel: CHANNEL,
clientType: CLIENT,
keyword,
lang: LANG,
mode: '1',
packageName: PKG,
page: '1',
size: '30'
});
const res = await castleRequest(`${CASTLE_BASE}/film-api/v1.1.0/movie/searchByKeyword?${params}`);
const cipher = await extractCipher(res);
return castleSafeParse(decryptCastle(cipher, secKey));
}
async function getCastleDetails(secKey, movieId) {
const url = `${CASTLE_BASE}/film-api/v1.9.9/movie?channel=${CHANNEL}&clientType=${CLIENT}&lang=${LANG}&movieId=${movieId}&packageName=${PKG}`;
const res = await castleRequest(url);
const cipher = await extractCipher(res);
return castleSafeParse(decryptCastle(cipher, secKey));
}
async function getVideoByLanguage(secKey, movieId, episodeId, languageId, resolution) {
const body = {
mode: '1',
appMarket: 'GuanWang',
clientType: CLIENT,
woolUser: 'false',
apkSignKey: 'ED0955EB04E67A1D9F3305B95454FED485261475',
androidVersion: '13',
movieId,
episodeId,
languageId,
isNewUser: 'true',
resolution: resolution.toString(),
packageName: PKG
};
const url = `${CASTLE_BASE}/film-api/v2.0.1/movie/getVideo2?clientType=${CLIENT}&packageName=${PKG}&channel=${CHANNEL}&lang=${LANG}`;
const res = await castleRequest(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const cipher = await extractCipher(res);
return castleSafeParse(decryptCastle(cipher, secKey));
}
async function getVideoShared(secKey, movieId, episodeId, resolution) {
const body = {
mode: '1',
appMarket: 'GuanWang',
clientType: CLIENT,
woolUser: 'false',
apkSignKey: 'ED0955EB04E67A1D9F3305B95454FED485261475',
androidVersion: '13',
movieId,
episodeId,
isNewUser: 'true',
resolution: resolution.toString(),
packageName: PKG
};
const url = `${CASTLE_BASE}/film-api/v2.0.1/movie/getVideo2?clientType=${CLIENT}&packageName=${PKG}&channel=${CHANNEL}&lang=${LANG}`;
const res = await castleRequest(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const cipher = await extractCipher(res);
return castleSafeParse(decryptCastle(cipher, secKey));
}
function resolutionLabel(res) {
const map = { 1: '480p', 2: '720p', 3: '1080p' };
return map[res] || `${res}p`;
}
const KNOWN_HEIGHTS = new Set([240, 360, 480, 540, 576, 720, 1080, 1440, 2160]);
function knownHeight(value) {
const n = Number(value);
return Number.isFinite(n) && KNOWN_HEIGHTS.has(n);
}
function resolutionNumToLabel(num) {
const map = { 1: '480p', 2: '720p', 3: '1080p', 4: '4K' };
return map[num] || null;
}
function streamQuality(url, description, resolutionNum, defaultQual) {
if (description) {
const m = /(?:SD|HD|FHD|UHD|4K)?\s*(\d{3,4})\s*p?/i.exec(String(description).trim());
if (m && knownHeight(m[1])) {
return `${m[1]}p`;
}
if (/4k|uhd/i.test(String(description))) return '4K';
}
const numLabel = resolutionNumToLabel(Number(resolutionNum));
if (numLabel) return numLabel;
if (url) {
const tokens = url.match(/[^/a-z](?:(\d{3,4})\s*p?)[^a-z]/gi);
if (tokens) {
for (const t of tokens) {
const m = /(\d{3,4})/i.exec(t);
if (m && knownHeight(m[1])) return `${m[1]}p`;
}
}
}
return defaultQual;
}
function formatSize(bytes) {
if (typeof bytes !== 'number' || bytes <= 0) return 'Unknown';
if (bytes > 1000000000) return `${(bytes / 1000000000).toFixed(2)} GB`;
return `${(bytes / 1000000).toFixed(0)} MB`;
}
function buildCastleStreams(raw, langLabel, titleLine, resolution) {
const data = unwrap(raw);
if (!data.videoUrl && !((data.videos || []).length)) return [];
const defaultQual = resolutionLabel(resolution);
const subtitles = (data.subtitles || [])
.filter((s) => typeof s.url === 'string' && s.url.length > 0)
.map((s, i) => ({
url: s.url.replace(/ /g, '%20'),
lang: s.abbreviate || s.title || 'Unknown',
id: `castle-${s.languageId || s.abbreviate || i}`
}));
const streams = [];
if (data.videos && data.videos.length > 0) {
const bestByUrl = new Map();
for (const v of data.videos) {
const videoUrl = v.url || data.videoUrl;
if (!videoUrl) continue;
const resNum = Number(v.resolution) || 0;
const qual = streamQuality(
videoUrl,
v.resolutionDescription,
resNum,
defaultQual
);
const existing = bestByUrl.get(videoUrl);
if (existing && existing.resNum >= resNum) continue;
const nameTag = langLabel ? `CastleTV ${langLabel}` : 'CastleTV';
bestByUrl.set(videoUrl, {
resNum,
stream: {
name: `${nameTag} | ${qual}`,
title: `${titleLine}\n${qual} | ${formatSize(v.size)} | Castle`,
url: videoUrl,
quality: qual,
provider: 'CastleTV',
headers: PLAYBACK_HEADERS,
...(subtitles.length ? { subtitles } : {})
}
});
}
for (const { stream } of bestByUrl.values()) {
streams.push(stream);
}
} else {
const videoUrl = data.videoUrl;
if (!videoUrl) return [];
const qual = streamQuality(videoUrl, data.resolutionDescription, 0, defaultQual);
const nameTag = langLabel ? `CastleTV ${langLabel}` : 'CastleTV';
streams.push({
name: `${nameTag} | ${qual}`,
title: `${titleLine}\n${qual} | ${formatSize(data.size)} | Castle`,
url: videoUrl,
quality: qual,
provider: 'CastleTV',
headers: PLAYBACK_HEADERS,
...(subtitles.length ? { subtitles } : {})
});
}
return streams;
}
function pickPreferredTracks(tracks) {
if (!tracks || !tracks.length) return tracks || [];
const withVideo = tracks.filter((t) => t.existIndividualVideo === true);
return withVideo.length > 0 ? withVideo : tracks;
}
async function getCastletvStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) {
console.log(`[CastleTV] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}`);
try {
const type = mediaType === 'tv' ? 'series' : 'movie';
const tmdbType = mediaType === 'tv' ? 'tv' : 'movie';
const details = await getDetails(tmdbType, tmdbId);
const title = (details && (details.title || details.name)) || '';
const year = (details && (details.release_date || details.first_air_date || '').slice(0, 4)) || null;
if (!title) {
console.log(`[CastleTV] No TMDB title resolved for ${tmdbId}`);
return [];
}
const season = seasonNum || 1;
const episode = episodeNum || 1;
const titleLine = type === 'series'
? `${title} S${String(season).padStart(2, '0')}E${String(episode).padStart(2, '0')}${year ? ` (${year})` : ''}`
: `${title}${year ? ` (${year})` : ''}`;
const secKey = await getSecurityKey();
const keyword = year ? `${title} ${year}` : title;
const searchResult = await searchCastle(secKey, keyword);
const rows = (unwrap(searchResult).rows || []);
if (rows.length === 0) {
console.log(`[CastleTV] No search results for "${keyword}"`);
return [];
}
const titleLc = title.toLowerCase();
const match = rows.find((r) => {
const name = (r.title || r.name || '').toLowerCase();
return name.includes(titleLc) || titleLc.includes(name);
}) || rows[0];
if (!match) return [];
const castleId = (match.id || match.redirectId || match.redirectIdStr || '').toString();
if (!castleId) return [];
let castleDetails = await getCastleDetails(secKey, castleId);
let activeId = castleId;
if (type === 'series') {
const seasons = (unwrap(castleDetails).seasons || []);
const seasonEntry = seasons.find((s) => s.number === season);
if (seasonEntry && seasonEntry.movieId && seasonEntry.movieId.toString() !== castleId) {
castleDetails = await getCastleDetails(secKey, seasonEntry.movieId.toString());
activeId = seasonEntry.movieId.toString();
}
}
const episodes = (unwrap(castleDetails).episodes || []);
let episodeId = null;
if (type === 'series') {
const ep = episodes.find((e) => e.number === episode);
episodeId = ep && ep.id ? ep.id.toString() : null;
} else {
episodeId = episodes[0] && episodes[0].id ? episodes[0].id.toString() : null;
}
if (!episodeId) {
console.log(`[CastleTV] No episode found (${season}/${episode})`);
return [];
}
const epEntry = episodes.find((e) => e.id && e.id.toString() === episodeId);
const allTracks = (epEntry && epEntry.tracks) || [];
const tracks = pickPreferredTracks(allTracks);
const streams = [];
const seenUrls = new Set();
if (tracks.length > 0) {
const langJobs = tracks.map((track) =>
Promise.allSettled(
[3, 2, 1].map((resolution) =>
getVideoByLanguage(secKey, activeId, episodeId, String(track.languageId), resolution)
.then((raw) => ({ raw, resolution, track }))
)
)
);
const allResults = await Promise.all(langJobs);
for (const trackResults of allResults) {
for (const r of trackResults) {
if (r.status !== 'fulfilled') continue;
const { raw, resolution, track } = r.value;
const langLabel = `[${track.languageName || track.abbreviate || 'Unknown'}]`;
for (const s of buildCastleStreams(raw, langLabel, titleLine, resolution)) {
if (!seenUrls.has(s.url)) {
seenUrls.add(s.url);
streams.push(s);
}
}
}
}
}
if (streams.length === 0) {
const sharedResults = await Promise.allSettled(
[3, 2, 1].map((resolution) =>
getVideoShared(secKey, activeId, episodeId, resolution)
.then((raw) => ({ raw, resolution }))
)
);
for (const r of sharedResults) {
if (r.status !== 'fulfilled') continue;
const { raw, resolution } = r.value;
for (const s of buildCastleStreams(raw, '', titleLine, resolution)) {
if (!seenUrls.has(s.url)) {
seenUrls.add(s.url);
streams.push(s);
}
}
}
}
console.log(`[CastleTV] Got ${streams.length} stream(s) for "${title}"`);
return streams;
} catch (err) {
console.error(`[CastleTV] Error: ${err.message}`);
return [];
}
}
module.exports = { getCastletvStreams };
+128
View File
@@ -0,0 +1,128 @@
const { getDetails, resolveImdbId } = require('../utils/tmdb');
const { findBestMatchWithRetry } = require('../utils/titleMatch');
const { tmdbTitleToImdbId } = require('../utils/tmdbTitleToImdb');
const HDGHARTV_API = 'https://hdghartv.cc/api';
const HEADERS = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept': 'application/json, */*',
'Referer': 'https://hdghartv.cc/'
};
async function fetchJson(url, timeoutMs = 10000) {
try {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
const res = await fetch(url, { headers: HEADERS, signal: controller.signal });
clearTimeout(timer);
if (!res.ok) return null;
return await res.json();
} catch (err) {
console.warn(`[HDGharTV] fetch failed for ${url}: ${err.message}`);
return null;
}
}
async function searchHdghartv(title, kind, variants) {
const searchByVariant = async (variantTitle) => {
const encoded = encodeURIComponent(variantTitle);
const data = await fetchJson(`${HDGHARTV_API}/search?q=${encoded}`);
if (!data) return [];
const results = kind === 'movie' ? data.movies : data.series;
return (results || []).map((r) => ({ title: r.title, type: kind, raw: r }));
};
const { best } = await findBestMatchWithRetry(
{ title, type: kind },
variants,
searchByVariant,
{ provider: 'HDGharTV' }
);
if (!best) return null;
return { id: best.raw._id, title: best.raw.title };
}
const QUALITY_RANK = { '4K': 0, '1080p': 1, '720p': 2, '480p': 3, '360p': 4 };
function sortLinks(links) {
return [...links].sort((a, b) => (QUALITY_RANK[a.quality] || 99) - (QUALITY_RANK[b.quality] || 99));
}
function linksToStreams(links, labelSuffix = '') {
return sortLinks(links.filter((l) => l.isActive && l.url)).map((l) => ({
name: 'HDGharTV',
title: `HDGharTV${labelSuffix ? ` ${labelSuffix}` : ''} · ${l.quality}`,
url: l.url,
quality: l.quality,
provider: 'HDGharTV',
headers: HEADERS
}));
}
async function getHdghartvStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) {
console.log(`[HDGharTV] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}`);
try {
const kind = mediaType === 'tv' ? 'series' : 'movie';
const tmdbType = mediaType === 'tv' ? 'tv' : 'movie';
const details = await getDetails(tmdbType, tmdbId);
const title = (details && (details.title || details.name)) || '';
if (!title) {
console.log(`[HDGharTV] No TMDB title resolved for ${tmdbId}`);
return [];
}
const variants = [title];
const imdbId = await resolveImdbId(tmdbType, tmdbId);
const match = await searchHdghartv(title, kind, variants);
if (!match) {
console.log(`[HDGharTV] "${title}" not found`);
return [];
}
if (imdbId && imdbId.startsWith('tt')) {
const resolvedId = await tmdbTitleToImdbId(match.title, undefined, kind).catch(() => null);
if (resolvedId && resolvedId !== imdbId) {
console.log(`[HDGharTV] IMDB ID mismatch — rejecting match: expected ${imdbId}, resolved ${resolvedId}`);
return [];
}
}
if (kind === 'movie') {
const movie = await fetchJson(`${HDGHARTV_API}/movies/public/${match.id}`);
if (!movie || !movie.streamingLinks || !movie.streamingLinks.length) {
console.log(`[HDGharTV] No streaming links for movie "${match.title}"`);
return [];
}
return linksToStreams(movie.streamingLinks);
}
const season = seasonNum || 1;
const episode = episodeNum || 1;
const series = await fetchJson(`${HDGHARTV_API}/series/public/${match.id}`);
if (!series || !series.seasons || !series.seasons.length) {
console.log(`[HDGharTV] No seasons for series "${match.title}"`);
return [];
}
const seasonData = series.seasons.find((s) => s.seasonNumber === season);
if (!seasonData) {
console.log(`[HDGharTV] Season ${season} not found`);
return [];
}
const episodeData = seasonData.episodes && seasonData.episodes.find((e) => e.episodeNumber === episode);
if (!episodeData || !episodeData.streamingLinks || !episodeData.streamingLinks.length) {
console.log(`[HDGharTV] Episode ${episode} or links not found`);
return [];
}
return linksToStreams(episodeData.streamingLinks, `S${season}E${episode}`);
} catch (err) {
console.error(`[HDGharTV] Error: ${err.message}`);
return [];
}
}
module.exports = { getHdghartvStreams };
-118
View File
@@ -1,118 +0,0 @@
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://snowhouse.lordflix.club';
const MULTI_DECRYPT_API = 'https://enc-dec.app/api';
const SERVERS = ['Berlin', 'Marseille', 'Backrooms', 'Phoenix', 'Oslo', 'Luna', 'Sakura', 'Rio', 'Ativa', 'Moscow'];
// 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 };
+303
View File
@@ -0,0 +1,303 @@
const { getDetails } = require('../utils/tmdb');
const STREAM_CACHE_TTL = 25 * 60 * 1000;
const PLATFORM_ORDER = ['netflix', 'primevideo', 'hotstar', 'disney'];
const UA_POOL = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_4_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Safari/605.1.15',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0',
'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.82 Mobile Safari/537.36'
];
const ACCEPT_LANG_POOL = [
'en-US,en;q=0.9',
'en-GB,en;q=0.9',
'en-IN,en;q=0.9,hi;q=0.7',
'en-US,en;q=0.8,es;q=0.5'
];
let uaIndex = 0;
let langIndex = 0;
function nextUA() { return UA_POOL[uaIndex++ % UA_POOL.length]; }
function nextLang() { return ACCEPT_LANG_POOL[langIndex++ % ACCEPT_LANG_POOL.length]; }
const streamCache = new Map();
const inflight = new Map();
let resolvedApiUrl = '';
const NEW_TV_DOMAINS = [
'aHR0cHM6Ly9tb2JpbGVkZXRlY3RzLmNvbQ==',
'aHR0cHM6Ly9tb2JpbGVkZXRlY3QuYXBw',
'aHR0cHM6Ly9tb2JpZGV0ZWN0LmFydA==',
'aHR0cHM6Ly9tb2JpZGV0ZWN0LmNj',
'aHR0cHM6Ly9tb2JpZGV0ZWN0LmNsaWNr',
'aHR0cHM6Ly9tb2JpZGV0ZWN0Lmluaw==',
'aHR0cHM6Ly9tb2JpZGV0ZWN0LmxpdmU=',
'aHR0cHM6Ly9tb2JpZGV0ZWN0LnBybw==',
'aHR0cHM6Ly9tb2JpZGV0ZWN0LnNob3A=',
'aHR0cHM6Ly9tb2JpZGV0ZWN0LnNpdGU=',
'aHR0cHM6Ly9tb2JpZGV0ZWN0LnNwYWNl',
'aHR0cHM6Ly9tb2JpZGV0ZWN0LnN0b3Jl',
'aHR0cHM6Ly9tb2JpZGV0ZWN0LnZpcA==',
'aHR0cHM6Ly9tb2JpZGV0ZWN0Lndpa2k=',
'aHR0cHM6Ly9tb2JpZGV0ZWN0Lnh5eg=='
];
const PLATFORM_MAP = { netflix: { ott: 'nf' }, primevideo: { ott: 'pv' }, hotstar: { ott: 'hs' }, disney: { ott: 'hs' } };
function apiBase() {
return (process.env.NETMIRROR_API_BASE || 'https://net27.cc').replace(/\/$/, '');
}
function streamReferer() {
return process.env.NETMIRROR_STREAM_REFERER || 'https://videodownloader.site/';
}
function newTvHeaders(ott, extra = {}) {
return {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0',
'X-Requested-With': 'NetmirrorNewTV v1.0',
'Accept': 'application/json, text/plain, */*',
'Ott': ott,
'User-Agent': nextUA(),
'Accept-Language': nextLang(),
...extra
};
}
async function resolveNewTvApi() {
if (resolvedApiUrl) return resolvedApiUrl;
const custom = (process.env.NEWTV_DOMAINS || '')
.split(',')
.map((value) => value.trim().replace(/\/$/, ''))
.filter(Boolean);
const domains = [
...custom,
...NEW_TV_DOMAINS.map((value) => Buffer.from(value, 'base64').toString('utf8'))
];
for (const domain of domains) {
try {
const response = await fetch(`${domain}/checknewtv.php`, {
headers: { ...newTvHeaders('nf') },
signal: AbortSignal.timeout(10000)
});
const data = await response.json();
if (data.token_hash) {
resolvedApiUrl = Buffer.from(data.token_hash, 'base64').toString('utf8').replace(/\/$/, '');
return resolvedApiUrl;
}
} catch {
// Try the next rotating discovery domain.
}
}
throw new Error('NetMirror NewTV API discovery failed');
}
async function fetchNetflix(tmdbId, type, title, season, episode) {
try {
const base = apiBase();
const url = type === 'series'
? `${base}/api/embed-tmdb/${tmdbId}?type=tv&se=${season}&ep=${episode}`
: `${base}/api/embed-tmdb/${tmdbId}`;
const response = await fetch(url, {
headers: {
Accept: 'application/json, text/plain, */*',
Referer: `${base}/`,
'User-Agent': nextUA(),
'Accept-Language': nextLang()
},
signal: AbortSignal.timeout(20000)
});
if (!response.ok) return [];
const data = await response.json();
if (data.ok !== true) return [];
const subtitles = (data.captions || []).flatMap((caption, index) => {
if (!caption.url) return [];
return [{
id: `netflix-${index}-${caption.lang || 'en'}`,
url: caption.url.startsWith('/') ? `${base}${caption.url}` : caption.url,
lang: caption.lang || 'en',
label: caption.name || 'English'
}];
});
const headers = { Referer: streamReferer(), 'User-Agent': nextUA() };
const streams = (data.streams || []).filter((stream) => stream.url).map((stream) => ({
name: 'NetMirror | Netflix',
title: `${title}\n${stream.resolution ? `${stream.resolution}p` : 'Auto'}`,
url: stream.url,
quality: stream.resolution ? `${stream.resolution}p` : 'Auto',
provider: 'NetMirror',
headers,
...(subtitles.length ? { subtitles } : {})
}));
if (streams.length || !data.mp4) return streams;
return [{
name: 'NetMirror | Netflix',
title: `${title}\nAuto`,
url: data.mp4,
quality: 'Auto',
provider: 'NetMirror',
headers,
...(subtitles.length ? { subtitles } : {})
}];
} catch (error) {
console.log(`[NetMirror] Netflix direct failed: ${error.message}`);
return [];
}
}
function parseNumber(value) {
if (!value) return null;
const parsed = parseInt(String(value).replace(/[^\d]/g, ''), 10);
return Number.isFinite(parsed) ? parsed : null;
}
async function getEpisodes(api, showId, postData, config) {
const result = [];
const selectedSeason = Array.isArray(postData.season) ? postData.season.findIndex((item) => item.selected) : -1;
const selectedSeasonId = selectedSeason >= 0 ? postData.season[selectedSeason].id : postData.nextPageSeason;
const add = (episode, seasonNumber) => {
result.push({
id: episode.id,
s: seasonNumber || parseNumber(episode.sNum),
ep: parseNumber(episode.ep) || parseNumber(episode.epNum)
});
};
for (const episode of postData.episodes || []) {
if (episode) add(episode, selectedSeason >= 0 ? selectedSeason + 1 : null);
}
if (postData.nextPageShow === 1 && selectedSeasonId) {
try {
const response = await fetch(
`${api}/newtv/episodes.php?id=${encodeURIComponent(selectedSeasonId)}&page=2`,
{ headers: newTvHeaders(config.ott), signal: AbortSignal.timeout(15000) }
);
const data = await response.json();
for (const episode of data.episodes || []) {
if (episode) add(episode, selectedSeason >= 0 ? selectedSeason + 1 : null);
}
} catch {
// The first page is still useful when pagination is unavailable.
}
}
return result;
}
async function fetchPlatform(platform, title, type, season, episode) {
try {
const config = PLATFORM_MAP[platform];
const api = await resolveNewTvApi();
const searchResponse = await fetch(
`${api}/newtv/search.php?s=${encodeURIComponent(title)}`,
{ headers: newTvHeaders(config.ott), signal: AbortSignal.timeout(15000) }
);
const searchData = await searchResponse.json();
const first = (searchData.searchResult || [])[0];
if (!first || !first.id) return [];
const postResponse = await fetch(
`${api}/newtv/post.php?id=${encodeURIComponent(first.id)}`,
{ headers: newTvHeaders(config.ott, { Lastep: '', Usertoken: '' }), signal: AbortSignal.timeout(15000) }
);
const postData = await postResponse.json();
let targetId = first.id;
if (type === 'series') {
const episodes = await getEpisodes(api, first.id, postData, config);
const target = episodes.find((item) => item.s === season && item.ep === episode);
if (!target) return [];
targetId = target.id;
} else if (postData.type === 't' || ((postData.episodes || []).filter(Boolean).length > 0)) {
return [];
} else {
targetId = postData.main_id || first.id;
}
const playerResponse = await fetch(
`${api}/newtv/player.php?id=${encodeURIComponent(targetId)}`,
{ headers: newTvHeaders(config.ott, { Usertoken: '' }), signal: AbortSignal.timeout(15000) }
);
const player = await playerResponse.json();
if (player.status && player.status !== 'ok') {
console.log(`[NetMirror] ${platform} player status: ${player.status} — accepting if link present`);
}
if (!player.video_link) return [];
return [{
name: `NetMirror | ${platform === 'primevideo' ? 'Prime Video' : platform.charAt(0).toUpperCase() + platform.slice(1)}`,
title: `${title}\nAuto`,
url: player.video_link,
quality: 'Auto',
provider: 'NetMirror',
headers: {
Referer: player.referer || api,
'User-Agent': nextUA()
}
}];
} catch (error) {
console.log(`[NetMirror] ${platform} failed: ${error.message}`);
return [];
}
}
function hdOnly(streams, forceHd) {
if (!forceHd) return streams;
const hd = streams.filter((stream) => {
const match = (stream.quality || '').match(/(\d+)p/i);
return !match || Number(match[1]) >= 720;
});
return hd.length ? hd : streams;
}
async function getNetmirrorStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) {
console.log(`[NetMirror] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}`);
const type = mediaType === 'tv' ? 'series' : 'movie';
const season = mediaType === 'tv' ? (seasonNum || null) : null;
const episode = mediaType === 'tv' ? (episodeNum || null) : null;
try {
const details = await getDetails(mediaType === 'tv' ? 'tv' : 'movie', tmdbId);
const title = (details && (details.title || details.name)) || '';
if (!title) {
console.log(`[NetMirror] No TMDB title resolved for ${tmdbId}`);
return [];
}
const key = `${tmdbId}:${type}:${season || 0}:${episode || 0}`;
const cached = streamCache.get(key);
if (cached && cached.expiresAt > Date.now()) {
return hdOnly(cached.streams, true);
}
const running = inflight.get(key);
if (running) return running;
const request = (async () => {
const direct = await fetchNetflix(tmdbId, type, title, season, episode);
const fallback = await Promise.all(
PLATFORM_ORDER
.filter((platform) => platform !== 'netflix' || direct.length === 0)
.map((platform) => fetchPlatform(platform, title, type, season, episode))
);
const streams = hdOnly([...direct, ...fallback.flat()], true);
streamCache.set(key, { streams, expiresAt: Date.now() + STREAM_CACHE_TTL });
console.log(`[NetMirror] ${tmdbId} streams ready: ${streams.length}`);
return streams;
})().finally(() => inflight.delete(key));
inflight.set(key, request);
return request;
} catch (err) {
console.error(`[NetMirror] Error: ${err.message}`);
return [];
}
}
module.exports = { getNetmirrorStreams };
-92
View File
@@ -1,92 +0,0 @@
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 };
+334
View File
@@ -0,0 +1,334 @@
const { createDecipheriv } = require('crypto');
const { getDetails, resolveImdbId } = require('../utils/tmdb');
const { getEpisodesPerSeason } = require('../utils/cinemetaEpisodes');
const { findBestMatch } = require('../utils/titleMatch');
const { tmdbTitleToImdbId } = require('../utils/tmdbTitleToImdb');
const MAIN_URL = 'https://api3.devcorp.me';
const AES_KEY = Buffer.from('im72charPasswordofdInitVectorStm', 'utf8');
const AES_IV = Buffer.from('im72charPassword', 'utf8');
const FETCH_HEADERS = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Referer': 'https://onetouchtv.xyz/'
};
const FETCH_TIMEOUT_MS = 15000;
const FETCH_RETRIES = 2;
const FETCH_RETRY_DELAY_MS = 400;
const CACHE_TTL_MS = 5 * 60 * 1000;
const cache = new Map();
function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
function fromCache(key) {
const entry = cache.get(key);
if (!entry) return null;
if (Date.now() - entry.ts > CACHE_TTL_MS) {
cache.delete(key);
return null;
}
return entry.data;
}
function toCache(key, data) {
cache.set(key, { data, ts: Date.now() });
}
function decrypt(encoded) {
let s = encoded
.replace(/-_\./g, '/')
.replace(/@/g, '+')
.replace(/\s+/g, '');
const pad = s.length % 4;
if (pad !== 0) s += '='.repeat(4 - pad);
const buf = Buffer.from(s, 'base64');
const decipher = createDecipheriv('aes-256-cbc', AES_KEY, AES_IV);
const dec = Buffer.concat([decipher.update(buf), decipher.final()]);
return JSON.parse(dec.toString('utf8'));
}
async function fetchEncryptedOnce(url) {
const res = await fetch(url, {
headers: FETCH_HEADERS,
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
});
if (!res.ok) throw new Error(`HTTP ${res.status} from ${url}`);
const text = await res.text();
const parsed = decrypt(text);
return parsed.result;
}
async function fetchEncrypted(path, cacheTtlMs = CACHE_TTL_MS) {
const url = path.startsWith('https://') ? path : `${MAIN_URL}${path}`;
if (cacheTtlMs > 0) {
const cached = fromCache(url);
if (cached !== null) return cached;
}
let lastErr;
for (let attempt = 0; attempt <= FETCH_RETRIES; attempt++) {
try {
const data = await fetchEncryptedOnce(url);
if (cacheTtlMs > 0) toCache(url, data);
return data;
} catch (err) {
lastErr = err;
if (attempt < FETCH_RETRIES) {
console.warn(`[OneTouchTV] fetch failed (attempt ${attempt + 1}): ${err.message}`);
await sleep(FETCH_RETRY_DELAY_MS * (attempt + 1));
}
}
}
throw lastErr;
}
const TYPE_MAP = {
movie: new Set(['movie']),
series: new Set(['drama', 'variety', 'anime'])
};
function isTypeCompatible(stremioType, onetouchType) {
const allowed = TYPE_MAP[stremioType];
if (!allowed) return true;
return allowed.has(onetouchType.toLowerCase());
}
function findBestOttMatch(results, title, year, stremioType, targetSeason) {
if (results.length === 0) return null;
const filtered = results.filter((r) => isTypeCompatible(stremioType, r.type));
if (filtered.length === 0) return null;
const candidates = filtered.map((r) => ({
title: r.title,
year: parseInt(r.year, 10) || undefined,
type: stremioType,
season: targetSeason ?? undefined,
raw: r
}));
const { best } = findBestMatch(
{
title,
year: year ?? undefined,
type: stremioType,
season: targetSeason ?? undefined
},
candidates,
{ provider: 'OneTouchTV' }
);
return best ? best.raw : null;
}
async function searchContent(keyword) {
const path = `/vod/search?keyword=${encodeURIComponent(keyword)}`;
try {
const results = await fetchEncrypted(path);
return Array.isArray(results) ? results : [];
} catch (err) {
console.error(`[OneTouchTV] search failed for "${keyword}": ${err.message}`);
return [];
}
}
async function getDetail(id) {
try {
const result = await fetchEncrypted(`/vod/${id}/detail`);
return result ?? null;
} catch (err) {
console.error(`[OneTouchTV] detail failed for ${id}: ${err.message}`);
return null;
}
}
async function getEpisodeStreams(id, playId) {
try {
const result = await fetchEncrypted(`/vod/${id}/episode/${playId}`, 0);
return result ?? null;
} catch (err) {
console.error(`[OneTouchTV] episode fetch failed ${id}/${playId}: ${err.message}`);
return null;
}
}
function resolveQuality(quality) {
if (!quality) return 'Unknown';
const q = quality.toLowerCase();
if (q === 'auto') return 'Auto';
if (q.includes('1080')) return '1080p';
if (q.includes('720')) return '720p';
if (q.includes('480')) return '480p';
if (q.includes('360')) return '360p';
return quality;
}
async function resolveAbsoluteEpisode(imdbId, season, episode) {
if (season <= 1) return episode;
const episodesPerSeason = await getEpisodesPerSeason(imdbId);
if (episodesPerSeason.length < season - 1) return null;
let offset = 0;
for (let s = 0; s < season - 1; s++) {
const count = episodesPerSeason[s] ?? 0;
if (count === 0) return null;
offset += count;
}
return offset + episode;
}
async function getOnetouchtvStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) {
console.log(`[OneTouchTV] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}`);
try {
const type = mediaType === 'tv' ? 'series' : 'movie';
const tmdbType = mediaType === 'tv' ? 'tv' : 'movie';
const details = await getDetails(tmdbType, tmdbId);
const title = (details && (details.title || details.name)) || '';
const year = (details && (details.release_date || details.first_air_date || '').slice(0, 4)) || null;
if (!title) {
console.log(`[OneTouchTV] No TMDB title resolved for ${tmdbId}`);
return [];
}
const imdbId = await resolveImdbId(tmdbType, tmdbId);
const season = seasonNum || 1;
const episode = episodeNum || 1;
const searchKeywords = [title];
if (type === 'series' && season > 1) {
searchKeywords.unshift(`${title} Season ${season}`);
}
let matchResult = null;
for (const keyword of searchKeywords) {
const results = await searchContent(keyword);
const searchYear = keyword !== title && season > 1 ? null : year;
const match = findBestOttMatch(results, keyword, searchYear, type, season);
if (match) {
matchResult = match;
break;
}
}
if (!matchResult) {
console.log(`[OneTouchTV] "${title}" not found`);
return [];
}
if (imdbId && imdbId.startsWith('tt')) {
const matchedYear = matchResult.year ? parseInt(matchResult.year, 10) : undefined;
const resolvedId = await tmdbTitleToImdbId(
matchResult.title,
Number.isFinite(matchedYear) ? matchedYear : undefined,
type
).catch(() => null);
if (resolvedId && resolvedId !== imdbId) {
console.log(`[OneTouchTV] IMDB ID mismatch — rejecting match: expected ${imdbId}, resolved ${resolvedId}`);
return [];
}
}
const detail = await getDetail(matchResult.id);
if (!detail || !detail.episodes || detail.episodes.length === 0) {
console.log(`[OneTouchTV] no episodes in detail for "${matchResult.title}"`);
return [];
}
let targetEpisode = null;
if (type === 'movie') {
targetEpisode = detail.episodes[0] ?? null;
} else {
const targetSeasonInTitle = new RegExp(`\\bseason\\s+${season}\\b`, 'i');
const anySeasonInTitle = /\bseason\s+(\d+)\b/i;
const matchedSeasonNum = anySeasonInTitle.exec(matchResult.title);
const entryIsForTargetSeason =
season === 1 || targetSeasonInTitle.test(matchResult.title);
if (
season > 1 &&
matchedSeasonNum !== null &&
parseInt(matchedSeasonNum[1], 10) !== season
) {
console.warn(`[OneTouchTV] matched title names a different season — aborting`);
return [];
}
if (entryIsForTargetSeason) {
targetEpisode = detail.episodes.find((e) => parseInt(e.episode, 10) === episode) ?? null;
} else {
let absEp = null;
if (imdbId) {
absEp = await resolveAbsoluteEpisode(imdbId, season, episode);
}
if (absEp !== null) {
targetEpisode = detail.episodes.find((e) => parseInt(e.episode, 10) === absEp) ?? null;
}
if (!targetEpisode && season === 1) {
targetEpisode = detail.episodes.find((e) => parseInt(e.episode, 10) === episode) ?? null;
}
}
}
if (!targetEpisode) {
console.log(`[OneTouchTV] episode not found (S${season}E${episode})`);
return [];
}
const streamData = await getEpisodeStreams(matchResult.id, targetEpisode.playId);
if (!streamData) return [];
const sources = streamData.sources ?? [];
if (sources.length === 0) {
console.log(`[OneTouchTV] no sources in episode for "${matchResult.title}"`);
return [];
}
const subtitles = [];
for (const track of streamData.track ?? []) {
if (track.file && track.name) {
subtitles.push({ url: track.file, lang: track.name });
}
}
const out = [];
for (const src of sources) {
if (!src.url) continue;
const quality = resolveQuality(src.quality);
const sourceName = src.name ? ` | ${src.name}` : '';
out.push({
name: `OneTouchTV${sourceName}`,
title: `${matchResult.title}\n${quality} · ${(src.type ?? 'HLS').toUpperCase()}`,
url: src.url,
quality,
provider: 'OneTouchTV',
headers: {
'User-Agent': 'Mozilla/5.0',
'Referer': `${MAIN_URL}/`
},
...(subtitles.length ? { subtitles } : {})
});
}
const seen = new Set();
const streams = out.filter((s) => {
if (seen.has(s.url)) return false;
seen.add(s.url);
return true;
});
console.log(`[OneTouchTV] Got ${streams.length} stream(s) for "${matchResult.title}"`);
return streams;
} catch (err) {
console.error(`[OneTouchTV] Error: ${err.message}`);
return [];
}
}
module.exports = { getOnetouchtvStreams };
+7 -2
View File
@@ -12,9 +12,14 @@ const providerFunctionMap = {
'vixsrc.js': 'getVixsrcStreams',
'videasy.js': 'getVideasyStreams',
'vidlink.js': 'getVidlinkStreams',
'lordflix.js': 'getLordflixStreams',
'notorrent.js': 'getNotorrentStreams',
'dahmermovies.js': 'getDahmermoviesStreams',
'streamflix.js': 'getStreamflixStreams',
'vaplayer.js': 'getVaplayerStreams',
'castletv.js': 'getCastletvStreams',
'hdghartv.js': 'getHdghartvStreams',
'netmirror.js': 'getNetmirrorStreams',
'onetouchtv.js': 'getOnetouchtvStreams',
'zxcstreams.js': 'getZxcstreamsStreams',
};
// Stats for debug endpoint
+133
View File
@@ -0,0 +1,133 @@
const axios = require('axios');
const API_BASE = 'https://api.streamflix.app';
const FIREBASE_BASE = 'https://chilflix-410be-default-rtdb.asia-southeast1.firebasedatabase.app';
const DATA_TTL = 30 * 60 * 1000;
const CONFIG_TTL = 5 * 60 * 1000;
const EPISODES_TTL = 60 * 60 * 1000;
let dataCache = null;
let configCache = null;
const episodesCache = new Map();
const REQUEST_HEADERS = {
'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',
'Accept': 'application/json, */*',
'Accept-Language': 'en-US,en;q=0.9'
};
async function getData() {
if (dataCache && Date.now() - dataCache.ts < DATA_TTL) return dataCache.items;
console.log('[StreamFlix] fetching data.json');
const res = await axios.get(`${API_BASE}/data.json`, { headers: REQUEST_HEADERS, timeout: 20000 });
const items = (res.data && res.data.data) || [];
dataCache = { items, ts: Date.now() };
console.log(`[StreamFlix] data.json cached (${items.length} items)`);
return items;
}
async function getConfig() {
if (configCache && Date.now() - configCache.ts < CONFIG_TTL) return configCache.config;
const res = await axios.get(`${API_BASE}/config/config-streamflixapp.json`, { headers: REQUEST_HEADERS, timeout: 8000 });
configCache = { config: res.data, ts: Date.now() };
return res.data;
}
async function getEpisodes(movieKey, season) {
const cacheKey = `${movieKey}:${season}`;
const cached = episodesCache.get(cacheKey);
if (cached && Date.now() - cached.ts < EPISODES_TTL) return cached.episodes;
const url = `${FIREBASE_BASE}/Data/${movieKey}/seasons/${season}/episodes.json`;
const res = await axios.get(url, { headers: REQUEST_HEADERS, timeout: 10000 });
const raw = res.data || {};
const episodes = {};
for (const [k, v] of Object.entries(raw)) {
episodes[parseInt(k, 10)] = v;
}
episodesCache.set(cacheKey, { episodes, ts: Date.now() });
return episodes;
}
function downloadBases(config) {
const seen = new Set();
const out = [];
for (const url of config.download || []) {
if (url && !seen.has(url)) {
seen.add(url);
out.push(url);
}
}
return out;
}
function subtitleHint(filename) {
const f = (filename || '').toLowerCase();
if (f.includes('esub') || f.includes('.srt') || f.includes('.ass') || f.includes('sub')) {
return ' [Embedded Subs]';
}
return '';
}
async function getStreamflixStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) {
console.log(`[StreamFlix] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}`);
try {
const [items, config] = await Promise.all([getData(), getConfig()]);
const match = items.find((item) => item.tmdb === String(tmdbId));
if (!match) {
console.log(`[StreamFlix] No match found for TMDB ID ${tmdbId}`);
return [];
}
const bases = downloadBases(config);
if (bases.length === 0) {
console.log(`[StreamFlix] No download CDN bases in config`);
return [];
}
if (mediaType === 'movie') {
if (!match.movielink) return [];
const subs = subtitleHint(match.movielink);
return bases.map((base, i) => ({
name: 'StreamFlix',
title: `StreamFlix${i > 0 ? ` Mirror ${i}` : ''}${subs} | ${match.moviename}`,
url: `${base}${match.movielink}`,
quality: 'Auto',
provider: 'StreamFlix',
headers: { 'User-Agent': REQUEST_HEADERS['User-Agent'] }
}));
}
if (seasonNum === null || episodeNum === null) return [];
try {
const episodes = await getEpisodes(match.moviekey, seasonNum);
const ep = episodes[episodeNum - 1] || episodes[episodeNum];
if (ep && ep.link) {
const subs = subtitleHint(ep.link);
return bases.map((base, i) => ({
name: 'StreamFlix',
title: `StreamFlix${i > 0 ? ` Mirror ${i}` : ''}${subs} | ${match.moviename} S${seasonNum}E${episodeNum}${ep.name ? ` • ${ep.name}` : ''}`,
url: `${base}${ep.link}`,
quality: 'Auto',
provider: 'StreamFlix',
headers: { 'User-Agent': REQUEST_HEADERS['User-Agent'] }
}));
}
} catch (err) {
console.log(`[StreamFlix] Firebase fetch failed: ${err.message}`);
}
return [];
} catch (err) {
console.error(`[StreamFlix] Error: ${err.message}`);
return [];
}
}
module.exports = { getStreamflixStreams };
+87
View File
@@ -0,0 +1,87 @@
const { resolveImdbId } = require('../utils/tmdb');
const STREAM_API = 'https://streamdata.vaplayer.ru/api.php';
const PLAYER_ORIGIN = 'https://nextgencloudfabric.com';
const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
async function fetchStreamUrls(imdb, type, season, episode) {
const params = new URLSearchParams({ imdb, type });
if (type === 'tv' && season != null && episode != null) {
params.set('season', String(season));
params.set('episode', String(episode));
}
const url = `${STREAM_API}?${params.toString()}`;
const referer = type === 'tv'
? `${PLAYER_ORIGIN}/embed/tv/${imdb}/${season}/${episode}`
: `${PLAYER_ORIGIN}/embed/movie/${imdb}`;
try {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 12000);
const resp = await fetch(url, {
signal: controller.signal,
headers: {
'User-Agent': USER_AGENT,
'Referer': referer,
'Origin': PLAYER_ORIGIN
}
});
clearTimeout(timer);
if (!resp.ok) {
throw new Error(`VaPlayer API HTTP ${resp.status}`);
}
const json = await resp.json();
if (json.status_code !== '200' && json.status_code !== 200) {
throw new Error(`VaPlayer API error: ${json.error || `status_code=${json.status_code}`}`);
}
return (json.data && json.data.stream_urls) || [];
} catch (err) {
console.warn(`[VaPlayer] fetch failed for ${imdb}: ${err.message}`);
return [];
}
}
function toStreams(streamUrls, labelSuffix = '') {
return streamUrls.map((url, i) => ({
name: 'VaPlayer',
title: `VaPlayer${labelSuffix ? ` ${labelSuffix}` : ''} · HLS ${i + 1}`,
url,
quality: 'Auto',
provider: 'VaPlayer',
headers: {
'Referer': `${PLAYER_ORIGIN}/`,
'Origin': PLAYER_ORIGIN,
'User-Agent': USER_AGENT
}
}));
}
async function getVaplayerStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) {
console.log(`[VaPlayer] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}`);
try {
const imdbId = await resolveImdbId(mediaType === 'tv' ? 'tv' : 'movie', tmdbId);
if (!imdbId || !imdbId.startsWith('tt')) {
console.log(`[VaPlayer] No IMDB ID resolved for TMDB ${tmdbId}`);
return [];
}
const urls = mediaType === 'tv' && seasonNum !== null && episodeNum !== null
? await fetchStreamUrls(imdbId, 'tv', seasonNum, episodeNum)
: await fetchStreamUrls(imdbId, 'movie');
const streams = toStreams(urls, mediaType === 'tv' ? `S${seasonNum}E${episodeNum}` : '');
console.log(`[VaPlayer] Got ${streams.length} stream(s) for ${imdbId}`);
return streams;
} catch (err) {
console.error(`[VaPlayer] Error: ${err.message}`);
return [];
}
}
module.exports = { getVaplayerStreams };
+154 -54
View File
@@ -8,21 +8,91 @@ const VIDEASY_HEADERS = {
'Referer': 'https://player.videasy.net/'
};
// Each server maps a name to its API URL. moviesOnly:true skips TV requests.
const VIDEASY_API = 'https://api.speedracelight.com';
// Each server maps a name to its API route. 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' }
'CDN': { url: `${VIDEASY_API}/cdn/sources-with-title` },
'LaMovie': { url: `${VIDEASY_API}/lamovie/sources-with-title` },
'Meine': { url: `${VIDEASY_API}/meine/sources-with-title`, moviesOnly: true }
};
const DECRYPT_URL = 'https://enc-dec.app/api/dec-videasy';
// --- Seed-based response decryption (mirrors player.videasy.net) ---
const MAGIC = [109, 118, 109, 49]; // "mvm1"
const HASH_TABLE = [1116352408, 1899447441, 3049323471, 3921009573, 961987163, 1508970993, 2453635748, 2870763221, 3624381080, 310598401, 607225278, 1426881987, 1925078388, 2162078206, 2614888103, 3248222580];
function u32x(x) { return x >>> 0; }
function mul32(a, b) { return Math.imul(a, b) >>> 0; }
function rotl32(x, n) {
x >>>= 0;
n &= 31;
return n === 0 ? x : ((x << n) | (x >>> (32 - n))) >>> 0;
}
function hash32(x) {
x = u32x(x);
x ^= x >>> 16;
x = mul32(x, 2246822507);
x ^= x >>> 13;
x = mul32(x, 3266489909);
x ^= x >>> 16;
return u32x(x);
}
function fnv1a(str) {
let h = 2166136261;
for (let i = 0; i < str.length; i++) h = mul32(h ^ str.charCodeAt(i), 16777619);
return hash32(h);
}
function initStream(seed, secondKey) {
const S = new Array(61);
let a = u32x(hash32(fnv1a(seed) ^ hash32(u32x((secondKey >>> 0) ^ 2654435769))));
for (let i = 0; i < 8; i++) {
if ((i * (i + 1) & 1) === 0) {
const idx = a % 61;
a = rotl32(a + u32x(2654435769), 7 + (7 & i));
S[idx] = u32x(a ^ hash32(a));
a = hash32(u32x(a + idx));
} else {
S[i] = HASH_TABLE[15 & i];
}
}
return { S, acc: u32x(hash32(2779096485 ^ a)) };
}
function nextByte(st, ctr) {
const r = st.S;
const o = st.acc;
const n = o % 61;
const inSet = 0 - Number(n in r);
const d = r[n] >>> 0;
const x = u32x(d ^ mul32(2654435769, ctr + 1));
const y = u32x((o ^ x) | (o & x & inSet));
const no = hash32(u32x(rotl32(u32x(y + o), 31 & n) ^ rotl32(o, 31 & Math.imul(n, 7))) + 2654435769);
r[n] = no >>> 0;
st.acc = no;
return no >>> 0;
}
function keystream(seed, secondKey, len) {
const st = initStream(seed, secondKey);
const out = new Uint8Array(len);
let ctr = 0;
for (let i = 0; i < len;) {
const b = nextByte(st, ctr++);
out[i++] = 255 & b;
if (i < len) out[i++] = (b >>> 8) & 255;
if (i < len) out[i++] = (b >>> 16) & 255;
if (i < len) out[i++] = (b >>> 24) & 255;
}
return out;
}
function decryptPayload(payload, seed, secondKey) {
const b64 = payload.replace(/-/g, '+').replace(/_/g, '/').padEnd(4 * Math.ceil(payload.length / 4), '=');
const data = Buffer.from(b64, 'base64');
const ks = keystream(seed, secondKey, data.length);
for (let i = 0; i < data.length; i++) data[i] ^= ks[i];
for (let i = 0; i < MAGIC.length; i++) {
if (data[i] !== MAGIC[i]) throw new Error('Invalid encrypted payload');
}
return data.subarray(MAGIC.length).toString('utf8');
}
async function getVideasyStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) {
console.log(`[Videasy] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}`);
@@ -60,57 +130,87 @@ async function getVideasyStreams(tmdbId, mediaType = 'movie', seasonNum = null,
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;
// Step 2: Fetch a decryption seed (cached server-side per mediaId, short TTL)
let seed;
try {
const seedRes = await axios.get(`${VIDEASY_API}/seed?mediaId=${tmdbId}`, {
headers: VIDEASY_HEADERS,
timeout: 8000
});
seed = seedRes.data && seedRes.data.seed;
} catch (err) {
console.error(`[Videasy] Seed fetch failed: ${err.message}`);
return [];
}
if (!seed) {
console.error('[Videasy] No seed returned.');
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}`;
const queryServers = async (activeSeed) => {
await Promise.all(Object.entries(SERVERS).map(async ([name, server]) => {
if (server.moviesOnly && mediaType === 'tv') return;
try {
const encRes = await axios.get(apiUrl, {
headers: VIDEASY_HEADERS,
timeout: 8000,
responseType: 'text'
});
let apiUrl = `${server.url}?title=${encodeURIComponent(details.title)}`
+ `&mediaType=${details.type === 'tv' ? 'TV Series' : 'Movie'}&year=${details.year}`
+ `&tmdbId=${tmdbId}&imdbId=${details.imdbId || ''}`;
if (mediaType === 'tv') apiUrl += `&seasonId=${seasonNum}&episodeId=${episodeNum}`;
apiUrl += `&enc=2&seed=${activeSeed}`;
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'
}
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 payload locally with the seed cipher
const plain = decryptPayload(encryptedText, activeSeed, String(tmdbId));
const resData = JSON.parse(plain);
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] Server ${name}: ${resData.sources.length} source(s)`);
}));
};
// Step 4: Query each server in parallel; retry once with a fresh seed if nothing matched
await queryServers(seed);
if (allStreams.length === 0) {
try {
const seedRes = await axios.get(`${VIDEASY_API}/seed?mediaId=${tmdbId}`, {
headers: VIDEASY_HEADERS,
timeout: 8000
});
const freshSeed = seedRes.data && seedRes.data.seed;
if (freshSeed && freshSeed !== seed) await queryServers(freshSeed);
} catch {
// server unreachable or returned no data — skip silently
// ignore retry failure
}
}));
}
console.log(`[Videasy] Total streams: ${allStreams.length}`);
return allStreams;
}
module.exports = { getVideasyStreams };
module.exports = { getVideasyStreams };
+37 -14
View File
@@ -5,6 +5,19 @@ const VIDLINK_HEADERS = {
'Referer': 'https://vidlink.pro'
};
function qualityToNumber(quality) {
if (quality === '4k') return 2160;
if (quality === 'Auto' || quality === 'unknown') return 0;
const parsed = parseInt(quality, 10);
return isNaN(parsed) ? 0 : parsed;
}
function qualityLabel(quality) {
if (quality === '4k') return '4K';
if (quality === 'Auto' || quality === 'unknown') return 'Auto';
return `${quality}p`;
}
async function getVidlinkStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) {
console.log(`[Vidlink] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}`);
@@ -20,32 +33,42 @@ async function getVidlinkStreams(tmdbId, mediaType = 'movie', seasonNum = null,
return [];
}
// Step 2: Fetch stream playlist from Vidlink API
// Step 2: Fetch file-quality streams 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.');
const streamData = apiRes.data && apiRes.data.stream;
if (!streamData || !streamData.qualities) {
console.log('[Vidlink] No quality streams in response.');
return [];
}
console.log(`[Vidlink] Got stream.`);
return [{
name: 'Vidlink',
title: 'Vidlink',
url: playlist,
quality: 'Auto',
provider: 'Vidlink',
headers: { 'Referer': 'https://vidlink.pro' }
}];
const streams = Object.entries(streamData.qualities)
.filter(([, entry]) => entry && entry.url)
.sort((a, b) => qualityToNumber(b[0]) - qualityToNumber(a[0]))
.map(([qualityKey, entry]) => ({
name: 'Vidlink',
title: `Vidlink - ${qualityLabel(qualityKey)}`,
url: entry.url,
quality: qualityKey,
provider: 'Vidlink',
headers: { 'Referer': 'https://vidlink.pro' }
}));
if (streams.length === 0) {
console.log('[Vidlink] No usable stream URLs in response.');
return [];
}
console.log(`[Vidlink] Got ${streams.length} stream(s).`);
return streams;
} catch (err) {
console.error(`[Vidlink] Error: ${err.message}`);
return [];
}
}
module.exports = { getVidlinkStreams };
module.exports = { getVidlinkStreams };
+309
View File
@@ -0,0 +1,309 @@
const { createHash } = require('crypto');
const { getDetails, resolveImdbId } = require('../utils/tmdb');
const PORTALS = ['https://zxcstream.xyz', 'https://zxcprime.xyz'];
const INITIAL_BASE = 'https://r1.zxcstream.xyz';
const SALT = '3435443433';
const SERVERS = ['icarus', 'berkas', 'orion', 'athena'];
const BASE_TTL = 10 * 60 * 1000;
const PROBE_SUBDOMAINS = ['r1', 'r2', 'r3', 'r4', 'r5', 'r6', 'v4', 'cdn', 'api', 'stream'];
const F = {
id: 'rgrwsdsdfgwrwrwwr',
fToken: 'xfgdfgdsffgrwgrwyjhkjt',
ts: 'rdghhdghhfssft',
token: 'ZDDVHJFGHYRHG',
title: 'TUKTHFSSFGDGHJS',
year: '53653TRFG647GF',
season: 'adkljfhdahfladhfjahfjlahfhfljkadfdf',
episode: '546745ygy46ytfgty',
imdbId: '564745ygtuy5yi75yuy'
};
const COMMON_HEADERS = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0 Safari/537.36',
'Accept': 'application/json, text/plain, */*'
};
let _base = INITIAL_BASE;
let _baseValidatedAt = 0;
let discoveryPromise = null;
function sha512Hex(data) {
return createHash('sha512').update(data).digest('hex');
}
async function verifyBase(base) {
const rt = Date.now();
const xt = sha512Hex(`${rt}:${SALT}:550`).slice(0, 64);
const r = await fetch(`${base}/backend/token`, {
method: 'POST',
headers: {
...COMMON_HEADERS,
Origin: base,
'Content-Type': 'application/json',
Referer: `${base}/player/movie/550`
},
body: JSON.stringify({ [F.id]: '550', [F.fToken]: xt, [F.ts]: rt }),
signal: AbortSignal.timeout(6000)
});
if (r.ok) {
const d = await r.json();
if (d[F.token]) return base;
}
throw new Error(`verify failed: ${r.status}`);
}
async function tryPortal(portal) {
const r = await fetch(portal, {
headers: { 'User-Agent': COMMON_HEADERS['User-Agent'] },
redirect: 'follow',
signal: AbortSignal.timeout(10000)
});
const redirectedBase = new URL(r.url).origin;
if (redirectedBase === new URL(portal).origin) {
throw new Error(`portal ${portal} did not redirect`);
}
return await verifyBase(redirectedBase);
}
async function probeSubdomain(sub) {
return verifyBase(`https://${sub}.zxcstream.xyz`);
}
async function discoverBase() {
const portalResults = await Promise.allSettled(PORTALS.map((portal) => tryPortal(portal)));
for (const r of portalResults) {
if (r.status === 'fulfilled') {
console.log(`[ZXCStreams] discovered base via portal redirect: ${r.value}`);
return r.value;
}
}
console.warn('[ZXCStreams] both portals failed, falling back to subdomain probing');
const settled = await Promise.allSettled(PROBE_SUBDOMAINS.map((sub) => probeSubdomain(sub)));
for (const r of settled) {
if (r.status === 'fulfilled') {
console.log(`[ZXCStreams] discovered base via subdomain probe: ${r.value}`);
return r.value;
}
}
console.warn('[ZXCStreams] all discovery methods failed, keeping last known base:', _base);
return _base;
}
async function getBase() {
if (Date.now() - _baseValidatedAt <= BASE_TTL) {
return _base;
}
if (!discoveryPromise) {
discoveryPromise = discoverBase()
.then((base) => {
_base = base;
_baseValidatedAt = Date.now();
return base;
})
.finally(() => {
discoveryPromise = null;
});
}
return discoveryPromise;
}
function invalidateBase() {
_baseValidatedAt = 0;
}
function generateFrontendToken(tmdbId) {
const rt = Date.now();
const xt = sha512Hex(`${rt}:${SALT}:${tmdbId}`).slice(0, 64);
return { xt, rt };
}
async function requestServerToken(base, tmdbId, referer) {
const { xt, rt } = generateFrontendToken(tmdbId);
const body = JSON.stringify({
[F.id]: tmdbId,
[F.fToken]: xt,
[F.ts]: rt
});
const res = await fetch(`${base}/backend/token`, {
method: 'POST',
headers: {
...COMMON_HEADERS,
Origin: base,
'Content-Type': 'application/json',
Referer: referer
},
body
});
if (!res.ok) throw new Error(`token failed ${res.status}`);
const data = await res.json();
return { serverToken: data[F.token], serverTs: data[F.ts], xt };
}
async function fetchServer(server, meta, type, season, episode) {
let base = await getBase();
const buildReferer = (b) =>
`${b}/player/${type}/${meta.tmdbId}${season != null ? `/${season}/${episode}` : ''}`;
let referer = buildReferer(base);
let tokenData;
try {
tokenData = await requestServerToken(base, meta.tmdbId, referer);
} catch (err) {
console.warn(`[ZXCStreams] token request failed on ${base}, re-discovering...`, err.message);
invalidateBase();
base = await getBase();
referer = buildReferer(base);
tokenData = await requestServerToken(base, meta.tmdbId, referer);
}
const { serverToken, serverTs, xt } = tokenData;
const params = {
[F.id]: meta.tmdbId,
b: type,
[F.ts]: String(serverTs),
[F.token]: serverToken,
[F.fToken]: xt,
[F.title]: meta.title,
[F.year]: meta.year,
date: meta.releaseDate,
[F.imdbId]: meta.imdbId
};
if (season != null && episode != null) {
params[F.season] = String(season);
params[F.episode] = String(episode);
}
const qs = new URLSearchParams(params).toString();
const res = await fetch(`${base}/backend_/servers/${server}?${qs}`, {
headers: { ...COMMON_HEADERS, Origin: base, Referer: referer }
});
if (!res.ok) return [];
const data = await res.json();
if (!data.success || !Array.isArray(data.links)) return [];
const requestHeaders = {
Referer: referer,
Origin: base,
'User-Agent': COMMON_HEADERS['User-Agent']
};
return data.links
.filter((l) => l.link)
.map((l) => ({
server,
type: l.type || (l.link.includes('.m3u8') ? 'hls' : 'mp4'),
resolution: l.resolution ?? (l.source && l.source !== 'default' ? l.source : undefined) ?? '?',
size: l.size,
url: l.link,
requestHeaders
}));
}
async function getAllStreams(type, meta, season, episode) {
const results = await Promise.allSettled(
SERVERS.map((s) => fetchServer(s, meta, type, season, episode))
);
const streams = [];
for (const r of results) {
if (r.status === 'fulfilled') streams.push(...r.value);
}
return streams;
}
function resolutionLabel(server, res) {
if (typeof res === 'number' && res <= 4) {
return ['360p', '480p', '720p', '1080p', '4K'][res] ?? `q${res}`;
}
return typeof res === 'number' ? `${res}p` : String(res);
}
function formatSize(bytes) {
if (!bytes) return '';
const n = Number(bytes);
if (!Number.isFinite(n)) return '';
if (n > 1e9) return `${(n / 1e9).toFixed(2)} GB`;
if (n > 1e6) return `${(n / 1e6).toFixed(0)} MB`;
return `${(n / 1e3).toFixed(0)} KB`;
}
async function getZxcstreamsStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) {
console.log(`[ZXCStreams] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}`);
try {
const type = mediaType === 'tv' ? 'tv' : 'movie';
const tmdbType = mediaType === 'tv' ? 'tv' : 'movie';
const details = await getDetails(tmdbType, tmdbId);
const title = (details && (details.title || details.name)) || '';
if (!title) {
console.log(`[ZXCStreams] No TMDB title resolved for ${tmdbId}`);
return [];
}
const releaseDate = (details && (details.release_date || details.first_air_date || '').slice(0, 10)) || '';
const year = (details && (details.release_date || details.first_air_date || '').slice(0, 4)) || '';
const imdbId = await resolveImdbId(tmdbType, tmdbId);
if (!imdbId || !imdbId.startsWith('tt')) {
console.log(`[ZXCStreams] No IMDB ID resolved for TMDB ${tmdbId}`);
return [];
}
const meta = {
tmdbId: String(tmdbId),
title,
year,
releaseDate,
imdbId
};
const links = await getAllStreams(type, meta, seasonNum, episodeNum);
if (!links.length) {
console.log(`[ZXCStreams] No streams found for "${title}"`);
return [];
}
const scored = links.map((l) => {
const r = typeof l.resolution === 'number' ? l.resolution : 0;
const height = r > 4 ? r : ([240, 480, 720, 1080, 2160][r] ?? 0);
return { l, score: (l.type === 'mp4' ? 10000 : 0) + height };
});
scored.sort((a, b) => b.score - a.score);
const streams = [];
for (const { l } of scored) {
const label = resolutionLabel(l.server, l.resolution);
const size = formatSize(l.size);
const kind = l.type === 'hls' ? 'HLS' : 'MP4';
const serverName =
l.server === 'icarus' ? 'Icarus' :
l.server === 'orion' ? 'Orion' :
l.server === 'athena' ? 'Athena' : 'Berkas';
const titleLine = [`${serverName} • ${label} • ${kind}`, size].filter(Boolean).join('\n');
streams.push({
name: `ZXCStreams ${serverName} ${label}`,
title: titleLine,
url: l.url,
quality: label,
provider: 'ZXCStreams',
headers: {
'Referer': l.requestHeaders.Referer,
'Origin': l.requestHeaders.Origin,
'User-Agent': l.requestHeaders['User-Agent']
}
});
}
console.log(`[ZXCStreams] Got ${streams.length} stream(s) for "${title}"`);
return streams;
} catch (err) {
console.error(`[ZXCStreams] Error: ${err.message}`);
return [];
}
}
module.exports = { getZxcstreamsStreams };