Replaces the simplified resolutions+4-sources+codec model with a real quality
ladder to actually stand in for Radarr/Sonarr:
- tiers: source×resolution ladder (Remux-2160p … SDTV) as one ranked, toggleable
list, best→worst, with a cutoff (stop upgrading once the library holds a tier
at/above it — no endless re-grabbing).
- rejects: hard blocks (cam/screener/workprint/3d, optional x264).
- soft preferences (score/tie-break, never reject alone): prefer_codec (any/hevc/av1),
prefer_hdr (off/prefer/require), prefer_audio (any/surround/lossless/atmos),
prefer_repack.
- size guard: min/max GB per item (0 = no limit), min pinned to a real cap.
Pure normalize/load/save (no DB/network), isolated from music. API endpoint is an
unchanged passthrough. 12 tests green, ruff clean. UI next.
130 lines
5 KiB
Python
130 lines
5 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,
|
||
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"] == "bluray-1080p"
|
||
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["min_size_gb"] == 0 and d["max_size_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_must_be_a_known_tier():
|
||
assert normalize({"cutoff": "web-720p"})["cutoff"] == "web-720p"
|
||
assert normalize({"cutoff": "nonsense"})["cutoff"] == "bluray-1080p" # falls back
|
||
|
||
|
||
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_clamps_and_pins_min_to_cap():
|
||
assert normalize({"max_size_gb": -5})["max_size_gb"] == 0
|
||
assert normalize({"max_size_gb": 99999})["max_size_gb"] == MAX_SIZE_CAP_GB
|
||
out = normalize({"min_size_gb": 50, "max_size_gb": 20}) # min above the cap
|
||
assert out["min_size_gb"] == 20 and out["max_size_gb"] == 20
|
||
keep = normalize({"min_size_gb": 5, "max_size_gb": 0}) # 0 cap = no limit, min stays
|
||
assert keep["min_size_gb"] == 5 and keep["max_size_gb"] == 0
|
||
|
||
|
||
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_size_gb": 40, "cutoff": "web-720p",
|
||
"tiers": [{"key": "remux-2160p", "enabled": True}]})
|
||
assert saved["prefer_codec"] == "av1" and saved["max_size_gb"] == 40
|
||
assert saved["cutoff"] == "web-720p"
|
||
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()
|