import asyncio import shutil import subprocess from pathlib import Path from fastapi import UploadFile from app.core.config import Settings async def save_upload(upload: UploadFile, job_dir: Path) -> Path: suffix = Path(upload.filename or "upload.mp4").suffix or ".mp4" destination = job_dir / f"source{suffix.lower()}" with destination.open("wb") as handle: while chunk := await upload.read(1024 * 1024): handle.write(chunk) return destination async def resolve_youtube_url(url: str, job_dir: Path, settings: Settings) -> Path: if settings.demo_mode: return await asyncio.to_thread(create_demo_video, job_dir, settings) try: import yt_dlp except Exception as exc: raise RuntimeError("yt-dlp is required for YouTube ingestion") from exc output_template = str(job_dir / "source.%(ext)s") ydl_opts = { "outtmpl": output_template, "format": "bv*[ext=mp4]+ba[ext=m4a]/b[ext=mp4]/best", "merge_output_format": "mp4", "quiet": True, "noprogress": True, } def download() -> Path: with yt_dlp.YoutubeDL(ydl_opts) as ydl: ydl.download([url]) matches = sorted(job_dir.glob("source.*")) if not matches: raise RuntimeError("yt-dlp finished without producing a video") return matches[0] return await asyncio.to_thread(download) def create_demo_video(job_dir: Path, settings: Settings) -> Path: destination = job_dir / "source.mp4" ffmpeg = shutil.which(settings.ffmpeg_binary) if not ffmpeg: destination.write_bytes(b"") return destination command = [ ffmpeg, "-y", "-f", "lavfi", "-i", "testsrc2=size=1280x720:rate=30:duration=120", "-f", "lavfi", "-i", "sine=frequency=660:sample_rate=48000:duration=120", "-shortest", "-c:v", "libx264", "-pix_fmt", "yuv420p", "-c:a", "aac", str(destination), ] try: subprocess.run(command, check=True, capture_output=True, text=True, timeout=45) except Exception: destination.write_bytes(b"") return destination