61 lines
1.7 KiB
Python
61 lines
1.7 KiB
Python
"""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
|