feat(tracks): add language_priority to sort preferred languages first

subtitle.language_priority and audio.language_priority prepend to the CLI
languages, so the named languages sort first and nothing is filtered.
--exact-lang now applies to the sorts too, through matching_languages, which
follows the same RFC 4647 rule as Tracks.by_language and ignores invalid tags.
This commit is contained in:
imSp4rky
2026-08-03 10:15:05 -06:00
parent 4e1ccf3230
commit a6997f6bca
10 changed files with 277 additions and 36 deletions
@@ -0,0 +1,33 @@
from __future__ import annotations
from typing import Sequence
import pytest
from unshackle.core.utilities import matching_languages
POOL = ["ar", "en", "en-GB", "en-US", "ja"]
@pytest.mark.parametrize(
("language", "available", "exact", "expected"),
[
("en", POOL, False, {"en", "en-GB", "en-US"}),
# CLDR rates en/en-US at distance 0, so exact mode leans on the RFC 4647 string preference
("en", POOL, True, {"en"}),
("en-GB", POOL, True, {"en-GB"}),
# no string-equal tag, so exact mode falls back to the distance match
("zh", ["ar", "cmn", "ja"], True, {"cmn"}),
("en", ["ar", "ja"], False, set()),
("en", [], False, set()),
("en", ["ar", None, "en"], True, {"en"}),
# unparseable tags (config typos) must match nothing, never raise
("engrish", POOL, False, set()),
("engrish", POOL, True, set()),
("", POOL, False, set()),
("", POOL, True, set()),
("f", POOL, False, set()),
],
)
def test_matching_languages(language: str, available: Sequence[str], exact: bool, expected: set[str]) -> None:
assert matching_languages(language, available, exact) == expected
@@ -0,0 +1,24 @@
from __future__ import annotations
from typing import Optional, Sequence
import pytest
from langcodes import Language
from unshackle.core.utilities import resolve_sort_langs
@pytest.mark.parametrize(
("tokens", "original", "expected"),
[
(["orig", "en", "es"], "ja", ["ja", "en", "es"]),
(["orig", "en", "es"], None, ["en", "es"]),
(["orig"], Language.get("pt-BR"), ["pt-BR"]),
(["en", "orig"], "en", ["en"]),
(["en", "es", "en", "fr"], None, ["en", "es", "fr"]),
(["all", "best", "en"], "ja", ["all", "best", "en"]),
([], "ja", []),
],
)
def test_resolve_sort_langs(tokens: Sequence[str], original: Optional[str], expected: list[str]) -> None:
assert resolve_sort_langs(tokens, original) == expected
+83 -2
View File
@@ -3,8 +3,8 @@ import pytest
from unshackle.core.tracks import Subtitle, Tracks
def sub(lang: str, forced: bool = False, sdh: bool = False, cc: bool = False) -> Subtitle:
return Subtitle(
def sub(lang: str, forced: bool = False, sdh: bool = False, cc: bool = False, orig: bool = False) -> Subtitle:
track = Subtitle(
url=f"https://example.test/{lang}-{forced}-{sdh}-{cc}",
language=lang,
codec=Subtitle.Codec.SubRip,
@@ -12,6 +12,8 @@ def sub(lang: str, forced: bool = False, sdh: bool = False, cc: bool = False) ->
sdh=sdh,
cc=cc,
)
track.is_original_lang = orig
return track
def shape(tracks: Tracks) -> list[tuple[str, str]]:
@@ -135,3 +137,82 @@ def test_language_grouping_keeps_regional_variants_adjacent_through_sections() -
("fi", "normal"),
("fi", "sdh"),
]
def langs(tracks: Tracks) -> list[str]:
return [str(s.language) for s in tracks.subtitles]
@pytest.fixture
def many() -> Tracks:
t = Tracks()
t.add([sub(lang, orig=lang == "ja") for lang in ("ar", "bg", "en", "es", "fr", "ja")])
return t
def test_priority_orders_listed_then_rest_alphabetically(many: Tracks) -> None:
many.sort_subtitles(by_language=["en", "es", "fr"], group_by="language")
assert langs(many) == ["en", "es", "fr", "ar", "bg", "ja"]
def test_priority_outranks_the_original_language(many: Tracks) -> None:
# "all" resolves to the original language (ja), which must stay below the named ones
many.sort_subtitles(by_language=["en", "es", "all"], group_by="language")
assert langs(many) == ["en", "es", "ja", "ar", "bg", "fr"]
def test_priority_matches_regional_variants() -> None:
t = Tracks()
t.add([sub("ar"), sub("en-US"), sub("es-419"), sub("ja")])
t.sort_subtitles(by_language=["en", "es"], group_by="language")
assert langs(t) == ["en-US", "es-419", "ar", "ja"]
def test_priority_overlapping_entries_are_harmless() -> None:
t = Tracks()
t.add([sub("ar"), sub("en-GB"), sub("en-US"), sub("es"), sub("ja")])
t.sort_subtitles(by_language=["en", "en-US", "es"], group_by="language")
assert langs(t) == ["en-GB", "en-US", "es", "ar", "ja"]
def test_no_priority_leaves_order_unchanged(many: Tracks) -> None:
many.sort_subtitles(by_language=[], group_by="language")
assert langs(many) == ["ar", "bg", "en", "es", "fr", "ja"]
def test_all_alone_still_floats_the_original_language(many: Tracks) -> None:
many.sort_subtitles(by_language=["all"], group_by="language")
assert langs(many) == ["ja", "ar", "bg", "en", "es", "fr"]
@pytest.fixture
def regionals() -> Tracks:
t = Tracks()
t.add([sub("ar"), sub("en"), sub("en-GB"), sub("en-US"), sub("ja")])
return t
def test_fuzzy_priority_floats_regional_variants(regionals: Tracks) -> None:
regionals.sort_subtitles(by_language=["en"], group_by="language")
assert langs(regionals) == ["en", "en-GB", "en-US", "ar", "ja"]
def test_exact_priority_floats_only_the_named_tag(regionals: Tracks) -> None:
# CLDR rates en/en-US at distance 0, so exact mode needs the RFC 4647 string preference
# to hold en-US back; without it en-US floats and en-GB does not.
regionals.sort_subtitles(by_language=["en"], group_by="language", exact_match=True)
assert langs(regionals) == ["en", "ar", "en-GB", "en-US", "ja"]
def test_exact_priority_still_falls_back_when_no_tag_is_string_equal() -> None:
t = Tracks()
t.add([sub("ar"), sub("cmn"), sub("ja")])
t.sort_subtitles(by_language=["zh"], group_by="language", exact_match=True)
assert langs(t) == ["cmn", "ar", "ja"]
@pytest.mark.parametrize("exact", [False, True])
def test_unparseable_priority_entry_is_ignored(many: Tracks, exact: bool) -> None:
# a config typo in language_priority must not crash the sort
many.sort_subtitles(by_language=["en", "engrish", "f", ""], group_by="language", exact_match=exact)
assert langs(many) == ["en", "ar", "bg", "es", "fr", "ja"]