MovixOpenSource/API/Mainapi/utils/tmdbCache.js
Movix eae5a24c51 feat: ajoute KissKH et fiabilise les sources, les sous-titres et le Cast — Android 2.5.8
- corrige l’extraction Fsvid/Vidzy dans proxiesembed, les extensions et le userscript avec un décodage XOR/P.A.C.K.E.R plus robuste
- rejette les URL « troll » et supprime les résultats Fsvid/Vidzy contaminés des caches
- ajoute KissKH pour les films et séries avec matching TMDB, catalogue, cache distribué, verrouillage, rotation des proxys et fallback sécurisé
- ajoute le relais KissKH HLS/MP4 dans proxiesembed, avec retry SOCKS5 sur les erreurs 403 et streaming des segments BYTERANGE
- prend en charge les sous-titres KissKH SRT/TXT/TXT2, leur déchiffrement AES et leur conversion en WebVTT
- intègre KissKH au lecteur, aux priorités de sources, aux extensions Chrome/Firefox et au userscript
- améliore la sélection, le formatage sécurisé, la traduction et l’affichage Picture-in-Picture des sous-titres
- corrige le Cast web et Android pour transmettre les sous-titres internes, externes, KissKH et traduits
- ajoute le support des pistes WebVTT inline via le relais LAN Android et améliore le suivi des sessions Cast
- protège les liens jouables des caches Cpasmal et Coflix contre les rafraîchissements vides ou concurrents
- fiabilise la sélection des saisons FStream et améliore le matching DarkiWorld par TMDB, titre et année
- simplifie les nouvelles URL TMDB tout en conservant le décodage des anciens identifiants encodés
- corrige l’opacité des icônes SVG dans toute l’interface en séparant la couleur de leur niveau d’opacité
- met à jour le Dockerfile vers Node.js 22 et installe uniquement les dépendances runtime du workspace Hono
- ajoute le workspace server avec ses dépendances dédiées et un arrêt gracieux sur SIGTERM/SIGINT
- accélère le healthcheck Docker et évite de recopier tous les node_modules du builder dans l’image finale
- retire les migrations et créations de tables automatiques au démarrage de la Main API au profit de db:init
- attribue un port CycleTLS distinct à chaque worker pour éviter les conflits en mode cluster
- ajoute des tests de non-régression pour KissKH, les caches de sources, CycleTLS et le Cast Android
- publie Android 2.5.8 build 17 avec le nouvel APK
- met à jour les extensions Chrome 1.3.13 et Firefox 1.5.10

Co-authored-by: GPT-5.6 Sol <noreply@openai.com>
2026-08-03 21:37:41 +02:00

226 lines
6.5 KiB
JavaScript

/**
* Cache Redis centralisé pour les appels TMDB API.
* Partagé entre tous les workers du cluster — évite les appels TMDB redondants.
*
* Pattern identique à wrappedRoutes.js : redisReady() + get/set avec TTL.
* Préfixe Redis : "tmdb:"
*/
const axios = require('axios');
const { redis } = require('../config/redis');
// TTL par type de requête
const TTL_DETAILS = 24 * 60 * 60; // 24h — les détails d'un film/série changent rarement
const TTL_SEARCH = 12 * 60 * 60; // 12h — les résultats de recherche peuvent évoluer
function redisReady() {
return redis && redis.status === 'ready';
}
async function redisGet(key) {
if (!redisReady()) return null;
try {
const data = await redis.get(key);
return data ? JSON.parse(data) : null;
} catch { return null; }
}
async function redisSet(key, value, ttl) {
if (!redisReady()) return;
try {
await redis.set(key, JSON.stringify(value), 'EX', ttl);
} catch { /* ignore */ }
}
/**
* Récupère les détails TMDB pour un ID donné, avec cache Redis.
* Clé : tmdb:details:{type}:{id}
*
* @param {string} tmdbApiUrl - Ex: "https://api.themoviedb.org/3"
* @param {string} tmdbApiKey - Clé API TMDB
* @param {string|number} id - ID TMDB
* @param {string} type - "movie" ou "tv"
* @param {string} [language] - Langue (défaut: "fr-FR")
* @returns {object|null}
*/
async function fetchTmdbDetails(tmdbApiUrl, tmdbApiKey, id, type, language = 'fr-FR') {
const redisKey = `tmdb:details:${type}:${id}:${language}`;
// 1. Cache Redis
const cached = await redisGet(redisKey);
if (cached) return cached;
// 2. Appel API TMDB
try {
const response = await axios.get(`${tmdbApiUrl}/${type}/${id}`, {
params: { api_key: tmdbApiKey, language },
timeout: 10000
});
if (!response.data) return null;
// 3. Sauvegarder dans Redis
await redisSet(redisKey, response.data, TTL_DETAILS);
return response.data;
} catch (error) {
// Ne pas cacher les erreurs
return null;
}
}
/**
* Recherche TMDB avec cache Redis.
* Clé : tmdb:search:{type}:{query}:{year}:{language}
*
* @param {string} tmdbApiUrl
* @param {string} tmdbApiKey
* @param {string} type - "movie" ou "tv"
* @param {string} query - Terme de recherche
* @param {object} [extraParams] - Params additionnels (first_air_date_year, year, etc.)
* @param {string} [language]
* @returns {object|null} - response.data complet (avec .results)
*/
async function searchTmdb(tmdbApiUrl, tmdbApiKey, type, query, extraParams = {}, language = 'fr-FR') {
const paramsSuffix = Object.entries(extraParams).sort().map(([k, v]) => `${k}=${v}`).join('&');
const redisKey = `tmdb:search:${type}:${query}:${paramsSuffix}:${language}`;
const cached = await redisGet(redisKey);
if (cached) return cached;
try {
const response = await axios.get(`${tmdbApiUrl}/search/${type}`, {
params: { api_key: tmdbApiKey, query, language, ...extraParams },
timeout: 10000
});
if (response.data) {
await redisSet(redisKey, response.data, TTL_SEARCH);
}
return response.data;
} catch (error) {
return null;
}
}
/**
* Récupère les détails d'une saison TMDB avec cache Redis.
* Clé : tmdb:season:{tvId}:{seasonNumber}:{language}
*/
async function fetchTmdbSeason(tmdbApiUrl, tmdbApiKey, tvId, seasonNumber, language = 'fr-FR') {
const redisKey = `tmdb:season:${tvId}:${seasonNumber}:${language}`;
const cached = await redisGet(redisKey);
if (cached) return cached;
try {
const response = await axios.get(`${tmdbApiUrl}/tv/${tvId}/season/${seasonNumber}`, {
params: { api_key: tmdbApiKey, language },
timeout: 10000
});
if (response.data) {
await redisSet(redisKey, response.data, TTL_DETAILS);
}
return response.data;
} catch (error) {
return null;
}
}
/**
* Recupere les titres alternatifs d'un media TMDB avec le TTL des details.
* Cle : tmdb:alternative_titles:{mediaType}:{id}
*/
async function fetchTmdbAlternativeTitles(tmdbApiUrl, tmdbApiKey, id, mediaType = 'tv') {
if (!['tv', 'movie'].includes(mediaType)) throw new TypeError('type media TMDB invalide');
const redisKey = `tmdb:alternative_titles:${mediaType}:${id}`;
const cached = await redisGet(redisKey);
if (cached) return cached;
try {
const response = await axios.get(`${tmdbApiUrl}/${mediaType}/${id}/alternative_titles`, {
params: { api_key: tmdbApiKey },
timeout: 10000
});
if (response.data) {
await redisSet(redisKey, response.data, TTL_DETAILS);
}
return response.data;
} catch {
return null;
}
}
/**
* Récupère l'année de sortie française d'un film via /movie/{id}/release_dates.
* Clé : tmdb:release_dates:movie:{id}
*/
async function fetchTmdbFrenchReleaseYear(tmdbApiUrl, tmdbApiKey, id) {
const redisKey = `tmdb:release_dates:movie:${id}`;
const cached = await redisGet(redisKey);
if (cached) {
const frRelease = cached.results ? cached.results.find(r => r.iso_3166_1 === 'FR') : null;
if (frRelease && frRelease.release_dates && frRelease.release_dates.length > 0) {
const date = frRelease.release_dates[0].release_date;
if (date) return new Date(date).getFullYear();
}
return null;
}
try {
const response = await axios.get(`${tmdbApiUrl}/movie/${id}/release_dates`, {
params: { api_key: tmdbApiKey },
timeout: 10000
});
if (response.data) {
await redisSet(redisKey, response.data, TTL_DETAILS);
}
const frRelease = response.data?.results?.find(r => r.iso_3166_1 === 'FR');
if (frRelease && frRelease.release_dates && frRelease.release_dates.length > 0) {
const date = frRelease.release_dates[0].release_date;
if (date) return new Date(date).getFullYear();
}
return null;
} catch (error) {
return null;
}
}
/**
* Récupère toutes les images TMDB (posters + backdrops, toutes langues).
* Clé : tmdb:images:{type}:{id}
*/
async function fetchTmdbImages(tmdbApiUrl, tmdbApiKey, id, type) {
const redisKey = `tmdb:images:${type}:${id}`;
const cached = await redisGet(redisKey);
if (cached) return cached;
try {
const response = await axios.get(`${tmdbApiUrl}/${type}/${id}/images`, {
params: { api_key: tmdbApiKey },
timeout: 10000
});
if (response.data) {
await redisSet(redisKey, response.data, TTL_DETAILS);
}
return response.data;
} catch (error) {
return null;
}
}
module.exports = {
fetchTmdbDetails,
searchTmdb,
fetchTmdbSeason,
fetchTmdbAlternativeTitles,
fetchTmdbFrenchReleaseYear,
fetchTmdbImages,
TTL_DETAILS,
TTL_SEARCH
};