Skip to main content

Media Manager

Overview

A completed torrent is a folder with a name like Show.Name.S02E05.2160p.WEB-DL.DDP5.1.HDR.H265-GROUP. A media server wants Show Name/Season 02/Show Name - S02E05 - Episode Title.mkv, with a poster, an overview, and subtitles.

Media Manager is the machine that turns the first thing into the second thing.

It scans your folders, identifies what each file actually is, enriches it with metadata and artwork, finds subtitles, generates NFO sidecars, and renames or hardlinks files into a media-server-shaped library — then tells Plex, Jellyfin, Emby, or Kodi to refresh.

It is a community module (id media_manager, permissions media_manager.*) that depends on auth and files. It can be disabled if you do not want it.

Why / when to use it

  • Your media server cannot see your downloads. Scene naming is not Plex naming. Media Manager bridges that.
  • You are seeding and cannot move files. Hardlinking (the default) puts the file in the library and leaves the original in place for the torrent client. One copy of the bytes, two paths.
  • You want Smart Download to work. Its entire "do I already own this?" logic reads Media Manager's library. Without a well-identified library, Smart Download is confidently wrong.
  • You want Missing Episodes to be accurate. Same reason.

Prerequisites

  • FILE_MANAGER_ROOTS must include the paths your libraries live under. This is the hard boundary — a library whose path falls outside it is rejected at scan time. See File Manager.
  • Somewhere for the library to live (which can be the same volume as your downloads — that is what hardlinking is for).
  • Permissions: media_manager.view to look, plus the granular ones for each action.
  • Optional but transformative: a TMDB API key (setting media.tmdbApiKey, or TMDB_API_KEY). Without it, metadata falls back to reading local .nfo sidecars only.

Concepts

Library (MediaLibrary) — a folder on disk plus the rules for organising what is in it: its kind, its naming preset, and its rename mode.

Item (MediaItem) — one identified title. A TV item is a show; a movie item is a movie.

File (MediaFile) — one physical file, with its parsed technical attributes (container, codecs, resolution, HDR, language, release group).

Identification — parsing a release name into type / title / year / season / episode, with a confidence score. When the filename alone omits the title — the common case for tidy libraries where the show name lives in the folder (Show/Season 01/S01E01.mkv) — identification climbs to the first meaningful parent folder, skipping generic Season N / Specials containers, to recover it.

Match statusunmatched (could not confidently resolve), matched (auto-identified), or manual (a human matched it). A manual match is never auto-overwritten.

Rename mode — what actually happens to the file. This is the most consequential setting in the module:

ModeEffectSafe to seed?
previewDry run. Build the plan, touch nothing.Yes
hardlinkDefault. Hardlink into the library, original stays. One copy of the bytes.Yes
symlinkSymlink into the library, original stays.Yes
copyCopy to the library, original stays. Two copies of the bytes.Yes
rename_in_placeRename the original in place.No
rename_moveMove the original to the destination.No

Preset — a set of default naming templates shaped for a media server: plex, jellyfin, emby, kodi, or custom.

How it works

Three properties of this pipeline are worth internalising:

  1. It is opt-in and scoped. The post-download workflow fires only for enabled libraries whose root path contains the torrent's save path. If you download something to a folder no library covers, nothing happens to it. This is deliberate — arbitrary downloads are never auto-organised.
  2. Each stage is isolated. A failure in one stage never aborts the rest, and the handler never throws — it cannot take down the torrent sync loop.
  3. Everything long-running is a background job. Scans, identification, metadata, artwork, subtitles, rename, and NFO all run through an in-process queue that persists each unit as a MediaProcessingJob and streams progress over WebSocket. That is why a scan of a 24,000-file library does not time out the HTTP request.

Configuration

Library

FieldWhat it doesDefaultRecommended
NameDisplay name.
Kindtv, anime, movie, music, audiobook, or general.tvGet this right. The library's kind is authoritative for movie-vs-TV identification. A TV show in a movie library will be identified as a movie.
PathThe root folder to scan. Must be inside FILE_MANAGER_ROOTS.Use the directory picker; it cannot select an out-of-root path.
Presetplex, jellyfin, emby, kodi, custom.plexMatch your media server.
TemplateA per-library rename template, overriding the preset.UnsetLeave unset unless you have a reason. See the template warning below.
ModeThe rename mode (table above).hardlinkhardlink. It puts the file in the library while leaving the original for the torrent client to seed.
EnabledWhether the library participates in scans and the post-download workflow.On
Scan interval (minutes)Optional periodic re-scan, which auto-populates metadata and artwork for new folders. Never renames or moves files.UnsetSet it if you add files outside UltraTorrent.
NFO enabledGenerate NFO sidecars during the workflow.OffOn, if your media server prefers local metadata.
Artwork enabledFetch artwork during the workflow.OnOn.
Hardlinks need one filesystem

A hardlink cannot cross a filesystem boundary. If /downloads and /media are different volumes, hardlinking will fail and you will end up copying (two copies of the bytes) or moving (breaking your seed). Mount them as one volume — this is the single most common Docker media-stack mistake.

Rename templates

Tokens are case-sensitive and use {Token} syntax. Numeric tokens accept zero-padding as {Token:00}. {Token?…} renders its inner literal only when the token is present.

TokenValue
{Movie Title}Movie title
{Series Title}Series/show title
{Episode Title}Episode title
{year}Release year
{season} / {episode} / {episodeEnd}Numbers, e.g. {season:00}
{Resolution}1080p, 2160p, …
{Source}BluRay, WEB-DL, …
{Codec}Video codec
{Release Group}Release group
{ext}File extension
{Series Title}/Season {season}/{Series Title} - S{season:00}E{episode:00}{episodeEnd? - E{episodeEnd:00}} - {Episode Title}.{ext}

Every path segment is sanitized (traversal neutralized), and Season 00 is rewritten to Specials.

A corrupt template used to be able to destroy filenames

A library whose template was truncated to a lone { rendered every episode's destination to the literal string { — an unclosed token is not a legal token, and { is not an illegal filename character, so it survived sanitisation. Every episode in a folder was renamed onto the same name, overwriting the last: on one real host, 284 files named {, ~111 GB, original names unrecoverable.

This is fixed. A rendered path is now only usable for a primary video if it is non-empty, contains no unresolved { or }, and its basename ends in the file's own extension. A file failing that check is skipped with reason: 'invalid naming template' and a warning, never moved.

Still: preview a template change before applying it. preview mode exists for exactly this.

Metadata providers

ProviderNeedsNotes
localNothingReads a local .nfo sidecar next to the media. Always available, offline.
tmdbAn API keyThe Movie Database v3. The good one.
imdbYour own dataset, or a licensed APISee below.

The TMDB key resolves at runtime: the setting media.tmdbApiKey first, then the environment variable TMDB_API_KEY. If neither is set, the provider silently degrades to the offline local provider — metadata still works from NFO sidecars, but you get nothing new.

IMDb integration

No scraping. Ever.

UltraTorrent does not scrape IMDb web pages. IMDb support uses user-provided IMDb datasets (the official non-commercial .tsv.gz files) or licensed IMDb API access that you are entitled to use. Neither is required to run UltraTorrent — with no IMDb configuration, the provider stays disabled and nothing else is affected.

Modes (Media → Settings → IMDb, setting media.imdb.mode):

ModeBehaviour
disabledOff. Default.
datasetServe from your imported dataset tables only — fully offline.
official_apiQuery your configured licensed IMDb REST API only.
hybridPrefer the dataset; fall back to the licensed API.

Dataset import, which is what Missing Episodes depends on:

  1. Get the datasets. Download the seven .tsv.gz files from IMDb's official datasets page, subject to IMDb's terms: title.basics, title.akas, title.crew, title.episode, title.principals, title.ratings, name.basics.
  2. Place them under your root path. They must live under one of your FILE_MANAGER_ROOTS. A path outside is rejected.
  3. Validate. The server checks each file exists, is in-root, and is a readable gzip/TSV with the expected header. Progress streams over WebSocket.
  4. Import. A detached, resumable job streams each gzipped TSV row-by-row into the IMDb tables. The endpoint returns immediately; the job continues in the background with live progress.

You must enable "Import TV series & episodes" if you want Missing Episodes to work at all. A movies-only import leaves the episode catalogue empty.

IMDb settingDefault
modedisabled
apiBaseUrl / apiKeynull (key is AES-GCM encrypted, redacted)
datasetPathnull
preferredRegion / preferredLanguagenull
includeAdultfalse
minVotes0
cacheTtl3600 s

Media-server integrations

Media Manager pushes library refreshes to Plex, Jellyfin, Emby, and Kodi, under /api/media/server-integrations (all gated by media_manager.manage_integrations).

Secret config keys (token, apiKey, password) are AES-GCM encrypted at rest and redacted to •••••••• in every response. On update, a placeholder of only characters means "keep the existing secret."

Permissions

PermissionGrants
media_manager.viewDashboards, libraries, items, artwork, subtitles, duplicates, presets, history.
media_manager.manage_librariesCreate / update / delete libraries.
media_manager.scanTrigger a library scan.
media_manager.matchMatch / unmatch / re-identify items.
media_manager.edit_metadataEdit items; fetch and edit metadata.
media_manager.manage_artworkSelect and upload artwork.
media_manager.manage_subtitlesScan subtitles.
media_manager.renameExecute a rename plan.
media_manager.generate_nfoGenerate NFO sidecars.
media_manager.manage_integrationsManage / test / refresh media-server integrations.
media_manager.imdb.*view, configure, import_dataset, search, match.

move_files, delete, and admin are declared in the catalog but reserved — no endpoint enforces them yet.

Step-by-step walkthrough

1. Get the volume layout right, first. Downloads and media must be on one filesystem for hardlinks to work. In Docker, mount a single volume (e.g. /data) with /data/torrents and /data/media inside it, and set FILE_MANAGER_ROOTS=/data.

2. Set a TMDB key. Media → Settings. Everything downstream is better with it.

3. Create a library. Media → Libraries → New. Kind = tv. Path = your TV folder (use the picker). Preset = plex. Mode = hardlink. Artwork on.

4. Scan it. The scan runs as a background job with a live progress bar and a per-file action log. On a large library this takes a while — that is expected, and the HTTP request will not time out.

5. Clear the unmatched pile. Media → Unmatched. For each item, either re-run auto-identification (match with an empty body), or match it manually. Use bulk re-identify to retry all the failures at once — manual matches are never overwritten.

6. Preview a rename before you apply one. Media → Rename Engine. Look at the plan. Confirm the destinations are what you expect. Then apply.

7. Connect your media server. Media → Settings → Media Server Integrations. Add Plex/Jellyfin/Emby/Kodi, Test it, and confirm it goes green.

8. Let the pipeline run. From now on, a completed torrent whose save path is inside the library's root is scanned, identified, hardlinked into place, enriched, and pushed to your media server — automatically.

Screenshots

Media Manager dashboard

Library scan progress

Unmatched items

Rename preview

IMDb dataset import

Watch this tutorial

Video coming soon.

Real-world examples

Seed and serve the same file

You are on a private tracker and must seed for weeks. You also want Plex to see the file now, named correctly. Set the library's mode to hardlink. The torrent client keeps seeding /data/torrents/Show.S02E05.../file.mkv; Plex reads /data/media/tv/Show/Season 02/Show - S02E05 - Title.mkv. One copy of the bytes on disk. Both paths point at the same inode. When you eventually stop seeding and delete the torrent's copy, the library's link survives.

Rescue a library the renamer never touched

You have thousands of files in scene-named folders that a previous tool never organised. Create the library over them, set the mode to hardlink (or rename_in_place if you are not seeding them and want them tidied in place), and scan. The scanner organises in-place files into Show/Season structure — with a safety guard that keeps the move within the file's own show folder, so it can never fling a file across your library. Then re-identify to fill in season/episode numbers, and let the IMDb link resolve.

Find and kill duplicates

Media → Duplicates groups items by reason: title_year, show_season_episode, external_id, file_hash, or similar_filename. That last one catches the case where you have the same episode from two different release groups at two qualities. Review the groups, keep the better copy, and remove the other — through the File Manager, which soft-deletes to Trash rather than destroying anything.

Troubleshooting

SymptomCauseFix
A library scan is rejected before it startsThe library's path falls outside FILE_MANAGER_ROOTS. This is a hard security boundary, checked at scan time.Move the library inside a configured root, or add the root to FILE_MANAGER_ROOTS.
Hardlinking fails / files are being copied/downloads and /media are on different filesystems. A hardlink cannot cross one.Mount them as one volume. This is the classic Docker media-stack mistake.
A large library scan returns 504 Gateway Time-outHistorically, scanning was synchronous — the HTTP request awaited the whole scan, and a ~24k-file library blew past the gateway's proxy_read_timeout. (The scan itself completed server-side.) Fixed: scans are now detached background jobs that return a jobId immediately, with live WebSocket progress.Update.
Every file in a folder collapsed onto a single file named {A corrupt/truncated naming template. See the template warning above. Fixed — a rendered path with unresolved braces is now skipped, never applied.Update. Repair the template. The original names are not recoverable.
A show fragments into one "show" per episodeHistorically, an episodic file whose filename carried no title produced a separate item per episode. Fixed: identification climbs to the show folder for episodic files.Update, then bulk re-identify.
A TV show is identified as a movieHistorically, the media type was inferred per file. Fixed: the library's kind is now authoritative for movie-vs-TV, and (Year) is stripped from episode titles.Set the library's kind correctly, then re-identify.
Hijack.2023.S02E03 gets a mangled titleA bare year before the episode marker is the series year, not part of the title. Fixed.Update.
A title with an acronym (M.I.A.) is mis-parsedSeparator normalization used to turn a title's own dots into spaces. Fixed: repeated single-letter-plus-dot patterns survive.Update.
Metadata never populatesNo TMDB key. The provider silently degrades to reading local NFO sidecars — and if there are none, you get nothing.Set media.tmdbApiKey or TMDB_API_KEY.
The IMDb dataset import fails validationA file is missing, is not under FILE_MANAGER_ROOTS, or is a truncated/renamed gzip.Confirm all seven .tsv.gz files are present, in-root, and intact. Re-download if truncated.
An IMDb-heavy operation hangs for minutesHistorically, IMDb lookups were full table scans — 47.8 s per call, and a movie scan could wedge indefinitely. Fixed with GIN trigram indexes, which now build concurrently at runtime with zero downtime.Update, and allow the index build to complete on first boot.
A media-server refresh failsBad URL, bad token, or the server is unreachable. Failures are audited without secrets.Test the integration. Check the audit log for media.integration.test_failed.
Nothing happens after a torrent completesThe torrent's save path is not inside an enabled library's root. This is by design — arbitrary downloads are never auto-organised.Set the save path on the RSS rule / watchlist item to a folder inside the library.

Best practices

  • One filesystem for downloads and media. Everything else follows from this.
  • hardlink mode, always, unless you have a specific reason not to. It is non-destructive and seeding-safe.
  • Preview before you apply. Especially after a template change.
  • Get the library kind right. It is authoritative for identification.
  • Re-identify after any identification fix. The improvements are real, but they only apply to items you re-run.
  • Import the IMDb dataset with TV enabled if you want gap detection.
  • Never rely on rename_move while seeding. You will break every torrent in that folder.

Common mistakes

  • Separate volumes for downloads and media. Hardlinks silently degrade, and you double your disk usage — or move the file and break your seed.
  • Setting mode to rename_move because it "sounds tidier", while the torrent client is still seeding the original.
  • Skipping the TMDB key and then wondering why every item has no poster and no overview.
  • A movies-only IMDb import followed by confusion about why Missing Episodes is empty.
  • Editing a template by hand and applying without previewing.
  • Expecting an unmatched item to be organised. Identification is a gate: unmatched items stop at step 2 and go no further.

FAQ

Does Media Manager move my files? Only if you tell it to. The default mode is hardlink, which is non-destructive — the original stays exactly where the torrent client put it. Only rename_in_place and rename_move relocate the original.

Will organising break my seeding? Not in hardlink, symlink, copy, or preview mode. It will in rename_in_place and rename_move.

Do I need TMDB? No, but without it metadata comes only from local .nfo sidecars. With it, you get titles, overviews, posters, ratings, and cast.

Does it scrape IMDb? No. It reads IMDb's official downloadable datasets that you provide, or a licensed IMDb REST API that you configure. Neither is required.

Why did nothing happen to my completed download? The post-download workflow fires only when the torrent's save path is inside an enabled library's root. That is deliberate: arbitrary downloads are never auto-organised.

Where do secrets go? Media-server tokens, the IMDb API key, and integration passwords are AES-GCM encrypted at rest, redacted from responses, and never logged.

Can I undo a rename? There is a rename history (GET /api/media/history), but no one-click undo. Preview first. That is what preview is for.

Checklist

  • Confirm downloads and media share one filesystem. Expected: stat shows the same device id for both.
  • Set a TMDB key. Expected: newly fetched items get a poster and an overview.
  • Create a library inside FILE_MANAGER_ROOTS with mode hardlink. Expected: it saves; the directory picker never offers an out-of-root path.
  • Scan it. Expected: a background job with a live progress bar; no 504.
  • Check Media → Unmatched. Expected: near-empty after a bulk re-identify.
  • Preview a rename. Expected: sensible destinations, no { or } in any path.
  • Apply it, then check disk usage. Expected: unchanged — hardlinks do not duplicate bytes.
  • Confirm the torrent is still seeding. Expected: yes.
  • Test a media-server integration. Expected: green, and a manual refresh makes the new item appear in Plex/Jellyfin.
  • Complete a torrent into the library's root. Expected: the full pipeline runs automatically, streamed over WebSocket.

See also