Files
unshackle/tests/remote/unit/test_handlers_serialize.py
imSp4rky 7654e91ebc feat(dl): gate s_lang/a_lang miss behind --best-available
Missing requested subtitle and audio languages now warn and continue when --best-available is set instead of hard-exiting. Without the flag, missing languages still produce an error and exit, matching the prior strict behavior. Audio missing-lang detection is now symmetric with subtitles.

- add find_missing_langs helper in core/utilities for reuse between s_lang and a_lang paths (skips all/best/orig sentinels)
- refactor dl.py s_lang/a_lang checks to share the helper
- add tests/lang_selection covering match primitives, helper output, and tricky langcodes corners (zh-Hans/zh-Hant/zh-CN/zh-TW/zh-HK, cmn/yue, fil/tl/tgl)
- clean up unused-var ruff F841 in tests/remote/unit/
2026-05-22 13:52:35 -06:00

267 lines
7.4 KiB
Python

"""Unit tests for unshackle.core.api.handlers serializers + validators."""
from __future__ import annotations
import pytest
from langcodes import Language
from unshackle.core.api.handlers import (sanitize_log, serialize_audio_track, serialize_drm, serialize_subtitle_track,
serialize_title, serialize_video_track, validate_download_parameters,
validate_service)
from unshackle.core.titles.episode import Episode
from unshackle.core.titles.movie import Movie
from unshackle.core.tracks import Audio, Subtitle, Video
from unshackle.core.tracks.track import Track
pytestmark = pytest.mark.unit
class _FakeSvc:
pass
def _video(**overrides) -> Video:
base = dict(
url="https://example.com/v.mpd",
language=Language.get("en"),
descriptor=Track.Descriptor.URL,
codec=Video.Codec.AVC,
range_=Video.Range.SDR,
bitrate=5_000_000,
width=1920,
height=1080,
fps=24,
id_="video-001",
)
base.update(overrides)
return Video(**base)
def _audio(**overrides) -> Audio:
base = dict(
url="https://example.com/a.mpd",
language=Language.get("en"),
descriptor=Track.Descriptor.URL,
codec=Audio.Codec.AAC,
bitrate=128_000,
channels=2,
joc=0,
descriptive=False,
id_="audio-001",
)
base.update(overrides)
return Audio(**base)
def _subtitle(**overrides) -> Subtitle:
base = dict(
url="https://example.com/s.vtt",
language=Language.get("en"),
descriptor=Track.Descriptor.URL,
codec=Subtitle.Codec.WebVTT,
cc=False,
sdh=False,
forced=False,
id_="sub-001",
)
base.update(overrides)
return Subtitle(**base)
# ---------- sanitize_log ----------
@pytest.mark.parametrize(
"raw, expected",
[
("hello\nworld", "helloworld"),
("a\r\nb\x00c", "abc"),
("clean", "clean"),
(12345, "12345"),
],
)
def test_sanitize_log(raw, expected: str) -> None:
assert sanitize_log(raw) == expected
# ---------- serialize_title ----------
def test_serialize_title_movie() -> None:
movie = Movie(id_="movie-0001", service=_FakeSvc, name="Title X", year=2024, language=Language.get("en"))
d = serialize_title(movie)
assert d["type"] == "movie"
assert d["name"] == "Title X"
assert d["year"] == 2024
assert d["id"] == "movie-0001"
assert d["language"] == "en"
def test_serialize_title_episode_named() -> None:
ep = Episode(
id_="ep-00001",
service=_FakeSvc,
title="My Show",
season=2,
number=3,
name="Pilot",
year=2024,
language=Language.get("en"),
)
d = serialize_title(ep)
assert d["type"] == "episode"
assert d["series_title"] == "My Show"
assert d["season"] == 2
assert d["number"] == 3
assert d["name"] == "Pilot"
def test_serialize_title_episode_unnamed_falls_back_to_number() -> None:
ep = Episode(
id_="ep-00002",
service=_FakeSvc,
title="Show",
season=1,
number=5,
name=None,
year=None,
language=Language.get("en"),
)
d = serialize_title(ep)
assert d["name"] == "Episode 05"
# ---------- serialize_video/audio/subtitle ----------
def test_serialize_video_track_basic() -> None:
d = serialize_video_track(_video())
assert d["id"] == "video-001"
assert d["codec"] == "AVC"
assert d["bitrate"] == 5000 # kbps
assert d["resolution"] == "1920x1080"
assert d["fps"] == 24
assert d["range"] == "SDR"
assert d["language"] == "en"
assert d["drm"] is None
assert "url" not in d
def test_serialize_video_track_include_url() -> None:
d = serialize_video_track(_video(), include_url=True)
assert d["url"] == "https://example.com/v.mpd"
def test_serialize_audio_track_basic() -> None:
d = serialize_audio_track(_audio())
assert d["id"] == "audio-001"
assert d["codec"] == "AAC"
assert d["bitrate"] == 128
assert d["channels"] == 2
assert d["descriptive"] is False
def test_serialize_subtitle_track_basic() -> None:
d = serialize_subtitle_track(_subtitle(forced=True))
assert d["id"] == "sub-001"
assert d["codec"] == "WebVTT"
assert d["forced"] is True
assert d["sdh"] is False
assert d["cc"] is False
# ---------- serialize_drm ----------
def test_serialize_drm_none_returns_none() -> None:
assert serialize_drm(None) is None
assert serialize_drm([]) is None
def test_serialize_drm_widevine_minimal() -> None:
class _PSSH:
def dumps(self) -> str:
return "BASE64PSSH=="
class _Widevine:
def __init__(self) -> None:
self._pssh = _PSSH()
self.kids = ["00112233445566778899aabbccddeeff"]
self.license_url = "https://lic.example.com/wv"
out = serialize_drm(_Widevine())
assert isinstance(out, list)
assert len(out) == 1
info = out[0]
assert info["type"] == "_widevine" # class name lowercased
assert info["pssh"] == "BASE64PSSH=="
assert info["kids"] == ["00112233445566778899aabbccddeeff"]
assert info["license_url"] == "https://lic.example.com/wv"
# ---------- validate_service ----------
def test_validate_service_unknown_returns_none() -> None:
assert validate_service("NOPE_THIS_IS_NOT_REAL_") is None
# ---------- validate_download_parameters ----------
def test_validate_download_params_accepts_defaults() -> None:
assert validate_download_parameters({}) is None
@pytest.mark.parametrize(
"data, fragment",
[
({"vcodec": "WUT"}, "Invalid vcodec"),
({"vcodec": 123}, "vcodec must be a string or list"),
({"acodec": "MP9"}, "Invalid acodec"),
({"sub_format": "doc"}, "Invalid sub_format"),
({"vbitrate": -1}, "vbitrate"),
({"abitrate": "no"}, "abitrate"),
({"vbitrate_range": "no-dash-but-letters"}, None),
({"vbitrate_range": "nope"}, "MIN-MAX"),
({"channels": -3}, "channels"),
({"workers": 0}, "workers"),
({"downloads": 0}, "downloads"),
({"video_only": True, "audio_only": True}, "exclusive"),
({"no_subs": True, "subs_only": True}, "no_subs and subs_only"),
({"no_audio": True, "audio_only": True}, "no_audio and audio_only"),
({"s_lang": ["en"], "require_subs": ["en"]}, "s_lang and require_subs"),
({"range": "UHD"}, "Invalid range"),
({"range": ["SDR", "UHD"]}, "Invalid range value"),
],
)
def test_validate_download_params_errors(data: dict, fragment) -> None:
result = validate_download_parameters(data)
if fragment is None:
# A dash-containing string is valid syntactically per current rule
assert result is None
else:
assert result is not None
assert fragment in result
def test_validate_download_params_accepts_valid_values() -> None:
assert (
validate_download_parameters(
{
"vcodec": "H264,H265",
"acodec": ["AAC", "EAC3"],
"sub_format": "VTT",
"vbitrate": 6000,
"abitrate": 128,
"vbitrate_range": "6000-7000",
"abitrate_range": "96-192",
"channels": 5.1,
"workers": 8,
"downloads": 2,
"range": ["SDR", "HDR10"],
}
)
is None
)