""" Background tasks for media processing and management. """ import logging from celery import shared_task from django.utils import timezone from datetime import timedelta logger = logging.getLogger(__name__) @shared_task(bind=True, max_retries=3, default_retry_delay=60) def process_uploaded_image(self, photo_id): """ Process an uploaded image asynchronously. This task runs after a photo is uploaded to perform additional processing like metadata extraction, validation, etc. Args: photo_id: ID of the Photo to process Returns: str: Processing result message """ from apps.media.models import Photo try: photo = Photo.objects.get(id=photo_id) # Log processing start logger.info(f"Processing photo {photo_id}: {photo.title}") # Additional processing could include: # - Generating additional thumbnails # - Extracting EXIF data # - Running image quality checks # - Updating photo metadata # For now, just log that processing is complete logger.info(f"Photo {photo_id} processed successfully") return f"Photo {photo_id} processed successfully" except Photo.DoesNotExist: logger.error(f"Photo {photo_id} not found") raise except Exception as exc: logger.error(f"Error processing photo {photo_id}: {str(exc)}") # Retry with exponential backoff raise self.retry(exc=exc, countdown=60 * (2 ** self.request.retries)) @shared_task(bind=True, max_retries=2) def cleanup_rejected_photos(self, days_old=30): """ Clean up photos that have been rejected for more than N days. This task runs periodically (e.g., weekly) to remove old rejected photos and free up storage space. Args: days_old: Number of days after rejection to delete (default: 30) Returns: dict: Cleanup statistics """ from apps.media.models import Photo from apps.media.services import PhotoService try: cutoff_date = timezone.now() - timedelta(days=days_old) # Find rejected photos older than cutoff old_rejected = Photo.objects.filter( moderation_status='rejected', moderated_at__lt=cutoff_date ) count = old_rejected.count() logger.info(f"Found {count} rejected photos to cleanup") # Delete each photo photo_service = PhotoService() deleted_count = 0 for photo in old_rejected: try: photo_service.delete_photo(photo, delete_from_cloudflare=True) deleted_count += 1 except Exception as e: logger.error(f"Failed to delete photo {photo.id}: {str(e)}") continue result = { 'found': count, 'deleted': deleted_count, 'failed': count - deleted_count, 'cutoff_date': cutoff_date.isoformat() } logger.info(f"Cleanup complete: {result}") return result except Exception as exc: logger.error(f"Error during photo cleanup: {str(exc)}") raise self.retry(exc=exc, countdown=300) # Retry after 5 minutes @shared_task(bind=True, max_retries=3) def generate_photo_thumbnails(self, photo_id, variants=None): """ Generate thumbnails for a photo on demand. This can be used to regenerate thumbnails if the original is updated or if new variants are needed. Args: photo_id: ID of the Photo variants: List of variant names to generate (None = all) Returns: dict: Generated variants and their URLs """ from apps.media.models import Photo from apps.media.services import CloudFlareService try: photo = Photo.objects.get(id=photo_id) cloudflare = CloudFlareService() if variants is None: variants = ['public', 'thumbnail', 'banner'] result = {} for variant in variants: url = cloudflare.get_image_url(photo.cloudflare_image_id, variant) result[variant] = url logger.info(f"Generated thumbnails for photo {photo_id}: {variants}") return result except Photo.DoesNotExist: logger.error(f"Photo {photo_id} not found") raise except Exception as exc: logger.error(f"Error generating thumbnails for photo {photo_id}: {str(exc)}") raise self.retry(exc=exc, countdown=60 * (2 ** self.request.retries)) @shared_task(bind=True, max_retries=2) def cleanup_orphaned_cloudflare_images(self): """ Clean up CloudFlare images that no longer have database records. This task helps prevent storage bloat by removing images that were uploaded but their database records were deleted. Returns: dict: Cleanup statistics """ from apps.media.models import Photo from apps.media.services import CloudFlareService try: cloudflare = CloudFlareService() # In a real implementation, you would: # 1. Get list of all images from CloudFlare API # 2. Check which ones don't have Photo records # 3. Delete the orphaned images # For now, just log that the task ran logger.info("Orphaned image cleanup task completed (not implemented in mock mode)") return { 'checked': 0, 'orphaned': 0, 'deleted': 0 } except Exception as exc: logger.error(f"Error during orphaned image cleanup: {str(exc)}") raise self.retry(exc=exc, countdown=300) @shared_task def update_photo_statistics(): """ Update photo-related statistics across the database. This task can update cached counts, generate reports, etc. Returns: dict: Updated statistics """ from apps.media.models import Photo from django.db.models import Count try: stats = { 'total_photos': Photo.objects.count(), 'pending': Photo.objects.filter(moderation_status='pending').count(), 'approved': Photo.objects.filter(moderation_status='approved').count(), 'rejected': Photo.objects.filter(moderation_status='rejected').count(), 'flagged': Photo.objects.filter(moderation_status='flagged').count(), 'by_type': dict( Photo.objects.values('photo_type').annotate(count=Count('id')) .values_list('photo_type', 'count') ) } logger.info(f"Photo statistics updated: {stats}") return stats except Exception as e: logger.error(f"Error updating photo statistics: {str(e)}") raise