mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-13 12:35: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.
|
often uppercase with `>>>` speaker markers and sound cues.
|
||||||
|
|
||||||
!!! note "SDH and CC are treated together for sorting and stripping"
|
!!! note "SDH and CC are treated together for sorting and stripping"
|
||||||
Within a language, unshackle orders subtitles from fewest to most captions:
|
unshackle orders subtitles from fewest to most captions: **Forced → Normal → SDH/CC**.
|
||||||
**Forced → Normal → SDH/CC**. For the purposes of hearing-impaired handling, SDH and
|
For the purposes of hearing-impaired handling, SDH and CC tracks are grouped as the
|
||||||
CC tracks are grouped as the "most captions" variant.
|
"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
|
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.
|
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. |
|
| `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. |
|
| `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`. |
|
| `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
|
```yaml
|
||||||
subtitle:
|
subtitle:
|
||||||
@@ -384,11 +385,30 @@ subtitle:
|
|||||||
sidecar_format: srt
|
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"
|
!!! note "`type_priority` also picks the default subtitle"
|
||||||
`type_priority` sorts the tracks within each language, and at mux time the first track
|
At mux time the first track in the default subtitle language gets the `default` flag.
|
||||||
in the default subtitle language gets the `default` flag. The built-in order is `forced`,
|
The built-in order is `forced`, `normal`, `sdh`, which means a forced track becomes
|
||||||
`normal`, `sdh`, which means a forced track becomes default whenever one exists. If you
|
default whenever one exists. If you want the full dialogue track as default instead,
|
||||||
want the full dialogue track as default instead, set:
|
set:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
subtitle:
|
subtitle:
|
||||||
|
|||||||
@@ -66,3 +66,72 @@ def test_cc_counts_as_sdh() -> None:
|
|||||||
t.add([sub("en", cc=True), sub("en")])
|
t.add([sub("en", cc=True), sub("en")])
|
||||||
t.sort_subtitles(by_language=["en"], type_priority=["normal", "sdh"])
|
t.sort_subtitles(by_language=["en"], type_priority=["normal", "sdh"])
|
||||||
assert shape(t) == [("en", "normal"), ("en", "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,
|
by_language=processed_audio_sort_lang,
|
||||||
codec_priority=config.audio.get("codec_priority"),
|
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_:
|
if list_:
|
||||||
available_tracks, _ = title.tracks.tree()
|
available_tracks, _ = title.tracks.tree()
|
||||||
|
|||||||
@@ -289,6 +289,7 @@ class Tracks:
|
|||||||
self,
|
self,
|
||||||
by_language: Optional[Sequence[Union[str, Language]]] = None,
|
by_language: Optional[Sequence[Union[str, Language]]] = None,
|
||||||
type_priority: Optional[Sequence[str]] = None,
|
type_priority: Optional[Sequence[str]] = None,
|
||||||
|
group_by: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Sort subtitle tracks by various track attributes to a common P2P standard.
|
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
|
- then rest ascending alphabetically after the prioritized groups
|
||||||
(Each section ascending alphabetically, but separated)
|
(Each section ascending alphabetically, but separated)
|
||||||
|
|
||||||
Language Group Order:
|
Type Order:
|
||||||
- Forced
|
- Forced
|
||||||
- Normal
|
- Normal
|
||||||
- Hard of Hearing (SDH/CC)
|
- Hard of Hearing (SDH/CC)
|
||||||
(Least to most captions expected in the subtitle)
|
(Least to most captions expected in the subtitle)
|
||||||
|
|
||||||
type_priority overrides the Language Group Order with an explicit ranking of
|
type_priority overrides the Type Order with an explicit ranking of "forced",
|
||||||
"forced", "normal", and "sdh" (cc counts as sdh); unlisted types fall to the end.
|
"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.
|
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:
|
if not self.subtitles:
|
||||||
return
|
return
|
||||||
# language groups
|
|
||||||
self.subtitles.sort(key=lambda x: str(x.language))
|
def by_type() -> None:
|
||||||
if type_priority:
|
if type_priority:
|
||||||
rank = {str(t).lower(): i for i, t in enumerate(type_priority)}
|
rank = {str(t).lower(): i for i, t in enumerate(type_priority)}
|
||||||
default_rank = len(rank)
|
default_rank = len(rank)
|
||||||
self.subtitles.sort(
|
self.subtitles.sort(
|
||||||
key=lambda x: rank.get("forced" if x.forced else "sdh" if (x.sdh or x.cc) else "normal", default_rank)
|
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:
|
else:
|
||||||
self.subtitles.sort(key=lambda x: x.sdh or x.cc)
|
by_lang()
|
||||||
self.subtitles.sort(key=lambda x: x.forced, reverse=True)
|
by_type()
|
||||||
# sections
|
# sections
|
||||||
for language in reversed(by_language or []):
|
for language in reversed(by_language or []):
|
||||||
if str(language) == "all":
|
if str(language) == "all":
|
||||||
|
|||||||
Reference in New Issue
Block a user