youtube_abschrift_korrigieren/tests/test_yt_untertitel.py
dschlueter 3d9a5037c2 feat: Standard-Dateiname aus Kanal, Titel und Datum des Videos
- yt_untertitel.default_name(): <Kanal>_<Titel>_<Datum>, z. B.
  John_Mearsheimer_Offensive_Realism_Episode_1_Origins_of_the_War_In_Ukraine_2026-07-30
- Bindestriche/Punkte im Wort bleiben, Kanal max. 40, Titel max. 60 Zeichen
  an Wortgrenze; ohne Kanal und Titel die Video-ID
- Gilt ohne -o/-n in extract_text.py (statt output.txt), yt_untertitel.py
  und rede_korrigieren.py -u (statt Video-ID)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 21:03:14 +02:00

198 lines
7.1 KiB
Python
Raw 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]
assert template == f"{tmp_path / 'Rede'}.%(ext)s"
assert (tmp_path / "Rede.de-orig.vtt").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