From d109fe63eb9c9b09a902de266f78662ac5285a14 Mon Sep 17 00:00:00 2001 From: imSp4rky Date: Sat, 6 Jun 2026 16:01:54 -0600 Subject: [PATCH] fix(dl): mux hybrid ingredients standalone only when range explicitly requested -r HYBRID alone muxed the HDR10/HDR10+ base layer as a standalone output because only the ingredient DV track was flagged hybrid_base_only. The inverse was also broken: HDR10/HDR10+/DV tracks never entered the standalone-deliverable pool, so -r HYBRID,HDR10P only delivered the standalone HDR10+ by accident of the first bug. - Add Tracks.partition_hybrid_videos: ingredient ranges (HDR10/HDR10+/DV) enter the deliverable pool only when their range is explicitly requested alongside HYBRID; replaces the duplicated filter in dl.py. - Add Tracks.flag_hybrid_ingredients: any track in the hybrid selection but not in the deliverable selection is flagged hybrid_base_only; replaces and generalises the DV-only dv_is_deliverable special case. --- tests/tracks/test_hybrid_selection.py | 97 ++++++++++++++++++++++++++- unshackle/commands/dl.py | 44 +++--------- unshackle/core/tracks/tracks.py | 26 +++++++ 3 files changed, 130 insertions(+), 37 deletions(-) diff --git a/tests/tracks/test_hybrid_selection.py b/tests/tracks/test_hybrid_selection.py index d0efa68..5f128d0 100644 --- a/tests/tracks/test_hybrid_selection.py +++ b/tests/tracks/test_hybrid_selection.py @@ -6,10 +6,14 @@ Covers the selection primitives that back `-r ...,DV,HYBRID` downloads: single lowest DV track used as a hybrid ingredient. - ``Tracks.merge_video_selections`` — de-duplicates the ingredient/deliverable overlap so a DV track that is chosen as both is not muxed/downloaded twice. +- ``Tracks.partition_hybrid_videos`` — splits the ladder into hybrid-ingredient + candidates and the standalone-deliverable pool; HDR10/HDR10+/DV only enter + the pool when their range was explicitly requested alongside HYBRID. +- ``Tracks.flag_hybrid_ingredients`` — marks ingredient-only tracks with + ``hybrid_base_only`` so the standalone mux loop skips them. -The full ``dl`` selection pipeline (range filtering, the ``dv_is_deliverable`` -partition, the ``hybrid_base_only`` ingredient flag and the standalone mux loop) -is orchestration glue inside the Click command; these tests lock down the pure +The remaining ``dl`` glue (the Cartesian deliverable product and the mux loop) +is orchestration inside the Click command; these tests lock down the pure units it relies on plus the documented end-state of a realistic ATV-style ladder. """ @@ -137,6 +141,93 @@ def test_merge_dedup_uses_track_identity_by_id() -> None: assert len(Tracks.merge_video_selections([a], [b])) == 1 +# --------------------------------------------------------------------------- +# partition_hybrid_videos +# --------------------------------------------------------------------------- + + +def test_partition_hybrid_only_keeps_ingredients_out_of_pool(ladder: list[Video]) -> None: + """`-r HYBRID`: HDR10+/DV are ingredients only; pool holds just SDR.""" + candidates, pool = Tracks.partition_hybrid_videos(ladder, []) + assert ids(candidates) == {"hdr10p-2160", "hdr10p-1080", "dv-2160", "dv-1080", "dv-360"} + assert ids(pool) == {"sdr-2160", "sdr-1080-avc", "sdr-1080-hevc"} + + +def test_partition_admits_hdr10p_to_pool_when_requested(ladder: list[Video]) -> None: + """`-r HYBRID,HDR10P`: HDR10+ tracks become standalone deliverable candidates.""" + candidates, pool = Tracks.partition_hybrid_videos(ladder, [Video.Range.HDR10P]) + assert {"hdr10p-2160", "hdr10p-1080"} <= ids(pool) + assert not any(t.range == Video.Range.DV for t in pool) + # Candidates are unaffected by the requested ranges. + assert ids(candidates) == {"hdr10p-2160", "hdr10p-1080", "dv-2160", "dv-1080", "dv-360"} + + +def test_partition_admits_dv_and_hdr10p_when_both_requested(ladder: list[Video]) -> None: + """`-r HYBRID,HDR10P,DV`: both ranges enter the deliverable pool.""" + _, pool = Tracks.partition_hybrid_videos(ladder, [Video.Range.HDR10P, Video.Range.DV]) + assert {"hdr10p-2160", "hdr10p-1080", "dv-2160", "dv-1080", "dv-360"} <= ids(pool) + + +def test_partition_hdr10_requested_does_not_admit_hdr10p() -> None: + H = Video.Codec.HEVC + tracks = [ + make_video("hdr10-2160", range_=Video.Range.HDR10, height=2160, bitrate=20_000_000, codec=H), + make_video("hdr10p-2160", range_=Video.Range.HDR10P, height=2160, bitrate=20_000_000, codec=H), + ] + _, pool = Tracks.partition_hybrid_videos(tracks, [Video.Range.HDR10]) + assert ids(pool) == {"hdr10-2160"} + + +# --------------------------------------------------------------------------- +# flag_hybrid_ingredients +# --------------------------------------------------------------------------- + + +def flagged(tracks: list[Video]) -> set[str]: + return {t.id for t in tracks if t.hybrid_base_only} + + +def test_flag_hybrid_only_flags_base_and_ingredient_dv(ladder: list[Video]) -> None: + """`-r HYBRID`: no deliverables, so the base and the ingredient DV are both + skipped by the standalone mux loop — only the hybrid output remains.""" + hybrid_selected = list(filter(Tracks().select_hybrid(ladder, [1080]), ladder)) + Tracks.flag_hybrid_ingredients(hybrid_selected, []) + assert flagged(ladder) == {"hdr10p-1080", "dv-360"} + + +def test_flag_hybrid_plus_hdr10p_keeps_base_deliverable(ladder: list[Video]) -> None: + """`-r HYBRID,HDR10P`: the base is also an explicit deliverable, only the + ingredient DV is skipped — hybrid + standalone HDR10+ are muxed.""" + hybrid_selected = list(filter(Tracks().select_hybrid(ladder, [1080]), ladder)) + base = next(t for t in ladder if t.id == "hdr10p-1080") + Tracks.flag_hybrid_ingredients(hybrid_selected, [base]) + assert flagged(ladder) == {"dv-360"} + + +def test_flag_hybrid_plus_hdr10p_and_dv_keeps_both_deliverables(ladder: list[Video]) -> None: + """`-r HYBRID,HDR10P,DV`: best DV is a deliverable, lowest DV stays + ingredient-only — hybrid + HDR10+ + DV are muxed.""" + hybrid_selected = list(filter(Tracks().select_hybrid(ladder, [1080]), ladder)) + base = next(t for t in ladder if t.id == "hdr10p-1080") + best_dv = next(t for t in ladder if t.id == "dv-1080") + Tracks.flag_hybrid_ingredients(hybrid_selected, [base, best_dv]) + assert flagged(ladder) == {"dv-360"} + + +def test_flag_single_dv_rendition_as_deliverable_stays_unflagged() -> None: + """`-r HYBRID,DV` with one DV rendition: the same track is ingredient and + deliverable, so it must still be muxed standalone.""" + H = Video.Codec.HEVC + tracks = [ + make_video("hdr10p-1080", range_=Video.Range.HDR10P, height=1080, bitrate=9_000_000, codec=H), + make_video("dv-1080", range_=Video.Range.DV, height=1080, bitrate=9_000_000, codec=H), + ] + hybrid_selected = list(filter(Tracks().select_hybrid(tracks, [1080]), tracks)) + dv = next(t for t in tracks if t.id == "dv-1080") + Tracks.flag_hybrid_ingredients(hybrid_selected, [dv]) + assert flagged(tracks) == {"hdr10p-1080"} + + # --------------------------------------------------------------------------- # documented end-state for the reported command # --------------------------------------------------------------------------- diff --git a/unshackle/commands/dl.py b/unshackle/commands/dl.py index 7bc7fda..1829678 100644 --- a/unshackle/commands/dl.py +++ b/unshackle/commands/dl.py @@ -1697,24 +1697,12 @@ class dl: has_hybrid = any(r == Video.Range.HYBRID for r in range_) non_hybrid_ranges = [r for r in range_ if r != Video.Range.HYBRID] - # DV is both a hybrid ingredient (lowest track) and, when explicitly - # requested, a standalone deliverable (best track per resolution). - dv_is_deliverable = Video.Range.DV in non_hybrid_ranges - if quality: missing_resolutions = [] if has_hybrid: - hybrid_candidate_tracks = [ - v - for v in title.tracks.videos - if v.range in (Video.Range.HDR10, Video.Range.HDR10P, Video.Range.DV) - ] - non_hybrid_tracks = [ - v - for v in title.tracks.videos - if v.range not in (Video.Range.HDR10, Video.Range.HDR10P, Video.Range.DV) - or (dv_is_deliverable and v.range == Video.Range.DV) - ] + hybrid_candidate_tracks, non_hybrid_tracks = Tracks.partition_hybrid_videos( + title.tracks.videos, non_hybrid_ranges + ) hybrid_filter = title.tracks.select_hybrid(hybrid_candidate_tracks, quality, worst=worst) hybrid_selected = list(filter(hybrid_filter, hybrid_candidate_tracks)) @@ -1757,17 +1745,9 @@ class dl: pre_hybrid_videos: list[Video] = list(title.tracks.videos) if has_hybrid else [] if has_hybrid: # Apply hybrid selection for HYBRID tracks - hybrid_candidate_tracks = [ - v - for v in title.tracks.videos - if v.range in (Video.Range.HDR10, Video.Range.HDR10P, Video.Range.DV) - ] - non_hybrid_tracks = [ - v - for v in title.tracks.videos - if v.range not in (Video.Range.HDR10, Video.Range.HDR10P, Video.Range.DV) - or (dv_is_deliverable and v.range == Video.Range.DV) - ] + hybrid_candidate_tracks, non_hybrid_tracks = Tracks.partition_hybrid_videos( + title.tracks.videos, non_hybrid_ranges + ) if not quality: best_resolution = max((v.height for v in hybrid_candidate_tracks), default=None) @@ -1811,14 +1791,10 @@ class dl: title.tracks.videos = Tracks.merge_video_selections(hybrid_selected, non_hybrid_selected) - # Flag the lowest DV track as ingredient-only so mux skips it standalone, - # unless it is itself the chosen DV deliverable (single DV rendition). - selected_dv = [v for v in title.tracks.videos if v.range == Video.Range.DV] - if selected_dv: - ingredient_dv = min(selected_dv, key=lambda v: v.height) - deliverable_dv = [v for v in non_hybrid_selected if v.range == Video.Range.DV] - if not (dv_is_deliverable and ingredient_dv in deliverable_dv): - ingredient_dv.hybrid_base_only = True + # Flag tracks selected only as hybrid ingredients (the HDR base and/or + # the lowest DV) so the standalone mux loop skips them. Tracks also + # picked as explicit deliverables stay unflagged. + Tracks.flag_hybrid_ingredients(hybrid_selected, non_hybrid_selected) else: selected_videos: list[Video] = [] if video_multi_lang: diff --git a/unshackle/core/tracks/tracks.py b/unshackle/core/tracks/tracks.py index afb1d56..5ed58d9 100644 --- a/unshackle/core/tracks/tracks.py +++ b/unshackle/core/tracks/tracks.py @@ -339,6 +339,32 @@ class Tracks: merged.append(video) return merged + @staticmethod + def partition_hybrid_videos( + videos: list[Video], non_hybrid_ranges: list[Video.Range] + ) -> tuple[list[Video], list[Video]]: + """Split videos into hybrid-ingredient candidates and the standalone-deliverable pool. + + HDR10/HDR10+/DV tracks are hybrid ingredients; they only enter the standalone + pool when their range was explicitly requested alongside HYBRID, so e.g. + `-r HYBRID` muxes only the hybrid while `-r HYBRID,HDR10P` also delivers HDR10+. + """ + ingredient_ranges = (Video.Range.HDR10, Video.Range.HDR10P, Video.Range.DV) + hybrid_candidates = [v for v in videos if v.range in ingredient_ranges] + non_hybrid = [v for v in videos if v.range not in ingredient_ranges or v.range in non_hybrid_ranges] + return hybrid_candidates, non_hybrid + + @staticmethod + def flag_hybrid_ingredients(hybrid_selected: list[Video], non_hybrid_selected: list[Video]) -> None: + """Mark tracks selected only as hybrid ingredients so the standalone mux loop skips them. + + A track that was also picked as an explicit deliverable (same track in both + selections) stays unflagged and is muxed standalone alongside the hybrid. + """ + for video in hybrid_selected: + if video not in non_hybrid_selected: + video.hybrid_base_only = True + def select_hybrid(self, tracks, quality, worst: bool = False): # Prefer HDR10+ over HDR10 as the base layer (preserves dynamic metadata) base_ranges = (Video.Range.HDR10P, Video.Range.HDR10)