Ctrl K
ComunidadGuías…

CLAUDE.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# 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.