Files
Inside4ndroid 2c9a2cabbf 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.
2026-08-19 23:35:13 +01:00

303 lines
12 KiB
JavaScript

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 };