From 684e56eb97413c1db31860b33bca6f2379232db4 Mon Sep 17 00:00:00 2001 From: imSp4rky Date: Mon, 18 May 2026 11:08:55 -0600 Subject: [PATCH] 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. --- docs/DOWNLOAD_CONFIG.md | 35 ++++++++++++++++++++++++++++++++ unshackle/commands/dl.py | 5 ++++- unshackle/core/config.py | 1 + unshackle/core/tracks/tracks.py | 13 ++++++++++-- unshackle/unshackle-example.yaml | 8 ++++++++ 5 files changed, 59 insertions(+), 3 deletions(-) diff --git a/docs/DOWNLOAD_CONFIG.md b/docs/DOWNLOAD_CONFIG.md index 6b29b83..119c2f2 100644 --- a/docs/DOWNLOAD_CONFIG.md +++ b/docs/DOWNLOAD_CONFIG.md @@ -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) Configuration for subtitle processing and conversion. diff --git a/unshackle/commands/dl.py b/unshackle/commands/dl.py index ecb0802..01aa45d 100644 --- a/unshackle/commands/dl.py +++ b/unshackle/commands/dl.py @@ -1562,7 +1562,10 @@ class dl: processed_audio_sort_lang.append(language) 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) if list_: diff --git a/unshackle/core/config.py b/unshackle/core/config.py index 06d5fe3..3ffbfdc 100644 --- a/unshackle/core/config.py +++ b/unshackle/core/config.py @@ -71,6 +71,7 @@ class Config: for name, filename in (kwargs.get("filenames") or {}).items(): setattr(self.filenames, name, filename) + self.audio: dict = kwargs.get("audio") or {} self.headers: dict = kwargs.get("headers") or {} self.key_vaults: list[dict[str, Any]] = kwargs.get("key_vaults", []) self.muxing: dict = kwargs.get("muxing") or {} diff --git a/unshackle/core/tracks/tracks.py b/unshackle/core/tracks/tracks.py index 146391f..15cb474 100644 --- a/unshackle/core/tracks/tracks.py +++ b/unshackle/core/tracks/tracks.py @@ -249,12 +249,21 @@ class Tracks: self.videos.sort(key=lambda x: str(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: - """Sort audio tracks by bitrate, Atmos, descriptive, and optionally language.""" + def sort_audio( + 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: return # bitrate (highest first) 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) self.audio.sort(key=lambda x: not x.atmos) # descriptive tracks last diff --git a/unshackle/unshackle-example.yaml b/unshackle/unshackle-example.yaml index a529c3f..e9ff580 100644 --- a/unshackle/unshackle-example.yaml +++ b/unshackle/unshackle-example.yaml @@ -422,6 +422,14 @@ curl_impersonate: browser: chrome120 # 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: sub_format: srt downloads: 4