posterpilot

posterpilot

Docker app from diegopeixoto's Repository

Overview

PosterPilot is a self-hosted artwork manager for Plex, Jellyfin and Emby. Browse your library, find covers across MediUX, Fanart.tv, TMDB and ThePosterDB, and apply them directly to your media server and/or by exporting Kometa/PMM-compatible YAML. Credentials can be set here or in the in-app Settings page (including Plex login). Point the /kometa volume at your existing Kometa config to have exported YAML picked up on its next run.
PosterPilot

Self-hosted artwork manager for Plex, Jellyfin & Emby

Find covers across MediUX, Fanart.tv, TMDB & ThePosterDB and apply them to your media server or via Kometa/PMM — in a single Docker container.

CI Release License: MIT Container Docs Translation status


Media servers

Plex Jellyfin Emby

Artwork providers

MediUX Fanart.tv TMDB ThePosterDB

🌐 Multi-language  ·  🖥️ Direct API  ·  📄 Kometa / PMM YAML  ·  🐳 Docker

Spec-driven via OpenSpec. See openspec/specs/ for the capability specs and openspec/changes/ for in-flight proposals.

📖 Documentation: full installation, configuration, usage, contributing, and translating guides live at diegopeixoto.github.io/posterpilot.

Screenshots

Library — filter/sort bar, spotlight, and poster wall Item detail — backdrop hero, cast, and artwork sets

What it does

  1. Sync your Plex / Jellyfin / Emby movie & show libraries, resolving each title to a TMDB id with rich metadata (backdrop, logo, rating, genres, cast).

  2. Find covers across the enabled providers (MediUX, Fanart.tv, TMDB, ThePosterDB), grouped into collapsible artwork sets per provider — pick a whole set or assemble a custom poster + backdrop set from any provider, a pasted URL, or an uploaded file. For shows you can stage per-season posters and per-episode title cards independently of the show cover, and the best-scored candidate is pre-selected per slot as an overridable suggestion.

  3. Apply a chosen cover, two ways (selectable):

    • Media server API — uploads the poster (and backdrop) and, on Plex, locks the field so agents won't overwrite it.
    • Kometa export — writes url_poster/url_background YAML into a mounted directory your existing Kometa instance consumes on its next run.

    One apply writes every staged slot — show, seasons, and episodes (direct upload resolves the season/episode children on the server; the Kometa export nests seasons: / episodes:). A dry-run preview shows the planned uploads, exports, and skips before a bulk apply runs, and Revert restores the original artwork for a whole show or a single season.

A guided first-install wizard (language → server → TMDB → providers → libraries → first sync) gets you running fast; for Plex it includes PIN login and connection discovery, and Jellyfin and Emby let you sign in with username + password — so you never have to hunt down a token, URL, or API key. Stored credentials and API keys are encrypted at rest (zero-setup with an auto-generated key, or your own APP_SECRET). A metadata-rich item page (backdrop hero, cast, artwork grouped into sets), a Notion-style filtered/sorted library wall with a per-item ignore list, an in-app Activity log, and a UI localized into five languages round it out. Library-wide work runs as background jobs with live progress (SSE) right on the Dashboard — repeat syncs are incremental and bulk apply runs concurrently — and an update checker plus What's New modal surface new releases.

Stack

  • SvelteKit (TypeScript) on Bun, built with adapter-node (run under Bun)
  • SQLite + Drizzle ORM (libsql) — library cache, candidates, history, jobs, settings
  • Tailwind CSS v4, dark image-forward UI
  • In-process job queue + Server-Sent Events for live progress

Develop

bun install
cp .env.example .env          # fill PLEX_URL / PLEX_TOKEN / TMDB_KEY (or use the Settings UI)
bun run db:generate           # generate SQL migrations from the Drizzle schema (already committed)
bun run dev                   # http://localhost:5173

Migrations are applied automatically on server startup. Useful scripts:

script purpose
bun run dev dev server
bun run build production build (adapter-node)
bun run start run the built server (node build)
bun run check svelte-check type checking
bun run test vitest unit tests
bun run format / lint prettier write / check
bun run fallow Fallow code-intelligence health report

Run with Docker (Mac and Unraid)

The same image runs anywhere. Use the official multi-arch image (amd64 + arm64) from GitHub Container Registry:

docker pull ghcr.io/diegopeixoto/posterpilot:latest

Then point docker-compose.yml at image: ghcr.io/diegopeixoto/posterpilot:latest (instead of build: .) and start it:

docker compose up -d
# UI at http://localhost:3000

Or build locally instead:

docker compose up -d --build

Configuration is via environment variables (or the in-app Settings page). Core variables — see the Configuration docs for the complete reference:

var meaning
SERVER_TYPE active media server: plex (default), jellyfin, or emby
PLEX_URL / PLEX_TOKEN Plex base URL and X-Plex-Token (or acquire via in-app login)
PLEX_CLIENT_ID stable per-install id for Plex PIN login / discovery (generated)
JELLYFIN_URL / JELLYFIN_API_KEY Jellyfin server URL and API key
EMBY_URL / EMBY_API_KEY Emby server URL and API key
TMDB_KEY TMDB v3 API key or v4 bearer/JWT (auto-detected)
FANART_KEY Fanart.tv API key (enables the Fanart.tv provider)
PROVIDER_MEDIUX / _TMDB / _FANART / _THEPOSTERDB per-provider on/off toggles
DEFAULT_APPLY_METHOD default apply method: plex, kometa, or both (default)
INCLUDED_SECTIONS library section keys to sync (empty = all movie/show libraries)
APP_LANGUAGE UI locale: en (default), es, zh, ja, pt-BR
KOMETA_ASSETS_DIR where exported Kometa YAML is written (default /kometa)
KOMETA_CONFIG_PATH path to Kometa's own config.yml to manage (empty = feature off)
KOMETA_CONFIG_MODE merge (default, surgical) or own (regenerate the whole file)
LOG_DIR rotating log file folder (default /data/logs in Docker)
EVENT_RETENTION max activity-log rows kept in the db (default 2000)
DATABASE_URL libsql file URL (default file:/data/posterpilot.db in Docker)
PORT listen port (default 3000)

Two volumes matter:

  • /data — persistent SQLite db, settings, and history. Keep this on a mounted volume so state survives container updates. The rotating log file (posterpilot.log, ~5 MB × 5 files) lives at /data/logs, so this one volume covers it too — no extra log mount is needed.
  • /kometa — mount your Kometa assets/config directory here so the exported YAML lands where Kometa reads it.
  • (optional) a Kometa config dir (read/write) if you want PosterPilot to manage Kometa's own config.yml — then set KOMETA_CONFIG_PATH to the mounted file, e.g. /kometa-config/config.yml. PosterPilot co-locates posterpilot.yml in that same directory, so this single mount is all the manager needs.

Unraid

PosterPilot is on the Unraid Community Apps store. Open the Apps tab and search for PosterPilot, then click Install — the template fills in the GHCR image and volumes for you.

Prefer to add it by hand? The template is also at unraid/posterpilot.xml: go to Docker → Add Container and paste the template URL into Template:

https://raw.githubusercontent.com/diegopeixoto/posterpilot/main/unraid/posterpilot.xml

It pre-fills the GHCR image, the WebUI port, the /data and /kometa volumes, and the credential fields (all optional — you can configure them in the in-app Settings page instead, including Plex login).

Point the Kometa volume at your existing Kometa config, e.g. in docker-compose.yml:

volumes:
  - /mnt/user/appdata/posterpilot:/data
  - /mnt/user/appdata/kometa/config:/kometa

Set your media-server and TMDB_KEY credentials in the container's environment (or leave them blank and configure via the Settings page — including Plex login), then browse to the container on port 3000.

How Kometa consumes the export

posterpilot writes a single metadata file (default posterpilot.yml) into KOMETA_ASSETS_DIR, keyed by TMDB id with url_poster / url_background entries — the same shape the legacy scraper produced. Add that file to your Kometa library config (e.g. under metadata_path/metadata_files) so Kometa applies the covers on its next run. Re-applying updates entries in place.

Optionally, PosterPilot can also manage Kometa's own config.yml for you on a dedicated /kometa manager page (a top-level nav item, not a Settings tab) — structured forms for every service connector (plex, tmdb, tautulli, trakt, radarr, sonarr, …), per-library collections, overlays and operations, global settings and webhooks, plus a raw config.yml editor for anything else. The libraries section gets posterpilot.yml wired in automatically, co-located in the same directory as config.yml (no separate metadata path). Set KOMETA_CONFIG_PATH (and mount Kometa's config dir) to enable it. It defaults to a surgical merge that preserves your hand-written keys and comments; an own mode (KOMETA_CONFIG_MODE=own) lets PosterPilot regenerate and fully own the file. Every write is previewed first and leaves a timestamped backup you can restore. See the Kometa manager docs for details.

Health check

The app exposes an unauthenticated GET /api/health that returns { "status": "ok", "version": "x.y.z" } with HTTP 200 — use it as a container health probe (the bundled docker-compose.yml already does):

curl -s http://localhost:3000/api/health

Translating

The UI is localized into English (default), Spanish, Simplified Chinese, Japanese, and Brazilian Portuguese, with per-key English fallback so an untranslated string always shows readable English, never a raw key. The active language is resolved per request from your persisted preference (set via the header switcher or Settings), then your browser's Accept-Language, then English.

Translation status

Translations live as one JSON catalog per locale under messages/ (e.g. messages/es.json), with messages/en.json as the complete source. They are managed through Weblate — join the project to translate in your browser; completed strings land back in the repo via git. New English strings added to en.json automatically appear as untranslated entries for every language. You can also edit a catalog directly and open a PR. See CONTRIBUTING.md for the full workflow.

Contributing

Issues and PRs welcome — see CONTRIBUTING.md for setup, the quality gates, and the Conventional Commits convention. Translations are managed through Weblate; you can also edit messages/<locale>.json and open a PR. Please follow the Code of Conduct. Report security issues per the Security Policy.

Reference

Scraping behavior is ported from the legacy Python tool mediux-scraper-monorepo (reference only — no Python code is reused).

Disclaimer

PosterPilot is an independent, community-built project. It is not affiliated with, endorsed by, or sponsored by Plex, Jellyfin, Emby, MediUX, Fanart.tv, TMDB, ThePosterDB, Kometa, or any other third-party service it integrates with. All product names, logos, and trademarks are the property of their respective owners and are used here for identification purposes only.

This product uses the TMDB API but is not endorsed or certified by TMDB.

License

Released under the MIT License.


Copyright (c) 2026 Diego Peixoto — MIT licensed.

Install Posterpilot on Unraid in a few clicks.

Find Posterpilot in Community Apps on your Unraid server, review the template, and click Install. Unraid handles the Docker app or plugin setup from the published template.

Open the Apps tab on your Unraid server Search Community Apps for Posterpilot Review the template variables and paths Click Install

Related apps

Details

Repository
ghcr.io/diegopeixoto/posterpilot:latest
Last Updated2026-07-01
First Seen2026-06-24

Runtime arguments

Web UI
http://[IP]:[PORT:3000]/
Network
bridge
Shell
sh
Privileged
false

Template configuration

WebUI PortPorttcp

HTTP port for the PosterPilot web UI.

Target
3000
Default
3000
Value
3000
App data (/data)Pathrw

Persistent SQLite database, settings, history and the rotating log file (/data/logs). Keep this on a mounted volume so state survives updates; no extra log volume is needed.

Target
/data
Default
/mnt/user/appdata/posterpilot
Value
/mnt/user/appdata/posterpilot
Kometa assets (/kometa)Pathrw

Optional. Mount your existing Kometa assets/config directory so exported YAML lands where Kometa reads it. Leave default if you do not use Kometa.

Target
/kometa
Default
/mnt/user/appdata/kometa/config
Value
/mnt/user/appdata/kometa/config
Kometa config dir (/kometa-config)Pathrw

Optional. To let PosterPilot manage Kometa's OWN config.yml (the dedicated /kometa manager page), mount Kometa's config directory here (read/write), then set 'Kometa config path' below to e.g. /kometa-config/config.yml. PosterPilot co-locates posterpilot.yml in this same directory, so no separate metadata mount is needed. Leave empty if unused.

Target
/kometa-config
Server typeVariable

Which media server to manage: plex, jellyfin, or emby. Default plex.

Target
SERVER_TYPE
Default
plex
Value
plex
TMDB keyVariable

TMDB v3 API key or v4 bearer/JWT (auto-detected). Required for metadata + TMDB artwork. Or set it in Settings.

Target
TMDB_KEY
Plex URLVariable

e.g. http://192.168.1.10:32400 — or use the in-app Plex login / connection discovery.

Target
PLEX_URL
Plex tokenVariable

Your X-Plex-Token. Leave blank to acquire it via the in-app Plex login.

Target
PLEX_TOKEN
Jellyfin URLVariable

Jellyfin base URL (when Server type = jellyfin).

Target
JELLYFIN_URL
Jellyfin API keyVariable

Jellyfin API key (when Server type = jellyfin).

Target
JELLYFIN_API_KEY
Emby URLVariable

Emby base URL (when Server type = emby).

Target
EMBY_URL
Emby API keyVariable

Emby API key (when Server type = emby).

Target
EMBY_API_KEY
Fanart.tv keyVariable

Fanart.tv personal API key — enables the Fanart.tv artwork provider.

Target
FANART_KEY
Kometa config pathVariable

Optional. Path INSIDE the container to Kometa's own config.yml to manage (e.g. /kometa-config/config.yml). Empty = Kometa manager off. Mount the dir via 'Kometa config dir' above.

Target
KOMETA_CONFIG_PATH
Kometa config modeVariable

merge (default, surgical — preserves your other keys and comments) or own (PosterPilot regenerates and fully owns config.yml). Empty = merge.

Target
KOMETA_CONFIG_MODE
UI languageVariable

UI locale: en (default), es, zh, ja, or pt-BR.

Target
LANGUAGE
Default
en
Value
en
Log directoryVariable

Folder for the rotating log file (posterpilot.log). Default /data/logs — already inside the /data volume, so no extra mount is needed.

Target
LOG_DIR
Default
/data/logs
Value
/data/logs
Event retentionVariable

Max number of activity-log rows kept in the database; older rows are pruned. Default 2000.

Target
EVENT_RETENTION
Default
2000
Value
2000
Port (internal)Variable

Internal listen port. Leave at 3000 unless you know what you are doing.

Target
PORT
Default
3000
Value
3000