Initial commit - Video Service for Coolify
This commit is contained in:
1
app/tasks/__init__.py
Executable file
1
app/tasks/__init__.py
Executable file
@@ -0,0 +1 @@
|
||||
# Tasks Module
|
||||
219
app/tasks/video_tasks.py
Executable file
219
app/tasks/video_tasks.py
Executable file
@@ -0,0 +1,219 @@
|
||||
"""
|
||||
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}
|
||||
Reference in New Issue
Block a user