2026-10-10 18:19:14 +02:00
|
|
|
|
"""Tests for yt_untertitel.py; yt-dlp is always replaced by fakes (no network)."""
|
|
|
|
|
|
import subprocess
|
|
|
|
|
|
import sys
|
2026-10-10 20:12:08 +02:00
|
|
|
|
from pathlib import Path
|
2026-10-10 18:19:14 +02:00
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
|
|
import yt_untertitel as yt
|
|
|
|
|
|
|
|
|
|
|
|
URL = "https://www.youtube.com/watch?v=abc123"
|
|
|
|
|
|
|
|
|
|
|
|
AUTO_ONLY = {
|
|
|
|
|
|
"id": "abc123",
|
|
|
|
|
|
"title": "Eine Rede",
|
2026-10-10 21:03:14 +02:00
|
|
|
|
"channel": "Kanal",
|
|
|
|
|
|
"upload_date": "20260328",
|
2026-10-10 18:19:14 +02:00
|
|
|
|
"subtitles": {},
|
|
|
|
|
|
"automatic_captions": {"de-orig": [], "de": [], "en": []},
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-10-10 19:52:20 +02:00
|
|
|
|
pytestmark = pytest.mark.usefixtures("yt_dlp_installed")
|
2026-10-10 18:19:14 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-10-10 19:51:32 +02:00
|
|
|
|
@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
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-10-10 20:28:05 +02:00
|
|
|
|
# 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
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-10-10 21:03:14 +02:00
|
|
|
|
@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
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-10-10 20:28:05 +02:00
|
|
|
|
def test_available_tracks_hides_dubs():
|
|
|
|
|
|
assert yt.available_tracks(DUBBED) == (["en-US"], ["en"])
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-10-10 18:19:14 +02:00
|
|
|
|
def test_available_tracks():
|
|
|
|
|
|
info = {"subtitles": {"fr": []}, "automatic_captions": {"de-orig": [], "de": []}}
|
|
|
|
|
|
assert yt.available_tracks(info) == (["fr"], ["de"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# === Command line ===
|
|
|
|
|
|
|
2026-10-10 19:52:20 +02:00
|
|
|
|
def test_main_downloads_auto_original(fake_yt_dlp, tmp_path, monkeypatch, capsys):
|
2026-10-10 18:19:14 +02:00
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-10-10 20:12:08 +02:00
|
|
|
|
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]
|
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
|
|
|
|
# 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()
|
2026-10-10 20:12:08 +02:00
|
|
|
|
|
|
|
|
|
|
|
2026-10-10 21:03:14 +02:00
|
|
|
|
def test_main_prefers_manual_and_default_name(fake_yt_dlp, tmp_path, monkeypatch):
|
2026-10-10 18:19:14 +02:00
|
|
|
|
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)
|
|
|
|
|
|
|
2026-10-10 21:03:14 +02:00
|
|
|
|
assert (tmp_path / "Kanal_Eine_Rede_2026-03-28.de.vtt").is_file()
|
2026-10-10 18:19:14 +02:00
|
|
|
|
assert "--write-subs" in run.calls[1]
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-10-10 19:52:20 +02:00
|
|
|
|
def test_main_no_matching_track(fake_yt_dlp, tmp_path, monkeypatch):
|
2026-10-10 18:19:14 +02:00
|
|
|
|
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):
|
2026-10-10 20:12:08 +02:00
|
|
|
|
def run(command, capture_output, text, cwd=None):
|
2026-10-10 18:19:14 +02:00
|
|
|
|
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"),
|
|
|
|
|
|
])
|
2026-10-10 19:52:20 +02:00
|
|
|
|
def test_main_download_errors(fake_yt_dlp, tmp_path, monkeypatch, kwargs, message):
|
2026-10-10 18:19:14 +02:00
|
|
|
|
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
|