Files
playground/youtube_subbed.py
2026-07-24 15:58:31 +02:00

176 lines
5.3 KiB
Python
Executable File

#!/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()