import asyncio from pathlib import Path from fastapi import HTTPException import logging logger = logging.getLogger(__name__) class VideoService: def __init__(self, upload_dir: str, max_size_mb: int, supported_formats: list[str]): self.upload_dir = Path(upload_dir) self.max_size_bytes = max_size_mb * 1024 * 1024 self.supported_formats = supported_formats self.upload_dir.mkdir(parents=True, exist_ok=True) def validate_video(self, filename: str | None, content_type: str | None, size_bytes: int) -> None: if not filename: raise HTTPException(status_code=400, detail="No file selected") ext = Path(filename).suffix.lower() if ext not in self.supported_formats: raise HTTPException( status_code=400, detail=f"Unsupported format: {ext}. Supported: {', '.join(self.supported_formats)}", ) if size_bytes > self.max_size_bytes: raise HTTPException( status_code=413, detail=f"File exceeds {self.max_size_bytes // 1024 // 1024}MB limit", ) def get_video_path(self, video_id: str) -> Path: candidates = list(self.upload_dir.glob(f"{video_id}.*")) if not candidates: raise HTTPException(status_code=404, detail="Video not found") return candidates[0] def delete_video(self, video_id: str) -> None: for p in self.upload_dir.glob(f"{video_id}.*"): p.unlink() async def extract_audio(self, video_id: str) -> Path: video_path = self.get_video_path(video_id) output_path = self.upload_dir / f"{video_id}_audio.wav" proc = await asyncio.create_subprocess_exec( "ffmpeg", "-i", str(video_path), "-vn", "-acodec", "pcm_s16le", "-ar", "16000", "-ac", "1", "-f", "wav", str(output_path), stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) stdout, stderr = await proc.communicate() if proc.returncode != 0: if output_path.exists(): output_path.unlink(missing_ok=True) logger.error("ffmpeg failed for video_id=%s: %s", video_id, stderr.decode(errors="replace")) raise HTTPException( status_code=500, detail=f"Audio extraction failed: {stderr.decode(errors='replace')[:200]}", ) return output_path