mirror of
https://github.com/Inside4ndroid/TMDB-Embed-API.git
synced 2026-10-22 09:35:11 +00:00
Full overhaul of the repo please see changelog
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"info": "Initial default credentials. CHANGE THE PASSWORD IMMEDIATELY using /auth/change-password after logging in.",
|
||||
"users": [
|
||||
{
|
||||
"username": "admin",
|
||||
"hash": "7fffd5f131a733035eaf443cd99905c0:f1cfa876f9b84fede40cd618a4352221f7ab36e4f8bead432ba5d866b06cb9fcc2903ce623d8286bb541f8d28490fc692d0bf3fadda08002265374915cdb27b5"
|
||||
}
|
||||
]
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
|
||||
// Path for auth users file
|
||||
const AUTH_FILE = path.join(process.cwd(), 'utils/auth-users.json');
|
||||
|
||||
// In-memory cache
|
||||
let usersCache = null;
|
||||
let lastLoaded = 0;
|
||||
const LOAD_INTERVAL_MS = 30 * 1000; // reload at most every 30s
|
||||
|
||||
function ensureAuthFile() {
|
||||
if (fs.existsSync(AUTH_FILE)) return;
|
||||
const defaultPassword = 'change-me';
|
||||
const { hash } = hashPassword(defaultPassword);
|
||||
const data = {
|
||||
info: 'Initial default credentials. CHANGE THE PASSWORD IMMEDIATELY using /auth/change-password after logging in.',
|
||||
users: [ { username: 'admin', hash } ]
|
||||
};
|
||||
fs.writeFileSync(AUTH_FILE, JSON.stringify(data, null, 2));
|
||||
console.warn('[auth] Created default auth-users.json with username "admin" and password "change-me". CHANGE IT NOW.');
|
||||
}
|
||||
|
||||
function loadUsers(force=false){
|
||||
if (!force && usersCache && (Date.now() - lastLoaded) < LOAD_INTERVAL_MS) return usersCache;
|
||||
ensureAuthFile();
|
||||
try {
|
||||
const raw = fs.readFileSync(AUTH_FILE, 'utf8');
|
||||
const parsed = JSON.parse(raw);
|
||||
usersCache = Array.isArray(parsed.users) ? parsed.users : [];
|
||||
lastLoaded = Date.now();
|
||||
} catch (e) {
|
||||
console.error('[auth] Failed loading users file:', e.message);
|
||||
usersCache = [];
|
||||
}
|
||||
return usersCache;
|
||||
}
|
||||
|
||||
function hashPassword(password, salt){
|
||||
salt = salt || crypto.randomBytes(16).toString('hex');
|
||||
const derived = crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha512').toString('hex');
|
||||
return { hash: `${salt}:${derived}`, salt };
|
||||
}
|
||||
|
||||
function verifyPassword(password, stored){
|
||||
if (!stored || !stored.includes(':')) return false;
|
||||
const [salt, derived] = stored.split(':');
|
||||
const test = crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha512').toString('hex');
|
||||
return crypto.timingSafeEqual(Buffer.from(derived,'hex'), Buffer.from(test,'hex'));
|
||||
}
|
||||
|
||||
function findUser(username){
|
||||
return loadUsers().find(u => u.username === username);
|
||||
}
|
||||
|
||||
function updatePassword(username, newPassword){
|
||||
const users = loadUsers(true);
|
||||
const u = users.find(x => x.username === username);
|
||||
if (!u) return false;
|
||||
u.hash = hashPassword(newPassword).hash;
|
||||
try {
|
||||
fs.writeFileSync(AUTH_FILE, JSON.stringify({ users }, null, 2));
|
||||
usersCache = users;
|
||||
lastLoaded = Date.now();
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error('[auth] Failed writing updated password:', e.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Session issuance & validation
|
||||
const SESSION_SECRET = crypto.randomBytes(32); // ephemeral; restart invalidates sessions
|
||||
const SESSION_TTL_MS = 12 * 60 * 60 * 1000; // 12h
|
||||
|
||||
function base64url(input){
|
||||
return Buffer.from(input).toString('base64').replace(/=/g,'').replace(/\+/g,'-').replace(/\//g,'_');
|
||||
}
|
||||
|
||||
function sign(payloadB64){
|
||||
return base64url(crypto.createHmac('sha256', SESSION_SECRET).update(payloadB64).digest());
|
||||
}
|
||||
|
||||
function issueSession(username){
|
||||
const payload = { u: username, exp: Date.now() + SESSION_TTL_MS };
|
||||
const payloadB64 = base64url(JSON.stringify(payload));
|
||||
const sig = sign(payloadB64);
|
||||
return `${payloadB64}.${sig}`;
|
||||
}
|
||||
|
||||
function parseCookies(req){
|
||||
const h = req.headers.cookie; if (!h) return {};
|
||||
return h.split(';').map(v=>v.trim()).reduce((acc, part)=>{ const eq = part.indexOf('='); if(eq>0){ acc[part.slice(0,eq)] = decodeURIComponent(part.slice(eq+1)); } return acc; },{});
|
||||
}
|
||||
|
||||
function validateSessionToken(token){
|
||||
if (!token || !token.includes('.')) return null;
|
||||
const [payloadB64, sig] = token.split('.');
|
||||
if (sign(payloadB64) !== sig) return null;
|
||||
try {
|
||||
const payload = JSON.parse(Buffer.from(payloadB64.replace(/-/g,'+').replace(/_/g,'/'),'base64').toString('utf8'));
|
||||
if (typeof payload.exp !== 'number' || Date.now() > payload.exp) return null;
|
||||
return payload;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function authenticate(username, password){
|
||||
const user = findUser(username);
|
||||
if (!user) return false;
|
||||
return verifyPassword(password, user.hash);
|
||||
}
|
||||
|
||||
function getSession(req){
|
||||
const cookies = parseCookies(req);
|
||||
if (!cookies.session) return null;
|
||||
return validateSessionToken(cookies.session);
|
||||
}
|
||||
|
||||
function requireAuth(req, res, next){
|
||||
const sess = getSession(req);
|
||||
if (!sess) return res.status(401).json({ success:false, error:'AUTH_REQUIRED' });
|
||||
req.session = sess;
|
||||
next();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
loadUsers,
|
||||
authenticate,
|
||||
issueSession,
|
||||
requireAuth,
|
||||
getSession,
|
||||
updatePassword,
|
||||
AUTH_FILE
|
||||
};
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
require('dotenv').config();
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
// Relocated user-config.json into utils directory; maintain backward-compatible fallback
|
||||
const NEW_OVERRIDE_PATH = path.join(process.cwd(), 'utils', 'user-config.json');
|
||||
const LEGACY_OVERRIDE_PATH = path.join(process.cwd(), 'user-config.json');
|
||||
const OVERRIDE_PATH = (fs.existsSync(NEW_OVERRIDE_PATH) || !fs.existsSync(LEGACY_OVERRIDE_PATH)) ? NEW_OVERRIDE_PATH : LEGACY_OVERRIDE_PATH;
|
||||
const CONFIG_SCHEMA_VERSION = 1; // Increment when structure / semantics change
|
||||
|
||||
function parseJsonMaybe(val) {
|
||||
if (!val) return null;
|
||||
const trimmed = val.trim();
|
||||
if (!trimmed) return null;
|
||||
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
||||
try { return JSON.parse(trimmed); } catch { return null; }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseCookies(raw) {
|
||||
if (!raw) return [];
|
||||
const text = raw.trim();
|
||||
if (!text) return [];
|
||||
let arr = [];
|
||||
const json = parseJsonMaybe(text);
|
||||
if (Array.isArray(json)) arr = json; else {
|
||||
arr = text.split(/[,;\n]+/).map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
return Array.from(new Set(arr.map(c => c.replace(/^ui=/,'').trim()).filter(Boolean)));
|
||||
}
|
||||
|
||||
function readOverrideFile() {
|
||||
try {
|
||||
if (fs.existsSync(OVERRIDE_PATH)) {
|
||||
const raw = fs.readFileSync(OVERRIDE_PATH, 'utf8');
|
||||
const data = JSON.parse(raw);
|
||||
return data && typeof data === 'object' ? data : {};
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[config] Failed to read override file:', e.message);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function writeOverrideFile(obj) {
|
||||
try {
|
||||
fs.writeFileSync(OVERRIDE_PATH, JSON.stringify(obj, null, 2));
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error('[config] Failed to write override file:', e.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeConfig(base) {
|
||||
const cfg = { ...base };
|
||||
// Marker: if user explicitly provided an empty array for tmdbApiKeys in override file we should not resurrect legacy key
|
||||
const explicitEmptyKeys = Array.isArray(cfg.tmdbApiKeys) && cfg.tmdbApiKeys.length === 0 && Object.prototype.hasOwnProperty.call(base,'tmdbApiKeys');
|
||||
if (!cfg.configVersion) cfg.configVersion = CONFIG_SCHEMA_VERSION;
|
||||
// Derive structured views
|
||||
cfg.minQualities = parseJsonMaybe(cfg.minQualitiesRaw) || (cfg.minQualitiesRaw ? { default: cfg.minQualitiesRaw } : null);
|
||||
cfg.excludeCodecs = parseJsonMaybe(cfg.excludeCodecsRaw) || null;
|
||||
cfg.febboxCookies = Array.isArray(cfg.febboxCookies) ? cfg.febboxCookies : parseCookies(cfg.febboxCookies);
|
||||
// TMDB multi-key support: allow tmdbApiKeys (array) or legacy single tmdbApiKey
|
||||
if (cfg.tmdbApiKeys && !Array.isArray(cfg.tmdbApiKeys)) {
|
||||
if (typeof cfg.tmdbApiKeys === 'string') {
|
||||
// split by newline/comma/semicolon
|
||||
cfg.tmdbApiKeys = cfg.tmdbApiKeys.split(/[\n,;]+/).map(s=>s.trim()).filter(Boolean);
|
||||
} else cfg.tmdbApiKeys = [];
|
||||
}
|
||||
if (!cfg.tmdbApiKeys || !cfg.tmdbApiKeys.length) {
|
||||
if (!explicitEmptyKeys && cfg.tmdbApiKey) cfg.tmdbApiKeys = [cfg.tmdbApiKey];
|
||||
else cfg.tmdbApiKeys = [];
|
||||
}
|
||||
// If user explicitly cleared tmdbApiKeys via override, also clear legacy single key so it is not shown
|
||||
if (explicitEmptyKeys) {
|
||||
cfg.tmdbApiKey = null;
|
||||
}
|
||||
// Ensure dedupe + trim
|
||||
cfg.tmdbApiKeys = Array.from(new Set(cfg.tmdbApiKeys.map(k=>String(k).trim()).filter(Boolean)));
|
||||
// Booleans normalization
|
||||
const boolKeys = [
|
||||
'enableShowboxProvider','enableXprimeProvider','enable4khdhubProvider','enableMoviesmodProvider','enableMp4hydraProvider','enableVidzeeProvider','enableVixsrcProvider',
|
||||
'disableCache','enablePStreamApi','showboxUseRotatingProxy','disableUrlValidation','disable4khdhubUrlValidation'
|
||||
];
|
||||
boolKeys.forEach(k=>{ if (cfg[k] === 'true') cfg[k] = true; else if (cfg[k] === 'false') cfg[k] = false; });
|
||||
// Default values if undefined
|
||||
if (cfg.enableShowboxProvider === undefined) cfg.enableShowboxProvider = true;
|
||||
if (cfg.enableXprimeProvider === undefined) cfg.enableXprimeProvider = true;
|
||||
if (cfg.enable4khdhubProvider === undefined) cfg.enable4khdhubProvider = true;
|
||||
if (cfg.enableMoviesmodProvider === undefined) cfg.enableMoviesmodProvider = true;
|
||||
if (cfg.enableMp4hydraProvider === undefined) cfg.enableMp4hydraProvider = true;
|
||||
if (cfg.enableVidzeeProvider === undefined) cfg.enableVidzeeProvider = true;
|
||||
if (cfg.enableVixsrcProvider === undefined) cfg.enableVixsrcProvider = true;
|
||||
if (cfg.disableCache === undefined) cfg.disableCache = false;
|
||||
if (cfg.enablePStreamApi === undefined) cfg.enablePStreamApi = true;
|
||||
if (cfg.showboxUseRotatingProxy === undefined) cfg.showboxUseRotatingProxy = false;
|
||||
if (cfg.disableUrlValidation === undefined) cfg.disableUrlValidation = false;
|
||||
if (cfg.disable4khdhubUrlValidation === undefined) cfg.disable4khdhubUrlValidation = false;
|
||||
return cfg;
|
||||
}
|
||||
|
||||
function applyConfigToEnv(cfg){
|
||||
// Mirror config values into process.env so legacy provider code continues to work without .env file
|
||||
if (cfg.port) process.env.API_PORT = String(cfg.port);
|
||||
// Backward compat: set single TMDB_API_KEY env to first key if available
|
||||
if (cfg.tmdbApiKeys && cfg.tmdbApiKeys.length) process.env.TMDB_API_KEY = cfg.tmdbApiKeys[0];
|
||||
else if (cfg.tmdbApiKey) process.env.TMDB_API_KEY = cfg.tmdbApiKey; else delete process.env.TMDB_API_KEY;
|
||||
if (cfg.defaultProviders) process.env.DEFAULT_PROVIDERS = cfg.defaultProviders.join(',');
|
||||
if (cfg.minQualitiesRaw) process.env.MIN_QUALITIES = cfg.minQualitiesRaw; else delete process.env.MIN_QUALITIES;
|
||||
if (cfg.excludeCodecsRaw) process.env.EXCLUDE_CODECS = cfg.excludeCodecsRaw; else delete process.env.EXCLUDE_CODECS;
|
||||
if (cfg.febboxCookies && cfg.febboxCookies.length) process.env.FEBBOX_COOKIES = cfg.febboxCookies.join(',');
|
||||
else delete process.env.FEBBOX_COOKIES;
|
||||
if (cfg.defaultRegion) process.env.DEFAULT_REGION = cfg.defaultRegion; else delete process.env.DEFAULT_REGION;
|
||||
// Provider enable flags
|
||||
process.env.ENABLE_SHOWBOX_PROVIDER = cfg.enableShowboxProvider ? 'true':'false';
|
||||
process.env.ENABLE_XPRIME_PROVIDER = cfg.enableXprimeProvider ? 'true':'false';
|
||||
process.env.ENABLE_4KHDHUB_PROVIDER = cfg.enable4khdhubProvider ? 'true':'false';
|
||||
process.env.ENABLE_MOVIESMOD_PROVIDER = cfg.enableMoviesmodProvider ? 'true':'false';
|
||||
process.env.ENABLE_MP4HYDRA_PROVIDER = cfg.enableMp4hydraProvider ? 'true':'false';
|
||||
process.env.ENABLE_VIDZEE_PROVIDER = cfg.enableVidzeeProvider ? 'true':'false';
|
||||
process.env.ENABLE_VIXSRC_PROVIDER = cfg.enableVixsrcProvider ? 'true':'false';
|
||||
// Caching / validation / PStream
|
||||
process.env.DISABLE_CACHE = cfg.disableCache ? 'true':'false';
|
||||
process.env.ENABLE_PSTREAM_API = cfg.enablePStreamApi ? 'true':'false';
|
||||
process.env.DISABLE_URL_VALIDATION = cfg.disableUrlValidation ? 'true':'false';
|
||||
process.env.DISABLE_4KHDHUB_URL_VALIDATION = cfg.disable4khdhubUrlValidation ? 'true':'false';
|
||||
// Showbox specific
|
||||
if (cfg.showboxCacheDir) process.env.SHOWBOX_CACHE_DIR = cfg.showboxCacheDir; else delete process.env.SHOWBOX_CACHE_DIR;
|
||||
process.env.SHOWBOX_USE_ROTATING_PROXY = cfg.showboxUseRotatingProxy ? 'true':'false';
|
||||
if (cfg.showboxProxyPrimary) process.env.SHOWBOX_PROXY_URL_VALUE = cfg.showboxProxyPrimary; else delete process.env.SHOWBOX_PROXY_URL_VALUE;
|
||||
if (cfg.showboxProxyAlternate) process.env.SHOWBOX_PROXY_URL_ALTERNATE = cfg.showboxProxyAlternate; else delete process.env.SHOWBOX_PROXY_URL_ALTERNATE;
|
||||
// Other provider proxies
|
||||
if (cfg.xprimeProxyUrl) process.env.XPRIME_PROXY_URL = cfg.xprimeProxyUrl; else delete process.env.XPRIME_PROXY_URL;
|
||||
if (cfg.vidzeeProxyUrl) process.env.VIDZEE_PROXY_URL = cfg.vidzeeProxyUrl; else delete process.env.VIDZEE_PROXY_URL;
|
||||
if (cfg.vidsrcProxyUrl) process.env.VIDSRC_PROXY_URL = cfg.vidsrcProxyUrl; else delete process.env.VIDSRC_PROXY_URL;
|
||||
if (cfg.moviesmodProxyUrl) process.env.MOVIESMOD_PROXY_URL = cfg.moviesmodProxyUrl; else delete process.env.MOVIESMOD_PROXY_URL;
|
||||
if (cfg.defaultRegion) process.env.FEBBOX_REGION = cfg.defaultRegion; // alias
|
||||
}
|
||||
|
||||
function loadConfig() {
|
||||
// Start with env for backward compat, then override with user-config.json
|
||||
const envCfg = {
|
||||
port: Number(process.env.API_PORT) || 8787,
|
||||
defaultRegion: process.env.DEFAULT_REGION || process.env.FEBBOX_REGION || null,
|
||||
defaultProviders: (process.env.DEFAULT_PROVIDERS || '').split(/[\s,]+/).map(p=>p.trim().toLowerCase()).filter(Boolean),
|
||||
minQualitiesRaw: process.env.MIN_QUALITIES || null,
|
||||
excludeCodecsRaw: process.env.EXCLUDE_CODECS || null,
|
||||
tmdbApiKey: process.env.TMDB_API_KEY || null,
|
||||
tmdbApiKeys: parseJsonMaybe(process.env.TMDB_API_KEYS) || null,
|
||||
febboxCookies: parseCookies(process.env.FEBBOX_COOKIES),
|
||||
// Extended advanced config (may not exist in env)
|
||||
enableShowboxProvider: process.env.ENABLE_SHOWBOX_PROVIDER,
|
||||
enableXprimeProvider: process.env.ENABLE_XPRIME_PROVIDER,
|
||||
enable4khdhubProvider: process.env.ENABLE_4KHDHUB_PROVIDER,
|
||||
enableMoviesmodProvider: process.env.ENABLE_MOVIESMOD_PROVIDER,
|
||||
enableMp4hydraProvider: process.env.ENABLE_MP4HYDRA_PROVIDER,
|
||||
enableVidzeeProvider: process.env.ENABLE_VIDZEE_PROVIDER,
|
||||
enableVixsrcProvider: process.env.ENABLE_VIXSRC_PROVIDER,
|
||||
disableCache: process.env.DISABLE_CACHE,
|
||||
enablePStreamApi: process.env.ENABLE_PSTREAM_API,
|
||||
showboxUseRotatingProxy: process.env.SHOWBOX_USE_ROTATING_PROXY,
|
||||
showboxCacheDir: process.env.SHOWBOX_CACHE_DIR || null,
|
||||
showboxProxyPrimary: process.env.SHOWBOX_PROXY_URL_VALUE || null,
|
||||
showboxProxyAlternate: process.env.SHOWBOX_PROXY_URL_ALTERNATE || null,
|
||||
xprimeProxyUrl: process.env.XPRIME_PROXY_URL || null,
|
||||
vidzeeProxyUrl: process.env.VIDZEE_PROXY_URL || null,
|
||||
vidsrcProxyUrl: process.env.VIDSRC_PROXY_URL || null,
|
||||
moviesmodProxyUrl: process.env.MOVIESMOD_PROXY_URL || null,
|
||||
disableUrlValidation: process.env.DISABLE_URL_VALIDATION,
|
||||
disable4khdhubUrlValidation: process.env.DISABLE_4KHDHUB_URL_VALIDATION
|
||||
};
|
||||
const override = readOverrideFile();
|
||||
const merged = { ...envCfg, ...override };
|
||||
const normalized = normalizeConfig(merged);
|
||||
applyConfigToEnv(normalized); // ensure providers see updated process.env values
|
||||
return normalized;
|
||||
}
|
||||
|
||||
const config = loadConfig();
|
||||
|
||||
function saveConfigPatch(patch) {
|
||||
const currentOverride = readOverrideFile();
|
||||
const updated = { ...currentOverride, ...patch };
|
||||
if (!updated.configVersion) updated.configVersion = CONFIG_SCHEMA_VERSION;
|
||||
// Remove keys set to null to fall back to env
|
||||
Object.keys(updated).forEach(k => { if (updated[k] === null) delete updated[k]; });
|
||||
if (writeOverrideFile(updated)) {
|
||||
Object.assign(config, loadConfig());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
module.exports = { config, reloadConfig: () => Object.assign(config, loadConfig()), saveConfigPatch, OVERRIDE_PATH, CONFIG_SCHEMA_VERSION };
|
||||
@@ -0,0 +1,49 @@
|
||||
function parseQuality(qualityString) {
|
||||
if (!qualityString || typeof qualityString !== 'string') return 0;
|
||||
const q = qualityString.toLowerCase();
|
||||
if (q.includes('auto')) return 1080;
|
||||
if (q.includes('4k') || q.includes('2160')) return 2160;
|
||||
if (q.includes('1440')) return 1440;
|
||||
if (q.includes('1080')) return 1080;
|
||||
if (q.includes('720')) return 720;
|
||||
if (q.includes('576')) return 576;
|
||||
if (q.includes('480')) return 480;
|
||||
if (q.includes('360')) return 360;
|
||||
if (q.includes('240')) return 240;
|
||||
const kbps = q.match(/(\d+)k/); if (kbps) return parseInt(kbps[1],10)/1000;
|
||||
if (q.includes('hd')) return 720;
|
||||
if (q.includes('sd')) return 480;
|
||||
if (q.includes('cam')) return 100;
|
||||
if (q.includes('ts')) return 200;
|
||||
if (q.includes('scr')) return 300;
|
||||
if (q.includes('dvdscr')) return 350;
|
||||
if (q.includes('r5') || q.includes('r6')) return 400;
|
||||
if (q.includes('org')) return 4320;
|
||||
return 0;
|
||||
}
|
||||
function filterByMinQuality(streams, minQualitySetting) {
|
||||
if (!minQualitySetting || minQualitySetting.toLowerCase() === 'all') return streams;
|
||||
const minNumeric = parseQuality(minQualitySetting);
|
||||
if (minNumeric === 0 && minQualitySetting.toLowerCase() !== 'all') return streams;
|
||||
return streams.filter(s => parseQuality(s.quality) >= minNumeric);
|
||||
}
|
||||
function filterByCodecs(streams, exclude) {
|
||||
if (!exclude || (!exclude.excludeDV && !exclude.excludeHDR)) return streams;
|
||||
return streams.filter(s => {
|
||||
if (!Array.isArray(s.codecs)) return true;
|
||||
if (exclude.excludeDV && s.codecs.includes('DV')) return false;
|
||||
if (exclude.excludeHDR && s.codecs.some(c => ['HDR','HDR10','HDR10+'].includes(c))) return false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
function applyFilters(streams, providerName, minQualitiesConfig, excludeCodecsConfig) {
|
||||
let minQuality = null;
|
||||
if (minQualitiesConfig) {
|
||||
if (typeof minQualitiesConfig === 'string') minQuality = minQualitiesConfig;
|
||||
else if (minQualitiesConfig.default) minQuality = minQualitiesConfig.default;
|
||||
}
|
||||
let filtered = filterByMinQuality(streams, minQuality);
|
||||
filtered = filterByCodecs(filtered, excludeCodecsConfig);
|
||||
return filtered;
|
||||
}
|
||||
module.exports = { applyFilters };
|
||||
@@ -0,0 +1,38 @@
|
||||
const fetch = (...args) => import('node-fetch').then(m => m.default(...args));
|
||||
const { config } = require('./config');
|
||||
|
||||
const cache = {
|
||||
imdbByTmdb: new Map(),
|
||||
details: new Map(),
|
||||
};
|
||||
const TTL_MS = 6 * 60 * 60 * 1000;
|
||||
function isFresh(entry) { return entry && (Date.now() - entry.ts) < TTL_MS; }
|
||||
|
||||
async function tmdbFetchJson(url) {
|
||||
if (!config.tmdbApiKey) throw new Error('TMDB_API_KEY missing');
|
||||
const sep = url.includes('?') ? '&' : '?';
|
||||
const full = `${url}${sep}api_key=${config.tmdbApiKey}`;
|
||||
const res = await fetch(full, { timeout: 15000 });
|
||||
if (!res.ok) throw new Error(`TMDB request failed ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
async function getExternalIds(type, tmdbId) {
|
||||
const key = `${type}:${tmdbId}`;
|
||||
const cached = cache.imdbByTmdb.get(key);
|
||||
if (isFresh(cached)) return cached.data;
|
||||
const json = await tmdbFetchJson(`https://api.themoviedb.org/3/${type}/${tmdbId}/external_ids`);
|
||||
cache.imdbByTmdb.set(key, { data: json, ts: Date.now() });
|
||||
return json;
|
||||
}
|
||||
async function getDetails(type, tmdbId) {
|
||||
const key = `${type}:${tmdbId}:details`;
|
||||
const cached = cache.details.get(key);
|
||||
if (isFresh(cached)) return cached.data;
|
||||
const json = await tmdbFetchJson(`https://api.themoviedb.org/3/${type}/${tmdbId}`);
|
||||
cache.details.set(key, { data: json, ts: Date.now() });
|
||||
return json;
|
||||
}
|
||||
async function resolveImdbId(type, tmdbId) {
|
||||
try { const ext = await getExternalIds(type, tmdbId); return ext.imdb_id || null; } catch { return null; }
|
||||
}
|
||||
module.exports = { getExternalIds, getDetails, resolveImdbId };
|
||||
@@ -0,0 +1,12 @@
|
||||
const { config } = require('./config');
|
||||
|
||||
function getTmdbApiKey() {
|
||||
if (config.tmdbApiKeys && config.tmdbApiKeys.length) {
|
||||
const idx = Math.floor(Math.random() * config.tmdbApiKeys.length);
|
||||
return config.tmdbApiKeys[idx];
|
||||
}
|
||||
// Fallback legacy single value (config or env)
|
||||
return config.tmdbApiKey || process.env.TMDB_API_KEY || null;
|
||||
}
|
||||
|
||||
module.exports = { getTmdbApiKey };
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"defaultProviders": [],
|
||||
"configVersion": 1,
|
||||
"tmdbApiKeys": [],
|
||||
"enableShowboxProvider": true
|
||||
}
|
||||
Reference in New Issue
Block a user