Files
unshackle/tests/tracks/test_subtitle_skip.py
Avi Cohen 41fef5d33a fix(dl): make a failed subtitle non-fatal under --skip-subtitle-errors
A single failing subtitle track previously aborted the whole download. Add an opt-in
--skip-subtitle-errors flag: when set, a Subtitle failure is logged and the track dropped from
the mux while the video/audio still complete (Video/Audio failures stay fatal; default
behaviour is unchanged).

Done at the right layer to avoid the shared-event race: a failed track sets the process-global
DOWNLOAD_CANCELLED event, which makes other in-flight tracks early-return without raising — so a
skipped subtitle could otherwise silently truncate the video/audio that still got muxed. The
download is split into two passes (download_tracks_in_passes): the fatal tracks download
concurrently first, then the skippable subtitles run in a separate sequential pass once nothing
else is in flight, with the event reset before each and at the start of every title. Skipped
languages are recorded on the dl instance (skipped_subtitles) for callers to surface.

Adds tests for the cancel-event interaction (a failing subtitle no longer truncates the
video/audio), the good/bad subtitle mix, the flag-off fatal path, and the per-title reset.
2026-06-04 08:06:23 +03:00

142 lines
4.5 KiB
Python

"""Tests for ``download_tracks_in_passes`` — the two-pass track download used by
``dl.result()`` when ``--skip-subtitle-errors`` is set.
The behaviour under test is the cancel-event interaction: a failed track sets the
process-global ``DOWNLOAD_CANCELLED`` event, which makes other in-flight tracks
early-return without raising. Running the fatal video/audio tracks to completion
*before* the skippable subtitles removes that race, so a subtitle failure can no
longer truncate the video/audio that still gets muxed.
"""
from __future__ import annotations
import pytest
from unshackle.commands.dl import download_tracks_in_passes
from unshackle.core.constants import DOWNLOAD_CANCELLED
from unshackle.core.tracks import Audio, Subtitle, Video
def make_video(track_id: str = "v") -> Video:
return Video(
id_=track_id,
url=f"https://example.test/{track_id}.m3u8",
language="en",
codec=Video.Codec.AVC,
range_=Video.Range.SDR,
width=1920,
height=1080,
bitrate=5_000_000,
)
def make_audio(track_id: str = "a") -> Audio:
return Audio(
id_=track_id,
url=f"https://example.test/{track_id}.m3u8",
language="en",
codec=Audio.Codec.AAC,
bitrate=128_000,
)
def make_subtitle(track_id: str, language: str) -> Subtitle:
return Subtitle(
id_=track_id,
url=f"https://example.test/{track_id}.vtt",
language=language,
codec=Subtitle.Codec.WebVTT,
)
class Harness:
"""Mimics how ``dl.result`` drives the helper, with a controllable downloader.
``run_one`` mirrors ``Track.download``: it early-returns (without recording a
completion) when the cancel event is already set, and a track flagged to fail
sets the event and raises — exactly what the real cancel sites do.
"""
def __init__(self, fail_ids: set[str]):
self.fail_ids = fail_ids
self.completed: list[str] = []
self.early_returned: list[str] = []
self.skipped: list[Subtitle] = []
def run_one(self, track, _index):
if DOWNLOAD_CANCELLED.is_set():
self.early_returned.append(track.id)
return
if track.id in self.fail_ids:
DOWNLOAD_CANCELLED.set()
raise RuntimeError(f"{track.id} failed")
self.completed.append(track.id)
def on_subtitle_skipped(self, track):
self.skipped.append(track)
@pytest.fixture(autouse=True)
def _clear_event():
DOWNLOAD_CANCELLED.clear()
yield
DOWNLOAD_CANCELLED.clear()
def test_failed_subtitle_does_not_truncate_video_or_audio():
"""A subtitle that fails *and sets the cancel event* must not stop the video/audio."""
video, audio = make_video(), make_audio()
sub = make_subtitle("s-he", "he")
h = Harness(fail_ids={"s-he"})
download_tracks_in_passes(
[video, audio, sub], 4, h.run_one,
skip_subtitle_errors=True, on_subtitle_skipped=h.on_subtitle_skipped,
)
assert set(h.completed) == {"v", "a"} # both fatal tracks fully downloaded
assert h.early_returned == [] # nothing early-returned because of the subtitle
assert h.skipped == [sub] # the subtitle was recorded as skipped
def test_good_subtitle_kept_bad_subtitle_skipped():
video = make_video()
good, bad = make_subtitle("s-en", "en"), make_subtitle("s-fr", "fr")
h = Harness(fail_ids={"s-fr"})
download_tracks_in_passes(
[video, good, bad], 4, h.run_one,
skip_subtitle_errors=True, on_subtitle_skipped=h.on_subtitle_skipped,
)
assert "s-en" in h.completed # the available subtitle still downloaded
assert h.skipped == [bad] # only the failing one was skipped
def test_subtitle_failure_stays_fatal_without_flag():
"""Default behaviour (flag off) is unchanged: a subtitle failure aborts the title."""
video = make_video()
sub = make_subtitle("s-he", "he")
h = Harness(fail_ids={"s-he"})
with pytest.raises(RuntimeError):
download_tracks_in_passes(
[video, sub], 4, h.run_one,
skip_subtitle_errors=False, on_subtitle_skipped=h.on_subtitle_skipped,
)
def test_cancel_event_is_reset_between_titles():
"""A cancel left set by a previous title must not skip this title's tracks."""
DOWNLOAD_CANCELLED.set()
video, audio = make_video(), make_audio()
h = Harness(fail_ids=set())
download_tracks_in_passes(
[video, audio], 4, h.run_one,
skip_subtitle_errors=True, on_subtitle_skipped=h.on_subtitle_skipped,
)
assert set(h.completed) == {"v", "a"}
assert h.early_returned == []