video quality profile: rebuild as rich-curated Radarr-class model (logic + tests)

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.
This commit is contained in:
BoulderBadgeDad 2026-06-19 08:38:16 -07:00
parent 975f81f476
commit 23cefd3c15
3 changed files with 189 additions and 99 deletions

View file

@ -1,15 +1,23 @@
"""Video quality profile — ONE unified profile applied to every video download
source (slskd / torrent / usenet).
"""Video quality profile — ONE unified, Radarr/Sonarr-class profile applied to
every video download source (slskd / torrent / usenet).
Unlike the music side (where quality is bitrate density), video quality is
**resolution + source + codec**, parsed from a release title or filename. So the
profile is a small, source-agnostic ranking the (later-phase) download engine will
use to pick the best candidate:
Unlike the music side (where quality is bitrate density), video quality is a
**source×resolution tier** parsed from a release title, refined by codec / HDR /
audio preferences. The (later-phase) download engine uses this profile to pick the
best candidate and to decide when a library item is "good enough" (the cutoff).
- resolution tiers (2160p / 1080p / 720p / 480p), each enabled + priority-ordered
- a preferred source order (bluray > web-dl > webrip > hdtv)
- a codec preference (any / x265 / x264) and an HDR preference
- an optional max size cap per item, and a fallback toggle
The model (rich-curated Radarr-competitive without a full custom-formats engine):
- ``tiers`` : the source×resolution quality ladder (Remux-2160p SDTV)
as ONE ranked list; each tier enabled + ordered bestworst.
- ``cutoff`` : once the library holds a tier at/above this rank, stop
upgrading (prevents endless re-grabbing).
- ``rejects`` : hard blocks never grab these (cam / screener / workprint /
3d / optionally x264).
- preferences (SOFT they score/tie-break, they never reject on their own):
``prefer_codec`` (any|hevc|av1), ``prefer_hdr`` (off|prefer|require),
``prefer_audio`` (any|surround|lossless|atmos), ``prefer_repack`` (bool).
- ``min_size_gb`` / ``max_size_gb`` : size guard per item (0 = no limit).
Pure data + normalize/validate here (no DB, no network) so it's unit-tested in
isolation. Persisted as a JSON blob in video.db's ``video_settings['quality_profile']``.
@ -21,28 +29,48 @@ from __future__ import annotations
import json
from typing import Any
# Ordered best→worst; the UI renders these as the default priority order.
RESOLUTIONS = ("2160p", "1080p", "720p", "480p")
SOURCES = ("bluray", "web-dl", "webrip", "hdtv")
CODECS = ("any", "x265", "x264")
MAX_SIZE_CAP_GB = 200 # slider ceiling; 0 means "no cap"
# The quality ladder, ordered best→worst. ``key`` = ``<source>-<resolution>`` (plus
# the two resolution-less SD tiers). This is the default ranking the UI renders.
TIERS = (
"remux-2160p", "bluray-2160p", "web-2160p",
"remux-1080p", "bluray-1080p", "web-1080p", "webrip-1080p", "hdtv-1080p",
"bluray-720p", "web-720p", "hdtv-720p",
"dvd", "sdtv",
)
# Default-enabled tiers: solid 1080p + 720p coverage. 4K tiers off (size) and the
# SD tiers (dvd/sdtv) off — users opt into those deliberately.
_DEFAULT_ON = frozenset({
"remux-1080p", "bluray-1080p", "web-1080p", "webrip-1080p", "hdtv-1080p",
"bluray-720p", "web-720p", "hdtv-720p",
})
# Hard rejects (never grabbed). x264 is offered but OFF by default (rejecting it
# would drop most releases) — power users who only want HEVC/AV1 can enable it.
REJECTS = ("cam", "screener", "workprint", "3d", "x264")
CODECS = ("any", "hevc", "av1") # SOFT codec preference (tie-breaker)
HDR_MODES = ("off", "prefer", "require") # require = HDR-only (a real filter)
AUDIO_MODES = ("any", "surround", "lossless", "atmos")
MAX_SIZE_CAP_GB = 200 # slider ceiling; 0 means "no limit"
_TIER_SET = frozenset(TIERS)
def default_profile() -> dict:
"""A sensible default: 1080p/720p on, 4K off (size), SD off."""
"""A sensible best-in-class default: full 1080p/720p ladder, cutoff at
BluRay-1080p, junk rejected, HEVC + HDR preferred (soft)."""
return {
"version": 1,
"resolutions": {
"2160p": {"enabled": False, "priority": 1},
"1080p": {"enabled": True, "priority": 2},
"720p": {"enabled": True, "priority": 3},
"480p": {"enabled": False, "priority": 4},
},
"source_priority": list(SOURCES),
"codec": "any",
"prefer_hdr": False,
"version": 2,
"tiers": [{"key": k, "enabled": k in _DEFAULT_ON} for k in TIERS],
"cutoff": "bluray-1080p",
"rejects": ["cam", "screener", "workprint", "3d"],
"prefer_codec": "hevc",
"prefer_hdr": "prefer",
"prefer_audio": "any",
"prefer_repack": True,
"min_size_gb": 0,
"max_size_gb": 0,
"fallback_enabled": True,
}
@ -53,39 +81,64 @@ def _coerce_int(value: Any, default: int) -> int:
return default
def _clamp_size(value: Any) -> int:
return min(MAX_SIZE_CAP_GB, max(0, _coerce_int(value, 0)))
def normalize_tiers(value: Any) -> list:
"""Rebuild the ranked tier ladder: keep the caller's order for known tiers,
coerce each ``enabled``, drop junk/dupes, then append any missing tiers in
canonical order so the ladder is always complete. Defaults from ``_DEFAULT_ON``."""
enabled = {k: (k in _DEFAULT_ON) for k in TIERS}
order: list = []
if isinstance(value, list):
for item in value:
if isinstance(item, dict):
k = str(item.get("key") or "").strip().lower()
else:
k = str(item or "").strip().lower()
if k in _TIER_SET and k not in order:
order.append(k)
if isinstance(item, dict) and "enabled" in item:
enabled[k] = bool(item.get("enabled"))
for k in TIERS: # complete the ladder, canonical order
if k not in order:
order.append(k)
return [{"key": k, "enabled": enabled[k]} for k in order]
def normalize(raw: Any) -> dict:
"""Coerce a stored/posted profile to a valid shape, filling gaps from the
default. Unknown keys are dropped; invalid values fall back. Never raises."""
default. Unknown keys dropped; invalid values fall back. Never raises."""
d = default_profile()
if not isinstance(raw, dict):
return d
res = raw.get("resolutions")
if isinstance(res, dict):
for i, key in enumerate(RESOLUTIONS):
r = res.get(key)
if isinstance(r, dict):
d["resolutions"][key] = {
"enabled": bool(r.get("enabled", d["resolutions"][key]["enabled"])),
"priority": _coerce_int(r.get("priority"), i + 1),
}
d["tiers"] = normalize_tiers(raw.get("tiers"))
sp = raw.get("source_priority")
if isinstance(sp, list):
clean = []
for s in sp: # keep known sources, in order, no dupes/junk
if s in SOURCES and s not in clean:
clean.append(s)
for s in SOURCES: # append any the caller dropped, canonical order
if s not in clean:
clean.append(s)
d["source_priority"] = clean
cut = str(raw.get("cutoff") or "").strip().lower()
if cut in _TIER_SET:
d["cutoff"] = cut
if raw.get("codec") in CODECS:
d["codec"] = raw["codec"]
d["prefer_hdr"] = bool(raw.get("prefer_hdr", d["prefer_hdr"]))
d["max_size_gb"] = min(MAX_SIZE_CAP_GB, max(0, _coerce_int(raw.get("max_size_gb"), 0)))
d["fallback_enabled"] = bool(raw.get("fallback_enabled", True))
rj = raw.get("rejects")
if isinstance(rj, list):
chosen = {str(x or "").strip().lower() for x in rj}
d["rejects"] = [r for r in REJECTS if r in chosen] # canonical order, valid only
if raw.get("prefer_codec") in CODECS:
d["prefer_codec"] = raw["prefer_codec"]
if raw.get("prefer_hdr") in HDR_MODES:
d["prefer_hdr"] = raw["prefer_hdr"]
if raw.get("prefer_audio") in AUDIO_MODES:
d["prefer_audio"] = raw["prefer_audio"]
d["prefer_repack"] = bool(raw.get("prefer_repack", d["prefer_repack"]))
mn = _clamp_size(raw.get("min_size_gb"))
mx = _clamp_size(raw.get("max_size_gb"))
if mx and mn > mx: # a min above a real cap is nonsense — pin it to the cap
mn = mx
d["min_size_gb"] = mn
d["max_size_gb"] = mx
return d
@ -108,6 +161,6 @@ def save(db, raw: Any) -> dict:
__all__ = [
"RESOLUTIONS", "SOURCES", "CODECS", "MAX_SIZE_CAP_GB",
"default_profile", "normalize", "load", "save",
"TIERS", "REJECTS", "CODECS", "HDR_MODES", "AUDIO_MODES", "MAX_SIZE_CAP_GB",
"default_profile", "normalize", "normalize_tiers", "load", "save",
]

View file

@ -392,13 +392,16 @@ def test_quality_profile_endpoint_roundtrips(tmp_path):
app.register_blueprint(videoapi.create_video_blueprint(), url_prefix="/api/video")
client = app.test_client()
try:
# Default profile served when unset.
# Default profile served when unset (rich-curated model: tier ladder + cutoff).
d = client.get("/api/video/downloads/quality").get_json()
assert d["resolutions"]["1080p"]["enabled"] is True and d["codec"] == "any"
# POST normalizes + persists; bad codec rejected.
assert [t["key"] for t in d["tiers"]][0] == "remux-2160p"
assert d["cutoff"] == "bluray-1080p" and d["prefer_codec"] == "hevc"
# POST normalizes + persists; bad codec rejected, valid cutoff kept.
out = client.post("/api/video/downloads/quality",
json={"codec": "bogus", "max_size_gb": 50, "prefer_hdr": True}).get_json()
assert out["codec"] == "any" and out["max_size_gb"] == 50 and out["prefer_hdr"] is True
json={"prefer_codec": "bogus", "max_size_gb": 50,
"cutoff": "web-720p", "prefer_hdr": "require"}).get_json()
assert out["prefer_codec"] == "hevc" and out["max_size_gb"] == 50
assert out["cutoff"] == "web-720p" and out["prefer_hdr"] == "require"
assert client.get("/api/video/downloads/quality").get_json()["max_size_gb"] == 50
finally:
videoapi._video_db = None

View file

@ -1,30 +1,42 @@
"""Video quality profile — the pure default/normalize/load/save seam (resolution
tiers + source/codec/HDR + size cap), isolated from the music side."""
"""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,
RESOLUTIONS,
SOURCES,
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 set(d["resolutions"]) == set(RESOLUTIONS)
assert d["resolutions"]["1080p"]["enabled"] is True
assert d["resolutions"]["2160p"]["enabled"] is False # 4K off by default (size)
assert d["source_priority"] == list(SOURCES)
assert d["codec"] == "any" and d["prefer_hdr"] is False
assert d["max_size_gb"] == 0 and d["fallback_enabled"] is True
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():
@ -33,32 +45,57 @@ def test_normalize_garbage_returns_default():
assert normalize(123) == default_profile()
def test_normalize_fills_gaps_and_coerces():
out = normalize({
"resolutions": {"2160p": {"enabled": True, "priority": "1"}}, # str priority coerced
"codec": "x265",
"prefer_hdr": 1,
"max_size_gb": "75",
"fallback_enabled": False,
})
assert out["resolutions"]["2160p"] == {"enabled": True, "priority": 1}
assert out["resolutions"]["1080p"]["enabled"] is True # untouched tier kept from default
assert out["codec"] == "x265" and out["prefer_hdr"] is True
assert out["max_size_gb"] == 75 and out["fallback_enabled"] is False
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_rejects_bad_codec_and_clamps_size():
assert normalize({"codec": "vp9"})["codec"] == "any" # unknown codec rejected
assert normalize({"max_size_gb": -5})["max_size_gb"] == 0 # negative clamped
assert normalize({"max_size_gb": 99999})["max_size_gb"] == MAX_SIZE_CAP_GB # capped
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_source_priority_dedupes_and_completes():
out = normalize({"source_priority": ["web-dl", "bogus", "web-dl"]})
# known one first, rest appended in canonical order, no dupes, no junk
assert out["source_priority"][0] == "web-dl"
assert set(out["source_priority"]) == set(SOURCES)
assert len(out["source_priority"]) == len(SOURCES)
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 ────────────────────────────────────────
@ -79,10 +116,11 @@ def test_load_default_when_unset():
def test_save_then_load_roundtrips_normalized():
db = _FakeDB()
saved = save(db, {"codec": "x264", "max_size_gb": 40,
"resolutions": {"480p": {"enabled": True, "priority": 4}}})
assert saved["codec"] == "x264" and saved["max_size_gb"] == 40
assert json.loads(db.get_setting("quality_profile"))["codec"] == "x264"
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
@ -90,7 +128,3 @@ def test_load_recovers_from_corrupt_json():
db = _FakeDB()
db.set_setting("quality_profile", "{not json")
assert load(db) == default_profile()
def test_codecs_constant():
assert CODECS == ("any", "x265", "x264")