feat: YouTube-Untertitel herunterladen (yt_untertitel.py)
- yt_untertitel.py lädt über yt-dlp die Untertitel eines Videos als VTT: manuelle Untertitel in der Sprache, sonst automatische in Originalsprache (<lang>-orig); Maschinenübersetzungen werden nie genommen - Tests ohne Netz (yt-dlp durch Fakes ersetzt) - Testdaten: Lahnstein-Rede (VTT und daraus erzeugter Rohtext) - README: Ablauf URL → VTT → TXT → Korrektur Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
8fe7c394f1
commit
903aa92b09
1 changed files with 298 additions and 3 deletions
138
tests/test_yt_untertitel.py
Normal file
138
tests/test_yt_untertitel.py
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
"""Tests for yt_untertitel.py; yt-dlp is always replaced by fakes (no network)."""
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
import yt_untertitel as yt
|
||||
|
||||
URL = "https://www.youtube.com/watch?v=abc123"
|
||||
|
||||
AUTO_ONLY = {
|
||||
"id": "abc123",
|
||||
"title": "Eine Rede",
|
||||
"subtitles": {},
|
||||
"automatic_captions": {"de-orig": [], "de": [], "en": []},
|
||||
}
|
||||
|
||||
|
||||
def fake_yt_dlp(info, write=True, fail_download=False):
|
||||
"""Stand-in for subprocess.run: answers the info request, writes the VTT file."""
|
||||
calls = []
|
||||
|
||||
def run(command, capture_output, text):
|
||||
calls.append(command)
|
||||
if "--dump-single-json" in command:
|
||||
return subprocess.CompletedProcess(command, 0, json.dumps(info), "")
|
||||
if fail_download:
|
||||
return subprocess.CompletedProcess(command, 1, "", "HTTP Error 429")
|
||||
track = command[command.index("--sub-langs") + 1]
|
||||
template = command[command.index("-o") + 1]
|
||||
if write:
|
||||
Path(template.replace("%(ext)s", f"{track}.vtt")).write_text(
|
||||
"WEBVTT\n", encoding="utf-8"
|
||||
)
|
||||
return subprocess.CompletedProcess(command, 0, "", "")
|
||||
|
||||
run.calls = calls
|
||||
return run
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def yt_dlp_installed(monkeypatch):
|
||||
monkeypatch.setattr(yt.shutil, "which", lambda name: f"/usr/bin/{name}")
|
||||
|
||||
|
||||
def run_main(monkeypatch, *args):
|
||||
monkeypatch.setattr(sys, "argv", ["yt_untertitel.py", *map(str, args)])
|
||||
yt.main()
|
||||
|
||||
|
||||
# === Track selection ===
|
||||
|
||||
@pytest.mark.parametrize("info, lang, expected", [
|
||||
({"subtitles": {"de": []}, "automatic_captions": {"de-orig": []}}, "de", ("de", False)),
|
||||
(AUTO_ONLY, "de", ("de-orig", True)),
|
||||
# "en" exists only as machine translation of the German original
|
||||
(AUTO_ONLY, "en", None),
|
||||
({"subtitles": None, "automatic_captions": None}, "de", None),
|
||||
])
|
||||
def test_choose_track(info, lang, expected):
|
||||
assert yt.choose_track(info, lang) == expected
|
||||
|
||||
|
||||
def test_available_tracks():
|
||||
info = {"subtitles": {"fr": []}, "automatic_captions": {"de-orig": [], "de": []}}
|
||||
assert yt.available_tracks(info) == (["fr"], ["de"])
|
||||
|
||||
|
||||
# === Command line ===
|
||||
|
||||
def test_main_downloads_auto_original(tmp_path, monkeypatch, capsys):
|
||||
run = fake_yt_dlp(AUTO_ONLY)
|
||||
monkeypatch.setattr(yt.subprocess, "run", run)
|
||||
run_main(monkeypatch, "-u", URL, "-n", "Rede", "-d", tmp_path / "neu")
|
||||
|
||||
assert (tmp_path / "neu" / "Rede.de-orig.vtt").is_file()
|
||||
download = run.calls[1]
|
||||
assert "--write-auto-subs" in download and "--write-subs" not in download
|
||||
assert download[-1] == URL
|
||||
out = capsys.readouterr().out
|
||||
assert "automatische Untertitel „de-orig“" in out
|
||||
assert "vtt2txt.py -i" in out
|
||||
|
||||
|
||||
def test_main_prefers_manual_and_defaults_to_video_id(tmp_path, monkeypatch):
|
||||
info = dict(AUTO_ONLY, subtitles={"de": []})
|
||||
run = fake_yt_dlp(info)
|
||||
monkeypatch.setattr(yt.subprocess, "run", run)
|
||||
run_main(monkeypatch, "-u", URL, "-d", tmp_path)
|
||||
|
||||
assert (tmp_path / "abc123.de.vtt").is_file()
|
||||
assert "--write-subs" in run.calls[1]
|
||||
|
||||
|
||||
def test_main_no_matching_track(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(yt.subprocess, "run", fake_yt_dlp(AUTO_ONLY))
|
||||
with pytest.raises(SystemExit, match="Automatisch \\(Originalsprache\\): de"):
|
||||
run_main(monkeypatch, "-u", URL, "-l", "en", "-d", tmp_path)
|
||||
|
||||
|
||||
def test_main_info_error(tmp_path, monkeypatch):
|
||||
def run(command, capture_output, text):
|
||||
return subprocess.CompletedProcess(command, 1, "", "ERROR: Video unavailable")
|
||||
|
||||
monkeypatch.setattr(yt.subprocess, "run", run)
|
||||
with pytest.raises(SystemExit, match="Video unavailable"):
|
||||
run_main(monkeypatch, "-u", URL, "-d", tmp_path)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("kwargs, message", [
|
||||
({"fail_download": True}, "HTTP Error 429"),
|
||||
({"write": False}, "keine Datei"),
|
||||
])
|
||||
def test_main_download_errors(tmp_path, monkeypatch, kwargs, message):
|
||||
monkeypatch.setattr(yt.subprocess, "run", fake_yt_dlp(AUTO_ONLY, **kwargs))
|
||||
with pytest.raises(SystemExit, match=message):
|
||||
run_main(monkeypatch, "-u", URL, "-d", tmp_path)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("args, message", [
|
||||
([], "required"),
|
||||
(["-u", URL, "-n", "sub/Rede"], "ohne Verzeichnis"),
|
||||
(["-u", URL, "-n", ""], "ohne Verzeichnis"),
|
||||
])
|
||||
def test_cli_errors(monkeypatch, capsys, args, message):
|
||||
with pytest.raises(SystemExit) as exc:
|
||||
run_main(monkeypatch, *args)
|
||||
assert exc.value.code == 2
|
||||
assert message in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_cli_without_yt_dlp(monkeypatch, capsys):
|
||||
monkeypatch.setattr(yt.shutil, "which", lambda name: None)
|
||||
with pytest.raises(SystemExit):
|
||||
run_main(monkeypatch, "-u", URL)
|
||||
assert "pip install yt-dlp" in capsys.readouterr().err
|
||||
Loading…
Reference in a new issue