mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-12 03:55:05 +00:00
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:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user