mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-11 19:45:06 +00:00
fix(titles): compare dub languages under their macrolanguage
A cmn track on a zh title no longer gets DUBBED, and nb and nn count as no; yue stays separate.
This commit is contained in:
@@ -306,6 +306,23 @@ A tag without a region, such as `zh`, gets no script because the region is what
|
||||
unshackle writes audio tags as the service gives them, because a script applies only to written text.
|
||||
Track selection, `--list` tags and file names use the service tag.
|
||||
|
||||
A service that knows the spoken language, such as Mandarin or Cantonese, gives the title language and
|
||||
the audio a region and no script, and gives the subtitles a script:
|
||||
|
||||
| Tracks | Mandarin, Taiwan | Cantonese, Hong Kong |
|
||||
|--------|------------------|----------------------|
|
||||
| Title language and audio | `cmn-TW` | `yue-HK` |
|
||||
| Subtitles | `zh-Hant` or `cmn-Hant` | `yue-Hant` |
|
||||
|
||||
With these tags, `-l orig` selects the audio and `-sl orig` selects the Traditional subtitles. Do not
|
||||
mix the forms. A bare `cmn` implies Simplified Chinese, so a `cmn` title language makes `-sl orig`
|
||||
select Simplified subtitles. A `zh-TW` title language with `cmn` audio selects no audio, because
|
||||
`zh-TW` implies Traditional Chinese. Cantonese and Chinese never match, so `-sl orig` on a `yue-HK`
|
||||
title does not select `zh-Hant` subtitles; select them with `-sl zh-Hant`.
|
||||
|
||||
The `{dual}`, `{multi}` and `{dubbed}` file name tags count `cmn` as `zh` and keep `yue` separate
|
||||
(see [`dual_multi_mode`](../reference/configuration/download.md#dual_multi_mode)).
|
||||
|
||||
Set `muxing.track_names` to `native` to name each track in its own language. The same rules apply
|
||||
to the script and the region:
|
||||
|
||||
|
||||
@@ -394,7 +394,10 @@ dual_multi_mode: strict
|
||||
|
||||
!!! note "Dialects count as one language"
|
||||
Regional variants of the same base language (e.g. `en-US` and `en-GB`) collapse to a single
|
||||
language in both modes, so they never trigger `{dual}` on their own.
|
||||
language in both modes, so they never trigger `{dual}` on their own. An individual language
|
||||
counts as its macrolanguage: `cmn` and `zh` are one language, and so are `arb` and `ar`.
|
||||
Norwegian `nb` (Bokmål) and `nn` (Nynorsk) count as `no`.
|
||||
Cantonese (`yue`) stays separate, so `cmn` and `yue` audio give `{dual}`.
|
||||
|
||||
## `hdr10plus_probe`
|
||||
|
||||
|
||||
@@ -99,6 +99,33 @@ def test_strict_single_unknown_original_no_dubbed(strict_mode):
|
||||
assert c["multi"] == ""
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("language", "languages", "tags"),
|
||||
[
|
||||
("zh", ["cmn"], set()),
|
||||
("zh-TW", ["cmn-TW"], set()),
|
||||
("cmn", ["zh-CN"], set()),
|
||||
("zh", ["zh", "cmn"], set()),
|
||||
("zh", ["cmn", "yue"], {"dual"}),
|
||||
("cmn", ["yue"], {"dubbed"}),
|
||||
("yue", ["cmn"], {"dubbed"}),
|
||||
("ar", ["arb"], set()),
|
||||
("no", ["nb-NO"], set()),
|
||||
("nb", ["nn"], set()),
|
||||
("no", ["nb", "da"], {"dual"}),
|
||||
],
|
||||
)
|
||||
def test_strict_counts_an_individual_language_as_its_macrolanguage(strict_mode, language, languages, tags):
|
||||
c = ctx(language, languages)
|
||||
assert {name for name in ("dual", "multi", "dubbed") if c[name]} == tags
|
||||
|
||||
|
||||
def test_the_primary_audio_is_the_original_language_under_its_macrolanguage(strict_mode):
|
||||
media_info = make_media_info(["yue", "cmn"])
|
||||
media_info.audio_tracks[1].channel_s = 6
|
||||
assert make_movie("zh").build_base_template_context(media_info)["audio_channels"] == "6.0"
|
||||
|
||||
|
||||
def test_count_dual_ignores_original(count_mode):
|
||||
c = ctx(None, ["en", "es"])
|
||||
assert c["dual"] == "DUAL"
|
||||
|
||||
@@ -14,6 +14,7 @@ from unshackle.core.constants import (
|
||||
VIDEO_CODEC_MAP,
|
||||
)
|
||||
from unshackle.core.tracks import Tracks
|
||||
from unshackle.core.utilities import spoken_language
|
||||
|
||||
|
||||
def rename(name: Optional[str]) -> str:
|
||||
@@ -74,21 +75,18 @@ class Title:
|
||||
returned dict with their specific fields (e.g., season/episode).
|
||||
"""
|
||||
primary_video_track = next(iter(media_info.video_tracks), None)
|
||||
original_lang_tag = str(self.language).split("-")[0].lower() if self.language else ""
|
||||
primary_audio_track = None
|
||||
if original_lang_tag:
|
||||
primary_audio_track = next(
|
||||
(
|
||||
t
|
||||
for t in media_info.audio_tracks
|
||||
if t.language and t.language.split("-")[0].lower() == original_lang_tag
|
||||
),
|
||||
None,
|
||||
)
|
||||
if primary_audio_track is None:
|
||||
primary_audio_track = next(iter(media_info.audio_tracks), None)
|
||||
audio_lang_bases = {x.language.split("-")[0].lower() for x in media_info.audio_tracks if x.language}
|
||||
unique_audio_languages = len(audio_lang_bases)
|
||||
original_lang_tag = spoken_language(self.language)
|
||||
spoken_audio = [spoken_language(t.language) for t in media_info.audio_tracks]
|
||||
primary_audio_track = next(
|
||||
(
|
||||
t
|
||||
for t, lang in zip(media_info.audio_tracks, spoken_audio)
|
||||
if original_lang_tag and lang == original_lang_tag
|
||||
),
|
||||
next(iter(media_info.audio_tracks), None),
|
||||
)
|
||||
audio_languages = set(spoken_audio) - {""}
|
||||
unique_audio_languages = len(audio_languages)
|
||||
|
||||
context: dict[str, Any] = {
|
||||
"source": self.service.__name__ if show_service else "",
|
||||
@@ -215,10 +213,10 @@ class Title:
|
||||
if unique_audio_languages > 2:
|
||||
context["multi"] = "MULTi"
|
||||
elif unique_audio_languages == 2:
|
||||
if not strict or (original_lang_tag and original_lang_tag in audio_lang_bases):
|
||||
if not strict or (original_lang_tag and original_lang_tag in audio_languages):
|
||||
context["dual"] = "DUAL"
|
||||
elif unique_audio_languages == 1 and strict:
|
||||
if original_lang_tag and original_lang_tag not in audio_lang_bases:
|
||||
if original_lang_tag and original_lang_tag not in audio_languages:
|
||||
context["dubbed"] = "DUBBED"
|
||||
|
||||
language_tags = config.language_tags if isinstance(config.language_tags, dict) else {}
|
||||
|
||||
@@ -172,6 +172,21 @@ def valid_language(language: Any) -> Optional[str]:
|
||||
return str(language)
|
||||
|
||||
|
||||
def spoken_language(tag: Any) -> str:
|
||||
"""
|
||||
Return the language a tag names, under its macrolanguage: cmn and zh-TW give zh, yue stays yue.
|
||||
|
||||
nb and nn give no by hand, because CLDR makes no their parent outside its macrolanguage data.
|
||||
"""
|
||||
if not tag:
|
||||
return ""
|
||||
try:
|
||||
language = Language.get(str(tag)).prefer_macrolanguage().language or ""
|
||||
except LanguageTagError:
|
||||
return ""
|
||||
return "no" if language in ("nb", "nn") else language
|
||||
|
||||
|
||||
@cache
|
||||
def multi_script_languages() -> frozenset[str]:
|
||||
"""Return the languages whose likely script changes with the region in CLDR (zh, yue, sr, ...)."""
|
||||
|
||||
Reference in New Issue
Block a user