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

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

