#!/usr/bin/env -S uv run --script # /// script # dependencies = ["httpx", "srt"] # /// import argparse import dataclasses import logging import subprocess import os from pathlib import Path import httpx import srt import subtitle_translator def parse_args() -> argparse.Namespace: p = argparse.ArgumentParser() p.add_argument("url", help="YouTube video URL") p.add_argument("--lang", help="Target language code (e.g. de, fr, es)") p.add_argument("--quality", choices=["720p", "1080p"], default="1080p", help="Video quality to download") p.add_argument("--hardcode", action="store_true", help="Burn subtitles into video") return p.parse_args() @dataclasses.dataclass class Video: title: str video_path: Path subtitle_path: Path def download_video(url: str, save_dir: Path, quality: str = "1080p") -> Video: result = subprocess.run(["yt-dlp", "--get-title", url], capture_output=True, text=True, check=True) title = result.stdout.strip() safe_title = "".join(c if c.isalnum() or c in " .-_" else "_" for c in title) height = quality.rstrip("p") fmt = f"bestvideo[height={height}]+bestaudio/best[height={height}]/best" # Check if subtitles already exist sub_path = None for pat in (f"{safe_title}.en.srt", f"{safe_title}.en.*.srt"): found = list(save_dir.glob(pat)) if found: sub_path = found[0] break if not sub_path: # Download only subtitles first sub_cmd = [ "yt-dlp", "--skip-download", "--write-auto-sub", "--write-subs", "--sub-lang", "en", "--convert-subs", "srt", "--output", str(save_dir / f"{safe_title}.%(ext)s"), url, ] logging.info(f"Downloading subtitles: {url}") subprocess.run(sub_cmd, check=True) sub_path = save_dir / f"{safe_title}.en.srt" # Check if video already exists for ext in ("mp4", "mkv", "webm"): video_path = save_dir / f"{safe_title}.{ext}" if video_path.exists(): logging.info(f"Video already exists: {video_path.name}, skipping download.") return Video(title=safe_title, video_path=video_path, subtitle_path=sub_path) outtmpl = str(save_dir / f"{safe_title}.%(ext)s") cmd = [ "yt-dlp", "-f", fmt, "--output", outtmpl, url, ] logging.info(f"Downloading video: {url} at {quality}") subprocess.run(cmd, check=True) for ext in ("mp4", "mkv", "webm"): video_path = save_dir / f"{safe_title}.{ext}" if video_path.exists(): return Video(title=safe_title, video_path=video_path, subtitle_path=sub_path) raise RuntimeError("Video not downloaded") def hardcode_subs(video: Path, srt: Path, lang: str) -> Path: out = video.with_name(f"{video.stem}.{lang}.hardcoded.mp4") cmd = [ "ffmpeg", "-y", "-hwaccel", "videotoolbox", "-i", str(video), "-vf", f"subtitles='{srt}'", "-c:v", "h264_videotoolbox", "-crf", "30", "-c:a", "copy", str(out), ] logging.info(f"Hardcoding subtitles into video: {out.name}") subprocess.run(cmd, check=True) return out type Subs = dict[str, Path] def embed_subs(video_path: Path, subs: dict[str, Path]) -> Path: # subs: {lang: srt_path} out = video_path.with_name(f"{video_path.stem}.embedded.mp4") cmd = [ "ffmpeg", "-y", "-i", str(video_path), ] # Add each subtitle as an input for srt_path in subs.values(): cmd.extend(["-i", str(srt_path)]) # Copy video and audio streams (no re-encoding) cmd.extend( [ "-c:v", "copy", "-c:a", "copy", ] ) # Add subtitle codecs and metadata for each sub for i, lang in enumerate(subs.keys()): cmd.extend([f"-c:s:{i}", "mov_text"]) cmd.extend([f"-metadata:s:s:{i}", f"language={lang}"]) # Map video, audio, and all subtitle streams cmd.extend(["-map", "0:v", "-map", "0:a"]) for i in range(len(subs)): cmd.extend(["-map", f"{i + 1}:s"]) cmd.append(str(out)) logging.info(f"Embedding {len(subs)} subtitles into video (no re-encoding): {out.name}") subprocess.run(cmd, check=True) return out def main(): logging.basicConfig(level=logging.INFO, format="%(name)s: %(asctime)s %(levelname)s: %(message)s") logging.getLogger("httpx").setLevel(logging.WARNING) # Suppress httpx debug logs args = parse_args() save_dir = Path.cwd() vid = download_video(args.url, save_dir=save_dir, quality=args.quality) subs = {"en": vid.subtitle_path} if args.lang: translated_path = vid.subtitle_path.with_name(f"{vid.subtitle_path.name}.{args.lang}.srt") subtitle_translator.translate( subtitle_path=vid.subtitle_path, lang=args.lang, save_path=translated_path, condense=2, ) subs[args.lang] = translated_path if args.hardcode: out = hardcode_subs(vid.video_path, subs[args.lang], args.lang) else: out = embed_subs(video_path=vid.video_path, subs=subs) logging.info(f"Saved: {vid.title} to {out.name}") if __name__ == "__main__": main()