youtube_abschrift_korrigieren/tests/test_yt_untertitel.py
dschlueter 3aa04fff4b feat: Arbeitsordner je Video, Quellenangabe und automatisches Video-Glossar
- Ohne -o/-d schreiben yt_untertitel.py, extract_text.py und
  abschrift_korrigieren.py -u in abschriften/<Name>/ (ein Ordner je
  Video, auch für alle Ausschnitte).
- Beim Download entsteht neben dem Rohtext <Rohtext>.quelle.json mit
  Titel, Kanal, Datum und URL, für den Quellenkopf des fertigen Textes.
- Ein Glossar, das wie sein Ordner heißt (abschriften/<Name>/<Name>.glossar.txt),
  nutzen abschrift_korrigieren.py, glossar_vorschlag.py und aenderungen.py
  automatisch, nach den mit -g angegebenen gemeinsamen Glossaren.
- .gitignore: abschriften/, fertig/, *.quelle.json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 23:24:32 +02:00

201 lines
7.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Tests for yt_untertitel.py; yt-dlp is always replaced by fakes (no network)."""
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",
"channel": "Kanal",
"upload_date": "20260328",
"subtitles": {},
"automatic_captions": {"de-orig": [], "de": [], "en": []},
}
pytestmark = pytest.mark.usefixtures("yt_dlp_installed")
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
@pytest.mark.parametrize("info, expected", [
(AUTO_ONLY, "de"),
({"language": "fr", "automatic_captions": {}}, "fr"),
({"subtitles": {"en": []}}, None),
])
def test_original_language(info, expected):
assert yt.original_language(info) == expected
# English original with AI-dubbed audio tracks: every dub has its own "-orig"
DUBBED = {
"id": "dub",
"language": "en-US",
"subtitles": {"en-US": []},
"automatic_captions": {"ar-orig": [], "en-orig": [], "de-DE-orig": [], "de": []},
}
DUBBED_NO_METADATA = {
"formats": [
{"format_note": "Arabic, low", "language": "ar"},
{"format_note": "English (US) original (default), low", "language": "en-US"},
],
"subtitles": {},
"automatic_captions": {"ar-orig": [], "en-orig": [], "de-DE-orig": []},
}
@pytest.mark.parametrize("info, lang, expected", [
(DUBBED, "en", ("en-US", False)), # region variant of a manual track
(DUBBED, "en-US", ("en-US", False)),
(DUBBED, "de", None), # only the German AI dub
(DUBBED, "ar", None),
(DUBBED_NO_METADATA, "en-US", ("en-orig", True)),
(DUBBED_NO_METADATA, "en", ("en-orig", True)),
(DUBBED_NO_METADATA, "de", None),
])
def test_choose_track_with_dubbed_audio(info, lang, expected):
assert yt.choose_track(info, lang) == expected
@pytest.mark.parametrize("info, expected", [
(DUBBED, "en-US"),
(DUBBED_NO_METADATA, "en-US"), # from the audio track marked original
({"automatic_captions": {"ar-orig": [], "en-orig": []}}, None),
])
def test_original_language_with_dubbed_audio(info, expected):
assert yt.original_language(info) == expected
@pytest.mark.parametrize("info, expected", [
({"channel": "John Mearsheimer - Offensive Realism",
"title": "Episode 1: Origins of the War In Ukraine", "upload_date": "20260730"},
"John_Mearsheimer_Offensive_Realism_Episode_1_Origins_of_the_War_In_Ukraine_2026-07-30"),
# Title cut at a word boundary after 60 characters; hyphens and dots in words stay
({"channel": "GGBLahnstein", "upload_date": "20260730",
"title": "Prof. Gabriele Krone-Schmalz – Respekt geht anders – "
"GGB-Gesundheitstage, Lahnstein 28.03.2026"},
"GGBLahnstein_Prof_Gabriele_Krone-Schmalz_Respekt_geht_anders_2026-07-30"),
({"uploader": "Ein Kanal", "title": "A/B: Krieg & Frieden 🎤?"}, "Ein_Kanal_A_B_Krieg_Frieden"),
({"title": "Nur Titel", "upload_date": "kaputt"}, "Nur_Titel"),
({"id": "abc123", "title": "🎤 – …"}, "abc123"),
({"title": "x" * 80}, "x" * 60),
])
def test_default_name(info, expected):
assert yt.default_name(info) == expected
def test_available_tracks_hides_dubs():
assert yt.available_tracks(DUBBED) == (["en-US"], ["en"])
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(fake_yt_dlp, 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_yt_dlp_runs_in_temporary_directory(fake_yt_dlp, tmp_path, monkeypatch):
# ffmpeg -bsfs, called by yt-dlp, leaves a file "3" in its working directory
run = fake_yt_dlp(AUTO_ONLY)
monkeypatch.setattr(yt.subprocess, "run", run)
monkeypatch.chdir(tmp_path)
run_main(monkeypatch, "-u", URL, "-n", "Rede")
assert all(cwd is not None and Path(cwd) != tmp_path for cwd in run.cwds)
template = run.calls[1][run.calls[1].index("-o") + 1]
# Without -d: working folder abschriften/<Name>/, named after -n
work = tmp_path / "abschriften" / "Rede"
assert template == f"{work / 'Rede'}.%(ext)s"
assert (work / "Rede.de-orig.vtt").is_file()
assert (work / "Rede.quelle.json").is_file()
def test_main_prefers_manual_and_default_name(fake_yt_dlp, 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 / "Kanal_Eine_Rede_2026-03-28.de.vtt").is_file()
assert "--write-subs" in run.calls[1]
def test_main_no_matching_track(fake_yt_dlp, 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, cwd=None):
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(fake_yt_dlp, 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