Guides & Troubleshooting

Use the Media Preview Generator web UI, trigger jobs from Sonarr, Radarr or Plex webhooks, handle HDR and Dolby Vision, and fix common problems.

Back to Docs

Guides for the web interface, remote nodes, webhooks, Intro & Credits, HDR handling, and troubleshooting.

Important

For installation and first-time setup, use Getting Started. For exact configuration values and API contracts, use Configuration & API Reference.

Contents


Web Interface

Setup Wizard

The first time you open the web interface, a Setup Wizard walks you through setup for Plex, Emby and Jellyfin:

  1. Choose your media server. Pick Plex, Emby or Jellyfin. The chosen card expands:
    • 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).
  2. Connect. Finish the selected server's connection. Plex includes server and library selection; Emby/Jellyfin manage libraries later from Servers → Edit → Libraries.
  3. Paths. Confirm the server's output location and media path mappings. Plex needs a writable data folder for BIFs; Emby and Jellyfin's default layout need writable media. Jellyfin off-media output has its own config-mount and plugin requirements.
  4. Options. Named CPU/GPU worker groups with allowed jobs and availability, per-device FFmpeg threads, thumbnail interval, and quality.
  5. Security. View or replace your access token (optional).

When setup finishes, you land on the dashboard. Add more servers (any vendor, any number) later from Servers → Add Server.

Reopening an unfinished setup resumes at the saved step and server. Saved path choices return; unsaved field edits do not.

In Paths → Check a real file, paste a File path on the server or webhook and choose Check file. The check applies the mappings currently in the form and reports the resulting container path and whether it is a readable file, missing, a folder or unreadable. It starts no job and doesn't say which library owns the file. Use Setup Health and a one-file job to verify the whole output path.

Accessing the Dashboard

  1. Start the container
  2. Open http://YOUR_SERVER_IP:8080
  3. Get your authentication token using Authentication Token
  4. Enter the token to log in

The version you're running shows under the app's name in the top bar. A dot beside it means a newer version is out; click the version for the release notes.

The top bar has Dashboard, Servers, Automation, Settings and Tools. Hover over Automation, Settings or Tools to see what's inside. Automation and Settings open their page when clicked; Tools opens the Inspector and also holds Library health, Logs, Webhook Activity and Run setup again. On a phone, tap one in the menu to list what's inside.

Dashboard Features

The Workers panel shows a compact table whatever the worker count. Busy workers and workers with a problem (a CPU fallback, say) come first. Idle workers fold into one line ("N workers idle"). A device with more than 8 rows shows the first 8 plus a Show N more button (Show less folds it back); rows with a problem always stay visible.

Note

Jobs queue with priority (1 = high, 2 = normal, 3 = low). A job's files run as soon as a worker that handles that job type is free. Only job start-up (loading settings and listing the files) is limited, to 3 jobs at a time, so a burst of webhooks doesn't flood your media servers. A job waiting for a start-up place shows Waiting to start; one whose files are listed but not yet picked up shows Waiting for a free worker. Manual, webhook and scheduled jobs follow the same rules. To hard-stop everything, use Pause in the Workers header. It survives restarts.

Priority also decides who gets the next free worker file by file. A High job overtakes a running full scan without cancelling or pausing it: the scan gets no new files until the High job drains. Jobs of the same priority take turns, so a small job isn't stuck behind every file of a large scan.

Among eligible pending jobs, start-up follows priority, then creation time, including after a restart. Jobs held by a pause, an unfinished dependency, or a retry timer let other eligible jobs pass. Change a job's priority from the Priority menu in its queue row. This reorders a waiting job and doesn't interrupt files already processing.

A job whose type has no open worker (every compatible worker group disabled or outside its hours) waits for one without taking a start-up place, and eligible work of another type can pass it. A retry keeps its due time across an app restart.

A full-library schedule keeps one unfinished job for the same selection and settings. Later ticks reuse that job while it is queued, running, paused, waiting for workers, or retrying, and don't clear a manual pause. A finished job or a changed selection starts a new run.

Choosing media for a library scan

In Start new job, select your libraries and open Filter media:

All applicable filters must match. If season 10 is the latest but season 9 was added yesterday, "latest 1 season" with "last 7 days" does not select season 9. If the latest season already has previews, missing previews in older seasons don't make those seasons eligible.

The summary shows active restrictions. Clear restores all selected media. Switching to movie-only or TV-only libraries clears the other type's restrictions. Missing only is the default Processing Mode.

Processing Order sets the queue order, separately from these filters. Newest and oldest use the library-added date and apply within each library. Random mixes the selected files. Parallel workers can start or finish out of order.

The same filters work for Intro & Credits and Plex loudness jobs, and on scheduled Whole library runs. A "last 30 days" schedule moves its window forward on every run; a date range stays fixed. Webhooks, Recently Added schedules and Process a file or folder use their own selection rules. Processing order applies to Previews and Plex loudness. Intro & Credits always checks a season's episodes together.

Letting new imports jump the queue

A full-library regeneration can run for hours. Settings → Processing options → Job execution → Incoming job priority sets the priority of the jobs the app creates when new media lands: webhook deliveries from Sonarr, Radarr, Plex, Emby and Jellyfin, and Recently Added sweeps. It defaults to High, so a freshly imported episode is processed within one file of arriving even while a full scan runs.

Manual and scheduled full scans keep the priority you gave them. A new Previews job starts at Normal; Intro & Credits and Plex loudness jobs start at Low, which leaves room for High jobs to overtake.

A schedule follows the incoming setting only while its own Job Priority is Default (from Settings), the value new schedules start on. Pick High, Normal or Low to pin that schedule instead.

Set the global setting to Normal for strict first-come-first-served ordering. A job you pin to High by hand can still overtake a running scan.

Manual Generation:

Process a file or folder runs Previews, Intro & Credits or Plex loudness for specific media. Tick one or more job types, then pick at least one file or folder. You can mix three ways of picking:

Each pick becomes a removable chip. The start button processes the chips and any pasted paths. For previews, Publish to scopes both search and publishing. For Intro & Credits and loudness, Search on only limits search results: processing uses the enabled owning servers. Loudness requires an eligible Plex server and CPU worker group. Regeneration applies to previews; Intro & Credits can re-check completed work. Loudness has no force-regeneration option.

Job details:

Jobs opens on Unfinished work: pending and running jobs, including paused jobs and retries. The status tabs are Unfinished, Running, Pending, Completed, Failed, Cancelled, Chapter warnings (only when a job has them) and All (all jobs and history). Each shows a count where one applies. The search box finds jobs by title or ID; Filters adds server, library and job type. Search and filters apply before pagination; job statistics stay totals for all visible jobs. The trash menu clears finished jobs by status.

Row buttons: View logs, Pause / Resume, Retry now (skips a retry wait; see Retries and attempts), Fire now (skips a webhook batch's wait), Cancel. To act on many jobs at once, tick their checkboxes (or the header checkbox for every selectable job shown). The toolbar then shows Cancel (n), which cancels the ticked waiting and running jobs (a job waiting to retry is cancelled with its retry), and Delete finished (n), which removes the ticked completed, failed and cancelled jobs. With nothing ticked, Clear jobs clears all finished jobs by status; with rows ticked it is replaced by these two buttons, so it never touches unticked jobs. Finished jobs offer View logs, Re-run and Delete. Retry rows distinguish files checked from a completed job. Intro & Credits and loudness companions are indented beneath their originating job when both are on the current page.

View logs opens Job Details with Logs and Files tabs. Results by server groups outcomes by server, with file issues, detection sources and CPU fallback information when available. A pending or retried job may already have saved results, so these counts don't mean the queue entry has finished. Current activity shows the current item when the job provides it. The Files tab lists Recorded outcomes and Requested paths (the whole selection, with search and pagination); a requested path may not be processed yet. File Inspector links open the Inspector in a new tab. An expanded queue row shows up to five requested paths; View all opens the Files tab.

Pause / Resume (global):

Scheduling:

The Dashboard shows the next scheduled run in Job statistics, and Run next schedule now in Quick actions. Full schedule management lives on the Automation page, under Schedules (/automation#schedules, also linked from the top nav). Add Schedule asks for:

Check servers is a built-in job (see Built-in jobs below): it runs every day, by default at 04:30, whenever Intro & Credits is on for a server. During quiet hours or Pause All it waits in the queue until processing resumes.

Built-in jobs: the app runs two daily jobs of its own. They have their own card below your schedules, with Run now, Edit and Disable. You can change their time or switch them off, but not delete them.

/schedules and /webhooks still work: they redirect to the Schedules and Triggers tabs of /automation.

Logs and webhook activity

Open Tools → Logs to follow application output. View switches between Structured rows and Raw text. The Source filter narrows the loaded records and Wrap lines controls long messages. Group by source keeps records in time order within each source. Copy and Download keep the timestamp, level, module, function, line and message of the visible records. They don't export older records that haven't loaded. The live status shows whether new entries are arriving and whether the view follows them.

Open Tools → Webhook Activity to see recent incoming deliveries. Filter by Title or file, Source, Outcome or Server. Filters apply to the in-memory history, which is not a durable archive. Reset filters restores the full list and Refresh fetches the current history. A no-match message means the filters excluded every delivery; a load error means the history couldn't be fetched. Neither proves that no webhook was sent.

Settings Page

Open Settings (/settings). On a wide screen a side menu groups the sections (Run, System, Info); on a phone the same sections are in a picker at the top.

A server's connection, libraries and path mappings are on the Servers page (Configure).

To audit a server's settings (Plex FSEvent flags, Jellyfin trickplay flags, Media Preview Bridge plugin, Plex config folder writability, path mappings), open Servers → Edit → Setup Health. Every check is in the Setup Health guide.

Most Settings controls and the Automation page's Triggers settings save as you edit. Worker group edits use Apply group changes, and the Global pause schedule has its own Save. Toggles, sliders and dropdowns save immediately; text fields save on blur or about 1 s after you stop typing. A status indicator in the page header shows Saving… and then Saved.

Worker groups and availability

Open Settings → Workers (or Manage groups in the Dashboard's Workers header) to choose which jobs can use your hardware. A group has:

Add a device with Add device; remove it to use zero workers on it. Start with one group and add more when devices need different hours. Add group, Add CPU group for loudness, the enable switch, Edit, Duplicate group and Remove group manage them. Edit opens the editor under the group. For a group with weekly hours, This group's week draws its active hours as a 7-day graph, one lane per device. Apply group changes saves all edits at once; Discard changes drops them. If another Settings session changed the groups first, applying shows a conflict instead of overwriting; discard and reload before editing again.

A CPU device can run Video previews, Intro & Credits and Plex loudness. GPU devices can run previews and Intro & Credits; loudness needs CPU workers. Chapters stay part of Previews. A GPU video job can still use CPU stages or CPU fallback. FFmpeg threads per GPU worker are set under Processing options → GPU device tuning.

For example, an Off-hours group open daily 01:00–07:00 can hold 3 NVIDIA workers for previews and Intro & Credits plus 10 CPU workers for loudness only. Outside those hours the group is idle. To keep loudness to one analysis at a time, give a CPU device 1 worker and allow only loudness. A GPU-only installation still does video, but loudness waits until you add a CPU device. Plex's own loudness measurements stay visible in the Inspector meanwhile.

On the Dashboard, each device row in the Workers panel has a − / + stepper that changes its worker count and saves at once. A disabled group shows Enable instead. A decrease retires workers after their current files finish; an increase can restore workers that were about to retire. The stepper never goes below 1; remove the device in Settings, or disable the group, to use zero workers. A disabled group keeps its counts for when you enable it again.

There are three separate timing controls:

  1. Job schedules, on Automation → Schedules, create or resume jobs. Their optional stop time holds that schedule's jobs and pauses active processing for those jobs. It does not disable a worker group.
  2. Worker availability, on Settings → Workers, controls new file starts. Closing a window lets current files finish; it does not create jobs or promise silence at the closing time. Select the days a window starts: Monday 23:00–07:00 means Monday night through Tuesday morning, in the displayed app timezone.
  3. The Global pause schedule, on Settings → Global pause schedule, pauses all processing, including active files. Opening a worker window or ending a pause window never clears a manual Pause.

A waiting job's queue row says why: no compatible workers, workers outside their hours, busy workers, or a full job limit. It keeps its ID and unfinished files and continues when capacity returns.

Global pause schedule

Settings → Global pause schedule pauses all processing, including files already running, during weekly windows. Turn on the toggle (labelled Enable quiet hours), then Add window. Each window has Start days (every day, weekdays, weekends or chosen days), Pause at and Resume at, in the app's timezone. Days select when a window starts: Monday 23:00–07:00 ends Tuesday morning. Pause windows this week draws them as a week graph. The section has its own Save. Windows are saved but not applied while the toggle is off.

While a window is active, the Dashboard shows Paused. Scheduled runs that come due are skipped and not caught up later; manual and webhook jobs can queue. A manual Pause stays until you Resume, even after a window ends.

Retries and attempts

Settings → Processing options → Retry policy has two controls:

A file is retried when the media server hasn't indexed it yet or a per-file failure is recoverable. A job and its retries stay one row in the queue (a retry chain). While it retries, the row shows a Retry N/M chip and Waiting to retry with a countdown. Retry now (on the row, or in Job Details) skips the wait. In Job Details, Attempts lists each run, such as "1 original + 2 retries", so a retry count of 5 allows up to 6 runs. Pick an attempt to read its log; Files combines the results of all runs. When the retries run out, the job reports the files that were still missing, and the next scheduled scan picks them up.

Plex chapter thumbnails

Chapter thumbnails are optional images for the chapters Plex already knows about. They are separate from the scrubber previews and from Intro & Credits markers. They support Plex Media Server 1.43.4.x or 1.43.5.x. Plex keeps one chapter list per title, so with several versions the thumbnails come from one version, as in Plex. A version split into several files is skipped. Extraction handles SDR, HDR10, HLG and Dolby Vision with a compatible HDR base layer. Dolby Vision without that base layer, such as Profile 5, is skipped and the scrubber result is kept. HDR chapter images are tone-mapped to SDR. If a chapter timestamp lies more than a second past the video's length, the result reports an invalid chapter timestamp instead of retrying. The app never changes the video or Plex's chapter timings.

  1. Open Servers → your Plex → Edit → Processing → Chapter thumbnails, enable Generate chapter thumbnails, and save. It is off by default. It lets this app write chapter images and update their references in Plex's database. It doesn't change your videos, create chapters or change chapter times.
  2. Open Setup Health. The app needs a Plex database it can positively identify on the same machine, or a compatible Plex database node beside Plex. For a node, pick it under Edit → Connection → Plex database. Intro & Credits can stay off.
  3. In Plex's Settings → Library → Generate chapter thumbnails, select Never, so Plex doesn't repeat the work and replace these images. This Plex setting covers all its libraries. The app checks it but doesn't change it.
  4. Run your usual Previews job. Existing schedules and webhooks include chapters while the toggle is on; there is no separate chapter schedule. Extraction runs on the CPU within the existing worker and thread limits. A normal run keeps current scrubber previews and fills missing or stale chapter images. A title whose chapters all have images Plex made itself is left as it is. Regenerate rebuilds the enabled outputs, including those.

Worker cards show the chapter count and percentage as images are checked or attempted, followed by Registering with Plex. The percentage measures image work; Ready confirms registration succeeded. Extraction runs on the CPU even when the slot is named GPU Worker. Failed attempts count toward progress and are shown separately.

The job's Files panel reports scrubber and chapter results separately:

Server summaries count scrubber and chapter results separately. Scrubber already existed can appear beside Chapters updated when a run creates chapter images or repairs their Plex references while reusing the scrubber. Chapters already existed means those images and references were already current. Counts are files, not images. Chapters waiting for Plex and Chapters failed name the remaining work. Open Files for per-file details. The job's retry details show whether another attempt is scheduled; that depends on the failure and your retry policy. Chapter retries reuse completed output, so they don't regenerate a current scrubber preview. If chapter seeking stalls or a check finds a damaged Matroska seek index, the app first tries to recover missing chapter images from its own scrubber previews. That needs the app's journal to match the current source fingerprint; a BIF Plex made without that journal isn't used. The chosen frame must fall inside the chapter, within five seconds of its start. Recovered images are lower resolution; the result says so, and the saved chapter manifest records their original dimensions and timestamps. The app never repairs, remuxes or rewrites the source video.

Once a stalled or corrupt seek triggers recovery, the rest of that source's chapter work uses recovery instead of waiting on FFmpeg again. A chapter extraction has a five-minute active-time limit; time spent paused doesn't count. Completed images are kept. If no verified recovery frame exists, the file keeps a chapter failure. A temporary stall follows the retry policy; proven container corruption isn't retried. Partial chapter sets are not registered with Plex.

Publishing still checks the exact source and chapter snapshot in a database transaction. If only Plex's source update timestamp changed, the app rereads once and retries after checking the source fingerprint. Changed chapter references, timings or source identities stay conflicts; the app never overwrites a concurrent publisher's references. Ready also requires Plex's API to expose the expected image references.

The Jobs page reports how many past jobs ended with chapter issues. Review chapter warnings opens those saved results even though they are no longer in Unfinished. This is a history count, not a sign the files are broken today. A completed job with warnings can have working scrubber previews and incomplete chapters. Its details offer Re-run job, which starts a new job with the original full selection and settings and keeps the old result. It isn't limited to failed files. Retry now applies only while a retry is pending. Plex's normal file checks can rewrite chapter image references even when its own generation is set to Never. A later Previews job reconciles those references without decoding images again.

Plex cache limitation: Plex can keep showing an older chapter image after regeneration, even after reloading Plex Web. Plex's own generator does the same. Never doesn't clear these cached images, and another job doesn't make clients show the replacements at once.

On the first enabled run, chapter images Plex made earlier are rebuilt once, because the app has no saved source fingerprint proving they match the current video. The replacements use Plex's existing chapter times; chapter rows and other marker types are kept. Current scrubber previews are reused. Turning the toggle off keeps the images already written. See chapter Setup Health checks for connection and native-generation problems.

Automatic GPU → CPU Fallback

When FFmpeg fails on a GPU, the file tries your other GPUs first, then the CPU. That happens for:

Other GPUs first. The file goes to another GPU that has workers for that job type and hasn't tried it yet. This helps when your GPUs decode different formats, such as an older NVIDIA card without HEVC next to an Intel iGPU with it. The file waits at the front of the queue for that GPU's next free worker while the job's other files keep going. The job shows 1 file waiting for another GPU, or Waiting for another GPU when nothing else in the job is left. A failed read or a stalled disk isn't the GPU's fault, so that file stays on its worker.

Then the CPU. When no other GPU is left, the worker holding the file retries it on the CPU. The job log records the reason ("Dolby Vision Profile 5 rejected by Intel VAAPI", "signal kill (signal 11)" and so on), and the worker card shows a yellow CPU fallback badge with a toast. Intro & Credits work on a GPU worker shows the same badge whenever a step of it runs on the CPU instead: a credits decode rerun, an end-picture check, or credit text detection its GPU couldn't do.

When one GPU falls back on 5 files in a row, the bell shows GPU keeps failing: files are running on the CPU with the last reason. It goes away once that GPU finishes a file on its own again. A finished job's summary says how many files ran on the CPU because the GPU failed.

The worker is busy on the CPU while the retry runs. If a lot of content never decodes on the GPU, add a CPU worker group (Settings → Workers) so that content goes straight to CPU workers instead of blocking a GPU worker each time.

Speeding up full-library scans on big libraries

A full scan first checks every file for an existing preview, then generates only the missing ones. The two steps have separate limits:

On a large, mostly complete library the check can dominate. Leave Files checked at once on Auto for most setups. Raise it if the checking phase is slow on fast storage, or lower it if a single spinning HDD thrashes. It never starts more FFmpeg processes; your worker counts govern those.

Automation Page

The Automation page (/automation) hosts two tabs, switched from its side menu:

The Triggers tab has:

The Plex direct webhook and the Recently Added scanner are set up under Servers → Edit → Webhook & Scanner.

Inspector

Tools → Inspector (/inspector) shows one film or episode on one page: its preview frames, where its intro and credits are on each server, and the loudness measurements Plex reports for its audio tracks.

Exact frames. Preview frames are 2 to 10 seconds apart, too coarse to place an edge, so Adjust and Needs your check read frames one second apart straight from the video. Only a file the app already knows is read (one in a server's library that Intro & Credits has seen, or that a server lists). ffmpeg runs on the CPU at low priority, at most two reads at a time, each stopping after 30 seconds. HDR10 and HLG frames are tone-mapped like the previews; Dolby Vision Profile 5 frames aren't, so their colours look off. The frames are kept in a size-capped folder under the system temp folder. Nothing is written next to your media.

Library health

Tools → Library health shows, for every library on every server, how many files still need previews, chapter thumbnails, loudness or Intro & Credits. There is one card per server and one row per library, with the columns Previews, Chapters (Plex with chapter thumbnails on), Loudness, Intro and Credits.

Production Server

The Docker image runs the web interface; there's nothing to configure. The dashboard updates live over WebSocket. To run the app outside Docker, or to see how the container is wired (gunicorn settings, WebSocket transport), read CONTRIBUTING.md → Architecture.

Reverse Proxy

To expose the web UI outside your local network, for example with HTTPS or a custom domain, put it behind a reverse proxy such as Nginx, Apache or Traefik.

The app listens on port 8080 (HTTP) and the proxy forwards requests to it. The web UI uses WebSocket (Socket.IO) for live updates, so the proxy must forward WebSocket upgrade requests.

Nginx

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;
}

Apache

Enable the required modules first:

sudo a2enmod proxy proxy_http proxy_wstunnel rewrite headers
sudo systemctl restart apache2

Example 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

Traefik v2+ forwards WebSocket upgrade headers automatically. A standard HTTP router and service is enough.

Authentication

The web interface uses token-based authentication:

  1. Auto-generated token: created on first run and saved to /config/auth.json.
  2. Custom token: set your own in the setup wizard (step 5).
  3. Fixed token: set the WEB_AUTH_TOKEN environment variable. It overrides the wizard setting.
  4. Token masking: logs show only the last 4 characters of a token.
  5. Server secrets: a media server token, API key or password inside an error (a request's URL, say) shows as **** in this app's log lines (tracebacks included), a job's error and log, and the Files panel.
  6. Protection from other sites: while you're signed in, every change the browser makes (saving settings, starting or cancelling a job, editing a server) carries a security token only this app's pages know, so a page on another site can't make those changes for you. If a button says the page's security token is out of date, reload the page. Webhooks use the webhook secret instead. A script must send the API token in a header on every request, as below: signing in once with POST /api/auth/login and reusing the cookie doesn't work for changes. See Reference — Authentication.
  7. Signing out: Logout signs out this browser only. Other browsers stay signed in, even after you change the token. To sign every browser out, delete flask_secret.key from the config folder (or change FLASK_SECRET_KEY, if you set it) and restart the container (see Reference — Authentication).

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/jobs

Rate Limiting

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

Real-Time Updates

The dashboard streams job progress over WebSocket (Flask-SocketIO, /jobs namespace). Events and payloads are in Reference — WebSocket Events.


Remote nodes

For what nodes are for and a quick setup, start with Use another machine's GPU.

A node is another machine that helps this app. The app keeps the queue, the schedules, your server tokens and every write to your servers. A node runs the heavy step it is handed and sends the result back.

A node has one or both roles:

Running a worker node

Use the app's own image, the same tag as the app, started with MODE=node:

docker run -d --name media-preview-node -p 9494:9494 \
  -e MODE=node -e NODE_NAME=gpu-box -e PUID=1000 -e PGID=1000 \
  --device /dev/dri:/dev/dri \
  -v /path/to/media:/media:ro -v /path/to/node/config:/config \
  stevezzau/media_preview_generator:latest

Adding it

  1. Settings → Nodes → Add node: enter a name, the node's address (http://machine:9494) and its key, then click Connect.
  2. Click Next: where it sees your media. For each library, enter the folder this app uses and the folder the node sees the same files at, then click Test access and Save. You can change the mappings later on the node's Media tab.
    • Test access takes 3 real files per library and checks that the node sees each one with the same size and modified time (within 2 seconds, for shares that store coarser times). A wrong mapping shows the exact path tried.
    • A library with no mapping row is skipped: the node never takes its files, so other workers do. To keep one library off the node, give every other library its own row and leave that one out.
    • If a library's files are links into another folder, the node follows a link only into a folder it has a row for, so add a row for the folder the links point into.
  3. Settings → Workers: add the node's GPU or CPU to a worker group. Its devices are listed under the node's name, after This app's own. Hours, job types and worker counts work as for this app's devices.

The node card shows its state: Ready, Connecting… (not checked yet), Offline, Key refused, Update needed, FFmpeg missing, Temp folder problem, Access problem, GPU lost, Added twice (the same machine at two addresses) or Turned off. Only a ready node gets work. A node that lost its GPU keeps its CPU workers. Each node also has its own Setup Health and Log tabs.

If a node goes away mid-file, the file goes back to the queue and runs on another worker. A file the node can't see at its mapped path also goes to another worker, and fails only when no other worker can run it. Three different files it can't see within 10 minutes pause it with Access problem until Test access passes, or until the check it runs every 5 minutes does. A file that changed since this app last looked at it doesn't count.

Updating a node

When you update this app, update every worker node to the same image tag. Until then its card says Update needed and it gets no work. Pull the new image and recreate the container with the same settings, keeping its /config: docker compose pull && docker compose up -d, or docker pull, then docker stop media-preview-node && docker rm media-preview-node and the same docker run again. Watchtower can update nodes too.

A Plex database node only has to be new enough, and its Setup Health says when it isn't. On Linux, set its image tag to the one in nodes/plex-db/docker-compose.yml and run docker compose pull && docker compose up -d. Its /config volume, with the loudness undo record, stays. An install from before nodes has no /config volume. Add -v plex-db-node-config:/config (or the compose file's volumes: line) when you update, or it writes no loudness. On Windows, download PlexDbNode-windows.zip from the newest Plex database node release and run its install.ps1 again with the options you installed with (-Port, -PlexConfigDir). The key stays.

Removing a node

Open the node and click Remove on its Settings tab. A node that a worker group or a Plex server still uses can't be removed until you take it out of them. The node keeps running after that. Stop and delete its container (docker compose down in its folder, or docker rm -f media-preview-node) and, if you won't add it again, its /config folder.

A Plex database node keeps the undo record of its loudness writes in /config (loudness-writes.jsonl; on Windows, in %LOCALAPPDATA%\MediaPreviewGenerator\node). Keep it, or undo those writes first. On Windows, remove the node with its uninstall.ps1, which keeps the record.

Plex on another machine

Run a Plex database node beside Plex: the Linux image or the Windows build. Add it under Settings → Nodes, then pick it in Servers → your Plex → Edit → Connection → Plex database. A Plex marker agent from before nodes moves to the Nodes page on upgrade and keeps working.

Plex on Windows with this app in Docker Desktop on the same PC needs the Windows node too. Its address is http://host.docker.internal:9494. If Plex runs as a service or starts before anyone signs in, install the Windows node with -AtStartup so it starts at boot too (Plex's Windows user must be an administrator).

One node with both roles

On the machine that runs Plex, one container can be both the worker and the Plex database node. Start the app's image as a worker node (above), then add -e NODE_ROLES=worker,plex_db, Plex's config folder and where it is inside the container:

docker run -d --name media-preview-node -p 9494:9494 \
  -e MODE=node -e NODE_NAME=plex-box -e NODE_ROLES=worker,plex_db -e PUID=1000 -e PGID=1000 \
  -e PLEX_CONFIG_DIR="/plex/Library/Application Support/Plex Media Server" \
  --device /dev/dri:/dev/dri \
  -v /path/to/media:/media:ro -v /path/to/plex/config:/plex -v /path/to/node/config:/config \
  stevezzau/media_preview_generator:latest

A node behind a reverse proxy

The node sees every request through a proxy as coming from the proxy's address, and its limits are per address. After too many wrong keys it holds back everyone behind that proxy for a while, and one address can hold only so many connections open. Send only this app's traffic to the node through the proxy, and give anything else that reaches it the right key.

If the node's https certificate is your own (self-signed, or from your own certificate authority), the app has to trust it. Put a file in the app's container that holds the usual public authorities plus yours (for example your host's /etc/ssl/certs/ca-certificates.crt with your certificate added at the end) and set REQUESTS_CA_BUNDLE to its path. It replaces the app's built-in list for every https connection, Plex, Emby and Jellyfin included, so keep the public authorities in it.


Webhook Integration

Generate previews when Radarr or Sonarr imports new media, or when a tool such as Tdarr or a script changes a file. A webhook processes only the imported files, after a delay that gives your media server time to index them.

How It Works

The source-specific URLs batch imports as described below. The universal /incoming and /server/<server_id> URLs also wait before processing, but keep a separate job for each resolved file version. All webhook URLs use Delay before processing unless overridden with delay.

  1. Radarr/Sonarr imports a file (or an external tool sends a custom webhook) and a POST is sent to this app.
  2. The app queues the file and starts (or resets) a timer. Imports from the same source (Radarr, Sonarr, or Custom) and server selection are batched together.
  3. Each accepted new file resets the timer using the URL's delay parameter, or the global delay if omitted. A batch becomes ready when that wait expires or it reaches its maximum age, whichever comes first. The maximum age is 10 minutes, extended to the longest delay accepted into the batch if that exceeds 10 minutes, measured from its first file. Files arriving once that limit is reached start the next batch. Processing also waits for an available worker and honors the global pause.
  4. The delay matters because your media server needs time to add the new file to its library. Process too soon and the file may not be indexed yet, so the job can fail or skip it. Files not yet indexed are retried on a backoff (1 m → 2 m → 5 m → 15 m → 60 m by default; Settings → Processing options → Retry policy sets the count and scales the waits). Once the retries run out, the job says the file wasn't indexed after that many retries, and the next scheduled scan picks it up. See Slow-backoff retry queue.
  5. When the timer fires, the app finds every configured server that owns each queued path, processes the file once, and publishes to each server in its native format: Plex BIF bundle, Emby sidecar BIF, Jellyfin trickplay tiles. Items that already have a fresh preview are skipped.
  6. A file already replaced by a newer one when its job runs isn't retried. This happens when Sonarr or Radarr imports the same episode or movie again under a new name, in the same folder or in that folder on another disk of the same library. The Files panel shows Gone from disk, in preview and Intro & Credits jobs alike, and the newer file is processed on its own. Any other missing file is retried as usual.

Prerequisites

Configure Radarr

  1. Open the web UI and navigate to Automation → Triggers tab (in the top nav)
  2. Copy the Radarr Webhook URL
  3. In Radarr, go to Settings → Connect → + → Webhook
  4. Set Name: Plex Previews
  5. Set URL: paste the Radarr Webhook URL
  6. Under Events, enable:
    • On Import
    • On Upgrade
  7. 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.
  8. Click Test to verify the connection
  9. Click Save

Configure Sonarr

  1. Copy the Sonarr Webhook URL from the web UI Automation → Triggers tab
  2. In Sonarr, go to Settings → Connect → + → Webhook
  3. Set Name: Plex Previews
  4. Set URL: paste the Sonarr Webhook URL
  5. Under Events, enable On File Import and On File Upgrade. On Import Complete can stay on too. It lists an import's files again once they're all in. A file its per-file event already sent is skipped (Sonarr's download id matches the two, so a season pack that takes an hour to import isn't queued twice), and a file only it lists is processed.
  6. 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.
  7. Click Test then Save

Set a delay per webhook URL

Append ?delay=<seconds> to the copied webhook URL to override Delay before processing for that request. Use a whole number from 1 to 3600 (up to one hour). For example, give movie imports 30 seconds while allowing five minutes for an episode batch:

http://your-server:8080/api/webhooks/radarr?delay=30
http://your-server:8080/api/webhooks/sonarr?delay=300

If the URL already contains a query parameter such as server_id or token, append &delay=30 instead:

http://your-server:8080/api/webhooks/radarr?server_id=plex-main&delay=30

Without delay, every ingestion URL uses the global setting: /radarr, /sonarr, /sportarr, /custom, /plex, /incoming, and /server/<server_id> under /api/webhooks. For example, a Plex, Emby or Jellyfin URL can use /api/webhooks/incoming?token=YOUR_TOKEN&delay=30.

/incoming and /server/<server_id> keep one job per resolved file version, each with its own deadline. The wait starts when the job is queued, after any media-server lookup needed to resolve the item. Later notifications don't extend another job's wait, and ignored duplicates create no job. Use delay=1 for the shortest wait.

On the source-specific /radarr, /sonarr, /sportarr, /custom and legacy /plex URLs, the latest accepted new file sets the wait for its whole source and server batch, up to the batch's maximum age. That age is the greater of 10 minutes and the longest delay accepted into the batch, measured from its first file. So delay=3600 allows a full hour for a new batch without letting later imports postpone it forever. Files joining near the age limit can get less than the full delay. If requests in one batch use different delays, the latest one sets the timer, so keep the URL consistent. Duplicate notifications don't reset the timer.

This sets the initial wait only. Automatic retries keep their own backoff, and manual Reprocess skips the initial wait. Pending webhook jobs keep their deadline across a restart when automatic job recovery is on. Media-server scan requests still happen when each import arrives.

Custom Webhook (Tdarr, scripts, etc.)

The custom webhook starts preview generation when a tool POSTs a file path. Use it when a tool such as Tdarr changes a file after Sonarr or Radarr imported it: Plex removes the old thumbnails, but Sonarr and Radarr send no new webhook because no import happened.

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

Configure Tdarr

Tdarr has no built-in webhooks. Use the Send Web Request Flow plugin to POST to the custom endpoint after each transcode.

  1. In the web UI, open Automation → Triggers and copy the Custom Webhook URL.
  2. In Tdarr, open the Flow you want to trigger previews from
  3. Add a Send Web Request plugin after your transcode step
  4. 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}}}"}
  5. Save the Flow

Tdarr replaces {{{args.inputFileObj._id}}} with the full path of the transcoded file.

Tip

If the webhook request fails (for example the server is briefly down), add a Reset Flow Error plugin after the Send Web Request step so Tdarr doesn't mark the entire transcode as failed.

Configure FileFlows

FileFlows needs two nodes at the end of your flow: Set Variable to build the final output path, then Web Request to POST it to the custom endpoint.

  1. In FileFlows, install the Web plugin (Plugins page → search "Web") so the Web Request node becomes available
  2. Open the Flow you want to trigger previews from
  3. Add a Set Variable node just before the Web Request. FileFlows has no "final output path" variable, so build it from the folder, final filename and extension:
    • Variable: output_file
    • Value: {folder.Orig.FullName}/{file.NameNoExtension}{ext}
  4. 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, value YOUR_TOKEN
    • Body: {"file_path": "{output_file}"}
  5. Save the flow

FileFlows replaces {output_file} with the absolute path of the file as it exists at the end of the flow, after any rename or re-extension steps.

curl Example

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"}'

Configuration

These settings are under Settings on the Automation → Triggers tab. They save as you edit.

Setting Default Description
Enable webhook processing On Master toggle
Delay before processing 60s Default initial wait for every webhook URL (slider: 10–300 s); override per request with delay=1 through delay=3600. Universal/per-server routes wait per job. Source-specific routes reset their batch timer on new files, with a maximum age of the greater of 10 minutes and the longest accepted delay.
Webhook Secret (empty) Dedicated authentication token for webhooks

Webhook processing uses each server's library selection (Servers → Edit → Libraries). If a webhook path belongs to a library that isn't selected, it is skipped.

Webhook Secret

By default, webhooks authenticate with your main API token. You can set a separate webhook secret instead:

  1. On Automation → Triggers, click Generate next to the secret field. It saves automatically.
  2. Use the secret as the token: in Radarr or Sonarr, put it in Password (leave Username empty) or in the X-Auth-Token header if your form has a Headers section.

Batching and the delay

On the source-specific URLs, files imported in quick succession (a season pack, say) are queued per source and server selection. Each accepted new file resets the timer to that request's delay. A batch is ready when the timer expires or it reaches its maximum age: the greater of 10 minutes and its longest accepted delay, measured from the first file. Radarr and Sonarr have separate batches, so a new episode doesn't extend a movie's wait.

Example: Sonarr imports 10 episodes over 30 seconds with a 60s delay. The timer resets as each episode arrives. One job runs 60 seconds after the last episode and processes all 10 files. A file that arrives at 59 seconds joins this batch, which then runs 60 seconds after it, so Plex has time to index it.

Example (long import): With the default 60s delay, Sonarr imports 300 episodes, one every 10 seconds (50 minutes in all). The batch runs 10 minutes after its first episode with the roughly 60 episodes it has by then, and the next episode opens a new batch. A file that joined just before the 10 minutes were up gets less than the full delay. If the server hasn't indexed it yet, it's retried.

Viewing files in a batch: In the Jobs queue, a webhook job shows a label like "Sonarr: 3 files", and Show files expands the list. Tools → Webhook Activity lists each delivery and its outcome.


Auto-trigger from Plex (no Sonarr/Radarr)

For media you add to Plex by hand (copying files into a watched folder, importing through Plex, or any tool other than Sonarr, Radarr or Tdarr), two built-in options start preview generation. Both are under Servers → your Plex → Edit → Webhook & Scanner (Plex Direct Webhook and Recently Added Scanner) and feed the same job pipeline as the other webhooks.

Important

Both options trigger only on new library items. When Sonarr or Radarr upgrades a file in place, Plex keeps the same library item, so neither option sees it. Use the Sonarr and Radarr webhooks (On Upgrade) for that.

Option A — Plex direct webhook (instant)

The app registers its own /api/webhooks/plex endpoint through Plex's account API, so you don't have to paste anything into Plex Web → Settings → Webhooks (you still can).

Requirements:

Setup:

  1. Open Servers → your Plex → Edit → Webhook & Scanner.
  2. The URL field is pre-filled with the address you're using to reach the app, which is usually right when Plex and the app share a host. If Plex is on another host or behind a different network or proxy, replace it with a URL Plex can reach. The URL must be reachable from Plex, not just your browser.
  3. Click Register with Plex. If your account has no Plex Pass, the page says so.
  4. To confirm, check Plex Web → Settings → Webhooks for your URL. Remove from Plex unregisters it.

What happens: Plex POSTs a library.new event to /api/webhooks/plex whenever a new item is added. The app ignores other events (media.play, media.rate, etc.), takes the file paths from Metadata.Media[].Part[].file or else looks the item up by ratingKey, and queues them like Radarr and Sonarr imports.

Authentication: Plex's webhook settings allow no custom headers or HTTP Basic credentials, so Register with Plex appends your webhook secret (or API token) to the stored URL as a ?token=… query parameter, and the endpoint checks it like the header tokens from Radarr and Sonarr. If you rotate the webhook secret, click Re-register with Plex so Plex picks up the new value. Saving a new secret re-registers it automatically.

Option B — Recently Added scanner (universal)

A scheduled poll for items whose Plex addedAt falls within a lookback window. It needs no Plex Pass and no push notifications, but new items wait up to one polling interval.

A scanner is a schedule. You create, edit, enable, disable and delete it in the Schedules UI, and you can have several 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.

Set one up:

  1. Open Automation → Schedules and click Add Schedule (or Edit on an existing scanner). The Recently Added Scanner card under Servers → Edit → Webhook & Scanner has Configure on Schedules page and Scan all now.
  2. Under Files, choose Recently added. Pick your Schedule type (an interval suits a scanner) and frequency.
  3. Choose a Lookback window: 15 minutes, 30 minutes, 1 hour (default), 2 hours, 6 hours, 24 hours, 3 days or 7 days.
  4. Pick a server and libraries (or leave all) and save.

Choosing a lookback window: items that already have previews are skipped, so a larger lookback is cheap: it only asks Plex about a wider window. Pick a window a few times your scan interval so a short outage (a 30-minute Plex hiccup, say) doesn't cause missed items. The default 1 hour is 4 times a 15-minute interval.

Scanners carry a Recently Added badge next to the schedule name in the Schedules table.

The scanner keeps no "last seen" timestamp. Every run it asks Plex for items added within the lookback window and queues them, and the existing preview check skips anything already done.

Which option to pick

  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. The scanner then catches any library.new event Plex drops.


Intro & Credits

Skip Intro and Skip Credits markers for Plex, Jellyfin and Emby. Each file is checked once, using its chapters, online databases, the theme tune matched across a season, and the on-screen credit roll. The markers are then published to every server that has the file. Precision over coverage: a missing marker is fine, a wrong one isn't, so a marker ships only when the evidence clears the bar below.

Turning it on

Detection settings are shared by every server: Settings → Intro & Credits. What each server receives is set per server: Servers → (server) → Edit → Processing → Intro & Credits → "Send intro & credits markers to this server". A server is off by default, even after you set up the shared settings.

Once the switch is on, that server's Libraries tab gets an Intro & Credits column beside Previews. Switch on the libraries that get markers. The two columns are independent: a library can get markers without previews, or previews without markers. Sports libraries start off, because no online source covers sports and local detection isn't reliable there. You can switch one on by hand.

Sources and the publish rule

In Settings → Intro & Credits, Where we check lists every source in the order it's checked. Drag to reorder. Each source's ⓘ says what it is and when it's used:

Source Notes
Chapters inside the file Free, exact when present. On a TV episode (a file whose name has a season and an episode number) a chapter named Ending counts as credits, since anime names its ending that way; on a movie it doesn't, because there it is usually the last scene. End alone never counts. An intro chapter much longer than the rest of the season's (more than twice their median length and more than 30 s longer than it, with at least 2 other episodes carrying one) doesn't decide alone: another source that isn't a server's own marker has to agree.
TheIntroDB Optional API key (masked once saved). Works without one (500 lookups/day); your own free key raises that. Off by default; see the note below.
IntroDB.app No key needed, TV only.
SkipDB Free, only counts an answer matched to your file's own length, and only once another source agrees with it.
Matching audio across a season TV intros. Finds the theme tune a season's episodes share, and publishes an intro by itself when nothing else answers — a show no online database has still gets intros. The online sources are still asked on their usual schedule (one with no entry again after 14 to 28 days). A server's own marker doesn't count as a second source for it, since a server's intro detection matches audio too. An online intro of the same length (within 5 s) that starts more than 15 s away from season audio's, with nothing else agreeing with it, was timed on another release of the episode (one with a different cold open, say), so it is set aside. Needs an ffmpeg with chromaprint, which the amd64 Docker image has; elsewhere Settings shows Not available and why. CPU, about 2 s per episode, on the worker that runs the file. See Season audio and weekly releases.
On-screen credit text Finds where the credit roll starts from text on screen in the last 15 minutes of a movie (7.5 of an episode, or the window you set under Advanced), and stops the skip at the last credit when a scene follows the roll (Emby skips to the end of the file). Text that stays in one place through the story (a channel logo, a score bug, a ticker) is ignored, and a file with text on screen through most of its ending (a burnt-in timecode, say) still gets no answer, and a show with burned-in captions through its story may get none when the credits run over story footage. Credits already running when those last minutes begin are still found, by reading 2 more minutes back. Credits that start in the first 30 seconds of those minutes right after a scene, or more than 1½ minutes before them, get no answer. It publishes credits on its own. On TV recordings with a channel logo or other on-screen graphics it is about as accurate as Plex's own credits detection, not better: on its own it can skip into the story, and it can put credits on sports broadcasts, which have none (sports libraries are left out of Intro & Credits unless you tick them). Runs where the worker runs, as previews do: a GPU worker uses its GPU when a quick self-test shows it finds the same text as the CPU, and a CPU worker uses the CPU (about 10–30 s per file; 4K without a GPU up to about 2 min). A file where every frame is a keyframe (ProRes, DNxHD, MJPEG, all-intra H.264) is checked for text one frame every 2 seconds at the end; its whole ending is still read from disk, so a very high-bitrate one on a slow network share can still time out. It is then left alone for a day unless it changes or you Re-detect it. A file that stops before the length it says it has (a download cut short) has no ending to read: the job log says so, and it isn't read again until it changes.
Markers already on your servers Second opinion only — see below.

How it decides (See how decisions are made in Settings says the same). Every file goes through the same steps, and each marker type is decided on its own, so one can be sent when the other isn't found:

Markers already on a Plex, Jellyfin or Emby server only confirm another source. They never publish on their own, and they can only shorten a skip (a later intro start, an earlier credits end), never lengthen one. This is deliberate: a crowd-sourced answer that runs to the very end of the file must not swallow a scene after the credits that the server's own marker correctly stops before. Once credits are decided, a server's own detected markers can also move the credits start later — when none of them already covers the decided start and one starts more than 10 seconds later — so an "End Credits" chapter placed on the last shot of the story doesn't skip that shot; the Inspector then says "Shortened to Plex's own credits start". Intro ends are never moved this way. A Jellyfin or Emby server's markers imported by its own intro-database plugin (e.g. an AniSkip importer) never do that and don't count as an independent second opinion — they join the online-database group instead of adding a vote of their own. AniSkip itself is not a source: its intros often match IntroDB's almost exactly, and nothing in the library carries the id it needs. Plex keeps one marker set per item, so when a Plex item has another version whose length differs from this file's by more than 2 seconds (or the lengths can't be read), that server's markers aren't used for this file at all. Emby keeps each version's markers apart (see Emby), so its markers are read only from the file's own version and no such check applies. Season audio and a server's own marker never confirm each other: another source has to agree. Markers this app wrote itself never count as a server's own on Jellyfin, or on Emby while its Bridge plugin is still installed: it asks its own plugin there which markers are its. Plex records no author, so if you lose or reset the app's config folder, or remove and add the Plex server again, markers it wrote to Plex before that read back as Plex's own for those files. They never publish a marker by themselves, but they can make one shorter than it should be, be the second opinion that publishes one, or keep one from being written.

A marker you lock is never replaced by detection; to let detection decide a type again, Unlock it. See Adjusting, adding and locking markers.

Advanced → Where to look for credits (collapsed by default) sets how far from the end of a file the on-screen credit text is searched for, separately for TV episodes and Movies. Each is Automatic (the last 7½ minutes of an episode, the last 15 minutes of a movie or of a file that isn't recognised as an episode) or 5, 10, 15, 20 or 30 minutes. Automatic fits nearly every library: raise a window only if credits are being missed because they start earlier than it reaches. When the credits are already rolling where the window begins, the search follows them back two minutes at a time until it finds where they start, but only as far as a start could still be used: the last quarter of the file, and for a movie 15 minutes from the end (or your movie window, if that is longer). The first two minutes before the window are always read when the credits fill its start; on Automatic a movie is never read further back than that. A file that isn't recognised as an episode or a movie gets the movie window but not the 15-minute limit, so for it the last quarter is the limit. A window that covers your credits is still the surer choice. It never spends longer on this than the ten minutes a single decode is allowed; a file that runs out of that time (a slow or stalled network share) is tried again the next day. A longer window takes longer to decode for every file, and a window you choose also lets credits start that early: without it a start earlier than the last quarter of the file, or (for a movie window above 15 minutes) more than 15 minutes from the end, would be found and then refused. It never lets credits start before the middle of the file. Changing it reads files again on the new window (files you leave on Automatic are not read again). Locked markers are unaffected.

TheIntroDB is used without the site's written permission (its terms restrict server-side use). It's off by default, and pasting your own free key is optional. The key is masked (****) everywhere it's shown or returned by the API, and never logged.

TheIntroDB's usage line in Settings reads "N of today's lookups used · limit set by TheIntroDB". When a library job's daily lookups (or the smaller share left for full-library backfills) run out, it reads "Daily limit reached — lookups resume at 00:00 UTC". The troubleshooting table below shows what a job or file says when this happens mid-run. Files checked without TheIntroDB are tried again by the next manual or scheduled Find markers job that includes them after the limit resets. To choose when that happens, create a Find markers schedule (tick Intro & Credits) under Schedules with your preferred cron expression and libraries.

To keep the daily lookups for shows TheIntroDB knows, a show it has no entry for (talk shows, for example) is left alone for 7 days once 3 of its episodes came back with nothing, while no episode of the show has an answer from it (one saved by an earlier run counts). The 7 days run from then: a new episode that comes back with nothing meanwhile doesn't add to them. After them the show's episodes are asked again, older ones due their re-check included, and it takes 3 more with nothing to leave it alone again. The log says so once per pause, at INFO: "TheIntroDB has no entry for 3 or more episodes of <show> …; its episodes aren't looked up there until <date>". Re-detect in the Inspector always asks.

How often the online databases are asked again. A database that had no entry for a file is asked again once that answer is 14 to 28 days old (fixed per file, so a big first run's answers don't all come due in the same job), by the next job that includes the file. A lookup that has to wait for a database's rate limit waits only while the job is checking the file, never on a GPU or CPU worker: there it says "unavailable (blocked)" and the next job asks. For regular re-checks, create a schedule with Intro & Credits ticked under Schedules, choose the libraries, and set its cron expression (for example, 0 3 * * sun for Sundays at 03:00 in the schedule's timezone). Each run reuses saved answers that are still current and asks online sources again when their answers are due. The app no longer creates a weekly online re-check or a daily TheIntroDB recheck itself. Use your own schedule for recurring checks. Jobs already queued stay in the queue and can be cancelled there.

After an update that improves a detector or changes marker rules. Files use the new detectors and rules when your next manual or scheduled Find markers job includes them. Create a schedule for the libraries you want to revisit, or run Find markers after updating. Startup creates no upgrade jobs. Locked markers stay unchanged. Saved answers are reused when current, and older detector answers are refreshed. A marker already on your servers that the new rules alone would leave out stays until a new or changed answer disagrees with it; the log says "kept: published before a rule change". New files use the current rules.

When a file is replaced. A Sonarr or Radarr upgrade, or a transcode, can leave out what decided a marker (the new release has no chapters, say). When the new file is the same length as the one it replaced (within a second) and no source finds anything for a marker type in it, the marker you had keeps its place, as Plex's own markers do; the job log says "from the file it replaced". Anything the new file's own sources find is used instead, and a file of another length is decided from scratch. A marker that only season audio or on-screen credit text found isn't kept when a newer version of that same check, from an update, read the new file and found nothing there: the update passed the old answer over. The same version finding nothing on the new file keeps it, since another release of the episode can sound or look different enough.

When nothing is found

If the app can't confirm an intro or credits from the file itself, it writes nothing for that type and the file shows No markers found with the reason. If the job sent the file's other markers, the file shows Markers written instead, and its reason in the Files panel names the missing marker and why. Nothing is guessed. Re-detect in the Inspector asks every source again. Adjust lets you place the marker by hand: Add intro or Add credits puts one on the timeline for you to drag. Saving locks it, so later checks leave it alone.

Season audio and weekly releases

Season audio compares an episode with the other episodes of its season on disk: the video files in the same folder with the same season number in their names (in a folder holding more than 40 of them, the 40 nearest by episode number). When a library spans several disks and a show's season is split across them, the show's folders for that season on every disk of the library count as one season, in matching and in the Season view. The same show is told by the id in its folder name ({tvdb-…}, else {tmdb-…}, else {imdb-…}), or by the folder name when there's none. While a disk of the season can't be read, season audio waits for it rather than deciding the season without it. The first episode of a season a job checks fingerprints every member that has no fingerprint yet, on a worker (once per file; a replaced file is fingerprinted again). The rest of the season then matches from those saved fingerprints, normally without taking a worker. An episode ffmpeg can't fingerprint (damaged audio, a network read that stalls) is left out of the other episodes' matching for a day while the file stays unchanged; its own check and a re-detect still try it.

A channel logo at the very start, or music under the cold open, repeats in every episode just like the theme does, so season audio passes over such a stretch: one in the first 2 seconds must last at least 10 seconds, every one needs about 8 seconds where the episodes' audio matches closely (except one starting after the first 2 seconds that lasts 30 seconds or more, or one after the first 30 seconds that lasts 10 seconds or more and that at least 2 other episodes share: a theme under dialogue matches only in patches), and one starting in the first 30 seconds must end on the same picture in the episodes it repeats in. An online database's intro that starts in the first 2 seconds and lasts under 10 seconds (a streaming service's logo) is ignored for the same reason, and so is a server plugin's copy of one. That check decodes 3 seconds of video from the episode and two others once, on a worker (its GPU when it has one), and saves the answer. A show whose title card after the cold open lasts only a few seconds gets no intro, as with Plex's own detection. A season whose opening changes partway (an anime's second cour) gets both, and a bumper at the start of every file (a streaming service's anniversary spot) gives way to the title sequence after the cold open. An intro still playing where the fingerprinted part of the file ends (the first 15 minutes at most) is left out rather than cut short. After updating, one job decides every intro that came from season audio again (a locked one is left alone) and takes any that no longer holds off your servers.

A season can mix releases that play at slightly different speeds: European broadcasts and some web releases play the whole episode a little faster than the film release. Season audio notices this, matches those episodes at the speed of the rest of the season, then gives their intro in their own time. It listens rather than trusting the frame rate: a release that only changed its frame rate, with its sound as it was, is matched as it plays. Online databases' times come from whichever release their users timed, so on such a file they are read at the file's speed only when that is what makes them agree with the file's own evidence. The published times are always the file's own.

Saved fingerprints take up to about 28 KB per episode. When an Intro & Credits job completes (except Season, retry and verify jobs), a cleanup starts in the background, at most once an hour. It looks for up to 2,000 fingerprinted files on disk, for at most a minute, and clears the fingerprints of files that are gone (renamed by an upgrade, or deleted) while their folder is still there; the next cleanup goes on where it stopped. A file whose folder is missing, as in an unmounted library, keeps its fingerprint until the file has been marked missing for 30 days. The app log (not the job's log) says how many were cleared, and warns (at most every 10 minutes) when a cleanup is still waiting on a stalled network share.

An episode with no other episode of its season on disk yet (a new season's first weekly release) is compared with the previous season's first 4 episodes that are already fingerprinted, when its folder is named like a season (Season 2 next to Season 1). That answer never publishes on its own, and it isn't a second opinion for season audio (it's the same method on the same show). The Inspector shows it as Previous season audio.

When a new episode arrives, the season's earlier episodes may now be decided differently: their audio answer can change, or the new episode's intro chapter changes what counts as normal for the season. Those earlier episodes are refreshed when your next manual or scheduled Find markers run includes them. Choose the season's library in a schedule to revisit it regularly; no additional Season job is created automatically.

Adjusting, adding and locking markers

Open the file in the Inspector (Tools → Inspector). Adjust appears once a job has checked the file and read its length.

A save is one web request, so it is bounded. Your times are saved and locked before any server is contacted, so a server that fails can't lose your edit. Each call to a server waits at most 8 seconds, and Plex's database waits at most 8 seconds for its locks. A server the request hasn't started on within 25 seconds isn't started: its row says "Couldn't publish to this server in time; the next Intro & Credits run publishes it". These are per-call limits and a start gate, not a cap on the whole request, so a server already under way can take a few times 8 seconds. If an Intro & Credits job is running on the same file, the row says "Intro & Credits is running for this file; the next run publishes your marker". Whenever a server's row comes back waiting or failed, the save queues that next run right away: a High-priority Intro & Credits job for the file that sends your markers (it doesn't detect again), and retries a server that hasn't added the file to its library yet. Saving again before it starts reuses it.

Season view in the Inspector

For a TV episode, the Inspector has a This episode / Whole season switch. Whole season lists the season's episodes (the same group season audio uses), each with its intro (blue) and credits (orange) drawn on one shared scale, the times, the sources behind them, a 🔒 when you locked one, and one dot per server (green: shows our markers, amber: waiting, red: failed, grey: off, skipped or nothing sent yet). A season of more than 40 episodes lists the 40 nearest, and the header says so ("60 episodes (showing the 40 nearest)"). It reads only this app's own records, so a whole season loads at once; This episode stays the place for what a server shows right now. An episode with nothing found shows — (hover for the reason). Choosing a row opens that episode.

Publish N to M servers queues a Normal-priority Intro & Credits job named Intro & Credits: <show> · Season N (or · Specials) for exactly the listed episodes: decided episodes go to every server that doesn't show them yet, and the rest are checked again. Clicking it again while that job is still queued or running reuses it. N (and N ready) counts the episodes with at least one decided marker.

Searching for a show also lists every episode of a season with its state, right in the search results.

Plex: writing straight into Plex's database

Plex has no API or plugin system for markers, so this app writes them into Plex's own SQLite database, where Plex stores its own. Plex doesn't support this, so it's opt-in, with a one-time confirmation the first time you turn it on for a Plex server:

Plex on another machine: the Plex database node

If this app and Plex run on different machines, run a Plex database node next to Plex, with Plex's config folder mounted. Use the Linux image in nodes/plex-db/ (ghcr.io/stevezau/plex-marker-agent, formerly the Plex marker agent) or the Windows build in nodes/windows/. The app asks the node to do the database write on Plex's machine. It is the only supported way to write markers to a Plex on another machine; without it that Plex stays read-only for markers. If the app and Plex share a machine, don't run it.

  1. Run the node as its README says: the compose file, the settings (NODE_KEY, PLEX_CONFIG, PUID/PGID) and the key. An install from the Plex marker agent days keeps its AGENT_TOKEN.
  2. In the app, open Settings → Nodes → Add node and enter its address (for example http://plex-host.lan:9494) and key. See Remote nodes.
  3. Open Servers → your Plex → Edit → Connection → Plex database and pick the node. The one-time database-write confirmation applies when you enable Intro & Credits. Chapter thumbnails have their own opt-in under Processing and can use this node while Intro & Credits is off.
  4. The node's card on the Nodes page shows its state (Ready, Connecting…, Offline, Key refused, Update needed, Turned off). The app refuses a node version it doesn't accept on the first call, in either direction, and says which side to update. On the server's Connection tab, a node that reports a new id and can't show it sits beside the same Plex shows Not confirmed. It gets nothing until you type its key again on its Settings tab.

A Plex marker agent from before nodes moves to the Nodes page on upgrade and keeps working. For loudness it needs a /config volume: see Updating a node.

For Intro & Credits, the node writes one item's marker rows into Plex's library database. A compatible node also supports chapter thumbnails, updating only existing chapter image references. Version 1.2.0 or newer also writes loudness and runs Library Health's database counts. It refuses a database that isn't on a local disk of its own machine, a database schema it doesn't know, and a Plex with no marker list yet, with the same messages a same-machine setup gives. Chapter registration doesn't need a marker list. While the node isn't answering, markers wait in this app and the next Intro & Credits run sends them. Setup Health shows a row for it (see Setup Health).

Jellyfin: the Media Preview Bridge plugin

Jellyfin has no core API for markers either. The Media Preview Bridge plugin, the same one used for trickplay tiles, also handles markers. The Processing tab's status block shows whether it's installed, its version, and an Install / Update button that works like the trickplay install. Markers show as Skip Intro, Skip Credits, Skip Recap or Skip Preview, depending on what was decided.

Emby: the Media Preview Bridge for Emby plugin

Emby has no API for markers, so a small Emby plugin, Media Preview Bridge for Emby, stores them and writes them as Emby's own intro/credits chapter markers, next to the file's own chapters (which it never changes). It puts them back when a "Replace all metadata" or "Search for missing metadata" refresh deletes them, stops showing them once the file is replaced by a different file until this app sends markers for the new one, and forgets them when the item is removed from Emby.

The Processing tab shows whether the plugin is installed and its version. When Emby's plugin catalog lists the plugin, Install installs it and restarts Emby. Otherwise the tab offers Install by hand: download the DLL for your Emby version (MediaPreviewBridge.Emby-4.10.dll for Emby 4.10, MediaPreviewBridge.Emby-4.9.dll for Emby 4.9) from the plugin's GitHub release (tags emby-plugin-v…), rename it to MediaPreviewBridge.Emby.dll, copy it into Emby's plugins folder (/config/plugins in the official Emby container) and restart Emby. The tab shows the plugin's version with a ✓ once Emby is back. The API key or user this app uses for Emby needs administrator rights.

Multi-version Plex items

When a Plex item has more than one version (a movie in two cuts, an episode with two releases), Plex serves one marker set for the whole item, not one per file. A marker type shows up only once every version has a decided marker of that type and they agree within 2 seconds. A newly added version that isn't decided yet hides that item's markers on Plex until it catches up. Jellyfin and Emby versions each get their own markers.

The Inspector shows the version you opened, with a switch to the item's other versions that are on this app's disk.

Checking the servers still show them

A server can lose or change our markers without this app doing anything: Plex's own detection replaces them, and a server that rescans a replaced file can drop them. So before a job reports a file Up to date on a server, it reads back what that server shows — Plex's marker rows for the item in its database (read-only, with the same same-machine checks as a write), Jellyfin's served segments (one request), Emby's chapter markers (one request) — and writes ours again when they're gone or different. If that read fails, the file stays Up to date for this run, and the job finishes with a warning such as "Couldn't check what 3 file(s) show on Home Plex".

Markers written always means the job changed what the server shows (a forced Re-detect that restores lost markers included); Up to date means the server already showed exactly this.

After a webhook job (or its retry) publishes to a file that was replaced (same path, new file), servers often rescan it shortly after. The job therefore queues one Verify: … job for those files, which waits three times the first retry delay (at least 10 minutes; its queue row reads "Checking again in 10 min") and then checks them again the same way. It follows Settings → Processing options → Retry policy (none when the retry count is 0) and holds at most 500 files (the job log says how many more wait for the next run). A verify job, and any retry it queues, never queues another verify; its retries count on from the retries already used, and a file gone from disk by then isn't retried. Library runs and files you picked yourself don't queue one: their replaced files may have changed long ago.

For Jellyfin and Emby, a replaced file whose markers are unchanged is always sent again when the plugin still holds the old file's size: the plugin serves nothing for a file whose size changed (Jellyfin can still list the old segments meanwhile).

In the Inspector, a server whose markers differ from ours shows Will replace or Will add, or Keeps Plex's / Keeps Emby's when the server's own markers replaced ours and that server is set to keep them; the reason names the kept types ("Keeping Plex's credits"), also next to Will add when another type is still written. Another Jellyfin provider's segments next to ours don't count as a difference. The note "All versions of this item share one set of markers" appears only on a Plex item with more than one version (a stacked file or a Plex optimized copy isn't a version).

Every server card also says what that server shows viewers right now, e.g. "Shows now: intro 0:41–1:12 · credits 41:48–end" or "Shows now: nothing", unless its Will replace lines already say it. Markers this app sent earlier read "our intro …". A Plex marker made for an earlier file at the same path (Plex keeps an item's markers when its file is replaced) is labelled "(made for an earlier file)". Only Plex's database can tell, so it is asked briefly and read-only; when it doesn't answer in time the label is left off.

A file no Intro & Credits job has looked at yet says "Not checked by Intro & Credits yet" and lists what each server already has ("Plex already has: intro 0:41–1:12 · credits 41:48–43:10"), or "No server shows intro or credits markers for this file yet". When a server with Intro & Credits on is set to Keep Plex's or Keep Emby's, the box says those markers stay as they are. The timeline shows them as grey, read-only bars (hatched when made for an earlier file), drawn to the length the server reports. With no length anywhere there is no timeline, only the list. Re-detect checks the file.

Check servers

The checks above only run for files a job looks at. To check everything this app has published, Intro & Credits · Check servers runs by itself by default at 04:30 whenever Intro & Credits is on for a server (skipped while processing is paused). You can also run it now: POST /api/markers/reconcile. A normal Intro & Credits job already re-checks servers for the files it covers, so Check servers is the sweep of everything. It runs at Low priority unless you pick another. A marker you locked in the Inspector that a server didn't get (it was down or not ready when you saved) is published again on every run until the server has it. Only one runs at a time: asking again while one is queued or running reuses it ("A Check servers job is already queued"). Re-run on a finished Check servers job asks the same way. When the job already there is paused, the message is "A Check servers job is paused. Resume or cancel it on the dashboard." Re-run clears Pause all, as it does for any job. With Intro & Credits off on every server there's nothing to check. Check servers schedules from earlier versions are deleted at startup, with a warning in the log.

Its tooltip says: "Checks that every server with Intro & Credits on still shows the markers this app sent, and sends them again where they're missing or changed (unless that server is set to keep its own). Covers all servers and libraries." In detail:

Turning it off, or revoking the Plex confirmation

Turning the per-server switch off — or revoking the Plex database-write confirmation — takes effect immediately, including for a job that's already running: its next per-file write to that server is skipped rather than going ahead on a stale setting.

Webhook follow-ups and retries

A webhook queues each job its file needs when it arrives: previews, Intro & Credits and Plex loudness, each only where that feature is on for the file's library, and none waits for another. With Previews off for the library, no preview job is made at all. A file no library holds (often a missing path mapping) gets no job; the webhook history shows it as ignored and the log says why, with the path mapping it likely needs. Each Recently Added scan also queues an Intro & Credits job for the files it finds. The Intro & Credits job starts after the webhook delay, so the server can add the file first, and is kept across a restart. It sends markers where previews would go: a webhook pinned to one server (/api/webhooks/server/<id>, or ?server_id=), an Emby or Jellyfin webhook, a file a Recently Added scan found on Emby or Jellyfin, or a Recently Added schedule pinned to one server, publishes to that server only; anything else goes to every server with Intro & Credits on. It runs at Normal priority, or at Low when incoming jobs run at Low. If a file isn't on disk yet, a server hasn't indexed it into its library yet, or Plex didn't answer its Plex Pass check, it's retried the way preview jobs are, with the same backoff — Settings → Processing options → Retry policy → Retry count / Initial retry delay. The job stays one row in the queue: Pending, with a Retry 1/5 chip (5 retries by default) and "Retry starting in …", then Running while the retry checks the files still waiting. Files that were already done keep their results. The row turns Completed once every file is in, or Failed when the retries run out with files still waiting (the Files panel lists them). Retry now starts the waiting retry at once. A manual job or an Inspector one retries the same way. A retry resolves the path Sonarr/Radarr sent again, so a file that lands on a different disk than the first mapped one is still found. A retry batch holds at most 500 files at a time — a bigger backlog (e.g. a brand new library) is picked up on the next run instead.

Senders report one file at a time. A file that arrives while an earlier webhook's Intro & Credits job is still waiting joins it (up to 500 files) when it came from the same sender or is in the same season, so a season pack becomes one Intro & Credits job rather than one per episode.

A new episode can change what the season's other episodes should get (for example, an intro chapter that looked normal turns out far longer than the rest of the season). Other episodes wait until a manual or scheduled Find markers run includes them. The app does not create additional Season: … jobs after a run, cancellation, or restart. You can also use Publish in the Inspector's Season view to run that season explicitly. Existing queued Season jobs remain available, but do not create another job when they finish.

Reading an Intro & Credits job's log

A job starts with one line saying how many files it checks and why it ran: a webhook, a Season job, a re-check after an update, a schedule, or a Re-detect you asked for. Each step of each file is then logged as it happens, one line each. A file opens with a line naming it and what it's checked for, and every later line of that file starts with its title, so files checked at the same time can share the log and still be read one at a time. When a GPU or CPU worker reads the file, the worker's own "picked up" and "completed" lines say so. This is a film whose credit text a GPU worker read:

[2026-10-02 13:49:15] INFO - Intro & Credits job 524d043e started: 1 file, Plex webhook
[2026-10-02 13:49:15] INFO - Example Movie (2026): checking credits (films get credits only)
[2026-10-02 13:49:15] INFO - Example Movie (2026) · Checking chapters… none (asked now)
[2026-10-02 13:49:16] INFO - Example Movie (2026) · Checking SkipDB… no entry (asked now)
[2026-10-02 13:49:16] INFO - GPU Worker 1 (NVIDIA TITAN RTX) picked up: Example Movie (2026), checking credits (films get credits only)
[2026-10-02 13:49:16] INFO - Example Movie (2026) · Reading credit text on the GPU (NVIDIA TITAN RTX)…
[2026-10-02 13:49:21] INFO - Example Movie (2026) · Credit text: credits start at 1:33:27 (5.6 s)
[2026-10-02 13:49:21] INFO - Example Movie (2026) · Checking Plex's own markers… none (asked now)
[2026-10-02 13:49:21] INFO - Example Movie (2026) · Decided: credits 1:33:27–1:37:25 (credit text)
[2026-10-02 13:49:21] INFO - Example Movie (2026) · [Plex] Added credits 1:33:27–1:37:25
[2026-10-02 13:49:21] INFO - GPU Worker 1 (NVIDIA TITAN RTX) completed: Example Movie (2026) (success, 5.7 s)
[2026-10-02 13:49:21] INFO - Done: 1 file · 1 sent to Plex · 0 need review · 0 nothing found

A TV episode:

[2026-10-02 14:26:26] INFO - Example Show (2026) S01E04: checking intro and credits
[2026-10-02 14:26:26] INFO - Example Show (2026) S01E04 · Checking chapters… none (saved 2026-10-02)
[2026-10-02 14:26:27] INFO - Example Show (2026) S01E04 · Checking IntroDB… no entry (asked now)
[2026-10-02 14:26:27] INFO - Example Show (2026) S01E04 · Checking SkipDB… no entry (asked now)
[2026-10-02 14:26:27] INFO - Example Show (2026) S01E04 · Reading season audio on the CPU…
[2026-10-02 14:26:27] INFO - Example Show (2026) S01E04 · Season audio: intro 2:20–2:50 (same theme found in 4 of 4 episodes; 0 s)
[2026-10-02 14:26:27] INFO - Example Show (2026) S01E04 · Checking last season's audio… no match (asked now)
[2026-10-02 14:26:27] INFO - GPU Worker 1 (NVIDIA TITAN RTX) picked up: Example Show (2026) S01E04, checking intro and credits
[2026-10-02 14:26:28] INFO - Example Show (2026) S01E04 · Reading credit text on the GPU (NVIDIA TITAN RTX)…
[2026-10-02 14:26:34] INFO - Example Show (2026) S01E04 · Credit text: credits start at 24:20 (5.9 s)
[2026-10-02 14:26:34] INFO - Example Show (2026) S01E04 · Checking Plex's own markers… none (asked now)
[2026-10-02 14:26:34] INFO - Example Show (2026) S01E04 · Decided: intro 2:20–2:50 (season audio) · credits 24:20–25:00 (credit text)
[2026-10-02 14:26:34] INFO - Example Show (2026) S01E04 · [Plex] Added intro 2:20–2:50 and credits 24:20–25:00
[2026-10-02 14:26:34] INFO - GPU Worker 1 (NVIDIA TITAN RTX) completed: Example Show (2026) S01E04 (success, 6.4 s)

An unchanged file, checked again later, logs every line again. Each source's line says its answer was reused ("saved 2026-10-02") instead of asked, no worker is needed, and the file's last line ends "(nothing new to send)":

[2026-10-02 13:50:13] INFO - Example Movie (2026): checking credits (films get credits only)
[2026-10-02 13:50:13] INFO - Example Movie (2026) · Checking chapters… none (saved 2026-10-02)
[2026-10-02 13:50:13] INFO - Example Movie (2026) · Checking SkipDB… no entry (saved 2026-10-02)
[2026-10-02 13:50:13] INFO - Example Movie (2026) · Checking credit text… credits start at 1:33:27 (saved 2026-10-02)
[2026-10-02 13:50:13] INFO - Example Movie (2026) · Checking Plex's own markers… none (saved 2026-10-02)
[2026-10-02 13:50:13] INFO - Example Movie (2026) · Decided: credits 1:33:27–1:37:25 (credit text)
[2026-10-02 13:50:13] INFO - Example Movie (2026) · [Plex] Already up to date (credits 1:33:27–1:37:25)
[2026-10-02 13:50:13] INFO - Example Movie (2026) · done in 0.1 s (nothing new to send)
[2026-10-02 13:50:13] INFO - Done: 1 file · 0 sent to Plex · 0 need review · 0 nothing found · 1 already up to date

Every file gets all of these lines, whatever it did. A file whose result didn't change, and whose servers are up to date, logs them too. A Season: … job (and a TheIntroDB recheck) additionally counts each episode into its own season's totals, logged once every episode's lines are: "Season re-check, Brave New World S01 (3 episodes): no change, E01/E03/E04 still need review (credits from credit text only)" ("TheIntroDB recheck, Brave New World S01 (3 episodes): no change" for a recheck), and logs each movie on its own. Every job ends with a totals line, e.g. "Done: 12 files · 9 sent to Plex · 2 need review · 1 nothing found · TheIntroDB skipped for 3 files".

Every job log line starts with its date and time, [YYYY-MM-DD HH:MM:SS], in the container's local time (its TZ, or the /etc/localtime you mounted), the same clock as the app's own log.

Troubleshooting Intro & Credits

Each server's Configure → Processing → Intro & Credits section checks whether that server can receive markers right now. This table covers every state the check can report, in its exact wording:

What you see Why What to do
Green "Installed ✓" / no warning banner, status block all green Everything needed to write markers checks out Nothing. Turn the server switch on if you haven't
(Plex) No Plex Pass row, amber banner: "Can't reach Plex to confirm Plex Pass, so markers wait until Plex answers" The database checks passed but Plex didn't answer the Plex Pass check (usually restarting or updating) Wait for Plex to come back; affected files show Waiting and are retried
No status block; only the off switch "Intro & Credits is off for this server" Turn on "Send intro & credits markers to this server"
(Plex) Status block: "Confirm the Plex database write to turn this on" Plex has no marker API, so writing needs a one-time confirmation per Plex server; flipping the switch opens that confirmation dialog Read the dialog and click "Enable for Plex"
(Jellyfin) Red "Not installed" badge + Install button: "Install the Media Preview Bridge plugin" Jellyfin has no core marker-write API; the plugin renders markers as media segments Click Install
(Emby) Red "Not installed" badge + Install button: "Install the Media Preview Bridge for Emby plugin" The plugin isn't on this Emby server, and Emby's plugin catalog lists it Click Install (Emby restarts; the tab checks again after 20 s)
(Emby) Red "Not installed" · Install by hand Emby's plugin catalog doesn't list the plugin yet (or couldn't be read) Follow the manual install
(Emby) After Install or Update: "Media Preview Bridge for Emby isn't in the Emby plugin catalog yet; install it by hand (see the Intro & Credits guide)" · Install by hand / "Couldn't read Emby's plugin catalog" The catalog doesn't list the plugin, or Emby didn't answer the catalog request Follow the manual install, or try again once Emby answers
(Emby) Amber "Update needed" badge, "— installed 1.0.0.0" (the version the plugin reports; "installed version unknown" when it doesn't say) + Update button; the job's rows say "Update Media Preview Bridge for Emby (installed …) to get markers support" An older plugin without markers support Click Update, or copy the newer DLL in by hand and restart Emby
(Emby) "Can't reach this Emby server" / "Can't reach the Media Preview Bridge markers endpoint on this Emby server" A transient connection problem Confirm the server is up and reachable; recheck
(Emby) "Emby rejected this server's credentials; reconnect it" The stored API key or login no longer works Reconnect the server from the Servers page
(Emby) "Emby refused the Media Preview Bridge markers endpoint; this server's API key or user needs administrator rights" The connected account isn't an administrator Reconnect with an admin account or API key
(Emby) Emby shows "Unlock Feature" when a viewer clicks Skip Intro, or stops showing Skip Intro; the tab's amber Emby Premiere row reads "Viewers can't skip intros: this Emby server has no Emby Premiere key. Skip Credits (Up Next) still works." Emby only skips intros on a server with an active Emby Premiere key. Without one, its player shows Skip Intro for the first few episodes, asks for Premiere instead of skipping, then hides the button Add an Emby Premiere key to this Emby server. Nothing to change in this app: the markers are written either way, and the tab notices the key within an hour
(Settings) Matching audio across a season shows "Not available" with a reason, e.g. "Needs an ffmpeg with the chromaprint muxer (jellyfin-ffmpeg in the amd64 image); none was found" This container's ffmpeg has no chromaprint (the arm64 image, or a custom ffmpeg) Use the amd64 Docker image; every other source keeps working. Saved season audio answers can't help decide an intro meanwhile: such an intro is decided from the other sources, and when they can't settle it, nothing is written
(Settings) Matching audio across a season shows "Not available": "ffmpeg didn't answer the check for the chromaprint muxer; it is checked again in 10 minutes" ffmpeg timed out, couldn't start, or exited with an error when asked for its muxers (a busy or slow container start) Nothing: jobs started meanwhile match no episodes, but saved season audio answers still count, and the check runs again after 10 minutes
(Settings) On-screen credit text says "Not available" with one of: "Needs ONNX Runtime and OpenCV, which the Docker image includes; they aren't installed here" · "Needs the text detection model, which the Docker image includes; it isn't at …" (or the text recognition model) · "The text detection model at … isn't the expected file" (or recognition) · "The text detection check didn't answer; it is checked again in 10 minutes" Running outside the Docker image without the packages/model installed, a moved or corrupted model file, or the check timed out/crashed Use the Docker image, which ships the packages and model; every other source keeps working meanwhile. The last reason clears itself — the check runs again after 10 minutes
Log line "Credit text detection on <device>: CPU (<reason>)" although this host has a GPU The GPU can't run text detection in this container: the reason is e.g. "Vulkan reports no hardware GPU", "no WebGPU device is this GPU", "… GPUs have no WebGPU path in this app", "this GPU has no usable WebGPU adapter", or "WebGPU would run on llvmpipe …, a software renderer, not on this GPU" Check the GPU is passed into the container the way previews use it. arm64 has no GPU path for credit text either way
Log warning "Credits on <device>: read on the CPU until the app restarts. In its accuracy check, the GPU got N of M test pictures wrong" (and a bell card "Credits are being read on the CPU") This GPU read the test pictures differently from the CPU, and a credits answer mustn't depend on which device read the file Nothing breaks; that GPU's workers read credit text on the CPU until the app restarts, and previews still use the GPU. If the log also mentions "GPU HANG", the GPU returned nothing where the CPU found text: check the host's kernel log for GPU hangs. On an Intel Tiger Lake or Alder Lake GPU, turning on GuC may fix it: add i915.enable_guc=3 to the host's kernel options (Unraid: the append line of the flash drive's Syslinux config; other Linux: options i915 enable_guc=3 in /etc/modprobe.d/i915.conf, then rebuild the initramfs) and reboot. When opening an issue, include the "Credit text details for a bug report" line
Log warning "Credits on <device>: the GPU crashed (…) while reading credits" (or "while starting up", or "didn't answer within … s"). This file is read on the CPU and the GPU is tried again in 5 s Its GPU text detection crashed, hung or failed to start. That request, and the others of the next few seconds, are read on the CPU (the worker card shows the CPU fallback badge); the wait before the GPU is tried again doubles with each failure in a row, up to 10 minutes, and "back on the GPU after N failed requests" says when it works again. Failures in a row log once If it keeps failing, check the host's kernel log for GPU hangs (dmesg). When opening an issue, include the "Credit text details for a bug report" line that follows the warning
Log warning "Credits decoding on <device> doesn't match the reference decode: <clip> at <size>: <reason>. Credits detection keeps decoding on this GPU." Once per start, in the background when the first Intro & Credits job that reads credits starts, each GPU decodes two small test clips and compares its frames with the CPU's. This one gave other frames, couldn't decode them, or took about a minute or more. Its credits are still read on the GPU: frames a few levels off the CPU's didn't change an answer where measured (some GPUs decode older MPEG-2 and MPEG-4 files that way every time) Nothing to fix for the answers. For a decode error or a timeout, check the GPU is passed into the container the way previews use it; if the reason says frames differ, please open an issue with the line
Log line "season_audio had no answer for <file> this time: Not fingerprinting <episode>: N earlier fingerprint ffmpegs are still stuck reading their files" Earlier fingerprint reads stalled on the media mount and can't be stopped until it answers; no more are started meanwhile, so episodes that still need fingerprinting get no season audio answer (nothing is held against the files) Check the media mount (NFS/SMB) is responding. It clears on its own once the stuck reads finish; the next run matches the season
A file fails with "Couldn't read the file: Not reading <path>: N earlier ffprobes are still stuck reading their files", or a log line "season_audio (or credits_text) had no answer for <file> this time: … Not reading <path>: N earlier ffprobes are still stuck reading their files" Earlier ffprobe reads stalled on the media mount; until they finish no new ffprobe is started, so new files can't be checked Check the media mount is responding; it clears on its own once the stuck reads finish, and the files are tried again on the next run
(Plex) Red "✕ Not active" next to Plex Pass: "This Plex server has no Plex Pass, so Plex won't show any markers." Plex hides all markers — even ones already in its database — without Plex Pass Add Plex Pass to this Plex server
(Plex) "Plex's database is on a network share (…). The app must run on the same machine as Plex to write markers; Plex stays read-only." SQLite's write mode doesn't work over NFS/SMB/CIFS Run this app on the same machine as Plex, or run a Plex database node next to Plex. When the message says "where the Plex database node runs", the node's mount is the network share
(Plex) "Plex's database is on a filesystem this app doesn't recognise as a local disk (…); Plex stays read-only." The app couldn't prove the folder is a real local disk, so it refuses to risk Plex's database Check the mount; open an issue if it's genuinely local
(Plex) "Plex is running, but not with the database file this app sees at …. Mount the exact folder Plex uses, on the same machine (on unRAID, the same /mnt/cache or /mnt/user path Plex uses)." The app proves it shares Plex's live database lock before ever writing; this Plex has a different copy open Map Plex's config folder into both containers from the identical host path
(Plex) "Plex doesn't have its database open through this folder right now (Plex is stopped, or this app sees a different path to the file). Markers are only written while Plex is running." Same same-host proof, failing because nothing has the database open Start Plex; recheck the mounted path
(Plex) "Plex hasn't created its marker tag yet. Run Plex's own intro or credits detection once on any item, then try again." Markers reuse a database row Plex creates itself the first time it ever writes a marker Run Plex's own intro or credits analysis once on any item, then recheck
(Jellyfin) Amber "Update needed" badge, "— installed 1.0.0.0" (the version the plugin reports; "installed version unknown" when it doesn't say) + Update button; the job's rows say "Update Media Preview Bridge (installed …) to get markers support" An older plugin build predates the markers feature Click Update
(Plex) "Plex […] data has an unknown shape; not writing markers." / "Plex's database has more than one marker tag row, so it's unclear which one Plex serves; not writing markers." A future Plex version changed its database in a way the app doesn't recognise Check for an app update; report your Plex version in an issue
(Plex) "Plex's database was busy (held by another program) for N s; trying again on the next run" (or "Plex is busy writing its database; trying again on the next run (database is locked)", or "Another task is still using this Plex database; trying again on the next run", or "Another Intro & Credits task is still checking this Plex server; trying again on the next run") Another program kept Plex's database locked for longer than a job waits (2 minutes, or 10 seconds for a file on a GPU or CPU worker when the job retries it): Plex itself during a big scan or database optimize, or a tool that writes to Plex's database (Kometa and the like) Nothing, usually. In a job the file's row says "this job tries again in a few minutes" instead, and the job does, on the usual retry schedule (Settings → Processing options → Retry policy). Only a file still refused after the last retry stays Failed, for Check servers' next day. Cancelling the job stops its wait within a second when this app opens Plex's database itself; through a Plex database node the node's own wait (up to 2 minutes) runs out first. If it keeps happening, schedule that tool outside your Intro & Credits runs
(Jellyfin) "Can't reach this Jellyfin server" / "Can't reach the Media Preview Bridge markers endpoint on this Jellyfin server" A transient connection problem Confirm the server is up and reachable; recheck
(Jellyfin) "Jellyfin rejected this server's credentials; reconnect it" The stored token/login no longer works Reconnect the server from the Servers page
(Jellyfin) "Jellyfin refused the Media Preview Bridge markers endpoint; this server's API key or user needs administrator rights" The connected account isn't an administrator Reconnect with an admin account or API key
(Plex) "This app (user N) can't write …. Run it with the user that owns Plex's database (user M)." (or "The Plex database node (user N) can't write …. Run the node as …") A PUID/PGID mismatch between the two containers Set this container's (or the node's) PUID/PGID to the user that owns Plex's files
(Plex) "… can't write …, though it runs as the user that owns Plex's database: the folder is read-only." Plex's config folder is mounted read-only (:ro) or its folder doesn't allow writes Mount it read-write
(Plex) "Disk full: Plex's database can't take any writes (…)." The disk holding Plex's database is full Free up space on that disk
Status block fails to load: "Couldn't check this server's Intro & Credits status" (Processing tab) or "Couldn't read this server's Intro & Credits state (…)" (Inspector row) An unexpected error while checking (e.g. markers.db unreadable) — one server's failure never blocks the rest of the page Reload; check the logs for the exception type named in the message

A file's row for one server (the job's Files panel, the Inspector) can also say:

Message Why What to do
Waiting: "Can't reach Plex to confirm Plex Pass" Plex answered its database checks but not the Plex Pass check, usually while restarting Nothing; the file is retried (up to your retry count), and the next file checks Plex again
Waiting: "Not in this server's library yet" The server hasn't scanned the file in yet Nothing; the file is retried
Up to date: "Keeping Plex's intro" (or credits, or both; also added to other rows, e.g. "1 marker(s); keeping Plex's credits") This Plex server is set to Keep Plex's, and Plex shows its own markers of that type (its detection replaced ours, or they were there before this app had a record of the item) Switch it to Use ours if you want ours; the next job that checks the file writes them
Skipped: "This server was removed" The server was deleted while the job ran Nothing
Skipped: "This server is turned off on the Servers page" The server was disabled while the job ran Turn it back on, then run the library or Re-detect the file
Skipped: "Intro & Credits is off for this server" The switch was turned off (or the Plex confirmation revoked) while the job ran Turn it back on; the job's next file already checks again
Skipped: "This library isn't selected for Intro & Credits on this server" The library was switched off for Intro & Credits, or removed from the server, while the job ran Switch it back on in Configure → Libraries → Intro & Credits column
Skipped: "This file is excluded on this server" The file matches one of that server's exclude paths Remove the exclusion if it's wrong
Up to date: "Keeping Emby's intro" (or credits; also added to other rows, e.g. "1 marker(s); keeping Emby's intro") This Emby server is set to Keep Emby's, and Emby shows its own markers of that type Switch it to Use ours if you want ours
Written: "… Replaced Plex's own marker. This server is set to keep Plex's, but a marker you adjust always wins." (Emby says the same about Emby's) You adjusted or locked that marker in the Inspector, so it was written over the server's own although the server is set to keep its own Nothing; unlock the marker if you want the server's own back
Any row ending "…; Emby skips to the end of the file" The credits end before the file does (a scene follows them), but Emby's Skip Credits always skips to the end of the file Nothing; Emby has no credits end
Waiting: "Emby doesn't show which of this item's versions is this file yet; if the file is already in Emby's library, check this server's path mappings" Emby groups several versions in this item, and none of them maps to this file with its own item id Nothing while Emby is still scanning; otherwise fix the server's path mappings
Failed: "This file is Emby item 55, another version of item 53; markers not written" The job found another version's item for this file Check how Emby grouped this item's versions
Failed: "Couldn't read this server's saved settings (…)" settings.json couldn't be read just before the write, so nothing was written Check the config volume and the log; the next run tries again
Failed: "Plex's database was busy (held by another program) for N s; this job tries again in a few minutes" The write waited 2 minutes (10 seconds on a GPU or CPU worker, which previews need back) for another program (Plex itself, or a tool writing to Plex's database) to let go of it; other files that were waiting behind it say the same Nothing: the job log says "N file(s) not written to Plex's busy database yet; retry 1 of 5 in 60s", and the retry writes it. The last retry's row says "trying again on the next run": that file stays Failed, and Check servers tries it again the next day
Waiting: "Another Intro & Credits job is running this file; this job tries again in a few minutes" (or "…; trying again on the next run") Two jobs have the same file (a library job and a webhook, say): the other one kept running it for a minute, or is paused, so this job gave its GPU or CPU worker back. With no retry left to give it, the worker waits up to 15 minutes, or not at all while the other job is paused by Pause all or its schedule's stop time, and leaves the file to the next run Nothing: the log says "N file(s) not released by another job yet; retry 1 of 5 in 60s", and the retry runs the file
Job Failed: "All N file(s) weren't found on disk — check the path mappings" None of the job's files is on disk where the app looks, and no retry waits for them, so the job ends Failed as a preview job does Check the server's path mappings and that the disk is mounted
Not Found: "File not found on disk" The file is gone from its library: a series removed by Sonarr (its season folders go with it), or an upgrade that renamed the file. The job marks it missing in markers.db, and a background check after jobs finds the rest. Nothing is marked while the library's folder is missing, empty or not answering (an unmounted or stalled disk), or when a symlink is still at the path (a remote mount behind it that dropped) Nothing. Nothing about it is deleted — its markers, including ones you locked or edited, stay — so if it comes back (a disk that was only unmounted, an upgrade copied in under the same name) the next job that sees it, or the background check, takes the mark off and it is as it was. A file moved to another folder (same size and modified time) keeps everything at its new path, so nothing is detected again
Evidence detail: "Couldn't read this server's plugins, so its markers aren't used" A Jellyfin/Emby server's plugin list couldn't be read, so its markers might be a crowd database's copy Nothing; they're read again on a later run
Evidence detail: "Markers on this server look imported from …; not a second opinion for that database" That server's markers came from a plugin that imports a skip database (IntroDB/TheIntroDB, SkipDB or AniSkip), so they don't confirm that database's own answer Nothing; this is expected
Job warning: "TheIntroDB's daily lookup limit was reached: N files were checked without it. It resets at 00:00 UTC; run Find markers again manually or with a schedule after that (or add a TheIntroDB API key for a higher limit)." The source's daily budget (or the smaller share full-library backfills may spend) ran out partway through the job Run Find markers after the reset, or create a schedule to check the library at your preferred time. Add your own TheIntroDB API key for a higher limit. IntroDB and SkipDB also wait for your next run.
File reason ends with "…; TheIntroDB not checked (daily limit reached)" This file's result could still change once the source is available again — a file every other source already decided doesn't get this note Run Find markers manually or with a schedule after 00:00 UTC; nothing was stored for this source, so that run asks it again
Log: "TheIntroDB has no entry for 3 or more episodes of <show> …; its episodes aren't looked up there until <date>" TheIntroDB had nothing for 3 episodes of the show and an answer for none, so the show's episodes stop using its daily lookups for 7 days Nothing; after the date its episodes are asked again. Re-detect in the Inspector asks at once

Per-file job outcomes use plainer labels in the job queue and Files panel: Markers written (the job changed what a server shows), Up to date (every server already showed this), Waiting (a server hasn't indexed the file yet, or Plex didn't answer its Plex Pass check), Skipped (the server can't take markers right now, or a setting changed while the job ran — see the tables above for why — or the file is a trailer or other extra: "Extras aren't checked for markers"), No markers found, and No server with Intro & Credits on. When a file's servers end differently, the file shows the one that still needs something: Failed, then Waiting, then Markers written, then Up to date. So a file whose intro was written while its credits weren't found counts as Markers written (its reason names the credits), and a file written to Plex but still waiting on Jellyfin shows Waiting; each server's own result is on the file's row.

Under a job's per-server results, Decided by counts how many files each source decided, per marker type — for example "Credits: chapters 40 · credit text 9 · TheIntroDB + server markers 3". It fills in while the job runs. A file counts once per type, under the sources its marker came from; a marker a chapter set counts as chapters even when other sources agreed, and server markers (markers already on your servers) only appear as the second opinion a single source needed; your own edited markers count as your edits. Markers that need review, and files that failed or weren't checked, aren't counted. See Reference — Decided-by counts.

Note

Rolling back to a version before Intro & Credits existed needs an extra step — see Rolling back to a previous version.


HDR & Dolby Vision

HDR thumbnails are tone-mapped to SDR (standard dynamic range) automatically, so previews aren't washed out or black. Most HDR formats need nothing from you. The tricky case is Dolby Vision Profile 5 (4K Dolby Vision rips with no HDR10 fallback layer), and the one setup step you may need is on NVIDIA; see the warning at the bottom of this section.

Tip

Glossary

  • Tone mapping: converting HDR's wide brightness range to the narrower SDR range a thumbnail can show. Without it, HDR content looks too dark or tinted.
  • HDR10 / HDR10+ / HLG: the common HDR formats. All use one video track and tone-map cleanly.
  • Dolby Vision (DV): Dolby's HDR. Profile 7/8 carries an HDR10 fallback layer and works like HDR10. Profile 5 doesn't, and needs special handling.
  • libplacebo / Vulkan: the GPU library that tone-maps DV Profile 5. It 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)

Tone-map algorithm

This is the formula that compresses HDR's brightness range into SDR. The default suits almost everyone. Change it only if your HDR thumbnails look too dark or tinted.

Non-DV HDR content (HDR10, HLG, HDR10+) uses a configurable algorithm, set in Settings → Processing options → Thumbnails → 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.

Dolby Vision Profile 5

Profile 5 has no HDR10 fallback layer, so the standard tone-mapping path can't read it. The app picks the right path for your GPU. NVIDIA users, see the warning below.

The path for each GPU vendor:

Vendor Notes
Intel iGPU / Arc Uses Jellyfin's DV-aware patch
NVIDIA Needs Vulkan driver — see the NVIDIA warning below
AMD Radeon Untested locally
CPU-only fallback No hardware Vulkan, so thumbnails come out with a green and purple tint

The image ships jellyfin-ffmpeg 8.1.2 as its preferred FFmpeg because Jellyfin's fork carries a Dolby Vision tone-mapping patch that upstream FFmpeg lacks. Non-amd64 builds use the base image's FFmpeg 8.1.2.

Profile 7/8 (with HDR10 fallback) uses the standard tone-mapping chain and needs no Vulkan.

Container edge cases handled automatically

The container handles these at startup. They're listed so you can read the logs when they mention DRI symlinks or Vulkan probe retries.

Important

NVIDIA users: set NVIDIA_DRIVER_CAPABILITIES=all (or include graphics).

Dolby Vision Profile 5 needs the NVIDIA Vulkan driver inside the container, and NVIDIA's container toolkit loads it only when the graphics capability is declared. The common compute,video,utility setting is fine for everything else but not for Profile 5. Without graphics, the app can't tone-map those files and their thumbnails come out with a green and purple tint.

Fix: add -e NVIDIA_DRIVER_CAPABILITIES=all to your docker run command (or the environment: block of your compose file) and restart the container. all is what the official NVIDIA Vulkan images use. For minimum privilege, compute,video,utility,graphics also works.

If NVIDIA's Vulkan driver arrives without the small config files that point to it (as with --gpus all on some Docker setups), the app writes them itself. If the banner says none of NVIDIA's graphics driver files are in the container, see NVIDIA graphics files missing from the container.

If the banner persists, the cause is less common (a driver-version regression, a missing library in the container runtime config, or an ICD file in the wrong place). The banner names it. GET /api/system/vulkan/debug returns a plain-text diagnostic bundle to attach to a GitHub issue.


FAQ

Common questions have moved to their own page — see FAQ.


Troubleshooting

Symptom Likely Cause Fix
Job Failed (red): "N of M items skipped (file not found locally) — check path mapping configuration" No file of the job got a preview and some weren't on disk where the app looks: a path mapping mismatch between a media server and this container, or a disk that isn't mounted Verify the server's per-entry mappings in Path Mappings (each Plex/Emby/Jellyfin entry has its own list).
Job Completed with warnings (amber): "N file(s) weren't found on disk" Some files weren't on disk and the rest were fine, so the path mappings work: usually files deleted or renamed since the server last scanned Nothing, usually. The Files panel lists them as Not Found.
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. Setup Health 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.
Job waits for workers No compatible enabled group, outside group hours, unavailable GPU, or compatible workers busy Read the waiting reason. In Settings → Workers, check job permissions, hours and hardware. Loudness needs a CPU device. Outside hours normally resolves at the next opening; busy workers finish their current files.
Job shows Waiting to start Three other jobs are loading and listing their files It starts as soon as one of them has listed its files; a High-priority job goes first. Check global and per-job pause holds too.
Job shows Waiting for a free worker Every worker that handles this job type is busy It starts when one frees up. Add workers or enable a worker group for this job type under Worker groups. A paused job also shows this until it's resumed.
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 check Enable webhook processing is on (Automation → Triggers → Settings).
Log warns Webhook from sonarr: ignored '…' — the payload didn't carry a file path The sender's payload has no file path anywhere: no episodeFile, no episodeFiles[], no filePath (Sonarr's own "On Import Complete" event lists its files, so it never causes this) Check the sender posts Sonarr's standard webhook body; a custom template or a proxy that rewrites the body must keep episodeFile.path (or series.path + episodeFile.relativePath).
New files are imported but previews are not generated Plex indexing delay or wrong library mapping Increase webhook delay and check the file's library is selected on the server (Servers → Edit → Libraries) and its path mapping.
Job warning: "N file(s) still weren't indexed by the media server after N retries, so no more retries are queued. The next scheduled scan will pick them up." The media server hadn't added the file to its library by the last retry Nothing, if you have a scheduled scan: it picks the file up once the server has it. Otherwise raise Retry count or Initial retry delay (Settings → Retry policy), or check the file is under a library folder the server scans.
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 in the Workers header. Global pause is persisted and pauses in-flight processing. Group reductions and closing worker hours instead let current files finish.
DV Profile 5 thumbnails have a green and purple tint, and the log warns that no working Vulkan device was found The container can't reach a hardware Vulkan device, so the app skips Profile 5 tone mapping and extracts plain frames instead 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; if they're still tinted, also start the container with --runtime=nvidia. Most users already pass /dev/dri for hardware video acceleration, which brings the Vulkan driver along for free.

Validate Plex Config Path

ls -la "/path/to/Library/Application Support/Plex Media Server"

Expected directories include Cache, Media, and Metadata.

Debug Logging

For a persistent problem, open Settings → Logging, set Log level to DEBUG and click Apply Log Level Now. Or set LOG_LEVEL=DEBUG as an environment variable (a one-time seed on first start).


Rolling back to a previous version

Schema downgrades are not automated. To revert from a release that ran a settings migration, do it by hand:

  1. Stop the container.
    docker stop media-preview-generator
    
  2. Restore the backups from your config volume. Before each save, the app copies the JSON file it is about to change to <file>.<timestamp>.bak next to it, for example settings.json.20261002-071500.bak (UTC). An upgrade that changes the settings format copies settings.json before it starts, and the Settings migrated notice names that copy. Use the newest backup from before the upgrade:
    cd /your/config/dir
    ls settings.json.*.bak                                # find the one from before the upgrade
    cp settings.json.<timestamp>.bak settings.json        # required
    cp schedules.json.<timestamp>.bak schedules.json      # if you use Schedules
    

    Only the newest 10 backups of each file are kept by default (Settings → Backups), so copy the pre-upgrade one somewhere safe if you may roll back later. The app only removes backups it made itself, never another .bak in the folder. jobs.db has no such backup. To recover an older job database, restore your full config-volume snapshot.

  3. Start the older app version that wrote those files.
    docker run ... your/image:older-tag
    

Warning

Rolling back from a version with Intro & Credits (settings schema v15 or newer). The older binary doesn't know about the intro_credits job kind — it treats every job row it finds the same way, so a leftover Intro & Credits job comes back as a full preview scan of that job's libraries instead of quietly disappearing. Before you stop the container, cancel any pending or running Intro & Credits jobs from the Dashboard, or run this against your config volume's jobs.db (check web/jobs.py's jobs table — columns kind and status — before running anything against a database you haven't looked at):

sqlite3 /config/jobs.db "UPDATE jobs SET status='cancelled' WHERE kind='intro_credits' AND status IN ('pending','running')"

This only stops the surprise scan; it doesn't restore markers already written to Plex or Jellyfin, and the older binary can't write or read them either way.

A markers.db at schema 3 is refused by older versions. A version from before it logs "markers.db was created by a newer version" and its Intro & Credits can't open the database; your settings and previews are unaffected. Before migrating it, the app copies it once to markers.db.pre-v3.bak next to it (in /config). To roll back, stop the container and put that copy back as markers.db; what Intro & Credits stored since the migration is lost with it.

Intro & Credits schedules are worse: the older binary runs each one as a full preview scan on every tick. While the new version is still running, open Automation → Schedules and delete (or disable) every schedule with the Intro & Credits badge. Both remove it from the scheduler's own database (scheduler.db) as well as from schedules.json. Editing schedules.json by hand isn't enough, because the older binary also starts the jobs already stored in scheduler.db, and there's no reliable command for those. Then keep the current schedules.json: in step 2, don't restore a schedules.json backup from before that change, or the schedules come back.

Multi-server caveat. A multi-server install can't downgrade to a single-server release without losing the second and third servers' settings, because the newer schema holds data the older one can't represent. The older binary refuses to start against a newer settings.json, and its log message says where the backups are.

Why it refuses to start. An older binary would drop unknown fields on the next save, which once wiped a user's job history. Refusing to start is loud and recoverable; silent truncation is not.


Support

Open a GitHub Issue.


Next Steps


Edit this page on GitHub
Type to search the documentation.