From 955bdc2659d19e785800e152cf458cd25ce312db Mon Sep 17 00:00:00 2001 From: dschlueter Date: Sun, 11 Oct 2026 20:29:34 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20-z=20-00:20=20als=20Kurzform=20f=C3=BCr?= =?UTF-8?q?=200-00:20=20(vom=20Anfang=20bis)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wörter hinter -z, die mit - und einer Ziffer beginnen, werden vor argparse zu 0-… umgeschrieben; parse_ranges nimmt -B auch in Anführungszeichen an. Co-Authored-By: Claude Opus 5.5 --- README.md | 11 +++++------ extract_text.py | 7 ++++--- rede_korrigieren.py | 6 +++--- tests/test_rede_korrigieren.py | 7 +++++++ tests/test_vtt2txt.py | 29 ++++++++++++++++++++++++--- vtt2txt.py | 36 +++++++++++++++++++++++++++------- 6 files changed, 74 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 8225c94..cf097d1 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ 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. Mit `-z` kommen die Bereiche dazu, z. B. -`…_2026-07-30_00-03-17_bis_00-05-25` (`-z A`: `_ab_…`, `-z 0-B`: `_bis_…`; +`…_2026-07-30_00-03-17_bis_00-05-25` (`-z A`: `_ab_…`, `-z 0-B` bzw. `-z -B`: `_bis_…`; mehrere Bereiche mit `+` verbunden: `…_00-05-00_bis_00-06-00+ab_00-25-00`). Gleiches Video ergibt immer denselben Namen; eine vorhandene Datei wird überschrieben (ein mit `-u` gespeicherter, abweichender Rohtext aber nie). @@ -171,15 +171,14 @@ Gilt gleich für `vtt2txt.py`, `extract_text.py` und `rede_korrigieren.py`. | Aufruf | Text | |---|---| | `-z 00:05:10` | ab 5:10 bis zum Ende des Videos | -| `-z 0-00:20` | vom Anfang bis Minute 20 | +| `-z 0-00:20` oder `-z -00:20` | vom Anfang bis Minute 20 | | `-z 00:05-00:06` | von Minute 5 bis Minute 6 | | `-z 00:5:00-00:6:00 00:10-00:12:00 00:25` | Minute 5–6, Minute 10–12 und alles ab Minute 25 | -Ein Bereich ist `A-B`, `A` (ab A bis zum Ende) oder `0-B` (vom Anfang bis B). -Mehrere Bereiche werden durch Leerzeichen getrennt, mit oder ohne +Ein Bereich ist `A-B`, `A` (ab A bis zum Ende) oder `0-B` bzw. kurz `-B` (vom +Anfang bis B). Mehrere Bereiche werden durch Leerzeichen getrennt, mit oder ohne Anführungszeichen (`-z "00:05-00:06 00:25"` ist dasselbe). Sie dürfen sich -nicht überschneiden und werden nach der Zeit sortiert. `-B` geht nicht, weil -die Kommandozeile es für eine Option hält. +nicht überschneiden und werden nach der Zeit sortiert. Alle Bereiche landen in **einer** Datei. Vor jedem Bereich steht eine Überschrift mit seinen tatsächlichen Zeiten: diff --git a/extract_text.py b/extract_text.py index b5f7d4f..a4c5a76 100755 --- a/extract_text.py +++ b/extract_text.py @@ -4,12 +4,13 @@ from pathlib import Path import argparse import shutil +import sys import tempfile import textwrap from vtt2txt import ( - add_time_options, join_segments, resolve_time_ranges, split_segments, time_suffix, - vtt_to_text, + add_time_options, join_segments, range_argv, resolve_time_ranges, split_segments, + time_suffix, vtt_to_text, ) from yt_untertitel import ( YT_DLP, choose_track, default_name, download, fetch_info, no_track_message, @@ -38,7 +39,7 @@ def parse_args(): help=f"Fließtext, auf {FLOW_WIDTH} Zeichen umbrochen, statt Untertitelzeilen", ) add_time_options(parser) - args = parser.parse_args() + args = parser.parse_args(range_argv(sys.argv[1:])) resolve_time_ranges(parser, args) if args.output is not None and args.output.is_dir(): diff --git a/rede_korrigieren.py b/rede_korrigieren.py index f13f6af..4a63544 100755 --- a/rede_korrigieren.py +++ b/rede_korrigieren.py @@ -14,8 +14,8 @@ import urllib.request from aenderungen import REVIEW_MARK, print_summary, report_path, write_report from extract_text import fetch_text from vtt2txt import ( - add_time_options, join_segments, resolve_time_ranges, split_segments, time_suffix, - vtt_to_text, + add_time_options, join_segments, range_argv, resolve_time_ranges, split_segments, + time_suffix, vtt_to_text, ) from yt_untertitel import YT_DLP, base_language, default_name, original_language @@ -509,7 +509,7 @@ def parse_args(): help="keinen Änderungsbericht .aenderungen.html schreiben", ) add_time_options(parser) - args = parser.parse_args() + args = parser.parse_args(range_argv(sys.argv[1:])) resolve_time_ranges(parser, args) for glossary in args.glossar: diff --git a/tests/test_rede_korrigieren.py b/tests/test_rede_korrigieren.py index ee581e2..6e22d1b 100644 --- a/tests/test_rede_korrigieren.py +++ b/tests/test_rede_korrigieren.py @@ -532,6 +532,13 @@ def test_main_url_time_range_default_names(from_url, tmp_path, monkeypatch): assert result == "[00:00:02 - 00:00:03]\n\nDas ist bekannt.\n" +def test_main_url_range_from_beginning(from_url, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + run_main(monkeypatch, "-u", URL, "-z", "-0:00:02") + raw = (tmp_path / "Kanal_Rede_2026-03-28_bis_00-00-02.txt").read_text(encoding="utf-8") + assert raw == "[00:00:01 - 00:00:02]\nDer Kremmel ist in Moskau.\n" + + def test_main_url_time_range_without_text(from_url, tmp_path, monkeypatch): with pytest.raises(SystemExit, match="im gewählten Zeitraum keinen Text"): run_main(monkeypatch, "-u", URL, "-z", "1:00", "-o", tmp_path / "x.korrigiert.txt") diff --git a/tests/test_vtt2txt.py b/tests/test_vtt2txt.py index 01bebe0..7e324b2 100644 --- a/tests/test_vtt2txt.py +++ b/tests/test_vtt2txt.py @@ -190,6 +190,8 @@ def test_parse_cue_time_keeps_minutes_seconds(text, seconds): @pytest.mark.parametrize("text, ranges", [ ("00:5:00-00:6:00 00:10-00:12:00 00:25", [(300, 360), (600, 720), (1500, None)]), ("0-00:20", [(None, 1200)]), + ("-00:20", [(None, 1200)]), + ("-00:05 00:10-00:12", [(None, 300), (600, 720)]), ("0:00:00-0:01", [(None, 60)]), ("00:25- 00:10-00:12 0-00:05", [(None, 300), (600, 720), (1500, None)]), ("00:05-00:06 00:06-00:07", [(300, 360), (360, 420)]), @@ -200,8 +202,9 @@ def test_parse_ranges(text, ranges): @pytest.mark.parametrize("text, message", [ ("", "keine Bereiche"), - ("-", "vom Anfang: 0-B"), - ("-00:20", "vom Anfang: 0-B"), + ("-", "ungültiger Bereich"), + ("-0", "Start muss vor Ende"), + ("-00:05 -00:10", "überschneiden"), ("0", "das ganze Video"), ("0-", "das ganze Video"), ("00:06-00:05", "Start muss vor Ende"), @@ -384,6 +387,26 @@ def test_main_ranges_with_or_without_quotes(sample_copy, monkeypatch, ranges): assert target.is_file() +@pytest.mark.parametrize("argv, expected", [ + (["-i", "x", "-z", "-00:20"], ["-i", "x", "-z", "0-00:20"]), + (["-z", "00:05-00:06", "-00:20", "-o", "f"], ["-z", "00:05-00:06", "0-00:20", "-o", "f"]), + (["--zeitraeume", "-0:01:30"], ["--zeitraeume", "0-0:01:30"]), + (["-z", "0:05", "--exakt", "-1"], ["-z", "0:05", "--exakt", "-1"]), + (["-i", "-5.vtt"], ["-i", "-5.vtt"]), + (["-z=-00:20"], ["-z=-00:20"]), +]) +def test_range_argv(argv, expected): + assert vtt2txt.range_argv(argv) == expected + + +@pytest.mark.parametrize("ranges", [["-0:01"], ["0-0:01"], ["-z=-0:01"]]) +def test_main_range_from_beginning(sample_copy, monkeypatch, ranges): + args = ranges if ranges[0].startswith("-z") else ["-z", *ranges] + run_main(monkeypatch, "-i", sample_copy, *args) + target = sample_copy.with_name("ramstein.de_bis_00-01-00.txt") + assert target.read_text(encoding="utf-8").startswith("[00:00:09 - ") + + def test_main_time_range_without_captions(sample_copy, monkeypatch, capsys): with pytest.raises(SystemExit) as exc: run_main(monkeypatch, "-i", sample_copy, "-z", "99:00:00") @@ -396,7 +419,7 @@ def test_main_time_range_without_captions(sample_copy, monkeypatch, capsys): (["-z", "1:00-0:30"], "Start muss vor Ende"), (["-z", "0:05 0:10"], "überschneiden"), (["-z", "0:05", "0:10"], "überschneiden"), - (["-z=-0:20"], "vom Anfang: 0-B"), + (["-z", "-"], "ungültiger Bereich"), (["-z", "0"], "das ganze Video"), (["-s", "0:01"], "unrecognized arguments: -s"), ]) diff --git a/vtt2txt.py b/vtt2txt.py index 04b2d09..c10a7b9 100755 --- a/vtt2txt.py +++ b/vtt2txt.py @@ -48,20 +48,24 @@ def parse_user_time(text): def parse_ranges(text): """Sorted (start, end) seconds from "00:05:00-00:06:00 00:10-00:12 00:25". - "A" or "A-" runs to the end, "0-B" from the beginning; None marks an open side. + "A" or "A-" runs to the end, "0-B" or "-B" from the beginning; None marks an + open side. """ ranges = [] for item in text.split(): - if item.startswith("-"): - raise ValueError(f"ungültiger Bereich „{item}“, vom Anfang: 0-B schreiben") start, _, end = item.partition("-") + if item == "-": + raise ValueError(f"ungültiger Bereich „{item}“") try: - span = (parse_user_time(start) or None, parse_user_time(end) if end else None) + span = ( + parse_user_time(start) or None if start else None, + parse_user_time(end) if end else None, + ) except ValueError: raise ValueError(f"ungültiger Bereich „{item}“") from None if span == (None, None): raise ValueError(f"Bereich „{item}“ ist das ganze Video: -z weglassen") - if None not in span and span[0] >= span[1]: + if span[1] is not None and (span[0] or 0) >= span[1]: raise ValueError(f"Bereich „{item}“: Start muss vor Ende liegen") ranges.append(span) if not ranges: @@ -103,7 +107,7 @@ def time_suffix(ranges): RANGE_FORMS = ( - "A-B (von A bis B), A (ab A bis zum Ende) oder 0-B (vom Anfang bis B); " + "A-B (von A bis B), A (ab A bis zum Ende) oder 0-B bzw. -B (vom Anfang bis B); " f"Zeiten {TIME_EXAMPLES}" ) @@ -121,6 +125,24 @@ def add_time_options(parser): ) +def range_argv(argv): + """Copy of `argv` with "-00:20" after -z written as "0-00:20". + + argparse takes a word starting with "-" for an option and would reject it; + no option starts with "-" and a digit, so such a word can only be a range. + """ + result = [] + after_ranges = False + for word in argv: + if re.match(r"-\d", word): + if after_ranges: + word = f"0{word}" + elif word.startswith("-"): + after_ranges = word in ("-z", "--zeitraeume") + result.append(word) + return result + + def resolve_time_ranges(parser, args): """Check -z and set args.bereiche: sorted list of (start, end), maybe empty.""" if args.zeitraeume is None: @@ -398,7 +420,7 @@ def parse_args(): "mit -z zusätzlich die Bereiche im Namen", ) add_time_options(parser) - args = parser.parse_args() + args = parser.parse_args(range_argv(sys.argv[1:])) resolve_time_ranges(parser, args) suffix = time_suffix(args.bereiche)