"""Tests für ripping.py: Kommando-Bau und Fortschritts-Parsing. Deckt genau die Stellen ab, an denen Reviews erfundene Schnittstellen fanden (abcde-Flags, Celery-API, HandBrake-Regex) — damit so etwas nie wieder still liegt. Das PRGV-Format stammt aus der MakeMKV-Doku (makemkv.com/developers/usage.txt). """ import os from ripping import ( build_abcde_cmd, build_handbrake_cmd, build_makemkv_cmd, get_progress_from_line, get_progress_from_prgv, parse_msg, write_abcde_config, ) def test_makemkv_cmd_vollstaendig(): cmd = build_makemkv_cmd("/dev/sr0", "/app/media/dvd/x") assert cmd[0] == "makemkvcon" assert "-r" in cmd # Robot-Mode: maschinenlesbar assert "--noscan" in cmd # Scan hängt/crasht im Container (23.07.) assert "--progress=-same" in cmd # Fortschritt im selben Stream assert "mkv" in cmd assert "dev:/dev/sr0" in cmd # Geräte-Notation laut Doku assert cmd[-2:] == ["all", "/app/media/dvd/x"] def test_handbrake_cmd_arbeitet_auf_datei_nicht_geraet(): """HandBrake kann AACS nicht — es bekommt IMMER die MKV-Datei aus dem MakeMKV-Rip, nie das Laufwerk (die alte Direkt-am-Gerät-Pipeline war für Blu-rays prinzipiell funktionsunfähig).""" cmd = build_handbrake_cmd("/app/temp/raw/x/t00.mkv", "/app/media/bluray/x/t00.mkv") assert cmd[0] == "HandBrakeCLI" assert cmd[cmd.index("--input") + 1] == "/app/temp/raw/x/t00.mkv" assert cmd[cmd.index("--output") + 1] == "/app/media/bluray/x/t00.mkv" assert "--preset" in cmd assert "--first-audio" in cmd # beste Spur pro Sprache behalten assert "--audio-codec" in cmd assert "--all-subtitles" in cmd def test_handbrake_progress_parsing(): # Testfund 22.07.: echtes HandBrake schreibt 45.50 % MIT Leerzeichen assert get_progress_from_line("Encoding: task 1 of 1, 45.50 %") == 45 assert get_progress_from_line("Encoding: task 1 of 1, 100.00 %") == 100 # Echte Zeile mit fps-Anhang, wie sie im Binary steht assert get_progress_from_line( "Encoding: task 1 of 1, 12.34 % (5.67 fps, avg 4.32 fps, ETA 00h12m34s)" ) == 12 # Ein echtes 0 % ist eine ANGABE, keine Leermeldung assert get_progress_from_line("Encoding: task 1 of 1, 0.00 %") == 0 def test_handbrake_progress_ignoriert_scan_durchlauf(): """Befund 25.07.2026 (Akira-UHD, live gemessen): HandBrake läuft VOR dem Encodieren einen Scan-Durchlauf, der ebenfalls Prozente ausgibt und dabei bis 100 % steigt. Die alte Regex nahm jede Zahl vor einem Prozentzeichen und meldete deshalb 99 %, während der Encode bei 1 % stand. -1 heißt „keine Encode-Fortschrittszeile" — dasselbe Muster wie bei get_progress_from_prgv. Fremde Zeilen dürfen NIE als 0 % gelten. """ assert get_progress_from_line("Scanning title 1 of 1, preview 3, 30.00 %") == -1 assert get_progress_from_line("Scanning title 1 of 1, preview 10, 100.00 %") == -1 # Vorlauf-Phase: Prozente beziehen sich auf die Suche, nicht auf den Encode assert get_progress_from_line( "Encoding: task 1 of 1, Searching for start time, 42.00 %" ) == -1 assert get_progress_from_line("kein Fortschritt hier") == -1 assert get_progress_from_line("Muxing: this may take awhile...") == -1 assert get_progress_from_line("") == -1 def test_handbrake_progress_rechnet_zwei_durchlaeufe_zusammen(): """Presets mit zwei Durchläufen zählen die Prozente je Durchlauf neu. Ohne Verrechnung sprang die Anzeige mitten in der Datei zurück auf 0.""" assert get_progress_from_line("Encoding: task 1 of 2, 50.00 %") == 25 assert get_progress_from_line("Encoding: task 2 of 2, 0.00 %") == 50 assert get_progress_from_line("Encoding: task 2 of 2, 100.00 %") == 100 def test_prgv_parsing(): # PRGV:current,total,max — total/max ist der Gesamtfortschritt assert get_progress_from_prgv("PRGV:100,32768,65536") == 50 assert get_progress_from_prgv("PRGV:0,65536,65536") == 100 assert get_progress_from_prgv("PRGV:0,0,65536") == 0 def test_prgv_parsing_ignoriert_fremde_zeilen(): # -1 heißt „keine Fortschrittszeile" — MSG-Zeilen dürfen NIE als 0% gelten, # sonst springt die Anzeige ständig auf null zurück. assert get_progress_from_prgv('MSG:1005,0,1,"MakeMKV gestartet","%1","x"') == -1 assert get_progress_from_prgv("irgendwas") == -1 assert get_progress_from_prgv("PRGV:kaputt") == -1 def test_parse_msg_trennt_code_und_klartext(): """Echte Zeilen aus einem makemkvcon-Lauf vom 25.07.2026 (Akira UHD). Feld 4 ist laut https://www.makemkv.com/developers/usage.txt der fertig zusammengesetzte Klartext — genau der landet im Rippy-Log. """ assert parse_msg( 'MSG:1005,0,1,"MakeMKV v1.18.4 linux(x64-release) started","%1 started","MakeMKV v1.18.4 linux(x64-release)"' ) == (1005, "MakeMKV v1.18.4 linux(x64-release) started") assert parse_msg( 'MSG:1011,0,1,"Using LibreDrive mode (v06.3 id=866A98CB9C4E)","%1","Using LibreDrive mode (v06.3 id=866A98CB9C4E)"' ) == (1011, "Using LibreDrive mode (v06.3 id=866A98CB9C4E)") def test_parse_msg_liest_die_uhd_fehlermeldung(): """3303 ist der Befund, um den es beim ganzen KEYDB-Thema geht: das Laufwerk läuft im LibreDrive-Modus, MakeMKV kennt nur den Schluessel DIESER Pressung nicht. Ohne diese Zeile im Log raet der Commander.""" assert parse_msg( 'MSG:3303,16777216,0,"The volume key is unknown for this disc - video can\'t be decrypted","The volume key is unknown for this disc - video can\'t be decrypted"' ) == (3303, "The volume key is unknown for this disc - video can't be decrypted") assert parse_msg('MSG:5010,0,0,"Failed to open disc","Failed to open disc"') == ( 5010, "Failed to open disc", ) def test_parse_msg_schneidet_meldungen_mit_komma_nicht_ab(): """DER Grund für die Regex (Stand 25.07.2026): vorher stand hier line.split(",", 4)[3]. Das schnitt jede Meldung ab, die selbst ein Komma enthält — und MakeMKV schreibt solche laufend. Im Log stand dann nur noch ein Satzfragment, das mehr verwirrt als hilft.""" zeile = ( 'MSG:3025,0,3,"Title #1 has length of 12 seconds, which is less than ' 'minimum title length of 120 seconds and was therefore skipped",' '"Title #%1 has length of %2 seconds which is less than minimum title ' 'length of %3 seconds and was therefore skipped","1","12","120"' ) code, text = parse_msg(zeile) assert code == 3025 assert text.endswith("and was therefore skipped") assert "which is less than" in text def test_parse_msg_ignoriert_fremde_zeilen(): # Alles außer MSG muss None liefern, sonst landet Fortschritts-Rauschen # (PRGV kommt mehrmals pro Sekunde) als Log-Eintrag in der Datenbank. assert parse_msg("PRGV:100,32768,65536") is None assert parse_msg('DRV:0,2,999,12,"BD-RE ASUS BW-16D1HT","AKIRA","/dev/sr0"') is None assert parse_msg("TCOUNT:5") is None assert parse_msg("") is None assert parse_msg("irgendwelcher Muell ohne Struktur") is None def test_abcde_cmd_hat_genau_ein_ausgabeformat(): """Review-Fund 22.07.: '-o' stand doppelt (Format UND Verzeichnis) — abcde parste das Verzeichnis als Format, CD-Ripping war nie funktionsfähig.""" cmd = build_abcde_cmd("/dev/sr0", "/tmp/test.abcde.conf") assert cmd.count("-o") == 1 assert cmd[cmd.index("-o") + 1] == "flac" assert "-c" in cmd assert cmd[cmd.index("-c") + 1] == "/tmp/test.abcde.conf" assert "-N" in cmd # nicht-interaktiv, sonst hängt der Worker def test_abcde_auswurf_folgt_der_einstellung(): """Befund 25.07.2026: `-x` (Auswurf) stand fest verdrahtet drin. Eine Audio-CD warf damit IMMER aus, eine DVD/Blu-ray NIE — und die Einstellung „Automatischer Auswurf" regelte keines von beidem, weil sie nirgends gelesen wurde.""" assert "-x" in build_abcde_cmd("/dev/sr0", "/tmp/c.conf", auswerfen=True) assert "-x" not in build_abcde_cmd("/dev/sr0", "/tmp/c.conf", auswerfen=False) # Standard bleibt „auswerfen" — so war das Verhalten bisher assert "-x" in build_abcde_cmd("/dev/sr0", "/tmp/c.conf") # Die Config darf durch das weggefallene -x nicht verrutschen ohne = build_abcde_cmd("/dev/sr0", "/tmp/c.conf", auswerfen=False) assert ohne[ohne.index("-c") + 1] == "/tmp/c.conf" def test_abcde_config_enthaelt_zielverzeichnis(): pfad = write_abcde_config("/app/media/cd/test123") try: with open(pfad, encoding="utf-8") as f: inhalt = f.read() assert "OUTPUTDIR='/app/media/cd/test123'" in inhalt assert "INTERACTIVE=n" in inhalt finally: os.unlink(pfad) def test_preset_fuer_nimmt_das_preset_des_disc_typs(): # Befund 25.07.2026: vorher galt EIN Preset für alles — eine 4K-UHD wurde # damit auf 1080p heruntergerechnet, und beim ersten echten UHD-Rip wäre # die 4K-Aufloesung still verlorengegangen. from ripping import preset_fuer einstellungen = { "transcodePreset": "HQ 1080p30 Surround", "transcodePresetDvd": "H.265 MKV 576p25", "transcodePresetBluray": "H.265 MKV 1080p30", "transcodePresetUhd": "H.265 MKV 2160p60 4K", } assert preset_fuer("uhd", einstellungen) == "H.265 MKV 2160p60 4K" assert preset_fuer("bluray", einstellungen) == "H.265 MKV 1080p30" assert preset_fuer("dvd", einstellungen) == "H.265 MKV 576p25" def test_preset_fuer_faellt_auf_das_allgemeine_preset_zurueck(): # Bestandsinstallationen kennen die drei neuen Felder nicht. Solange der # Nutzer sie nicht speichert, MUSS sich sein Verhalten nicht ändern. from ripping import preset_fuer alt = {"transcodePreset": "HQ 1080p30 Surround"} assert preset_fuer("uhd", alt) == "HQ 1080p30 Surround" assert preset_fuer("dvd", alt) == "HQ 1080p30 Surround" # Leere Zeichenkette zählt als "nicht gesetzt" (leeres Select-Feld im UI) assert preset_fuer("uhd", {"transcodePresetUhd": " ", "transcodePreset": "X"}) == "X" def test_preset_fuer_ohne_einstellungen_nimmt_den_eingebauten_standard(): # Unbekannter Disc-Typ, leere oder fehlende Einstellungen: nie None, nie # Absturz — sonst stirbt die Kompression an einem leeren --preset-Argument. from ripping import DEFAULT_HB_PRESET, preset_fuer assert preset_fuer("uhd", {}) == DEFAULT_HB_PRESET assert preset_fuer("", None) == DEFAULT_HB_PRESET assert preset_fuer(None, {}) == DEFAULT_HB_PRESET assert preset_fuer("cd", {"transcodePresetUhd": "egal"}) == DEFAULT_HB_PRESET def test_handbrake_prueft_abbruch_bei_jeder_zeile_nicht_nur_bei_fortschritt(): """Befund 25.07.2026 (am laufenden Akira-Job beobachtet): Der Abbruch hing am Fortschritts-Callback, und der stieg bei unveraenderter Prozentzahl sofort aus. Bei einem 4K-Encode mit einem Prozent je halber Stunde sah „Abbrechen" minutenlang wirkungslos aus (gemessen: 3,4 min). Der Abbruch-Kanal muss deshalb JEDE Ausgabezeile sehen — auch die des Scan-Durchlaufs, der gar keine Encode-Prozente liefert. """ import ripping zeilen = [ "Scanning title 1 of 1, preview 1, 10.00 %\n", "Scanning title 1 of 1, preview 2, 20.00 %\n", "Encoding: task 1 of 1, 0.00 %\n", "Encoding: task 1 of 1, 0.00 %\n", ] gesehen = [] class FakeProcess: def __init__(self): self.stdout = iter(zeilen) self.returncode = 0 self.getoetet = False def kill(self): self.getoetet = True def wait(self): return 0 prozess = FakeProcess() def abbruch_cb(): gesehen.append(1) if len(gesehen) == 2: # beim zweiten Mal abbrechen raise ripping.RipAbbruch() ergebnis = ripping._handbrake_schleife(prozess, "/x.mkv", abbruch_cb, None) assert ergebnis["status"] == "cancelled" assert prozess.getoetet is True # Zwei Scan-Zeilen genügten — es musste NICHT auf eine Encode-Zeile gewartet # werden. Genau das war der Fehler. assert len(gesehen) == 2 def test_komprimieren_fuer_globaler_schalter_schlaegt_alles(): from ripping import komprimieren_fuer assert komprimieren_fuer("uhd", {"transcodeEnabled": False}) is False assert komprimieren_fuer("dvd", {"transcodeEnabled": False}) is False # Standard ist AN - Bestandsinstallationen ändern ihr Verhalten nicht assert komprimieren_fuer("bluray", {}) is True assert komprimieren_fuer("bluray", None) is True def test_komprimieren_fuer_einzelner_disc_typ_abwaehlbar(): """Der Grund dafür (Befund 25.07.2026): 4K-HEVC brauchte auf der Rippy-VM gemessene 28-55 Stunden, weil die CPU kein AVX2 hat. Vorher gab es nur einen globalen Schalter - wer 4K verlustfrei behalten und DVDs trotzdem schrumpfen wollte, hatte keine Möglichkeit.""" from ripping import PRESET_KEINE, komprimieren_fuer nur_uhd_aus = {"transcodePresetUhd": PRESET_KEINE, "transcodePresetDvd": "H.265 MKV 576p25"} assert komprimieren_fuer("uhd", nur_uhd_aus) is False assert komprimieren_fuer("dvd", nur_uhd_aus) is True assert komprimieren_fuer("bluray", nur_uhd_aus) is True def test_preset_fuer_gibt_den_reservewert_nie_als_preset_zurueck(): """Sonst bekaeme HandBrake `--preset keine` und würde scheitern. Wer über "Neu komprimieren" ausdruecklich doch komprimieren will, soll ein brauchbares Preset bekommen statt eines Fehlers.""" from ripping import DEFAULT_HB_PRESET, PRESET_KEINE, preset_fuer assert preset_fuer("uhd", {"transcodePresetUhd": PRESET_KEINE, "transcodePreset": "HQ 1080p30 Surround"}) == "HQ 1080p30 Surround" # Auch ohne allgemeines Preset kommt ein gültiger Name heraus assert preset_fuer("uhd", {"transcodePresetUhd": PRESET_KEINE}) == DEFAULT_HB_PRESET def test_reservewert_kollidiert_mit_keinem_echten_preset(): """Gegengeprueft gegen alle 90 Presets aus `HandBrakeCLI --preset-list` im Worker-Image (25.07.2026) - keines heißt so.""" from ripping import PRESET_KEINE assert PRESET_KEINE == "keine" assert " " not in PRESET_KEINE # echte Preset-Namen haben durchweg Leerzeichen # --- Unbekanntes Preset: aus „Code 3" wird eine handelbare Meldung ----------- def test_unbekanntes_preset_wird_erkannt(): """Wortlaut echt gemessen (26.07.2026, Worker-Image): HandBrake antwortet „Invalid preset " und listet danach die gültigen auf.""" from ripping import unbekanntes_preset assert unbekanntes_preset("Invalid preset Gibt Es Nicht 999p") == "Gibt Es Nicht 999p" assert unbekanntes_preset(" Invalid preset H.265 XYZ 4K ") == "H.265 XYZ 4K" # Alles andere ist kein Preset-Fehler assert unbekanntes_preset("Valid presets are:") == "" assert unbekanntes_preset("Encoding: task 1 of 1, 5.00 %") == "" assert unbekanntes_preset("") == "" assert unbekanntes_preset(None) == "" def test_falsches_preset_erklaert_den_fehlschlag_statt_nur_den_code(): """Vorher stand im UI nur „HandBrake endete mit Code 3" — daraus war nicht zu erraten, dass der Preset-NAME das Problem ist. Der Fall wird häufiger, weil ein Remote-Worker eine andere HandBrake-Version haben kann.""" import ripping class FakeProcess: def __init__(self): self.stdout = iter([ "Invalid preset H.265 VCN 2160p 4K\n", "Valid presets are:\n", ]) self.returncode = 3 def kill(self): pass def wait(self): return 3 ergebnis = ripping._handbrake_schleife(FakeProcess(), "/gibt-es-nicht.mkv") assert ergebnis["status"] == "error" assert "H.265 VCN 2160p 4K" in ergebnis["error"] assert "Bestes wählen" in ergebnis["error"] assert ergebnis["return_code"] == 3 def test_fehler_ohne_preset_problem_bleibt_der_alte(): import ripping class FakeProcess: def __init__(self): self.stdout = iter(["irgendwas ganz anderes\n"]) self.returncode = 1 def kill(self): pass def wait(self): return 1 ergebnis = ripping._handbrake_schleife(FakeProcess(), "/gibt-es-nicht.mkv") assert ergebnis["error"] == "HandBrake endete mit Code 1" # --- Auswurf: das ioctl meldet Erfolg und tut nichts (Befund 26.07.2026) ----- def _laufwerk(verriegelt=True, kennt_lockdoor=True): """Ein nachgebautes Laufwerk, das sich wie das echte verhaelt. Gemessen am BU40N der Rippy-VM: Nach einem MakeMKV-Rip ist die Tuer verriegelt. CDROMEJECT wird dann ANGENOMMEN und tut nichts - der Status bleibt auf 4 (Disc drin). Erst CDROM_LOCKDOOR 0 macht den Auswurf wirksam. """ import ripping zustand = {"verriegelt": verriegelt, "status": ripping.CDS_DISC_OK, "aufrufe": []} def ioctl_fn(fd, befehl, arg=0): zustand["aufrufe"].append(befehl) if befehl == ripping.CDROM_LOCKDOOR: if not kennt_lockdoor: raise OSError("ioctl unbekannt") zustand["verriegelt"] = bool(arg) return 0 if befehl == ripping.CDROMEJECT: if not zustand["verriegelt"]: zustand["status"] = ripping.CDS_TRAY_OPEN return 0 # <- auch verriegelt: ERFOLG, aber ohne Wirkung if befehl == ripping.CDROM_DRIVE_STATUS: return zustand["status"] raise OSError("unerwartetes ioctl") return zustand, ioctl_fn def test_auswurf_entriegelt_zuerst_und_klappt_dann(): import ripping zustand, ioctl_fn = _laufwerk(verriegelt=True) ok = ripping.wirf_disc_aus( "/dev/sr0", ioctl_fn=ioctl_fn, oeffnen=lambda p: 42, schliessen=lambda fd: None, warten=lambda s: None) assert ok is True assert zustand["status"] == ripping.CDS_TRAY_OPEN # Reihenfolge: entriegeln VOR auswerfen assert zustand["aufrufe"][0] == ripping.CDROM_LOCKDOOR assert zustand["aufrufe"][1] == ripping.CDROMEJECT def test_auswurf_meldet_fehlschlag_wenn_die_disc_drin_bleibt(): """Der eigentliche Fehler. Vorher gab wirf_disc_aus True zurueck, weil das ioctl nicht geworfen hatte - und ins Log kam "Disc ausgeworfen", waehrend die Schublade zu blieb. Ein ioctl-Rueckgabewert beweist nichts.""" import ripping # Ein Laufwerk, das LOCKDOOR nicht kennt und verriegelt bleibt zustand, ioctl_fn = _laufwerk(verriegelt=True, kennt_lockdoor=False) ok = ripping.wirf_disc_aus( "/dev/sr0", ioctl_fn=ioctl_fn, oeffnen=lambda p: 42, schliessen=lambda fd: None, warten=lambda s: None) assert ok is False assert zustand["status"] == ripping.CDS_DISC_OK # nie aufgegangen def test_auswurf_bei_slot_laufwerk_ohne_schublade(): """Ein Slot-Laufwerk hat keine Schublade und meldet nach dem Auswerfen CDS_NO_DISC. Das muss als Erfolg zaehlen.""" import ripping assert ripping._auswurf_geglueckt(ripping.CDS_NO_DISC) is True assert ripping._auswurf_geglueckt(ripping.CDS_TRAY_OPEN) is True assert ripping._auswurf_geglueckt(ripping.CDS_DISC_OK) is False assert ripping._auswurf_geglueckt(ripping.CDS_DRIVE_NOT_READY) is False def test_auswurf_ohne_laufwerk_wirft_nicht(): import ripping def oeffnen_kaputt(p): raise OSError("kein Laufwerk") assert ripping.wirf_disc_aus( "/dev/sr9", ioctl_fn=lambda *a: 0, oeffnen=oeffnen_kaputt, schliessen=lambda fd: None, warten=lambda s: None) is False # --- Sprachen der Disc: gemessen an der Akira-Blu-ray (26.07.2026) ----------- # # Woertlich aus `makemkvcon -r --noscan info dev:/dev/sr0` im Worker-Container # der Rippy-VM, gekuerzt auf die Attribute, die Rippy liest (AGENTS Regel D). SINFO_ECHT = """SINFO:0,0,1,6201,"Video" SINFO:0,0,19,0,"1920x1080" SINFO:0,0,28,0,"eng" SINFO:0,0,29,0,"English" 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,28,0,"eng" SINFO:0,1,29,0,"English" SINFO:0,1,30,0,"DTS-HD MA Stereo German" SINFO:0,2,1,6202,"Audio" SINFO:0,2,3,0,"jpn" SINFO:0,2,4,0,"Japanese" SINFO:0,2,6,0,"DTS-HD MA" SINFO:0,2,14,0,"6" SINFO:0,2,30,0,"DTS-HD MA 5.1 Japanese" SINFO:0,3,1,6203,"Subtitles" SINFO:0,3,3,0,"deu" SINFO:0,3,4,0,"German" SINFO:0,3,6,0,"PGS" SINFO:1,0,1,6202,"Audio" SINFO:1,0,3,0,"eng" SINFO:1,0,4,0,"English" SINFO:1,0,6,0,"AC3" """ def test_streams_werden_nach_typ_getrennt(): from ripping import parse_stream_info je_titel = parse_stream_info(SINFO_ECHT) assert set(je_titel) == {0, 1} assert [a["lang"] for a in je_titel[0]["audio"]] == ["deu", "jpn"] assert [u["lang"] for u in je_titel[0]["untertitel"]] == ["deu"] assert je_titel[0]["audio"][1]["kanaele"] == "6" assert je_titel[0]["audio"][0]["codec"] == "DTS-HD MA" def test_die_sprache_steht_in_attribut_3_nicht_28(): """DIE Falle: 28/29 tragen auf JEDEM Stream "eng"/"English" - auch auf einer deutschen Tonspur und auf dem Videostream. Das ist MakeMKVs Anzeigesprache. Wer 28 nimmt, haelt jede Disc fuer englisch.""" from ripping import parse_stream_info deutsche_spur = parse_stream_info(SINFO_ECHT)[0]["audio"][0] assert deutsche_spur["lang"] == "deu" assert deutsche_spur["sprache"] == "German" def test_video_taucht_nicht_als_tonspur_auf(): from ripping import parse_stream_info je_titel = parse_stream_info(SINFO_ECHT) assert all(a["typ"] == "Audio" for a in je_titel[0]["audio"]) def test_sprachen_werden_ueber_alle_titel_vereinigt(): """Vor dem Rip zaehlt nicht "Titel 3 Stream 7", sondern was die DISC hat.""" from ripping import parse_stream_info, sprachen_zusammenfassen s = sprachen_zusammenfassen(parse_stream_info(SINFO_ECHT)) assert [a["lang"] for a in s["audio"]] == ["deu", "jpn", "eng"] assert [a["sprache"] for a in s["audio"]] == ["German", "Japanese", "English"] assert [u["lang"] for u in s["untertitel"]] == ["deu"] # deu kommt in Titel 0 einmal vor assert s["audio"][0]["spuren"] == 1 def test_leere_ausgabe_gibt_leere_sprachen(): from ripping import parse_stream_info, sprachen_zusammenfassen assert parse_stream_info("") == {} assert sprachen_zusammenfassen({}) == {"audio": [], "untertitel": []} assert sprachen_zusammenfassen(None) == {"audio": [], "untertitel": []} # --- Sprachliste aus den Einstellungen -------------------------------------- def test_sprachliste_raeumt_auf(): from ripping import sprachliste assert sprachliste("deu, eng , ,DEU") == ["deu", "eng"] assert sprachliste(["deu", "deu", "jpn"]) == ["deu", "jpn"] assert sprachliste("") == [] assert sprachliste(None) == [] def test_grossschreibung_macht_die_auswahl_nicht_unwirksam(): """HandBrake will ISO-639-2 klein. Ein "DEU" aus einem Eingabefeld darf nicht still die ganze Auswahl aushebeln.""" from ripping import sprachliste assert sprachliste("DEU,ENG") == ["deu", "eng"] # --- Das HandBrake-Kommando ------------------------------------------------- def test_handbrake_kommando_ohne_auswahl_bleibt_wie_bisher(): from ripping import build_handbrake_cmd cmd = build_handbrake_cmd("/a.mkv", "/b.mkv", "H.265 MKV 1080p30") assert "--audio-lang-list" not in cmd assert "--subtitle-lang-list" not in cmd assert "--first-audio" in cmd and "--all-subtitles" in cmd def test_handbrake_kommando_mit_sprachauswahl(): """--audio-lang-list zusammen mit --first-audio heisst laut HandBrake-Hilfe: nur die erste (beste) Tonspur, die zu einer der Sprachen passen.""" from ripping import build_handbrake_cmd cmd = build_handbrake_cmd("/a.mkv", "/b.mkv", "H.265 MKV 1080p30", audio_sprachen=["deu", "jpn"], untertitel_sprachen=["deu"]) assert cmd[cmd.index("--audio-lang-list") + 1] == "deu,jpn" assert cmd[cmd.index("--subtitle-lang-list") + 1] == "deu" assert "--first-audio" in cmd and "--all-subtitles" in cmd