mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-06-10 11:12:13 +00:00
Covers RemoteClient/RemoteService, REST routes, handlers, SessionStore, InputBridge, DownloadQueueManager, errors, compression, and serve CLI. E2e tier opts in via --live and can auto-spawn its own serve.
274 lines
7.4 KiB
Python
274 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
|
|
)
|