feat(debug): add JSONL debug logging to decryption, muxing, and all downloaders

Expand debug logging coverage for better diagnostics when investigating download/decryption issues like QUICKTIME/cbcs problem.
This commit is contained in:
Andy
2026-02-17 13:58:36 -07:00
parent 0217086abf
commit cc89f4ca93
7 changed files with 241 additions and 7 deletions

View File

@@ -441,6 +441,37 @@ def download(
yield dict(downloaded=f"{filesize.decimal(dl_speed)}/s") yield dict(downloaded=f"{filesize.decimal(dl_speed)}/s")
time.sleep(1) time.sleep(1)
if debug_logger:
output_files = []
output_total_size = 0
if output_dir.exists():
try:
for f in sorted(output_dir.iterdir()):
if f.is_file():
fsize = f.stat().st_size
output_files.append(f.name)
output_total_size += fsize
except OSError:
output_files = ["<error listing files>"]
debug_logger.log(
level="DEBUG",
operation="downloader_aria2c_complete",
message="Aria2c download completed successfully",
context={
"url_count": len(urls),
"gid_count": len(gids),
"completed_count": len(completed),
"output_dir": str(output_dir),
"output_dir_exists": output_dir.exists(),
"output_files_count": len(output_files),
"output_files": output_files[:10],
"output_total_size": output_total_size,
"filename": filename,
},
)
except KeyboardInterrupt: except KeyboardInterrupt:
DOWNLOAD_CANCELLED.set() DOWNLOAD_CANCELLED.set()
raise raise

View File

@@ -293,6 +293,18 @@ def curl_impersonate(
download_sizes.clear() download_sizes.clear()
if debug_logger: if debug_logger:
output_files = []
output_total_size = 0
if output_dir.exists():
try:
for f in sorted(output_dir.iterdir()):
if f.is_file():
fsize = f.stat().st_size
output_files.append(f.name)
output_total_size += fsize
except OSError:
output_files = ["<error listing files>"]
debug_logger.log( debug_logger.log(
level="DEBUG", level="DEBUG",
operation="downloader_curl_impersonate_complete", operation="downloader_curl_impersonate_complete",
@@ -300,6 +312,10 @@ def curl_impersonate(
context={ context={
"url_count": len(urls), "url_count": len(urls),
"output_dir": str(output_dir), "output_dir": str(output_dir),
"output_dir_exists": output_dir.exists(),
"output_files_count": len(output_files),
"output_files": output_files[:10],
"output_total_size": output_total_size,
"filename": filename, "filename": filename,
}, },
) )

View File

@@ -316,6 +316,17 @@ def download(
]) ])
track_url_display = track.url[:200] + "..." if len(track.url) > 200 else track.url track_url_display = track.url[:200] + "..." if len(track.url) > 200 else track.url
# Determine decryption engine info for logging
decryption_engine = None
decryption_binary = None
if content_keys:
decryption_config = config.decryption.lower()
decryption_engine = DECRYPTION_ENGINE.get(decryption_config) or "SHAKA_PACKAGER"
if decryption_engine == "SHAKA_PACKAGER" and binaries.ShakaPackager:
decryption_binary = str(binaries.ShakaPackager)
elif decryption_engine == "MP4DECRYPT" and binaries.Mp4decrypt:
decryption_binary = str(binaries.Mp4decrypt)
debug_logger.log( debug_logger.log(
level="DEBUG", level="DEBUG",
operation="downloader_n_m3u8dl_re_start", operation="downloader_n_m3u8dl_re_start",
@@ -331,6 +342,9 @@ def download(
"retry_count": retry_count, "retry_count": retry_count,
"has_content_keys": bool(content_keys), "has_content_keys": bool(content_keys),
"content_key_count": len(content_keys) if content_keys else 0, "content_key_count": len(content_keys) if content_keys else 0,
"decryption_engine": decryption_engine,
"decryption_binary": decryption_binary,
"decryption_config": config.decryption if content_keys else None,
"has_proxy": bool(proxy), "has_proxy": bool(proxy),
"skip_merge": skip_merge, "skip_merge": skip_merge,
"has_custom_args": bool(track.downloader_args), "has_custom_args": bool(track.downloader_args),

View File

@@ -303,6 +303,18 @@ def requests(
raise raise
if debug_logger: if debug_logger:
output_files = []
output_total_size = 0
if output_dir.exists():
try:
for f in sorted(output_dir.iterdir()):
if f.is_file():
fsize = f.stat().st_size
output_files.append(f.name)
output_total_size += fsize
except OSError:
output_files = ["<error listing files>"]
debug_logger.log( debug_logger.log(
level="DEBUG", level="DEBUG",
operation="downloader_requests_complete", operation="downloader_requests_complete",
@@ -310,6 +322,10 @@ def requests(
context={ context={
"url_count": len(urls), "url_count": len(urls),
"output_dir": str(output_dir), "output_dir": str(output_dir),
"output_dir_exists": output_dir.exists(),
"output_files_count": len(output_files),
"output_files": output_files[:10],
"output_total_size": output_total_size,
"filename": filename, "filename": filename,
}, },
) )

View File

@@ -20,7 +20,7 @@ from unshackle.core import binaries
from unshackle.core.config import config from unshackle.core.config import config
from unshackle.core.console import console from unshackle.core.console import console
from unshackle.core.constants import AnyTrack from unshackle.core.constants import AnyTrack
from unshackle.core.utilities import get_boxes from unshackle.core.utilities import get_boxes, get_debug_logger
from unshackle.core.utils.subprocess import ffprobe from unshackle.core.utils.subprocess import ffprobe
@@ -257,6 +257,21 @@ class Widevine:
decrypter = str(getattr(config, "decryption", "")).lower() decrypter = str(getattr(config, "decryption", "")).lower()
debug_logger = get_debug_logger()
if debug_logger:
debug_logger.log(
level="DEBUG",
operation="drm_decrypt_start",
message=f"Starting Widevine decryption with {decrypter or 'shaka_packager (default)'}",
context={
"decrypter": decrypter or "shaka_packager",
"path": str(path),
"file_size": path.stat().st_size if path.exists() else 0,
"content_key_count": len(self.content_keys),
"kids": [kid.hex for kid in self.content_keys.keys()],
},
)
if decrypter == "mp4decrypt": if decrypter == "mp4decrypt":
return self._decrypt_with_mp4decrypt(path) return self._decrypt_with_mp4decrypt(path)
else: else:
@@ -264,6 +279,8 @@ class Widevine:
def _decrypt_with_mp4decrypt(self, path: Path) -> None: def _decrypt_with_mp4decrypt(self, path: Path) -> None:
"""Decrypt using mp4decrypt""" """Decrypt using mp4decrypt"""
debug_logger = get_debug_logger()
if not binaries.Mp4decrypt: if not binaries.Mp4decrypt:
raise EnvironmentError("mp4decrypt executable not found but is required.") raise EnvironmentError("mp4decrypt executable not found but is required.")
@@ -284,10 +301,48 @@ class Widevine:
str(output_path), str(output_path),
] ]
if debug_logger:
debug_logger.log(
level="DEBUG",
operation="drm_mp4decrypt_start",
message="Running mp4decrypt",
context={
"binary": str(binaries.Mp4decrypt),
"input_path": str(path),
"input_size": path.stat().st_size if path.exists() else 0,
"output_path": str(output_path),
"key_count": len(self.content_keys),
"command": " ".join(str(c) for c in cmd[:3]) + " --key <REDACTED> ...",
},
)
try: try:
subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding='utf-8') result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding='utf-8')
if debug_logger:
debug_logger.log(
level="DEBUG",
operation="drm_mp4decrypt_complete",
message="mp4decrypt completed successfully",
context={
"output_path": str(output_path),
"output_exists": output_path.exists(),
"output_size": output_path.stat().st_size if output_path.exists() else 0,
"stderr": (result.stderr or "")[:500],
},
)
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
error_msg = e.stderr if e.stderr else f"mp4decrypt failed with exit code {e.returncode}" error_msg = e.stderr if e.stderr else f"mp4decrypt failed with exit code {e.returncode}"
if debug_logger:
debug_logger.log(
level="ERROR",
operation="drm_mp4decrypt_failed",
message=f"mp4decrypt failed with exit code {e.returncode}",
context={
"returncode": e.returncode,
"stderr": (e.stderr or "")[:1000],
"stdout": (e.stdout or "")[:500],
},
)
raise subprocess.CalledProcessError(e.returncode, cmd, output=e.stdout, stderr=error_msg) raise subprocess.CalledProcessError(e.returncode, cmd, output=e.stdout, stderr=error_msg)
if not output_path.exists(): if not output_path.exists():
@@ -300,6 +355,8 @@ class Widevine:
def _decrypt_with_shaka_packager(self, path: Path) -> None: def _decrypt_with_shaka_packager(self, path: Path) -> None:
"""Decrypt using Shaka Packager (original method)""" """Decrypt using Shaka Packager (original method)"""
debug_logger = get_debug_logger()
if not binaries.ShakaPackager: if not binaries.ShakaPackager:
raise EnvironmentError("Shaka Packager executable not found but is required.") raise EnvironmentError("Shaka Packager executable not found but is required.")
@@ -328,6 +385,21 @@ class Widevine:
config.directories.temp, config.directories.temp,
] ]
if debug_logger:
debug_logger.log(
level="DEBUG",
operation="drm_shaka_start",
message="Running Shaka Packager decryption",
context={
"binary": str(binaries.ShakaPackager),
"input_path": str(path),
"input_size": path.stat().st_size if path.exists() else 0,
"output_path": str(output_path),
"key_count": len(self.content_keys),
"kids": [kid.hex for kid in self.content_keys.keys()],
},
)
p = subprocess.Popen( p = subprocess.Popen(
[binaries.ShakaPackager, *arguments], [binaries.ShakaPackager, *arguments],
stdout=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
@@ -366,6 +438,21 @@ class Widevine:
p.wait() p.wait()
if debug_logger:
debug_logger.log(
level="DEBUG" if p.returncode == 0 and not had_error else "ERROR",
operation="drm_shaka_complete" if p.returncode == 0 and not had_error else "drm_shaka_failed",
message="Shaka Packager completed" if p.returncode == 0 and not had_error else "Shaka Packager failed",
context={
"returncode": p.returncode,
"had_error": had_error,
"stream_skipped": stream_skipped,
"output_exists": output_path.exists(),
"output_size": output_path.stat().st_size if output_path.exists() else 0,
"shaka_log": shaka_log_buffer.strip()[:1000] if shaka_log_buffer else None,
},
)
if p.returncode != 0 or had_error: if p.returncode != 0 or had_error:
raise subprocess.CalledProcessError(p.returncode, [binaries.ShakaPackager, *arguments]) raise subprocess.CalledProcessError(p.returncode, [binaries.ShakaPackager, *arguments])

View File

@@ -642,7 +642,24 @@ class DASH:
if skip_merge: if skip_merge:
# N_m3u8DL-RE handles merging and decryption internally # N_m3u8DL-RE handles merging and decryption internally
shutil.move(segments_to_merge[0], save_path) source_file = segments_to_merge[0]
if debug_logger:
debug_logger.log(
level="DEBUG",
operation="manifest_dash_skip_merge_move",
message="Moving n_m3u8dl_re output (decryption handled by downloader)",
context={
"track_id": getattr(track, "id", None),
"track_type": track.__class__.__name__,
"source_file": str(source_file),
"source_size": source_file.stat().st_size if source_file.exists() else 0,
"dest_path": str(save_path),
"has_drm": bool(drm),
"drm_type": drm.__class__.__name__ if drm else None,
"content_key_count": len(drm.content_keys) if drm and hasattr(drm, "content_keys") else 0,
},
)
shutil.move(source_file, save_path)
if drm: if drm:
track.drm = None track.drm = None
events.emit(events.Types.TRACK_DECRYPTED, track=track, drm=drm, segment=None) events.emit(events.Types.TRACK_DECRYPTED, track=track, drm=drm, segment=None)
@@ -676,8 +693,34 @@ class DASH:
events.emit(events.Types.TRACK_DOWNLOADED, track=track) events.emit(events.Types.TRACK_DOWNLOADED, track=track)
if not skip_merge and drm: if not skip_merge and drm:
if debug_logger:
debug_logger.log(
level="DEBUG",
operation="manifest_dash_decrypt_start",
message="Starting post-download decryption",
context={
"track_id": getattr(track, "id", None),
"track_type": track.__class__.__name__,
"save_path": str(save_path),
"file_size": save_path.stat().st_size if save_path.exists() else 0,
"drm_type": drm.__class__.__name__,
"content_key_count": len(drm.content_keys) if hasattr(drm, "content_keys") else 0,
},
)
progress(downloaded="Decrypting", completed=0, total=100) progress(downloaded="Decrypting", completed=0, total=100)
drm.decrypt(save_path) drm.decrypt(save_path)
if debug_logger:
debug_logger.log(
level="DEBUG",
operation="manifest_dash_decrypt_complete",
message="Post-download decryption completed",
context={
"track_id": getattr(track, "id", None),
"track_type": track.__class__.__name__,
"save_path": str(save_path),
"file_size": save_path.stat().st_size if save_path.exists() else 0,
},
)
track.drm = None track.drm = None
events.emit(events.Types.TRACK_DECRYPTED, track=track, drm=drm, segment=None) events.emit(events.Types.TRACK_DECRYPTED, track=track, drm=drm, segment=None)
progress(downloaded="Decrypting", advance=100) progress(downloaded="Decrypting", advance=100)

View File

@@ -535,6 +535,10 @@ class Tracks:
raise ValueError("No tracks provided, at least one track must be provided.") raise ValueError("No tracks provided, at least one track must be provided.")
debug_logger = get_debug_logger() debug_logger = get_debug_logger()
# Build the full command for both execution and logging
full_cmd = [*cl, "--output", str(output_path), "--gui-mode"]
if debug_logger: if debug_logger:
debug_logger.log( debug_logger.log(
level="DEBUG", level="DEBUG",
@@ -548,16 +552,38 @@ class Tracks:
"subtitle_count": len(self.subtitles), "subtitle_count": len(self.subtitles),
"attachment_count": len(self.attachments), "attachment_count": len(self.attachments),
"has_chapters": bool(self.chapters), "has_chapters": bool(self.chapters),
"command": [str(c) for c in full_cmd],
"video_tracks": [ "video_tracks": [
{"id": v.id, "codec": getattr(v, "codec", None), "language": str(v.language)} {
"id": v.id,
"codec": str(getattr(v, "codec", None)),
"language": str(v.language),
"path": str(v.path) if v.path else None,
"path_exists": v.path.exists() if v.path else False,
"file_size": v.path.stat().st_size if v.path and v.path.exists() else 0,
"drm": str(v.drm) if v.drm else None,
}
for v in self.videos for v in self.videos
], ],
"audio_tracks": [ "audio_tracks": [
{"id": a.id, "codec": getattr(a, "codec", None), "language": str(a.language)} {
"id": a.id,
"codec": str(getattr(a, "codec", None)),
"language": str(a.language),
"path": str(a.path) if a.path else None,
"path_exists": a.path.exists() if a.path else False,
"file_size": a.path.stat().st_size if a.path and a.path.exists() else 0,
"drm": str(a.drm) if a.drm else None,
}
for a in self.audio for a in self.audio
], ],
"subtitle_tracks": [ "subtitle_tracks": [
{"id": s.id, "codec": getattr(s, "codec", None), "language": str(s.language)} {
"id": s.id,
"codec": str(getattr(s, "codec", None)),
"language": str(s.language),
"path": str(s.path) if s.path else None,
}
for s in self.subtitles for s in self.subtitles
], ],
}, },
@@ -566,7 +592,7 @@ class Tracks:
# let potential failures go to caller, caller should handle # let potential failures go to caller, caller should handle
try: try:
errors = [] errors = []
p = subprocess.Popen([*cl, "--output", str(output_path), "--gui-mode"], text=True, stdout=subprocess.PIPE) p = subprocess.Popen(full_cmd, text=True, stdout=subprocess.PIPE)
for line in iter(p.stdout.readline, ""): for line in iter(p.stdout.readline, ""):
if line.startswith("#GUI#error") or line.startswith("#GUI#warning"): if line.startswith("#GUI#error") or line.startswith("#GUI#warning"):
errors.append(line) errors.append(line)
@@ -595,6 +621,7 @@ class Tracks:
context={ context={
"output_path": str(output_path), "output_path": str(output_path),
"output_exists": output_path.exists() if output_path else False, "output_exists": output_path.exists() if output_path else False,
"output_size": output_path.stat().st_size if output_path and output_path.exists() else 0,
}, },
) )