Initial commit

This commit is contained in:
ponzischeme89
2026-04-15 09:27:29 +12:00
commit e0b51397e7
1151 changed files with 4516 additions and 0 deletions
+395
View File
@@ -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 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.
+20
View File
@@ -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"]
+83
View File
@@ -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
Binary file not shown.
Binary file not shown.
+1581
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 686 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 596 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 663 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 497 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 486 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 996 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 596 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 537 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 430 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 481 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 619 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 469 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 568 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 425 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 430 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 793 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 809 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 569 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 546 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 955 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 969 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 465 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 482 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 506 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 549 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 552 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 521 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 648 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 969 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 682 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 KiB

Some files were not shown because too many files have changed in this diff Show More