# app/tasks/thumbnail.py """ Thumbnail generation tasks. Handles generating thumbnails for images and videos, both individually and in bulk. """ import logging from datetime import datetime from app.celery_app import celery from app import db from app.models import ImageRecord, ImportTask log = logging.getLogger('celery.tasks.thumbnail') @celery.task(bind=True, name='app.tasks.thumbnail.generate_thumbnail_task', max_retries=2, default_retry_delay=30) def generate_thumbnail_task(self, image_id: int, overwrite: bool = False): """ Generate thumbnail for a single image/video. Args: image_id: ID of the ImageRecord overwrite: Whether to overwrite existing thumbnail Returns: dict with status and thumbnail path """ from app.utils.image_importer import generate_thumbnail, generate_video_thumbnail_mirrored record = ImageRecord.query.get(image_id) if not record: log.warning(f"Image {image_id} not found") return {'error': 'Image not found'} try: filepath = record.filepath if not filepath: return {'error': 'No filepath'} # Check if thumbnail already exists and we're not overwriting if record.thumb_path and not overwrite: import os if os.path.exists(record.thumb_path): return {'status': 'exists', 'thumb_path': record.thumb_path} log.debug(f"Generating thumbnail for: {filepath}") is_video = filepath.lower().endswith(('.mp4', '.mov', '.webm', '.avi', '.mkv')) if is_video: thumb_path = generate_video_thumbnail_mirrored(filepath) else: thumb_path = generate_thumbnail(filepath, overwrite=overwrite) if thumb_path: record.thumb_path = thumb_path db.session.commit() log.info(f"Generated thumbnail: {thumb_path}") return {'status': 'success', 'thumb_path': thumb_path} else: return {'status': 'failed', 'error': 'Thumbnail generation returned None'} except Exception as e: db.session.rollback() log.error(f"Thumbnail generation failed for {image_id}: {e}", exc_info=True) raise self.retry(exc=e) @celery.task(name='app.tasks.thumbnail.regenerate_all_thumbnails') def regenerate_all_thumbnails(batch_size: int = 100, overwrite: bool = True): """ Queue thumbnail regeneration for all images. Processes images in batches to avoid memory issues. Args: batch_size: Number of images to process per batch overwrite: Whether to overwrite existing thumbnails Returns: dict with count of queued tasks """ log.info("Starting bulk thumbnail regeneration") count = 0 last_id = 0 while True: records = ImageRecord.query.filter( ImageRecord.id > last_id, ImageRecord.filepath.isnot(None) ).order_by(ImageRecord.id.asc()).limit(batch_size).all() if not records: break for record in records: generate_thumbnail_task.delay(record.id, overwrite=overwrite) count += 1 last_id = records[-1].id log.info(f"Queued {count} thumbnail tasks so far...") log.info(f"Thumbnail regeneration complete: queued {count} tasks") return {'queued': count} @celery.task(name='app.tasks.thumbnail.regenerate_missing_thumbnails') def regenerate_missing_thumbnails(batch_size: int = 100): """ Generate thumbnails only for images that don't have them. Args: batch_size: Number of images to process per batch Returns: dict with count of queued tasks """ import os log.info("Checking for missing thumbnails") count = 0 last_id = 0 while True: records = ImageRecord.query.filter( ImageRecord.id > last_id, ImageRecord.filepath.isnot(None) ).order_by(ImageRecord.id.asc()).limit(batch_size).all() if not records: break for record in records: # Check if thumbnail is missing needs_thumb = False if not record.thumb_path: needs_thumb = True elif not os.path.exists(record.thumb_path): needs_thumb = True if needs_thumb: generate_thumbnail_task.delay(record.id, overwrite=False) count += 1 last_id = records[-1].id log.info(f"Missing thumbnail check complete: queued {count} tasks") return {'queued': count}