# 🎬 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 -- journalctl -u cineplete -f # live logs pct exec -- bash # open shell # Update: pct exec -- 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 β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` ---