CLAUDE.md
Rama main · réplica de lectura del repositorio original.
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
CinePlete is a self-hosted Docker tool that scans Plex or Jellyfin movie libraries and identifies missing content:
- Missing movies from franchises (TMDB collections)
- Missing films from directors/actors in your library
- Classic films (TMDB Top Rated)
- Personalized suggestions based on library recommendations
- Metadata diagnostics (missing TMDB GUIDs)
- Radarr integration for adding movies
- Telegram notifications for scan results
## Development Commands
### Running Tests
```bash
# Unit tests (Python)
pytest --tb=short -v
# E2E tests (Playwright) — requires the app to be running on port 8787
cd e2e && npm ci && npx playwright install --with-deps chromium
BASE_URL=http://localhost:8787 npx playwright test
```
Unit tests are in `tests/` and cover:
- `test_config.py` - Config deep-merge and validation
- `test_overrides.py` - Ignore/wishlist persistence
- `test_scoring.py` - Movie scoring algorithms
- `test_scheduler.py` - Library polling logic
- `test_telegram.py` - Notification formatting
E2E tests are in `e2e/tests/` (Playwright + Chromium):
- `smoke.spec.js` - App loads, API endpoints respond (`/api/version`, `/api/scan/status`, `/api/config`)
- `navigation.spec.js` - All sidebar nav buttons present, `.active` class toggles correctly
- `config.spec.js` - Settings form fields, TMDB key masking, config save/reload roundtrip, modal
### Running Locally
```bash
# Install dependencies
pip install -r requirements.txt
# Run FastAPI server
uvicorn app.web:app --host 0.0.0.0 --port 8787
```
Access UI at `http://localhost:8787`
### Docker
```bash
# Build
docker build -t cineplete .
# Run
docker compose up -d
```
## Architecture
### Core Components
**Backend (FastAPI)**
- `app/web.py` - All API endpoints, lifespan management, scheduler startup
- `app/scanner.py` - 8-step scan engine with background threading and progress state
- `app/config.py` - Config loader/saver with deep-merge pattern (preserves defaults)
- `app/logger.py` - Rotating file logger (2 MB × 3 files)
**Media Server Scanners**
- `app/plex_xml.py` - Fast Plex XML API scanner (~2s for 1000 movies)
- `app/jellyfin_api.py` - Jellyfin API scanner (drop-in replacement for Plex)
Both scanners return identical tuple structure:
```python
(plex_ids, directors, actors, stats, no_tmdb_guid)
```
**TMDB Integration**
- `app/tmdb.py` - TMDB API client with persistent caching in `/data/tmdb_cache.json`
**Automation & Notifications**
- `app/scheduler.py` - APScheduler-based library polling (checks movie count, triggers scan if changed)
- `app/telegram.py` - Telegram notifications with rate limiting
**Data Management**
- `app/overrides.py` - Ignore/wishlist persistence helpers
**Frontend**
- `static/index.html` - Single-page app shell with inline CSS
- `static/app.js` - All UI logic (routing, rendering, API calls, Chart.js integration)
### Data Flow
```
1. Config loaded from /config/config.yml (deep-merged with defaults)
2. Scheduler starts on app boot, polls library every N minutes
3. User triggers scan → scanner.build_async() spawns background thread
4. Scanner runs 8 steps with progress updates:
- Load config
- Scan Plex/Jellyfin library (determined by SERVER.MEDIA_SERVER)
- Validate TMDB metadata
- Analyze collections (franchises)
- Analyze directors
- Analyze actors
- Build suggestions (TMDB recommendations × library overlap)
- Build results
5. Results written to /data/results.json
6. Frontend polls /api/scan/status for progress
7. Telegram notification sent if enabled
```
### Configuration System
Config is stored at `/config/config.yml` with a deep-merge pattern:
- `DEFAULT_CONFIG` in `app/config.py` defines all possible keys
- User config is deep-merged over defaults (missing keys auto-filled)
- `save_config()` always writes the full merged result
- `is_configured()` validates based on media server type (Plex vs Jellyfin)
**Media Server Selection**
- `SERVER.MEDIA_SERVER` - `"plex"` or `"jellyfin"`
- Scanner dynamically imports the appropriate module at runtime
- Configuration requirements differ:
- Plex: PLEX_URL, PLEX_TOKEN, LIBRARY_NAME
- Jellyfin: JELLYFIN_URL, JELLYFIN_API_KEY, JELLYFIN_LIBRARY_NAME
### Persistent Data (`/data/`)
| File | Purpose |
|------|---------|
| `results.json` | Full scan output (regenerated each scan) |
| `tmdb_cache.json` | TMDB API response cache (permanent) |
| `overrides.json` | Ignored items, wishlist, rec_fetched_ids |
| `cineplete.log` | Rotating log file (2 MB × 3 files) |
| `last_telegram.txt` | Timestamp of last Telegram notification |
### Scan State Management
`scanner.py` uses a shared `scan_state` dict with threading lock:
```python
scan_state = {
"running": bool,
"step": str,
"step_index": int,
"step_total": 8,
"detail": str,
"error": None,
"last_completed": timestamp,
"last_duration": seconds,
}
```
Only one scan can run at a time. Concurre
t requests are rejected.
## Key Patterns
### Deep Merge
Config uses a recursive deep-merge to preserve all default keys when user saves partial config. See `_deep_merge()` in `app/config.py`.
### Media Server Abstraction
Both `plex_xml.py` and `jellyfin_api.py` export `scan_movies()` that returns identical tuple structure, allowing scanner.py to work with either server without logic changes.
### TMDB Caching
All TMDB API calls are cached permanently in `tmdb_cache.json`. Only new lookups hit the API. Cache is shared across scans.
### Background Scanning
`build_async()` spawns a thread running `build()`. The main thread never blocks. Progress is exposed via `scan_state` dict.
### Telegram Rate Limiting
Notifications respect `TELEGRAM_MIN_INTERVAL` to avoid spam. Last notification time persisted in `/data/last_telegram.txt`.
## API Endpoints
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/version` | App version |
| GET | `/api/config` | Full config |
| POST | `/api/config` | Save config (triggers scheduler restart) |
| GET | `/api/config/status` | `{configured: bool}` |
| GET | `/api/results` | Last scan results (never blocks) |
| POST | `/api/scan` | Start background scan |
| GET | `/api/scan/status` | Live scan progress |
| POST | `/api/ignore` | Ignore movie/franchise/director/actor |
| POST | `/api/unignore` | Remove ignore |
| POST | `/api/wishlist/add` | Add to wishlist |
| POST | `/api/wishlist/remove` | Remove from wishlist |
| POST | `/api/radarr/add` | Add movie to Radarr |
| GET | `/api/logs` | Last N lines of cineplete.log |
## CI/CD
GitHub Actions workflow (`.github/workflows/docker.yml`) runs on **every branch push**:
1. Security scan (Trivy — blocks on CRITICAL/HIGH CVEs)
2. Unit tests (pytest)
3. **E2E tests** (Playwright Chromium — builds image locally, spins up container, runs full suite)
4. Semantic versioning based on commit message (main branch only):
- `fix:` → patch bump
- `feat:` → minor bump
- `feat!:` → major bump
5. Multi-arch Docker build (amd64 + arm64)
6. Push to Docker Hub (`latest` + version tag on main, `STG` + branch-sha on other branches)
**Version tags and DockerHub push only happen after all e2e tests pass.**
**Version tags only created on main branch**. Feature branches get `STG` + branch-sha tags.
## Important Notes
### Commit Message Conventions
Follow conventional commits for automatic versioning:
- `fix: bug description` → patch (v1.2.3 → v1.2.4)
- `feat: new feature` → minor (v1.2.3 → v1.3.0)
- `feat!: breaking change` → major (v1.2.3 → v2.0.0)
### TMDB API Key
Use the **API Key (v3)** from TMDB settings, NOT the Read Access Token (JWT). The API key is a short alphanumeric string, the token is a long JWT starting with `eyJ`.
### Radarr Integration
Movies added to Radarr have `searchForMovie = false` by default (controlled by `RADARR_SEARCH_ON_ADD`). They are added but NOT automatically downloaded.
### Jellyfin Support
Jellyfin support is on the `feature-jellyfin` branch. The scanner uses the exact same return structure as Plex, so no changes to `scanner.py` are needed.
### Docker Volumes
- `/config` - config.yml (editable from UI)
- `/data` - results, cache, logs, overrides (persist across updates)
### Port Configuration
Default port is 8787. Override with `APP_PORT` environment variable or change `SERVER.UI_PORT` in config.
## Security Rules
**All sensitive fields must use `secret` input type (password-masked with show/hide toggle).**
This applies to every field that holds a token, key, password, secret, or bearer credential — without exception:
- API keys (TMDB, Radarr, Overseerr, Jellyseerr, Watchtower, any future integration)
- Bearer tokens
- Webhook secrets
- Passwords
- Any field whose name contains: `key`, `token`, `secret`, `password`, `bearer`, `credential`
Implementation pattern (already used for `cfg_tmdb_key` and `cfg_wh_secret`):
```js
field("cfg_field_name", "Label", value, "secret")
```
The `field()` helper in `config.js` renders a `type="password"` input with a show/hide eye toggle when the 4th argument is `"secret"`. Never render these as `type="text"`.
## Testing Rules
**Every code change must keep the test suite green. Specifically:**
[### When adding a new backend feature or API endpoint
- Add or update unit tests in `tests/` covering the new logic
- Add or update the relevant smoke test in `e2e/tests/smoke.spec.js` if a new API endpoint is introduced (check it returns 200 and the expected shape)
](https://x.com/sdblepas/status/2036704070631674111?s=20)### When changing the UI (new tab, renamed tab, new button, new form field, modal change)
- Update `e2e/tests/navigation.spec.js` if a nav tab is added, removed, or renamed — keep `ALL_TABS`, `TITLE_TABS`, and `DATA_GATED` arrays in sync
- Update `e2e/tests/config.spec.js` if the Settings form changes (new fields, renamed buttons, new sections)
- If a new modal is added, add a DOM-manipulation open/close test (do NOT call API-backed JS functions like `openMovieModal()` in tests — they trigger real external API calls)
### Key facts about the CI test environment
- The app starts with **no scan data** and **no configuration** (empty `/data` and `/config` volumes)
- `!CONFIGURED` → the app forces the config tab for all navigation — only `dashboard` and `config` tabs show their correct `#page-title`
- Data-gated tabs (`notmdb`, `nomatch`, `duplicates`, `franchises`, `directors`, `actors`, `classics`, `suggestions`, `wishlist`) call `renderSkeleton()` when no data exists — `#page-title` stays `"Dashboard"`. Test the `.active` class, not the page title, for these tabs
- `PAGE_TITLES.config = "Configuration"` (not "Settings") — always use the value from `app.js` `PAGE_TITLES` dict as the source of truth for expected titles
## Shipped ✅
- **Radarr 4K instance** — v2.6.0
- **Scheduled auto-scan** (daily / weekly cron) — v2.6.0
- **Quality profile dropdown** — fetch real profile names from Radarr API — v2.6.0
- **Movie posters** — v2.5.x
- **CSV export** — v2.5.x
- **Dashboard charts** (donut + KPI strip) — v2.5.x
- **Smart authentication** — network-aware, local bypass, PBKDF2, sliding cookie — v2.5.x
- **Watchtower on-demand update** — `POST /v1/update?scope=cineplete` — v2.4.x
- **Watchtower UI** — "Update now" button in Settings + auto-update toggle — v2.4.x
- **New release notification** — non-intrusive badge in sidebar footer when newer version on GitHub — v2.5.x
- **Ignore list** — 🚫 button on every movie card, dedicated Ignored tab, reversible, metadata stored — v2.7.0
- **Export button fix** — was hidden on all tabs due to CSS specificity conflict — v2.7.0
- **Letterboxd tab** — persistent URL list, multi-list scoring (×N badge), owned movies filtered, curator RSS auto-expansion, description HTML fallback — v2.7.0
- **FlareSolverr support** — automatic fallback for Cloudflare-blocked Letterboxd list RSS feeds — v2.7.0
## Backlog (next version)
- **Multiple library support** — currently only one Plex/Jellyfin movie library can be scanned at a time. Users with separate libraries (e.g. 4K + 1080p, or Movies + Anime) need to reconfigure to switch. Future work: allow selecting or scanning multiple libraries and merging results.
- ~~**Letterboxd tab: background fetch + caching**~~ ✅ fixed — `data/letterboxd_cache.json`, background thread via `_lb_do_refresh()`, instant GET, auto-refresh on add/remove URL, JS polls every 5s, shows "Updated Xm ago" / "↻ Refreshing…".
- **Bulk actions** — No way to select multiple movies and act on them at once. Every tab should support a multi-select mode with "Add all to Radarr", "Add all to Wishlist", and "Ignore all" batch actions.
- **Wishlist → Radarr sync status** — After sending a movie to Radarr there is no feedback on whether Radarr has it, is searching, or has downloaded it. The wishlist should show a status badge per movie: queued / downloading / available / missing.
- **Notifications for Radarr grabs** — "Radarr downloaded X from your wishlist" — closes the loop between discovery and acquisition. Telegram/webhook notification when a wishlist movie is grabbed by Radarr.