feat(tracks)!: match a language tag to the variant it means

A tag with a region (en-US, es-419) matches only that variant and reports a miss instead of taking another region. A bare tag takes the variant it means (pt -> pt-BR, es -> es-ES) and falls back to any variant only when that one is missing. `orig` prefers the title's own tag, `-sl orig` resolves, forced and SDH subtitles are picked from their own pools, and the track name no longer strips the region from the cached language.

BREAKING CHANGE: -l es-419 no longer selects es-ES, and language_priority, language_tags and muxing.default_language treat a regional tag as exact.
This commit is contained in:
imSp4rky
2026-10-15 12:26:39 -06:00
parent c231114cda
commit a4ce70a53c
26 changed files with 584 additions and 253 deletions
+1 -1
View File
@@ -448,7 +448,7 @@ Make a download job. It requires `service` and `title_id`. Every other field is
| `forced_subs` | boolean | `false` | Include forced subtitles. |
| `forced_subs_only` | boolean | `false` | Download forced subtitles and no other subtitle; implies `forced_subs`. Not valid with `no_subs`, `video_only`, `audio_only` or `chapters_only`. |
| `forced_s_lang` | string[] | `[]` | Forced subtitle language(s); implies `forced_subs`. |
| `exact_lang` | boolean | `false` | Exact language matching. |
| `exact_lang` | boolean | `false` | A language tag without a region never falls back to another variant. A tag with a region is always exact. |
| `sub_format` | string | `null` | Output subtitle format. |
| `video_only` / `audio_only` / `subs_only` / `chapters_only` | boolean | `false` | Track-type restriction (at most one). |
| `no_subs` / `no_audio` / `no_chapters` / `no_video` / `no_attachments` | boolean | `false` | Skip a track type. |
+1 -1
View File
@@ -104,7 +104,7 @@ unshackle dl [OPTIONS] SERVICE [SERVICE ARGS...]
| `-fs`, `--forced-subs` | off | Include forced subtitle tracks. |
| `-fso`, `--forced-subs-only` | off | Keep only forced subtitle tracks; drop the other subtitles. Implies `-fs`. |
| `-fsl`, `--forced-s-lang` | none | Language(s) wanted for forced subtitles; implies `-fs`. A `-` prefix excludes. |
| `--exact-lang` | off | Exact matching only: `-l es-419` matches `es-419`, not `es-ES`. Applies to selection and to sort order. |
| `--exact-lang` | off | A tag without a region selects only the variant it means and never falls back to another: `-l es` does not select `es-419`. A tag with a region, such as `es-419`, is always exact. Applies to selection and to sort order. |
| `--sub-format` | - | Output subtitle format (`SRT`/`srt`, `VTT`/`webvtt`, `ASS`/`ssa`, `TTML`, `SMI`, ...), or `original` to keep the source format. |
!!! tip "These flags select languages, they do not order them"
+25 -6
View File
@@ -315,9 +315,9 @@ unshackle dl -l en,-es -vl ja EXAMPLE 81234567
!!! note "Rules for exclusion tokens"
- `orig` becomes the title's own original language, so `-sl -orig` drops the
subtitles in that language.
- `--exact-lang` applies to exclusions with the same matching rules as selection:
`-es` removes exactly the tracks that `es` would select. Without it, `-es` also
removes `es-419` and `es-ES`.
- An exclusion without a region removes every regional form: `-es` removes `es-419`
and `es-ES`. An exclusion with a region removes only that variant: `-es-419` removes
only `es-419`. With `--exact-lang`, `-es` removes only the variant `es` means.
- Tracks with no language tag are never excluded.
- unshackle rejects `-all`. Name the languages you do not want instead.
- The comma form is the documented style. `-sl -es` also works, but a shell can read
@@ -345,9 +345,28 @@ See [`subtitle.language_priority`](../reference/configuration/download.md#subtit
### Exact vs fuzzy matching
By default language matching is fuzzy: `-l en` also accepts `en-US`, `en-GB`, and the
other regional forms (up to a small distance). Pass `--exact-lang` when only an exact
match is acceptable: with it, `-l es-419` selects only `es-419`, not `es-ES`.
A tag without a region asks for the variant it means: `pt` is Brazilian Portuguese, `es` is
Spanish from Spain, and `en` is US English. With `pt-PT` and `pt-BR` tracks, `-l pt`
selects `pt-BR`. If the title has no such variant, the tag takes every regional form, so
`-l es` on a title with only `es-419` selects `es-419`.
A tag with a region asks for that variant only: `-l es-419` selects `es-419`, never `es-ES`
or `es-MX`. If the service has no `es-419` track, unshackle reports `es-419 not found` and
stops. With `--best-available`, it continues with the other languages.
A track without a region matches only the region its language implies. `en` is US English
and `pt` is Brazilian Portuguese, so `-l en-US` accepts a track tagged `en`, but `-l en-GB`
and `-l pt-PT` do not accept `en` or `pt`. To take any English track, write `en`.
`--exact-lang` turns off that fallback: a tag without a region then selects only the
variant it means, so `-l es` does not select `es-419`.
`orig` is the title language that the service gives. It selects that exact variant first,
so a `fr-CA` title takes `fr-CA` audio over `fr-FR`. When the title has no track in that
variant, `orig` takes the closest regional form, unless you pass `--exact-lang`.
An exclusion without a region removes every regional form: `-es` drops `es-ES` and `es-419`.
See the [exclusion rules](#excluding-languages) for `--exact-lang`.
!!! tip "The `all` and `best` tokens"
For audio, the tokens `all` and `best` bypass the usual one-track-per-language pick
+1 -1
View File
@@ -116,7 +116,7 @@ Every variable below is valid in both output and folder templates. unshackle tak
| `lang_tag` | Result of your `language_tags` rules, if configured | `MULTi` |
!!! note "How `language_tags` rules match"
unshackle tries the rules in order, but language comparison is **fuzzy**: a rule condition of `en` matches `en-US`, `en-GB`, and any other `en-*` form. Within a single rule, **all** conditions must match (AND logic) for it to apply. The first rule that fully matches wins. If none match, unshackle removes `{lang_tag?}` from the filename cleanly.
unshackle tries the rules in order, and they compare languages with the same [matching rules](downloading.md#exact-vs-fuzzy-matching) as `-l`: a rule condition of `en` matches `en-US`, `en-GB`, and any other `en-*` form, but `es-419` matches only `es-419`. Within a single rule, **all** conditions must match (AND logic) for it to apply. The first rule that fully matches wins. If none match, unshackle removes `{lang_tag?}` from the filename cleanly.
### Episode-specific
+14 -12
View File
@@ -76,7 +76,7 @@ Subtitle selection happens through `dl` flags. The most important is language se
| `-fs`, `--forced-subs` | Include forced subtitle tracks (excluded by default). |
| `-fso`, `--forced-subs-only` | Keep only forced subtitle tracks and drop the other subtitles; implies `-fs`. |
| `-fsl`, `--forced-s-lang` | Language(s) wanted for forced subtitles; implies `-fs`. A `-` prefix excludes. |
| `--exact-lang` | Exact language matching, with no regional variants. |
| `--exact-lang` | A tag without a region never falls back to another variant: `es` does not match `es-419`. |
| `-S`, `--subs-only` | Download only subtitle tracks. |
| `-ns`, `--no-subs` | Do not download subtitle tracks at all. |
| `--sub-format` | Output subtitle format / conversion target (see [conversion](#converting-subtitle-formats)). |
@@ -91,17 +91,18 @@ To narrow it down, pass a comma-separated list of language tags:
unshackle dl --s-lang en,es EXAMPLE 81234567
```
Language matching is fuzzy by default: `en` matches `en-US`, `en-GB`, and similar
variants. If a requested language is missing but others were found, unshackle continues
with whatever remains and tells you which languages it dropped.
For strict matching, where `es-419` matches only `es-419` and never `es-ES`, add
`--exact-lang`:
A tag without a region selects the variant it means, such as `en-US` for `en`, and every
regional form only when the title has no such variant. A tag with a region matches only
that variant:
```shell title="Only Latin-American Spanish, no other Spanish variants"
unshackle dl --s-lang es-419 --exact-lang EXAMPLE 'https://www.example.com/...'
unshackle dl --s-lang es-419 EXAMPLE 'https://www.example.com/...'
```
If a requested language is missing, unshackle stops with an error. With `--best-available`,
it continues with the languages it found and tells you which ones it dropped. See
[exact vs fuzzy matching](downloading.md#exact-vs-fuzzy-matching) for the full rules.
### Requiring languages before downloading
`--require-subs` is a gate rather than a filter. It downloads **all** available
@@ -193,10 +194,11 @@ can make a clean non-SDH version for you by stripping the hearing-impaired cues:
sound effects, speaker labels, music notes, and similar. This is on by default and
controlled by the [`strip_sdh`](#subtitle-configuration) config key.
The check is deliberately loose: if a close-enough plain track already exists (for
example an `en-GB` non-SDH track when the SDH track is `en-US`), no stripped copy is
created. When a stripped copy *is* created, the original SDH track is kept as well, so
you end up with both.
The check uses the same [matching rules](downloading.md#exact-vs-fuzzy-matching) as
`--s-lang`. A plain track covers an SDH track only when it is the same variant: `en` and
`en-US` cover each other, but a plain `en-GB` track does not stop a stripped `en` or `en-US`
copy. When unshackle creates a stripped
copy, it keeps the original SDH track as well, so you end up with both.
The [`sdh_method`](#subtitle-configuration) config key selects the stripping engine
itself. The default (`auto`) prefers the `subby` cleaner for SRT, falls back to
+11 -8
View File
@@ -219,13 +219,15 @@ subtitle:
unshackle removes nothing, unlike `-sl`, which drops every subtitle language it does not name.
Use `orig` in the list for the title's original language. A base tag also covers its
regional forms, so `en` sorts `en-US` and `en-GB`, but `zh` does **not** cover
`zh-Hant`, which you must name. The sort ignores entries that match no track, and it
ignores entries that are not valid language tags.
Use `orig` in the list for the title's original language. A base tag puts the variant it
means first and its other regional forms after it, so `en` sorts `en-US` and then
`en-GB`, but `zh` does **not** cover `zh-Hant`, which you must name. A tag with a region sorts only that variant, so
`[en-US]` puts `en-US` before `en-GB`. The sort ignores entries that match no track, and
it ignores entries that are not valid language tags.
`--exact-lang` applies here too: with it, `en` sorts only `en`, and `en-US` falls to
the alphabetical tail with the rest. Name each tag you want when you use that flag.
`--exact-lang` applies here too: with it, `en` sorts only the variant it means, and
`en-GB` falls to the alphabetical tail with the rest. Name each tag you want when you use
that flag.
!!! note "`type_priority` picks the default subtitle only with `muxing.default_language`"
At mux time the `default` flag goes to every forced subtitle whose language matches the
@@ -277,7 +279,7 @@ Matroska (MKV) muxing options.
| Sub-key | Type | Default | Description |
|---------|------|---------|-------------|
| `set_title` | bool | `true` | Write a human-readable title into the MKV container. |
| `default_language` | dict | `{}` | Force which language is flagged *default* per track type, e.g. `{audio: en, subtitle: en}`. |
| `default_language` | dict | `{}` | Force which language is flagged *default* per track type, e.g. `{audio: en, subtitle: en}`. A tag with a region, such as `en-US`, matches only that variant. |
| `merge_audio` | bool | `true` | Merge all audio into one file. `--split-audio` on the CLI flips this off. |
| `merge_video` | bool | `false` | Merge video tracks that share height, range, and codec into one file, so only language varies inside a file. `--merge-video` on the CLI flips this on. |
| `concurrency` | int | `4` | How many mkvmerge processes run at the same time when a title muxes more than one output file (one per video variant or audio codec). Each process uses one CPU core, so this shortens the mux step on an SSD. Set it to `1` on a hard disk, where parallel writes are slower. |
@@ -308,7 +310,8 @@ Language-tag remapping and the rule engine behind the `{lang_tag}` filename vari
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
language tag (for example `japanese`) never matches. `dual`, `multi` and `dubbed` take a bare
language tag (for example `japanese`) never matches. A tag with a region matches only that
variant, so `audio: es-419` does not match `es-ES` audio. `dual`, `multi` and `dubbed` take a bare
`true` or `false`, not a quoted string. unshackle skips a rule with a condition name it does
not know, or with any other value in a boolean condition. It logs a warning in each of these
cases and tries the next rule.