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: