proxytmdb
Proxy/cache local de l'API TMDB enrichi des notes IMDb, avec recherche intelligente par titre/année/épisode. 100% Node.js (Fastify + worker_threads).
Conçu pour servir une base TMDB complète (~1.7M entrées movies + tv) sans appel API à chaque requête : tout est mis en cache sur disque par un cron quotidien et indexé en mémoire.
Stack
- Node.js 20+ (ESM, fetch natif, worker_threads, Intl.NumberFormat)
- Fastify 5 +
@fastify/secure-session+@fastify/static - Aucune base de données — système de fichiers
<type>/<floor(id/1000)>/<id>.json
Installation
npm install
cp .env.example .env
# Edite .env : TMDB_API_KEY, PROXYTMDB_PASSWORD, SESSION_SECRET
Démarrage du serveur
npm start
Le .env est chargé automatiquement (Node 20+ --env-file-if-exists).
Variables d'environnement
Obligatoires (sans valeur par défaut) :
| Variable | Rôle |
|---|---|
TMDB_API_KEY |
Clé API TMDB |
PROXYTMDB_PASSWORD |
Mot de passe page d'index |
SESSION_SECRET |
Clé de signature des sessions (32 chars) |
Optionnelles (voir .env.example pour la liste complète) :
PORT, HOST, PAGE_TITLE, URLs externes, paramètres de recherche
(TITLE_TOLERANCE, LEV_*, YEAR_TOLERANCE...).
Cron (mise à jour des données)
Pipeline complet :
npm run cron
Étapes individuelles disponibles :
npm run cron:imdb # title.ratings.tsv depuis datasets.imdbws.com
npm run cron:tmdb # sync incrementale TMDB (movie + tv)
npm run cron:justwatch # watch providers
npm run cron:tmdb2imdb # mappings bidirectionnels TMDb <-> IMDb
npm run cron:search # construction des chunks de recherche
npm run cron:ambiguity # detection des doublons (CSV)
Crontab système recommandée (remplace /chemin/vers/proxytmdb par le
chemin absolu d'installation) :
13 13 * * * /chemin/vers/proxytmdb/cron/run.sh > /chemin/vers/proxytmdb/lastcron.txt 2>&1
Le wrapper cron/run.sh charge nvm puis lance Node sur la
version nvm alias default — pratique si tu mets à jour Node via nvm, le
cron suivra automatiquement.
Si tu n'utilises pas nvm :
13 13 * * * cd /chemin/vers/proxytmdb && /usr/bin/node --env-file-if-exists=.env cron/runAll.js > lastcron.txt 2>&1
Endpoints HTTP
GET /api?t=movie&q=<tmdb_id>
Retourne le JSON TMDB complet (cache local) avec les notes IMDb fusionnées
(note_imdb, vote_imdb).
GET /api?t=tv&q=<tmdb_id>
Idem pour les séries.
GET /api?t=search&q=<requête>
Recherche par titre/année/épisode. Parse la requête (extraction année,
extraction épisode SxxExx/partN/NxN/Exxx), choisit movie ou tv selon
la présence d'un épisode, calcule les distances Levenshtein UTF-8 sur les
titres FR/EN/VO, retourne les meilleurs résultats triés par score puis
popularité puis écart d'année.
Réponse JSON :
{
"results": [
{
"title": "Inception",
"english_title": "Inception",
"original_title": "Inception",
"years": 2010,
"poster": "https://image.tmdb.org/t/p/w200/...",
"genres": "Action Science-Fiction Aventure ",
"countries": "GB US ",
"runtime": "2 h 28 min",
"imdb_id": "tt1375666",
"imdb_url": "https://www.imdb.com/title/tt1375666",
"note_imdb": 8.8,
"vote_imdb": 2809792,
"tmdb_id": 27205,
"tmdb_url": "https://www.themoviedb.org/movie/27205",
"note_tmdb": 8.4,
"vote_tmdb": 39044,
"budget": "$160,000,000",
"revenue": "$839,030,630",
"tagline": "...",
"overview": "..."
}
]
}
L'ancien chemin /api.php est aussi exposé pour compatibilité.
Réponses cachées en mémoire (LRU 1000 entrées, TTL 1h). Reload automatique
des chunks de recherche après chaque cron (watcher fs.watch).
GET /api?t=imdb&q=<imdb_id>
Lookup direct par IMDb ID (tt0133093). Renvoie le détail movie ou tv
correspondant, avec note IMDb fusionnée. Utilise les mappings
imdb2movie.json / imdb2tv.json chargés en mémoire.
GET /api?t=providers&type=movie|tv&q=<id>
Watch providers JustWatch par pays (FR, US, etc.) — données déjà téléchargées par le cron, exposées sous le format TMDB original.
GET / — Interface web
SPA vanilla JS (zéro build), thème sombre. Une barre de recherche, grille de
posters, modal de détail avec tagline/overview/budget/revenue/providers FR.
Accepte requêtes texte (Inception 2010), IMDb IDs (tt0133093) et URLs
TMDB collées (themoviedb.org/movie/27205).
GET /admin — Dashboard admin
SPA protégée par mot de passe (argon2id), 3 onglets, refresh automatique 10 s :
- Tableau de bord — cartes de statut (dernier cron, films/séries TMDB, notes IMDb, cache hit-rate, process), barres d'utilisation disque par catégorie, résumé du cron (compteurs + log tail).
- Métriques —
/metricsPrometheus parsé en HTML : table HTTP par route, table latences p50/p95, compteurs internes, process Node. - Fichiers — listing du projet, fichiers servis sous
/admin/files/.
Pour générer un nouveau hash :
node tools/hashPassword.js 'mon-mot-de-passe'
Puis copier la sortie dans .env sous ADMIN_PASSWORD_HASH=.
Endpoints internes admin (auth requise)
GET /admin/api/stats— JSON agrégé (cron, fichiers, disque, cache, process)GET /admin/api/metrics— métriques Prometheus parsées en JSON structuréGET /admin/api/files— listing des fichiers du projet en JSON
GET /health
JSON liveness/readiness : status, uptime, mémoire, nombre de notes IMDb chargées. Renvoie 503 si l'index IMDb n'a pas pu être chargé.
GET /metrics
Format Prometheus standard : http_requests_total, http_request_duration_seconds,
search_cache_hits_total, search_cache_misses_total, imdb_ratings_total,
search_workers, plus métriques process par défaut (CPU, RSS, event loop).
Rate limit
50 requêtes/seconde par IP par défaut (configurable via RATE_LIMIT_PER_SEC).
/health et /metrics exemptés.
Architecture
proxytmdb/
├── server.js # Bootstrap Fastify + rate limit + sessions + warmup
├── config.js # Env vars, chemins, constantes
├── biome.json # Lint + format
├── public/ # Statique (UI publique + SPA admin)
│ ├── index.html # UI publique : recherche + grille posters + modal
│ ├── style.css # Theme dark partage
│ ├── app.js # Logique UI publique
│ ├── admin.html # SPA admin : 3 onglets
│ ├── admin.css # Styles dashboard (cartes, barres, tableaux)
│ └── admin.js # Logique dashboard + parsing metriques
├── lib/
│ ├── paths.js # Layout <type>/<floor(id/1000)>/<id>.json
│ ├── mbLevenshtein.js # Levenshtein UTF-8 par codepoint
│ ├── titleFilter.js # Translit ligatures + filtre Latin/chiffres
│ ├── queryParser.js # Extraction annee/episode/titre
│ ├── imdbRatings.js # Index IMDb en memoire (Map, auto-reload mtime)
│ ├── imdbMapping.js # Mapping IMDb -> TMDb id (+ preload au boot)
│ ├── http.js # fetch + retry + Limiter de concurrence
│ ├── format.js # Devises (Intl) + runtime
│ ├── lockFile.js # Lock PID-based pour le cron (O_EXCL)
│ ├── password.js # Argon2id hash + verify
│ ├── metrics.js # Counters/Histograms/Gauges Prometheus
│ ├── stats.js # Agregateur stats admin (TTL cache)
│ ├── dataReload.js # Hot reload post-cron (chunks/mappings/ratings)
│ ├── searchEngine.js # Pool workers persistants + reloadAllPools()
│ └── searchWorker.js # Worker thread (1 chunk de la base)
├── routes/
│ ├── api.js # /api (movie/tv/imdb/providers/search) + cache LRU
│ ├── search.js # /search HTML compat ancien PHP
│ ├── admin.js # /admin (login argon2 + dashboard SPA + APIs)
│ └── health.js # /health + /metrics (Prometheus)
├── cron/
│ ├── runAll.js # Pipeline complet (avec lock file)
│ ├── run.sh # Wrapper nvm pour crontab
│ ├── imdbRatings.js # title.ratings.tsv.gz
│ ├── tmdbExports.js # Exports quotidiens TMDB (avec fallback veille)
│ ├── tmdbSync.js # Sync incrementale via /changes
│ ├── justwatchSync.js # /watch/providers
│ ├── tmdb2imdb.js # Mappings bidirectionnels
│ ├── buildSearch.js # Chunks de recherche
│ └── ambiguity.js # Detection des doublons
├── tools/
│ └── hashPassword.js # CLI : generer un hash argon2id
├── test/
│ └── helpers.test.js # Tests unitaires (Levenshtein, query parser, paths)
├── tmdbintegral/ # Donnees TMDB + JustWatch (gitignore)
└── imdbratings.tsv # Donnees IMDb (gitignore)
Qualité de code
Lint + format avec Biome (config dans biome.json) :
npm run lint # Verifie sans modifier
npm run format # Formate uniquement (write)
npm run fix # Lint + format + auto-fix
Tests
npm test
13 tests unitaires sur les helpers (Levenshtein UTF-8, filtre titres, parser de requête, paths). La parité contre le PHP original a été validée pendant le portage sur 5 requêtes différentes (films, séries, ASCII, UTF-8, avec et sans année) — top-N IDs identiques dans le même ordre.
Notes
- Les fichiers
searchmovie<i>.jsonetsearchtv<i>.jsonsont des chunks de recherche générés par le cron. Le serveur en charge un par worker au démarrage (mémoire résidente, ~120 Mo total). - L'index IMDb (
imdbratings.tsv, ~30 Mo, 1.66M lignes) est chargé une fois enMapau démarrage. Les mappings IMDb→TMDB (imdb2movie.json/imdb2tv.json, ~16 Mo combinés) sont préchargés au warmup. - Hot reload post-cron : un watcher unifié (lib/dataReload.js)
déclenche en arrière-plan le rechargement des 3 sources de données dès que
les fichiers changent —
fs.watch(inotify) pour les chunks et mappings,fs.watchFile(poll 10s) pourimdbratings.tsv. Aucun redémarrage du serveur n'est nécessaire après le cron quotidien. - Le layout disque
<type>/<floor(id/1000)>/<id>.jsonest conservé à l'identique de l'ancienne version PHP : on peut basculer sans regénérer les ~1700 dossiers x 1000 fichiers existants. - Les 8 workers de recherche sont persistants (vs PHP qui forkait 8 processus à chaque requête). Gain mesuré : ~30% sur le temps de réponse.
- Le cron utilise un lock file PID (
.cron.lock) — si le précédent run n'est pas terminé (ou tourne toujours), le suivant échoue proprement. - Logs cron :
cron.txt(start/finish dates) +lastcron.txt(sortie complète stdout, redirigée par crontab). Les deux sont gitignored et régénérés à chaque run.