Initial commit - Video Service for Coolify

This commit is contained in:
2025-12-17 10:07:44 +01:00
commit baa1675738
46 changed files with 2386 additions and 0 deletions

1
app/tasks/__init__.py Executable file
View File

@@ -0,0 +1 @@
# Tasks Module

219
app/tasks/video_tasks.py Executable file
View 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}