Initial commit
@@ -0,0 +1,395 @@
|
||||
# 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
|
||||
- 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
|
||||
|
||||
## 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`
|
||||
|
||||
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.
|
||||
|
||||
## 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:
|
||||
|
||||
- Separate primary preview in the UI
|
||||
- Remote/provider image search 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.
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
fonts-dejavu-core \
|
||||
fonts-liberation \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Pre-download the rembg u2net model
|
||||
RUN python -c "from rembg import remove; remove(b'\x89PNG\r\n')" || true
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 8500
|
||||
|
||||
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8500"]
|
||||
@@ -0,0 +1,83 @@
|
||||
# Emby Thumbnail Generator
|
||||
|
||||
A self-hosted web UI that generates landscape thumbnails from your Emby library posters. Uses AI-powered subject extraction (rembg/U2-Net, runs entirely locally) to isolate characters from poster art, then composites them into widescreen thumbnails with customisable layouts.
|
||||
|
||||
## What it does
|
||||
|
||||
1. Connects to your Emby server via API
|
||||
2. Search/browse your movie and TV library
|
||||
3. Pulls the poster for a selected item
|
||||
4. Extracts the subject (person/character) using rembg (offline, no external API)
|
||||
5. Generates a landscape thumbnail with the subject positioned to one side and the title on the other
|
||||
6. Optionally pushes the generated thumbnail back to Emby as a custom Thumb image
|
||||
|
||||
## Templates
|
||||
|
||||
- **Subject Left, Text Right** — character on the left, title text on the right
|
||||
- **Subject Right, Text Left** — character on the right, title text on the left
|
||||
- **Subject Center, Text Overlay** — character centered with title overlaid
|
||||
|
||||
## Background Modes
|
||||
|
||||
- **Auto Gradient** — samples dominant colours from the poster and creates a dark gradient
|
||||
- **Blurred Poster** — darkened, heavily blurred version of the original poster
|
||||
- **Solid Colour** — pick your own background colour
|
||||
|
||||
## Setup
|
||||
|
||||
### Docker (recommended)
|
||||
|
||||
1. Edit `docker-compose.yml` with your Emby details:
|
||||
```yaml
|
||||
environment:
|
||||
- EMBY_URL=http://192.168.1.x:8096
|
||||
- EMBY_API_KEY=your-api-key
|
||||
```
|
||||
|
||||
2. Build and run:
|
||||
```bash
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
3. Open `http://localhost:8500`
|
||||
|
||||
### Manual
|
||||
|
||||
1. Install dependencies:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
2. Set environment variables:
|
||||
```bash
|
||||
export EMBY_URL=http://192.168.1.x:8096
|
||||
export EMBY_API_KEY=your-api-key
|
||||
```
|
||||
|
||||
3. Run:
|
||||
```bash
|
||||
python app.py
|
||||
```
|
||||
|
||||
4. Open `http://localhost:8500`
|
||||
|
||||
## Getting your Emby API Key
|
||||
|
||||
1. Open Emby Dashboard → Advanced → API Keys
|
||||
2. Click "New API Key"
|
||||
3. Give it a name (e.g. "Thumb Generator")
|
||||
4. Copy the key
|
||||
|
||||
## Notes
|
||||
|
||||
- First generation will be slower as rembg downloads the U2-Net model (~170MB)
|
||||
- The model runs entirely offline after first download — no data leaves your network
|
||||
- Generated thumbnails are cached in the `cache/` directory
|
||||
- Works with both movies and TV series
|
||||
- The "Apply to Emby" button sets the generated image as the item's Thumb image type
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Backend:** Python, FastAPI, Pillow, rembg (U2-Net)
|
||||
- **Frontend:** Vanilla HTML/CSS/JS
|
||||
- **Deployment:** Docker
|
||||
|
After Width: | Height: | Size: 686 KiB |
|
After Width: | Height: | Size: 376 KiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 596 KiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 663 KiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 497 KiB |
|
After Width: | Height: | Size: 486 KiB |
|
After Width: | Height: | Size: 996 KiB |
|
After Width: | Height: | Size: 475 KiB |
|
After Width: | Height: | Size: 596 KiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 597 KiB |
|
After Width: | Height: | Size: 537 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 430 KiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 496 KiB |
|
After Width: | Height: | Size: 481 KiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 619 KiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 469 KiB |
|
After Width: | Height: | Size: 568 KiB |
|
After Width: | Height: | Size: 386 KiB |
|
After Width: | Height: | Size: 425 KiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 604 KiB |
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 374 KiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 456 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 430 KiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 283 KiB |
|
After Width: | Height: | Size: 793 KiB |
|
After Width: | Height: | Size: 403 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 377 KiB |
|
After Width: | Height: | Size: 303 KiB |
|
After Width: | Height: | Size: 522 KiB |
|
After Width: | Height: | Size: 564 KiB |
|
After Width: | Height: | Size: 327 KiB |
|
After Width: | Height: | Size: 809 KiB |
|
After Width: | Height: | Size: 569 KiB |
|
After Width: | Height: | Size: 546 KiB |
|
After Width: | Height: | Size: 501 KiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 415 KiB |
|
After Width: | Height: | Size: 955 KiB |
|
After Width: | Height: | Size: 422 KiB |
|
After Width: | Height: | Size: 172 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 388 KiB |
|
After Width: | Height: | Size: 600 KiB |
|
After Width: | Height: | Size: 299 KiB |
|
After Width: | Height: | Size: 496 KiB |
|
After Width: | Height: | Size: 422 KiB |
|
After Width: | Height: | Size: 969 KiB |
|
After Width: | Height: | Size: 465 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 482 KiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 456 KiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 391 KiB |
|
After Width: | Height: | Size: 471 KiB |
|
After Width: | Height: | Size: 385 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 506 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 452 KiB |
|
After Width: | Height: | Size: 549 KiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 446 KiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 505 KiB |
|
After Width: | Height: | Size: 552 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 551 KiB |
|
After Width: | Height: | Size: 521 KiB |
|
After Width: | Height: | Size: 258 KiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 421 KiB |
|
After Width: | Height: | Size: 377 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 648 KiB |
|
After Width: | Height: | Size: 422 KiB |
|
After Width: | Height: | Size: 969 KiB |
|
After Width: | Height: | Size: 682 KiB |
|
After Width: | Height: | Size: 2.4 MiB |
|
After Width: | Height: | Size: 388 KiB |