mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-12 12:05:07 +00:00
feat(naming): add audio_contain_all language tag condition
An empty audio_contain_all or subs_contain_all list now never matches; before this, an empty subs_contain_all list matched every release.
This commit is contained in:
@@ -312,17 +312,18 @@ Language-tag remapping and the rule engine behind the `{lang_tag}` filename vari
|
|||||||
| Condition | Meaning |
|
| Condition | Meaning |
|
||||||
|-----------|---------|
|
|-----------|---------|
|
||||||
| `audio` | At least one audio language matches. |
|
| `audio` | At least one audio language matches. |
|
||||||
|
| `audio_contain_all` | Every listed language must be present in the audio. |
|
||||||
| `subs_contain` | At least one subtitle language matches. |
|
| `subs_contain` | At least one subtitle language matches. |
|
||||||
| `subs_contain_all` | Every listed language must be present. |
|
| `subs_contain_all` | Every listed language must be present in the subtitles. |
|
||||||
| `dual` / `multi` / `dubbed` | `true` matches when that filename variable is set, `false` when it is not. |
|
| `dual` / `multi` / `dubbed` | `true` matches when that filename variable is set, `false` when it is not. |
|
||||||
|
|
||||||
Each language condition takes one language tag or a list of them. A list matches when **any**
|
Each language condition takes one language tag or a list of them. A list matches when **any**
|
||||||
entry matches, except `subs_contain_all`, which needs them all. An entry that is not a valid
|
entry matches, except `audio_contain_all` and `subs_contain_all`, which need them all. An empty
|
||||||
language tag (for example `japanese`) never matches. A tag with a region matches only that
|
list never matches. An entry that is not a valid language tag (for example `japanese`) never
|
||||||
variant, so `audio: es-419` does not match `es-ES` audio. `dual`, `multi` and `dubbed` take a bare
|
matches. A tag with a region matches only that variant, so `audio: es-419` does not match
|
||||||
`true` or `false`, not a quoted string. unshackle skips a rule with a condition name it does
|
`es-ES` audio. `dual`, `multi` and `dubbed` take a bare `true` or `false`, not a quoted string.
|
||||||
not know, or with any other value in a boolean condition. It logs a warning in each of these
|
unshackle skips a rule with a condition name it does not know, or with any other value in a
|
||||||
cases and tries the next rule.
|
boolean condition. It logs a warning in each of these cases and tries the next rule.
|
||||||
|
|
||||||
`dual`, `multi` and `dubbed` carry the same meaning as the
|
`dual`, `multi` and `dubbed` carry the same meaning as the
|
||||||
[filename variables of those names](#dual_multi_mode). Pair one with a language to name a
|
[filename variables of those names](#dual_multi_mode). Pair one with a language to name a
|
||||||
@@ -343,6 +344,24 @@ language_tags:
|
|||||||
tag: "THAIDUAL"
|
tag: "THAIDUAL"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
unshackle examines the rules in order, so put a rule that needs more languages before a rule that
|
||||||
|
needs fewer. Here, a release with both French dubs gets `VF2`, not `VFQ`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
language_tags:
|
||||||
|
rules:
|
||||||
|
- audio_contain_all: [fr-FR, fr-CA]
|
||||||
|
tag: "VF2"
|
||||||
|
- audio: fr-CA
|
||||||
|
tag: "VFQ"
|
||||||
|
- audio: fr-FR
|
||||||
|
tag: "VFF"
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! tip "Give regional tags in an `_all` list"
|
||||||
|
A tag without a region also matches a regional track, so one `fr-CA` track satisfies both
|
||||||
|
entries of `[fr, fr-CA]`. Use `[fr-FR, fr-CA]` to require two different tracks.
|
||||||
|
|
||||||
!!! note "Put `{lang_tag}` where you want the name"
|
!!! note "Put `{lang_tag}` where you want the name"
|
||||||
These rules fill `{lang_tag}`. To use your own name in place of the fixed text, keep
|
These rules fill `{lang_tag}`. To use your own name in place of the fixed text, keep
|
||||||
`{lang_tag?}` in the template and leave out `{dual?}`, `{multi?}` and `{dubbed?}`.
|
`{lang_tag?}` in the template and leave out `{dual?}`, `{multi?}` and `{dubbed?}`.
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ language_tags:
|
|||||||
rules:
|
rules:
|
||||||
- audio: no
|
- audio: no
|
||||||
tag: NORDIC
|
tag: NORDIC
|
||||||
|
- audio_contain_all: [no, da]
|
||||||
|
tag: NORDIC2
|
||||||
update_checks: no
|
update_checks: no
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -57,6 +59,10 @@ def test_a_no_rule_tags_a_norwegian_track(tmp_path: Path) -> None:
|
|||||||
assert evaluate_language_tag(rules, [Language.get("no")], []) == "NORDIC"
|
assert evaluate_language_tag(rules, [Language.get("no")], []) == "NORDIC"
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_no_entry_survives_in_an_audio_contain_all_list(tmp_path: Path) -> None:
|
||||||
|
assert _load(tmp_path).language_tags["rules"][1]["audio_contain_all"] == ["no", "da"]
|
||||||
|
|
||||||
|
|
||||||
def test_tag_rules_keep_their_own_booleans(tmp_path: Path) -> None:
|
def test_tag_rules_keep_their_own_booleans(tmp_path: Path) -> None:
|
||||||
"""tag_rules matches text and warns about a bare bool itself."""
|
"""tag_rules matches text and warns about a bare bool itself."""
|
||||||
path = tmp_path / "unshackle.yaml"
|
path = tmp_path / "unshackle.yaml"
|
||||||
|
|||||||
@@ -31,6 +31,23 @@ def test_subs_contain_all_needs_every_entry() -> None:
|
|||||||
assert evaluate_language_tag(rules, JA, EN_ES) == ""
|
assert evaluate_language_tag(rules, JA, EN_ES) == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_audio_contain_all_tags_a_release_with_every_listed_language() -> None:
|
||||||
|
rules = [
|
||||||
|
{"audio_contain_all": ["fr-FR", "fr-CA"], "tag": "VF2"},
|
||||||
|
{"audio": "fr-CA", "tag": "VFQ"},
|
||||||
|
{"audio": "fr-FR", "tag": "VFF"},
|
||||||
|
]
|
||||||
|
fr_fr, fr_ca, en = Language.get("fr-FR"), Language.get("fr-CA"), Language.get("en-US")
|
||||||
|
assert evaluate_language_tag(rules, [en, fr_fr, fr_ca], []) == "VF2"
|
||||||
|
assert evaluate_language_tag(rules, [en, fr_ca], []) == "VFQ"
|
||||||
|
assert evaluate_language_tag(rules, [en, fr_fr], []) == "VFF"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("condition", ["audio_contain_all", "subs_contain_all"])
|
||||||
|
def test_an_empty_contain_all_list_matches_nothing(condition) -> None:
|
||||||
|
assert evaluate_language_tag([{condition: [], "tag": "ALL"}], JA, EN_ES) == ""
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("bad", ["japanese", 1, [["ja"]], {"ja": 1}])
|
@pytest.mark.parametrize("bad", ["japanese", 1, [["ja"]], {"ja": 1}])
|
||||||
def test_an_invalid_rule_value_is_no_match_not_a_crash(bad, caplog) -> None:
|
def test_an_invalid_rule_value_is_no_match_not_a_crash(bad, caplog) -> None:
|
||||||
rules = [{"audio": bad, "tag": "BAD"}, {"audio": "ja", "tag": "OK"}]
|
rules = [{"audio": bad, "tag": "BAD"}, {"audio": "ja", "tag": "OK"}]
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ LANGUAGE_KEYS = frozenset(
|
|||||||
"default_language",
|
"default_language",
|
||||||
"video",
|
"video",
|
||||||
"audio",
|
"audio",
|
||||||
|
"audio_contain_all",
|
||||||
"subtitle",
|
"subtitle",
|
||||||
"subs_contain",
|
"subs_contain",
|
||||||
"subs_contain_all",
|
"subs_contain_all",
|
||||||
|
|||||||
@@ -14,7 +14,13 @@ log = logging.getLogger(__name__)
|
|||||||
|
|
||||||
# Conditions that test a filename variable of the same name rather than a language.
|
# Conditions that test a filename variable of the same name rather than a language.
|
||||||
STATE_CONDITIONS = ("dual", "multi", "dubbed")
|
STATE_CONDITIONS = ("dual", "multi", "dubbed")
|
||||||
LANGUAGE_CONDITIONS = ("audio", "subs_contain", "subs_contain_all")
|
# Each language condition: whether it tests the subtitles, and whether any or all of its entries must match.
|
||||||
|
LANGUAGE_CONDITIONS = {
|
||||||
|
"audio": (False, any),
|
||||||
|
"audio_contain_all": (False, all),
|
||||||
|
"subs_contain": (True, any),
|
||||||
|
"subs_contain_all": (True, all),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def evaluate_language_tag(
|
def evaluate_language_tag(
|
||||||
@@ -73,23 +79,13 @@ def rule_matches(
|
|||||||
|
|
||||||
has_condition = False
|
has_condition = False
|
||||||
|
|
||||||
audio_lang = rule.get("audio")
|
for name, (subs, quantifier) in LANGUAGE_CONDITIONS.items():
|
||||||
if audio_lang is not None:
|
wanted = rule.get(name)
|
||||||
has_condition = True
|
if wanted is not None:
|
||||||
if not any(_matches(lang, audio_languages) for lang in _as_list(audio_lang)):
|
has_condition = True
|
||||||
return False
|
wanted_langs = _as_list(wanted)
|
||||||
|
languages = subtitle_languages if subs else audio_languages
|
||||||
subs_contain = rule.get("subs_contain")
|
if not wanted_langs or not quantifier(_matches(lang, languages) for lang in wanted_langs):
|
||||||
if subs_contain is not None:
|
|
||||||
has_condition = True
|
|
||||||
if not any(_matches(lang, subtitle_languages) for lang in _as_list(subs_contain)):
|
|
||||||
return False
|
|
||||||
|
|
||||||
subs_contain_all = rule.get("subs_contain_all")
|
|
||||||
if subs_contain_all is not None:
|
|
||||||
has_condition = True
|
|
||||||
for lang in _as_list(subs_contain_all):
|
|
||||||
if not _matches(lang, subtitle_languages):
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
for name in STATE_CONDITIONS:
|
for name in STATE_CONDITIONS:
|
||||||
|
|||||||
Reference in New Issue
Block a user