"""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 "--all-audio" in cmd # alle Sprachen behalten 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 assert get_progress_from_line("kein Fortschritt hier") == 0 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 laeuft 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 fuer die Regex (Stand 25.07.2026): vorher stand hier line.split(",", 4)[3]. Das schnitt jede Meldung ab, die selbst ein Komma enthaelt — 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 ausser 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_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)