Files
playground/youtube_to_srt.py
T

87 lines
3.7 KiB
Python
Executable File

#!/usr/bin/env -S uv run --script
# /// script
# dependencies = ["yt-dlp"]
# ///
import argparse
import logging
import re
import subprocess
import tempfile
from pathlib import Path
import yt_dlp
VIDEO_TO_SRT = Path(__file__).resolve().with_name("video_to_srt.py")
MAX_FILENAME_BYTES = 240
logger = logging.getLogger(__name__)
def parse_args() -> tuple[argparse.Namespace, list[str]]:
"""Parse command-line arguments; unknown options are returned to be forwarded to video_to_srt.py."""
parser = argparse.ArgumentParser(
description="Download a YouTube video's audio and transcribe it with video_to_srt.py. "
"Options not listed here (--model, --speed-up, --language, --chunk-minutes, --api-key, --no-diarize, ...) are passed through.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument("video", help="YouTube URL or video id")
parser.add_argument("--output-dir", "-d", type=Path, default=Path.cwd(), help="Directory for the SRT file")
return parser.parse_known_args()
def sanitize(text: str) -> str:
"""Make text safe for a filename: path separators and other reserved characters become '-', whitespace collapses."""
# [\\/:*?"<>|\x00-\x1f] = characters not allowed on common filesystems, plus control characters
text = re.sub(r'[\\/:*?"<>|\x00-\x1f]', "-", text)
return re.sub(r"\s+", " ", text).strip()
def format_date(upload_date: str | None) -> str:
"""Convert yt-dlp's YYYYMMDD upload date to yyyy-mm-dd ('unknown-date' if missing)."""
if not upload_date or len(upload_date) != 8:
return "unknown-date"
return f"{upload_date[:4]}-{upload_date[4:6]}-{upload_date[6:]}"
def build_filename(info: dict) -> str:
"""Build '<author> -- @<channel> -- <title> -- <yyyy-mm-dd>.srt' from yt-dlp metadata.
Author is the channel's display name, channel is its @handle (falls back to the channel id).
The title is shortened so the whole name stays under MAX_FILENAME_BYTES.
"""
author = sanitize(info.get("uploader") or info.get("channel") or "unknown")
handle = (info.get("uploader_id") or info.get("channel_id") or "unknown").lstrip("@")
suffix = f" -- @{sanitize(handle)} -- "
date = f" -- {format_date(info.get('upload_date'))}.srt"
title = sanitize(info.get("title") or info["id"])
budget = MAX_FILENAME_BYTES - len(f"{author}{suffix}{date}".encode())
while len(title.encode()) > budget:
title = title[:-1]
return f"{author}{suffix}{title.strip()}{date}"
def download_audio(url: str, directory: Path) -> tuple[Path, dict]:
"""Download the best audio-only stream into `directory`. Returns (file path, yt-dlp metadata)."""
options = {"format": "bestaudio/best", "outtmpl": str(directory / "%(id)s.%(ext)s"), "noplaylist": True, "quiet": True}
with yt_dlp.YoutubeDL(options) as downloader:
info = downloader.extract_info(url, download=True)
return Path(downloader.prepare_filename(info)), info
def main() -> None:
"""Download the audio, transcribe it with video_to_srt.py and save the SRT under the metadata-based name."""
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s: %(message)s")
args, forwarded = parse_args()
url = args.video if "/" in args.video else f"https://www.youtube.com/watch?v={args.video}"
args.output_dir.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory() as temp_directory:
audio, info = download_audio(url=url, directory=Path(temp_directory))
output = args.output_dir / build_filename(info=info)
logger.info("Downloaded %s, writing %s", audio.name, output)
subprocess.run([str(VIDEO_TO_SRT), str(audio), "--output", str(output), *forwarded], check=True)
if __name__ == "__main__":
main()