mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-12 03:55:05 +00:00
Fix DASH SegmentTimeline negative @r and Pattern, TTML sidecar sets with no contentType, HLS CHANNELS identifiers and Dolby channel masks; test against 408 public DASH/HLS/ISM manifests.
175 lines
6.9 KiB
Python
175 lines
6.9 KiB
Python
"""Run the kind-only filters through the real dl.result() selection loop.
|
|
|
|
A fake service hands in-memory tracks to the loop, and the run stops at the first
|
|
title that reaches ``Tracks.tree``, the step right after selection. A title with no
|
|
track of the kind never gets there, so what the hook records is what the run kept.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import inspect
|
|
import logging
|
|
from typing import Any, Optional, cast
|
|
|
|
import pytest
|
|
|
|
from unshackle.commands.dl import dl
|
|
from unshackle.core.api.handlers import DEFAULT_DOWNLOAD_PARAMS
|
|
from unshackle.core.service import Service
|
|
from unshackle.core.titles import Episode, Series
|
|
from unshackle.core.tracks import Audio, Subtitle, Tracks, Video
|
|
|
|
|
|
class Selected(BaseException):
|
|
"""Carries the kept track ids out of the loop."""
|
|
|
|
def __init__(self, tracks: Tracks) -> None:
|
|
self.ids = {
|
|
"video": [t.id for t in tracks.videos],
|
|
"audio": [t.id for t in tracks.audio],
|
|
"subs": [t.id for t in tracks.subtitles],
|
|
}
|
|
|
|
|
|
class Harness(dl):
|
|
def __getattr__(self, name: str) -> None:
|
|
# every optional dl attribute (tmdb ids, cdm, vaults, ...) reads as unset
|
|
return None
|
|
|
|
|
|
class FakeService:
|
|
"""One episode per entry; ``ad`` adds a descriptive track, ``forced`` a forced subtitle."""
|
|
|
|
def __init__(self, episodes: dict[int, dict[str, bool]]) -> None:
|
|
self.episodes = episodes
|
|
self.session = None
|
|
|
|
def authenticate(self, cookies: Any = None, credential: Any = None) -> None:
|
|
pass
|
|
|
|
def get_titles_cached(self) -> Series:
|
|
return Series(
|
|
Episode(id_=f"show-s01-e{n:03}", service=FakeService, title="Show", season=1, number=n, language="en")
|
|
for n in self.episodes
|
|
)
|
|
|
|
def get_tracks(self, title: Episode) -> Tracks:
|
|
kinds = self.episodes[title.number]
|
|
tracks = Tracks()
|
|
tracks.add(Video(id_="v", url="https://x/v", language="en", codec=Video.Codec.AVC, range_=Video.Range.SDR))
|
|
tracks.add(Audio(id_="a", url="https://x/a", language="en", codec=Audio.Codec.AAC, bitrate=128_000))
|
|
if kinds.get("ad"):
|
|
tracks.add(
|
|
Audio(id_="ad", url="https://x/ad", language="en", codec=Audio.Codec.AAC, bitrate=1, descriptive=True)
|
|
)
|
|
tracks.add(Subtitle(id_="s", url="https://x/s", language="en", codec=Subtitle.Codec.WebVTT))
|
|
if kinds.get("forced"):
|
|
tracks.add(Subtitle(id_="sf", url="https://x/sf", language="en", codec=Subtitle.Codec.WebVTT, forced=True))
|
|
return tracks
|
|
|
|
def get_chapters(self, title: Episode) -> list:
|
|
return []
|
|
|
|
def __getattr__(self, name: str) -> Any:
|
|
if name.startswith("on_"):
|
|
# the event hooks (on_track_downloaded, ...) the loop subscribes
|
|
return lambda *a, **k: None
|
|
raise AttributeError(name)
|
|
|
|
|
|
def run(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
caplog: pytest.LogCaptureFixture,
|
|
episodes: dict,
|
|
service: Optional[FakeService] = None,
|
|
**flags: Any,
|
|
) -> Any:
|
|
"""Return the Selected ids, the exit code, or None for a run that completes."""
|
|
monkeypatch.setattr(Tracks, "tree", lambda self, *a, **k: (_ for _ in ()).throw(Selected(self)))
|
|
monkeypatch.setattr("unshackle.core.providers.search_metadata", lambda *a, **k: None)
|
|
cmd = Harness.__new__(Harness)
|
|
cmd.log = logging.getLogger("test_kind_only")
|
|
cmd.service = "FAKE"
|
|
cmd.tmdb_searched = True
|
|
cmd.completed_files = []
|
|
kwargs: dict[str, Any] = {}
|
|
for name, param in inspect.signature(dl.result).parameters.items():
|
|
if name in DEFAULT_DOWNLOAD_PARAMS:
|
|
kwargs[name] = DEFAULT_DOWNLOAD_PARAMS[name]
|
|
elif name not in ("self", "service"):
|
|
kwargs[name] = None if param.default is inspect.Parameter.empty else param.default
|
|
kwargs.update(quality=[], vcodec=[], acodec=[], range_=[], wanted=[], cdm_only=None, downloads=1)
|
|
kwargs.update(flags)
|
|
caplog.set_level(logging.WARNING, logger="test_kind_only")
|
|
try:
|
|
cmd.result(cast(Service, service or FakeService(episodes)), **kwargs)
|
|
except Selected as sel:
|
|
return sel.ids
|
|
except SystemExit as exc:
|
|
return exc.code
|
|
return None
|
|
|
|
|
|
def test_ado_keeps_the_descriptive_track_and_the_other_types(
|
|
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
|
|
) -> None:
|
|
ids = run(monkeypatch, caplog, {1: {"ad": True, "forced": True}}, audio_description_only=True)
|
|
assert ids == {"video": ["v"], "audio": ["ad"], "subs": ["s"]}
|
|
|
|
|
|
def test_fso_keeps_the_forced_track_and_the_other_types(
|
|
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
|
|
) -> None:
|
|
ids = run(monkeypatch, caplog, {1: {"ad": True, "forced": True}}, forced_subs_only=True)
|
|
assert ids == {"video": ["v"], "audio": ["a"], "subs": ["sf"]}
|
|
|
|
|
|
def test_only_flags_with_their_type_flags_give_a_single_kind(
|
|
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
|
|
) -> None:
|
|
ids = run(monkeypatch, caplog, {1: {"ad": True, "forced": True}}, audio_description_only=True, audio_only=True)
|
|
assert ids == {"video": [], "audio": ["ad"], "subs": []}
|
|
ids = run(monkeypatch, caplog, {1: {"ad": True, "forced": True}}, forced_subs_only=True, subs_only=True)
|
|
assert ids == {"video": [], "audio": [], "subs": ["sf"]}
|
|
|
|
|
|
@pytest.mark.parametrize("flag", ["audio_description_only", "forced_subs_only"])
|
|
def test_a_single_title_with_no_such_track_exits(
|
|
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, flag: str
|
|
) -> None:
|
|
assert run(monkeypatch, caplog, {1: {}}, **{flag: True}) == 1
|
|
assert "Skipping" not in caplog.text
|
|
|
|
|
|
def test_a_multi_title_run_skips_the_title_and_continues(
|
|
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
|
|
) -> None:
|
|
ids = run(monkeypatch, caplog, {1: {}, 2: {"ad": True}}, audio_description_only=True)
|
|
assert ids["audio"] == ["ad"]
|
|
assert "Skipping Show S01E01" in caplog.text
|
|
|
|
|
|
def test_a_multi_title_run_with_every_title_skipped_completes(
|
|
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
|
|
) -> None:
|
|
assert run(monkeypatch, caplog, {1: {}, 2: {}}, forced_subs_only=True) is None
|
|
assert caplog.text.count("Skipping Show S01E") == 2
|
|
|
|
|
|
class LcevcService(FakeService):
|
|
"""A 540p base video and the 1080p LCEVC enhancement layer that decodes on top of it."""
|
|
|
|
def get_tracks(self, title: Episode) -> Tracks:
|
|
tracks = super().get_tracks(title)
|
|
tracks.videos[0].height = 540
|
|
tracks.add(Video(id_="lcevc", url="https://x/l", language="en", codec=Video.Codec.LCEVC, height=1080))
|
|
return tracks
|
|
|
|
|
|
def test_an_lcevc_layer_is_left_out_unless_asked_for(
|
|
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
|
|
) -> None:
|
|
assert run(monkeypatch, caplog, {1: {}}, service=LcevcService({1: {}}))["video"] == ["v"]
|
|
asked = run(monkeypatch, caplog, {1: {}}, service=LcevcService({1: {}}), vcodec=[Video.Codec.LCEVC])
|
|
assert asked["video"] == ["lcevc"]
|