youtube quality: map profile -> yt-dlp format selection
pure format_selection(profile) -> {format, format_sort, merge_output_format}.
caps to the resolution ceiling (falls back to uncapped so above-cap-only videos
still grab), ranks codec/res/fps/sdr as soft prefs (never excludes a stream).
the one piece the youtube downloader needs regardless of how the engine is wired.
exact yt-dlp tokens tunable on live yt-dlp; tests pin the shape.
This commit is contained in:
parent
31d284ab0d
commit
568e297d36
2 changed files with 75 additions and 1 deletions
|
|
@ -54,6 +54,44 @@ def normalize(raw: Any) -> dict:
|
|||
return d
|
||||
|
||||
|
||||
# Resolution → pixel-height ceiling for yt-dlp's ``height<=`` filter. "best" = no cap.
|
||||
_RES_HEIGHT = {"best": None, "4320p": 4320, "2160p": 2160, "1440p": 1440,
|
||||
"1080p": 1080, "720p": 720, "480p": 480, "360p": 360}
|
||||
|
||||
|
||||
def format_selection(profile: Any) -> dict:
|
||||
"""Map a profile to the yt-dlp options the (download) engine passes through:
|
||||
``{format, format_sort, merge_output_format}``. Pure — no DB, no network.
|
||||
|
||||
* ``format`` takes the best video+audio capped to the resolution ceiling, falling
|
||||
back to an uncapped best so a video that only exists above the cap still grabs.
|
||||
* ``format_sort`` is an ordered soft-preference list (codec → resolution → fps →
|
||||
SDR) — yt-dlp picks the top match, so these never *exclude* a stream, they rank it.
|
||||
* ``merge_output_format`` is the container yt-dlp muxes into.
|
||||
|
||||
NB: the exact yt-dlp tokens are tunable against the live yt-dlp version when the
|
||||
downloader is wired; the shape (one capped format expr + a ranked sort list) is the
|
||||
contract the tests pin.
|
||||
"""
|
||||
p = normalize(profile)
|
||||
height = _RES_HEIGHT.get(p["max_resolution"])
|
||||
if height:
|
||||
fmt = "bv*[height<=%d]+ba/b[height<=%d]/bv*+ba/b" % (height, height)
|
||||
else:
|
||||
fmt = "bv*+ba/b"
|
||||
|
||||
sort: list[str] = []
|
||||
if p["video_codec"] != "any":
|
||||
sort.append("vcodec:%s" % p["video_codec"]) # soft codec preference
|
||||
sort.append("res:%d" % height if height else "res") # prefer the ceiling, else highest
|
||||
if p["prefer_60fps"]:
|
||||
sort.append("fps") # prefer higher frame rate
|
||||
if not p["allow_hdr"]:
|
||||
sort.append("hdr:SDR") # rank SDR above HDR (don't exclude)
|
||||
|
||||
return {"format": fmt, "format_sort": sort, "merge_output_format": p["container"]}
|
||||
|
||||
|
||||
def load(db) -> dict:
|
||||
"""Read + normalize the stored profile, or the default if none/garbage."""
|
||||
raw = db.get_setting("youtube_quality_profile")
|
||||
|
|
@ -74,5 +112,5 @@ def save(db, raw: Any) -> dict:
|
|||
|
||||
__all__ = [
|
||||
"RESOLUTIONS", "CODECS", "CONTAINERS",
|
||||
"default_profile", "normalize", "load", "save",
|
||||
"default_profile", "normalize", "format_selection", "load", "save",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from core.video.youtube_quality import (
|
|||
CONTAINERS,
|
||||
RESOLUTIONS,
|
||||
default_profile,
|
||||
format_selection,
|
||||
load,
|
||||
normalize,
|
||||
save,
|
||||
|
|
@ -79,3 +80,38 @@ def test_load_recovers_from_corrupt_json():
|
|||
db = _FakeDB()
|
||||
db.set_setting("youtube_quality_profile", "{nope")
|
||||
assert load(db) == default_profile()
|
||||
|
||||
|
||||
# ── yt-dlp format mapping (what the downloader passes through) ────────────────
|
||||
def test_format_selection_default_caps_to_1080_sdr_60fps_mp4():
|
||||
sel = format_selection(default_profile())
|
||||
assert sel["format"] == "bv*[height<=1080]+ba/b[height<=1080]/bv*+ba/b"
|
||||
assert sel["merge_output_format"] == "mp4"
|
||||
# default: any-codec (no vcodec pref), prefer the cap res, 60fps, SDR
|
||||
assert sel["format_sort"] == ["res:1080", "fps", "hdr:SDR"]
|
||||
|
||||
|
||||
def test_format_selection_best_has_no_height_cap():
|
||||
sel = format_selection({"max_resolution": "best", "video_codec": "any",
|
||||
"prefer_60fps": False, "allow_hdr": True})
|
||||
assert sel["format"] == "bv*+ba/b"
|
||||
assert sel["format_sort"] == ["res"] # no codec/fps/sdr prefs → just highest res
|
||||
|
||||
|
||||
def test_format_selection_codec_pref_leads_the_sort():
|
||||
sel = format_selection({"max_resolution": "2160p", "video_codec": "av1",
|
||||
"container": "mkv", "prefer_60fps": True, "allow_hdr": False})
|
||||
assert sel["format"].startswith("bv*[height<=2160]")
|
||||
assert sel["format_sort"] == ["vcodec:av1", "res:2160", "fps", "hdr:SDR"]
|
||||
assert sel["merge_output_format"] == "mkv"
|
||||
|
||||
|
||||
def test_format_selection_allow_hdr_drops_the_sdr_rank():
|
||||
sel = format_selection({"max_resolution": "1080p", "video_codec": "any",
|
||||
"prefer_60fps": True, "allow_hdr": True})
|
||||
assert "hdr:SDR" not in sel["format_sort"]
|
||||
|
||||
|
||||
def test_format_selection_normalizes_garbage_input():
|
||||
# a junk profile still yields a valid default selection (never raises)
|
||||
assert format_selection("nope")["format"] == "bv*[height<=1080]+ba/b[height<=1080]/bv*+ba/b"
|
||||
|
|
|
|||
Loading…
Reference in a new issue