diff --git a/README.md b/README.md index 0dda9a0..a8d1c00 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,13 @@ NICHT als emuliertes CD-ROM (`media=cdrom`), das kann keine SCSI-Kommandos. per Laufzeitabgleich (TMDB) automatisch zu „Serie S01E02.mkv" benannt (nur bei eindeutiger Zuordnung — sonst bleiben die Namen, mit Log). 3. **Kompression** (HandBrake, eigener Job auf eigener Queue): x265/x264, - Preset im UI wählbar; Rohdatei wird erst nach Erfolg gelöscht + **je Disc-Typ ein eigenes Preset** (Einstellungen → Verarbeitung) — + Rippy kennt den Typ und wählt selbst. Wichtig, weil ein 1080p-Preset + eine 4K-UHD stillschweigend herunterrechnet und ein 1080p-Preset eine + DVD sinnlos hochskaliert. Sind die drei Felder nie gespeichert worden, + gilt weiter das alte allgemeine Preset — Bestandsinstallationen ändern + ihr Verhalten also nicht von selbst. + Rohdatei wird erst nach Erfolg gelöscht („Original behalten" als Option). Fehlgeschlagene Kompressionen lassen sich ohne Neu-Rip neu anstoßen. 4. **Media-Server-Ablage**: Unter Einstellungen → Ripping (oder im diff --git a/ROADMAP.md b/ROADMAP.md index 5e75699..3a73866 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -541,6 +541,35 @@ completed", `TCOUNT:5`, fünf Titel, identisch zum Windows-Ergebnis. --- +## Etappe 19 (25.07.2026): Preset je Disc-Typ + +**Quelle:** Rückfrage des Commanders beim ersten echten UHD-Rip („merkt +Rippy eigentlich, wenn es eine UHD-Disc ist, und wendet direkt das +4K-Preset an?"). Antwort war: nein. Die Kompression fragte den Disc-Typ +gar nicht — ein globales `transcodePreset` galt für alles, live eingestellt +`HQ 1080p30 Surround`. Der laufende 4K-Rip wäre danach auf 1080p +heruntergerechnet und der Rohschnitt gelöscht worden (`keepOriginal: False`). + +**Gebaut:** +- [x] **`preset_fuer(disc_type, einstellungen)`** in `ripping.py` — pure + Funktion, drei Tests. Reihenfolge: Preset des Disc-Typs → allgemeines + `transcodePreset` → `DEFAULT_HB_PRESET`. Bestandsinstallationen + ändern ihr Verhalten NICHT, solange die neuen Felder ungespeichert sind. +- [x] **`transcode_files`** holt den Disc-Typ aus dem Job-Datensatz und + schreibt ihn mit ins Log („Disc-Typ 'uhd', Preset '…'"). +- [x] **UI**: drei Auswahlfelder (DVD / Blu-ray / 4K-UHD) statt einem, mit + Erklärung, warum 4K auf ein 2160p-Preset gehört. Preset-Namen aus + `HandBrakeCLI --preset-list` im Worker-Image belegt, nicht geraten. +- [x] **Sofortmaßnahme am laufenden Job**: `keepOriginal` auf `True`, damit + der 4K-Rohschnitt die Kompression überlebt. + +**Offen aus dieser Runde:** +- [ ] **Deploy steht aus** — er würde den laufenden Akira-Rip abbrechen. + Erst nach Abschluss des Jobs deployen, dann bei Bedarf „Neu + komprimieren" mit dem 4K-Preset. + +--- + ## Ideen-Katalog (Rest) — bewusst offen 1. **Design 2.0** — an Gemini übergeben (24.07.2026). Vollständiges diff --git a/SAVEPOINT.md b/SAVEPOINT.md index 1a7185c..243dede 100644 --- a/SAVEPOINT.md +++ b/SAVEPOINT.md @@ -1,6 +1,41 @@ # SAVEPOINT — Rippy -## Aktueller Stand: v3.11 — 4K-UHD GELÖST: Akira geht auf (25.07.2026) +## Aktueller Stand: v3.12 — Preset je Disc-Typ (25.07.2026) + +- **Der Fund:** Beim ersten echten UHD-Rip aufgefallen — die Kompression + fragte den Disc-Typ **gar nicht**: `preset = einstellungen.get( + "transcodePreset")`, ein globales Preset für alles. Live eingestellt war + `HQ 1080p30 Surround`. Der laufende Akira-Rip wäre also verlustfrei in 4K + gerippt und danach **auf 1080p heruntergerechnet** worden — und mit + `keepOriginal: False` wäre der 4K-Rohschnitt anschließend gelöscht worden. + Aufgefallen ist es nur, weil der Commander gefragt hat, ob Rippy das + UHD-Preset automatisch nimmt. +- **Sofortmaßnahme am laufenden Job:** `keepOriginal` auf `True` gesetzt + (nur dieses eine Feld, gegengeprüft: kein anderer Schlüssel verändert). + Damit überlebt der 4K-Rohschnitt die Kompression auf jeden Fall. +- **Gebaut:** `preset_fuer(disc_type, einstellungen)` in `ripping.py` (pure, + getestet) plus drei Einstellungen `transcodePresetDvd` / `…Bluray` / + `…Uhd`. Reihenfolge: Preset des Disc-Typs → allgemeines + `transcodePreset` → `DEFAULT_HB_PRESET`. **Bestandsinstallationen ändern + ihr Verhalten nicht**, solange die neuen Felder nicht gespeichert sind. + `transcode_files` holt den Disc-Typ aus dem Job-Datensatz und schreibt ihn + mit ins Log. +- **UI (Einstellungen → Verarbeitung):** drei Auswahlfelder statt einem, mit + Klartext dazu, warum 4K auf ein 2160p-Preset gehört. Alle Preset-Namen + stammen aus `HandBrakeCLI --preset-list` im Worker-Image (1.6.1) — nicht + geraten (AGENTS Regel D). +- **⚠️ Deploy bewusst zurückgehalten:** `docker compose up -d --build` + würde den Worker-Container neu erstellen und den **laufenden Akira-Rip + abbrechen**. Erst deployen, wenn der Job durch ist. Der 4K-Rohschnitt ist + durch `keepOriginal` geschützt; danach reicht „Neu komprimieren" im UI, + um mit dem richtigen Preset in 4K zu komprimieren. +- **Nebenbefund:** `ps` gibt es im Worker-Image nicht (python-slim). Frühere + Prüfungen auf laufende Rips per `ps | grep` lieferten deshalb still + „nichts aktiv" — richtig geht es über `/proc`. + +--- + +## Vorheriger Stand: v3.11 — 4K-UHD GELÖST: Akira geht auf (25.07.2026) - **🎉 Der Durchbruch:** Nach Übernahme des Schlüsselspeichers öffnet `makemkvcon` auf der VM die Akira-UHD: „Operation successfully diff --git a/docker/ui/src/pages/Settings.tsx b/docker/ui/src/pages/Settings.tsx index 12e8d26..3374fed 100644 --- a/docker/ui/src/pages/Settings.tsx +++ b/docker/ui/src/pages/Settings.tsx @@ -25,7 +25,13 @@ interface SettingsState { autoRipStart: boolean notificationWebhook: string transcodeEnabled: boolean + // transcodePreset bleibt als Rückfall für Bestandsinstallationen bestehen — + // solange die drei Felder darunter nicht gespeichert sind, gilt weiter dieser + // Wert (siehe preset_fuer() in docker/worker/ripping.py). transcodePreset: string + transcodePresetDvd: string + transcodePresetBluray: string + transcodePresetUhd: string keepOriginal: boolean mediaServer: string jellyfinUrl: string @@ -49,6 +55,9 @@ const defaultSettings: SettingsState = { notificationWebhook: '', transcodeEnabled: true, transcodePreset: 'H.265 MKV 1080p30', + transcodePresetDvd: 'H.265 MKV 576p25', + transcodePresetBluray: 'H.265 MKV 1080p30', + transcodePresetUhd: 'H.265 MKV 2160p60 4K', keepOriginal: false, mediaServer: 'none', jellyfinUrl: '', @@ -547,16 +556,53 @@ export default function SettingsPage() { /> - + {/* + Ein Preset JE DISC-TYP (25.07.2026). Vorher lief alles durch + dasselbe Preset — eine 4K-UHD wurde damit auf 1080p + heruntergerechnet und eine DVD auf 1080p hochskaliert. + Preset-Namen stammen aus `HandBrakeCLI --preset-list` im + Worker-Image (HandBrake 1.6.1), nicht aus dem Kopf. + */} +
+

+ Ein Preset je Quelle — Rippy erkennt den Disc-Typ selbst und wählt passend aus. + Eine 4K-UHD gehört auf ein + 2160p-Preset, sonst rechnet HandBrake sie auf 1080p herunter und die + 4K-Auflösung ist weg. +

+ + + + + + +
bool: DEFAULT_HB_PRESET = "H.265 MKV 1080p30" +# Ein Preset je Disc-Typ (Befund 25.07.2026). Vorher galt EIN Preset für +# alles: eine 4K-UHD wurde damit auf 1080p heruntergerechnet und eine DVD auf +# 1080p hochskaliert — beides Unsinn, und beim ersten UHD-Rip wäre die 4K- +# Auflösung still verlorengegangen. Den Disc-Typ kennt Rippy ohnehin. +PRESET_SCHLUESSEL = { + "dvd": "transcodePresetDvd", + "bluray": "transcodePresetBluray", + "uhd": "transcodePresetUhd", +} + + +def preset_fuer(disc_type: str, einstellungen: dict) -> str: + """Welches HandBrake-Preset gilt für diesen Disc-Typ? (pure, testbar) + + Reihenfolge: Preset des Disc-Typs → allgemeines transcodePreset → + DEFAULT_HB_PRESET. Diese Kette hält Bestandsinstallationen am Leben, in + denen nur `transcodePreset` gesetzt ist: solange der Nutzer die neuen + Felder nicht speichert, ändert sich sein Verhalten nicht. + """ + einstellungen = einstellungen or {} + schluessel = PRESET_SCHLUESSEL.get(disc_type or "") + if schluessel: + eigen = (einstellungen.get(schluessel) or "").strip() + if eigen: + return eigen + 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: """Baut das HandBrake-Kommando für die Kompressions-Stufe (pure Funktion). diff --git a/docker/worker/tasks.py b/docker/worker/tasks.py index 266b1af..fb78230 100644 --- a/docker/worker/tasks.py +++ b/docker/worker/tasks.py @@ -35,11 +35,11 @@ except ImportError: # Windows: kein fcntl disc_size_bytes = None from ripping import ( - DEFAULT_HB_PRESET, RIP_OUTPUT_DIR, RipAbbruch, lies_datei_dauer, lies_titel_info, + preset_fuer, rip_cd, rip_video, run_handbrake, @@ -565,14 +565,19 @@ def transcode_files(self, job_id: str, raw_dir: str, final_dir: str): return ergebnis einstellungen = db.get_settings() - preset = einstellungen.get("transcodePreset") or DEFAULT_HB_PRESET + # Preset nach Disc-Typ (Befund 25.07.2026): vorher lief JEDE Quelle durch + # dasselbe Preset — eine 4K-UHD wurde damit auf 1080p heruntergerechnet. + job = db.get_job(job_id) or {} + disc_type = job.get("disc_type") or "" + preset = preset_fuer(disc_type, einstellungen) original_behalten = einstellungen.get("keepOriginal", False) os.makedirs(final_dir, exist_ok=True) db.update_job(job_id, status="transcoding", progress=0, error=None) db.add_log( "info", "worker", - f"Job {job_id}: Kompression gestartet ({len(quellen)} Datei(en), Preset '{preset}')", + f"Job {job_id}: Kompression gestartet ({len(quellen)} Datei(en), " + f"Disc-Typ '{disc_type or 'unbekannt'}', Preset '{preset}')", ) anzahl = len(quellen) diff --git a/docker/worker/test_ripping_helpers.py b/docker/worker/test_ripping_helpers.py index d927a8c..af47e77 100644 --- a/docker/worker/test_ripping_helpers.py +++ b/docker/worker/test_ripping_helpers.py @@ -138,3 +138,43 @@ def test_abcde_config_enthaelt_zielverzeichnis(): 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 fuer alles — eine 4K-UHD wurde + # damit auf 1080p heruntergerechnet, und beim ersten echten UHD-Rip waere + # 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 aendern. + 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 zaehlt 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