From a70eab3a9cdbdeb1a10324795a9a405733b59769 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 7 Jun 2026 23:02:18 +0000 Subject: [PATCH] Harden YouTube download fallback to pick the largest landed file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When yt-dlp's prepared filename doesn't exist (post-processing changed the extension), download_audio fell back to the first entry from iterdir(), whose order is arbitrary. yt-dlp can leave sidecars (thumbnail, .info.json, a leftover .part fragment) in the destination directory, so the fallback could return a non-audio file that then fails downstream with a confusing decode error. Pick the largest file instead — the decoded audio track dwarfs any metadata sidecar. https://claude.ai/code/session_01QVfdQDhreXWkEHQoArh71E --- aai_cli/youtube.py | 5 ++++- tests/test_youtube.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/aai_cli/youtube.py b/aai_cli/youtube.py index 51ba20a1..86af557a 100644 --- a/aai_cli/youtube.py +++ b/aai_cli/youtube.py @@ -57,6 +57,9 @@ def download_audio(url: str, dest_dir: Path) -> Path: if not path.is_file(): # Post-processing can change the extension; fall back to whatever landed. + # yt-dlp may also drop sidecars (thumbnail, .info.json, a leftover .part) + # in dest_dir, and iterdir() order is arbitrary, so pick the largest file: + # the decoded audio track dwarfs any metadata sidecar. files = [p for p in dest_dir.iterdir() if p.is_file()] if not files: raise CLIError( @@ -64,5 +67,5 @@ def download_audio(url: str, dest_dir: Path) -> Path: error_type="youtube_error", exit_code=1, ) - path = files[0] + path = max(files, key=lambda p: p.stat().st_size) return path diff --git a/tests/test_youtube.py b/tests/test_youtube.py index 3353a534..f9625a9d 100644 --- a/tests/test_youtube.py +++ b/tests/test_youtube.py @@ -80,6 +80,34 @@ def prepare_filename(self, info): assert youtube.download_audio("https://youtu.be/x", tmp_path) == landed +def test_download_audio_falls_back_to_largest_file(tmp_path, monkeypatch): + # yt-dlp can leave sidecars (thumbnail, .info.json) next to the audio track; + # the fallback must pick the audio (largest), not an arbitrary iterdir() entry. + audio = tmp_path / "actual.webm" + thumb = tmp_path / "actual.webp" + + class FakeYDL: + def __init__(self, opts): + pass + + def __enter__(self): + return self + + def __exit__(self, *exc): + return False + + def extract_info(self, url, download): + thumb.write_bytes(b"\x00" * 16) # small sidecar + audio.write_bytes(b"\x00" * 4096) # the real, much larger track + return {"id": "x"} + + def prepare_filename(self, info): + return str(tmp_path / "guessed.m4a") # wrong extension; file doesn't exist + + _fake_ytdlp(monkeypatch, FakeYDL) + assert youtube.download_audio("https://youtu.be/x", tmp_path) == audio + + def test_download_audio_no_file_produced_raises(tmp_path, monkeypatch): # prepare_filename points at a missing file and nothing landed in dest_dir. class FakeYDL: