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.

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:

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:

  1. yt-dlp's own default client selection — the best chance at full quality.
  2. If that fails and a cookies file is configured: retry using the client that actually honors cookies, for age-restricted content specifically.
  3. 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:

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.