"""The editor's GIF output: one marked range of a library video made into a GIF (downloads/gif), held to the GIF's limits at enqueue, and made by the worker with real ffmpeg.""" import shutil import subprocess import pytest from app.config import settings from app.downloads import edit as editmod from app.downloads import pipeline, service from app.downloads import gif as gifmod from app.downloads import upload as uploadmod from app.downloads.models import DownloadJob, MediaAsset needs_ffmpeg = pytest.mark.skipif(shutil.which("ffmpeg") is None, reason="ffmpeg not available") def test_a_gif_edit_spec_keeps_its_size_and_is_another_edit_than_the_video(): video = editmod.normalize_edit_spec({"trim": {"start_s": 1, "end_s": 5}}) gif = editmod.normalize_edit_spec({"trim": {"start_s": 1, "end_s": 5}, "gif": {"side": 320}}) assert gif["gif"] == {"side": 320} and "gif" not in video assert editmod.edit_sig(gif) != editmod.edit_sig(video) # A GIF is always made, never stream-copied. assert editmod.needs_reencode(gif) def _source(db, user, tmp_path, seconds=40): src = tmp_path / "in.mp4" subprocess.run( [ "ffmpeg", "-y", "-loglevel", "error", "-f", "lavfi", "-i", f"testsrc2=size=640x360:rate=10:duration={seconds}", "-c:v", "libx264", "-pix_fmt", "yuv420p", str(src), ], check=True, ) # fmt: skip return uploadmod.ingest_upload( db, user.id, filename="in.mp4", src_path=src, size=src.stat().st_size ) @needs_ffmpeg @pytest.mark.parametrize( ("spec", "reason"), [ # A GIF is one range with an end: no join, no open end. ({"segments": [{"start_s": 0, "end_s": 2}, {"start_s": 4, "end_s": 6}]}, "gif_range"), ({"trim": {"start_s": 3}}, "gif_range"), ({"trim": {"start_s": 0, "end_s": 30.5}}, "gif_too_long"), ({"trim": {"start_s": 0, "end_s": 5}, "gif_side": 999}, "bad_gif"), ], ) def test_a_gif_edit_it_cannot_make_is_refused_with_its_reason( db, make_user, tmp_path, monkeypatch, spec, reason ): monkeypatch.setattr(settings, "files_root", str(tmp_path / "root")) u = make_user() source = _source(db, u, tmp_path) side = spec.pop("gif_side", 480) with pytest.raises(service.EditError) as e: service.enqueue_edit(db, u.id, source, {**spec, "gif": {"side": side}}) assert e.value.reason == reason @needs_ffmpeg def test_a_gif_edit_is_made_by_the_worker_and_kept_as_an_image_file( db, make_user, tmp_path, monkeypatch ): monkeypatch.setattr(settings, "files_root", str(tmp_path / "root")) u = make_user() source = _source(db, u, tmp_path, seconds=6) spec = {"trim": {"start_s": 1, "end_s": 3}, "crop": {"x": 0, "y": 0, "w": 320, "h": 360}} job = service.enqueue_edit(db, u.id, source, {**spec, "gif": {"side": 320}}) asset = db.get(MediaAsset, job.asset_id) assert asset.media_kind == "file" 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, asset.status, asset.container, asset.media_kind) == ( "done", "ready", "gif", "file", ) placed = tmp_path / "root" / asset.rel_path assert placed.read_bytes()[:6] == b"GIF89a" # The crop's 320x360 has its long side (360) fitted into 320. assert (asset.width, asset.height) == gifmod.size(placed) == (284, 320) assert asset.poster_path and (tmp_path / "root" / asset.poster_path).read_bytes()[:2] == ( b"\xff\xd8" )