From a2c6798fe6557ef11b7bc2175c326c55815fa22a Mon Sep 17 00:00:00 2001 From: Andy Date: Thu, 25 Sep 2025 23:21:35 +0000 Subject: [PATCH 1/3] fix: optimize audio track sorting by grouping descriptive tracks and sorting by bitrate, fixes bug that does not identify ATMOS or DD+ as the highest quality available in filenaming. --- unshackle/core/tracks/tracks.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/unshackle/core/tracks/tracks.py b/unshackle/core/tracks/tracks.py index 34c9da6..cf691b7 100644 --- a/unshackle/core/tracks/tracks.py +++ b/unshackle/core/tracks/tracks.py @@ -202,17 +202,16 @@ class Tracks: """Sort audio tracks by bitrate, descriptive, and optionally language.""" if not self.audio: return - # bitrate - self.audio.sort(key=lambda x: float(x.bitrate or 0.0), reverse=True) # descriptive - self.audio.sort(key=lambda x: str(x.language) if x.descriptive else "") + self.audio.sort(key=lambda x: x.descriptive) + # bitrate (within each descriptive group) + self.audio.sort(key=lambda x: float(x.bitrate or 0.0), reverse=True) # language for language in reversed(by_language or []): if str(language) in ("all", "best"): language = next((x.language for x in self.audio if x.is_original_lang), "") if not language: continue - self.audio.sort(key=lambda x: str(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: From 30314fdb46b9a9739a219a564325371ea2395fb3 Mon Sep 17 00:00:00 2001 From: Andy Date: Fri, 26 Sep 2025 01:41:07 +0000 Subject: [PATCH 2/3] Fix missing movie/episode changes for last commit --- unshackle/core/titles/episode.py | 12 +++++++++++- unshackle/core/titles/movie.py | 12 +++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/unshackle/core/titles/episode.py b/unshackle/core/titles/episode.py index f66bcb7..16ecab6 100644 --- a/unshackle/core/titles/episode.py +++ b/unshackle/core/titles/episode.py @@ -89,7 +89,17 @@ class Episode(Title): def get_filename(self, media_info: MediaInfo, folder: bool = False, show_service: bool = True) -> str: primary_video_track = next(iter(media_info.video_tracks), None) - primary_audio_track = next(iter(media_info.audio_tracks), None) + primary_audio_track = None + if media_info.audio_tracks: + sorted_audio = sorted( + media_info.audio_tracks, + key=lambda x: ( + float(x.bit_rate) if x.bit_rate else 0, + bool(x.format_additionalfeatures and "JOC" in x.format_additionalfeatures) + ), + reverse=True + ) + primary_audio_track = sorted_audio[0] unique_audio_languages = len({x.language.split("-")[0] for x in media_info.audio_tracks if x.language}) # Title [Year] SXXEXX Name (or Title [Year] SXX if folder) diff --git a/unshackle/core/titles/movie.py b/unshackle/core/titles/movie.py index 3d552d2..2e1d8bb 100644 --- a/unshackle/core/titles/movie.py +++ b/unshackle/core/titles/movie.py @@ -52,7 +52,17 @@ class Movie(Title): def get_filename(self, media_info: MediaInfo, folder: bool = False, show_service: bool = True) -> str: primary_video_track = next(iter(media_info.video_tracks), None) - primary_audio_track = next(iter(media_info.audio_tracks), None) + primary_audio_track = None + if media_info.audio_tracks: + sorted_audio = sorted( + media_info.audio_tracks, + key=lambda x: ( + float(x.bit_rate) if x.bit_rate else 0, + bool(x.format_additionalfeatures and "JOC" in x.format_additionalfeatures) + ), + reverse=True + ) + primary_audio_track = sorted_audio[0] unique_audio_languages = len({x.language.split("-")[0] for x in media_info.audio_tracks if x.language}) # Name (Year) From e92e5c2ba3b0f6632e1e326b756ab7a517491e25 Mon Sep 17 00:00:00 2001 From: Andy Date: Fri, 26 Sep 2025 04:42:06 +0000 Subject: [PATCH 3/3] feat: add AC4 codec support in Audio class and update mime/profile handling --- unshackle/core/tracks/audio.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/unshackle/core/tracks/audio.py b/unshackle/core/tracks/audio.py index ff5de9f..0069efa 100644 --- a/unshackle/core/tracks/audio.py +++ b/unshackle/core/tracks/audio.py @@ -12,6 +12,7 @@ class Audio(Track): AAC = "AAC" # https://wikipedia.org/wiki/Advanced_Audio_Coding AC3 = "DD" # https://wikipedia.org/wiki/Dolby_Digital EC3 = "DD+" # https://wikipedia.org/wiki/Dolby_Digital_Plus + AC4 = "AC-4" # https://wikipedia.org/wiki/Dolby_AC-4 OPUS = "OPUS" # https://wikipedia.org/wiki/Opus_(audio_format) OGG = "VORB" # https://wikipedia.org/wiki/Vorbis DTS = "DTS" # https://en.wikipedia.org/wiki/DTS_(company)#DTS_Digital_Surround @@ -31,6 +32,8 @@ class Audio(Track): return Audio.Codec.AC3 if mime == "ec-3": return Audio.Codec.EC3 + if mime == "ac-4": + return Audio.Codec.AC4 if mime == "opus": return Audio.Codec.OPUS if mime == "dtsc": @@ -60,6 +63,8 @@ class Audio(Track): return Audio.Codec.AC3 if profile.startswith("ddplus"): return Audio.Codec.EC3 + if profile.startswith("ac4"): + return Audio.Codec.AC4 if profile.startswith("playready-oggvorbis"): return Audio.Codec.OGG raise ValueError(f"The Content Profile '{profile}' is not a supported Audio Codec")