mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-12 03:55:05 +00:00
merge: subtitle.group_by track option
This commit is contained in:
+10
-3
@@ -134,9 +134,16 @@ flags drive selection, sort order, output filenames, and the Matroska track flag
|
||||
often uppercase with `>>>` speaker markers and sound cues.
|
||||
|
||||
!!! note "SDH and CC are treated together for sorting and stripping"
|
||||
Within a language, unshackle orders subtitles from fewest to most captions:
|
||||
**Forced → Normal → SDH/CC**. For the purposes of hearing-impaired handling, SDH and
|
||||
CC tracks are grouped as the "most captions" variant.
|
||||
unshackle orders subtitles from fewest to most captions: **Forced → Normal → SDH/CC**.
|
||||
For the purposes of hearing-impaired handling, SDH and CC tracks are grouped as the
|
||||
"most captions" variant.
|
||||
|
||||
By default this type order is the primary sort, so all forced tracks come first, then
|
||||
all normal tracks, then all SDH/CC tracks, each block sorted by language. To keep each
|
||||
language next to its own variants instead, set
|
||||
[`subtitle.group_by`](../reference/configuration.md#subtitle) to `language`. Use
|
||||
[`subtitle.type_priority`](../reference/configuration.md#subtitle) to change the
|
||||
Forced → Normal → SDH/CC order itself.
|
||||
|
||||
A single track cannot be both CC and SDH, and a forced track cannot also be flagged
|
||||
CC or SDH. These combinations are rejected as invalid.
|
||||
|
||||
@@ -375,7 +375,8 @@ written as sidecar files. See [Subtitles](../guide/subtitles.md) for the full gu
|
||||
| `preferred_conversion_method` | str | *(unset)* | Preferred converter when several are available. |
|
||||
| `output_mode` | str | `"mux"` | `mux` embeds subtitles in the MKV; `sidecar` writes separate files; `both` writes sidecars and still muxes. |
|
||||
| `sidecar_format` | str | `"srt"` | Format for sidecar files when `output_mode: sidecar`. |
|
||||
| `type_priority` | list | *(unset)* | Ordered ranking of subtitle types (`forced`, `normal`, `sdh`; CC counts as SDH) that sorts the variants within each language. Types you leave out fall to the end. |
|
||||
| `type_priority` | list | *(unset)* | Ordered ranking of subtitle types (`forced`, `normal`, `sdh`; CC counts as SDH). Types you leave out fall to the end. |
|
||||
| `group_by` | str | `"type"` | Which key groups the subtitle tracks. `type` puts all forced tracks together, then all normal, then all SDH. `language` keeps each language next to its own variants. |
|
||||
|
||||
```yaml
|
||||
subtitle:
|
||||
@@ -384,11 +385,30 @@ subtitle:
|
||||
sidecar_format: srt
|
||||
```
|
||||
|
||||
!!! note "`group_by` puts a language next to its own SDH track"
|
||||
The default `type` order groups by subtitle type first, so every normal track comes
|
||||
before every SDH track. In a title with many languages, a track ends up far from its
|
||||
own SDH version, for example Finnish at track 5 and Finnish SDH at track 42. Set
|
||||
`group_by: language` to keep them together:
|
||||
|
||||
```yaml
|
||||
subtitle:
|
||||
group_by: language
|
||||
```
|
||||
|
||||
| `group_by` | Result |
|
||||
|---|---|
|
||||
| `type` | Danish, Finnish, French, … Danish SDH, Finnish SDH, French SDH |
|
||||
| `language` | Danish, Danish SDH, Finnish, Finnish SDH, French, French SDH |
|
||||
|
||||
`type_priority` still sets the order of the variants, so with `group_by: language` it
|
||||
controls the order inside each language.
|
||||
|
||||
!!! note "`type_priority` also picks the default subtitle"
|
||||
`type_priority` sorts the tracks within each language, and at mux time the first track
|
||||
in the default subtitle language gets the `default` flag. The built-in order is `forced`,
|
||||
`normal`, `sdh`, which means a forced track becomes default whenever one exists. If you
|
||||
want the full dialogue track as default instead, set:
|
||||
At mux time the first track in the default subtitle language gets the `default` flag.
|
||||
The built-in order is `forced`, `normal`, `sdh`, which means a forced track becomes
|
||||
default whenever one exists. If you want the full dialogue track as default instead,
|
||||
set:
|
||||
|
||||
```yaml
|
||||
subtitle:
|
||||
|
||||
@@ -66,3 +66,72 @@ def test_cc_counts_as_sdh() -> None:
|
||||
t.add([sub("en", cc=True), sub("en")])
|
||||
t.sort_subtitles(by_language=["en"], type_priority=["normal", "sdh"])
|
||||
assert shape(t) == [("en", "normal"), ("en", "sdh")]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def unprioritised() -> Tracks:
|
||||
"""Languages that no by_language argument floats to the top."""
|
||||
t = Tracks()
|
||||
t.add([sub("fi", sdh=True), sub("fi"), sub("de", sdh=True), sub("de")])
|
||||
return t
|
||||
|
||||
|
||||
def test_type_grouping_separates_a_language_from_its_sdh(unprioritised: Tracks) -> None:
|
||||
unprioritised.sort_subtitles()
|
||||
assert shape(unprioritised) == [
|
||||
("de", "normal"),
|
||||
("fi", "normal"),
|
||||
("de", "sdh"),
|
||||
("fi", "sdh"),
|
||||
]
|
||||
|
||||
|
||||
def test_language_grouping_keeps_a_language_with_its_sdh(unprioritised: Tracks) -> None:
|
||||
unprioritised.sort_subtitles(group_by="language")
|
||||
assert shape(unprioritised) == [
|
||||
("de", "normal"),
|
||||
("de", "sdh"),
|
||||
("fi", "normal"),
|
||||
("fi", "sdh"),
|
||||
]
|
||||
|
||||
|
||||
def test_language_grouping_still_honours_type_priority_and_sections(tracks: Tracks) -> None:
|
||||
# de sits outside by_language, where type-major sorting would interleave it with en
|
||||
tracks.add([sub("de"), sub("de", sdh=True)])
|
||||
tracks.sort_subtitles(by_language=["it"], type_priority=["normal", "sdh", "forced"], group_by="language")
|
||||
assert shape(tracks) == [
|
||||
("it", "normal"),
|
||||
("it", "forced"),
|
||||
("de", "normal"),
|
||||
("de", "sdh"),
|
||||
("en", "normal"),
|
||||
("en", "sdh"),
|
||||
("en", "forced"),
|
||||
]
|
||||
|
||||
|
||||
def test_language_grouping_keeps_regional_variants_adjacent_through_sections() -> None:
|
||||
# "en" floats en-GB and en-US as one block, so each exact tag keeps its variants
|
||||
t = Tracks()
|
||||
t.add(
|
||||
[
|
||||
sub("en-US", sdh=True),
|
||||
sub("fi"),
|
||||
sub("en-GB"),
|
||||
sub("en-US"),
|
||||
sub("fi", sdh=True),
|
||||
sub("en-GB", forced=True),
|
||||
sub("en-GB", sdh=True),
|
||||
]
|
||||
)
|
||||
t.sort_subtitles(by_language=["en"], group_by="language")
|
||||
assert shape(t) == [
|
||||
("en-GB", "forced"),
|
||||
("en-GB", "normal"),
|
||||
("en-GB", "sdh"),
|
||||
("en-US", "normal"),
|
||||
("en-US", "sdh"),
|
||||
("fi", "normal"),
|
||||
("fi", "sdh"),
|
||||
]
|
||||
|
||||
@@ -2397,7 +2397,11 @@ class dl:
|
||||
by_language=processed_audio_sort_lang,
|
||||
codec_priority=config.audio.get("codec_priority"),
|
||||
)
|
||||
title.tracks.sort_subtitles(by_language=s_lang, type_priority=config.subtitle.get("type_priority"))
|
||||
title.tracks.sort_subtitles(
|
||||
by_language=s_lang,
|
||||
type_priority=config.subtitle.get("type_priority"),
|
||||
group_by=config.subtitle.get("group_by"),
|
||||
)
|
||||
|
||||
if list_:
|
||||
available_tracks, _ = title.tracks.tree()
|
||||
|
||||
@@ -289,6 +289,7 @@ class Tracks:
|
||||
self,
|
||||
by_language: Optional[Sequence[Union[str, Language]]] = None,
|
||||
type_priority: Optional[Sequence[str]] = None,
|
||||
group_by: Optional[str] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Sort subtitle tracks by various track attributes to a common P2P standard.
|
||||
@@ -299,30 +300,46 @@ class Tracks:
|
||||
- then rest ascending alphabetically after the prioritized groups
|
||||
(Each section ascending alphabetically, but separated)
|
||||
|
||||
Language Group Order:
|
||||
Type Order:
|
||||
- Forced
|
||||
- Normal
|
||||
- Hard of Hearing (SDH/CC)
|
||||
(Least to most captions expected in the subtitle)
|
||||
|
||||
type_priority overrides the Language Group Order with an explicit ranking of
|
||||
"forced", "normal", and "sdh" (cc counts as sdh); unlisted types fall to the end.
|
||||
The first track after sorting receives the default flag at mux time, so this also
|
||||
controls which subtitle type becomes default.
|
||||
type_priority overrides the Type Order with an explicit ranking of "forced",
|
||||
"normal", and "sdh" (cc counts as sdh); unlisted types fall to the end.
|
||||
|
||||
group_by sets the major key. "type" (default) keeps every forced track together,
|
||||
then every normal, then every SDH, each block ascending by language. "language"
|
||||
groups by language instead, so Finnish sits next to Finnish SDH, with the Type
|
||||
Order applied inside each language.
|
||||
"""
|
||||
if not self.subtitles:
|
||||
return
|
||||
# language groups
|
||||
self.subtitles.sort(key=lambda x: str(x.language))
|
||||
if type_priority:
|
||||
rank = {str(t).lower(): i for i, t in enumerate(type_priority)}
|
||||
default_rank = len(rank)
|
||||
self.subtitles.sort(
|
||||
key=lambda x: rank.get("forced" if x.forced else "sdh" if (x.sdh or x.cc) else "normal", default_rank)
|
||||
)
|
||||
|
||||
def by_type() -> None:
|
||||
if type_priority:
|
||||
rank = {str(t).lower(): i for i, t in enumerate(type_priority)}
|
||||
default_rank = len(rank)
|
||||
self.subtitles.sort(
|
||||
key=lambda x: rank.get(
|
||||
"forced" if x.forced else "sdh" if (x.sdh or x.cc) else "normal", default_rank
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.subtitles.sort(key=lambda x: x.sdh or x.cc)
|
||||
self.subtitles.sort(key=lambda x: x.forced, reverse=True)
|
||||
|
||||
def by_lang() -> None:
|
||||
self.subtitles.sort(key=lambda x: str(x.language))
|
||||
|
||||
# stable sorts, so the last pass is the major key
|
||||
if str(group_by or "type").lower() == "language":
|
||||
by_type()
|
||||
by_lang()
|
||||
else:
|
||||
self.subtitles.sort(key=lambda x: x.sdh or x.cc)
|
||||
self.subtitles.sort(key=lambda x: x.forced, reverse=True)
|
||||
by_lang()
|
||||
by_type()
|
||||
# sections
|
||||
for language in reversed(by_language or []):
|
||||
if str(language) == "all":
|
||||
|
||||
Reference in New Issue
Block a user