Guides for the web interface, automation and webhooks, HDR handling, and troubleshooting.
Important
This page is the source of truth for web operations, webhook workflows, and troubleshooting. For installation and first-time setup, use Getting Started. For exact configuration values and API contracts, use Configuration & API Reference.
- Web Interface
- Previews readiness (per-check toggles & explanations)
- Webhook Integration
- Auto-trigger from Plex (no Sonarr/Radarr)
- HDR & Dolby Vision
- Troubleshooting
- FAQ
Dashboard for managing preview generation jobs, settings, and schedules.
When you first access the web interface, you'll be guided through a Setup Wizard that supports Plex, Emby, and Jellyfin:
- Choose your media server — pick Plex, Emby, or Jellyfin. The chosen card expands inline:
- Plex — sign in via Plex OAuth (no manual token copying), or paste a URL + token if you prefer.
- Emby — enter the server URL and an API key.
- Jellyfin — enter the URL and run a Quick Connect ceremony (or paste an API key).
- Server & Libraries (Plex only) — pick which Plex server (if you have several) and which libraries to enable. Emby/Jellyfin flows skip this step; libraries are managed later from Settings → Media Servers.
- Path Configuration (Plex only) — confirm the Plex application data folder where BIF files are written, plus any media path mappings. Emby/Jellyfin push their output via HTTP, so this step is skipped for those flows.
- Processing Options — per-GPU enable/workers/FFmpeg threads, CPU workers, thumbnail interval, and quality.
- Security — view or replace your access token (optional).
After setup completes, you'll land on the dashboard. You can add additional servers (any vendor, any number) at any time from Settings → Media Servers without re-running the wizard.
- Start the container
- Open
http://YOUR_SERVER_IP:8080 - Get your authentication token using Authentication Token
- Enter the token to log in
Connection Status — shows every configured server (Plex, Emby, Jellyfin):
- Connected — server name, vendor, and available GPUs displayed
- Not configured — link to the setup wizard or Servers page to add one
Job Management:
- Start new jobs — process all libraries or specific ones
- View progress — real-time progress with WebSocket updates
- Cancel jobs — stop running jobs
- Job history — view completed/failed jobs
Note
Jobs queue with priority (1 = high, 2 = normal, 3 = low). The dispatcher runs up to the configured concurrent-job cap; extra jobs sit in Pending and the gate releases them in priority order as slots free up. Manual, webhook, and scheduled jobs all share the same gate. To hard-stop everything, use Pause Processing — the global pause is persisted and survives restarts.
Manual Generation:
The Manual Trigger button generates previews for specific media on demand — no Sonarr/Radarr webhook or library scan needed. There are three ways to pick what to process, and they can be mixed:
- Search — start typing a show, movie, or episode name. The app searches your enabled servers and lists matches grouped by Shows / Movies / Episodes, each tagged with a badge showing which server(s) it came from. Pick a show to generate previews for every episode in it; pick a movie or episode for just that file. The path comes straight from the server, so you never have to know the in-container path (the common cause of "missing on disk" confusion).
- Browse — open the folder picker to navigate your mounted media and select either a folder (expanded to every video inside) or an individual video file.
- Or paste paths manually — the collapsible box still accepts one absolute container path per line, for power users or scripts.
Each pick becomes a removable chip; Start Job processes them all. The Publish to which server? dropdown scopes both the search and where previews are published — leave it on All servers to publish to whoever owns each file, or pick one server to limit both.
Pause / Resume (global):
- Pause Processing — Stops all processing system-wide: no new jobs will start (manual, scheduled, or webhook), and the current job stops dispatching new tasks. Files already mid-process finish first, then workers go idle (a "soft" pause — nothing is killed mid-frame). Use this to cap bandwidth or pause overnight.
- Resume Processing — Clears the global pause; new jobs can start and the current job resumes dispatching.
- Controls appear in the Current Job header and to the left of Clear Jobs in the Job Queue. State is persisted and survives restarts.
Scheduling:
The Dashboard shows a compact "Schedules" teaser with the next upcoming run and a total count. Full schedule management lives on the Automation page, under the Schedules tab (/automation#schedules, also linked from the top nav):
- Cron schedules — set up recurring processing
- Interval-based — run every X minutes
- Per-library — schedule specific libraries
- Scan mode — each schedule is either a Full library scan (default) or a Recently added only scan (see Auto-trigger from Plex)
Legacy URL note:
/schedulesand/webhooksstill work — they 302-redirect to/automation#schedulesand/automation#webhooksrespectively, so existing bookmarks and shared links keep working.
Access settings at /settings to manage:
- Plex Connection — re-authenticate, test connection
- Libraries — select which libraries to process
- Path Mappings — media path, Plex videos path, local videos path
- Processing Options — per-GPU settings (enable/disable, workers, FFmpeg threads), CPU threads, thumbnail interval and quality
For per-server settings audits (Plex FSEvent flags, Jellyfin trickplay flags, Media Preview Bridge plugin presence, Plex config folder writability, path mappings), open Servers → Edit → Setup Health. Full per-check reference: Previews Readiness guide.
The Settings page and the Automation page's Triggers tab save automatically as you edit — there's no Save button. Toggles, sliders, and dropdowns commit immediately; text fields commit on blur (or ~1 s after you stop typing). A small status indicator in the page header shows Saving… / Saved at HH:MM so you can tell the change landed. If a save fails (e.g. the backend is down), the indicator shows an error and you can click it to retry.
Every GPU worker includes automatic CPU fallback — no extra configuration is needed. If FFmpeg fails on the GPU for any of the common reasons:
- Unsupported codec on the HW decoder
- Hardware-accelerator runtime error (CUDA sync/transfer failure, VAAPI surface exhaustion)
- Driver crash or FFmpeg signal kill (segfault, OOM)
…the same worker retries the file on CPU in-place. The job log records the specific reason ("Dolby Vision Profile 5 rejected by Intel VAAPI", "signal kill (signal 11)", etc.) and the dashboard shows a yellow "CPU fallback" badge on the affected worker card along with a toast.
The worker is busy on CPU while the retry runs. If you have a lot of content that never decodes on the GPU, set CPU Workers > 0 so that content routes directly to dedicated CPU workers from the main queue instead of blocking a GPU worker each time.
A full scan first sweeps every file to check whether a fresh preview already exists, then only generates the missing ones. Those two jobs have independent concurrency:
- GPU Workers / CPU Workers cap how many previews generate at once (heavy FFmpeg/GPU work) — keep these matched to your hardware.
- Library Scanning → Files checked at once (
scan_workers) caps how many files the existence check sweeps in parallel. This is light disk I/O and adds no FFmpeg/GPU load.
On a large, mostly-complete library the sweep can dominate. Leave Files checked at once on Auto for most setups; raise it if the "checking" phase feels slow on fast storage, or lower it if a single spinning HDD thrashes under many parallel stats. Raising it never spawns more simultaneous FFmpeg processes — that's still governed by your worker counts.
Settings are saved to /config/settings.json and persist across restarts.
The Automation page (/automation) hosts two tabs:
- Triggers — incoming webhooks from Radarr, Sonarr, Tdarr / custom scripts, and Plex Direct. Also houses the Recently Added Scanner shortcut. This is where you wire the app up to whatever puts media into Plex.
- Schedules — full CRUD for recurring scans (cron / interval / specific time). Both Full library and Recently-Added scanners live here.
The Triggers tab includes:
- Enable/Disable — master toggle for webhook processing
- Webhook URLs — copy-ready URLs for Radarr, Sonarr, and the generic Custom webhook
- Delay — seconds to wait after import (gives Plex time to index)
- Webhook Secret — optional dedicated authentication token
- Setup instructions — step-by-step guides for each source
- Activity Log — recent webhook events with status badges
The legacy /webhooks and /schedules URLs still work — they 302-redirect to the Triggers and Schedules tabs on the new page.
The Docker image runs the web interface for you — there's nothing to configure. The dashboard updates in real time over WebSocket; long-running jobs survive the default proxy timeouts. If you're running the app outside Docker (or just curious how the container is wired internally — gunicorn settings, single-worker rationale, WebSocket transport), see CONTRIBUTING.md → Architecture.
If you want to expose the web UI outside your local network — for example with HTTPS, a custom domain, or alongside other services — you can place it behind a reverse proxy such as Nginx, Apache, or Traefik.
The built-in server listens on port 8080 (HTTP) and the reverse proxy
forwards external requests to it. The web UI uses WebSocket (Socket.IO)
for real-time updates, so your reverse proxy must forward WebSocket
upgrade requests.
location / {
proxy_pass http://localhost:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}Enable the required modules first:
sudo a2enmod proxy proxy_http proxy_wstunnel rewrite headers
sudo systemctl restart apache2Example HTTPS virtual host:
<VirtualHost *:443>
ServerName previews.example.com
SSLEngine On
SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
SSLProtocol +TLSv1.2
RequestHeader set X-Forwarded-Proto https
RequestHeader set X-Forwarded-Ssl on
RewriteEngine On
RewriteCond %{HTTP:Upgrade} =websocket [NC]
RewriteRule /(.*) ws://127.0.0.1:8080/$1 [P,L]
ProxyPass / http://127.0.0.1:8080/
ProxyPassReverse / http://127.0.0.1:8080/
ProxyRequests Off
ProxyPreserveHost On
Header edit Location ^http://(.*)$ https://$1
</VirtualHost>Traefik v2+ forwards WebSocket upgrade headers automatically. No extra configuration is required beyond a standard HTTP router and service.
The web interface uses token-based authentication:
- Auto-generated token — created on first run, saved to
/config/auth.json - Custom token via wizard — set your own token during the setup wizard (Step 5)
- Fixed token — set
WEB_AUTH_TOKENenvironment variable (overrides wizard setting) - Token masking — tokens are always masked in logs (only last 4 chars shown)
API authentication:
# Bearer token
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:8080/api/jobs
# X-Auth-Token header
curl -H "X-Auth-Token: YOUR_TOKEN" http://localhost:8080/api/jobsLogin and API endpoints are rate-limited to protect against brute force. See Reference — Rate Limiting for the exact limits and the RATELIMIT_STORAGE_URL env var for multi-worker deployments.
The dashboard streams live job progress over WebSocket (Flask-SocketIO, /jobs namespace). See Reference — WebSocket Events for the event table and payloads.
Automatically generate preview thumbnails when Radarr or Sonarr imports new media, or when any external tool (Tdarr, scripts, etc.) modifies a file. Webhooks trigger processing of only the imported file(s) after a configurable delay, giving Plex time to detect and index the new files.
- Radarr/Sonarr imports a file (or an external tool sends a custom webhook) and a POST is sent to this app.
- The app queues the file and starts (or resets) a timer. Imports from the same source (Radarr, Sonarr, or Custom) are batched together.
- A batch is processed only after the delay (e.g. 60s) has passed with no new imports from that source. So if another file arrives 1 second before the batch would run, it is added to the queue and the timer resets — the batch runs 60 seconds after that file. Every file gets at least 60 seconds before we process it.
- This delay is important because your media servers need time to add the new file to their library. If we process too soon, the file may not be indexed yet (regardless of vendor) and the job can fail or skip the item. Not-yet-indexed files are automatically re-queued on a 5-step backoff (30 s → 2 m → 5 m → 15 m → 60 m), so transient indexing lag doesn't drop work.
- When the timer fires, the app resolves each queued path against every configured server that owns it, processes it once, and publishes to each in its native format — Plex BIF bundle, Emby sidecar BIF, Jellyfin trickplay tiles. Items that already have a fresh preview are skipped automatically (source-aware dedup).
- Media Preview Generator running with the web UI accessible
- Radarr and/or Sonarr installed and managing your media (for Radarr/Sonarr webhooks)
- At least one media server configured in Servers (Plex, Emby, or Jellyfin — any combination). The app publishes webhook-triggered work to every server that owns the file.
- Open the web UI and navigate to Automation → Triggers tab (in the top nav)
- Copy the Radarr Webhook URL
- In Radarr, go to Settings → Connect → + → Webhook
- Set Name:
Plex Previews - Set URL: paste the Radarr Webhook URL
- Under Events, enable:
- On Import
- On Upgrade
- Authentication (use one):
- Username/Password (works in all versions): Leave Username empty and set Password to your API token (see Authentication Token) or webhook secret. The app treats the password as the token.
- Custom headers (if your webhook form has a Headers section): Add Key =
X-Auth-Token, Value = your API token or webhook secret.
- Click Test to verify the connection
- Click Save
- Copy the Sonarr Webhook URL from the web UI Automation → Triggers tab
- In Sonarr, go to Settings → Connect → + → Webhook
- Set Name:
Plex Previews - Set URL: paste the Sonarr Webhook URL
- Under Events, enable On File Import and On File Upgrade
- Authentication (use one):
- Username/Password (works in all versions): Leave Username empty and set Password to your API token or webhook secret. The app treats the password as the token.
- Custom headers (if your webhook form has a Headers section): Add Key =
X-Auth-Token, Value = your API token or webhook secret.
- Click Test then Save
The custom webhook endpoint lets any tool trigger preview generation by POSTing a file path. This is useful when an external tool (like Tdarr) modifies a media file after Sonarr/Radarr has already imported it — Plex detects the change and removes the old thumbnails, but Sonarr/Radarr won't send a new webhook since no import occurred.
Endpoint: POST /api/webhooks/custom
Expected payload — single file:
{
"file_path": "/media/movies/Movie (2024)/Movie.mkv"
}Expected payload — multiple files:
{
"file_paths": [
"/media/tv/Show/Season 01/S01E01.mkv",
"/media/tv/Show/Season 01/S01E02.mkv"
],
"title": "Optional display label"
}Test connectivity (no processing):
{
"eventType": "Test"
}| Field | Type | Required | Description |
|---|---|---|---|
file_path |
string | One of file_path or file_paths required |
Single absolute file path to process |
file_paths |
array of strings | One of file_path or file_paths required |
Multiple absolute file paths to process |
title |
string | No | Display label shown in history/jobs (defaults to first file's basename) |
eventType |
string | No | Set to "Test" to verify the connection without triggering processing |
Authentication is the same as Radarr/Sonarr: use X-Auth-Token header, Authorization: Bearer, or Basic auth (password = token).
Tdarr doesn't have built-in webhook support like Sonarr/Radarr. Instead, use the Send Web Request Flow plugin to POST to the custom endpoint after each transcode.
- Open the web UI and navigate to Automation → Triggers tab — copy the Custom Webhook URL
- In Tdarr, open the Flow you want to trigger previews from
- Add a Send Web Request plugin after your transcode step
- Configure the plugin:
- Method:
POST - Request URL: paste the Custom Webhook URL (e.g.
http://your-server:8080/api/webhooks/custom) - Request Headers:
{"Content-Type": "application/json", "X-Auth-Token": "YOUR_TOKEN"} - Request Body:
{"file_path": "{{{args.inputFileObj._id}}}"}
- Method:
- Save the Flow
The {{{args.inputFileObj._id}}} template variable is replaced by Tdarr at runtime with the full path of the transcoded file.
Tip
If the webhook request fails (e.g. the server is temporarily down), add a Reset Flow Error plugin after the Send Web Request step so Tdarr doesn't mark the entire transcode as failed.
FileFlows uses two nodes at the end of your flow: a Set Variable node to construct the final output path, then a Web Request node to POST it to the custom endpoint.
- In FileFlows, install the Web plugin (Plugins page → search "Web") so the Web Request node becomes available
- Open the Flow you want to trigger previews from
- Add a Set Variable node just before where the Web Request will go. FileFlows doesn't expose a built-in "final output path" variable, so build it from the folder + final filename + extension:
- Variable:
output_file - Value:
{folder.Orig.FullName}/{file.NameNoExtension}{ext}
- Variable:
- Add a Web Request node after the Set Variable node:
- Method:
POST - URL: paste the Custom Webhook URL (e.g.
http://your-server:8080/api/webhooks/custom) - Content Type:
JSON - Headers: key
X-Auth-Token, valueYOUR_TOKEN - Body:
{"file_path": "{output_file}"}
- Method:
- Save the flow
The {output_file} placeholder is substituted by FileFlows at runtime with the absolute path of the file as it exists at the end of the flow (after any rename / re-extension steps).
curl -X POST "http://your-server:8080/api/webhooks/custom" \
-H "Content-Type: application/json" \
-H "X-Auth-Token: YOUR_TOKEN" \
-d '{"file_path": "/media/movies/Movie (2024)/Movie.mkv"}'All settings are configurable from the Automation page → Triggers tab in the web UI.
| Setting | Default | Description |
|---|---|---|
| Enable Webhooks | On | Master toggle |
| Delay before processing | 60s | How long to wait with no new imports before running a batch (10–300 s). Incoming files are queued; a batch runs only after this many seconds of “quiet” from that source. Each new import resets the timer so every file gets at least this long for Plex to add it to the library before we process. |
| Webhook Secret | (empty) | Dedicated authentication token for webhooks |
Webhook processing uses your Settings library selection. If a webhook path belongs to an unchecked library, it is skipped.
By default, webhooks authenticate using your main API token. You can optionally configure a dedicated webhook secret for better security isolation:
- On the Automation page (Triggers tab), click Generate next to the secret field
- Click Save Changes
- Use the generated secret as the token: in Radarr/Sonarr, either put it in Password (leave Username empty) or in the X-Auth-Token header if your form has a Headers section.
When multiple files are imported in quick succession (e.g., a season pack), the app queues them per source (Radarr, Sonarr, or Custom). Each new import resets the delay timer for that source. A batch runs only when the timer finally fires — i.e. when that many seconds have passed with no new imports. So every file in the batch has had at least that long for Plex to add it to the library before we process.
Example: Sonarr imports 10 episodes over 30 seconds with a 60s delay. The timer keeps resetting as each episode arrives. One job runs 60 seconds after the last episode and processes all 10 files. A file that arrived at 59 seconds is not processed in an earlier batch — it goes in this batch, and the batch runs 60 seconds after it, so Plex has time to index it.
Viewing files in a batch: On the Dashboard, jobs from webhooks show a label like "Sonarr: 3 files". Click the + (chevron) next to the label to expand and see the list of files. On the Automation page (Triggers tab), Activity Log rows for triggered batches include a chevron; click it to expand and see the files in that batch.
For media you add to Plex manually — copying files into a watched folder, importing through Plex itself, or using any tool other than Sonarr/Radarr/Tdarr — there are two built-in ways to auto-trigger preview generation. Both live on the Automation page's Triggers tab as dedicated sections (Plex Direct and Recently Added Scanner in the sidebar), and both feed into the same job pipeline as the existing webhooks.
Important
Both options trigger only on new library items. When Sonarr or Radarr upgrades an existing file in place, Plex keeps the same library item, so neither option will see it. Use the existing Sonarr/Radarr webhooks (which fire on On Upgrade) for that case.
This uses Plex's built-in webhook feature. The app calls Plex's account API to register its own /api/webhooks/plex endpoint, so you don't have to copy/paste anything into Plex Web → Settings → Webhooks (though you still can if you'd rather).
Requirements:
- An active Plex Pass subscription on the server-owner account. Plex's webhook feature is Plex-Pass-only.
- Mobile Push Notifications enabled on your Plex server. This is the catch: Plex's
library.newevent is delivered through the same code path as mobile push notifications, and if push notifications are off, library events are silently dropped. Enable them under Plex Web → Settings → General (toggle Enable mobile push notifications). You don't have to actually use mobile push — they just need to be turned on.
Setup:
- Open the web UI → Automation → Triggers tab and scroll to (or click) the Plex Direct sidebar link.
- The URL field is pre-filled with the URL you're currently accessing the app at (typically correct for same-host setups). If your Plex Media Server is on a different host or behind a different network/proxy, override it with a URL Plex can reach.
- Click Test reachability to verify the URL is routable. The app self-POSTs a synthetic ping; success means Plex should also be able to deliver.
- Click Register with Plex. If you're missing Plex Pass, the UI will tell you and disable the button.
- (Optional) Confirm by checking Plex Web → Settings → Webhooks — your URL should appear there.
How it works at runtime: Plex POSTs a library.new event to /api/webhooks/plex whenever a new item is added. The app filters out everything else (media.play, media.rate, etc.), pulls the file paths from Metadata.Media[].Part[].file if present, otherwise looks the item up by ratingKey, and feeds the paths into the same debounce → batch → process pipeline as Radarr/Sonarr.
How auth works: Plex's webhook UI doesn't allow custom headers or HTTP Basic credentials, so there's no way to put an X-Auth-Token header on the requests Plex sends. Instead, the Register with Plex button appends your webhook secret (or API token) to the URL Plex stores as a ?token=… query parameter. When Plex POSTs to that URL, the endpoint validates the query token the same way it validates header tokens from Radarr/Sonarr. If you rotate the webhook secret, click Re-register with Plex (or just save settings — the app auto-re-registers on secret change) so Plex picks up the new value.
A scheduled poll for items where Plex's addedAt falls within a configured lookback window. Works without Plex Pass and without push notifications, at the cost of a polling interval of latency.
The scanner is a first-class schedule type. You create, edit, enable, disable, and delete Recently Added scanners through the same Schedules UI as any other scheduled job — and you can create multiple scanners with different libraries, intervals, or lookback windows. For example: scan Movies every 15 minutes with a 1-hour lookback, and your 4K library every 6 hours with a 24-hour lookback.
Quick start (one click):
- Open the web UI → Automation → Triggers tab → Recently Added Scanner (sidebar link).
- Click Create default scanner. A schedule is created with sensible defaults: runs every 15 minutes, lookback window 1 hour, all libraries.
- That's it. You can stop here, or continue to customize it.
Customize or add more scanners:
- Click Manage in Schedules tab on the scanner card, or switch to the Schedules tab directly.
- Click Add Schedule (or Edit on an existing scanner).
- In the modal, choose Scan mode → Recently added only. The Schedule Type field defaults to Interval; pick your frequency.
- Choose a Lookback window — 15 min / 30 min / 1 hour (default) / 2 hours / 6 hours / 24 hours / 3 days / 7 days.
- Pick a Library (or leave as "All Libraries") and click Create / Save.
Choosing a lookback window: items that already have BIF previews are skipped automatically by the job runner, so a larger lookback is cheap — it just re-queries Plex for a wider window. Pick something a few times larger than your scan interval so transient outages (e.g. a 30-minute Plex hiccup) don't cause missed items. The default 1 hour gives a 4× safety buffer over a 15-min interval, which is plenty for the happy path while staying light on Plex.
Scheduled scanners are marked with a blue "Recently Added" badge next to the schedule name in the Schedules table, so you can tell them apart from full-library scans at a glance.
Why stateless? The scanner doesn't track a "last seen" timestamp. Every tick it asks Plex for items added within the lookback window and submits them to the job pipeline; the job runner's existing BIF-existence check skips anything that's already done. This avoids cursor migrations, restart races, and clock-skew bugs.
| Plex direct webhook | Recently Added scanner | |
|---|---|---|
| Latency | Instant (event-driven) | Up to your scan interval |
| Plex Pass required? | Yes | No |
| Other Plex requirements? | Mobile Push Notifications must be enabled | None |
| Detects new items? | Yes | Yes |
| Detects in-place file upgrades? | No | No |
| Setup complexity | One click after entering URL | Toggle + pick interval |
| Network requirements | Plex must be able to reach this app | This app must be able to reach Plex |
You can enable both if you want belt-and-suspenders behavior — the recently-added scan acts as a safety net for any library.new event Plex's push-notification code path might drop.
The short version: HDR thumbnails get tone-mapped to SDR (standard dynamic range) automatically, so you don't see washed-out or pitch-black previews. Most HDR formats just work; the trickiest case is Dolby Vision Profile 5 (4K Dolby Vision rips with no HDR10 fallback layer), and the only setup step you may need is on NVIDIA — see the warning at the bottom of this section.
Tip
Quick glossary (so the rest of this section is readable):
- Tone mapping — converting HDR's wide brightness range down to the narrower SDR range a thumbnail can show. Without it, HDR content looks too dark or has a colour tint.
- HDR10 / HDR10+ / HLG — the common HDR formats; all use a single video track and tone-map cleanly.
- Dolby Vision (DV) — Dolby's HDR. Profile 7/8 carries an HDR10 fallback layer (works the same as HDR10). Profile 5 doesn't, and needs special handling.
- libplacebo / Vulkan — the GPU library that handles DV Profile 5 tone mapping. Needs a working Vulkan driver.
| Format | What we do |
|---|---|
| HDR10 | Tone-map to SDR (algorithm configurable, default: Hable) |
| HLG | Tone-map to SDR (algorithm configurable, default: Hable) |
| HDR10+ (without Dolby Vision) | Tone-map to SDR (algorithm configurable, default: Hable) |
| Dolby Vision Profile 7/8 (with HDR10 fallback) | Tone-map the HDR10 fallback layer with hardware decode (#178) |
| Dolby Vision Profile 5 (no fallback layer) | Per-GPU specialist path (see below); software fallback uses libplacebo (#172, #178, #212) |
What: the formula used to compress HDR's brightness range into SDR. Default works for almost everyone. Change only if your HDR thumbnails look too dark or oddly tinted.
Non-DV HDR content (HDR10, HLG, HDR10+) uses a configurable algorithm, set in Settings → Thumbnail Settings → HDR Tone Mapping or via the TONEMAP_ALGORITHM env var. Options: hable (default), reinhard, mobius, clip, gamma, linear. If HDR thumbnails look too dark, try reinhard.
What: the trickiest HDR format — has no HDR10 fallback layer, so the standard tone-mapping path can't read it. What to do: nothing — the tool picks the right path for whatever GPU you have. (NVIDIA users: see the warning below.)
The tool picks the fastest working path per GPU vendor:
| Vendor | Typical speed on 4K | Notes |
|---|---|---|
| Intel iGPU / Arc | ~17× (UHD 770) | Uses Jellyfin's DV-aware patch — currently the fastest path |
| NVIDIA | ~10–16× (Turing); faster on Ada/Hopper | Needs Vulkan driver — see the NVIDIA warning below |
| AMD Radeon | (untested locally; same flags as NVIDIA) | |
| CPU-only fallback | ~5–10× (CPU-bound) | When no GPU is available |
The image ships jellyfin-ffmpeg 7.1.3 as its preferred FFmpeg because Jellyfin's fork carries a Dolby-Vision-aware tone-mapping patch upstream FFmpeg still lacks. Non-amd64 builds fall back to the base image's FFmpeg 8.0.1 automatically.
Profile 7/8 (with HDR10 fallback) uses the standard tone-mapping chain — no Vulkan or special handling needed.
You don't have to do anything for these — the container handles them on startup. Listed here so you know what the logs are telling you when they mention DRI symlinks or Vulkan probe retries.
- Intel GPU under
--runtime=nvidia. When you're running both an Intel iGPU and an NVIDIA card under the NVIDIA container runtime, NVIDIA's tooling hides the Intel device from one specific OpenCL discovery path. The container quietly re-adds the missing symlinks on startup so the Intel iGPU stays usable for tone mapping. - NVIDIA Vulkan on dual-GPU hosts. When both an Intel iGPU and an NVIDIA dGPU are present, Vulkan defaults to Intel — but Dolby Vision tone mapping is faster on the NVIDIA card. The container's Vulkan startup probe tries up to four configurations to force NVIDIA selection so frames don't bounce between the two cards.
Important
NVIDIA users: set NVIDIA_DRIVER_CAPABILITIES=all (or include graphics).
Dolby Vision Profile 5 needs the NVIDIA Vulkan driver inside the container. NVIDIA's container toolkit only loads it when the graphics capability is declared. The common compute,video,utility setting is fine for everything else but not for Dolby Vision — without graphics, DV Profile 5 thumbnails come out with a green rectangle.
Fix: add -e NVIDIA_DRIVER_CAPABILITIES=all to your docker run command (or set it in the environment: block of your compose file) and restart the container. all is what the official NVIDIA Vulkan images use. If you prefer minimum privilege, compute,video,utility,graphics works too.
If the in-app warning banner persists after the restart, you may be hitting a less-common cause (a driver-version regression, a missing library in the container runtime config, or an ICD file in the wrong place). The banner will name the specific cause; GET /api/system/vulkan/debug returns a plain-text diagnostic bundle you can attach to a GitHub issue.
Common questions have moved to their own page — see FAQ.
Use this table to diagnose common failures quickly.
| Symptom | Likely Cause | Fix |
|---|---|---|
Skipping as file not found |
Path mapping mismatch between a media server and this container | Verify the server's per-entry mappings in Path Mappings (each Plex/Emby/Jellyfin entry has its own list). |
GPU permission denied |
Container user cannot access GPU device files | Set PUID/PGID to a user with GPU access; on Unraid use PUID=99, PGID=100. |
Plex config folder does not exist / unwritable |
Incorrect mount or wrong plex_config_folder |
Confirm the mounted /plex path contains Cache, Media, and Metadata. Previews Readiness surfaces this per-Plex-server. |
Connection failed on a server card |
Bad URL, unreachable host, or invalid token | Use server IP (not localhost in Docker), verify the server is running, and test the URL + token with curl. |
| Webhook job sits in Pending for a long time | The concurrent-job gate is full — active jobs are running at capacity | Wait for a slot to free up (priority-ordered), raise the cap in Settings → Processing Options, or check the global Pause Processing toggle isn't on. Pausing ≠ cancelling — paused jobs stay in Pending. |
Webhook returns 401 |
Invalid or missing authentication | In Sonarr/Radarr webhook settings, leave Username empty and set Password to your API token or webhook secret. |
| Webhook test passes but imports do not trigger jobs | Wrong webhook events or webhooks disabled | Enable On Import in Radarr/Sonarr and verify webhook_enabled=true. |
| New files are imported but previews are not generated | Plex indexing delay or wrong library mapping | Increase webhook delay and verify Radarr/Sonarr library mapping in Webhooks settings. |
| Radarr/Sonarr cannot reach webhook URL | Network routing or hostname issue | Use host IP or reachable Docker hostname (not localhost), then verify firewall and port 8080. |
| New job starts after I paused | Global pause not set or UI not refreshed | Use Pause Processing (Current Job or Job Queue header). Pause is global and persisted; in-flight files finish before workers idle. |
| DV Profile 5 thumbnails have a bright green rectangle or overall green cast | The container can't reach a real Vulkan-capable GPU, so DV tone mapping falls back to a slow software path that has a known rendering bug | Pass an iGPU to the container with --device /dev/dri:/dev/dri (Intel/AMD), or for NVIDIA set NVIDIA_DRIVER_CAPABILITIES=all so the NVIDIA Vulkan driver gets injected. Most users already pass /dev/dri for hardware video acceleration, which brings the Vulkan driver along for free. |
ls -la "/path/to/Library/Application Support/Plex Media Server"Expected directories include Cache, Media, and Metadata.
Enable detailed logs when diagnosing persistent issues. In Settings → Processing Options, set Log Level to DEBUG. Alternatively, set LOG_LEVEL=DEBUG as an environment variable (one-time seed on first start).
Schema downgrades are not automated. If you need to revert from a release that ran a settings migration, do it manually:
- Stop the container.
docker stop media-preview-generator
- Restore the relevant
.bakfiles from your config volume. Each JSON file the app owns leaves a single rolling.baknext to it on every save:cd /your/config/dir mv settings.json.bak settings.json # required mv schedules.json.bak schedules.json # if you use Schedules mv webhook_history.json.bak webhook_history.json # optional mv setup_state.json.bak setup_state.json # optional
jobs.dbdoes not have a JSON.bak(jobs moved to SQLite as of this release). To recover an older job database, restore your full config-volume snapshot. - Start the older app version that wrote those files.
docker run ... your/image:older-tag
Multi-server caveat. Multi-server installs cannot meaningfully downgrade to a single-server release without losing the second / third server's settings. The newer schema holds richer data than the older one can represent. The downgrade-refusal guard (introduced in this release) intentionally refuses to start the older binary against a newer
settings.json— its log message names the.bakpath so you have a one-line recovery hint.
Why it refuses to "just work". Silent acceptance would drop unknown fields on the next save — exactly the failure mode that wiped a user's job history during a tag-drift incident on the multi-server branch. Refusing to boot is loud and recoverable; silent truncation is quiet and final.
Open a GitHub Issue.
- Validate installation and mounts in Getting Started
- Confirm environment variables and API behavior in Configuration & API Reference