Files
unshackle/tests/tracks/test_track_selection_fixes.py
T
imSp4rky 6434fc2081 fix(dl): make audio/video track selection consistent and crash-safe
Share one best-audio selector across the episode and music paths so best/all works for songs, descriptive audio is capped to one track, and codec_priority is honored. Guard --channels and 16:9 canvas math against tracks missing channel/width metadata. Extend --best-available to video languages and full resolution misses in both normal and hybrid mode, and count only deliverable ranges when reporting missing resolutions. Rank video by resolution before bitrate, and trim group audio selections to one track per song with a warning.

BREAKING CHANGE: a missing resolution with -r HYBRID now errors without --best-available, matching non-hybrid behavior.
2026-07-11 18:41:41 -06:00

139 lines
4.4 KiB
Python

"""Unit tests for the audio/video track-selection primitives.
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)``.
- ``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;
the first track after sorting is the "best" pick the dl layer selects.
"""
from __future__ import annotations
from unshackle.core.tracks import Audio, Tracks, Video
def make_video(
track_id: str,
*,
height: int,
bitrate: int | None,
width: int | None = None,
codec: Video.Codec = Video.Codec.HEVC,
) -> Video:
return Video(
id_=track_id,
url=f"https://example.test/{track_id}.m3u8",
language="en",
codec=codec,
range_=Video.Range.SDR,
width=width,
height=height,
bitrate=bitrate,
)
def make_audio(
track_id: str,
*,
codec: Audio.Codec,
bitrate: int,
joc: int | None = None,
descriptive: bool = False,
language: str = "en",
) -> Audio:
return Audio(
id_=track_id,
url=f"https://example.test/{track_id}.m4a",
language=language,
codec=codec,
bitrate=bitrate,
joc=joc,
descriptive=descriptive,
)
# ---------------------------------------------------------------------------
# by_resolutions: width=None must not crash the canvas fallback
# ---------------------------------------------------------------------------
def test_by_resolutions_tolerates_width_none_track() -> None:
tracks = Tracks()
tracks.videos = [
make_video("h1080", height=1080, width=1920, bitrate=8_000_000), # no exact/canvas 720 match
make_video("no-width", height=480, width=None, bitrate=1_000_000), # width-less, must be skipped
make_video("canvas-720", height=406, width=1280, bitrate=3_000_000), # 1280*9/16 == 720
]
tracks.by_resolutions([720])
assert [t.id for t in tracks.videos] == ["canvas-720"]
# ---------------------------------------------------------------------------
# sort_videos: resolution before bitrate
# ---------------------------------------------------------------------------
def test_sort_videos_resolution_outranks_bitrate() -> None:
tracks = Tracks()
tracks.videos = [
make_video("hi-bitrate-720", height=720, bitrate=10_000_000, width=1280),
make_video("lo-bitrate-2160", height=2160, bitrate=5_000_000, width=3840),
]
tracks.sort_videos()
assert [t.id for t in tracks.videos] == ["lo-bitrate-2160", "hi-bitrate-720"]
def test_sort_videos_unknown_bitrate_still_wins_on_resolution() -> None:
tracks = Tracks()
tracks.videos = [
make_video("known-720", height=720, bitrate=10_000_000, width=1280),
make_video("unknown-1080", height=1080, bitrate=None, width=1920),
]
tracks.sort_videos()
assert [t.id for t in tracks.videos] == ["unknown-1080", "known-720"]
# ---------------------------------------------------------------------------
# sort_audio: first track after sort is the "best" the dl layer picks
# ---------------------------------------------------------------------------
def test_sort_audio_atmos_ranks_first() -> None:
tracks = Tracks()
tracks.audio = [
make_audio("ec3-320", codec=Audio.Codec.EC3, bitrate=320_000),
make_audio("ac3-640", codec=Audio.Codec.AC3, bitrate=640_000),
make_audio("ec3-atmos", codec=Audio.Codec.EC3, bitrate=768_000, joc=16),
make_audio("ec3-desc", codec=Audio.Codec.EC3, bitrate=768_000, descriptive=True),
]
tracks.sort_audio(codec_priority=["EC3", "AC3"])
best = Tracks.by_language(tracks.audio, ["en"])[0]
assert best.id == "ec3-atmos"
# descriptive always sorts last
assert tracks.audio[-1].id == "ec3-desc"
def test_sort_audio_codec_priority_beats_bitrate() -> None:
tracks = Tracks()
tracks.audio = [
make_audio("ac3-640", codec=Audio.Codec.AC3, bitrate=640_000),
make_audio("ec3-320", codec=Audio.Codec.EC3, bitrate=320_000),
]
tracks.sort_audio(codec_priority=["EC3", "AC3"])
best = Tracks.by_language(tracks.audio, ["en"])[0]
# EC3 is prioritized even though the AC3 track has the higher bitrate
assert best.id == "ec3-320"