Ctrl K
ComunidadGuías…

README.md

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

Archivo original ↗Preguntar a KiBot
# 🎬 Cineplete — Plex, Jellyfin & Emby Movie Audit

[![Build & Publish Docker](https://github.com/sdblepas/CinePlete/actions/workflows/docker.yml/badge.svg)](https://github.com/sdblepas/CinePlete/actions/workflows/docker.yml)
[![Docker Pulls](https://img.shields.io/docker/pulls/sdblepas/cineplete)](https://hub.docker.com/r/sdblepas/cineplete)
[![Docker Image Version](https://img.shields.io/docker/v/sdblepas/cineplete/latest)](https://hub.docker.com/r/sdblepas/cineplete)
![License](https://img.shields.io/github/license/sdblepas/CinePlete)

![Python](https://img.shields.io/badge/python-3.11-blue)
![Self Hosted](https://img.shields.io/badge/self--hosted-ready-brightgreen)
![Multi-Arch](https://img.shields.io/badge/docker-multiarch-blue)
![FastAPI](https://img.shields.io/badge/FastAPI-powered-green)
[![Playwright](https://img.shields.io/badge/tested%20with-Playwright-45ba4b?logo=playwright&logoColor=white)](https://playwright.dev)

![Plex](https://img.shields.io/badge/Plex-compatible-orange)
![Jellyfin](https://img.shields.io/badge/Jellyfin-compatible-7B2FBE)
![Emby](https://img.shields.io/badge/Emby-compatible-00A4DC)
![Radarr](https://img.shields.io/badge/Radarr-integration-purple)
![Overseerr](https://img.shields.io/badge/Overseerr-integration-F59E0B?logo=overseerr&logoColor=white)
![Jellyseerr](https://img.shields.io/badge/Jellyseerr-integration-29B4E8)
![Trakt](https://img.shields.io/badge/Trakt-integration-ED2224?logo=trakt&logoColor=white)
![Watchtower](https://img.shields.io/badge/Watchtower-auto--update-2496ED)
![TMDB](https://img.shields.io/badge/TMDB-API-blue)
![Homelab](https://img.shields.io/badge/homelab-friendly-blue)
![Proxmox](https://img.shields.io/badge/Proxmox-LXC--ready-E57000?logo=proxmox&logoColor=white)
![GitHub Stars](https://img.shields.io/github/stars/sdblepas/CinePlete?style=social)

> 🇫🇷 [Version française](README.fr.md)

---

Ever wondered **which movies you're missing** from your favorite franchises, directors, or actors?

**Cineplete scans your Plex, Jellyfin or Emby library in seconds and shows exactly what's missing.**

✔ Missing movies from franchises
✔ Missing films from directors you collect
✔ Popular movies from actors already in your library
✔ Classic films missing from your collection
✔ Tailor-made suggestions based on your library

All in a **beautiful dashboard with charts and Radarr integration.**
Supports **Plex, Jellyfin and Emby** — switchable from the Config tab.

![Cineplete Demo](assets/Demo.gif)

## Overview

**Cineplete** is a self-hosted Docker tool that scans your **Plex, Jellyfin or Emby** movie library and identifies:

- Missing movies from franchises
- Missing films from directors you already collect
- Popular films from actors already present in your library
- Classic movies missing from your collection
- Personalized suggestions based on what your library recommends
- Metadata issues (missing TMDB GUID or broken matches)
- Wishlist management
- Direct Radarr integration

The tool includes a **web UI dashboard with charts**, a **Logs tab** for diagnostics, and performs **ultra-fast library scans** (~2 seconds for Plex, depends on library size for Jellyfin).

---

## Features

### Media Server Support — Plex, Jellyfin & Emby

Cineplete supports three media servers. Switch between them from the **Config tab** — no restart needed.

| | Plex | Jellyfin | Emby |
|---|---|---|---|
| Scanner | Native XML API | Jellyfin HTTP API | Emby HTTP API |
| Speed | ~2s for 1000 movies | Depends on library size | Depends on library size |
| Credentials | URL + Token | URL + API Key | URL + API Key |
| Library polling | ✔ | ✔ | ✔ |
| Test Connection | ✔ | ✔ | ✔ |

**Jellyfin setup:**
1. In Jellyfin, go to **Dashboard → API Keys** and create a new key for Cineplete.
2. In Cineplete Config, set `Media Server` to `Jellyfin`, enter the URL, API key, and library name.
3. Use the **Test Connection** button to verify before saving.

**Emby setup:**
1. In Emby, go to **Dashboard → API Keys** and create a new key for Cineplete.
2. In Cineplete Config, add an Emby library entry, enter the URL (e.g. `http://emby:8096`), API key, and library name.
3. Use the **Test Connection** button to verify before saving.

---

### Multiple Libraries Support

**New in v3.0:** Scan multiple libraries simultaneously — mix Plex and Jellyfin, or connect to multiple servers.

**Key features:**
- **Concurrent scanning** — all enabled libraries scanned in parallel using ThreadPoolExecutor
- **Merged results** — duplicates detected automatically across libraries (same TMDB ID)
- **Per-library toggle** — enable/disable individual libraries from the Config tab
- **Unified dashboard** — all charts and tabs show merged data from all active libraries
- **Progressive optimization** — unchanged libraries skip re-analysis (2-3 minutes → 2 seconds)

**Use cases:**
- Scan both Plex (main server) + Jellyfin (4K server)
- Multiple Plex servers (e.g. home + remote)
- Separate libraries (Movies + Anime + Foreign)
- Mix server types in one deployment

**Configuration:**

Libraries are managed from **Config → Libraries**. Each library has:
- **Type** — `plex`, `jellyfin` or `emby`
- **Enabled** — toggle on/off without deleting credentials
- **Label** — friendly name (shown in scan progress)
- **Connection settings** — URL, token/API key, library name

**Example config.yml:**

```yaml
LIBRARIES:
  - id: "plex-main"
    type: "plex"
    enabled: true
    label: "Plex Main"
    url: "http://192.168.1.10:32400"
    token: "xxxxxxxxxxxx"
    library_name: "Movies"
    page_size: 500
    short_movie_limit: 60

  - id: "jellyfin-4k"
    type: "jellyfin"
    enabled: true
    label: "Jellyfin 4K"
    url: "http://192.168.1.20:8096"
    api_key: "xxxxxxxxxxxx"
    library_name: "Movies 4K"
    page_size: 500
    short_movie_limit: 60

  - id: "emby-main"
    type: "emby"
    enabled: true
    label: "Emby Main"
    url: "http://192.168.1.30:8096"
    api_key: "xxxxxxxxxxxx"
    library_name: "Movies"
    page_size: 500
    short_movie_limit: 60
```

**Auto-migration from v2.x:**

Legacy flat config (single `MEDIA_SERVER` setting) automatically migrates to the new `LIBRARIES` list format on first startup. No manual action required — your existing setup continues to work.

**Performance:**

With 2 libraries (1000 movies each):
- Sequential: 4 seconds (Plex) + 6 seconds (Jellyfin) = 10 seconds
- **Concurrent: ~6 seconds** (parallel scan)

---

### Ultra Fast Plex Scanner

The scanner uses the **native Plex XML API** instead of slow metadata requests.

Performance example:

- 1000 movies → ~2 seconds
- 3000 movies → ~4 seconds

---

### Dashboard

The dashboard shows a full visual overview of your library:

**Score cards:**
- Franchise Completion %
- Directors Score %
- Classics Coverage %
- Global Cinema Score %

**Charts (Chart.js):**
- Franchise Status — doughnut: Complete / Missing 1 / Missing 2+
- Classics Coverage — doughnut: In library vs missing
- Metadata Health — doughnut: Valid TMDB / No GUID / No Match
- Top 10 Actors in library — horizontal bar
- Directors by missing films — grouped bar (0 / 1–2 / 3–5 / 6–10 / 10+)
- Library Stats panel

Ignored franchises are excluded from the Franchise Status chart automatically.

---

### Franchises

Detects **TMDB collections (sagas)** and lists missing films.

Example:

```
Alien Collection (6/7)
Missing: Alien Romulus
```

---

### Directors

Detects missing films from directors already in your library.

Example:

```
Christopher Nolan
Missing: Following, Insomnia
```

---

### Actors

Finds **popular films of actors already in your Plex library**.

Filter criteria:

```
vote_count >= 500
```

Sorted by popularity, vote_count, vote_average.

---

### Classics

Detects missing films from **TMDB Top Rated**.

Default criteria:

```
vote_average >= 8.0
vote_count >= 5000
```

---

### Suggestions

Personalized movie recommendations based on **your own library**.

For each film in your Plex library, Cineplete fetches TMDB recommendations and scores each suggested title by how many of your films recommended it. A film recommended by 30 of your movies ranks higher than one recommended by 2.

Each suggestion card shows a **⚡ N matches** badge so you can see at a glance how strongly your library points to it.

API calls are cached permanently — only newly added films incur real HTTP calls on subsequent scans.

---

### Wishlist

Interactive wishlist with UI buttons on every movie card.

Movies can be added from any tab: franchises, directors, actors, classics, suggestions.

Wishlist is stored in:

```
data/overrides.json
```

---

### Metadata Diagnostics

**No TMDB GUID** — Movies without TMDB metadata.  
Fix inside Plex: `Fix Match → TheMovieDB`

**TMDB No Match** — Films with an invalid TMDB ID that returns no data. The Plex title is shown so you can identify the film immediately.  
Fix: Refresh metadata or fix match manually in Plex.

---

### Ignore System

Permanently ignore franchises, directors, actors, or specific movies via UI buttons.
Ignored items are excluded from all lists and charts.

**Movie-level ignore** — every movie card now has a 🚫 button. Ignored movies appear in a dedicated **Ignored** tab where they can be restored with one click. Metadata (title, year, poster) is stored so the Ignored tab displays properly even before a rescan.

Stored in:

```
data/overrides.json
```

---

### Letterboxd Tab

Import and browse movies from any public Letterboxd URL — watchlists, named lists, diary feeds, or curator profile RSS feeds.

**Features:**
- Add multiple URLs — each is fetched and merged into a single scored grid
- Movies appearing in more than one list get a **×N** badge and are sorted to the top
- Movies already in your library are automatically filtered out
- ↻ refresh button to re-fetch all lists on demand
- URLs are persisted in `overrides.json` and survive container restarts

**Supported URL formats:**

| URL | RSS used |
|-----|----------|
| `letterboxd.com/you/watchlist/` | `/watchlist/rss/` |
| `letterboxd.com/you/list/my-list/` | `/list/my-list/rss/` |
| `letterboxd.com/you/rss/` | used as-is (diary) |
| `letterboxd.com/you/films/` | falls back to diary `/rss/` |
| curator profile RSS (e.g. `/mscorsese/rss/`) | auto-expanded — each linked list's RSS is fetched individually |

> All lists must be public. Private Letterboxd accounts or patron-only lists are not accessible.

---

### FlareSolverr Integration

Some Letterboxd list RSS feeds are protected by Cloudflare (403). If you run [FlareSolverr](https://github.com/FlareSolverr/FlareSolverr) in your stack, CinePlete will automatically route blocked requests through it.

Configure the URL in **Config → FlareSolverr** (e.g. `http://flaresolverr:8191`). When a direct fetch returns 403, CinePlete retries via FlareSolverr transparently — no changes needed to your Letterboxd URLs.

```yaml
flaresolverr:
  image: ghcr.io/flaresolverr/flaresolverr:latest
  container_name: flaresolverr
  ports:
    - "8191:8191"
  restart: unless-stopped
```

---

### Search, Filter & Sort

All tabs support live filtering:

- **Search** by title or group name (director / actor / franchise)
- **Year filter** — 2020s / 2010s / 2000s / 1990s / Older
- **Sort** — popularity / rating / votes / year / title

---

### Async Scan with Progress

Clicking **Rescan** launches a background scan immediately without blocking the UI.

A live progress card appears showing:

```
Step 3/8 — Analyzing collections
[=====>      ] 43%
```

The progress card disappears automatically when the scan completes.

Only one scan can run at a time. Concurrent scan requests are rejected cleanly.

---

### Logs

A dedicated **Logs tab** shows the last 200 lines of `/data/cineplete.log` with color-coded severity levels (ERROR in red, WARNING in amber). Useful for diagnosing scan issues, TMDB API errors, and Plex connectivity problems.

The log file rotates automatically (2 MB × 3 files) and never fills your disk.

---

### Version Notifications

CinePlete checks the [GitHub Releases](https://github.com/sdblepas/CinePlete/releases) API once per hour. When a new version is available, a banner appears in the sidebar below the current version with a direct link to the release notes.

---

### Smart Authentication

CinePlete supports Radarr-style authentication, configurable from **Config → Authentication**.

| Mode | Behaviour |
|------|-----------|
| **None** | Open access — no login required (default) |
| **Forms** | Username + password required for all access |
| **Local network free** | No auth on local IPs (`10.x`, `192.168.x`, `127.x`), login required from internet |

- Passwords hashed with **PBKDF2-SHA256** — never stored in plain text
- **7-day sliding session cookie** — stay logged in across browser sessions
- **"Trust this browser"** toggle — persistent vs session cookie
- Logout button in the sidebar footer

> Auth defaults to **None** — existing deployments are unaffected until you configure it.

---

### API Key Authentication

Generate named API keys from **Config → API Keys** to allow headless access from external tools (n8n, Home Assistant, scripts, cron jobs) without a browser session.

**Usage — pass the key in any request:**

```bash
# Header (recommended)
curl -H "X-Api-Key: cp_YOUR_KEY" http://your-server/api/results

# Query parameter
curl "http://your-server/api/results?apikey=cp_YOUR_KEY"
```

> Keys are only enforced when Auth Mode is set to **Forms** or **Local network free**. With Auth Mode **None**, the API is open regardless.

**Key format:** `cp_` prefix + 64 random hex characters. Only the SHA-256 hash is stored — the raw key is shown once at creation and cannot be recovered.

**All endpoints accessible with an API key:**

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/results` | Full scan results (all tabs) |
| POST | `/api/scan` | Trigger a background scan |
| GET | `/api/scan/status` | Live scan progress |
| GET | `/api/export` | Export to CSV or Letterboxd format |
| GET | `/api/search` | Full-text search across all tabs |
| GET | `/api/movie/{tmdb_id}` | Movie detail (metadata, cast, trailer) |
| GET | `/api/config` | Current configuration |
| POST | `/api/config` | Save configuration |
| POST | `/api/ignore` | Ignore a movie / franchise / director / actor |
| POST | `/api/unignore` | Remove an ignore |
| GET | `/api/ignored` | List all ignored items |
| POST | `/api/wishlist/add` | Add movie to wishlist |
| POST | `/api/wishlist/remove` | Remove from wishlist |
| POST | `/api/radarr/add` | Send movie to Radarr |
| POST | `/api/overseerr/add` | Request movie in Overseerr |
| POST | `/api/jellyseerr/add` | Request movie in Jellyseerr |
| GET | `/api/streaming/{tmdb_id}` | JustWatch streaming availability |
| GET | `/api/theaters` | Now-playing + upcoming films |
| GET | `/api/quality/upgrades` | Movies eligible for 4K upgrade |
| GET | `/api/trakt/watched` | Trakt watched movie IDs |
| POST | `/api/trakt/watched/refresh` | Bust Trakt watched cache |
| GET | `/api/trakt/status` | Trakt connection state |
| GET | `/api/trakt/debug` | Trakt diagnostic info |
| GET | `/api/letterboxd/urls` | Saved Letterboxd URLs |
| POST | `/api/letterboxd/urls` | Add a Letterboxd URL |
| POST | `/api/letterboxd/urls/remove` | Remove a Letterboxd URL |
| GET | `/api/letterboxd/movies` | Scored Letterboxd movie list |
| POST | `/api/letterboxd/refresh` | Refresh Letterboxd cache |
| GET | `/api/logs` | Last N lines of cineplete.log |
| GET | `/api/cache/info` | TMDB cache file info |
| POST | `/api/cache/backup` | Backup TMDB cache |
| POST | `/api/cache/restore` | Restore TMDB cache from backup |
| GET | `/api/apikeys` | List API keys (no hashes exposed) |
| POST | `/api/apikeys` | Generate a new API key |
| DELETE | `/api/apikeys/{id}` | Revoke an API key |
| POST | `/api/webhook` | Trigger scan via webhook |
| POST | `/api/watchtower/update` | Trigger Watchtower auto-update |

---

### Auto-Update via Watchtower

CinePlete can automatically update itself using [Watchtower](https://containrrr.dev/watchtower/).

Configure it from the **Config → Watchtower** section:
- Toggle **Auto-update enabled**
- Set the **Watchtower URL** (e.g. `http://10.0.0.1:8081`)
- Set the **API Token** (matches `WATCHTOWER_HTTP_API_TOKEN` on your Watchtower container)
- Click **Update Now** to trigger an immediate pull & restart

Watchtower docker-compose example:
```yaml
watchtower:
  image: containrrr/watchtower
  environment:
    - WATCHTOWER_HTTP_API_UPDATE=true
    - WATCHTOWER_HTTP_API_TOKEN=your_token_here
    - WATCHTOWER_HTTP_API_PERIODIC_POLLS=true  # enable scheduled + on-demand
    - WATCHTOWER_POLL_INTERVAL=86400           # 24h auto-check
    - WATCHTOWER_CLEANUP=true
  ports:
    - "8081:8080"
  volumes:
    - /var/run/docker.sock:/var/run/docker.sock
  restart: unless-stopped
```

> The `?scope=cineplete` parameter ensures only CinePlete is updated. Add `com.centurylinklabs.watchtower.scope=cineplete` to your CinePlete container labels (already included in the docker-compose example above).

---

### Trakt.tv Integration

Connect your Trakt account to overlay your watched history on every movie card and optionally hide already-watched films from all grids.

**Setup:**
1. Create an application at [trakt.tv/oauth/applications](https://trakt.tv/oauth/applications) to get a **Client ID** and **Client Secret**.
2. In CinePlete, go to **Config → Trakt.tv**, enter your credentials, and click **Connect**.
3. A device code appears — visit [trakt.tv/activate](https://trakt.tv/activate) and enter it.
4. CinePlete polls Trakt automatically and saves your tokens once authorised.

**Features:**
- 🔴 **Watched badge** — a red ribbon appears on every card for films already in your Trakt history
- 🚫 **Hide watched** — toggle *Hide watched movies from all grids* to remove watched films from Franchises, Directors, Actors, Classics and Suggestions (Wishlist always shows all films)
- ⟳ **Manual refresh** — click *Refresh history* to force an immediate update (server cache is 1 hour)
- 🔌 **Disconnect** — clears tokens from `config.yml` immediately; reconnect any time

**Config reference:**

```yaml
TRAKT:
  TRAKT_ENABLED: true
  TRAKT_CLIENT_ID: "your_client_id"
  TRAKT_CLIENT_SECRET: "your_client_secret"
  TRAKT_ACCESS_TOKEN: ""      # filled automatically after connect
  TRAKT_REFRESH_TOKEN: ""     # filled automatically after connect
  TRAKT_USERNAME: ""          # filled automatically after connect
  TRAKT_HIDE_WATCHED: false
```

> Trakt uses **Device Code OAuth** — no redirect URI required, works behind NAT and reverse proxies. Access tokens are refreshed automatically on expiry.

---

### Radarr Integration

Movies can be added to Radarr with one click from any movie card.

**Important:** `searchForMovie = false`
- ✔ Movie is added to Radarr
- ✘ Download is NOT started automatically

---

## Configuration

Configuration is stored in `config/config.yml` and editable from the **Config** tab in the UI.

**Basic settings:**

| Key | Description |
|-----|-------------|
| `TMDB_API_KEY` | TMDB classic API Key (v3) — **not** the Read Access Token |

> ⚠️ Use the **API Key** found under TMDB → Settings → API → **API Key** (short alphanumeric string starting with letters/numbers). Do **not** use the Read Access Token (long JWT string starting with `eyJ`).

**Libraries (v3.0+):**

Libraries are configured from **Config → Libraries** in the UI. Each library entry includes:

| Key | Required | Description |
|-----|----------|-------------|
| `id` | Yes | Unique identifier (e.g. `plex-0`, `jellyfin-4k`, `emby-main`) |
| `type` | Yes | `plex`, `jellyfin` or `emby` |
| `enabled` | Yes | `true` to scan, `false` to skip |
| `label` | No | Friendly name shown in scan progress |
| `url` | Yes | Server URL (e.g. `http://192.168.1.10:32400`) |
| `token` | Plex only | Plex authentication token |
| `api_key` | Jellyfin / Emby only | Jellyfin or Emby API key |
| `library_name` | Yes | Name of the movie library |
| `page_size` | No | API page size (default: 500) |
| `short_movie_limit` | No | Skip films shorter than N minutes (default: 60) |

**Example multi-library config:**

```yaml
LIBRARIES:
  - id: "plex-main"
    type: "plex"
    enabled: true
    label: "Plex Main"
    url: "http://192.168.1.10:32400"
    token: "xxxxxxxxxxxx"
    library_name: "Movies"

  - id: "jellyfin-4k"
    type: "jellyfin"
    enabled: false
    label: "Jellyfin 4K"
    url: "http://192.168.1.20:8096"
    api_key: "xxxxxxxxxxxx"
    library_name: "Movies 4K"

  - id: "emby-main"
    type: "emby"
    enabled: false
    label: "Emby Main"
    url: "http://192.168.1.30:8096"
    api_key: "xxxxxxxxxxxx"
    library_name: "Movies"
```

> **Legacy config (v2.x):** Flat `PLEX_URL`, `PLEX_TOKEN`, `JELLYFIN_URL` settings still work and auto-migrate to `LIBRARIES` format on first load.

**Advanced settings** (accessible via the UI "Advanced settings" section):

| Key | Default | Description |
|-----|---------|-------------|
| `CLASSICS_PAGES` | 4 | Number of TMDB Top Rated pages to fetch |
| `CLASSICS_MIN_VOTES` | 5000 | Minimum vote count for classics |
| `CLASSICS_MIN_RATING` | 8.0 | Minimum rating for classics |
| `CLASSICS_MAX_RESULTS` | 120 | Maximum classic results to return |
| `ACTOR_MIN_VOTES` | 500 | Minimum votes for an actor's film to appear |
| `ACTOR_MAX_RESULTS_PER_ACTOR` | 10 | Max missing films shown per actor |
| `PLEX_PAGE_SIZE` | 500 | Plex API page size |
| `JELLYFIN_PAGE_SIZE` | 500 | Jellyfin API page size |
| `SHORT_MOVIE_LIMIT` | 60 | Films shorter than this (minutes) are ignored |
| `SUGGESTIONS_MAX_RESULTS` | 100 | Maximum suggestions to return |
| `SUGGESTIONS_MIN_SCORE` | 2 | Minimum number of your films that must recommend a suggestion |

---

## Installation

### Option 1 — Generic LXC / VM / Bare Metal (Debian · Ubuntu · Raspberry Pi)

One-liner — run inside your container or VM:

```bash
curl -fsSL https://raw.githubusercontent.com/sdblepas/CinePlete/main/install/install.sh | sudo bash
```

What it does: installs Python 3.11+, creates a dedicated `cineplete` user, downloads the latest release, sets up a Python virtualenv, and registers a systemd service that starts automatically on boot.

**Re-running the same command acts as an update.**

Post-install management:
```bash
journalctl -u cineplete -f          # live logs
systemctl restart cineplete          # restart
systemctl status cineplete           # status
```

---

### Option 2 — Proxmox LXC (one command on the Proxmox host)

Run this **on your Proxmox host** as root — it creates a fresh unprivileged Debian 12 LXC and installs CinePlete inside it automatically:

```bash
bash -c "$(curl -fsSL https://raw.githubusercontent.com/sdblepas/CinePlete/main/install/proxmox-lxc.sh)"
```

**With custom options:**
```bash
CT_ID=200 CT_IP=192.168.1.50/24 CT_GW=192.168.1.1 CT_RAM=1024 \
  bash -c "$(curl -fsSL https://raw.githubusercontent.com/sdblepas/CinePlete/main/install/proxmox-lxc.sh)"
```

| Variable | Default | Description |
|----------|---------|-------------|
| `CT_ID` | next available | LXC container ID |
| `CT_IP` | `dhcp` | Static IP in CIDR notation (e.g. `192.168.1.50/24`) |
| `CT_GW` | _(none)_ | Gateway for static IP |
| `CT_CORES` | `2` | CPU cores |
| `CT_RAM` | `512` | RAM in MB |
| `CT_DISK` | `4` | Disk in GB |
| `CT_BRIDGE` | `vmbr0` | Network bridge |
| `PORT` | `7474` | App listen port |

Post-install management from the Proxmox host:
```bash
pct exec <CT_ID> -- journalctl -u cineplete -f     # live logs
pct exec <CT_ID> -- bash                            # open shell
# Update:
pct exec <CT_ID> -- bash -c "curl -fsSL https://raw.githubusercontent.com/sdblepas/CinePlete/main/install/install.sh | bash"
```

---

### Option 3 — Docker Compose (recommended)

```yaml
version: "3.9"
services:
  cineplete:
    image: sdblepas/cineplete:latest
    container_name: cineplete
    ports:
      - "8787:8787"
    volumes:
      - /path/to/config:/config
      - /path/to/data:/data
    labels:
      net.unraid.docker.webui: "http://[IP]:[PORT:8787]"
      net.unraid.docker.icon: "https://raw.githubusercontent.com/sdblepas/CinePlete/main/assets/icon.png"
      org.opencontainers.image.url: "http://localhost:8787"
      com.centurylinklabs.watchtower.scope: "cineplete"
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8787')"]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 20s
    restart: unless-stopped
```

**Port conflict?** Add `APP_PORT` to change the internal port:

```yaml
environment:
  - APP_PORT=8788
ports:
  - "8788:8788"
```

Start:

```bash
docker compose up -d
```

Open UI:

```
http://YOUR_NAS_IP:8787
```

---

## Project Structure

```
CinePlete/
├── .github/
│   └── workflows/
│       └── docker.yml        # CI/CD pipeline (scan → test → version → build)
├── app/
│   ├── web.py                # FastAPI backend + all API endpoints
│   ├── scanner.py            # 8-step scan engine (threaded)
│   ├── plex_xml.py           # Plex XML API scanner
│   ├── jellyfin_api.py       # Jellyfin HTTP API scanner
│   ├── emby_api.py           # Emby HTTP API scanner
│   ├── scheduler.py          # Library auto-poll scheduler (Plex, Jellyfin & Emby)
│   ├── tmdb.py               # TMDB API client (cached, key-safe, error logging)
│   ├── overrides.py          # Ignore/wishlist/rec_fetched_ids helpers
│   ├── config.py             # Config loader/saver with deep-merge
│   └── logger.py             # Shared rotating logger (console + file)
├── static/
│   ├── index.html            # Single-page app shell + all CSS
│   └── app.js                # All UI logic: routing, rendering, API calls
├── assets/
│   └── icon.png              # App icon (used by Unraid WebUI label)
├── config/
│   └── config.yml            # Default config template
├── tests/
│   ├── test_config.py
│   ├── test_jellyfin_api.py  # 16 unit tests for Jellyfin scanner
│   ├── test_overrides.py
│   ├── test_scheduler.py
│   └── test_scoring.py
├── docker-compose.yml
├── Dockerfile
└── README.md
```

---

## Data Files

All persistent data lives in the mounted `/data` volume and survives container updates:

| File | Description |
|------|-------------|
| `results.json` | Full scan output — regenerated on each scan |
| `tmdb_cache.json` | TMDB API response cache — persists between scans |
| `overrides.json` | Ignored items, wishlist, rec_fetched_ids |
| `cineplete.log` | Rotating log file (2 MB × 3 files) |

---

## API Endpoints

> All endpoints below are accessible with an **API key** (`X-Api-Key` header or `?apikey=` query param) when auth is enabled. See [API Key Authentication](#api-key-authentication) for details.

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/version` | Current app version + update check |
| GET | `/api/results` | Full scan results (all tabs) |
| POST | `/api/scan` | Start a background scan |
| GET | `/api/scan/status` | Live scan progress (8 steps) |
| GET | `/api/export` | Export to CSV or Letterboxd format |
| GET | `/api/search` | Full-text search across all tabs |
| GET | `/api/movie/{tmdb_id}` | Movie detail (metadata, cast, trailer) |
| GET | `/api/config` | Current configuration |
| POST | `/api/config` | Save configuration |
| GET | `/api/config/status` | Returns `{configured: bool}` |
| POST | `/api/library/test` | Test Plex / Jellyfin / Emby connection |
| POST | `/api/ignore` | Ignore a movie / franchise / director / actor |
| POST | `/api/unignore` | Remove an ignore |
| GET | `/api/ignored` | List all ignored items with metadata |
| POST | `/api/wishlist/add` | Add a movie to wishlist |
| POST | `/api/wishlist/remove` | Remove from wishlist |
| POST | `/api/radarr/add` | Send a movie to Radarr |
| GET | `/api/radarr/profiles` | Fetch Radarr quality profiles |
| GET | `/api/radarr/rootfolders` | Fetch Radarr root folders |
| GET | `/api/radarr/library` | TMDB IDs of all Radarr movies |
| GET | `/api/radarr/status` | Per-movie Radarr status |
| POST | `/api/overseerr/add` | Request a movie in Overseerr |
| POST | `/api/jellyseerr/add` | Request a movie in Jellyseerr |
| GET | `/api/streaming/{tmdb_id}` | JustWatch streaming availability |
| GET | `/api/theaters` | Now-playing + upcoming films |
| GET | `/api/quality/upgrades` | Movies eligible for 4K upgrade |
| POST | `/api/quality/refresh` | Bust quality upgrade cache |
| GET | `/api/trakt/watched` | Trakt watched movie IDs (cached 1 h) |
| POST | `/api/trakt/watched/refresh` | Bust Trakt watched cache |
| GET | `/api/trakt/status` | Trakt connection state |
| GET | `/api/trakt/debug` | Trakt diagnostic info (no tokens exposed) |
| POST | `/api/trakt/device/code` | Start Trakt device-code OAuth flow |
| POST | `/api/trakt/device/poll` | Poll Trakt OAuth approval |
| POST | `/api/trakt/disconnect` | Revoke and clear Trakt tokens |
| GET | `/api/letterboxd/urls` | Saved Letterboxd URLs |
| POST | `/api/letterboxd/urls` | Add a Letterboxd URL |
| POST | `/api/letterboxd/urls/remove` | Remove a Letterboxd URL |
| GET | `/api/letterboxd/movies` | Scored Letterboxd movie list |
| POST | `/api/letterboxd/refresh` | Refresh Letterboxd cache |
| GET | `/api/apikeys` | List API keys (no hashes exposed) |
| POST | `/api/apikeys` | Generate a new API key |
| DELETE | `/api/apikeys/{id}` | Revoke an API key |
| GET | `/api/cache/info` | TMDB cache file age and size |
| POST | `/api/cache/backup` | Back up TMDB cache |
| POST | `/api/cache/restore` | Restore TMDB cache from backup |
| GET | `/api/logs` | Last N lines of cineplete.log |
| GET | `/api/auth/status` | Current auth mode and login state |
| POST | `/api/auth/login` | Authenticate with username + password |
| POST | `/api/auth/logout` | Clear the session cookie |
| POST | `/api/webhook` | Trigger scan via authenticated webhook |
| POST | `/api/watchtower/update` | Trigger Watchtower auto-update |

---

## Technologies

- Python 3.11
- FastAPI + Uvicorn
- Docker (multi-arch: amd64 + arm64)
- TMDB API v3
- Plex XML API
- Jellyfin HTTP API
- Emby HTTP API
- Chart.js
- Tailwind CSS (CDN)

---

## Architecture

```
Plex Server          Jellyfin Server          Emby Server
     │                      │                      │
     │ XML API               │ HTTP REST API         │ HTTP REST API
     │ (~2s/1000 movies)     │ (paginated)           │ (/emby/ prefix)
     ▼                      ▼                      ▼
 plex_xml.py         jellyfin_api.py          emby_api.py
  (TMDB IDs,          (TMDB IDs,               (TMDB IDs,
   directors,          directors,               directors,
   actors,             actors,                  actors,
   duplicates)         top 5/film)              top 5/film)
       \                   |                   /
        └──────────── scan_movies() ──────────┘
                           │
                           ▼
            8-Step Scan Engine — scanner.py (background thread)
                           │
     ┌─────────────────────┼──────────────────────┐
     ▼                     ▼                       ▼
 Franchises            Directors               Actors
 (TMDB collections)    (person_credits)        (vote_count ≥ 500)
     │                     │                       │
     └─────────── TMDB API client — tmdb.py ───────┘
                  (thread-safe, disk-cached,
                   key-normalized, flush/50 calls)
                           │
     ┌─────────────────────┼──────────────────────┐
     ▼                     ▼                       ▼
  Classics             Suggestions             Scores
 (Top Rated TMDB)    (recommendations        (franchise / directors /
                      scored by library       classics / global)
                      match count)
                           │
                           ▼
                     results.json ◄──── overrides.json
                     (/data volume)     (wishlist, ignores,
                           │            letterboxd_urls,
                           ▼            rec_fetched_ids)
                   FastAPI — web.py
                   (42 API endpoints)
         ┌─────────────────┼──────────────────┐
         ▼                 ▼                  ▼
    Radarr / 4K      Overseerr /         Letterboxd
    (add, status,    Jellyseerr          (RSS → TMDB,
     grab poller)    (request)           multi-URL merge,
         │                               FlareSolverr)
         ▼
    Telegram
    (scan summary +
     grab notifications)
                           │
                           ▼
         Single-page app — static/js/
    ┌────────┬────────┬────────┬─────────┬─────────┬────────┬────────┐
    │ app.js │scan.js │ api.js │render.js│config.js│filters │modal.js│
    │routing │polling │ fetch  │tabs /   │config   │search/ │detail  │
    │state   │progress│toast   │cards /  │form /   │filter/ │modal / │
    │nav     │badges  │utils   │charts   │cache    │sort    │trailer │
    └────────┴────────┴────────┴─────────┴─────────┴────────┴────────┘
```

---