fix(downloads): a download larger than the space left says so, not "processing failed"

yt-dlp refuses a file announced over `max_filesize` by logging one line and returning with no file
(measured, and pinned in test_formats against a local server): the worker found no output and
failed the job as `postprocess`. `formats.watching_size_cap` now sees that line through our yt-dlp
logger, and the download (and, should it ever happen, a YouTube clip's range fetch) fails as the
owner's refusal with DOWNLOAD_TOO_BIG_MESSAGE, classified `quota_exceeded`. The download's quota
copy says it does not fit in what is left — true at a full cap too — instead of "Storage full".
A pre-existing type error the change touched is fixed (baseline down by one).
This commit is contained in:
2026-10-10 21:56:47 +02:00
parent 977ba244fa
commit e1d325b942
11 changed files with 195 additions and 34 deletions
+44
View File
@@ -3,6 +3,13 @@ playability guarantee) and must SELECT H.264 (not merely prefer it), so shared d
every browser incl. iOS/Safari. Non-mp4 containers and non-H.264 vcodecs are coerced, not honoured.
See the iPad AV1 broken-player fix + the format narrowing."""
import contextlib
import functools
import http.server
import threading
import yt_dlp
from app.downloads import edit as editmod
from app.downloads import formats
@@ -162,3 +169,40 @@ def test_the_mp3_fix_moved_the_audio_cache_identity(monkeypatch):
current = formats.format_sig(audio)
monkeypatch.setattr(formats, "_AUDIO_SIG_VERSION", 4)
assert formats.format_sig(audio) != current
class _QuietFiles(http.server.SimpleHTTPRequestHandler):
def log_message(self, *args): # the test's output is yt-dlp's, not the server's
pass
def handle(self):
# yt-dlp hangs up on a file over the cap as soon as it reads its size.
with contextlib.suppress(ConnectionError):
super().handle()
def test_yt_dlp_refuses_a_file_over_the_cap_without_raising_and_the_watch_sees_it(tmp_path):
# The boundary `watching_size_cap` reads, held against the yt-dlp this image pins: over
# `max_filesize` its HTTP downloader logs one line and RETURNS — no exception, no file — so
# without the watch the pipeline would only find "no output file" and call it a processing fault.
served = tmp_path / "served"
served.mkdir()
(served / "v.mp4").write_bytes(b"\0" * 200_000)
srv = http.server.ThreadingHTTPServer(
("127.0.0.1", 0), functools.partial(_QuietFiles, directory=str(served))
)
threading.Thread(target=srv.serve_forever, daemon=True).start()
url = f"http://127.0.0.1:{srv.server_port}/v.mp4"
try:
for cap, refused in ((1_000, True), (10_000_000, False)):
out = tmp_path / f"out-{cap}"
opts = formats.build_ydl_opts(
{}, str(out / "%(id)s.%(ext)s"), lambda d: None, max_bytes=cap
)
with formats.watching_size_cap() as over, yt_dlp.YoutubeDL(opts) as ydl:
ydl.extract_info(url, download=True)
assert over[0] is refused, cap
files = [p for p in out.glob("*") if p.suffix == ".mp4"] if out.exists() else []
assert bool(files) is not refused, cap
finally:
srv.shutdown()