Files
2026-04-20 23:16:53 +12:00

12 KiB
Raw Permalink Blame History

Emby Cover App Maintenance Guide

Purpose

This app is a FastAPI-based internal tool for generating and applying custom Emby artwork.

It currently supports:

  • Searching Emby movies and series
  • Generating a wide Thumb image
  • Optionally generating a matching tall Primary poster
  • Using Emby-known logos and backdrops already attached to the item
  • Reframing any Emby or imported image in a premium artwork editor
  • Exporting editor output as Primary, Thumb, or Backdrop
  • Searching provider-backed external image sources and importing remote images into cache
  • Applying generated artwork back to Emby
  • Adding optional studio badges and a red NEW EPISODES series tag

The codebase is intentionally small and centralized:

  • Backend and image pipeline: app.py
  • Frontend UI, CSS, and browser logic: templates/index.html
  • Static studio logos: static/studios/
  • Generated cache files: cache/

Runtime Model

The app is mostly a single-server process with one HTML page.

  • FastAPI serves the UI and JSON/image endpoints
  • httpx.AsyncClient is reused for Emby API calls
  • Pillow is used for all image composition
  • The browser page is a single template with inline CSS and JS

There is no frontend build system and no database.

Configuration

Environment variables:

  • EMBY_URL
  • EMBY_API_KEY

Defaults are currently hard-coded in app.py. That is convenient for local usage but should be treated carefully if this app is ever shared more broadly.

Key Files

app.py

This file contains:

  • App startup/shutdown
  • Emby API helpers
  • Image loading and upload helpers
  • Font selection helpers
  • The entire artwork render pipeline
  • Search/image/apply API routes

This is the core of the system. Most behavior changes happen here.

templates/index.html

This file contains:

  • Full UI markup
  • Full CSS
  • Full browser-side state management
  • Search/pagination logic
  • Asset picker logic for Emby-known logos and backdrops
  • Generate/apply button behavior

There is no JS framework. Everything is plain DOM code.

How The App Works

1. Search flow

Browser:

  • User types into the search box
  • JS debounces and cancels stale requests
  • GET /api/search is called with q, start, and limit
  • Results are rendered in the left sidebar with lightweight poster images

Server:

  • /api/search queries Emby /Items
  • Returns paginated search results with metadata needed by the UI
  • Search result posters are served via /api/poster/{item_id} using reduced dimensions for speed

2. Item image discovery flow

When an item is selected:

  • The browser immediately starts a preview generate call
  • In parallel, it calls GET /api/images/{item_id}

That endpoint returns the Emby-known images already attached to the item:

  • logos
  • backdrops
  • primaries

The UI uses that to populate:

  • Logo asset picker
  • Backdrop asset picker
  • Accurate backdrop counts

Important:

  • This is Phase 1 of image browsing only
  • It only exposes images Emby already knows for the item
  • It does not yet perform remote/provider image lookup

3. Generate flow

Browser:

  • POST /api/generate
  • Sends the selected options:
    • background mode
    • selected backdrop index
    • selected logo index
    • logo position
    • logo size
    • darkness
    • studio badge
    • series tag
    • whether to also generate a matching primary poster

Server:

  • Pulls Primary, selected Logo, and selected Backdrop from Emby
  • Builds the wide thumb with generate_thumbnail(...)
  • Writes the thumb to cache
  • Optionally builds and caches a tall primary via generate_primary_cover(...)
  • Returns the thumb image stream to the browser

4. Apply flow

Browser:

  • POST /api/apply
  • Sends the same effective rendering settings as generate

Server:

  • Rebuilds the same cache key
  • Reads the cached thumb if present
  • If missing, regenerates on demand to avoid cache-miss 400 failures
  • Uploads the thumb to Emby as Thumb
  • If generate_primary is enabled, uploads the cached or regenerated tall poster as Primary

Important:

  • Apply must remain tolerant of cache misses
  • Do not reintroduce a hard dependency on a pre-existing cache file only

5. Artwork editor flow

Browser:

  • The editor appears after an Emby item is selected
  • Editor state is kept separate from the existing thumb-generator state object
  • The editor has three panels:
    • source image panel
    • fixed-ratio editor canvas
    • export panel
  • Users can choose Emby Primary, selected Emby Backdrop, Emby Thumb, or an imported remote image
  • Users can target poster, thumb, or backdrop
  • Users can drag, zoom, reset, and switch cover / contain
  • Landscape-to-portrait conversion supports blurred, mirrored, or solid background fill
  • POST /api/artwork/export generates a live preview and returns cache headers
  • POST /api/artwork/apply uploads the cached or regenerated export to the matching Emby image type

Server:

  • Editor rendering is separate from generate_thumbnail(...)
  • render_artwork_editor_image(...) handles fixed-frame reframing and high-quality JPEG output
  • fit_image_positioned(...) is the shared crop/contain positioning helper
  • build_editor_fill(...) handles blurred, mirrored, and solid background fill
  • resolve_editor_source_bytes(...) loads either Emby source bytes or cached remote imports
  • Editor exports are cached as <cache_key>.editor.jpg
  • Remote imports are cached under cache/imports/

Remote image search:

  • Provider lookup is intentionally abstracted behind ImageSearchProvider
  • GET /api/artwork/providers exposes available providers to the UI
  • GET /api/artwork/search calls the selected provider
  • POST /api/artwork/import downloads, validates, size-limits, and caches a selected remote image
  • Current providers are Wikimedia Commons, optional TMDB, and optional Google Custom Search
  • TMDB is enabled with TMDB_BEARER_TOKEN or TMDB_API_KEY
  • Google Custom Search is enabled with GOOGLE_CUSTOM_SEARCH_API_KEY and GOOGLE_CUSTOM_SEARCH_ENGINE_ID
  • Add future providers by implementing ImageSearchProvider and registering it in IMAGE_SEARCH_PROVIDERS

Rendering Pipeline

The central renderer is generate_thumbnail(...).

It is used for:

  • Wide thumbs
  • Tall primary covers, through generate_primary_cover(...)

Pipeline stages:

  1. Resolve background
  2. Crop/fit to target aspect ratio
  3. Apply darkening and vignette
  4. Resolve logo or fallback title text
  5. Position logo/text based on selected alignment
  6. Add studio logo overlay
  7. Add optional NEW EPISODES tag
  8. Encode as PNG

Layout assumptions

The renderer now contains separate behavior for tall layouts.

This matters because:

  • Wide thumbnails and tall posters cannot share the same padding rules
  • Tall primary posters need different bottom spacing and logo height caps

When changing artwork layout, always test both:

  • 800x450 thumb
  • 1000x1500 primary

Caching

Cache files live in cache/.

Current cache helpers:

  • Thumb cache: <cache_key>.png
  • Primary cache: <cache_key>.primary.png
  • Artwork editor export cache: <cache_key>.editor.jpg
  • Imported remote image cache: cache/imports/<sha256>.img

The cache key is derived from rendering inputs such as:

  • item id
  • backdrop index
  • logo index
  • colors
  • logo position
  • scale
  • darkness
  • studio settings
  • series tag state

Important:

  • If you add any new visual option, add it to build_cache_key(...)
  • If you forget, the app will reuse stale cached artwork from a different configuration

Image Selection Rules

Logo images

Logo selection is index-based.

Notes:

  • The app should always treat invalid or non-image logo responses as optional
  • emby_get_image_optional(...) already guards against non-image content-types
  • generate_thumbnail(...) also guards against invalid logo bytes and falls back to text

Do not make logo loading a hard-fail path.

Backdrops

Backdrop selection is index-based and uses the asset picker.

If a backdrop is unavailable:

  • The render should still succeed
  • The fallback is blurred poster or solid background

Upload Rules

Uploading back to Emby is done through emby_upload_image(...).

Current behavior:

  • Upload source image is normalized to RGB JPEG
  • Thumb and Primary uploads both use that path

Be careful when changing upload behavior:

  • Emby may accept source formats differently than display formats
  • The preview proxy can preserve PNG content-type
  • The upload path currently standardizes to JPEG intentionally

Frontend State

The browser keeps a single state object in templates/index.html.

Key fields:

  • item
  • backdropIndex
  • logoIndex
  • imageInfo
  • searchQuery
  • searchStart
  • searchLimit
  • searchTotal
  • generated

When adding a new visual option:

  1. Add UI control
  2. Add local JS state if needed
  3. Include it in POST /api/generate
  4. Include it in POST /api/apply
  5. Add it to build_cache_key(...)
  6. Apply it in the render pipeline

If any of those steps are skipped, behavior will drift.

Artwork editor state is intentionally separate:

  • editorState.sourceKind
  • editorState.sourceType
  • editorState.sourceIndex
  • editorState.importId
  • editorState.cacheKey
  • transform controls from the editor sliders

Keep editor payloads aligned across:

  • /api/artwork/export
  • /api/artwork/apply
  • build_editor_cache_key(...)
  • render_artwork_editor_image(...)

Laptop UX Constraints

The app has custom responsive rules for laptop-sized screens.

Areas that have already needed tuning:

  • Preview area height
  • Controls bar spacing
  • Sidebar width
  • Results panel layout

When changing layout:

  • Test desktop wide view
  • Test laptop-height view
  • Test narrow desktop widths before mobile breakpoints

The controls container is particularly sensitive because the preview and controls compete vertically.

Known Fragile Areas

1. Cache key drift

Symptoms:

  • Apply uploads unexpected artwork
  • Generate/apply feel out of sync

Cause:

  • A new option was added to rendering but not to build_cache_key(...)

2. Emby returns non-image content for optional assets

Symptoms:

  • PIL UnidentifiedImageError

Cause:

  • Emby returned HTML, JSON, or another unexpected response for an optional asset

Current guardrails already exist. Preserve them.

3. Tall poster layout quality

Symptoms:

  • Tall primary poster logo sits too low
  • Title/logo scale looks like a stretched thumb layout

Cause:

  • Using wide-layout spacing assumptions on primary posters

Always treat primary layout as distinct.

4. Search feels slow

Symptoms:

  • Listing poster thumbnails load slowly
  • Search UI feels laggy

Current mitigations:

  • Paginated search
  • Smaller poster fetches
  • Shared HTTP client
  • Stale request cancellation

Avoid switching result posters back to full-size originals.

Safe Maintenance Workflow

When changing this app:

  1. Read both app.py and the relevant section of templates/index.html
  2. Keep generate/apply payloads aligned
  3. Keep cache key aligned with render options
  4. Test thumb and primary rendering
  5. Run:
python -m py_compile app.py
  1. Manually verify:
    • search
    • item select
    • generate
    • apply
    • optional primary generation
    • logo/backdrop asset switching

Likely next improvements:

  • Additional image search providers from Emby metadata sources
  • Better poster-specific typography/layout rules
  • Better user-visible error reporting for partial apply failures
  • Cache cleanup strategy

Rules For Future Agents

  • Do not split backend logic into multiple files unless there is a strong reason. This app is currently maintainable specifically because it is centralized.
  • Do not introduce a frontend framework casually. The current plain JS approach is appropriate for the apps size.
  • Do not remove the cache-miss regeneration fallback from apply.
  • Do not assume Emby optional image endpoints always return valid images.
  • Do not change wide-thumb behavior without checking tall-primary behavior too.
  • If adding a new render option, update both generate and apply paths and the cache key in the same change.