diff --git a/unshackle/commands/dl.py b/unshackle/commands/dl.py index 14c6481..0bac6e7 100644 --- a/unshackle/commands/dl.py +++ b/unshackle/commands/dl.py @@ -1147,8 +1147,9 @@ class dl: with Live(Padding(progress, (0, 5, 1, 5)), console=console): for task_id, task_tracks in multiplex_tasks: progress.start_task(task_id) # TODO: Needed? + audio_expected = not video_only and not no_audio muxed_path, return_code, errors = task_tracks.mux( - str(title), progress=partial(progress.update, task_id=task_id), delete=False + str(title), progress=partial(progress.update, task_id=task_id), delete=False, audio_expected=audio_expected, title_language=title.language ) muxed_paths.append(muxed_path) if return_code >= 2: diff --git a/unshackle/core/tracks/track.py b/unshackle/core/tracks/track.py index d0c6f5a..2a02a02 100644 --- a/unshackle/core/tracks/track.py +++ b/unshackle/core/tracks/track.py @@ -420,7 +420,7 @@ class Track: for drm in self.drm: if isinstance(drm, PlayReady): return drm - elif hasattr(cdm, 'is_playready'): + elif hasattr(cdm, "is_playready"): if cdm.is_playready: for drm in self.drm: if isinstance(drm, PlayReady): @@ -567,15 +567,32 @@ class Track: output_path = original_path.with_stem(f"{original_path.stem}_repack") def _ffmpeg(extra_args: list[str] = None): - subprocess.run( + args = [ + binaries.FFMPEG, + "-hide_banner", + "-loglevel", + "error", + "-i", + original_path, + *(extra_args or []), + ] + + if hasattr(self, "data") and self.data.get("audio_language"): + audio_lang = self.data["audio_language"] + audio_name = self.data.get("audio_language_name", audio_lang) + args.extend( + [ + "-metadata:s:a:0", + f"language={audio_lang}", + "-metadata:s:a:0", + f"title={audio_name}", + "-metadata:s:a:0", + f"handler_name={audio_name}", + ] + ) + + args.extend( [ - binaries.FFMPEG, - "-hide_banner", - "-loglevel", - "error", - "-i", - original_path, - *(extra_args or []), # Following are very important! "-map_metadata", "-1", # don't transfer metadata to output file @@ -584,7 +601,11 @@ class Track: "-codec", "copy", str(output_path), - ], + ] + ) + + subprocess.run( + args, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, diff --git a/unshackle/core/tracks/tracks.py b/unshackle/core/tracks/tracks.py index 0b3bbe5..f2af797 100644 --- a/unshackle/core/tracks/tracks.py +++ b/unshackle/core/tracks/tracks.py @@ -305,7 +305,14 @@ class Tracks: ) return selected - def mux(self, title: str, delete: bool = True, progress: Optional[partial] = None) -> tuple[Path, int, list[str]]: + def mux( + self, + title: str, + delete: bool = True, + progress: Optional[partial] = None, + audio_expected: bool = True, + title_language: Optional[Language] = None, + ) -> tuple[Path, int, list[str]]: """ Multiplex all the Tracks into a Matroska Container file. @@ -315,7 +322,28 @@ class Tracks: delete: Delete all track files after multiplexing. progress: Update a rich progress bar via `completed=...`. This must be the progress object's update() func, pre-set with task id via functools.partial. + audio_expected: Whether audio is expected in the output. Used to determine + if embedded audio metadata should be added. + title_language: The title's intended language. Used to select the best video track + for audio metadata when multiple video tracks exist. """ + if self.videos and not self.audio and audio_expected: + video_track = None + if title_language: + video_track = next((v for v in self.videos if v.language == title_language), None) + if not video_track: + video_track = next((v for v in self.videos if v.is_original_lang), None) + + video_track = video_track or self.videos[0] + if video_track.language.is_valid(): + lang_code = str(video_track.language) + lang_name = video_track.language.display_name() + + for video in self.videos: + video.needs_repack = True + video.data["audio_language"] = lang_code + video.data["audio_language_name"] = lang_name + if not binaries.MKVToolNix: raise RuntimeError("MKVToolNix (mkvmerge) is required for muxing but was not found") @@ -332,12 +360,20 @@ class Tracks: raise ValueError("Video Track must be downloaded before muxing...") events.emit(events.Types.TRACK_MULTIPLEX, track=vt) + is_default = False + if title_language: + is_default = vt.language == title_language + if not any(v.language == title_language for v in self.videos): + is_default = vt.is_original_lang or i == 0 + else: + is_default = i == 0 + # Prepare base arguments video_args = [ "--language", f"0:{vt.language}", "--default-track", - f"0:{i == 0}", + f"0:{is_default}", "--original-flag", f"0:{vt.is_original_lang}", "--compression", @@ -363,6 +399,18 @@ class Tracks: ] ) + if hasattr(vt, "data") and vt.data.get("audio_language"): + audio_lang = vt.data["audio_language"] + audio_name = vt.data.get("audio_language_name", audio_lang) + video_args.extend( + [ + "--language", + f"1:{audio_lang}", + "--track-name", + f"1:{audio_name}", + ] + ) + cl.extend(video_args + ["(", str(vt.path), ")"]) for i, at in enumerate(self.audio):