mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-06-10 03:02:09 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d109fe63eb |
@@ -6,10 +6,14 @@ Covers the selection primitives that back `-r ...,DV,HYBRID` downloads:
|
|||||||
single lowest DV track used as a hybrid ingredient.
|
single lowest DV track used as a hybrid ingredient.
|
||||||
- ``Tracks.merge_video_selections`` — de-duplicates the ingredient/deliverable
|
- ``Tracks.merge_video_selections`` — de-duplicates the ingredient/deliverable
|
||||||
overlap so a DV track that is chosen as both is not muxed/downloaded twice.
|
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``
|
The remaining ``dl`` glue (the Cartesian deliverable product and the mux loop)
|
||||||
partition, the ``hybrid_base_only`` ingredient flag and the standalone mux loop)
|
is orchestration inside the Click command; these tests lock down the pure
|
||||||
is orchestration glue 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.
|
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
|
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
|
# documented end-state for the reported command
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -1697,24 +1697,12 @@ class dl:
|
|||||||
|
|
||||||
has_hybrid = any(r == Video.Range.HYBRID for r in range_)
|
has_hybrid = any(r == Video.Range.HYBRID for r in range_)
|
||||||
non_hybrid_ranges = [r for r in range_ if r != Video.Range.HYBRID]
|
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:
|
if quality:
|
||||||
missing_resolutions = []
|
missing_resolutions = []
|
||||||
if has_hybrid:
|
if has_hybrid:
|
||||||
hybrid_candidate_tracks = [
|
hybrid_candidate_tracks, non_hybrid_tracks = Tracks.partition_hybrid_videos(
|
||||||
v
|
title.tracks.videos, non_hybrid_ranges
|
||||||
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_filter = title.tracks.select_hybrid(hybrid_candidate_tracks, quality, worst=worst)
|
hybrid_filter = title.tracks.select_hybrid(hybrid_candidate_tracks, quality, worst=worst)
|
||||||
hybrid_selected = list(filter(hybrid_filter, hybrid_candidate_tracks))
|
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 []
|
pre_hybrid_videos: list[Video] = list(title.tracks.videos) if has_hybrid else []
|
||||||
if has_hybrid:
|
if has_hybrid:
|
||||||
# Apply hybrid selection for HYBRID tracks
|
# Apply hybrid selection for HYBRID tracks
|
||||||
hybrid_candidate_tracks = [
|
hybrid_candidate_tracks, non_hybrid_tracks = Tracks.partition_hybrid_videos(
|
||||||
v
|
title.tracks.videos, non_hybrid_ranges
|
||||||
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)
|
|
||||||
]
|
|
||||||
|
|
||||||
if not quality:
|
if not quality:
|
||||||
best_resolution = max((v.height for v in hybrid_candidate_tracks), default=None)
|
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)
|
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,
|
# Flag tracks selected only as hybrid ingredients (the HDR base and/or
|
||||||
# unless it is itself the chosen DV deliverable (single DV rendition).
|
# the lowest DV) so the standalone mux loop skips them. Tracks also
|
||||||
selected_dv = [v for v in title.tracks.videos if v.range == Video.Range.DV]
|
# picked as explicit deliverables stay unflagged.
|
||||||
if selected_dv:
|
Tracks.flag_hybrid_ingredients(hybrid_selected, non_hybrid_selected)
|
||||||
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
|
|
||||||
else:
|
else:
|
||||||
selected_videos: list[Video] = []
|
selected_videos: list[Video] = []
|
||||||
if video_multi_lang:
|
if video_multi_lang:
|
||||||
|
|||||||
@@ -339,6 +339,32 @@ class Tracks:
|
|||||||
merged.append(video)
|
merged.append(video)
|
||||||
return merged
|
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):
|
def select_hybrid(self, tracks, quality, worst: bool = False):
|
||||||
# Prefer HDR10+ over HDR10 as the base layer (preserves dynamic metadata)
|
# Prefer HDR10+ over HDR10 as the base layer (preserves dynamic metadata)
|
||||||
base_ranges = (Video.Range.HDR10P, Video.Range.HDR10)
|
base_ranges = (Video.Range.HDR10P, Video.Range.HDR10)
|
||||||
|
|||||||
Reference in New Issue
Block a user