How It Works
What a run actually does, in order, for every movie and series in scope.
Overview
The decision tree below covers the whole per-item flow described in this page: when a trailer is searched for at all, how a result is chosen, when an existing trailer gets replaced, and when file organization and theme songs happen. The numbered sections that follow go through each part in detail.
flowchart TD
Start(["For each movie / series in scope"]) --> SeriesRename{"Series AND
'Rename series folders' on?"}
SeriesRename -- Yes --> RenameSeries["Rename the series' top-level folder to its
resolved title, and season subfolders to 'Season
NN'/'Specials' by Jellyfin's own season number -
unconditional, regardless of trailer status;
episode files untouched"] --> HasTrailer
SeriesRename -- No --> HasTrailer
HasTrailer{"Already has a local trailer?"}
HasTrailer -- No --> BuildSources
HasTrailer -- Yes --> CheckUpdate{"'Update existing trailers' on?"}
CheckUpdate -- No --> KeepAsIs["Leave trailer untouched"]
CheckUpdate -- Yes --> ProbeExisting["Probe existing trailer's resolution"]
ProbeExisting --> BelowMin{"Below 'Video: minimum resolution'?"}
BelowMin -- No --> KeepAsIs
BelowMin -- Yes --> Backup["Back up the existing trailer"] --> BuildSources
BuildSources["Build ordered source list:
official trailer URL (if known), then
native-language search stages
(skipped if Audio = Best quality),
then English / bare-title stages"] --> TrySource
TrySource["Try next source in order"] --> Filter{"Passes title / duration / keyword filter?"}
Filter -- No --> MoreSources{"Sources left?"}
Filter -- Yes --> Download["Download candidate"]
Download --> DownloadOk{"Download succeeded?"}
DownloadOk -- No --> MoreSources
DownloadOk -- Yes --> ProbeNew["Probe resolution"]
ProbeNew --> Better{"Better than the best found so far this run?"}
Better -- No --> Discard["Discard - keep previous best"] --> MoreSources
Better -- Yes --> KeepBest["Keep as new best this run"]
KeepBest --> MeetsMin{"Meets 'Video: minimum resolution'?"}
MeetsMin -- Yes --> RunResult
MeetsMin -- No --> MoreSources
MoreSources -- Yes --> TrySource
MoreSources -- No --> RunResult["Best result found this run
(or nothing found)"]
RunResult --> WasUpgrade{"Replacing a backed-up existing trailer?"}
WasUpgrade -- No --> SaveNew["Save as the trailer
(or leave as not-found)"]
WasUpgrade -- Yes --> Compare{"New result genuinely
higher-resolution than the original?"}
Compare -- Yes --> ReplaceOld["Keep the new one, delete the backup"]
Compare -- No --> RestoreOld["Restore the original, discard the new attempt"]
SaveNew --> Organize
ReplaceOld --> Organize
RestoreOld --> Organize
KeepAsIs --> Organize
Organize["Movies only: rename original file if configured,
migrate into its own folder per Migration mode"] --> ThemeCheck{"'Fetch theme songs' on
AND item in its own dedicated folder?"}
ThemeCheck -- Yes --> Theme["Look up ThemerrDB by TMDb id,
download via yt-dlp if found"]
ThemeCheck -- No --> SkipTheme["Skip theme song"]
Theme --> Stats
SkipTheme --> Stats
Stats["Update run stats"] --> ScanCheck{"Anything downloaded/moved
AND 'Trigger library scan' on?"}
ScanCheck -- Yes --> Scan["Trigger a Jellyfin library scan"]
ScanCheck -- No --> Done(["Next item"])
Scan --> Done
1. Skip what already has a trailer
A movie or series with a local trailer file already sitting next to it — or already recognized by
Jellyfin's own LocalTrailers — is skipped, unless Update
existing trailers is on and it's below the configured minimum resolution, in which case it still
gets a full search/download attempt (see Quality and the upgrade check below).
2. Try the official trailer first
If Jellyfin already knows an official trailer URL for the item (its RemoteTrailers
metadata, typically from TMDb), that's tried before any search — it's already the correct trailer, no
matching needed.
3. Fall back to a multi-stage YouTube search
If there's no official trailer link, or it fails to download, a series of increasingly broad YouTube
searches are tried in order (title + year, title alone, etc.) until one produces a match. The item's own
preferred metadata language (resolved per-item via its library/server configuration - no interactive
Jellyfin user session needed) is tried first when it isn't English - e.g. a German-configured library
searches for "<title> trailer"/"<title> vorschau" before falling
back to an English "official trailer" search - so a same-language trailer gets first shot rather than
always searching in English regardless of how the library is actually configured. This is never a hard
restriction: English and a bare-title search are still always tried afterward too. Set
Audio to "Best quality" to skip the native-language stage from the very
first search for an item, prioritizing resolution over language match throughout - not just when
re-checking an existing trailer. Each search result is filtered before being accepted:
- Title matching — sequel- and franchise-aware, checked against every resolved title variant (original title, sanitized filename title, ...) to reject a same-franchise-but-wrong-entry result.
- Duration — rejected if it exceeds Maximum trailer duration, or (for movies) if it's suspiciously close to the movie's own runtime.
- Trailer keyword — the title must contain a recognizable keyword
(
trailer/teaser/vorschau/preview/...).
Turn on Verbose logging to see the specific reason each rejected candidate failed.
4. Download at the best available quality
Downloads use yt-dlp's own true best-quality format selector — not restricted to any
particular container or codec — so the highest resolution genuinely available for that video is what
gets saved, remuxed into an .mp4 container. Jellyfin transcodes on the fly for any client
that can't direct-play the resulting codec, the same as for any other video in your library.
Downloading tries up to three tiers, in order, each more conservative than the last:
- yt-dlp's own default client selection — the best chance at full quality.
- If that fails and a cookies file is configured: retry using the client that actually honors cookies, for age-restricted content specifically.
- If that still fails: retry with a more conservative client that reliably gets something downloadable — but its higher-quality formats are commonly blocked by YouTube's PO-token requirement (see Troubleshooting: low-quality trailers), so this tier can land as low as ~360p even when the first tier would have gotten much better.
If the result comes in below Video: minimum resolution, the search
doesn't stop there — it keeps trying the remaining sources from the multi-stage
search above, downloading and probing each via ffprobe, and keeps whichever comes out
highest-resolution once the target is met or every source has been tried. This applies to every
search, including an item's very first one, not just when re-checking an existing trailer - so a fresh
item doesn't need a later run to reach quality it could have gotten immediately.
With Update existing trailers on, a trailer you already have below the configured minimum resolution gets this same treatment too. The existing file is moved aside first (not deleted), and once the search above settles on its best result, the two are compared - the old file is only replaced if the new one is genuinely higher-resolution, otherwise the original is restored untouched. An attempt that can't beat the original - e.g. every source lands on the same tier-3 fallback - leaves it kept.
5. Rate-limit handling
YouTube can rate-limit a session that makes too many requests in a short window — a real risk for a large library, since a single movie/series can trigger several requests on its own (search + candidate checks + download attempts). Two things guard against this:
- Pacing — a configurable delay (Delay between requests) before every single request, spread out rather than sent back to back.
- Bounded retry — if the limit gets hit anyway, the run logs it clearly, waits Rate-limit retry delay minutes, then resumes the same run from exactly where it stopped (not a full restart). If that retry also gets rate-limited, the run stops for good rather than retrying indefinitely — continuing to hammer an apparently-still-active limit doesn't help and risks making it worse. The single retry re-arms after any movie/series that completes normally, so a long run that gets rate-limited more than once still gets a fresh retry each time, not just once for the whole run.
There's no reliable way to know exactly when a rate limit lifts — YouTube's own message only states an upper bound ("up to an hour") — so the retry delay is a configurable guess, not a guarantee.
6. File organisation
A downloaded (or already-existing) trailer is saved as <title>-trailer.mp4 next to
the movie or series. A movie can additionally get two optional, movie-only steps afterward — see
Rename original movie files and Migrate
movies into their own folder for what each does and why the folder migration exists at all (a
series always already has its own dedicated folder, so migration doesn't apply to it). A series can
instead get Rename series folders, which renames its own top-level
folder and its season subfolders (to Jellyfin's own canonical Season NN/Specials
naming, by each season's own resolved number — never its episode files) — unlike a movie's file rename,
this runs before the search step, and unconditionally (not just when a trailer search happens this
run), since the problem it fixes — Jellyfin's own poster match and displayed title in the collection/
list view — exists independently of trailer status.
On any Unix-like server (Linux, macOS, or similar - not Windows), any file or folder the plugin creates (a downloaded trailer/theme song, or a new folder from migration) also gets its permission bits and group matched to the movie/series' own existing file or folder. This matters on a setup where media gets added by more than one user/process — e.g. copied in over SMB as one user, with Jellyfin's own process running as another — since a folder created by one of them can otherwise end up unwritable by the other. Only permission bits and group are matched, never the file's owner (which generally requires root); this is best-effort and never blocks a trailer/theme song fetch that already succeeded.
7. Theme songs (optional)
With Fetch theme songs on, each movie/series also gets a local
theme.mp3 — but the source is different from trailers. Rather than searching YouTube, the
item's TMDb id is looked up directly on ThemerrDB,
a static, community-curated, daily-updated database that maps items straight to the correct theme song's
YouTube URL — the same database the Themerr-jellyfin
plugin uses. No title/duration/keyword filtering happens here, since ThemerrDB's own curators already
picked the right video; if there's no entry for an item, no theme song is fetched for it.
The download itself goes through the exact same yt-dlp pipeline as trailers (audio
extracted to mp3) — pacing, client-fallback tiers, and rate-limit handling all apply the same way. This
is the actual point of the feature: Themerr downloads the same ThemerrDB URLs using
YoutubeExplode, a pure C# reimplementation of
YouTube's extraction logic that isn't actively maintained against YouTube's anti-bot changes the way
yt-dlp is, and fails outright on plenty of videos yt-dlp handles without
issue.
A movie is only ever given a theme song once it's confirmed to be in its own dedicated folder (after
folder migration, if any, this run) — unlike a trailer's <title>-trailer.mp4,
theme.mp3 is a fixed filename with no per-movie disambiguation, so downloading it into a
folder shared by other movies would misattribute one movie's theme song to every other movie in that
folder. A TV series always already has its own folder, so this restriction only affects movies. An
existing theme.mp3 — whether you placed it yourself or Themerr downloaded it previously —
is always left untouched.
8. Summary and library scan
At the end of a run, a summary is logged and saved for the settings page's Statistics section — counts of scanned/downloaded/upgraded/skipped (and theme songs, if enabled) for movies and series separately, and why the run stopped early if it did. If anything was downloaded or moved and Trigger a library scan is on, a single Jellyfin library scan is triggered so the changes show up immediately.
Dependencies
Jellyfin Enricherr shells out to yt-dlp for the actual YouTube extraction/download, and to a
deno JavaScript runtime that yt-dlp needs to solve YouTube's signature/cipher
challenges. Neither needs to be installed on the server — the plugin downloads both
directly from their own GitHub releases into its own data folder the first time they're needed, verifies
each download's checksum, and keeps yt-dlp self-updated (it needs frequent updates to keep
working against YouTube's changes). This needs outbound internet access from wherever the Jellyfin
server process runs, which it already needs to reach YouTube itself.
Logging
Jellyfin Enricherr's own log entries are mirrored into a dedicated file,
<plugin data folder>/enricherr.log, in addition to Jellyfin's main server log —
so a run can be inspected without wading through unrelated server noise.