140 lines
3.8 KiB
Python
Executable File
140 lines
3.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Fetch a YouTube video's title and transcript through yt-dlp."""
|
|
|
|
import argparse
|
|
import glob
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
|
|
def get_metadata(yt_dlp, url):
|
|
cmd = [
|
|
yt_dlp,
|
|
"--dump-json",
|
|
"--no-warnings",
|
|
"--skip-download",
|
|
url,
|
|
]
|
|
try:
|
|
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
return json.loads(result.stdout)
|
|
except subprocess.CalledProcessError as error:
|
|
print(f"Error executing yt-dlp: {error.stderr}", file=sys.stderr)
|
|
sys.exit(1)
|
|
except Exception as error:
|
|
print(f"Unexpected error: {error}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
def pick_english_lang(info):
|
|
"""Return (lang_code, is_auto) for the best English caption track."""
|
|
subs = info.get("subtitles") or {}
|
|
auto = info.get("automatic_captions") or {}
|
|
|
|
preferred = ["en", "en-US", "en-GB"]
|
|
|
|
for lang in preferred:
|
|
if lang in subs:
|
|
return lang, False
|
|
for lang in subs:
|
|
if lang.startswith("en"):
|
|
return lang, False
|
|
for lang in preferred:
|
|
if lang in auto:
|
|
return lang, True
|
|
for lang in auto:
|
|
if lang.startswith("en"):
|
|
return lang, True
|
|
return None
|
|
|
|
|
|
def download_subtitle(yt_dlp, url, lang, is_auto, outdir):
|
|
cmd = [
|
|
yt_dlp,
|
|
"--skip-download",
|
|
"--no-warnings",
|
|
"--sub-format",
|
|
"json3",
|
|
"--sub-langs",
|
|
lang,
|
|
"--write-auto-subs" if is_auto else "--write-subs",
|
|
"-o",
|
|
os.path.join(outdir, "%(id)s.%(ext)s"),
|
|
url,
|
|
]
|
|
try:
|
|
subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
except subprocess.CalledProcessError as error:
|
|
print(f"Error downloading subtitles with yt-dlp: {error.stderr}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
matches = glob.glob(os.path.join(outdir, "*.json3"))
|
|
if not matches:
|
|
print("yt-dlp did not produce a json3 subtitle file.", file=sys.stderr)
|
|
sys.exit(1)
|
|
return matches[0]
|
|
|
|
|
|
def extract_text_from_json3(path):
|
|
with open(path, "r", encoding="utf-8") as file:
|
|
data = json.load(file)
|
|
|
|
text_parts = []
|
|
for event in data.get("events", []):
|
|
for segment in event.get("segs", []) or []:
|
|
text = segment.get("utf8", "")
|
|
if text.strip() and text != "\n":
|
|
text_parts.append(text.strip())
|
|
return " ".join(text_parts)
|
|
|
|
|
|
def parse_args():
|
|
parser = argparse.ArgumentParser(
|
|
description="Fetch a YouTube video's title and English transcript as JSON."
|
|
)
|
|
parser.add_argument(
|
|
"url",
|
|
metavar="youtube_url",
|
|
help="YouTube URL or video ID.",
|
|
)
|
|
parser.add_argument(
|
|
"--yt-dlp",
|
|
default="yt-dlp",
|
|
help="Path to yt-dlp. Defaults to yt-dlp from PATH.",
|
|
)
|
|
return parser.parse_args()
|
|
|
|
|
|
def main():
|
|
args = parse_args()
|
|
|
|
print("Fetching video metadata...", file=sys.stderr)
|
|
info = get_metadata(args.yt_dlp, args.url)
|
|
title = info.get("title", "Unknown_Title")
|
|
|
|
pick = pick_english_lang(info)
|
|
if pick is None:
|
|
print(f"No English subtitles found for video: {title}", file=sys.stderr)
|
|
sys.exit(1)
|
|
lang, is_auto = pick
|
|
|
|
tmpdir = tempfile.mkdtemp(prefix="yt-transcript-")
|
|
try:
|
|
caption_kind = "auto captions" if is_auto else "captions"
|
|
print(f"Downloading {caption_kind} ({lang})...", file=sys.stderr)
|
|
sub_path = download_subtitle(args.yt_dlp, args.url, lang, is_auto, tmpdir)
|
|
transcript = extract_text_from_json3(sub_path)
|
|
finally:
|
|
shutil.rmtree(tmpdir, ignore_errors=True)
|
|
|
|
output = {"title": title, "transcript": transcript}
|
|
print(json.dumps(output, indent=2))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|