#!/usr/bin/env -S uv run --script # /// script # dependencies = [ # "youtube-transcript-api", # ] # /// import argparse import logging import sys from pathlib import Path from urllib.parse import urlparse, parse_qs from youtube_transcript_api import YouTubeTranscriptApi logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s: %(message)s") def extract_video_id(url: str) -> str: parsed = urlparse(url) if parsed.hostname in ("youtu.be", "www.youtu.be"): return parsed.path.lstrip("/") if parsed.hostname in ("youtube.com", "www.youtube.com", "m.youtube.com"): if parsed.path == "/watch": return parse_qs(parsed.query)["v"][0] elif parsed.path.startswith("/embed/"): return parsed.path.split("/")[2] elif parsed.path.startswith("/v/"): return parsed.path.split("/")[2] raise ValueError(f"Could not extract video ID from URL: {url}") def format_timestamp(seconds: float) -> str: hours = int(seconds // 3600) minutes = int((seconds % 3600) // 60) secs = int(seconds % 60) millis = int((seconds % 1) * 1000) return f"{hours:02d}:{minutes:02d}:{secs:02d},{millis:03d}" def transcript_to_srt(transcript: list[dict]) -> str: srt_lines = [] for i, entry in enumerate(transcript, start=1): start_time = format_timestamp(entry["start"]) end_time = format_timestamp(entry["start"] + entry["duration"]) text = entry["text"] srt_lines.append(f"{i}") srt_lines.append(f"{start_time} --> {end_time}") srt_lines.append(text) srt_lines.append("") return "\n".join(srt_lines) PRIORITY_LANGUAGES = ("de", "en", "tr") def pick_auto_language(transcript_list) -> str: available = {t.language_code: t for t in transcript_list} if not available: raise ValueError("No transcripts available for this video") for lang in PRIORITY_LANGUAGES: if lang in available: return lang return next(iter(available)) def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Fetch YouTube video transcripts", formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) parser.add_argument("url", help="YouTube video URL") parser.add_argument( "--save-srt", type=Path, metavar="FILE_PATH", help="Save transcript as SRT file to the specified path", ) lang_group = parser.add_mutually_exclusive_group() lang_group.add_argument( "--language", metavar="LANG", help="Fetch transcript in the given language code (exits with error if unavailable)", ) lang_group.add_argument( "--auto", action="store_true", help=f"Auto-pick a language, preferring {', '.join(PRIORITY_LANGUAGES)} in that order, " "then falling back to the first available generated/translated language", ) return parser.parse_args() def main() -> None: args = parse_args() try: video_id = extract_video_id(args.url) logging.info(f"Fetching transcript for video ID: {video_id}") ytt_api = YouTubeTranscriptApi() if args.language: transcript_list = ytt_api.list(video_id) try: chosen = transcript_list.find_transcript([args.language]) except Exception: available = ", ".join(sorted(t.language_code for t in transcript_list)) logging.error( f"Language '{args.language}' not available. Available languages: {available}" ) sys.exit(1) logging.info(f"Using language: {chosen.language_code}") transcript = chosen.fetch() elif args.auto: transcript_list = ytt_api.list(video_id) lang = pick_auto_language(transcript_list) logging.info(f"Auto-selected language: {lang}") transcript = transcript_list.find_transcript([lang]).fetch() else: try: transcript = ytt_api.fetch(video_id) except Exception: transcript_list = ytt_api.list(video_id) available = ", ".join(sorted(t.language_code for t in transcript_list)) logging.error( f"No transcript available in the default language. " f"Available languages: {available}. Use --language or --auto." ) sys.exit(1) if args.save_srt: srt_content = transcript_to_srt(transcript.to_raw_data()) args.save_srt.write_text(srt_content) logging.info(f"SRT file saved to: {args.save_srt}") else: text = " ".join(snippet.text for snippet in transcript) print(text) except Exception as e: logging.error(f"Failed to fetch transcript: {e}") sys.exit(1) if __name__ == "__main__": main()