feat(tracks): configurable audio codec priority for tie-breaking

Adds optional `audio.codec_priority` list in unshackle.yaml to define preferred audio codec order when tracks share the same bitrate and language. Listed codecs rank in the order given; unlisted codecs retain bitrate-based order and fall after the listed group (soft priority - nothing dropped). Atmos and descriptive rules still apply last.
This commit is contained in:
imSp4rky
2026-05-18 11:08:55 -06:00
parent 900ad1fde1
commit 684e56eb97
5 changed files with 59 additions and 3 deletions

View File

@@ -168,6 +168,41 @@ dl:
--- ---
## audio (dict)
Configuration for audio track selection.
- `codec_priority`
Optional list of audio codec names defining the preferred order when multiple audio
tracks share the same bitrate and language. Listed codecs are ranked in the order given.
Codecs not in the list retain their bitrate-based ordering and are placed after all
listed codecs (i.e. soft priority — nothing is dropped).
Atmos tracks still take precedence over codec priority, and audio description tracks
are still moved to the end.
Valid codec names: `AAC`, `AC3`, `EC3`, `AC4`, `OPUS`, `OGG`, `DTS`, `ALAC`, `FLAC`.
For example,
```yaml
audio:
codec_priority: [FLAC, ALAC, AC4, EC3, DTS, AC3, OPUS, AAC, OGG]
```
Or to only prefer a subset (e.g. surround codecs first, everything else falls back to
bitrate order):
```yaml
audio:
codec_priority: [EC3, DTS, AC3, AAC]
```
When unset, audio tracks are sorted by bitrate alone (with Atmos/descriptive rules still
applied).
---
## subtitle (dict) ## subtitle (dict)
Configuration for subtitle processing and conversion. Configuration for subtitle processing and conversion.

View File

@@ -1562,7 +1562,10 @@ class dl:
processed_audio_sort_lang.append(language) processed_audio_sort_lang.append(language)
title.tracks.sort_videos(by_language=processed_video_sort_lang) title.tracks.sort_videos(by_language=processed_video_sort_lang)
title.tracks.sort_audio(by_language=processed_audio_sort_lang) title.tracks.sort_audio(
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)
if list_: if list_:

View File

@@ -71,6 +71,7 @@ class Config:
for name, filename in (kwargs.get("filenames") or {}).items(): for name, filename in (kwargs.get("filenames") or {}).items():
setattr(self.filenames, name, filename) setattr(self.filenames, name, filename)
self.audio: dict = kwargs.get("audio") or {}
self.headers: dict = kwargs.get("headers") or {} self.headers: dict = kwargs.get("headers") or {}
self.key_vaults: list[dict[str, Any]] = kwargs.get("key_vaults", []) self.key_vaults: list[dict[str, Any]] = kwargs.get("key_vaults", [])
self.muxing: dict = kwargs.get("muxing") or {} self.muxing: dict = kwargs.get("muxing") or {}

View File

@@ -249,12 +249,21 @@ class Tracks:
self.videos.sort(key=lambda x: str(x.language)) self.videos.sort(key=lambda x: str(x.language))
self.videos.sort(key=lambda x: not is_close_match(language, [x.language])) self.videos.sort(key=lambda x: not is_close_match(language, [x.language]))
def sort_audio(self, by_language: Optional[Sequence[Union[str, Language]]] = None) -> None: def sort_audio(
"""Sort audio tracks by bitrate, Atmos, descriptive, and optionally language.""" self,
by_language: Optional[Sequence[Union[str, Language]]] = None,
codec_priority: Optional[Sequence[str]] = None,
) -> None:
"""Sort audio tracks by bitrate, codec priority, Atmos, descriptive, and optionally language."""
if not self.audio: if not self.audio:
return return
# bitrate (highest first) # bitrate (highest first)
self.audio.sort(key=lambda x: float(x.bitrate or 0.0), reverse=True) self.audio.sort(key=lambda x: float(x.bitrate or 0.0), reverse=True)
# codec priority (listed codecs ranked in order; unlisted fall to end with bitrate order preserved)
if codec_priority:
rank = {str(c).upper(): i for i, c in enumerate(codec_priority)}
default_rank = len(rank)
self.audio.sort(key=lambda x: rank.get(x.codec.name if x.codec else "", default_rank))
# Atmos tracks first (prioritize over higher bitrate non-Atmos) # Atmos tracks first (prioritize over higher bitrate non-Atmos)
self.audio.sort(key=lambda x: not x.atmos) self.audio.sort(key=lambda x: not x.atmos)
# descriptive tracks last # descriptive tracks last

View File

@@ -422,6 +422,14 @@ curl_impersonate:
browser: chrome120 browser: chrome120
# Pre-define default options and switches of the dl command # Pre-define default options and switches of the dl command
# Audio track selection preferences
audio:
# Codec priority order used as a tiebreaker when multiple audio tracks share the same
# bitrate and language. Listed codecs are ranked in the order given; codecs not in the
# list keep their bitrate-based ordering and are placed after all listed codecs.
# Atmos still trumps codec priority. Valid names: AAC, AC3, EC3, AC4, OPUS, OGG, DTS, ALAC, FLAC.
# codec_priority: [FLAC, ALAC, AC4, EC3, DTS, AC3, OPUS, AAC, OGG]
dl: dl:
sub_format: srt sub_format: srt
downloads: 4 downloads: 4