# 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)
```

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](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 :

```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** — `/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/`.

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=`.

### 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`) :

```bash
npm run lint     # Verifie sans modifier
npm run format   # Formate uniquement (write)
npm run fix      # Lint + format + auto-fix
```

## 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
  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.**
- 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.
- 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.
