fix(tracks): match resolutions per range and codec

An exact height match in one range denied the 16:9 canvas fallback to the others, so a cropped HDR10 or SDR track was dropped whenever an uncropped DV track matched the requested height. Also sweep the hybrid L5 temp files so a killed run cannot leave a stale RPU behind.
This commit is contained in:
imSp4rky
2026-08-27 14:32:36 -06:00
parent dcc1c8af18
commit 4ef577c1d1
4 changed files with 44 additions and 16 deletions
+4 -1
View File
@@ -86,7 +86,10 @@ unshackle dl -q 2160,1080,720 EXAMPLE 81234567
!!! note "16:9 canvas matching"
Resolution matching is by track **height** first. If no track height matches exactly,
unshackle falls back to a 16:9-canvas match, so `-q 1080` also selects an
anamorphic `1920×804` track (computed as `int(width × 9 / 16)`).
anamorphic `1920×804` track (computed as `int(width × 9 / 16)`). unshackle applies the
fallback to each range and codec on its own. A service that crops only its HDR10 tracks
therefore keeps its `3840×1920` HDR10 track, even when another range has an exact
`2160` height match.
### Best-available and worst
+19 -2
View File
@@ -3,7 +3,8 @@
Cover the pure ``Tracks`` helpers behind the ``dl`` selection layer:
- ``Tracks.by_resolutions``: the 16:9 canvas fallback must tolerate tracks with
no known width instead of crashing on ``int(None * 9 / 16)``.
no known width instead of crashing on ``int(None * 9 / 16)``, and it applies to
each range and codec on its own.
- ``Tracks.sort_videos``: resolution outranks bitrate, and an unknown-bitrate
track can still win on resolution.
- ``Tracks.sort_audio``: descriptive-last, Atmos, codec_priority then bitrate;
@@ -22,13 +23,14 @@ def make_video(
bitrate: int | None,
width: int | None = None,
codec: Video.Codec = Video.Codec.HEVC,
range_: Video.Range = Video.Range.SDR,
) -> Video:
return Video(
id_=track_id,
url=f"https://example.test/{track_id}.m3u8",
language="en",
codec=codec,
range_=Video.Range.SDR,
range_=range_,
width=width,
height=height,
bitrate=bitrate,
@@ -71,6 +73,21 @@ def test_by_resolutions_tolerates_width_none_track() -> None:
assert [t.id for t in tracks.videos] == ["canvas-720"]
def test_by_resolutions_keeps_cropped_variants_of_other_ranges() -> None:
"""A DV track with an exact height must not remove the cropped HDR10 and SDR tracks."""
tracks = Tracks()
tracks.videos = [
make_video("dv-2160", height=2160, width=3840, bitrate=20_000_000, range_=Video.Range.DV),
make_video("hdr10-2160", height=1920, width=3840, bitrate=20_000_000, range_=Video.Range.HDR10),
make_video("sdr-2160", height=1920, width=3840, bitrate=15_000_000, range_=Video.Range.SDR),
make_video("dv-1080", height=1080, width=1920, bitrate=5_000_000, range_=Video.Range.DV),
]
tracks.by_resolutions([2160])
assert [t.id for t in tracks.videos] == ["dv-2160", "hdr10-2160", "sdr-2160"]
# sort_videos: resolution before bitrate
+10 -1
View File
@@ -3473,7 +3473,16 @@ class dl:
temp_path.unlink(missing_ok=True)
except PermissionError:
self.log.warning(f"Failed to delete temp file (in use?): {temp_path}")
for leftover in ("HDR10-DV.hevc", "DV.hevc", "RPU.bin", "RPU_UNT.bin", "RPU_L6.bin"):
for leftover in (
"HDR10-DV.hevc",
"HDR10.hevc",
"DV.hevc",
"RPU.bin",
"RPU_UNT.bin",
"RPU_L5.bin",
"RPU_L6.bin",
"L5.json",
):
leftover_path = config.directories.temp / leftover
try:
leftover_path.unlink(missing_ok=True)
+11 -12
View File
@@ -455,19 +455,18 @@ class Tracks:
return select
def by_resolutions(self, resolutions: list[int], per_resolution: int = 0) -> None:
# Note: Do not merge these list comprehensions. They must be done separately so the results
# from the 16:9 canvas check is only used if there's no exact height resolution match.
selected = []
groups: dict[tuple, list[Video]] = {}
for video in self.videos:
groups.setdefault((video.range, video.codec), []).append(video)
keep: set[int] = set()
for resolution in resolutions:
matches = [ # exact matches
x for x in self.videos if x.height == resolution
]
if not matches:
matches = [ # 16:9 canvas matches
x for x in self.videos if x.width and int(x.width * (9 / 16)) == resolution
]
selected.extend(matches[: per_resolution or None])
self.videos = selected
for group in groups.values():
matches = [x for x in group if x.height == resolution]
if not matches:
matches = [x for x in group if x.width and int(x.width * (9 / 16)) == resolution]
keep.update(id(x) for x in matches[: per_resolution or None])
self.videos = [x for x in self.videos if id(x) in keep]
@staticmethod
def by_language(