mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-06-10 11:12:13 +00:00
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/
150 lines
4.9 KiB
Python
150 lines
4.9 KiB
Python
"""Unit tests for module-level helpers in unshackle.core.remote_service."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from enum import Enum
|
|
|
|
import pytest
|
|
|
|
from unshackle.core.remote_service import (_build_title, _build_tracks, _deserialize_audio, _deserialize_subtitle,
|
|
_deserialize_video, _enum_get, _match_track, _reconstruct_drm)
|
|
from unshackle.core.titles.episode import Episode
|
|
from unshackle.core.titles.movie import Movie
|
|
from unshackle.core.tracks import Audio, Subtitle, Video
|
|
|
|
pytestmark = pytest.mark.unit
|
|
|
|
|
|
class _Color(Enum):
|
|
RED = 1
|
|
BLUE = 2
|
|
|
|
|
|
def test_enum_get_known() -> None:
|
|
assert _enum_get(_Color, "RED") is _Color.RED
|
|
|
|
|
|
def test_enum_get_unknown_returns_default() -> None:
|
|
assert _enum_get(_Color, "PURPLE", default=_Color.BLUE) is _Color.BLUE
|
|
|
|
|
|
def test_enum_get_none_returns_default() -> None:
|
|
assert _enum_get(_Color, None, default=_Color.RED) is _Color.RED
|
|
|
|
|
|
def test_deserialize_video_minimal() -> None:
|
|
v = _deserialize_video({"id": "video-1", "codec": "AVC", "width": 1920, "height": 1080, "bitrate": 5000})
|
|
assert isinstance(v, Video)
|
|
assert v.id == "video-1"
|
|
assert v.codec is Video.Codec.AVC
|
|
assert v.bitrate == 5_000_000 # kbps -> bps
|
|
assert v.width == 1920
|
|
assert v.height == 1080
|
|
assert v.range is Video.Range.SDR
|
|
|
|
|
|
def test_deserialize_video_unknown_codec_falls_back_to_none() -> None:
|
|
v = _deserialize_video({"id": "v2", "codec": "MADE_UP", "width": 0, "height": 0})
|
|
assert v.codec is None
|
|
|
|
|
|
def test_deserialize_audio_atmos_flag_sets_joc() -> None:
|
|
a = _deserialize_audio({"id": "a1", "codec": "AAC", "atmos": True, "channels": 6, "bitrate": 256})
|
|
assert isinstance(a, Audio)
|
|
assert a.joc == 1
|
|
assert a.channels == 6
|
|
assert a.bitrate == 256_000
|
|
|
|
|
|
def test_deserialize_audio_no_atmos() -> None:
|
|
a = _deserialize_audio({"id": "a2", "codec": "AAC", "channels": 2})
|
|
assert a.joc == 0
|
|
|
|
|
|
def test_deserialize_subtitle_forced_flag() -> None:
|
|
s = _deserialize_subtitle({"id": "s1", "codec": "WebVTT", "language": "en", "forced": True})
|
|
assert isinstance(s, Subtitle)
|
|
assert s.forced is True
|
|
assert s.sdh is False
|
|
|
|
|
|
def test_deserialize_subtitle_sdh_flag() -> None:
|
|
s = _deserialize_subtitle({"id": "s2", "codec": "WebVTT", "language": "en", "sdh": True})
|
|
assert s.sdh is True
|
|
assert s.forced is False
|
|
|
|
|
|
def test_reconstruct_drm_empty() -> None:
|
|
assert _reconstruct_drm(None) == []
|
|
assert _reconstruct_drm([]) == []
|
|
|
|
|
|
def test_reconstruct_drm_skips_entries_without_pssh() -> None:
|
|
assert _reconstruct_drm([{"type": "widevine"}]) == []
|
|
|
|
|
|
def test_reconstruct_drm_invalid_pssh_silently_dropped() -> None:
|
|
assert _reconstruct_drm([{"type": "widevine", "pssh": "not-real-pssh"}]) == []
|
|
|
|
|
|
def test_build_tracks_aggregates() -> None:
|
|
data = {
|
|
"video": [{"id": "v", "codec": "AVC", "width": 1280, "height": 720, "bitrate": 2500}],
|
|
"audio": [{"id": "a", "codec": "AAC", "channels": 2, "bitrate": 128}],
|
|
"subtitles": [{"id": "s", "codec": "WebVTT", "language": "en"}],
|
|
"attachments": [],
|
|
}
|
|
t = _build_tracks(data)
|
|
assert len(t.videos) == 1
|
|
assert len(t.audio) == 1
|
|
assert len(t.subtitles) == 1
|
|
|
|
|
|
def test_match_track_by_id() -> None:
|
|
a = _deserialize_video({"id": "v1", "codec": "AVC", "width": 1920, "height": 1080})
|
|
b = _deserialize_video({"id": "v2", "codec": "AVC", "width": 1280, "height": 720})
|
|
remote = _deserialize_video({"id": "v2", "codec": "AVC", "width": 1280, "height": 720})
|
|
assert _match_track(remote, [a, b]) is b
|
|
|
|
|
|
def test_match_track_by_attributes_when_id_missing() -> None:
|
|
local = _deserialize_video({"id": "X", "codec": "AVC", "width": 1920, "height": 1080, "language": "en"})
|
|
remote = _deserialize_video({"id": "Y", "codec": "AVC", "width": 1920, "height": 1080, "language": "en"})
|
|
assert _match_track(remote, [local]) is local
|
|
|
|
|
|
def test_match_track_no_candidates_returns_none() -> None:
|
|
remote = _deserialize_video({"id": "X", "codec": "AVC", "width": 1, "height": 1})
|
|
assert _match_track(remote, []) is None
|
|
|
|
|
|
def test_build_title_movie() -> None:
|
|
info = {"type": "movie", "id": "movie-0001", "name": "Foo", "year": 2024, "language": "en"}
|
|
title = _build_title(info, "ATV", "fallback")
|
|
assert isinstance(title, Movie)
|
|
assert title.id == "movie-0001"
|
|
assert title.name == "Foo"
|
|
|
|
|
|
def test_build_title_episode() -> None:
|
|
info = {
|
|
"type": "episode",
|
|
"id": "ep-00001",
|
|
"series_title": "Show",
|
|
"season": 1,
|
|
"number": 2,
|
|
"name": "Pilot",
|
|
"year": 2024,
|
|
"language": "en",
|
|
}
|
|
title = _build_title(info, "ATV", "fallback")
|
|
assert isinstance(title, Episode)
|
|
assert title.season == 1
|
|
assert title.number == 2
|
|
assert title.name == "Pilot"
|
|
|
|
|
|
def test_build_title_falls_back_to_id_when_missing() -> None:
|
|
title = _build_title({"type": "movie", "name": "x"}, "ATV", "fallback-id")
|
|
assert title.id == "fallback-id"
|