220 lines
7.2 KiB
Python
Executable File
220 lines
7.2 KiB
Python
Executable File
"""
|
|
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}
|