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.
On this page
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 Health (per-check toggles & explanations)
- Remote nodes
- Webhook Integration
- Auto-trigger from Plex (no Sonarr/Radarr)
- Intro & Credits
- HDR & Dolby Vision
- Troubleshooting
- FAQ
Related Docs
Web Interface
Setup Wizard
The first time you open the web interface, a Setup Wizard walks you through setup for Plex, Emby and Jellyfin:
- 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).
- Connect. Finish the selected server's connection. Plex includes server and library selection; Emby/Jellyfin manage libraries later from Servers → Edit → Libraries.
- 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.
- Options. Named CPU/GPU worker groups with allowed jobs and availability, per-device FFmpeg threads, thumbnail interval, and quality.
- 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
- Start the container
- Open
http://YOUR_SERVER_IP:8080 - Get your authentication token using Authentication Token
- 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
- System — each media server (Plex, Emby, Jellyfin) with its connection state, one line per worker group with its worker count, and the app version. With no server set up, a card links to the Servers page.
- Quick actions — Start new job, Process a file or folder, Run next schedule now, and links to the Inspector, Webhook activity and Logs.
- Job statistics — Pending, Running, Completed, Failed, Cancelled and Total tiles, an outcome bar and the next schedule.
- Workers — one card per worker group, with a row and − / + stepper for each device (including devices on nodes), and Pause / Resume and Manage groups in the header.
- Jobs — the job queue, with status tabs, search, filters and per-job controls.
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:
- Added to library applies to all selected media: any time, the last X days, or a date range including both endpoints, in the app's timezone. It is the date Plex, Emby or Jellyfin added the item, not its release date or file modification time.
- TV shows can keep all seasons or the most recent X available seasons per show. The default count is 1 when you enable the limit. Season numbers decide which are latest; an ended show's final season still qualifies. Specials are excluded when limiting seasons.
- Movies can keep all release years or use an inclusive year range. Leave either bound blank for an open-ended range, such as 2020 onwards.
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:
- Search. Type a show, movie or episode name. The app searches your enabled servers and lists matches grouped by Shows / Movies / Episodes, each tagged with the server(s) it came from. Pick a show to process its episodes, or a movie or episode for just that file. The path comes from the server, so you don't need to know the in-container path.
- Browse. Open the folder picker and select a folder (every video inside) or one video file.
- Paste paths. The collapsible box takes one absolute container path per line.
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):
- Pause stops all processing: no new jobs start (manual, scheduled or webhook) and the current job dispatches nothing new. Files already mid-process stop where they are and carry on when you resume. Nothing is killed or lost.
- Resume clears your manual global pause. A global pause schedule window can still hold processing until it ends.
- The global control sits in the Workers header, beside Manage groups. Pause shows while there is work to hold; Resume shows whenever processing is paused. The state survives restarts.
- Each pending or running job has its own Pause and Resume, including Previews, Intro & Credits and loudness. Pausing one job does not pause other jobs. Manual and schedule holds are independent: resuming by hand cannot clear a schedule stop, and a schedule start cannot clear a manual pause. Global Resume does not resume a job you paused on its own.
- A Previews job you pause by hand lets its running files finish and starts no new ones. Resume carries on. A schedule stop time or the global pause still stops the running files where they are.
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:
- Job type: any of Previews, Intro & Credits and Plex loudness, as in Start new job. Each tick queues its own job.
- Libraries: all, or specific ones.
- Files: Whole library, or Recently added (see Auto-trigger from Plex). Recently added makes previews only if Previews is ticked, then queues the other ticked types for the files it finds.
- Schedule type: Specific Time, Interval or Cron Expression.
- Stop time (optional): pauses the schedule's running jobs at that time of day.
- Job Priority, and for a whole library Processing Order (Previews and loudness) and the same media filters as Start new job.
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.
- Library Health check counts what every library still needs, by default at 03:00. After it, it starts a low-priority catch-up job for each Plex library with 500 files or fewer still to do. You can change its time, turn the daily check off, and turn the catch-up jobs off. The 500 limit is fixed. Catch-up jobs show "Library Health" as their source in the job list. Check now on the Library Health page works even when the daily check is off.
- Check servers puts back markers a server lost. You can change its time or switch it off. It sits idle while Intro & Credits is off everywhere. Run now works even when it is off.
/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.
- Workers: named groups, job permissions, counts and availability.
- Nodes: other machines that lend a GPU or CPU, or open Plex's database (see Remote nodes).
- Global pause schedule: weekly windows when all processing pauses.
- Processing options: incoming job priority, retry policy, restart recovery, GPU device tuning, library scanning, thumbnails and HDR tone mapping, smart caching across servers.
- Intro & Credits: detection settings shared by every server.
- Logging: log level, log file rotation, job history.
- Authentication, Backups and About.
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:
- Name
- Devices: up to 8, each the CPU or one detected GPU, once per group. Devices are listed under This app first, then under the name of each node. Each device has its own:
- Workers: 1 to 64. A worker is one simultaneous task, not a CPU core or a share of CPU usage.
- Jobs allowed: Video previews, Intro & Credits, Plex loudness.
- Availability: Always available, or Weekly hours with one or more time windows. It applies to every device in the group.
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.
- A group's card shows its state: Available, Workers busy, Outside hours, Finishing current files or Globally paused. No open workers means waiting for capacity, not a global pause: current files can still finish.
- Counts from different open groups add, including groups that use the same device. Overlapping windows within one group don't multiply its workers. Finishing files still use the shared budget, so a newly opened group may wait for them. The weekly peak is capped at 64 CPU and 64 GPU workers per machine (this app, and each node on its own); the editor shows the current and peak totals.
There are three separate timing controls:
- 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.
- 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.
- 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:
- Retry count: 0 to 10, default 5. 0 turns retries off.
- Initial retry delay: 10 to 300 seconds, default 30. At 30 s the waits are 1 min, 2 min, 5 min, 15 min, then 1 h each; other values scale those waits.
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.
- 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.
- 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.
- 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.
- 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:
- Ready: chapter images are complete and registered with Plex.
- Waiting for Plex: Plex has not indexed the file or analyzed its chapters yet. The detail names what is missing.
- Failed: chapter extraction or registration failed. The detail gives the error, even when a retry is allowed.
- No chapters: there are no chapter images to generate.
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:
- a codec the GPU's decoder doesn't support
- a hardware error (CUDA sync or transfer failure, VAAPI surface exhaustion, an OpenCL filter failing)
- FFmpeg stopped from outside (out of memory, a stalled or failing read)
- a crash before 95% of the thumbnails were written (past that, the thumbnails it wrote are published)
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:
- Worker groups cap how many previews generate at once (heavy FFmpeg and GPU work). Match these to your hardware.
- Library Scanning → Files checked at once (
scan_workers) caps how many files the existence check handles in parallel. This is light disk I/O with no FFmpeg or GPU load.
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:
- Triggers: incoming webhooks from Radarr, Sonarr and Sportarr, Tdarr, FileFlows and custom scripts, and how to hook up Plex, Emby and Jellyfin.
- Schedules: recurring job starts and optional stops (cron, interval or specific time). A link jumps to the global pause schedule. Worker hours live in Settings; see Worker groups and availability.
The Triggers tab has:
- Publish to which server? scopes the copy-ready *arr and custom webhook URLs to one server.
- Enable webhook processing, the master toggle.
- Delay before processing, the seconds to wait after an import so the server can index the file.
- Webhook Secret, an optional dedicated authentication token (Generate).
- Setup instructions for each source.
- View webhook activity, a link to Tools → Webhook Activity.
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.
- Search. Results appear as you type, from every server (or the one picked in the dropdown). A
film several servers have is one row. Each row says whether the preview is ready (and how many frames it has) and
what Intro & Credits has for it: Intro + credits, Credits set, Not checked yet or Nothing found. A TV
show opens in place: pick the season, then the episode, each with its own state. A path starting with
/opens that file directly. Choosing a file folds the results away; Results for “…” brings them back. - Links.
/inspector?path=<file>opens a file and can be bookmarked. The eye button on a job's file rows opens the Inspector on that file. The old/bif-vieweraddress redirects here. - The file. Four tiles sum it up: what your servers show, what the app found, the preview (how many frames, how far apart, and where it stops if it is shorter than the video), and when Intro & Credits last checked it.
- Timeline. One strip of every preview frame, each labelled with its time. Scroll it, drag it, use ‹ › to step ten frames, or click the bar above it to jump; Jump to goes straight to the intro, the credits or a server's own markers. Under the strip, on the same scale, We found shows the app's intro (blue) and credits (orange), then one row per server shows what that server gives viewers: ours tinted, a server's own markers outlined in grey. Click a frame to see it large; ← / → step and Esc closes. Places the preview doesn't cover say No preview.
- How it was decided lists every source, what it found and whether it was used. On your servers says, per server, what it shows, its intro & credits state and its preview; File locations opens to each preview's path (Plex's bundle BIF, Emby's BIF next to the video, Jellyfin's trickplay folder).
- Loudness shows each Plex audio track's integrated loudness, true peak, loudness range, threshold and gain offset, and whether Plex can normalize the track. These are Plex's own measurements, shown even when this app's loudness feature is off. Missing or incomplete results are labelled. Opening the Inspector starts no analysis; use New job → Plex loudness for that.
- A file not checked yet is the same page: the rows show what each server has today, and Check intro & credits now checks it.
- Regenerate preview rebuilds the preview for every server that has the file (a job on the Dashboard); Re-detect intro & credits asks every source again. While a job works on the file, a bar says so and shows its progress; the page reads the file again when the job ends.
- States. A path in no server's library says Not in any library; a file that has gone says Gone from disk. A server that can't be reached marks only its own row. An item with several files shows a switch between them.
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.
- The number. The bold number is how many files still have work to do. For Intro and Credits, to do means nobody has looked yet; many shows have no intro. The striped part of the bar is files with nothing left to do: for Intro and Credits, the app looked and found none; for Chapters, files with no chapters in the video, plus files that can't get thumbnails (a chapter that starts after the video ends, a version split across several files, one the chapter step gave up on until the file changes, a title Plex shows from another version, or a video no preview job could decode). Previews, Loudness, Intro and Credits count can't be read for a file a job found broken (a protected or unknown video codec, no frames, damaged audio, no readable length); it counts again once the file changes. Loudness doesn't count files with no audio.
- Other cell text. Off for this library means the feature is turned off in Servers. Not used for films is Intro on a movie library. Plex only is Loudness on Emby or Jellyfin.
- Click a number to list those files. From the list you can open a file in the Inspector, or start the matching job (previews, loudness, or Intro & Credits) for exactly those files. Past 1,000 files, the job covers the whole library. Chapters starts preview jobs for exactly those files; previews that already exist are skipped.
- Checking only reads. It never changes your servers or files.
- When it runs. Check now runs it, and it runs every night at 3:00 AM.
- Nightly catch-up. After the nightly check, a Plex library with up to 500 files still missing previews or chapter thumbnails gets one low-priority job for exactly those files (for example, chapter thumbnails Plex dropped when it re-read an upgraded file). A library with more is left to your own scan or schedule.
- How long it takes. Plex with access to its database, directly or through a Plex database node, takes seconds, even for 100,000+ files. Without that (for example, Plex's folder on a network share such as Docker Desktop's mount of a Windows folder), only previews are counted. Jellyfin Intro and Credits and Emby previews take longer, because they need one request or folder read per file. Until a check finishes, they show the last check's numbers.
- "N previews are made but Plex isn't showing them." The preview file exists, but Plex hasn't re-read the video since. Review & fix asks Plex to re-read those videos one at a time, in the background. Plex can take a few seconds each, so thousands take hours. No previews are made again. A re-read clears chapter thumbnails, so if those are on, a preview job adds them back afterwards. The bell at the top of every page shows the same notice with a link to the fix; dismiss it there if a few never clear. A failed check shows in the bell too.
- "N show in Plex after its maintenance window." Asking Plex to show a new preview also starts every Plex job set to "As a scheduled task": intro markers, credits markers, loudness analysis, voice activity detection and chapter thumbnails. So the app waits for Plex's maintenance window (Plex Settings → Scheduled Tasks) and asks then. Nothing to fix. To see new previews within a minute, set those jobs to "As a scheduled task and when media is added"; Setup Health offers the change. A server this app makes chapter thumbnails for is asked straight away, because those need Plex to re-read the file first.
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:
- Auto-generated token: created on first run and saved to
/config/auth.json. - Custom token: set your own in the setup wizard (step 5).
- Fixed token: set the
WEB_AUTH_TOKENenvironment variable. It overrides the wizard setting. - Token masking: logs show only the last 4 characters of a token.
- 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. - 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/loginand reusing the cookie doesn't work for changes. See Reference — Authentication. - 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.keyfrom the config folder (or changeFLASK_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:
- Worker: lends its GPU or CPU for video previews, Intro & Credits detection and loudness. It must be able to read your media. It never gets a server token and writes nothing outside its own temp folder.
- Plex database: runs where Plex runs (Linux or Windows) and opens Plex's database for Intro & Credits, chapter thumbnails, loudness and Library Health counts. It replaces the Plex marker agent.
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
NODE_NAMEis the name the app shows for it.- Mount your media read-only. If you mount it at the same path as in the app's container, the mappings Add node fills in work unchanged.
- Pass the GPU the same way you pass it to the app. For NVIDIA, use
--gpus all -e NVIDIA_DRIVER_CAPABILITIES=all(Dolby Vision tone mapping needs the second). Keep--device /dev/dri:/dev/drionly for an Intel or AMD GPU: without/dev/drithe container doesn't start ("error gathering device information"). - The node makes its own key and prints it once in
docker logs. To pick your own, add-e NODE_KEY=...with a key fromopenssl rand -hex 24, or put it in a file and setFILE__NODE_KEYto the file's path. - A compose file is in
nodes/worker/.
Adding it
- Settings → Nodes → Add node: enter a name, the node's address (
http://machine:9494) and its key, then click Connect. - 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.
- 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
- Keep
--device /dev/dri:/dev/drionly for an Intel or AMD GPU. - Set
PUIDandPGIDto the user Plex runs as. The node writes Plex's database as that user. - Don't mount Plex's config folder read-only.
- Add it once under Settings → Nodes, then use it in a worker group and as your Plex's Plex database.
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.
- Radarr/Sonarr imports a file (or an external tool sends a custom webhook) and a POST is sent to this app.
- The app queues the file and starts (or resets) a timer. Imports from the same source (Radarr, Sonarr, or Custom) and server selection are batched together.
- Each accepted new file resets the timer using the URL's
delayparameter, 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. - 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.
- 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.
- 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
- Media Preview Generator running with the web UI accessible
- Radarr and/or Sonarr installed and managing your media (for Radarr/Sonarr webhooks)
- At least one media server in Servers (Plex, Emby or Jellyfin, in any combination). Webhook work is published to every server that owns the file.
Configure Radarr
- Open the web UI and navigate to Automation → Triggers tab (in the top nav)
- Copy the Radarr Webhook URL
- In Radarr, go to Settings → Connect → + → Webhook
- Set Name:
Plex Previews - Set URL: paste the Radarr Webhook URL
- Under Events, enable:
- On Import
- On Upgrade
- Authentication (use one):
- Username/Password (works in all versions): leave Username empty and set Password to your API token (see Authentication Token) or webhook secret. The app treats the password as the token.
- Custom headers (if your webhook form has a Headers section): Add Key =
X-Auth-Token, Value = your API token or webhook secret.
- Click Test to verify the connection
- Click Save
Configure Sonarr
- Copy the Sonarr Webhook URL from the web UI Automation → Triggers tab
- In Sonarr, go to Settings → Connect → + → Webhook
- Set Name:
Plex Previews - Set URL: paste the Sonarr Webhook URL
- Under Events, enable On File Import and On File Upgrade. 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.
- Authentication (use one):
- Username/Password (works in all versions): leave Username empty and set Password to your API token or webhook secret. The app treats the password as the token.
- Custom headers (if your webhook form has a Headers section): Add Key =
X-Auth-Token, Value = your API token or webhook secret.
- Click Test then Save
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.
- In the web UI, open Automation → Triggers and copy the Custom Webhook URL.
- In Tdarr, open the Flow you want to trigger previews from
- Add a Send Web Request plugin after your transcode step
- Configure the plugin:
- Method:
POST - Request URL: paste the Custom Webhook URL (e.g.
http://your-server:8080/api/webhooks/custom) - Request Headers:
{"Content-Type": "application/json", "X-Auth-Token": "YOUR_TOKEN"} - Request Body:
{"file_path": "{{{args.inputFileObj._id}}}"}
- Method:
- Save the Flow
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.
- In FileFlows, install the Web plugin (Plugins page → search "Web") so the Web Request node becomes available
- Open the Flow you want to trigger previews from
- Add a Set Variable node just before 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}
- Variable:
- Add a Web Request node after the Set Variable node:
- Method:
POST - URL: paste the Custom Webhook URL (e.g.
http://your-server:8080/api/webhooks/custom) - Content Type:
JSON - Headers: key
X-Auth-Token, valueYOUR_TOKEN - Body:
{"file_path": "{output_file}"}
- Method:
- Save the flow
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:
- On Automation → Triggers, click Generate next to the secret field. It saves automatically.
- 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:
- An active Plex Pass subscription on the server-owner account. Plex webhooks need Plex Pass.
- Mobile push notifications turned on in your Plex server. Plex sends its
library.newevent through the same code path as mobile push, and drops library events when push is off. Turn it on under Plex Web → Settings → General (Enable mobile push notifications). You don't have to use mobile push.
Setup:
- Open Servers → your Plex → Edit → Webhook & Scanner.
- 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.
- Click Register with Plex. If your account has no Plex Pass, the page says so.
- 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:
- 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.
- Under Files, choose Recently added. Pick your Schedule type (an interval suits a scanner) and frequency.
- Choose a Lookback window: 15 minutes, 30 minutes, 1 hour (default), 2 hours, 6 hours, 24 hours, 3 days or 7 days.
- 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:
-
Agreement. Two answers agree when their intro ends are within 5 s, or their credits starts within 10 s. When sources agree, the one higher in your order supplies the times. Once a type is settled by anything other than chapters alone or season audio alone, the sources after it are skipped as "not needed"; your servers' own markers are always read.
- Chapters in the file decide on their own, unless two other independent sources agree on something different, or an intro chapter is much longer than the rest of its season's (then one other source has to agree). When SkipDB's credits disagree with a credits chapter, the on-screen credit text is read in the same run: if it agrees with SkipDB, those two decide, starting where the credit text does; if not, the chapter stands. The credit text also reads every file whose credits a chapter decided: when the chapter is off the credit roll (inside it, or on the story before it), the skip starts at the first credit text the frames show and still ends where the chapter does. When an online answer ends an intro inside an intro chapter, the episode waits for season audio: if season audio agrees with the online answer, those two decide.
- When sources agree on credits, the start comes from the first of them in your order, except that on-screen credit text supplies the start when it is more than 5 s from that one's: the online databases were timed on other releases.
- An online database's answer is published once an independent source agrees with it: on-screen credit text, season audio, another database, or a server's own marker. IntroDB and TheIntroDB count as one, because IntroDB's data looks partly copied. Their credits may end up to 5 s past the end of your file (they were timed on a slightly longer release); the skip then stops at the end of the file.
- A single answer decides alone only when it checks your file's own cut: on-screen credit text (credits) or season audio (intros, so a show no online database has still gets them). A lone IntroDB or TheIntroDB answer never publishes, because neither knows which cut of the file you have. Nor does a lone SkipDB answer: matching your file's length finds the right cut but not the right edges (its credits often start minutes before the real credit roll, and some of its intros cover only part of the title sequence). The previous season's audio (a season's first episode) never publishes alone.
- Nothing found: no source answered, the answers didn't clear the bar above, or every answer failed the sanity checks: an intro starting after 35% of the file or running to its end, or credits starting before the last 25%. If it can't confirm an intro or credits from the file itself, it writes nothing for that type; the file's other marker type is still sent, and you can add or adjust one in the Inspector.
- Sending. Each server gets the decided markers. With Keep Plex's or Keep Emby's on, that server's own marker of a type stays unless it's impossible for the file (outside it, or under 3 s).
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.
- Adjust opens under the Timeline with frames read straight from the video, one second apart, around each edge. Click the frame where an edge belongs, step it with ‹ 1s / 1s ›, or type the time. Credits (and a preview) can run to the end of the file or end earlier. Save and send to … saves; Cancel throws the edit away. Only two things are refused: a marker outside the file, and one that ends before it starts.
- Add a marker where nothing was found. In Adjust, an intro or credits nothing was found for has Add intro or Add credits. It starts from times that are round on purpose, so they can't be mistaken for something the app found: an intro from 0:00 to 0:30, credits the last 60 seconds. Move its edges to where it really is.
- Saving locks. There is no adjusted-but-unlocked marker. A saved marker is locked and published straight away to every server that has the file and has Intro & Credits on. Detection and later jobs leave it alone.
- Lock (beside Adjust) locks the times the app already decided, unchanged, and publishes them. It lists the times and where they go first (Lock and send to … / Leave them as they are); a type no server with Intro & Credits on could show is left out.
- Back to automatic replaces Lock once anything on the file is locked. It asks first, then drops the lock. Your times stay on the servers for now; the next check decides those types again and may move them, and Re-detect does that straight away. It publishes nothing.
- Your marker wins over "Keep Plex's" and "Keep Emby's". See Plex and Emby for the row message.
- Recap and preview a job decided stay editable in Adjust; only Jellyfin shows them. Emby and credits: an edited credits end is accepted, but Emby's Skip Credits always skips to the end of the file, and the message after saving says so.
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:
- The app must run on the same machine as Plex, or you run a Plex database node next to Plex. SQLite's write mode (WAL) doesn't work over a network filesystem, so a network-mounted database stays read-only. See Plex on another machine.
- Map Plex's config folder into both containers from the identical host path. On unRAID, don't mix
/mnt/userand/mnt/cachebetween the two containers. Both reach the same files, but the app proves it shares Plex's machine only if both open the database through the exact same path. - Plex Pass is needed on the server and for viewers (or their Plex Home). Without it Plex hides all markers, even ones already in its database.
- If Plex's own detection re-analyzes an item, it can replace our markers with its own. The next Intro & Credits job that checks the file notices (see Checking the servers still show them): with "When Plex has its own markers" → "Use ours" (the default) it writes ours again; with "Keep Plex's" it leaves Plex's markers of that type, and the file's row says so, e.g. "Keeping Plex's credits" or "1 marker(s); keeping Plex's credits". This is decided per type: Plex's intro can be kept while our credits are still written. Once kept, no job touches Plex's markers of that type — not a forced Re-detect, not a run after a failed or skipped attempt, not another version of the item — until you switch the server to Use ours (the next job writes ours) or Plex no longer has markers of that type (then ours are written again). Markers that are simply gone are written again either way. A file Plex already has its own markers of a type for isn't read again for that type (no credit-text or season-audio pass), as long as every server the file's markers go to keeps its own and shows one: the row says "Keeping Plex's credits" and the Inspector "Kept Plex's own marker". Check servers still watches that marker: once Plex no longer has it, or the server is switched to Use ours, it runs the file, which then reads it and writes ours.
- Keep Plex's only keeps Plex's markers of a type that can be right for the file. When every one of them is impossible — it starts or ends after the file does, or has no length — Plex hasn't really processed the file: the file is read for that type and ours are written. Where on the file Plex put a marker is never second-guessed.
- Plex keeps an item's markers when its file is replaced (a Sonarr or Radarr upgrade, say), so they can be the old file's. A type counts as made for an earlier file when Plex's database has no record of detecting it on the file it has now and its markers are older than that file. Such a type doesn't stop the file being read, and Plex's marker isn't used as a second opinion. When the file gives an answer, ours replaces Plex's marker (row message e.g. "Replaced Plex's intro: it was detected for an earlier file"); when it gives none, Keep Plex's keeps Plex's, which is still the closest there is. The job log marks those markers (made for an earlier file). When Plex's database can't tell this time (busy, or an older Plex database node), every Plex marker counts as before and the next run asks again.
- Don't turn off a library's own Intro markers / Credits markers setting (Edit library → Advanced). While it is off, Plex hides every skip marker of that type in the library, ours included, so nobody sees a skip button there. Servers → Edit → Setup Health shows such a library under Must fix with a Turn on button. To stop Plex detecting on its own, set its server-wide Generate intro video markers / Generate credits video markers (Settings → Library) to Never instead: Setup Health offers Set server-wide to Never for that. With Keep Plex's it doesn't suggest it. See Setup Health.
- A marker you adjust or lock in the Inspector is the exception: it is written over Plex's own markers of that type even on a server set to Keep Plex's, and the server's row says so — "Replaced Plex's own marker. This server is set to keep Plex's, but a marker you adjust always wins." The Inspector says it before you save, too. Emby works the same way with Keep Emby's. Unlock the marker and that type goes back to the setting.
- With "Keep Plex's", markers Plex already shows of a type this app has no record of writing on that item are
kept too, unless they already match ours: Plex's own markers from before Intro & Credits was turned on, markers
Plex filled in after this app removed its own, and markers on an
item after the app's Intro & Credits data was reset or the Plex server was removed and added again (the app can't
tell those from Plex's own). What's kept is remembered per Plex item in the app's
markers.db; after such a reset it is worked out again from what Plex shows. - Tested against Plex 1.43. If a Plex update changes the database's shape, the app stops writing and shows a message.
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.
- 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 itsAGENT_TOKEN. - In the app, open Settings → Nodes → Add node and enter its address (for example
http://plex-host.lan:9494) and key. See Remote nodes. - 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.
- 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.
- What Emby shows: the tab's "Can show" reads Intro · credits start. Emby has no credits end: its Skip Credits button always skips to the end of the file, including any scene after the credits. The app still sends the decided credits start; when those credits end before the file does, the file's row and the Inspector's Emby card say "Emby skips to the end of the file". Recaps and previews aren't sent to Emby (it has no button for them).
- "When Emby has its own markers" works like Plex's setting. Emby can have intro and credits markers of its own, from its intro detection (an Emby Premiere feature) or another plugin. Use ours (the default) replaces them with ours. Keep Emby's leaves a type Emby already has and sends ours only for the other types; the row says so, e.g. "1 marker(s); keeping Emby's intro", and the Inspector shows Keeps Emby's. The plugin still keeps ours for a kept type and shows them once Emby's are gone.
- Versions: Emby keeps each version of a video (a movie in two cuts, an episode with two releases) as its own item with its own chapters, and its player shows the chapters of the version playing. So each version gets the markers decided for its own file. A file that Emby lists under another version's item isn't written: "This file is Emby item 55, another version of item 53; markers not written".
- Emby Premiere: Emby only lets viewers skip intros when the Emby server has an active Emby Premiere key: without one, Emby shows "Skip Intro" a few times and then asks for Premiere instead of skipping. The credits "Up Next" prompt works either way. The key is the server's, so it covers every user and app on that server. When the server has no key, the tab's amber Emby Premiere row says so.
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:
- It reads back every item whose last publish from this app succeeded, on each server with Intro & Credits on (Plex one item at a time, with the same same-machine checks as a write; Jellyfin and Emby one request per item), 500 items per step so its progress moves. Pausing the job during the read-back holds it until you resume.
- Only the files of items whose markers are gone or different, whose Plex item gained or lost a version, or that the server replaced with a new item go through the normal rules again (Keep Plex's / Keep Emby's, versions, the server switch). A server set back to Use ours gets ours for the types it was keeping. The Plex item's current versions go along too: a file that replaced the one this app published there (an upgrade that deleted the old file) gets its own markers on that item, and markers decided for the old file come off.
- Items whose last publish failed (Plex's database still busy after a job's retries, a server that stopped answering mid-write) aren't read back; their files run again 1 day after the failure, then 2, 4, 8 and 16 days after each retry that fails too. After 5 retries they wait for the next job that runs those files. A publish that works, or a new failure after one that did, starts over.
- It also re-reads a server's own markers for files with decided credits where that server had none (or its answer couldn't be used) and shows none of ours, since its own detection may have run since and can shorten our credits (see Sources and the publish rule). Every enabled server counts, with Intro & Credits on or not. The re-read waits until the answer is 1 day old, then 2, 4, 8 and 16 days after each re-read that stays empty or fails; after 5 such re-reads it stops.
- One run takes at most 500 files; the job warns "N more changed file(s) are checked on a later run", and waiting items take turns across runs. Whatever is still waiting is listed again next run, with one exception below.
- An item the server no longer has, with no file here that still belongs to it, is dropped from Check servers quietly. When a listed file's row is Waiting because it isn't in that server's library yet, and that server confirms the item is gone, the file gets one retry job, the same one any job queues for a file not in a server's library yet (same delay and retry count), since the server may not have added the file's new item yet. Once that retry is queued the item is dropped too, until this app publishes to it again. If the job is cancelled or fails first, or retries are off, nothing is dropped and the next run checks the file again. If the server doesn't confirm the item is gone, there is no retry and the file is listed again next run.
- A server that can't take markers is skipped with its reason ("Skipped Home Plex: …"). Items whose read fails get a warning ("Couldn't read what 3 item(s) show on Home Plex") and are read again next run. "Couldn't check Home Plex" means reading that server raised an error, or a Jellyfin or Emby server failed 20 reads in a row and the rest of it was left for the next run; "Couldn't check Home Plex: no connection to it" means there was no client for it.
- With nothing to fix it finishes at once; its log says "Every server checked still shows what this app published".
- A Check servers job that was running when the app restarted picks up where it was: it checks the files its first run listed, skipping those it had finished, rather than listing again. A file's recheck or retry is used up only once that file is actually checked, so a cancelled or interrupted run doesn't push any file's next check further out. The list is kept on the job only until it ends.
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
- A movie is named by its title on your media server, an episode by its show and
SxxEyy. - One "Checking …" line per source you turned on: what it answered, or why it wasn't asked ("not read (already
decided)", "skipped (daily limit reached, resets 00:00 UTC)", "skipped (no data for this show; asked again after
<date>)", "not read (every server keeps its own credits)"). A film leaves out IntroDB and season audio, which
describe TV episodes only. An answer this job asked for says (asked now); one it reused says (saved
<date>), the day it was last stored (by an earlier run, or while the job checked another episode of the season).
Times are
m:ss, orh:mm:sspast an hour. - Credit text and season audio read the file itself, so they get two lines: "Reading … on the GPU (…)…" or "… on the CPU…" when the read starts, and the answer with how long it took when it ends.
- Decided gives each type and why: the chapter it came from, the sources that agree, "kept Plex's own", or "needs review" / "nothing found" with the reason. When the file's own frames or audio corrected a chapter or an online answer, it says so: "moved to the first credit card by credit text", "start from credit text", or an intro chapter that "runs on into the episode".
- A file with a "Credits" chapter has credit text read it too. Its line says whether the frames keep the chapter or move its start to the first credit card (a chapter is often a few seconds or more off, early on the last shot or late in the roll).
- One line per server, starting with its name:
[Plex] Added credits 1:33:27–1:37:25,[Plex] Replaced credits 42:02–42:48 → 42:02–42:44,[Plex] Already up to date (…),[Plex] Kept Plex's own credits,[Plex] Failed (…),[Plex] Waiting (…), or[Plex] Not in Plex's library yet (…)(the job tries it again). - A file that failed has its lines as warnings, with the reason.
- Credit text says so when the file stops before its stated length ("the file ends before its stated length (27:10 of 44:02 readable)"), and when the GPU read nothing and the CPU read the ending instead ("read on the CPU after the GPU read nothing").
- A read that fails on a worker's GPU logs a warning with FFmpeg's own last lines, then the worker reads the file again on the CPU: "Credit text: couldn't be read on the GPU (the GPU's decoder hit a hardware or driver error (ffmpeg exited 251)). FFmpeg's last lines: …".
- Files that wait for a retry get one line: "2 file(s) not in a server's library yet; retry 1 of 5 in 60s (job 3f2a9c1e)".
- A file a newer one replaced gets one line: "Blood Legacy (2024) S01E05: Skipped: replaced by a newer file (…)".
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.
- Intel GPU under
--runtime=nvidia. With both an Intel iGPU and an NVIDIA card under the NVIDIA container runtime, NVIDIA's tooling hides the Intel device from one OpenCL discovery path. The container re-adds the missing symlinks so the Intel iGPU stays usable for tone mapping. - NVIDIA Vulkan on dual-GPU hosts. With both an Intel iGPU and an NVIDIA dGPU, Vulkan defaults to Intel, but Dolby Vision tone mapping is faster on the NVIDIA card. The container's Vulkan startup probe tries up to four configurations to force NVIDIA, so frames don't bounce between the two cards.
Important
NVIDIA users: set NVIDIA_DRIVER_CAPABILITIES=all (or include graphics).
Dolby Vision Profile 5 needs the NVIDIA Vulkan driver inside the container, 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:
- Stop the container.
docker stop media-preview-generator - Restore the backups from your config volume. Before each save, the app copies the JSON file it is about to change to
<file>.<timestamp>.baknext to it, for examplesettings.json.20261002-071500.bak(UTC). An upgrade that changes the settings format copiessettings.jsonbefore 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 SchedulesOnly 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
.bakin the folder.jobs.dbhas no such backup. To recover an older job database, restore your full config-volume snapshot. - Start the older app version that wrote those files.
docker run ... your/image:older-tag
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
- Validate installation and mounts in Getting Started
- Confirm environment variables and API behavior in Configuration & API Reference
Edit this page on GitHub