mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-13 12:35:05 +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.
|
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.
|
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
|
Set `muxing.track_names` to `native` to name each track in its own language. The same rules apply
|
||||||
to the script and the region:
|
to the script and the region:
|
||||||
|
|
||||||
|
|||||||
@@ -394,7 +394,10 @@ dual_multi_mode: strict
|
|||||||
|
|
||||||
!!! note "Dialects count as one language"
|
!!! note "Dialects count as one language"
|
||||||
Regional variants of the same base language (e.g. `en-US` and `en-GB`) collapse to a single
|
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`
|
## `hdr10plus_probe`
|
||||||
|
|
||||||
|
|||||||
@@ -99,6 +99,33 @@ def test_strict_single_unknown_original_no_dubbed(strict_mode):
|
|||||||
assert c["multi"] == ""
|
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):
|
def test_count_dual_ignores_original(count_mode):
|
||||||
c = ctx(None, ["en", "es"])
|
c = ctx(None, ["en", "es"])
|
||||||
assert c["dual"] == "DUAL"
|
assert c["dual"] == "DUAL"
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from unshackle.core.constants import (
|
|||||||
VIDEO_CODEC_MAP,
|
VIDEO_CODEC_MAP,
|
||||||
)
|
)
|
||||||
from unshackle.core.tracks import Tracks
|
from unshackle.core.tracks import Tracks
|
||||||
|
from unshackle.core.utilities import spoken_language
|
||||||
|
|
||||||
|
|
||||||
def rename(name: Optional[str]) -> str:
|
def rename(name: Optional[str]) -> str:
|
||||||
@@ -74,21 +75,18 @@ class Title:
|
|||||||
returned dict with their specific fields (e.g., season/episode).
|
returned dict with their specific fields (e.g., season/episode).
|
||||||
"""
|
"""
|
||||||
primary_video_track = next(iter(media_info.video_tracks), None)
|
primary_video_track = next(iter(media_info.video_tracks), None)
|
||||||
original_lang_tag = str(self.language).split("-")[0].lower() if self.language else ""
|
original_lang_tag = spoken_language(self.language)
|
||||||
primary_audio_track = None
|
spoken_audio = [spoken_language(t.language) for t in media_info.audio_tracks]
|
||||||
if original_lang_tag:
|
primary_audio_track = next(
|
||||||
primary_audio_track = next(
|
(
|
||||||
(
|
t
|
||||||
t
|
for t, lang in zip(media_info.audio_tracks, spoken_audio)
|
||||||
for t in media_info.audio_tracks
|
if original_lang_tag and lang == original_lang_tag
|
||||||
if t.language and t.language.split("-")[0].lower() == original_lang_tag
|
),
|
||||||
),
|
next(iter(media_info.audio_tracks), None),
|
||||||
None,
|
)
|
||||||
)
|
audio_languages = set(spoken_audio) - {""}
|
||||||
if primary_audio_track is None:
|
unique_audio_languages = len(audio_languages)
|
||||||
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)
|
|
||||||
|
|
||||||
context: dict[str, Any] = {
|
context: dict[str, Any] = {
|
||||||
"source": self.service.__name__ if show_service else "",
|
"source": self.service.__name__ if show_service else "",
|
||||||
@@ -215,10 +213,10 @@ class Title:
|
|||||||
if unique_audio_languages > 2:
|
if unique_audio_languages > 2:
|
||||||
context["multi"] = "MULTi"
|
context["multi"] = "MULTi"
|
||||||
elif unique_audio_languages == 2:
|
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"
|
context["dual"] = "DUAL"
|
||||||
elif unique_audio_languages == 1 and strict:
|
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"
|
context["dubbed"] = "DUBBED"
|
||||||
|
|
||||||
language_tags = config.language_tags if isinstance(config.language_tags, dict) else {}
|
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)
|
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
|
@cache
|
||||||
def multi_script_languages() -> frozenset[str]:
|
def multi_script_languages() -> frozenset[str]:
|
||||||
"""Return the languages whose likely script changes with the region in CLDR (zh, yue, sr, ...)."""
|
"""Return the languages whose likely script changes with the region in CLDR (zh, yue, sr, ...)."""
|
||||||
|
|||||||
Reference in New Issue
Block a user