# 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 `.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: `.png` - Primary cache: `.primary.png` - Artwork editor export cache: `.editor.jpg` - Imported remote image cache: `cache/imports/.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: ```powershell python -m py_compile app.py ``` 6. Manually verify: - search - item select - generate - apply - optional primary generation - logo/backdrop asset switching ## Recommended Future Enhancements 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 app’s 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.