Addressing UX feedback (Boulder): the cutoff and size guard were confusing for a
library that holds BOTH movies and TV.
- 'Upgrade until' is now a LOOSE resolution target (4K / 1080p / 720p / SD /
'best — never stop'), not a specific source×resolution tier. 4K is always in
the list regardless of which tiers are toggled on (the old dropdown only
listed enabled tiers, so 4K vanished when off). Stored as cutoff_resolution.
- Size guard split into Max movie size + Max episode size (runtime-aware, like
Radarr's MB/min but human-readable) — a flat GB cap was meaningless across a
2-hour movie and a 25-min episode. Dropped the confusing min slider.
Pure-logic + API + UI in lockstep; removed the now-dead per-row cutoff marker.
12 tests green, ruff clean, JS/HTML balance clean.
131 lines
5.2 KiB
Python
131 lines
5.2 KiB
Python
"""Video quality profile — the pure default/normalize/load/save seam for the
|
||
rich-curated (Radarr-class) model: a ranked source×resolution tier ladder +
|
||
cutoff + hard rejects + soft codec/HDR/audio/repack preferences + size guard,
|
||
isolated from the music side."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
|
||
from core.video.quality_profile import (
|
||
AUDIO_MODES,
|
||
CODECS,
|
||
HDR_MODES,
|
||
MAX_SIZE_CAP_GB,
|
||
REJECTS,
|
||
RESOLUTIONS,
|
||
TIERS,
|
||
default_profile,
|
||
load,
|
||
normalize,
|
||
normalize_tiers,
|
||
save,
|
||
)
|
||
|
||
|
||
def _keys(tiers):
|
||
return [t["key"] for t in tiers]
|
||
|
||
|
||
def test_default_shape():
|
||
d = default_profile()
|
||
assert _keys(d["tiers"]) == list(TIERS) # complete ladder, canonical order
|
||
on = {t["key"] for t in d["tiers"] if t["enabled"]}
|
||
assert "bluray-1080p" in on and "web-720p" in on # 1080p/720p on
|
||
assert "remux-2160p" not in on and "sdtv" not in on # 4K + SD off by default
|
||
assert d["cutoff_resolution"] == "1080p" # loose resolution target
|
||
assert "cam" in d["rejects"] and "x264" not in d["rejects"] # junk blocked, x264 allowed
|
||
assert d["prefer_codec"] == "hevc" and d["prefer_hdr"] == "prefer"
|
||
assert d["prefer_audio"] == "any" and d["prefer_repack"] is True
|
||
assert d["max_movie_gb"] == 0 and d["max_episode_gb"] == 0
|
||
|
||
|
||
def test_normalize_garbage_returns_default():
|
||
assert normalize(None) == default_profile()
|
||
assert normalize("nope") == default_profile()
|
||
assert normalize(123) == default_profile()
|
||
|
||
|
||
def test_normalize_tiers_preserves_order_completes_and_coerces():
|
||
# caller sends a re-ranked subset with a couple toggled; rest must be appended
|
||
out = normalize_tiers([
|
||
{"key": "web-1080p", "enabled": False},
|
||
{"key": "bogus", "enabled": True}, # junk dropped
|
||
{"key": "remux-2160p", "enabled": True},
|
||
"web-1080p", # dupe ignored
|
||
])
|
||
keys = _keys(out)
|
||
assert keys[0] == "web-1080p" and keys[1] == "remux-2160p" # caller order kept
|
||
assert set(keys) == set(TIERS) and len(keys) == len(TIERS) # ladder completed
|
||
by = {t["key"]: t["enabled"] for t in out}
|
||
assert by["web-1080p"] is False and by["remux-2160p"] is True
|
||
assert by["bluray-1080p"] is True # untouched default stays on
|
||
|
||
|
||
def test_normalize_cutoff_is_a_loose_resolution_target():
|
||
assert normalize({"cutoff_resolution": "2160p"})["cutoff_resolution"] == "2160p"
|
||
assert normalize({"cutoff_resolution": ""})["cutoff_resolution"] == "" # best/always-upgrade
|
||
assert normalize({"cutoff_resolution": "nonsense"})["cutoff_resolution"] == "1080p" # falls back
|
||
assert "2160p" in RESOLUTIONS # 4K always offered
|
||
|
||
|
||
def test_normalize_rejects_keep_canonical_order_and_drop_junk():
|
||
out = normalize({"rejects": ["x264", "bogus", "cam", "x264"]})
|
||
assert out["rejects"] == ["cam", "x264"] # canonical order, valid only, deduped
|
||
|
||
|
||
def test_normalize_soft_prefs_validate():
|
||
out = normalize({"prefer_codec": "av1", "prefer_hdr": "require",
|
||
"prefer_audio": "atmos", "prefer_repack": 0})
|
||
assert out["prefer_codec"] == "av1" and out["prefer_hdr"] == "require"
|
||
assert out["prefer_audio"] == "atmos" and out["prefer_repack"] is False
|
||
# unknown enum values fall back to the default
|
||
bad = normalize({"prefer_codec": "vp9", "prefer_hdr": "maybe", "prefer_audio": "8ch"})
|
||
assert bad["prefer_codec"] == "hevc" and bad["prefer_hdr"] == "prefer"
|
||
assert bad["prefer_audio"] == "any"
|
||
|
||
|
||
def test_normalize_size_splits_movie_and_episode_and_clamps():
|
||
assert normalize({"max_movie_gb": -5})["max_movie_gb"] == 0 # negative clamped
|
||
assert normalize({"max_movie_gb": 99999})["max_movie_gb"] == MAX_SIZE_CAP_GB # capped
|
||
out = normalize({"max_movie_gb": 40, "max_episode_gb": 5}) # independent caps
|
||
assert out["max_movie_gb"] == 40 and out["max_episode_gb"] == 5
|
||
|
||
|
||
def test_constants():
|
||
assert CODECS == ("any", "hevc", "av1")
|
||
assert HDR_MODES == ("off", "prefer", "require")
|
||
assert AUDIO_MODES == ("any", "surround", "lossless", "atmos")
|
||
assert "cam" in REJECTS and "screener" in REJECTS
|
||
|
||
|
||
# ── DB round-trip via an injected fake ────────────────────────────────────────
|
||
class _FakeDB:
|
||
def __init__(self):
|
||
self._kv = {}
|
||
|
||
def get_setting(self, key, default=None):
|
||
return self._kv.get(key, default)
|
||
|
||
def set_setting(self, key, value):
|
||
self._kv[key] = value
|
||
|
||
|
||
def test_load_default_when_unset():
|
||
assert load(_FakeDB()) == default_profile()
|
||
|
||
|
||
def test_save_then_load_roundtrips_normalized():
|
||
db = _FakeDB()
|
||
saved = save(db, {"prefer_codec": "av1", "max_movie_gb": 40, "cutoff_resolution": "2160p",
|
||
"tiers": [{"key": "remux-2160p", "enabled": True}]})
|
||
assert saved["prefer_codec"] == "av1" and saved["max_movie_gb"] == 40
|
||
assert saved["cutoff_resolution"] == "2160p"
|
||
assert json.loads(db.get_setting("quality_profile"))["prefer_codec"] == "av1"
|
||
assert load(db) == saved
|
||
|
||
|
||
def test_load_recovers_from_corrupt_json():
|
||
db = _FakeDB()
|
||
db.set_setting("quality_profile", "{not json")
|
||
assert load(db) == default_profile()
|