""" Celery Tasks for Video Processing """ import logging from datetime import UTC, datetime import httpx from app.celery_app import celery_app from app.config import get_settings from app.models.schemas import VideoMetadata, VideoStatus, WebhookPayload from app.services.converter import ConverterService from app.services.storage import StorageService logger = logging.getLogger(__name__) def send_webhook(payload: WebhookPayload) -> bool: """Send webhook notification to WordPress.""" settings = get_settings() if not settings.wordpress_webhook_url: logger.warning("WordPress webhook URL not configured") return False headers = {"Content-Type": "application/json"} if settings.wordpress_api_key: headers["X-API-Key"] = settings.wordpress_api_key try: with httpx.Client(timeout=30.0) as client: response = client.post( settings.wordpress_webhook_url, json=payload.model_dump(mode="json"), headers=headers, ) response.raise_for_status() logger.info(f"Webhook sent successfully: {payload.event}") return True except httpx.HTTPError as e: logger.error(f"Webhook failed: {e}") return False @celery_app.task(bind=True, max_retries=3) def process_video(self, video_id: str, input_filename: str) -> dict: """ Process uploaded video: 1. Extract metadata 2. Generate thumbnail 3. Convert to HLS 4. Send webhook on completion """ storage = StorageService() converter = ConverterService() input_path = storage.get_upload_path(video_id, input_filename) output_dir = storage.get_converted_path(video_id) try: # Update status to processing meta = storage.load_metadata(video_id) if not meta: raise RuntimeError(f"Metadata not found for video {video_id}") storage.update_status(video_id, VideoStatus.PROCESSING, progress=5) # Send progress webhook send_webhook( WebhookPayload( event="video.progress", video_id=video_id, kurs_id=meta["kurs_id"], status=VideoStatus.PROCESSING, progress=5, timestamp=datetime.now(UTC), ) ) # Step 1: Extract metadata (10%) logger.info(f"Extracting metadata for {video_id}") try: video_metadata = converter.extract_metadata(input_path) storage.update_status(video_id, VideoStatus.PROCESSING, progress=10) except Exception as e: logger.error(f"Metadata extraction failed: {e}") video_metadata = VideoMetadata() # Step 2: Generate thumbnail (20%) logger.info(f"Generating thumbnail for {video_id}") thumbnail_path = storage.get_thumbnail_path(video_id) thumbnail_success = converter.generate_thumbnail( input_path, thumbnail_path, time_offset=min(5.0, video_metadata.duration_seconds / 2), ) storage.update_status(video_id, VideoStatus.PROCESSING, progress=20) if thumbnail_success: logger.info(f"Thumbnail generated: {thumbnail_path}") else: logger.warning(f"Thumbnail generation failed for {video_id}") # Step 3: Convert to HLS (20% - 90%) logger.info(f"Starting HLS conversion for {video_id}") # Progress callback for conversion def update_progress(progress: int): # Map 0-100 to 20-90 mapped_progress = 20 + int(progress * 0.7) storage.update_status( video_id, VideoStatus.PROCESSING, progress=mapped_progress ) result = converter.convert_to_hls( input_path=input_path, output_dir=output_dir, progress_callback=update_progress, ) if not result["success"]: raise RuntimeError(f"HLS conversion failed: {result.get('error')}") # Step 4: Finalize (100%) logger.info(f"Conversion complete for {video_id}") # Save final metadata storage.save_metadata( video_id=video_id, kurs_id=meta["kurs_id"], title=meta["title"], status=VideoStatus.READY, metadata=video_metadata, ) storage.update_status(video_id, VideoStatus.READY, progress=100) # Build thumbnail URL settings = get_settings() if settings.environment == "production": base_url = f"https://{settings.video_domain}" else: base_url = "http://localhost:8500" thumbnail_url = ( f"{base_url}/api/v1/videos/{video_id}/thumbnail" if thumbnail_success else None ) # Send completion webhook send_webhook( WebhookPayload( event="video.ready", video_id=video_id, kurs_id=meta["kurs_id"], status=VideoStatus.READY, progress=100, metadata=video_metadata, thumbnail_url=thumbnail_url, timestamp=datetime.now(UTC), ) ) # Optionally delete source file to save space # input_path.unlink() return { "success": True, "video_id": video_id, "qualities": result["qualities"], "duration": video_metadata.duration_seconds, } except Exception as e: logger.exception(f"Video processing failed for {video_id}") # Update status to error storage.update_status(video_id, VideoStatus.ERROR, error_message=str(e)) # Load metadata for webhook meta = storage.load_metadata(video_id) or {} # Send error webhook send_webhook( WebhookPayload( event="video.error", video_id=video_id, kurs_id=meta.get("kurs_id", 0), status=VideoStatus.ERROR, error_message=str(e), timestamp=datetime.now(UTC), ) ) # Retry with exponential backoff raise self.retry(exc=e, countdown=60 * (2**self.request.retries)) from e @celery_app.task def cleanup_old_uploads(days: int = 7) -> dict: """Clean up old upload files that were never processed.""" from datetime import timedelta storage = StorageService() deleted_count = 0 cutoff = datetime.now(UTC) - timedelta(days=days) for video_id in storage.get_video_ids(): meta = storage.load_metadata(video_id) if meta: # Check if stuck in pending/uploading if meta.get("status") in ["pending", "uploading"]: created = datetime.fromisoformat(meta.get("created_at", "")) if created.replace(tzinfo=UTC) < cutoff: storage.delete_video(video_id) deleted_count += 1 logger.info(f"Deleted stale video: {video_id}") return {"deleted_count": deleted_count}