Find covers across MediUX, Fanart.tv, TMDB & ThePosterDB and apply them to your media server or via Kometa/PMM β in a single Docker container.
Media servers
Artwork providers
π Multi-language Β Β·Β π₯οΈ Direct API Β Β·Β π Kometa / PMM YAML Β Β·Β π³ Docker
Spec-driven via OpenSpec. See
openspec/specs/for the capability specs andopenspec/changes/for in-flight proposals.
π Documentation: full installation, configuration, usage, contributing, and translating guides live at diegopeixoto.github.io/posterpilot.
-
Sync your Plex / Jellyfin / Emby movie & show libraries, resolving each title to a TMDB id with rich metadata (backdrop, logo, rating, genres, cast).
-
Find covers across the enabled providers (MediUX, Fanart.tv, TMDB, ThePosterDB), grouped into collapsible artwork sets per provider, in the provider order you set β 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. Every tile has an enlarge control that opens the full-size artwork without staging anything, and load more pulls the rest of a provider's inventory instead of stopping at the first page. A preferred artwork language β set globally, or switched per item β narrows TMDB results while keeping textless artwork available.
-
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_backgroundYAML into a mounted directory your existing Kometa instance consumes on its next run, split by media type (posterpilot-movies.ymlandposterpilot-shows.yml) so a movie and a show sharing an id can't collide.
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 six languages round it out. Artwork coverage reports what is actually in
place per destination and per slot β a Kometa export says exactly that and never
claims your media server is serving it β and both the library and review lists can
be filtered by it, including needs artwork. 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.
- 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
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:5173Migrations 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 |
unit + component suites |
bun run test:unit |
vitest unit tests (node) |
bun run test:component |
component tests in a real browser |
bun run test:e2e |
Playwright end-to-end tests |
bun run format / lint |
prettier write / check |
bun run fallow |
Fallow code-intelligence health report |
The same image runs anywhere. Use the official multi-arch image (amd64 + arm64) from GitHub Container Registry:
docker pull ghcr.io/diegopeixoto/posterpilot:latestThen 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:3000Or build locally instead:
docker compose up -d --buildConfiguration 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, fr |
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) |
KOMETA_METADATA_PATH_PREFIX |
Kometa-visible relative metadata prefix (default config) |
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 setKOMETA_CONFIG_PATHto the mounted file, e.g./kometa-config/config.yml. PosterPilot co-locates the type-safe movie/show metadata files there;KOMETA_METADATA_PATH_PREFIXseparately describes the relative path visible inside Kometa.
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:/kometaSet 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.
PosterPilot writes two type-safe metadata files: posterpilot-movies.yml uses
TMDB movie IDs with IMDb fallback, while posterpilot-shows.yml uses TVDB IDs
with IMDb fallback.
Both contain url_poster / url_background entries and are written to
KOMETA_ASSETS_DIR (or beside managed config.yml). Reference only the matching
file from each Kometa library. 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. Each
managed library gets exactly one authoritative reference:
posterpilot-movies.yml for movies or posterpilot-shows.yml for shows. Both are
co-located with config.yml. Set KOMETA_CONFIG_PATH (and mount Kometa's
config dir) to enable it; KOMETA_METADATA_PATH_PREFIX separately describes the
relative path Kometa sees at runtime (config by default, . for bare names).
The manager 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.
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/healthThe 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.
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.
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.
Scraping behavior is ported from the legacy Python tool mediux-scraper-monorepo
(reference only β no Python code is reused).
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.
Released under the MIT License.
Copyright (c) 2026 Diego Peixoto β MIT licensed.



