"""Tests for per-language subtitle selection and dict-typed ``Config.languages``. These tests are pure: no network, no yt-dlp, no DB. They cover the policy logic that decides which subtitle track to pick for a given ``info`` dict produced by yt-dlp, plus the legacy/back-compat shims that let older YAML configs still load. """ from __future__ import annotations import pytest from yt_scraper.config import Config, parse_languages from yt_scraper.extract import pick_subtitle # ---------- fixtures ---------- def _track(ext: str = "json3", url: str = "u") -> dict: return {"ext": ext, "url": url} def _info(manual: dict | None = None, auto: dict | None = None) -> dict: return {"subtitles": manual or {}, "automatic_captions": auto or {}} # ---------- pick_subtitle: mixed per-language mode ---------- def test_mixed_manual_and_auto_per_language() -> None: """`es: auto` only takes auto; iterating `es` first means it wins. With ``{"es": "auto", "en": "manual"}`` the iteration matches `es` against the auto dict and picks the auto track; for `en` only the manual dict is consulted and the manual track is returned. The two policies do NOT cross-pollinate between languages. """ info = _info( manual={"en": [_track(url="man-en")]}, auto={"en": [_track(url="auto-en")], "es": [_track(url="auto-es")]}, ) pick = pick_subtitle(info, {"es": "auto", "en": "manual"}, prefer_manual=True) assert pick is not None assert pick.lang == "es" assert pick.source == "auto" def test_manual_only_skips_auto_even_when_present() -> None: info = _info(manual={}, auto={"es": [_track(url="auto")]}) pick = pick_subtitle(info, {"es": "manual"}, prefer_manual=True) assert pick is None def test_auto_only_skips_manual_even_when_present() -> None: info = _info(manual={"es": [_track(url="man")]}, auto={}) pick = pick_subtitle(info, {"es": "auto"}, prefer_manual=True) assert pick is None def test_any_defer_to_prefer_manual_default_true() -> None: """`any` honours the legacy prefer_manual=True default.""" info = _info( manual={"es": [_track(url="man")]}, auto={"es": [_track(url="auto")]}, ) pick = pick_subtitle(info, {"es": "any"}, prefer_manual=True) assert pick is not None assert pick.source == "manual" def test_any_defer_to_prefer_manual_default_false() -> None: info = _info( manual={"es": [_track(url="man")]}, auto={"es": [_track(url="auto")]}, ) pick = pick_subtitle(info, {"es": "any"}, prefer_manual=False) assert pick is not None assert pick.source == "auto" def test_picks_best_format_within_track() -> None: info = _info(manual={"en": [_track(ext="ttml", url="t"), _track(ext="json3", url="j")]}) pick = pick_subtitle(info, {"en": "manual"}, prefer_manual=True) assert pick.url == "j" # ---------- language normalisation ---------- def test_language_base_match() -> None: """`es-419` preference matches the `es` caption track.""" info = _info(manual={"es": [_track(url="u")]}) pick = pick_subtitle(info, {"es-419": "manual"}, prefer_manual=True) assert pick is not None assert pick.lang == "es" # ---------- legacy list input ---------- def test_legacy_list_input_uses_prefer_manual() -> None: info = _info( manual={"es": [_track(url="m")]}, auto={"es": [_track(url="a")]}, ) pick = pick_subtitle(info, ["es"], prefer_manual=True) assert pick.source == "manual" pick = pick_subtitle(info, ["es"], prefer_manual=False) assert pick.source == "auto" def test_unknown_mode_falls_back_to_any() -> None: info = _info(manual={"es": [_track()]}, auto={"es": [_track()]}) pick = pick_subtitle(info, {"es": "garbage"}, prefer_manual=True) assert pick is not None assert pick.source == "manual" # ---------- parse_languages config helper ---------- def test_parse_languages_dict_pass_through() -> None: assert parse_languages({"en": "manual", "es": "auto"}, True) == { "en": "manual", "es": "auto", } def test_parse_languages_dict_unknown_mode_to_any() -> None: assert parse_languages({"en": "garbage"}, True) == {"en": "any"} def test_parse_languages_legacy_list_to_manual_by_default() -> None: assert parse_languages(["es", "en"], prefer_manual=True) == { "es": "manual", "en": "manual", } def test_parse_languages_legacy_list_to_auto_when_prefer_manual_false() -> None: assert parse_languages(["es", "en"], prefer_manual=False) == { "es": "auto", "en": "auto", } def test_parse_languages_none_returns_empty() -> None: assert parse_languages(None, True) == {} # ---------- Config dataclass default shape ---------- def test_config_languages_defaults_to_dict() -> None: cfg = Config() assert isinstance(cfg.languages, dict) # "any", not "manual": the default must not exclude auto-generated captions. assert cfg.languages == {"es": "any", "en": "any"} def test_config_prefer_manual_defaults_true() -> None: cfg = Config() assert cfg.prefer_manual is True