mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-13 12:35:05 +00:00
feat(tracks): add subtitle.type_priority config for variant ordering
Optional ranking of forced/normal/sdh per language, mirroring audio.codec_priority; first track gets the default flag, so this also picks the default subtitle. Unset = existing behavior.
This commit is contained in:
@@ -371,6 +371,7 @@ 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. |
|
| `output_mode` | str | `"mux"` | `mux` embeds subtitles in the MKV; `sidecar` writes separate files. |
|
||||||
| `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. |
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
subtitle:
|
subtitle:
|
||||||
@@ -379,6 +380,17 @@ subtitle:
|
|||||||
sidecar_format: srt
|
sidecar_format: srt
|
||||||
```
|
```
|
||||||
|
|
||||||
|
!!! 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:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
subtitle:
|
||||||
|
type_priority: [normal, sdh, forced]
|
||||||
|
```
|
||||||
|
|
||||||
### `audio`
|
### `audio`
|
||||||
|
|
||||||
- **Type:** `dict` · **Default:** `{}`
|
- **Type:** `dict` · **Default:** `{}`
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from unshackle.core.tracks import Subtitle, Tracks
|
||||||
|
|
||||||
|
|
||||||
|
def sub(lang: str, forced: bool = False, sdh: bool = False, cc: bool = False) -> Subtitle:
|
||||||
|
return Subtitle(
|
||||||
|
url=f"https://example.test/{lang}-{forced}-{sdh}-{cc}",
|
||||||
|
language=lang,
|
||||||
|
codec=Subtitle.Codec.SubRip,
|
||||||
|
forced=forced,
|
||||||
|
sdh=sdh,
|
||||||
|
cc=cc,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def shape(tracks: Tracks) -> list[tuple[str, str]]:
|
||||||
|
return [
|
||||||
|
(str(s.language), "forced" if s.forced else "sdh" if (s.sdh or s.cc) else "normal") for s in tracks.subtitles
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def tracks() -> Tracks:
|
||||||
|
t = Tracks()
|
||||||
|
t.add([sub("en", sdh=True), sub("en", forced=True), sub("en"), sub("it", forced=True), sub("it")])
|
||||||
|
return t
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_order_is_forced_normal_sdh(tracks: Tracks) -> None:
|
||||||
|
tracks.sort_subtitles(by_language=["en"])
|
||||||
|
assert shape(tracks) == [
|
||||||
|
("en", "forced"),
|
||||||
|
("en", "normal"),
|
||||||
|
("en", "sdh"),
|
||||||
|
("it", "forced"),
|
||||||
|
("it", "normal"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_type_priority_overrides_group_order(tracks: Tracks) -> None:
|
||||||
|
tracks.sort_subtitles(by_language=["en"], type_priority=["normal", "sdh", "forced"])
|
||||||
|
assert shape(tracks) == [
|
||||||
|
("en", "normal"),
|
||||||
|
("en", "sdh"),
|
||||||
|
("en", "forced"),
|
||||||
|
("it", "normal"),
|
||||||
|
("it", "forced"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_type_priority_unlisted_types_fall_to_end(tracks: Tracks) -> None:
|
||||||
|
tracks.sort_subtitles(by_language=["en"], type_priority=["normal"])
|
||||||
|
# unlisted types keep their pre-sort relative order (stable sort)
|
||||||
|
assert shape(tracks) == [
|
||||||
|
("en", "normal"),
|
||||||
|
("en", "sdh"),
|
||||||
|
("en", "forced"),
|
||||||
|
("it", "normal"),
|
||||||
|
("it", "forced"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_cc_counts_as_sdh() -> None:
|
||||||
|
t = Tracks()
|
||||||
|
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")]
|
||||||
@@ -2229,7 +2229,7 @@ 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)
|
title.tracks.sort_subtitles(by_language=s_lang, type_priority=config.subtitle.get("type_priority"))
|
||||||
|
|
||||||
if list_:
|
if list_:
|
||||||
available_tracks, _ = title.tracks.tree()
|
available_tracks, _ = title.tracks.tree()
|
||||||
|
|||||||
@@ -277,7 +277,11 @@ class Tracks:
|
|||||||
continue
|
continue
|
||||||
self.audio.sort(key=lambda x: not is_close_match(language, [x.language]))
|
self.audio.sort(key=lambda x: not is_close_match(language, [x.language]))
|
||||||
|
|
||||||
def sort_subtitles(self, by_language: Optional[Sequence[Union[str, Language]]] = None) -> None:
|
def sort_subtitles(
|
||||||
|
self,
|
||||||
|
by_language: Optional[Sequence[Union[str, Language]]] = None,
|
||||||
|
type_priority: Optional[Sequence[str]] = 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.
|
||||||
You may optionally provide a sequence of languages to prioritize to the top.
|
You may optionally provide a sequence of languages to prioritize to the top.
|
||||||
@@ -292,13 +296,25 @@ class Tracks:
|
|||||||
- 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
|
||||||
|
"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.
|
||||||
"""
|
"""
|
||||||
if not self.subtitles:
|
if not self.subtitles:
|
||||||
return
|
return
|
||||||
# language groups
|
# language groups
|
||||||
self.subtitles.sort(key=lambda x: str(x.language))
|
self.subtitles.sort(key=lambda x: str(x.language))
|
||||||
self.subtitles.sort(key=lambda x: x.sdh or x.cc)
|
if type_priority:
|
||||||
self.subtitles.sort(key=lambda x: x.forced, reverse=True)
|
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)
|
||||||
# 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