2026-04-23 08:33:37 +02:00
|
|
|
# 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
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
npm install
|
|
|
|
|
cp .env.example .env
|
|
|
|
|
# Edite .env : TMDB_API_KEY, PROXYTMDB_PASSWORD, SESSION_SECRET
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Démarrage du serveur
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
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](.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 :
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
npm run cron
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Étapes individuelles disponibles :
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
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)
|
|
|
|
|
```
|
|
|
|
|
|
2026-04-23 08:47:13 +02:00
|
|
|
Crontab système recommandée (remplace `/chemin/vers/proxytmdb` par le
|
|
|
|
|
chemin absolu d'installation) :
|
2026-04-23 08:33:37 +02:00
|
|
|
|
|
|
|
|
```
|
2026-04-23 08:47:13 +02:00
|
|
|
13 13 * * * /chemin/vers/proxytmdb/cron/run.sh > /chemin/vers/proxytmdb/lastcron.txt 2>&1
|
2026-04-23 08:33:37 +02:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Le wrapper [cron/run.sh](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.
|
|
|
|
|
|
2026-04-23 08:47:13 +02:00
|
|
|
Si tu n'utilises pas nvm :
|
2026-04-23 08:33:37 +02:00
|
|
|
|
|
|
|
|
```
|
2026-04-23 08:47:13 +02:00
|
|
|
13 13 * * * cd /chemin/vers/proxytmdb && /usr/bin/node --env-file-if-exists=.env cron/runAll.js > lastcron.txt 2>&1
|
2026-04-23 08:33:37 +02:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 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 :
|
|
|
|
|
|
|
|
|
|
```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é.
|
|
|
|
|
|
Phase 1: lock cron, reload chaud, argon2, providers, IMDb lookup, cache LRU, /health, /metrics, rate limit, UI dark, biome
2026-04-24 07:35:10 +02:00
|
|
|
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`).
|
|
|
|
|
|
2026-04-24 08:25:02 +02:00
|
|
|
### `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** — `/metrics` Prometheus parsé en HTML : table HTTP par route,
|
|
|
|
|
table latences p50/p95, compteurs internes, process Node.
|
|
|
|
|
- **Fichiers** — listing du projet, fichiers servis sous `/admin/files/`.
|
|
|
|
|
|
Phase 1: lock cron, reload chaud, argon2, providers, IMDb lookup, cache LRU, /health, /metrics, rate limit, UI dark, biome
2026-04-24 07:35:10 +02:00
|
|
|
Pour générer un nouveau hash :
|
|
|
|
|
```bash
|
|
|
|
|
node tools/hashPassword.js 'mon-mot-de-passe'
|
|
|
|
|
```
|
|
|
|
|
Puis copier la sortie dans `.env` sous `ADMIN_PASSWORD_HASH=`.
|
|
|
|
|
|
2026-04-24 08:25:02 +02:00
|
|
|
### 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
|
|
|
|
|
|
Phase 1: lock cron, reload chaud, argon2, providers, IMDb lookup, cache LRU, /health, /metrics, rate limit, UI dark, biome
2026-04-24 07:35:10 +02:00
|
|
|
### `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é.
|
2026-04-23 08:33:37 +02:00
|
|
|
|
Phase 1: lock cron, reload chaud, argon2, providers, IMDb lookup, cache LRU, /health, /metrics, rate limit, UI dark, biome
2026-04-24 07:35:10 +02:00
|
|
|
### `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.
|
2026-04-23 08:33:37 +02:00
|
|
|
|
|
|
|
|
## Architecture
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
proxytmdb/
|
2026-04-24 08:25:02 +02:00
|
|
|
├── server.js # Bootstrap Fastify + rate limit + sessions + warmup
|
|
|
|
|
├── config.js # Env vars, chemins, constantes
|
Phase 1: lock cron, reload chaud, argon2, providers, IMDb lookup, cache LRU, /health, /metrics, rate limit, UI dark, biome
2026-04-24 07:35:10 +02:00
|
|
|
├── biome.json # Lint + format
|
2026-04-24 08:25:02 +02:00
|
|
|
├── 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
|
2026-04-23 08:33:37 +02:00
|
|
|
├── lib/
|
Phase 1: lock cron, reload chaud, argon2, providers, IMDb lookup, cache LRU, /health, /metrics, rate limit, UI dark, biome
2026-04-24 07:35:10 +02:00
|
|
|
│ ├── paths.js # Layout <type>/<floor(id/1000)>/<id>.json
|
2026-04-23 08:33:37 +02:00
|
|
|
│ ├── mbLevenshtein.js # Levenshtein UTF-8 par codepoint
|
|
|
|
|
│ ├── titleFilter.js # Translit ligatures + filtre Latin/chiffres
|
|
|
|
|
│ ├── queryParser.js # Extraction annee/episode/titre
|
2026-04-24 08:25:02 +02:00
|
|
|
│ ├── 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)
|
Phase 1: lock cron, reload chaud, argon2, providers, IMDb lookup, cache LRU, /health, /metrics, rate limit, UI dark, biome
2026-04-24 07:35:10 +02:00
|
|
|
│ ├── password.js # Argon2id hash + verify
|
2026-04-24 08:25:02 +02:00
|
|
|
│ ├── 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()
|
2026-04-23 08:33:37 +02:00
|
|
|
│ └── searchWorker.js # Worker thread (1 chunk de la base)
|
|
|
|
|
├── routes/
|
Phase 1: lock cron, reload chaud, argon2, providers, IMDb lookup, cache LRU, /health, /metrics, rate limit, UI dark, biome
2026-04-24 07:35:10 +02:00
|
|
|
│ ├── api.js # /api (movie/tv/imdb/providers/search) + cache LRU
|
2026-04-24 08:25:02 +02:00
|
|
|
│ ├── search.js # /search HTML compat ancien PHP
|
|
|
|
|
│ ├── admin.js # /admin (login argon2 + dashboard SPA + APIs)
|
|
|
|
|
│ └── health.js # /health + /metrics (Prometheus)
|
2026-04-23 08:33:37 +02:00
|
|
|
├── cron/
|
Phase 1: lock cron, reload chaud, argon2, providers, IMDb lookup, cache LRU, /health, /metrics, rate limit, UI dark, biome
2026-04-24 07:35:10 +02:00
|
|
|
│ ├── runAll.js # Pipeline complet (avec lock file)
|
|
|
|
|
│ ├── run.sh # Wrapper nvm pour crontab
|
2026-04-23 08:33:37 +02:00
|
|
|
│ ├── imdbRatings.js # title.ratings.tsv.gz
|
2026-04-24 08:25:02 +02:00
|
|
|
│ ├── tmdbExports.js # Exports quotidiens TMDB (avec fallback veille)
|
2026-04-23 08:33:37 +02:00
|
|
|
│ ├── tmdbSync.js # Sync incrementale via /changes
|
|
|
|
|
│ ├── justwatchSync.js # /watch/providers
|
|
|
|
|
│ ├── tmdb2imdb.js # Mappings bidirectionnels
|
|
|
|
|
│ ├── buildSearch.js # Chunks de recherche
|
|
|
|
|
│ └── ambiguity.js # Detection des doublons
|
Phase 1: lock cron, reload chaud, argon2, providers, IMDb lookup, cache LRU, /health, /metrics, rate limit, UI dark, biome
2026-04-24 07:35:10 +02:00
|
|
|
├── tools/
|
2026-04-24 08:25:02 +02:00
|
|
|
│ └── hashPassword.js # CLI : generer un hash argon2id
|
2026-04-23 08:33:37 +02:00
|
|
|
├── test/
|
2026-04-24 08:25:02 +02:00
|
|
|
│ └── helpers.test.js # Tests unitaires (Levenshtein, query parser, paths)
|
|
|
|
|
├── tmdbintegral/ # Donnees TMDB + JustWatch (gitignore)
|
2026-04-23 08:33:37 +02:00
|
|
|
└── imdbratings.tsv # Donnees IMDb (gitignore)
|
|
|
|
|
```
|
|
|
|
|
|
Phase 1: lock cron, reload chaud, argon2, providers, IMDb lookup, cache LRU, /health, /metrics, rate limit, UI dark, biome
2026-04-24 07:35:10 +02:00
|
|
|
## Qualité de code
|
|
|
|
|
|
|
|
|
|
Lint + format avec **Biome** (config dans `biome.json`) :
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
npm run lint # Verifie sans modifier
|
|
|
|
|
npm run format # Formate uniquement (write)
|
|
|
|
|
npm run fix # Lint + format + auto-fix
|
|
|
|
|
```
|
|
|
|
|
|
2026-04-23 08:33:37 +02:00
|
|
|
## Tests
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
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>.json` et `searchtv<i>.json` sont 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
|
2026-04-24 08:25:02 +02:00
|
|
|
en `Map` au 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](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) pour `imdbratings.tsv`. **Aucun redémarrage du
|
|
|
|
|
serveur n'est nécessaire après le cron quotidien.**
|
2026-04-23 08:33:37 +02:00
|
|
|
- Le layout disque `<type>/<floor(id/1000)>/<id>.json` est 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.
|
2026-04-24 08:25:02 +02:00
|
|
|
- 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.
|