From 036368b3c3c95b3295b9f7c2a2ee9ea3576e6ca7 Mon Sep 17 00:00:00 2001 From: imSp4rky Date: Thu, 16 Jul 2026 22:47:20 -0600 Subject: [PATCH] fix(api): keep sub_format "original" working through the API --- tests/remote/unit/test_download_manager.py | 23 ++++++++++++++++++++++ unshackle/core/api/download_manager.py | 20 ++++++++++++++++--- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/tests/remote/unit/test_download_manager.py b/tests/remote/unit/test_download_manager.py index 59cc0d79..bc973042 100644 --- a/tests/remote/unit/test_download_manager.py +++ b/tests/remote/unit/test_download_manager.py @@ -118,3 +118,26 @@ def test_job_status_values() -> None: "failed", "cancelled", } + + +def test_normalize_sub_format_preserves_original_sentinel() -> None: + """An API job asking for sub_format="original" must keep the "original" sentinel. + + dl.py treats "original" as "keep source format" (skip conversion). If it were normalized to + None (as to_enum(["original"]) yields, since there is no such enum member), dl.py would fall + into the default branch and re-encode TTML -> WebVTT, contradicting the CLI. + """ + from unshackle.core.api.download_manager import _normalize_sub_format + from unshackle.core.tracks import Subtitle + from unshackle.core.utils.click_types import SubtitleCodecChoice + + # API path keeps the sentinel, case-insensitively. + assert _normalize_sub_format("original") == "original" + assert _normalize_sub_format("ORIGINAL") == "original" + + # The CLI produces the exact same sentinel value, so dl.py's `== "original"` branch handles both. + assert SubtitleCodecChoice(Subtitle.Codec).convert("original") == "original" + + # A real codec still maps to its enum member; unknowns collapse to None. + assert _normalize_sub_format("srt") is Subtitle.Codec.SubRip + assert _normalize_sub_format("nonsense") is None diff --git a/unshackle/core/api/download_manager.py b/unshackle/core/api/download_manager.py index ef8590c8..e9393bc8 100644 --- a/unshackle/core/api/download_manager.py +++ b/unshackle/core/api/download_manager.py @@ -298,6 +298,21 @@ def to_enum(values: List[str], enum_cls: type[Enum]) -> List[Enum]: return [lookup[v.upper()] for v in values if v.upper() in lookup] +def _normalize_sub_format(sub_format_raw: str) -> Any: + """Normalize an API sub_format string to what dl.py expects. + + "original" is a sentinel meaning "keep source format" (dl.py skips conversion), so it must + pass through unchanged to match the CLI (SubtitleCodecChoice returns the same string). Any + other value maps to a Subtitle.Codec, or None when unrecognized. + """ + from unshackle.core.tracks import Subtitle + + if sub_format_raw.lower() == "original": + return "original" + mapped = to_enum([sub_format_raw], Subtitle.Codec) + return mapped[0] if mapped else None + + def _perform_download( job_id: str, service: str, @@ -318,7 +333,7 @@ def _perform_download( from unshackle.core.api.errors import APIError, APIErrorCode from unshackle.core.config import config from unshackle.core.services import Services - from unshackle.core.tracks import Subtitle, Video + from unshackle.core.tracks import Video from unshackle.core.utils.click_types import ContextData from unshackle.core.utils.collections import merge_dict @@ -357,8 +372,7 @@ def _perform_download( sub_format_raw = params.get("sub_format") if sub_format_raw and isinstance(sub_format_raw, str): - mapped = to_enum([sub_format_raw], Subtitle.Codec) - params["sub_format"] = mapped[0] if mapped else None + params["sub_format"] = _normalize_sub_format(sub_format_raw) if params.get("export"): params["export"] = bool(params["export"])