Skip to content

Quality Profiles and Naming

github-actions[bot] edited this page Sep 9, 2026 · 6 revisions

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.


Release parsing

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.


Quality profiles

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: default

Profiles 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 acceptance rule

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.


File naming

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.

Tokens

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.

Zero-padding

{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.

Unknown tokens render empty

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.

Sanitisation

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.

Examples

# 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}'

Renaming existing files

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/rename

This 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.

Clone this wiki locally