fix(downloads): a produced file is held to the space left by its real size

yt-dlp's `max_filesize` only stops a plain HTTP file that announces its size: YouTube's fragmented
formats never meet it — measured on localdev, a 718 MB download ran to the end with 1 MB left and
put its owner four times over the cap. The download now re-reads what is left once its file is made
(after the phone-safe pass) and refuses one larger than that as the owner's refusal; an editor clip,
which nothing checked, is held the same way, as a player clip already was.
This commit is contained in:
2026-10-10 22:10:56 +02:00
parent 8312c6d592
commit 5b48018e92
4 changed files with 128 additions and 1 deletions
+3 -1
View File
@@ -307,7 +307,9 @@ def build_ydl_opts(
`max_bytes` (SEC-13) is the caller's remaining storage budget; yt-dlp's HTTP downloader refuses
a file announced larger than it rather than filling the disk and only then being caught by the
quota accounting. Its ffmpeg downloader (a `download_ranges` fetch) does not read it."""
quota accounting. Its ffmpeg downloader (a `download_ranges` fetch) does not read it, nor does
a fragmented download (YouTube's formats: measured, 718 MB fetched with 1 MB left) — so the
pipeline also holds every produced file to what is left by its real size."""
s = normalize(spec)
h = s["max_height"]
opts: dict = {
+14
View File
@@ -563,6 +563,13 @@ def _download(
CONVERTING,
int(info["duration"]) if info.get("duration") else None,
)
# Held to the cap by its real size, as a clip is: yt-dlp's `max_filesize` only stops a plain
# HTTP file that announces its size, and YouTube's fragmented formats never meet it
# (measured: 718 MB fetched with 1 MB left). Read again — other jobs may have landed.
with SessionLocal() as qdb:
room = quota.remaining_bytes(qdb, user_id)
if room is not None and produced.stat().st_size > room:
raise _OwnerRefused(dlerrors.DOWNLOAD_TOO_BIG_MESSAGE)
ext = produced.suffix.lstrip(".").lower()
# Codecs/dims/duration come from a PROBE of the produced file — the ground truth — NOT yt-dlp's
# `info` dict, whose fields are absent for some sources (Facebook among them): deriving
@@ -871,6 +878,13 @@ def _process_edit(job_id: int, asset_id: int) -> None:
# playable as its source: hold it to the same rule as every other ready file.
dest_staging = _ensure_phone_safe(job_id, dest_staging, staging, "editing", total_s)
out_ext = dest_staging.suffix.lstrip(".").lower() or out_ext
# New storage on the owner's footprint, held to what is left by its real size, as a
# download and a player clip are.
if user_id is not None:
with SessionLocal() as qdb:
room = quota.remaining_bytes(qdb, user_id)
if room is not None and dest_staging.stat().st_size > room:
raise StatedFailure(dlerrors.CLIP_TOO_BIG_MESSAGE)
rel = storage.edit_rel_path(user_id, asset_id, out_ext)
storage.place_file(dest_staging, settings.download_root, rel)