"""generate_thumbnail task: PIL/ffmpeg thumbnail generation on the thumbnail queue. Lives separately from import_file because thumbnails can be regenerated en masse (FC-2c adds the 'regenerate all' admin action) and they're CPU-bound so they deserve their own queue lane. """ from pathlib import Path from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from ..celery_app import celery from ..config import get_config from ..models import ImageRecord from ..services.importer import is_video from ..services.thumbnailer import Thumbnailer IMAGES_ROOT = Path("/images") def _sync_session_factory(): cfg = get_config() engine = create_engine(cfg.database_url_sync, future=True, pool_pre_ping=True) return sessionmaker(engine, expire_on_commit=False) @celery.task(name="backend.app.tasks.thumbnail.generate_thumbnail", bind=True) def generate_thumbnail(self, image_id: int) -> dict: SessionLocal = _sync_session_factory() with SessionLocal() as session: record = session.get(ImageRecord, image_id) if record is None: return {"status": "missing", "image_id": image_id} thumbnailer = Thumbnailer(images_root=IMAGES_ROOT) source = Path(record.path) try: if is_video(source): result = thumbnailer.generate_video_thumbnail(source, record.sha256) else: result = thumbnailer.generate_image_thumbnail(source, record.sha256) except Exception as exc: # pragma: no cover — thumbnail failure is non-fatal return {"status": "failed", "image_id": image_id, "error": f"{type(exc).__name__}: {exc}"} record.thumbnail_path = str(result.path) session.add(record) session.commit() return {"status": "ok", "image_id": image_id, "path": str(result.path)}