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