mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-11 19:45:06 +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. |
|
||||
| `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`. |
|
||||
| `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
|
||||
subtitle:
|
||||
@@ -379,6 +380,17 @@ subtitle:
|
||||
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`
|
||||
|
||||
- **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,
|
||||
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_:
|
||||
available_tracks, _ = title.tracks.tree()
|
||||
|
||||
@@ -277,7 +277,11 @@ class Tracks:
|
||||
continue
|
||||
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.
|
||||
You may optionally provide a sequence of languages to prioritize to the top.
|
||||
@@ -292,13 +296,25 @@ class Tracks:
|
||||
- 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.
|
||||
"""
|
||||
if not self.subtitles:
|
||||
return
|
||||
# language groups
|
||||
self.subtitles.sort(key=lambda x: str(x.language))
|
||||
self.subtitles.sort(key=lambda x: x.sdh or x.cc)
|
||||
self.subtitles.sort(key=lambda x: x.forced, reverse=True)
|
||||
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)
|
||||
# sections
|
||||
for language in reversed(by_language or []):
|
||||
if str(language) == "all":
|
||||
|
||||
Reference in New Issue
Block a user