feat(sprachen): Rippy fragt vor dem Rip, welche Sprachen du willst
Ampel / ampel (push) Successful in 29s
Ampel / ampel (push) Successful in 29s
Commander-Anforderung: "Die Disc hat Material in X Sprachen und X Untertiteln -
Rippy muss VOR dem Rip fragen: Was genau willst du haben? In der Automatik muss
das ebenfalls einstellbar sein."
Die Auskunft lag laengst vor und wurde weggeworfen: Derselbe Titel-Scan, der die
Titel-Tabelle fuellt, liefert in derselben makemkvcon-Ausgabe die Streams mit.
Ein zweiter Info-Lauf haette eine Minute Wartezeit gekostet - jetzt kommt beides
aus einem Aufruf.
Format an der Akira-Blu-ray im Laufwerk gemessen (AGENTS Regel D):
SINFO:<titel>,<stream>,<attribut>,<code>,"<wert>"
1 = Typ ("Audio"/"Subtitles"), 3 = Sprachcode, 4 = Sprachname,
6 = Codec, 14 = Kanaele, 30 = Beschreibung
DIE FALLE dabei: Die Sprache steht in 3/4, NICHT in 28/29. Die tragen auf JEDEM
Stream "eng"/"English" - auch auf einer deutschen Tonspur und auf dem
Videostream; das ist MakeMKVs eigene Anzeigesprache. Wer 28 nimmt, haelt jede
Disc fuer englisch. Ein Test haelt das fest.
Angewendet wird die Wahl bei der KOMPRESSION, nicht beim Rippen - drei Gruende:
der Rip bleibt vollstaendig und verlustfrei (Muss-Feature laut KONZEPT); HandBrake
hat dafuer dokumentierte Schalter (--audio-lang-list / --subtitle-lang-list, die
genau die ISO-639-2-Codes nehmen, die MakeMKV liefert - beides gegengeprueft);
und wer spaeter andere Sprachen will, komprimiert neu statt die Disc wieder
einzulegen. Genau das sagt der Dialog auch, sonst glaubt man, es werde
unvollstaendig gerippt.
Nichts angeklickt heisst "alles behalten" - das Verhalten von vorher. Auch die
Einstellung ist bewusst LEER vorbelegt: Ein stilles "deu" wuerde bei einem
japanischen Original die Originaltonspur wegwerfen, ohne dass jemand gefragt hat.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+162
-9
@@ -209,13 +209,118 @@ def parse_titel_info(ausgabe: str) -> list:
|
||||
return [titel[nr] for nr in sorted(titel)]
|
||||
|
||||
|
||||
def lies_titel_info(device_path: str, timeout: int = 300) -> list:
|
||||
"""Volle Titel-Liste der eingelegten Disc (makemkvcon info, Robot-Mode)."""
|
||||
# Attribut-IDs der SINFO-Zeilen. Am 26.07.2026 an der Akira-Blu-ray im Laufwerk
|
||||
# der Rippy-VM abgenommen (AGENTS Regel D):
|
||||
#
|
||||
# SINFO:0,1,1,6202,"Audio"
|
||||
# SINFO:0,1,3,0,"deu"
|
||||
# SINFO:0,1,4,0,"German"
|
||||
# SINFO:0,1,6,0,"DTS-HD MA"
|
||||
# SINFO:0,1,14,0,"2"
|
||||
# SINFO:0,1,30,0,"DTS-HD MA Stereo German"
|
||||
#
|
||||
# ⚠️ Die Sprache steht in 3/4 — NICHT in 28/29. Die tragen auf JEDEM Stream
|
||||
# „eng"/„English", auch auf dem Videostream einer deutschen Tonspur; das ist
|
||||
# MakeMKVs eigene Anzeigesprache und nicht die des Tracks. Wer 28 nimmt, hält
|
||||
# jede Disc für englisch.
|
||||
SINFO_TYP = 1
|
||||
SINFO_SPRACHE_CODE = 3
|
||||
SINFO_SPRACHE_NAME = 4
|
||||
SINFO_CODEC = 6
|
||||
SINFO_KANAELE = 14
|
||||
SINFO_BESCHREIBUNG = 30
|
||||
|
||||
SINFO_ZEILE = re.compile(r'SINFO:(\d+),(\d+),(\d+),\d+,"(.*)"\s*$')
|
||||
|
||||
|
||||
def parse_stream_info(ausgabe: str) -> dict:
|
||||
"""SINFO-Zeilen → Tonspuren und Untertitel je Titel (pure Funktion).
|
||||
|
||||
Commander-Anforderung 26.07.2026: *„Es muss verfügbar sein, dass man die
|
||||
Sprache von Medien auswählen kann … Rippy muss VOR dem Rip fragen: Was genau
|
||||
willst du haben?"* Die Auskunft liegt längst vor — der Titel-Scan liest sie
|
||||
mit und hat sie bisher weggeworfen.
|
||||
|
||||
Rückgabe: {titel_nr: {"audio": [...], "untertitel": [...]}}, jeder Eintrag
|
||||
{"nr", "lang", "sprache", "codec", "kanaele", "beschreibung"}.
|
||||
"""
|
||||
titel: dict = {}
|
||||
for zeile in (ausgabe or "").splitlines():
|
||||
treffer = SINFO_ZEILE.match(zeile.strip())
|
||||
if not treffer:
|
||||
continue
|
||||
t_nr, s_nr, attr = (int(treffer.group(i)) for i in (1, 2, 3))
|
||||
wert = treffer.group(4)
|
||||
streams = titel.setdefault(t_nr, {})
|
||||
eintrag = streams.setdefault(s_nr, {"nr": s_nr, "typ": "", "lang": "",
|
||||
"sprache": "", "codec": "",
|
||||
"kanaele": "", "beschreibung": ""})
|
||||
if attr == SINFO_TYP:
|
||||
eintrag["typ"] = wert
|
||||
elif attr == SINFO_SPRACHE_CODE:
|
||||
eintrag["lang"] = wert
|
||||
elif attr == SINFO_SPRACHE_NAME:
|
||||
eintrag["sprache"] = wert
|
||||
elif attr == SINFO_CODEC:
|
||||
eintrag["codec"] = wert
|
||||
elif attr == SINFO_KANAELE:
|
||||
eintrag["kanaele"] = wert
|
||||
elif attr == SINFO_BESCHREIBUNG:
|
||||
eintrag["beschreibung"] = wert
|
||||
|
||||
ergebnis = {}
|
||||
for t_nr, streams in titel.items():
|
||||
audio, untertitel = [], []
|
||||
for s_nr in sorted(streams):
|
||||
s = streams[s_nr]
|
||||
# MakeMKV schreibt „Subtitles" (englisch, Plural) — gemessen.
|
||||
if s["typ"] == "Audio":
|
||||
audio.append(s)
|
||||
elif s["typ"].startswith("Subtitle"):
|
||||
untertitel.append(s)
|
||||
ergebnis[t_nr] = {"audio": audio, "untertitel": untertitel}
|
||||
return ergebnis
|
||||
|
||||
|
||||
def sprachen_zusammenfassen(streams_je_titel: dict) -> dict:
|
||||
"""Welche Sprachen hat die DISC insgesamt? (pure Funktion)
|
||||
|
||||
Für die Frage vor dem Rip zählt nicht Titel 3 Stream 7, sondern: „Diese Disc
|
||||
hat Ton auf Deutsch, Englisch und Japanisch, Untertitel auf Deutsch und
|
||||
Englisch." Deshalb über alle Titel vereinigt, Reihenfolge stabil (so wie sie
|
||||
auf der Disc stehen).
|
||||
|
||||
Rückgabe: {"audio": [{"lang","sprache","spuren"}], "untertitel": [...]}
|
||||
"""
|
||||
def sammeln(schluessel):
|
||||
gefunden = {}
|
||||
for streams in (streams_je_titel or {}).values():
|
||||
for s in streams.get(schluessel) or []:
|
||||
code = s.get("lang") or ""
|
||||
if not code:
|
||||
continue
|
||||
eintrag = gefunden.setdefault(
|
||||
code, {"lang": code, "sprache": s.get("sprache") or code,
|
||||
"spuren": 0})
|
||||
eintrag["spuren"] += 1
|
||||
return list(gefunden.values())
|
||||
|
||||
return {"audio": sammeln("audio"), "untertitel": sammeln("untertitel")}
|
||||
|
||||
|
||||
def lies_titel_info(device_path: str, timeout: int = 300) -> tuple:
|
||||
"""(Titel-Liste, Streams je Titel) der eingelegten Disc — EIN Info-Lauf.
|
||||
|
||||
Beides aus demselben Aufruf: Ein `makemkvcon info` dauert je Disc 20–120 s,
|
||||
und die Sprach-Auskunft steht in derselben Ausgabe. Sie zweimal zu holen
|
||||
wäre eine Minute Wartezeit für nichts.
|
||||
"""
|
||||
ergebnis = subprocess.run(
|
||||
["makemkvcon", "-r", "--noscan", "info", f"dev:{device_path}"],
|
||||
capture_output=True, text=True, timeout=timeout,
|
||||
)
|
||||
return parse_titel_info(ergebnis.stdout or "")
|
||||
ausgabe = ergebnis.stdout or ""
|
||||
return parse_titel_info(ausgabe), parse_stream_info(ausgabe)
|
||||
|
||||
|
||||
def rip_titel_auswahl(device_path: str, output_dir: str, titel_liste: list,
|
||||
@@ -393,22 +498,49 @@ def preset_fuer(disc_type: str, einstellungen: dict) -> str:
|
||||
return (einstellungen.get("transcodePreset") or "").strip() or DEFAULT_HB_PRESET
|
||||
|
||||
|
||||
def build_handbrake_cmd(input_path: str, output_path: str, preset: str = DEFAULT_HB_PRESET) -> list:
|
||||
def build_handbrake_cmd(input_path: str, output_path: str,
|
||||
preset: str = DEFAULT_HB_PRESET,
|
||||
audio_sprachen=None, untertitel_sprachen=None) -> list:
|
||||
"""Baut das HandBrake-Kommando für die Kompressions-Stufe (pure Funktion).
|
||||
|
||||
Arbeitet auf der MKV-DATEI aus dem MakeMKV-Rip — nie auf dem Laufwerk:
|
||||
HandBrake kann AACS-verschlüsselte Blu-rays nicht lesen, MakeMKV bleibt
|
||||
deshalb zwingend die erste Stufe. --all-audio/--all-subtitles behalten
|
||||
alle Sprachen (Preset-Default wäre nur die erste Tonspur).
|
||||
|
||||
## Die Sprachauswahl greift HIER, nicht beim Rippen
|
||||
|
||||
Commander-Anforderung 26.07.2026. Bewusst in der KOMPRESSION und nicht im
|
||||
Rip, aus drei Gründen:
|
||||
|
||||
1. Der Rip bleibt vollständig und verlustfrei — das ist ein Muss-Feature
|
||||
(KONZEPT). Wer später eine andere Sprachwahl will, komprimiert neu, statt
|
||||
die Disc wieder einzulegen.
|
||||
2. MakeMKV kann Tracks nur über eine Auswahl-Profildatei filtern; HandBrake
|
||||
hat dafür dokumentierte Schalter, die genau die Codes nehmen, die MakeMKV
|
||||
liefert (ISO 639-2, „deu"/„eng" — beides am 26.07.2026 gegengeprüft).
|
||||
3. Wird ein Disc-Typ NICHT komprimiert (4K verlustfrei), bleiben ohnehin alle
|
||||
Sprachen drin — und das ist bei einer verlustfreien Ablage richtig.
|
||||
|
||||
`--audio-lang-list` zusammen mit `--all-audio` heißt laut HandBrake-Hilfe:
|
||||
alle Tonspuren, die zu einer der Sprachen passen. Leere Liste = alles, wie
|
||||
bisher.
|
||||
"""
|
||||
return [
|
||||
befehl = [
|
||||
"HandBrakeCLI",
|
||||
"--input", input_path,
|
||||
"--output", output_path,
|
||||
"--preset", preset,
|
||||
"--all-audio",
|
||||
"--all-subtitles",
|
||||
]
|
||||
audio = [s for s in (audio_sprachen or []) if s]
|
||||
if audio:
|
||||
befehl += ["--audio-lang-list", ",".join(audio)]
|
||||
befehl.append("--all-audio")
|
||||
untertitel = [s for s in (untertitel_sprachen or []) if s]
|
||||
if untertitel:
|
||||
befehl += ["--subtitle-lang-list", ",".join(untertitel)]
|
||||
befehl.append("--all-subtitles")
|
||||
return befehl
|
||||
|
||||
|
||||
HB_ENCODE_RE = re.compile(
|
||||
@@ -449,8 +581,28 @@ def get_progress_from_line(line: str) -> int:
|
||||
return int(((aktuell - 1) * 100 + prozent) / gesamt)
|
||||
|
||||
|
||||
def sprachliste(wert) -> list:
|
||||
"""„deu, eng , ,DEU" → ["deu", "eng"] (pure Funktion).
|
||||
|
||||
Nimmt Komma-Text aus den Einstellungen oder eine fertige Liste. Doppelte und
|
||||
Leeres fliegen raus, Kleinschreibung wird erzwungen — HandBrake will
|
||||
ISO-639-2 in Kleinbuchstaben, und ein „DEU" aus einem Eingabefeld soll nicht
|
||||
still die ganze Auswahl unwirksam machen.
|
||||
"""
|
||||
if not wert:
|
||||
return []
|
||||
teile = wert.split(",") if isinstance(wert, str) else list(wert)
|
||||
sauber = []
|
||||
for t in teile:
|
||||
code = str(t).strip().lower()
|
||||
if code and code not in sauber:
|
||||
sauber.append(code)
|
||||
return sauber
|
||||
|
||||
|
||||
def run_handbrake(input_path: str, output_path: str, preset: str = DEFAULT_HB_PRESET,
|
||||
progress_cb=None, abbruch_cb=None) -> dict:
|
||||
progress_cb=None, abbruch_cb=None,
|
||||
audio_sprachen=None, untertitel_sprachen=None) -> dict:
|
||||
"""Komprimiert eine MKV-Datei mit HandBrakeCLI; meldet Fortschritt.
|
||||
|
||||
`abbruch_cb` wird bei JEDER Ausgabezeile aufgerufen und soll RipAbbruch
|
||||
@@ -467,7 +619,8 @@ def run_handbrake(input_path: str, output_path: str, preset: str = DEFAULT_HB_PR
|
||||
|
||||
try:
|
||||
process = subprocess.Popen(
|
||||
build_handbrake_cmd(input_path, output_path, preset),
|
||||
build_handbrake_cmd(input_path, output_path, preset,
|
||||
audio_sprachen, untertitel_sprachen),
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
|
||||
Reference in New Issue
Block a user