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>
This commit is contained in:
parent
fb9201b721
commit
3d9a5037c2
7 changed files with 105 additions and 29 deletions
31
README.md
31
README.md
|
|
@ -62,19 +62,36 @@ Zwischenschritt im temporären Verzeichnis. Die Spurwahl ist dieselbe wie bei
|
||||||
`yt_untertitel.py`, nur ist die Standardsprache die des Original-Audios.
|
`yt_untertitel.py`, nur ist die Standardsprache die des Original-Audios.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./extract_text.py -u 'https://www.youtube.com/watch?v=rm6lADgf6GQ' # → output.txt
|
./extract_text.py -u 'https://www.youtube.com/watch?v=0LmCveTM788'
|
||||||
|
# → John_Mearsheimer_Offensive_Realism_Episode_1_Origins_of_the_War_In_Ukraine_2026-07-30.txt
|
||||||
./extract_text.py -u URL -l de -o testdaten/rede.txt # Sprache und Ziel
|
./extract_text.py -u URL -l de -o testdaten/rede.txt # Sprache und Ziel
|
||||||
./extract_text.py -u URL -f -o rede.txt # lesbarer Fließtext
|
./extract_text.py -u URL -f -o rede.txt # lesbarer Fließtext
|
||||||
./rede_korrigieren.py -i output.txt # → output.korrigiert.txt
|
./rede_korrigieren.py -i rede.txt # → rede.korrigiert.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
| Option | Bedeutung |
|
| Option | Bedeutung |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `-u`, `--url` | YouTube-URL, Pflicht |
|
| `-u`, `--url` | YouTube-URL, Pflicht |
|
||||||
| `-l`, `--lang` | Sprache; Standard: Sprache des Original-Audios (`<sprache>-orig`) |
|
| `-l`, `--lang` | Sprache; Standard: Sprache des Original-Audios (`<sprache>-orig`) |
|
||||||
| `-o`, `--output` | Ausgabedatei (Verzeichnisse werden angelegt, vorhandene Datei überschrieben); Standard: `output.txt` |
|
| `-o`, `--output` | Ausgabedatei (Verzeichnisse werden angelegt, vorhandene Datei überschrieben); Standard: `<Kanal>_<Titel>_<Datum>.txt`, siehe unten |
|
||||||
| `-f`, `--fliesstext` | Fließtext, auf 80 Zeichen umbrochen (Wörter werden nie getrennt), statt der kurzen Untertitelzeilen; beides versteht `rede_korrigieren.py` |
|
| `-f`, `--fliesstext` | Fließtext, auf 80 Zeichen umbrochen (Wörter werden nie getrennt), statt der kurzen Untertitelzeilen; beides versteht `rede_korrigieren.py` |
|
||||||
|
|
||||||
|
### Standard-Dateinamen für YouTube-Videos
|
||||||
|
|
||||||
|
Ohne `-o` bzw. `-n` bilden `extract_text.py`, `yt_untertitel.py` und
|
||||||
|
`rede_korrigieren.py -u` den Namen aus Kanal, Titel und Hochladedatum:
|
||||||
|
|
||||||
|
| Video | Name |
|
||||||
|
|---|---|
|
||||||
|
| John Mearsheimer, „Episode 1: Origins of the War In Ukraine“ | `John_Mearsheimer_Offensive_Realism_Episode_1_Origins_of_the_War_In_Ukraine_2026-07-30` |
|
||||||
|
| GGBLahnstein, „Prof. Gabriele Krone-Schmalz – Respekt geht anders – …“ | `GGBLahnstein_Prof_Gabriele_Krone-Schmalz_Respekt_geht_anders_2026-07-30` |
|
||||||
|
|
||||||
|
Wörter werden mit `_` verbunden. Bindestriche und Punkte im Wort bleiben
|
||||||
|
(„Krone-Schmalz“), andere Zeichen fallen weg. Der Kanal wird auf 40, der Titel auf
|
||||||
|
60 Zeichen an einer Wortgrenze gekürzt. Ohne verwertbaren Kanal und Titel gilt die
|
||||||
|
Video-ID. Gleiches Video ergibt immer denselben Namen; eine vorhandene Datei wird
|
||||||
|
überschrieben (ein mit `-u` gespeicherter, abweichender Rohtext aber nie).
|
||||||
|
|
||||||
### Untertitel herunterladen: `yt_untertitel.py`
|
### Untertitel herunterladen: `yt_untertitel.py`
|
||||||
|
|
||||||
Lädt die Untertitel eines YouTube-Videos über `yt-dlp` als VTT-Datei, ohne das
|
Lädt die Untertitel eines YouTube-Videos über `yt-dlp` als VTT-Datei, ohne das
|
||||||
|
|
@ -94,14 +111,14 @@ gehören zur Synchronisation und werden nicht genommen. Sprachen werden auch mit
|
||||||
Region gefunden: `-l en` findet manuelle Untertitel `en-US`.
|
Region gefunden: `-l en` findet manuelle Untertitel `en-US`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./yt_untertitel.py -u URL # → <Video-ID>.de-orig.vtt
|
./yt_untertitel.py -u URL # → <Kanal>_<Titel>_<Datum>.de-orig.vtt
|
||||||
./yt_untertitel.py -u URL -n Lahnstein -d testdaten # → testdaten/Lahnstein.de-orig.vtt
|
./yt_untertitel.py -u URL -n Lahnstein -d testdaten # → testdaten/Lahnstein.de-orig.vtt
|
||||||
```
|
```
|
||||||
|
|
||||||
| Option | Bedeutung |
|
| Option | Bedeutung |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `-u`, `--url` | YouTube-URL, Pflicht |
|
| `-u`, `--url` | YouTube-URL, Pflicht |
|
||||||
| `-n`, `--name` | Dateiname ohne Endung; Standard: Video-ID |
|
| `-n`, `--name` | Dateiname ohne Endung; Standard: `<Kanal>_<Titel>_<Datum>` |
|
||||||
| `-l`, `--lang` | Sprache; Standard: `de` |
|
| `-l`, `--lang` | Sprache; Standard: `de` |
|
||||||
| `-d`, `--dir` | Zielverzeichnis (wird angelegt); Standard: aktuelles Verzeichnis |
|
| `-d`, `--dir` | Zielverzeichnis (wird angelegt); Standard: aktuelles Verzeichnis |
|
||||||
|
|
||||||
|
|
@ -139,7 +156,7 @@ werden trotzdem umgewandelt; der Exit-Code ist dann 1.
|
||||||
./rede_korrigieren.py -i rede.txt -g rede.glossar.txt # mit Glossar
|
./rede_korrigieren.py -i rede.txt -g rede.glossar.txt # mit Glossar
|
||||||
./rede_korrigieren.py -i rede.txt -g allgemein.glossar.txt -g rede.glossar.txt # kombiniert
|
./rede_korrigieren.py -i rede.txt -g allgemein.glossar.txt -g rede.glossar.txt # kombiniert
|
||||||
./rede_korrigieren.py -i rede.de.vtt # VTT direkt → rede.de.korrigiert.txt
|
./rede_korrigieren.py -i rede.de.vtt # VTT direkt → rede.de.korrigiert.txt
|
||||||
./rede_korrigieren.py -u URL # YouTube → <Video-ID>.korrigiert.txt
|
./rede_korrigieren.py -u URL # YouTube → <Kanal>_<Titel>_<Datum>.korrigiert.txt
|
||||||
./rede_korrigieren.py -i speech.txt -l en # englische Rede
|
./rede_korrigieren.py -i speech.txt -l en # englische Rede
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -148,7 +165,7 @@ werden trotzdem umgewandelt; der Exit-Code ist dann 1.
|
||||||
| `-i`, `--input` | Eingabedatei: Rohabschrift (`.txt`) oder Untertitel (`.vtt`, wird wie mit `vtt2txt.py` umgewandelt) |
|
| `-i`, `--input` | Eingabedatei: Rohabschrift (`.txt`) oder Untertitel (`.vtt`, wird wie mit `vtt2txt.py` umgewandelt) |
|
||||||
| `-u`, `--url` | statt `-i`: YouTube-URL; Untertitel wie bei `extract_text.py`. Der Rohtext wird neben der Ausgabe gespeichert (`rede.korrigiert.txt` → `rede.txt`, sonst `<Name>.roh.txt`), eine vorhandene, abweichende Datei aber nie überschrieben |
|
| `-u`, `--url` | statt `-i`: YouTube-URL; Untertitel wie bei `extract_text.py`. Der Rohtext wird neben der Ausgabe gespeichert (`rede.korrigiert.txt` → `rede.txt`, sonst `<Name>.roh.txt`), eine vorhandene, abweichende Datei aber nie überschrieben |
|
||||||
| `-l`, `--lang` | Sprache der Rede: `de` oder `en` (auch mit Region, z. B. `en-US`); wählt Prompts und Prüfmarke. Mit `-u` zugleich die Sprache der Untertitel. Standard: `de`, mit `-u` die Sprache des Original-Audios |
|
| `-l`, `--lang` | Sprache der Rede: `de` oder `en` (auch mit Region, z. B. `en-US`); wählt Prompts und Prüfmarke. Mit `-u` zugleich die Sprache der Untertitel. Standard: `de`, mit `-u` die Sprache des Original-Audios |
|
||||||
| `-o`, `--output` | Ausgabedatei; Standard: `<Eingabe ohne Endung>.korrigiert.txt` neben der Eingabe, mit `-u` `<Video-ID>.korrigiert.txt` |
|
| `-o`, `--output` | Ausgabedatei; Standard: `<Eingabe ohne Endung>.korrigiert.txt` neben der Eingabe, mit `-u` `<Kanal>_<Titel>_<Datum>.korrigiert.txt` |
|
||||||
| `-g`, `--glossar` | Glossar mit festen Ersetzungen und Schreibweisen; mehrfach angeben zum Kombinieren (Ersetzungen in Reihenfolge der Angabe) |
|
| `-g`, `--glossar` | Glossar mit festen Ersetzungen und Schreibweisen; mehrfach angeben zum Kombinieren (Ersetzungen in Reihenfolge der Angabe) |
|
||||||
| `-m`, `--model` | Ollama-Modell; Standard: `qwen3:32b` (Konstante `MODEL`) |
|
| `-m`, `--model` | Ollama-Modell; Standard: `qwen3:32b` (Konstante `MODEL`) |
|
||||||
| `--kein-bericht` | keinen Änderungsbericht schreiben |
|
| `--kein-bericht` | keinen Änderungsbericht schreiben |
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,8 @@ import textwrap
|
||||||
|
|
||||||
from vtt2txt import vtt_to_lines
|
from vtt2txt import vtt_to_lines
|
||||||
from yt_untertitel import (
|
from yt_untertitel import (
|
||||||
YT_DLP, choose_track, download, fetch_info, no_track_message, original_language,
|
YT_DLP, choose_track, default_name, download, fetch_info, no_track_message,
|
||||||
|
original_language,
|
||||||
)
|
)
|
||||||
|
|
||||||
FLOW_WIDTH = 80 # Zeilenbreite für --fliesstext
|
FLOW_WIDTH = 80 # Zeilenbreite für --fliesstext
|
||||||
|
|
@ -25,8 +26,8 @@ def parse_args():
|
||||||
help="Sprache der Untertitel, z. B. de (Standard: Sprache des Original-Audios)",
|
help="Sprache der Untertitel, z. B. de (Standard: Sprache des Original-Audios)",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-o", "--output", type=Path, default=Path("output.txt"),
|
"-o", "--output", type=Path,
|
||||||
help="Ausgabedatei (Standard: output.txt)",
|
help="Ausgabedatei (Standard: <Kanal>_<Titel>_<Datum>.txt)",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-f", "--fliesstext", action="store_true",
|
"-f", "--fliesstext", action="store_true",
|
||||||
|
|
@ -34,7 +35,7 @@ def parse_args():
|
||||||
)
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if args.output.is_dir():
|
if args.output is not None and args.output.is_dir():
|
||||||
parser.error(f"-o/--output ist ein Verzeichnis: {args.output}")
|
parser.error(f"-o/--output ist ein Verzeichnis: {args.output}")
|
||||||
if shutil.which(YT_DLP) is None:
|
if shutil.which(YT_DLP) is None:
|
||||||
parser.error(f"{YT_DLP} nicht gefunden (Installation: pip install yt-dlp).")
|
parser.error(f"{YT_DLP} nicht gefunden (Installation: pip install yt-dlp).")
|
||||||
|
|
@ -73,7 +74,8 @@ def fetch_text(url, lang=None):
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
args = parse_args()
|
args = parse_args()
|
||||||
_, lines = fetch_text(args.url, args.lang)
|
info, lines = fetch_text(args.url, args.lang)
|
||||||
|
output = args.output or Path(f"{default_name(info)}.txt")
|
||||||
|
|
||||||
if args.fliesstext:
|
if args.fliesstext:
|
||||||
# Never split words, not even at hyphens ("EU-Gipfel")
|
# Never split words, not even at hyphens ("EU-Gipfel")
|
||||||
|
|
@ -84,10 +86,10 @@ def main():
|
||||||
else:
|
else:
|
||||||
text = "\n".join(lines)
|
text = "\n".join(lines)
|
||||||
|
|
||||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
args.output.write_text(text + "\n", encoding="utf-8")
|
output.write_text(text + "\n", encoding="utf-8")
|
||||||
print(
|
print(
|
||||||
f"Gespeichert: {args.output} "
|
f"Gespeichert: {output} "
|
||||||
f"({len(text.splitlines())} Zeilen, {len(text.split())} Wörter)"
|
f"({len(text.splitlines())} Zeilen, {len(text.split())} Wörter)"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import urllib.request
|
||||||
from aenderungen import REVIEW_MARK, print_summary, report_path, write_report
|
from aenderungen import REVIEW_MARK, print_summary, report_path, write_report
|
||||||
from extract_text import fetch_text
|
from extract_text import fetch_text
|
||||||
from vtt2txt import vtt_to_lines
|
from vtt2txt import vtt_to_lines
|
||||||
from yt_untertitel import YT_DLP, base_language, original_language
|
from yt_untertitel import YT_DLP, base_language, default_name, original_language
|
||||||
|
|
||||||
MODEL = "qwen3:32b"
|
MODEL = "qwen3:32b"
|
||||||
OLLAMA_URL = "http://127.0.0.1:11434/api/chat"
|
OLLAMA_URL = "http://127.0.0.1:11434/api/chat"
|
||||||
|
|
@ -452,7 +452,7 @@ def parse_args():
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-o", "--output", type=Path,
|
"-o", "--output", type=Path,
|
||||||
help="Ausgabedatei (Standard: <Eingabe ohne Endung>.korrigiert.txt, "
|
help="Ausgabedatei (Standard: <Eingabe ohne Endung>.korrigiert.txt, "
|
||||||
"mit -u <Video-ID>.korrigiert.txt)",
|
"mit -u <Kanal>_<Titel>_<Datum>.korrigiert.txt)",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-g", "--glossar", type=Path, action="append", default=[],
|
"-g", "--glossar", type=Path, action="append", default=[],
|
||||||
|
|
@ -516,7 +516,7 @@ def read_source(args):
|
||||||
raise SystemExit(
|
raise SystemExit(
|
||||||
f"Die Rede ist auf „{lang}“; unterstützt: {', '.join(PROMPTS)} (-l)."
|
f"Die Rede ist auf „{lang}“; unterstützt: {', '.join(PROMPTS)} (-l)."
|
||||||
)
|
)
|
||||||
target = args.output or Path(f"{info['id']}.korrigiert.txt")
|
target = args.output or Path(f"{default_name(info)}.korrigiert.txt")
|
||||||
raw = "\n".join(lines) + "\n"
|
raw = "\n".join(lines) + "\n"
|
||||||
save_raw_text(raw_text_path(target), raw)
|
save_raw_text(raw_text_path(target), raw)
|
||||||
return raw, target, lang
|
return raw, target, lang
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ VTT = (
|
||||||
GERMAN_VIDEO = {
|
GERMAN_VIDEO = {
|
||||||
"id": "abc123",
|
"id": "abc123",
|
||||||
"title": "Eine Rede",
|
"title": "Eine Rede",
|
||||||
|
"channel": "Kanal",
|
||||||
|
"upload_date": "20260328",
|
||||||
"subtitles": {"en": []},
|
"subtitles": {"en": []},
|
||||||
"automatic_captions": {"de-orig": [], "de": [], "en": []},
|
"automatic_captions": {"de-orig": [], "de": [], "en": []},
|
||||||
}
|
}
|
||||||
|
|
@ -36,14 +38,14 @@ def test_default_language_is_original_audio(fake_yt_dlp, tmp_path, monkeypatch,
|
||||||
monkeypatch.chdir(tmp_path)
|
monkeypatch.chdir(tmp_path)
|
||||||
run_main(monkeypatch, "-u", URL)
|
run_main(monkeypatch, "-u", URL)
|
||||||
|
|
||||||
text = (tmp_path / "output.txt").read_text(encoding="utf-8")
|
text = (tmp_path / "Kanal_Eine_Rede_2026-03-28.txt").read_text(encoding="utf-8")
|
||||||
assert text == "Guten Abend.\nSchön, dass Sie da sind.\n"
|
assert text == "Guten Abend.\nSchön, dass Sie da sind.\n"
|
||||||
assert "de-orig" in run.calls[1]
|
assert "de-orig" in run.calls[1]
|
||||||
assert "--write-auto-subs" in run.calls[1]
|
assert "--write-auto-subs" in run.calls[1]
|
||||||
out = capsys.readouterr().out
|
out = capsys.readouterr().out
|
||||||
assert "output.txt (2 Zeilen, 7 Wörter)" in out
|
assert "Kanal_Eine_Rede_2026-03-28.txt (2 Zeilen, 7 Wörter)" in out
|
||||||
# Only the text file remains, no VTT
|
# Only the text file remains, no VTT
|
||||||
assert [p.name for p in tmp_path.iterdir()] == ["output.txt"]
|
assert [p.name for p in tmp_path.iterdir()] == ["Kanal_Eine_Rede_2026-03-28.txt"]
|
||||||
|
|
||||||
|
|
||||||
def test_default_language_ignores_dubbed_tracks(fake_yt_dlp, tmp_path, monkeypatch):
|
def test_default_language_ignores_dubbed_tracks(fake_yt_dlp, tmp_path, monkeypatch):
|
||||||
|
|
|
||||||
|
|
@ -378,7 +378,10 @@ def test_correct_chunk_sends_model(monkeypatch):
|
||||||
|
|
||||||
|
|
||||||
URL = "https://www.youtube.com/watch?v=abc123"
|
URL = "https://www.youtube.com/watch?v=abc123"
|
||||||
VIDEO = {"id": "abc123", "title": "Rede", "subtitles": {}, "automatic_captions": {"de-orig": []}}
|
VIDEO = {
|
||||||
|
"id": "abc123", "title": "Rede", "channel": "Kanal", "upload_date": "20260328",
|
||||||
|
"subtitles": {}, "automatic_captions": {"de-orig": []},
|
||||||
|
}
|
||||||
VTT = (
|
VTT = (
|
||||||
"WEBVTT\n\n00:00:01.000 --> 00:00:02.000\nDer Kremmel ist in Moskau.\n\n"
|
"WEBVTT\n\n00:00:01.000 --> 00:00:02.000\nDer Kremmel ist in Moskau.\n\n"
|
||||||
"00:00:02.000 --> 00:00:03.000\nDer Kremmel ist in Moskau.\nDas ist bekannt.\n"
|
"00:00:02.000 --> 00:00:03.000\nDer Kremmel ist in Moskau.\nDas ist bekannt.\n"
|
||||||
|
|
@ -397,11 +400,11 @@ def test_main_url_default_names(from_url, tmp_path, monkeypatch):
|
||||||
monkeypatch.chdir(tmp_path)
|
monkeypatch.chdir(tmp_path)
|
||||||
run_main(monkeypatch, "-u", URL, "-g", SHARED_GLOSSARY)
|
run_main(monkeypatch, "-u", URL, "-g", SHARED_GLOSSARY)
|
||||||
|
|
||||||
raw = (tmp_path / "abc123.txt").read_text(encoding="utf-8")
|
raw = (tmp_path / "Kanal_Rede_2026-03-28.txt").read_text(encoding="utf-8")
|
||||||
assert raw == "Der Kremmel ist in Moskau.\nDas ist bekannt.\n"
|
assert raw == "Der Kremmel ist in Moskau.\nDas ist bekannt.\n"
|
||||||
result = (tmp_path / "abc123.korrigiert.txt").read_text(encoding="utf-8")
|
result = (tmp_path / "Kanal_Rede_2026-03-28.korrigiert.txt").read_text(encoding="utf-8")
|
||||||
assert result == "Der Kreml ist in Moskau. Das ist bekannt.\n"
|
assert result == "Der Kreml ist in Moskau. Das ist bekannt.\n"
|
||||||
assert (tmp_path / "abc123.aenderungen.html").is_file()
|
assert (tmp_path / "Kanal_Rede_2026-03-28.aenderungen.html").is_file()
|
||||||
|
|
||||||
|
|
||||||
def test_main_url_output_option_and_kept_raw_text(from_url, tmp_path, monkeypatch, capsys):
|
def test_main_url_output_option_and_kept_raw_text(from_url, tmp_path, monkeypatch, capsys):
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ URL = "https://www.youtube.com/watch?v=abc123"
|
||||||
AUTO_ONLY = {
|
AUTO_ONLY = {
|
||||||
"id": "abc123",
|
"id": "abc123",
|
||||||
"title": "Eine Rede",
|
"title": "Eine Rede",
|
||||||
|
"channel": "Kanal",
|
||||||
|
"upload_date": "20260328",
|
||||||
"subtitles": {},
|
"subtitles": {},
|
||||||
"automatic_captions": {"de-orig": [], "de": [], "en": []},
|
"automatic_captions": {"de-orig": [], "de": [], "en": []},
|
||||||
}
|
}
|
||||||
|
|
@ -86,6 +88,24 @@ def test_original_language_with_dubbed_audio(info, expected):
|
||||||
assert yt.original_language(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():
|
def test_available_tracks_hides_dubs():
|
||||||
assert yt.available_tracks(DUBBED) == (["en-US"], ["en"])
|
assert yt.available_tracks(DUBBED) == (["en-US"], ["en"])
|
||||||
|
|
||||||
|
|
@ -124,13 +144,13 @@ def test_yt_dlp_runs_in_temporary_directory(fake_yt_dlp, tmp_path, monkeypatch):
|
||||||
assert (tmp_path / "Rede.de-orig.vtt").is_file()
|
assert (tmp_path / "Rede.de-orig.vtt").is_file()
|
||||||
|
|
||||||
|
|
||||||
def test_main_prefers_manual_and_defaults_to_video_id(fake_yt_dlp, tmp_path, monkeypatch):
|
def test_main_prefers_manual_and_default_name(fake_yt_dlp, tmp_path, monkeypatch):
|
||||||
info = dict(AUTO_ONLY, subtitles={"de": []})
|
info = dict(AUTO_ONLY, subtitles={"de": []})
|
||||||
run = fake_yt_dlp(info)
|
run = fake_yt_dlp(info)
|
||||||
monkeypatch.setattr(yt.subprocess, "run", run)
|
monkeypatch.setattr(yt.subprocess, "run", run)
|
||||||
run_main(monkeypatch, "-u", URL, "-d", tmp_path)
|
run_main(monkeypatch, "-u", URL, "-d", tmp_path)
|
||||||
|
|
||||||
assert (tmp_path / "abc123.de.vtt").is_file()
|
assert (tmp_path / "Kanal_Eine_Rede_2026-03-28.de.vtt").is_file()
|
||||||
assert "--write-subs" in run.calls[1]
|
assert "--write-subs" in run.calls[1]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,17 @@ als Eingabe für vtt2txt.py bzw. rede_korrigieren.py."""
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
YT_DLP = "yt-dlp"
|
YT_DLP = "yt-dlp"
|
||||||
|
|
||||||
|
NAME_WORD = re.compile(r"\w+(?:[-.]\w+)*") # keeps "Krone-Schmalz", "28.03.2026"
|
||||||
|
MAX_CHANNEL_CHARS = 40
|
||||||
|
MAX_TITLE_CHARS = 60
|
||||||
|
|
||||||
|
|
||||||
def run_yt_dlp(arguments):
|
def run_yt_dlp(arguments):
|
||||||
"""Run yt-dlp in a temporary working directory.
|
"""Run yt-dlp in a temporary working directory.
|
||||||
|
|
@ -80,6 +85,33 @@ def choose_track(info, lang):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def name_part(text, limit):
|
||||||
|
"""Words of `text` joined by "_", cut at a word boundary after `limit` chars."""
|
||||||
|
words = NAME_WORD.findall(text or "")
|
||||||
|
part = ""
|
||||||
|
for word in words:
|
||||||
|
candidate = f"{part}_{word}" if part else word
|
||||||
|
if len(candidate) > limit:
|
||||||
|
break
|
||||||
|
part = candidate
|
||||||
|
# A single overlong word is cut rather than dropped
|
||||||
|
return part or (words[0][:limit] if words else "")
|
||||||
|
|
||||||
|
|
||||||
|
def default_name(info):
|
||||||
|
"""File name without extension from channel, title and upload date.
|
||||||
|
|
||||||
|
"John_Mearsheimer_Offensive_Realism_Episode_1_Origins_of_the_War_In_Ukraine_2026-07-30"
|
||||||
|
"""
|
||||||
|
date = info.get("upload_date") or ""
|
||||||
|
parts = [
|
||||||
|
name_part(info.get("channel") or info.get("uploader"), MAX_CHANNEL_CHARS),
|
||||||
|
name_part(info.get("title"), MAX_TITLE_CHARS),
|
||||||
|
f"{date[:4]}-{date[4:6]}-{date[6:]}" if re.fullmatch(r"\d{8}", date) else "",
|
||||||
|
]
|
||||||
|
return "_".join(p for p in parts if p) or info.get("id") or "video"
|
||||||
|
|
||||||
|
|
||||||
def available_tracks(info):
|
def available_tracks(info):
|
||||||
"""Manual tracks and the automatic ones usable for this program."""
|
"""Manual tracks and the automatic ones usable for this program."""
|
||||||
manual = sorted((info.get("subtitles") or {}).keys())
|
manual = sorted((info.get("subtitles") or {}).keys())
|
||||||
|
|
@ -126,7 +158,7 @@ def parse_args():
|
||||||
parser.add_argument("-u", "--url", required=True, help="YouTube-URL des Videos")
|
parser.add_argument("-u", "--url", required=True, help="YouTube-URL des Videos")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-n", "--name",
|
"-n", "--name",
|
||||||
help="Dateiname ohne Endung, z. B. Lahnstein (Standard: Video-ID)",
|
help="Dateiname ohne Endung, z. B. Lahnstein (Standard: <Kanal>_<Titel>_<Datum>)",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-l", "--lang", default="de", help="Sprache der Untertitel (Standard: de)"
|
"-l", "--lang", default="de", help="Sprache der Untertitel (Standard: de)"
|
||||||
|
|
@ -158,7 +190,7 @@ def main():
|
||||||
print(f"Lade {kind} Untertitel „{track}“ …", flush=True)
|
print(f"Lade {kind} Untertitel „{track}“ …", flush=True)
|
||||||
|
|
||||||
args.dir.mkdir(parents=True, exist_ok=True)
|
args.dir.mkdir(parents=True, exist_ok=True)
|
||||||
target_base = args.dir / (args.name or info["id"])
|
target_base = args.dir / (args.name or default_name(info))
|
||||||
download(args.url, track, is_auto, target_base)
|
download(args.url, track, is_auto, target_base)
|
||||||
|
|
||||||
target = Path(f"{target_base}.{track}.vtt")
|
target = Path(f"{target_base}.{track}.vtt")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue