"""ffmpeg thumbnail generation for gallery media (videos + gifs). Grid previews use a small cached JPEG extracted by ffmpeg; the browser only receives the real file when the full-screen modal opens. """ import shutil import subprocess from pathlib import Path from django.conf import settings FFMPEG = shutil.which('ffmpeg') MAX_WIDTH = 480 def thumbnail_stored_path(file_index): """Relative stored path of the cached thumbnail for an asset file.""" return f'thumbnails/{file_index.uuid}.jpg' def thumbnail_ready(file_index): return (Path(settings.MEDIA_ROOT) / thumbnail_stored_path(file_index)).is_file() def generate_thumbnail(file_index): """Generate (and cache) a JPEG thumbnail for a video/gif FileIndex. Returns the relative stored path, or None when ffmpeg is unavailable or extraction fails. Idempotent — a cached thumbnail is reused. """ if not FFMPEG or not file_index.stored_path: return None src = (Path(settings.MEDIA_ROOT) / file_index.stored_path).resolve() if not src.is_file(): return None out_rel = thumbnail_stored_path(file_index) out = (Path(settings.MEDIA_ROOT) / out_rel).resolve() if out.is_file(): return out_rel out.parent.mkdir(parents=True, exist_ok=True) tmp = out.with_suffix('.tmp.jpg') cmd = [ FFMPEG, '-y', '-i', str(src), '-frames:v', '1', '-vf', f"scale='min({MAX_WIDTH},iw)':-2", '-q:v', '4', str(tmp), ] try: result = subprocess.run(cmd, capture_output=True, timeout=30) except (subprocess.TimeoutExpired, OSError): return None if result.returncode != 0 or not tmp.is_file(): return None tmp.replace(out) return out_rel