mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2025-10-23 15:11:08 +00:00
feat: automatic audio language metadata for embedded audio tracks
- Add intelligent embedded audio language detection at mux stage - Automatically set audio language metadata when no separate audio tracks exist - Respect user flags (-V, --no-audio) to avoid unnecessary processing - Smart video track selection based on title language with fallbacks - Improved default track selection to prioritize title language matches - Enhanced FFmpeg repackaging with audio stream metadata injection - Works automatically for all services without service-specific code
This commit is contained in:
@@ -1147,8 +1147,9 @@ class dl:
|
|||||||
with Live(Padding(progress, (0, 5, 1, 5)), console=console):
|
with Live(Padding(progress, (0, 5, 1, 5)), console=console):
|
||||||
for task_id, task_tracks in multiplex_tasks:
|
for task_id, task_tracks in multiplex_tasks:
|
||||||
progress.start_task(task_id) # TODO: Needed?
|
progress.start_task(task_id) # TODO: Needed?
|
||||||
|
audio_expected = not video_only and not no_audio
|
||||||
muxed_path, return_code, errors = task_tracks.mux(
|
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)
|
muxed_paths.append(muxed_path)
|
||||||
if return_code >= 2:
|
if return_code >= 2:
|
||||||
|
|||||||
@@ -420,7 +420,7 @@ class Track:
|
|||||||
for drm in self.drm:
|
for drm in self.drm:
|
||||||
if isinstance(drm, PlayReady):
|
if isinstance(drm, PlayReady):
|
||||||
return drm
|
return drm
|
||||||
elif hasattr(cdm, 'is_playready'):
|
elif hasattr(cdm, "is_playready"):
|
||||||
if cdm.is_playready:
|
if cdm.is_playready:
|
||||||
for drm in self.drm:
|
for drm in self.drm:
|
||||||
if isinstance(drm, PlayReady):
|
if isinstance(drm, PlayReady):
|
||||||
@@ -567,8 +567,7 @@ class Track:
|
|||||||
output_path = original_path.with_stem(f"{original_path.stem}_repack")
|
output_path = original_path.with_stem(f"{original_path.stem}_repack")
|
||||||
|
|
||||||
def _ffmpeg(extra_args: list[str] = None):
|
def _ffmpeg(extra_args: list[str] = None):
|
||||||
subprocess.run(
|
args = [
|
||||||
[
|
|
||||||
binaries.FFMPEG,
|
binaries.FFMPEG,
|
||||||
"-hide_banner",
|
"-hide_banner",
|
||||||
"-loglevel",
|
"-loglevel",
|
||||||
@@ -576,6 +575,24 @@ class Track:
|
|||||||
"-i",
|
"-i",
|
||||||
original_path,
|
original_path,
|
||||||
*(extra_args or []),
|
*(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(
|
||||||
|
[
|
||||||
# Following are very important!
|
# Following are very important!
|
||||||
"-map_metadata",
|
"-map_metadata",
|
||||||
"-1", # don't transfer metadata to output file
|
"-1", # don't transfer metadata to output file
|
||||||
@@ -584,7 +601,11 @@ class Track:
|
|||||||
"-codec",
|
"-codec",
|
||||||
"copy",
|
"copy",
|
||||||
str(output_path),
|
str(output_path),
|
||||||
],
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
subprocess.run(
|
||||||
|
args,
|
||||||
check=True,
|
check=True,
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.PIPE,
|
||||||
|
|||||||
@@ -305,7 +305,14 @@ class Tracks:
|
|||||||
)
|
)
|
||||||
return selected
|
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.
|
Multiplex all the Tracks into a Matroska Container file.
|
||||||
|
|
||||||
@@ -315,7 +322,28 @@ class Tracks:
|
|||||||
delete: Delete all track files after multiplexing.
|
delete: Delete all track files after multiplexing.
|
||||||
progress: Update a rich progress bar via `completed=...`. This must be the
|
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.
|
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:
|
if not binaries.MKVToolNix:
|
||||||
raise RuntimeError("MKVToolNix (mkvmerge) is required for muxing but was not found")
|
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...")
|
raise ValueError("Video Track must be downloaded before muxing...")
|
||||||
events.emit(events.Types.TRACK_MULTIPLEX, track=vt)
|
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
|
# Prepare base arguments
|
||||||
video_args = [
|
video_args = [
|
||||||
"--language",
|
"--language",
|
||||||
f"0:{vt.language}",
|
f"0:{vt.language}",
|
||||||
"--default-track",
|
"--default-track",
|
||||||
f"0:{i == 0}",
|
f"0:{is_default}",
|
||||||
"--original-flag",
|
"--original-flag",
|
||||||
f"0:{vt.is_original_lang}",
|
f"0:{vt.is_original_lang}",
|
||||||
"--compression",
|
"--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), ")"])
|
cl.extend(video_args + ["(", str(vt.path), ")"])
|
||||||
|
|
||||||
for i, at in enumerate(self.audio):
|
for i, at in enumerate(self.audio):
|
||||||
|
|||||||
Reference in New Issue
Block a user