Files
siftlode/backend/tests/test_odd_frame.py
T
peter 68cfc7e25d fix(downloads): an upload's conversion is guarded, counted, named and cleaned up
- Every ffmpeg/ffprobe over a library file names the demuxers it may use
  (edit.INPUT_GUARD; a downloaded thumbnail edit.THUMB_GUARD): a playlist-type
  demuxer would read local paths named inside a user's upload.
- A re-encode ends on an even frame (edit.EVEN_FRAME): an odd 4:4:4/RGB source
  failed its conversion; a clip stores the frame its file has.
- An upload counts against its owner's cap from the moment it lands (queued, or
  kept after a failed conversion) — the footprint, the GC's cap and the admin
  view read one ON_DISK predicate — and a conversion that outgrows the cap is
  refused, keeping the original.
- The converted name keeps _a{id} (a dotted bare name once lost it and two
  uploads shared a file); a converted file placed but not committed is removed,
  and the orphan sweep enters .uploads; the LRU cap never evicts an upload.
- Ingest judges the extension the file is stored under; a failed conversion is
  convert_failed / upload_missing, reports the "converting" phase, and notifies
  as an upload. Job statuses in routes/downloads use JobStatus.
2026-10-06 03:19:32 +02:00

74 lines
3.1 KiB
Python

"""A libx264 re-encode of an odd-sized frame: 4:2:0 halves the chroma, so libx264 refuses an odd
width or height ("width not divisible by 2"). Measured: a source that is odd AND not 4:2:0 already
(a 4:4:4 or RGB screen capture, 1365x767) failed its conversion outright; a 4:2:0 source never
reaches the encoder odd (ffmpeg's crop rounds to its chroma grid), but the clip then stored the
size the user dragged, not the one the file has. Every re-encode now ends its filter chain on
`edit.EVEN_FRAME` (at most one pixel row or column), and a clip stores its probed frame.
Real ffmpeg on both paths — the refusal is the encoder's, so only the encoder can show it is gone.
"""
import shutil
import subprocess
from pathlib import Path
import pytest
from app.config import settings
from app.downloads import pipeline, playable, probe, service, storage
from app.downloads import upload as uploadmod
from app.models import DownloadJob, MediaAsset
pytestmark = pytest.mark.skipif(shutil.which("ffmpeg") is None, reason="ffmpeg not available")
def _ffmpeg(*args: str) -> None:
subprocess.run(["ffmpeg", "-y", "-loglevel", "error", *args], check=True)
def _frame(path: Path) -> tuple[int | None, int | None]:
_, _, w, h, _ = probe.probe_media(path)
return w, h
def test_an_odd_sized_4_4_4_video_converts_to_the_even_frame_below_it(tmp_path):
src = tmp_path / "in.mkv"
_ffmpeg(
"-f", "lavfi", "-i", "testsrc=size=161x121:rate=10:duration=1",
"-c:v", "libx264", "-pix_fmt", "yuv444p", str(src),
) # fmt: skip
assert _frame(src) == (161, 121) # odd for real (`testsrc2` rounds its own size)
p = playable.plan(probe.probe_json(src), "mkv")
assert p is not None and p.reencode_video
dest = tmp_path / "out.mp4"
subprocess.run(playable.build_cmd(src, dest, p), check=True, capture_output=True)
assert _frame(dest) == (160, 120)
def test_an_odd_crop_makes_a_clip_and_stores_the_frame_it_has(db, make_user, tmp_path, monkeypatch):
monkeypatch.setattr(settings, "download_root", str(tmp_path))
src = tmp_path / "in.mp4"
_ffmpeg(
"-f", "lavfi", "-i", "testsrc2=size=160x120:rate=10:duration=2",
"-f", "lavfi", "-i", "sine=duration=2",
"-c:v", "libx264", "-pix_fmt", "yuv420p", "-c:a", "aac", str(src),
) # fmt: skip
u = make_user()
source = uploadmod.ingest_upload(
db, u.id, filename="clip.mp4", src_path=src, size=src.stat().st_size
)
assert source.status == "done" # already phone-safe: the clip's source as it came
job = service.enqueue_edit(db, u.id, source, {"crop": {"x": 3, "y": 5, "w": 101, "h": 51}})
asset = db.get(MediaAsset, job.asset_id)
job.status, asset.status = "running", "downloading" # the worker's claim
db.commit()
pipeline._process_edit(job.id, asset.id)
db.expire_all()
job, asset = db.get(DownloadJob, job.id), db.get(MediaAsset, job.asset_id)
assert (job.status, job.error, asset.status) == ("done", None, "ready")
final = storage.abs_path(str(tmp_path), asset.rel_path)
assert _frame(final) == (100, 50)
assert (asset.width, asset.height) == (100, 50) # what the file is, not what was asked