Skip to main content

Building a Movie Library

Level: πŸ”΅ Intermediate Β· Time: ~45 minutes

You have downloads. They are named things like Some.Movie.2024.2160p.UHD.BluRay.REMUX.DV.HDR.TrueHD.7.1.Atmos-GROUP. Your media server hates that. This tutorial turns that folder into a library.

Overview​

Purpose​

To build a movie library that:

  • Names every file the way Plex/Jellyfin/Emby expects.
  • Keeps seeding, because it hardlinks instead of moving.
  • Enriches itself with metadata, posters and NFO sidecars.
  • Tells you which items it could not identify, instead of silently guessing.
  • Organises future downloads automatically, with no further work from you.

When to use this tutorial​

Use it when…Use something else when…
You have downloads and want them organised.You want TV series β†’ Automating TV shows.
Your media server shows garbage titles.You want to acquire movies, not organise them β†’ Smart RSS rules.
You want future downloads organised automatically.You just want one download to work β†’ My First Download.

Prerequisites​

  • A running stack (Quick Start).
  • At least one completed download to practise on.
  • The media_manager module enabled (it is, by default β€” check Administration β†’ Modules).
  • Permissions: media_manager.manage_libraries, media_manager.scan, media_manager.rename, media_manager.move_files.
  • 10 minutes of thinking about your folder layout before you touch anything. This matters more than any setting.
Decide your filesystem layout first

Two things depend on it and both are painful to change later:

  1. Hardlinks cannot cross filesystems. Your downloads and your library must be on the same volume/mount, or hardlinking will fail and you will be forced to copy (2Γ— the disk).
  2. The post-download pipeline is opt-in by path. It organises a completed torrent only when an enabled library's root path contains that torrent's save path.

A layout that satisfies both, from day one:

/downloads ← FILE_MANAGER_ROOTS (one volume)
β”œβ”€β”€ movies/ ← the Movies library points HERE
β”œβ”€β”€ tv/ ← the TV library points here
└── unsorted/ ← no library β€” never auto-organised

Concepts​

TermMeaning
LibraryA folder + its kind, naming preset, template, rename mode and scan interval.
Kindmovie / tv / anime / music / audiobook / general. Authoritative over the filename.
Media itemOne title in a library, with a matchStatus of unmatched / matched / manual.
IdentificationParsing the release name into type/title/year/season/episode with a confidence score.
Rename modepreview Β· rename_in_place Β· rename_move Β· copy Β· hardlink (default) Β· symlink.
NFOA Kodi-style XML sidecar that media servers read.

Step-by-step​

Step 1 β€” Choose your naming preset​

Go to Media Management β†’ Libraries (/media/libraries).

Before creating anything, decide the preset, because it fills the naming template for you:

PresetChoose it if your media server is…
plexPlex
jellyfinJellyfin
embyEmby
kodiKodi
customSomething else, or you want full control of the template

Expected result: you know which preset you want. If you are unsure, pick the one matching the server you actually run.


Step 2 β€” Create the library, in preview mode​

Click Add library and fill it in:

FieldValueWhy
NameMoviesYours.
Path/downloads/moviesMust be inside FILE_MANAGER_ROOTS. The picker will offer to create it.
KindmovieThis is authoritative β€” it stops a folder like 9-1-1 (2018) being mis-read.
Presetplex (or yours)Supplies the template.
ModepreviewStart here. It touches nothing.
Template(blank)The preset fills it in. Override later if you want.
Scan interval(blank for now)Blank or 0 = manual scans only.
EnabledonA disabled library is never scanned or auto-organised.

Save.

Expected result: the library appears in the list with three badges β€” its kind, its preset, and its mode. The path is shown in monospace beneath the name.

Add library dialog with kind, preset and mode

A library cannot live outside the hard roots

FILE_MANAGER_ROOTS (default /downloads) is a hard boundary enforced after canonicalisation. Traversal, symlink-escape and absolute-escape are all rejected. If you need a library elsewhere, add that path to FILE_MANAGER_ROOTS and restart the backend β€” do not try to trick it.


Step 3 β€” Scan it​

Click Scan on the library.

Scanning walks the folder tree, discovers media files, and creates one media item per title. It runs as a background job β€” it does not block the UI, and its progress streams over WebSocket.

Expected result: go to Media Management β†’ Media Items (/media/items). You should see one item per movie found.

Media Items page after a library scan


Step 4 β€” Check what it could not identify​

This is the step everyone skips and then complains about.

Go to Media Management β†’ Unmatched Media (/media/unmatched).

Identification parses the release name into type/title/year with a confidence score, and sets a matchStatus:

matchStatusMeaningWhat to do
matchedIdentified automatically.Nothing.
manualYou corrected it by hand.Nothing.
unmatchedThe name did not parse confidently.Fix it here.

Match each unmatched item by hand. Once matched, everything downstream β€” metadata, artwork, renaming, duplicate detection, missing-episode ownership β€” starts working for it.

Expected result: the unmatched list is empty, or contains only genuinely unidentifiable junk.

Unmatched items poison everything downstream

An unidentified item has no title, no year and no external IDs. It cannot be renamed correctly, it will not be counted as owned by Missing Episodes (so you will be told you are missing things you actually have), and it will not deduplicate. Clear the unmatched list before you trust any other number.

Unmatched media page with manual matching


Step 5 β€” Turn on metadata​

Go to Media Management β†’ Media Settings (/media/settings). This page hosts Metadata Providers, Artwork preferences, Subtitle preferences, NFO tooling and Media Server Integrations.

Metadata comes from providers, tried in order:

ProviderSourceNeeds
localNFO sidecars already next to your filesNothing. Always available.
tmdbThe Movie DatabaseA TMDB API key.
imdbUser-provided IMDb datasets and/or a licensed IMDb APIConfigured on Media β†’ Settings β†’ IMDb (/media/settings/imdb).

Set a TMDB key if you have one β€” it is the highest-confidence source, and it is also what powers TV show airing-status awareness later.

UltraTorrent does not scrape IMDb

The IMDb provider works from user-provided datasets and/or a licensed IMDb API. It never scrapes IMDb web pages. Dataset import is confined to the hard roots and the API key is AES-GCM encrypted at rest. See /media/settings/imdb.

Expected result: your media items gain overviews, genres, cast and external IDs (tmdb/tvdb/imdb/omdb/anilist).

Media settings with metadata providers configured


Step 6 β€” Preview the rename. Read it. Actually read it.​

Go to Media Management β†’ Rename Engine (/media/rename-preview).

This builds the complete rename plan β€” every source path, every destination path β€” and changes absolutely nothing. Every path segment is sanitized.

Read every line. Ask yourself:

  • Is the title right?
  • Is the year right?
  • Is the destination inside the library, where you expect?
  • Are there any obviously wrong parses? (Go back to Step 4.)

Expected result: a plan you would be happy to execute.

Rename preview showing source to destination paths

The template is token-based

The preset gives you a sensible default, but you can write your own with tokens (title, year, resolution, source, edition, and so on). The rename page has a token help panel. Every segment is sanitized, so you cannot template your way outside the hard roots.


Step 7 β€” Choose your mode with your eyes open​

Now the important decision.

ModeExtra diskSeeding survivesVerdict
previewβ€”β€”Where you start.
hardlink (default)Noneβœ…What you want. Same bytes, two names.
copy2Γ—βœ…When the two paths are on different filesystems.
symlinkNoneβœ…A pointer. Some servers/containers do not follow symlinks across mounts.
rename_in_placeNone⚠️Renames where it already is.
rename_moveNone❌The engine loses the file. Only if you do not seed.

Edit the library, change Mode to hardlink, save, and apply the rename.

Expected result: the file now exists at a clean path (/downloads/movies/Some Movie (2024)/Some Movie (2024).mkv) and the torrent is still seeding β€” because a hardlink is two directory entries pointing at the same bytes.

Verify both facts:

# One inode, two names, one copy of the data.
docker compose exec backend \
find /downloads -samefile "/downloads/movies/Some Movie (2024)/Some Movie (2024).mkv"

You should see two paths listed. That is the whole trick.

"Invalid cross-device link"

That error means /downloads and your library are on different filesystems. Hardlinks are two names for one inode, and an inode belongs to exactly one filesystem. Either restructure your mounts onto one volume (best), or set the mode to copy and accept the double disk usage.


Step 8 β€” Artwork, subtitles and NFO​

Once items are matched, the enrichment stages can run:

StageWhat it doesWhere to see it
ArtworkDownloads typed artwork (poster, fanart, logo, clearart, banner, thumbnail) via the artwork provider, into the hard roots, through the same magic-byte + size validation as uploads.The media item detail page.
SubtitlesSidecar discovery with language/forced/SDH flags, plus missing-language detection.The media item detail page.
NFOWrites Kodi-style movie/tvshow/season/episode sidecars β€” inside the hard roots only.Next to the media file.
Custom uploads win

If you upload your own poster, it keeps selection precedence over auto-imported art. UltraTorrent will not overwrite your choice.

Remote subtitle download is not shipped yet

Sidecar discovery ships today (it finds the .srt files you already have and tells you which languages are missing). Downloading subtitles from a remote provider such as OpenSubtitles is planned, not present.


Step 9 β€” Clean up duplicates​

Go to Media Management β†’ Duplicates (/media/duplicates).

Duplicate groups are formed by reason:

  • Same title + year
  • Same show + season + episode
  • Same external ID
  • Same file hash
  • Similar filename

Review each group and keep the one you want.

Expected result: one copy of each movie, at the quality you chose.

Duplicates page showing a detected duplicate group


Step 10 β€” Make it automatic, forever​

Two switches, and you never do any of this by hand again.

A. Future downloads organise themselves. Already done β€” as long as the torrent's save path is inside the library's root. So when you add a movie torrent, set its save path to /downloads/movies. The torrent.completed event runs the whole pipeline: scan β†’ identify β†’ metadata β†’ rename/hardlink β†’ artwork β†’ subtitles β†’ NFO β†’ media-server refresh.

B. Files you drop in by hand get enriched too. Edit the library and set a scan interval (e.g. 360 minutes). A cheap 5-minute tick picks up any library whose scan is due and enriches it.

The periodic scan behaves differently on purpose

It carries no torrent context, so it fires no media.* automation triggers, and it never renames or moves files β€” it enriches in place. Renaming stays the download organiser's job. It fills only the gaps (identity, metadata, poster) that are actually missing, so steady-state scans do almost no work.

A blank or zero scan interval means manual scans only β€” that library is never auto-scanned.

Expected result: you stop visiting these pages, which is the goal.


Step 11 β€” Check library health​

Go to Media Management β†’ Media Dashboard (/media).

It surfaces:

  • Unmatched items
  • Missing artwork
  • Missing subtitles
  • Duplicates

Aim for zeroes. That is a healthy library.

Media dashboard with library health widgets

Watch this tutorial

Video coming soon.


Examples​

A three-library layout that scales​

LibraryPathKindPresetModeScan interval
Movies/downloads/moviesmovieplexhardlink720
TV/downloads/tvtvplexhardlink360
Anime/downloads/animeanimeplexhardlink360

Nothing points at /downloads/unsorted, so nothing there is ever touched.

docker compose exec backend stat -c '%h %i %n' \
"/downloads/movies/Some Movie (2024)/Some Movie (2024).mkv"

A link count (%h) of 2 or more means the hardlink worked. A count of 1 means you copied or moved.


Troubleshooting​

SymptomCauseFix
Scan finds nothingWrong path, empty folder, or the library is disabled.Check the path in the File Manager (/files) and that Enabled is on.
Everything is unmatchedRelease names are unconventional or the files are loose.Match manually on /media/unmatched; improve source naming going forward.
A movie was detected as a TV showYou used a general library.Set the library's kind to movie β€” kind is authoritative; only general guesses from the filename.
Rename produced a weird pathThe parsed title or year is wrong.Fix the item's identification first, then re-preview.
"Invalid cross-device link"Downloads and library are on different filesystems.Use copy, or unify the mounts.
Renamed, but the torrent erroredYou used rename_move and the engine lost the file.Switch to hardlink. Recheck the torrent, or re-add it.
Downloads are never auto-organisedThe save path is not inside an enabled library's root.Set the torrent's save path correctly, or move the library root.
Files created as rootThe engine ran as root.Set PUID/PGID in .env to the owning user (id someuser) and recreate the engine container.
No artwork appearsNo metadata provider configured, or the item is unmatched.Set a TMDB key in /media/settings; clear the unmatched list.
Plex still shows the old namePlex has not rescanned.Configure a media-server integration so refreshes are pushed automatically β†’ Integrating Plex/Jellyfin.

Tips​

Always start a new library in preview

It costs you nothing and it has saved everyone who has ever used it at least once.

Fix identification before you fix anything else

Metadata, artwork, renaming, duplicates and missing-episode ownership all read from the parsed identity. Get that right and the rest falls out for free.

Set the save path when you add the torrent

It is the single field that decides whether the whole media pipeline runs. Setting it takes two seconds; retrofitting it means moving files.

Everything long-running is a job

Scans, metadata, artwork, subtitle scans, rename previews, rename execution, NFO generation and media-server refresh all run as tracked background jobs with live progress. Nothing blocks the API.


FAQ​

Do I need TMDB? No, but you want it. Without an online provider you get only what local NFO sidecars already carry. TMDB is also the highest-confidence source for TV airing status.

Can I have several libraries pointing at the same folder? Don't. One folder, one library, one kind. Overlapping roots make "which library owns this download?" ambiguous.

What happens if I change the template later? Nothing, until you run a rename. Preview it first β€” a template change can rewrite your whole library's paths.

Can I organise files I did not download through UltraTorrent? Yes. Drop them in the library folder and either scan manually or set a scan interval. Remember: the periodic scan enriches but never renames.

Does deleting a media item delete the file? Deletion is permission-gated (media_manager.delete) and audited. Be deliberate β€” and remember that a hardlinked file still exists under its download path.

Why is my item manual instead of matched? Because you matched it by hand. That is a good thing: manual matches are respected and not overwritten.


Checklist​

Verification​

  • A library exists, enabled, with the right kind and preset.
  • Its path is inside FILE_MANAGER_ROOTS.
  • A scan discovered your files and created media items.
  • /media/unmatched is empty.
  • A metadata provider is configured and items have overviews/IDs.
  • The rename preview showed a plan you agreed with.
  • Mode is hardlink (or copy, if you are cross-filesystem).
  • find -samefile shows two paths for a renamed file.
  • The original torrent is still seeding.
  • /media/duplicates is clean.
  • The Media Dashboard shows zero (or explained) health issues.

Expected results​

ScreenExpected
/media/libraries1+ libraries, each with kind/preset/mode badges
/media/itemsOne item per movie, all matched or manual
/media/unmatchedEmpty
/mediaHealth widgets at or near zero
/torrentsStill seeding, unharmed

Next steps​

  1. Automating TV shows β€” the same, but for series, plus missing-episode detection.
  2. Integrating Plex / Jellyfin β€” auto-refresh your server after every import.
  3. Smart RSS rules β€” fill the library automatically.

See also​