Skip to content

Latest commit

Β 

History

301 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

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 and sort controls above the poster wall Item detail β€” artwork candidates grouped by provider, each with its own enlarge control

Enlarged artwork preview β€” the full-size poster, its provider, and its place in the set Artwork coverage β€” media server and Kometa metadata reported separately per slot

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, 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.

  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, split by media type (posterpilot-movies.yml and posterpilot-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.

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 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

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, 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 set KOMETA_CONFIG_PATH to the mounted file, e.g. /kometa-config/config.yml. PosterPilot co-locates the type-safe movie/show metadata files there; KOMETA_METADATA_PATH_PREFIX separately describes the relative path visible inside Kometa.

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 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.

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.

About

PosterPilot is a self-hosted artwork manager for Plex, Jellyfin, and Emby. It scans your library, finds posters/backgrounds from multiple providers, and lets you apply them directly to your media server or export them as Kometa-compatible YAML. Runs in a single Docker container and is open source (MIT).

Resources

Code of conduct

Contributing

Security policy

Stars

11 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages