Files
olivv-cs-ComfyUI-FunPack/tests/test_media_thumb.py
T
digital-garbageandClaude Sonnet 5 27a82eead1 feat: server-side media thumbnails, cached, instead of the full original file
The media bin's grid always requested the FULL original file for a
thumbnail (an <img>/video-frame decode of the whole thing, only
cropped down visually via CSS/canvas afterward). A 4K photo or a
multi-MB clip got fully downloaded and decoded just to show a 96px
square, every render.

Added GET /api/media/{mid}/thumb: generates a small JPEG on first
request (Pillow resize for images, one ffmpeg frame-grab for video),
caches it under media/thumbnails/, and serves that instead. Deleting
a media item removes its cached thumbnail too. mediabrowser.js now
loads from this endpoint for both image and video kinds -- the same
canvas-drawing path as before, just fed a file that's already small
instead of the original. Measured live: a 1.57MB test clip's cached
thumbnail is 1.6KB.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 18:05:26 +03:00

105 lines
3.8 KiB
Python

"""media.thumb_path_for: cached, downscaled thumbnails instead of serving the full original
file for every media-bin grid render (see mediabrowser.js's own client-side history of the
same problem, fixed alongside this on the frontend side)."""
import shutil
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from movie_editor.backend import config, media # noqa: E402
pytest.importorskip("PIL", reason="Pillow is a real dependency of this project")
from PIL import Image # noqa: E402
FFMPEG = shutil.which("ffmpeg")
@pytest.fixture
def media_dir(tmp_path, monkeypatch):
monkeypatch.setattr(config, "MEDIA_DIR", tmp_path)
monkeypatch.setattr(media, "_load_index", lambda: media._items)
monkeypatch.setattr(media, "_save_index", lambda items: media._items.__setitem__(slice(None), items))
media._items = []
return tmp_path
def _add_image(media_dir, name="big.png", size=(1200, 600)):
Image.new("RGB", size, (200, 80, 40)).save(media_dir / name)
mid = "img" + name.replace(".", "")
media._items.append({"id": mid, "name": name, "filename": name, "kind": "image",
"size": 0, "added": 0})
return mid
def test_generates_a_downscaled_jpeg_for_an_oversized_image(media_dir):
mid = _add_image(media_dir, "big.png", (1200, 600))
out = media.thumb_path_for(mid)
assert out is not None and out.is_file()
assert out.suffix == ".jpg"
with Image.open(out) as im:
assert max(im.size) <= media.THUMB_MAX_DIM
assert im.size[0] / im.size[1] == pytest.approx(1200 / 600, rel=0.02)
def test_a_small_image_is_not_upscaled(media_dir):
mid = _add_image(media_dir, "small.png", (64, 32))
out = media.thumb_path_for(mid)
with Image.open(out) as im:
assert im.size == (64, 32)
def test_second_call_reuses_the_cached_file_instead_of_regenerating(media_dir, monkeypatch):
mid = _add_image(media_dir)
first = media.thumb_path_for(mid)
first_mtime = first.stat().st_mtime
monkeypatch.setattr(media, "_make_image_thumb",
lambda *a, **k: (_ for _ in ()).throw(AssertionError("should not regenerate")))
second = media.thumb_path_for(mid)
assert second == first
assert second.stat().st_mtime == first_mtime
def test_audio_and_other_kinds_have_no_thumbnail(media_dir):
media._items.append({"id": "a1", "name": "x.mp3", "filename": "x.mp3", "kind": "audio",
"size": 0, "added": 0})
(media_dir / "x.mp3").write_bytes(b"\0")
assert media.thumb_path_for("a1") is None
def test_missing_source_file_returns_none(media_dir):
media._items.append({"id": "gone", "name": "gone.png", "filename": "gone.png",
"kind": "image", "size": 0, "added": 0})
assert media.thumb_path_for("gone") is None
def test_unknown_id_returns_none(media_dir):
assert media.thumb_path_for("nope") is None
def test_delete_removes_the_cached_thumbnail_too(media_dir):
mid = _add_image(media_dir)
out = media.thumb_path_for(mid)
assert out.is_file()
assert media.delete(mid)
assert not out.is_file()
@pytest.mark.skipif(not FFMPEG, reason="needs a real ffmpeg on PATH")
def test_generates_a_frame_grab_for_video(media_dir, tmp_path):
import subprocess
src = media_dir / "clip.mp4"
subprocess.run(
[FFMPEG, "-y", "-f", "lavfi", "-i", "color=c=blue:s=640x360:d=1", "-frames:v", "3", str(src)],
capture_output=True, check=True,
)
media._items.append({"id": "vid1", "name": "clip.mp4", "filename": "clip.mp4",
"kind": "video", "size": 0, "added": 0})
out = media.thumb_path_for("vid1")
assert out is not None and out.is_file()
with Image.open(out) as im:
assert max(im.size) <= media.THUMB_MAX_DIM
assert im.size[0] / im.size[1] == pytest.approx(640 / 360, rel=0.05)