Repository navigation
Quality Profiles and Naming
Two mechanisms, both driven by parsing the release title: what Streamline is willing to accept, and what it calls the file afterwards.
Everything starts here. Streamline extracts structured fields from a release title with regexes:
| Field | Recognised as |
|---|---|
| Title | Everything before the year / season marker |
| Year | (19|20)\d\d |
| Season / Episode | S01E02 |
| Season pack |
S01 with no episode marker |
| Multi-season / complete |
S01-S05, S01.S02, complete, intégrale
|
| Absolute number |
- 18 style anime numbering. Deliberately conservative; used only as a fallback for anime |
| Air date |
2024.03.15 and separator variants, for dailies |
| Resolution |
720p, 1080p, 2160p, 4K
|
| Source |
BluRay, WEB-DL, WEBDL, WEBRip, HDTV, DVDRip, BDRip, BRRip, Remux, WEB
|
| Codec |
x264, x265, H.264, H.265, H264, H265, HEVC, AV1, MPEG2, VC-1, AVC
|
| Group | Trailing -GROUP, or a trailing .GROUP when there's no dash form. Known technical tags (MULTI, COMPLETE, resolutions) are excluded |
Important
Anything the parser can't read, Streamline won't accept. That's a deliberate posture — silently grabbing an unknown-quality release is worse than grabbing nothing.
A profile's core is three fields, all resolution-driven:
| Field | Meaning |
|---|---|
preferred_resolution |
Hard ceiling of the accepted band |
min_resolution |
Hard floor |
upgrade_allowed |
Whether a file already on disk can be replaced by a better release |
Profiles also carry a custom-format scoring layer — matched formats, a minimum score, an upgrade cutoff — covered in full on Quality Profiles and Custom Formats. This page sticks to resolution and naming; that one covers "which release wins" and "when does Streamline replace a file you already have".
Resolutions rank:
| Resolution | Rank |
|---|---|
| (unparseable) | 0 |
480p |
1 |
720p |
2 |
1080p |
3 |
2160p / 4K
|
4 |
quality_profiles:
- name: default
preferred_resolution: 1080p
min_resolution: 1080p
upgrade_allowed: true
- name: 4k
preferred_resolution: 2160p
min_resolution: 1080p
upgrade_allowed: true
- name: exactly-1080p
preferred_resolution: 1080p
min_resolution: 1080p
upgrade_allowed: false
quality_default_profile: defaultProfiles are also fully manageable at Settings → Quality profiles and via /api/v1/quality-profiles.
Movies and shows reference a profile by name. An empty reference resolves to quality_default_profile.
Profiles are resolved per item at search time, not cached at add time — so editing a profile takes effect on the next search with no restart.
The resolution part, in full:
parsed = Parse(releaseTitle)
if parsed.Resolution is empty → REJECT
if rank(parsed) == 0 → REJECT
if rank(parsed) < rank(min_resolution) → REJECT
if rank(parsed) > rank(preferred_resolution) → REJECT
otherwise → continue to custom-format scoring
Three consequences that account for most "why won't it grab this" questions:
1. No resolution in the title means rejection. Not a downgrade, not a warning — a refusal.
2. preferred_resolution is a ceiling, not a target. A release above it is rejected regardless of upgrade_allowed — that switch only governs whether an already-downloaded file can be replaced, not which resolutions are acceptable to grab in the first place. Everything from min_resolution through preferred_resolution is fair game.
3. With no profiles configured at all, everything is rejected. qualityFor logs no quality profile configured, rejecting every release and returns a zero-value filter that nothing satisfies.
A release that survives the resolution band is then scored against the profile's custom formats — that part, plus source/codec preference, upgrade behaviour and the whole scoring model, is Quality Profiles and Custom Formats.
Tip
Manual grabs bypass the profile entirely. That's the escape hatch.
Two templates:
library:
movie_naming: '{title} ({year}) {tmdb-{tmdb_id}}/{title} ({year}) [{quality}].{ext}'
series_naming: '{title} ({year})/Season {season}/{title} - S{season:2}E{episode:2} - {episode_title} [{quality}].{ext}'Templates include directory separators — the whole relative path under movie_path / series_path is templated, not just the filename.
Movies:
| Token | Expands to | Example |
|---|---|---|
{title} |
Movie title from TMDB | The Matrix |
{year} |
Release year — omitted if unknown | 1999 |
{tmdb_id} |
TMDB ID | 603 |
{quality} |
Parsed resolution | 1080p |
{source} |
Parsed source | BluRay |
{codec} |
Parsed codec | x265 |
{group} |
Release group | GROUP |
{ext} |
File extension | mkv |
Episodes:
| Token | Expands to | Example |
|---|---|---|
{title} |
Show title | Show |
{year} |
Show year | 2019 |
{tvdb_id} |
TVDB ID | 12345 |
{season}, {episode}
|
Numbers |
1, 2
|
{episode_title} |
Episode title | Pilot |
{quality} |
Parsed resolution | 1080p |
{source} |
Parsed source | WEB-DL |
{codec} |
Parsed codec | x264 |
{group} |
Release group | GROUP |
{absolute} |
Absolute episode number, when parsed | 018 |
{air_date} |
YYYY-MM-DD, when parsed |
2024-03-15 |
{ext} |
File extension | mkv |
{tmdb_id} is movie-only and {tvdb_id} series-only (they are the same token in different namespaces); {absolute} and {air_date} are episode-only. Everything else is available to both.
Write {tvdb_id} as {tvdb-{tvdb_id}} for the same Plex/Jellyfin ID hint the movie default gets from {tmdb-{tmdb_id}}. Streamline reads that marker back when scanning an existing library, so a folder carrying it is matched by ID rather than by title.
Where {quality}, {source}, {codec} and {group} come from. On import, from the release name. On a rename, from what Streamline recorded about the file — not from the name on disk, which the previous rename wrote and which carries only the tokens your template kept. {quality} additionally falls back to the probed video width, so a file whose release name never stated a resolution still gets one once ffmpeg has probed it. The name on disk is consulted last, for a file adopted in place that Streamline never parsed at import time.
{codec} keeps the release's spelling (x265) where there is one and only falls back to the probe's (hevc) when nothing claimed a codec — the two are different vocabularies and a template means the first.
{token:N} zero-pads a numeric value to width N:
| Template | Season 1, Episode 2 |
|---|---|
S{season}E{episode} |
S1E2 |
S{season:2}E{episode:2} |
S01E02 |
S{season:3}E{episode:3} |
S001E002 |
Padding applies only when the value parses as a number; otherwise it's rendered as-is.
An unrecognised token, or one whose value isn't populated, becomes an empty string rather than an error or a literal {token}. That's what lets optional segments stay clean:
{title} ({year}) [{group}] → The Matrix (1999) # when unparsed
{title} - {group} → The Matrix - # when unparsed
An empty token wrapped in [...] or (...) takes the brackets and the space before them with it — punctuation in a template is there to delimit a value, and with no value there is nothing to delimit. A bare token has no pair to remove, so a separator around one survives; keep an optional token in brackets if you want the segment to disappear cleanly.
Note
The movie default contains {tmdb-{tmdb_id}} — nested braces. The parser matches {tmdb_id} inside it, so this renders as {tmdb-603} rather than tmdb-603: the literal outer braces stay in the directory name. Plex and Jellyfin both read {tmdb-603} as an ID hint, so this is intentional and works — but if you write your own template, know that the braces are literal text, not template syntax.
Rendered paths are sanitised for filesystem safety:
| Character | Becomes |
|---|---|
: |
- (space-hyphen) |
/ \
|
- |
< > " | ? *
|
removed |
Runs of whitespace are then collapsed and the ends trimmed, so a title that
already had a space beside the character doesn't end up with two: Alien: Romulus becomes Alien - Romulus, and Mission : Impossible — which is how
TMDB writes it in some languages — becomes Mission - Impossible, not
Mission - Impossible.
Sanitisation applies to the values substituted into the template, not to the
rendered path, so a / inside a title (In/Spectre, Face/Off) becomes a dash
rather than a directory separator. The / in the template itself still marks a
directory.
Warning
Existing folders keep their old spelling until you re-run a rename — changing a template or fixing sanitisation doesn't touch files already on disk. POST /movies/{id}/rename moves the file and removes the directory it emptied.
# Plex-friendly, IDs in the folder name
movie_naming: '{title} ({year}) {tmdb-{tmdb_id}}/{title} ({year}) [{quality}].{ext}'
# → The Matrix (1999) {tmdb-603}/The Matrix (1999) [1080p].mkv
# Flat, quality-rich
movie_naming: '{title} ({year}) [{quality}] [{source}] [{codec}]-{group}.{ext}'
# → The Matrix (1999) [1080p] [BluRay] [x265]-GROUP.mkv
# Anime, absolute numbering
series_naming: '{title}/{title} - {absolute:3} - {episode_title} [{quality}].{ext}'
# → Cowboy Bebop/Cowboy Bebop - 018 - Speak Like a Child [1080p].mkv
# Daily shows
series_naming: '{title}/{title} - {air_date} - {episode_title}.{ext}'Changing a template does not rewrite files already on disk. Apply it with the per-title Rename files… action, or across a selection: pick titles on the Movies or Series page, then ⋯ → Rename files… in the bulk bar. The bulk action skips titles with nothing on disk and applies without a preview — use the per-title action when you want to see the exact moves first. Or over the API:
curl -X POST -H "X-API-Key: $KEY" \
https://streamline.example.com/api/v1/movies/42/rename
curl -X POST -H "X-API-Key: $KEY" \
https://streamline.example.com/api/v1/series/7/renameThis is also how you normalise files adopted in place by an import scan — adoption records paths as-is; renaming is a deliberate, separate step.
If you're re-rooting a whole library rather than renaming within it, use path migration instead.
🎬 Operating Streamline
- Installation
- First-Run Setup
- Adding Movies and TV
- Importing an Existing Library
- Activity and Calendar
- Requests and Users
- NixOS and Nix
- Troubleshooting
- Roadmap
⚙️ Advanced
- Configuration Reference
- Authentication and SSO
- Quality Profiles and Naming
- Quality Profiles and Custom Formats
- Scheduled Jobs
- REST API
- Observability and Logging
- GitOps and Kubernetes