diff --git a/backend/src/db/defaultSettings.js b/backend/src/db/defaultSettings.js index 4e24ad1..a79a1cc 100644 --- a/backend/src/db/defaultSettings.js +++ b/backend/src/db/defaultSettings.js @@ -221,6 +221,18 @@ const defaultSchema = [ validation: {}, orderIndex: 320 }, + { + key: 'handbrake_restart_delete_incomplete_output', + category: 'Tools', + label: 'Encode-Neustart: unvollständige Ausgabe löschen', + type: 'boolean', + required: 1, + description: 'Wenn aktiv, wird bei "Encode neu starten" der bisherige (nicht erfolgreiche) Output vor Start entfernt.', + defaultValue: 'true', + options: [], + validation: {}, + orderIndex: 325 + }, { key: 'output_extension', category: 'Tools', diff --git a/backend/src/services/historyService.js b/backend/src/services/historyService.js index 368ef26..b7c9f3e 100644 --- a/backend/src/services/historyService.js +++ b/backend/src/services/historyService.js @@ -210,6 +210,8 @@ function enrichJobRow(job) { const omdbInfo = parseJsonSafe(job.omdb_json, null); const encodePlan = parseJsonSafe(job.encode_plan_json, null); const mediaType = inferMediaType(job, makemkvInfo, mediainfoInfo, encodePlan); + const backupSuccess = String(makemkvInfo?.status || '').trim().toUpperCase() === 'SUCCESS'; + const encodeSuccess = String(handbrakeInfo?.status || '').trim().toUpperCase() === 'SUCCESS'; return { ...job, @@ -219,6 +221,8 @@ function enrichJobRow(job) { omdbInfo, encodePlan, mediaType, + backupSuccess, + encodeSuccess, rawStatus, outputStatus, movieDirStatus @@ -959,17 +963,34 @@ class HistoryService { } else if (!fs.existsSync(job.output_path)) { summary.movie.reason = 'Movie-Datei/Pfad existiert nicht.'; } else { - const stat = fs.lstatSync(job.output_path); + const outputPath = normalizeComparablePath(job.output_path); + const movieRoot = normalizeComparablePath(settings.movie_dir); + const stat = fs.lstatSync(outputPath); if (stat.isDirectory()) { - const result = deleteFilesRecursively(job.output_path, true); + const keepRoot = outputPath === movieRoot; + const result = deleteFilesRecursively(outputPath, keepRoot ? true : false); summary.movie.deleted = true; summary.movie.filesDeleted = result.filesDeleted; summary.movie.dirsRemoved = result.dirsRemoved; } else { - fs.unlinkSync(job.output_path); - summary.movie.deleted = true; - summary.movie.filesDeleted = 1; - summary.movie.dirsRemoved = 0; + const parentDir = normalizeComparablePath(path.dirname(outputPath)); + const canDeleteParentDir = parentDir + && parentDir !== movieRoot + && isPathInside(movieRoot, parentDir) + && fs.existsSync(parentDir) + && fs.lstatSync(parentDir).isDirectory(); + + if (canDeleteParentDir) { + const result = deleteFilesRecursively(parentDir, false); + summary.movie.deleted = true; + summary.movie.filesDeleted = result.filesDeleted; + summary.movie.dirsRemoved = result.dirsRemoved; + } else { + fs.unlinkSync(outputPath); + summary.movie.deleted = true; + summary.movie.filesDeleted = 1; + summary.movie.dirsRemoved = 0; + } } } } diff --git a/backend/src/services/pipelineService.js b/backend/src/services/pipelineService.js index 68808ce..86fed87 100644 --- a/backend/src/services/pipelineService.js +++ b/backend/src/services/pipelineService.js @@ -4894,8 +4894,31 @@ class PipelineService extends EventEmitter { throw error; } + const settings = await settingsService.getSettingsMap(); + const restartDeleteIncompleteOutput = settings?.handbrake_restart_delete_incomplete_output !== undefined + ? Boolean(settings.handbrake_restart_delete_incomplete_output) + : true; + const handBrakeInfo = this.safeParseJson(job.handbrake_info_json); + const encodePreviouslySuccessful = String(handBrakeInfo?.status || '').trim().toUpperCase() === 'SUCCESS'; const previousOutputPath = String(job.output_path || '').trim() || null; + if (previousOutputPath && restartDeleteIncompleteOutput && !encodePreviouslySuccessful) { + try { + const deleteResult = await historyService.deleteJobFiles(jobId, 'movie'); + await historyService.appendLog( + jobId, + 'USER_ACTION', + `Encode-Neustart: unvollständigen Output vor Start entfernt (movie files=${deleteResult?.summary?.movie?.filesDeleted ?? 0}, dirs=${deleteResult?.summary?.movie?.dirsRemoved ?? 0}).` + ); + } catch (error) { + logger.warn('restartEncodeWithLastSettings:delete-incomplete-output-failed', { + jobId, + outputPath: previousOutputPath, + error: errorToMeta(error) + }); + } + } + await historyService.updateJob(jobId, { status: 'READY_TO_ENCODE', last_state: 'READY_TO_ENCODE', @@ -4908,7 +4931,7 @@ class PipelineService extends EventEmitter { jobId, 'USER_ACTION', previousOutputPath - ? `Encode-Neustart angefordert. Letzte bestätigte Auswahl wird verwendet. Vorheriger Output-Pfad: ${previousOutputPath}` + ? `Encode-Neustart angefordert. Letzte bestätigte Auswahl wird verwendet. Vorheriger Output-Pfad: ${previousOutputPath}. autoDeleteIncomplete=${restartDeleteIncompleteOutput ? 'on' : 'off'}` : 'Encode-Neustart angefordert. Letzte bestätigte Auswahl wird verwendet.' ); diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md index 00d74c7..4531c6d 100644 --- a/docs/getting-started/quickstart.md +++ b/docs/getting-started/quickstart.md @@ -1,144 +1,330 @@ -# Schnellstart +# Schnellstart – Vollständiger Workflow -Nach der [Installation](installation.md) und [Konfiguration](configuration.md) kannst du sofort mit dem ersten Rip beginnen. +Nach der [Installation](installation.md) und [Konfiguration](configuration.md) führt diese Seite Schritt für Schritt durch den ersten Rip – mit allen Details aus dem Code. --- -## 1. Ripster starten +## Übersicht: Pipeline-Zustände + +```mermaid +stateDiagram-v2 + direction LR + [*] --> IDLE + IDLE --> DISC_DETECTED: Disc eingelegt + DISC_DETECTED --> METADATA_SELECTION: Analyse gestartet + METADATA_SELECTION --> READY_TO_START: Metadaten bestätigt\n(keine Obfuskierung) + METADATA_SELECTION --> WAITING_FOR_USER_DECISION: Obfuskierung erkannt\n→ Playlist wählen + WAITING_FOR_USER_DECISION --> READY_TO_START: Playlist bestätigt + READY_TO_START --> MEDIAINFO_CHECK: Vorhandene Raw-Datei\ngefunden + READY_TO_START --> RIPPING: Ripping starten + RIPPING --> MEDIAINFO_CHECK: MakeMKV fertig + MEDIAINFO_CHECK --> READY_TO_ENCODE: Track-Review bereit + READY_TO_ENCODE --> ENCODING: Encode bestätigt + ENCODING --> FINISHED: Erfolg + ENCODING --> ERROR: Fehler + RIPPING --> ERROR: Fehler +``` + +--- + +## Schritt 1 – Ripster starten ```bash cd ripster ./start.sh ``` -Öffne [http://localhost:5173](http://localhost:5173) im Browser. +Öffne [http://localhost:5173](http://localhost:5173) im Browser. Das Dashboard zeigt `IDLE`. --- -## 2. Dashboard +## Schritt 2 – Disc einlegen → `DISC_DETECTED` -Das Dashboard zeigt den aktuellen Pipeline-Status: +Lege eine DVD oder Blu-ray ein. Der `diskDetectionService` pollt das Laufwerk alle `disc_poll_interval_ms` Millisekunden (Standard: 5 Sekunden). -``` -Status: IDLE – Bereit -Warte auf Disc... -``` +**Was passiert im Code:** ---- +- `diskDetectionService` emittiert `disc:inserted` mit Geräteinformationen +- `pipelineService.onDiscInserted()` wird aufgerufen +- Dashboard zeigt Badge **"Neue Disc erkannt"** +- Der **"Analyse starten"**-Button wird aktiv -## 3. Disc einlegen - -Lege eine DVD oder Blu-ray in das Laufwerk ein. Ripster erkennt die Disc automatisch (Polling-Intervall konfigurierbar, Standard: 5 Sekunden) und wechselt in den Status **ANALYZING**. - -!!! tip "Manuelle Analyse" - Falls die Disc nicht automatisch erkannt wird, kann über die API eine manuelle Analyse ausgelöst werden: +!!! tip "Manuelle Auslösung" + Falls die automatische Erkennung nicht greift: ```bash curl -X POST http://localhost:3001/api/pipeline/analyze ``` --- -## 4. Analyse abwarten +## Schritt 3 – Analyse starten → `METADATA_SELECTION` -MakeMKV analysiert die Disc-Struktur. Dieser Vorgang dauert je nach Disc **30 Sekunden bis 5 Minuten**. +Klicke auf **"Analyse starten"** oder warte auf automatischen Start. -Der Fortschritt wird live im Dashboard angezeigt. +**Was passiert im Code:** + +1. Ein neuer Job-Datensatz wird in der Datenbank angelegt (`status: METADATA_SELECTION`) +2. Ripster versucht, den Titel automatisch aus dem Disc-Label/Modell zu ermitteln +3. Mit diesem erkannten Titel wird sofort eine **OMDb-Suche** ausgelöst +4. Der `MetadataSelectionDialog` öffnet sich im Frontend mit den vorgeladenen Suchergebnissen + +**Erkannter Titel:** Der Disc-Label (z. B. `INCEPTION`) wird als Suchbegriff verwendet. Falls kein Label vorhanden, bleibt das Suchfeld leer. --- -## 5. Metadaten auswählen +## Schritt 4 – Metadaten auswählen (`MetadataSelectionDialog`) -Nach der Analyse öffnet sich der **Metadaten-Dialog**: +Der Dialog zeigt vorgeladene OMDb-Suchergebnisse. Du kannst: -1. **Titel suchen**: Gib den Filmtitel in die Suchleiste ein -2. **Ergebnis auswählen**: Wähle den passenden Film aus der OMDb-Liste -3. **Playlist wählen** *(nur Blu-ray)*: Bei Blu-rays mit mehreren Playlists zeigt Ripster eine Analyse der wahrscheinlich korrekten Playlist an -4. **Bestätigen**: Klicke auf "Bestätigen" +### 4a) OMDb-Suchergebnis wählen -!!! info "Playlist-Obfuskierung" - Einige Blu-rays enthalten absichtlich viele Fake-Playlists. Ripster analysiert diese automatisch und schlägt die wahrscheinlich korrekte Playlist vor. +``` +┌─────────────────────────────────────────────────┐ +│ Suche: [Inception ] 🔍 │ +├─────────────────────────────────────────────────┤ +│ ▶ Inception (2010) · Movie · tt1375666 │ +│ Inception: ... · Series · ... │ +├─────────────────────────────────────────────────┤ +│ [Auswahl übernehmen] │ +└─────────────────────────────────────────────────┘ +``` + +- Suche durch Titel anpassen und Enter drücken +- Typ-Filter: `movie` / `series` umschalten möglich +- Einen Eintrag anklicken, dann **"Auswahl übernehmen"** + +### 4b) Manuelle Eingabe (ohne OMDb) + +Falls kein passendes Ergebnis gefunden wird: +- Titel, Jahr und IMDb-ID manuell eingeben +- OMDb-Poster wird übersprungen + +**Was passiert nach Bestätigung:** + +Ripster ruft `pipelineService.selectMetadata()` auf und führt sofort eine **Playlist-Analyse** durch: + +- MakeMKV wird im Info-Modus gestartet +- Alle Titel und deren Segment-Reihenfolgen werden analysiert +- Das Ergebnis entscheidet über den nächsten Zustand (→ Schritt 5) --- -## 6. Ripping starten +## Schritt 5 – Playlist-Situation (zwei Wege) -Nach der Metadaten-Auswahl wechselt der Status zu **READY_TO_START**. +### 5a) Keine Obfuskierung → `READY_TO_START` -Klicke auf **"Starten"** – MakeMKV beginnt mit dem Ripping. +Der Dialog schließt sich automatisch. Die empfohlene Playlist wird still übernommen. Weiter zu **Schritt 6**. + +### 5b) Obfuskierung erkannt → `WAITING_FOR_USER_DECISION` + +Der **Playlist-Auswahl-Dialog** erscheint **zusätzlich** (nach dem Metadaten-Dialog): + +``` +┌───────────────────────────────────────────────────────────────┐ +│ Playlist-Auswahl │ +│ Es wurden mehrere Titel mit ähnlicher Laufzeit gefunden. │ +│ Bitte wähle die korrekte Playlist: │ +├───────────┬──────────┬────────┬──────────────────────────────┤ +│ Playlist │ Laufzeit │ Score │ Bewertung │ +├───────────┼──────────┼────────┼──────────────────────────────┤ +│ ● 00800 │ 2:28:05 │ +18 │ wahrscheinlich korrekt │ +│ │ │ │ (lineare Segmentfolge) │ +├───────────┼──────────┼────────┼──────────────────────────────┤ +│ ○ 00801 │ 2:28:12 │ −4 │ Auffällige Segmentreihenfolge │ +├───────────┼──────────┼────────┼──────────────────────────────┤ +│ ○ 00900 │ 2:28:05 │ −32 │ Fake-Struktur │ +│ │ │ │ (alternierendes Sprungmuster) │ +└───────────┴──────────┴────────┴──────────────────────────────┘ + 847 Playlists insgesamt · 3 relevante Kandidaten (≥ 15 min) + Empfehlung: 00800 (vorausgewählt) + [Playlist bestätigen] +``` + +- Die empfohlene Playlist ist **vorausgewählt** (Radio-Button) +- Score und Bewertungslabel helfen bei der Entscheidung +- Nach Bestätigung: Pipeline wechselt zu `READY_TO_START` + +!!! info "Scoring-Details" + Wie die Scores berechnet werden, erklärt die [Playlist-Analyse](../pipeline/playlist-analysis.md)-Seite. + +--- + +## Schritt 6 – Ripping starten → `RIPPING` + +**Vorher prüft Ripster:** Existiert bereits eine Raw-Datei für diesen Job? + +- **Ja, Raw-Datei vorhanden** → Direkt zu Schritt 7 (Track-Review), kein erneutes Ripping +- **Nein** → MakeMKV-Ripping startet + +Klicke auf **"Starten"** im Dashboard. + +**Was MakeMKV ausführt (MKV-Modus):** + +```bash +makemkvcon mkv disc:0 all /mnt/raw/Inception-2010/ \ + --minlength=900 -r +``` + +**Was MakeMKV ausführt (Backup-Modus):** + +```bash +makemkvcon backup disc:0 /mnt/raw/Inception-2010-backup/ \ + --decrypt -r +``` + +**Live-Fortschritt** wird aus der MakeMKV-Ausgabe geparst: + +``` +PRGV:2048,0,65536 → Fortschritt wird berechnet und per WebSocket gesendet +PRGT:5011,0,"Sichern..." → Aktueller Task-Name +``` **Typische Dauer:** -- DVD: 20–40 Minuten +- DVD: 20–45 Minuten - Blu-ray: 45–120 Minuten --- -## 7. Encode-Review +## Schritt 7 – Track-Review → `READY_TO_ENCODE` -Nach dem Ripping analysiert MediaInfo die Track-Struktur. Im **Encode-Review** kannst du: +Nach dem Ripping (oder direkt bei vorhandener Raw-Datei) startet der **HandBrake-Scan**: -- **Audio-Tracks** auswählen (z. B. Deutsch + Englisch) -- **Untertitel-Tracks** auswählen -- Überflüssige Tracks deaktivieren +```bash +HandBrakeCLI --scan -i -t 0 +``` -Klicke auf **"Encodierung bestätigen"**. +Dieser Scan liest alle Tracks aus ohne zu encodieren. Ripster baut daraus den Encode-Plan mit automatischer Vorauswahl: + +**Status: `MEDIAINFO_CHECK`** – läuft automatisch, kein Benutzereingriff + +Danach öffnet sich das **Encode-Review-Panel** (`READY_TO_ENCODE`): + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Encode-Review │ +│ Titel: Disc Title 1 · Laufzeit: 2:28:05 · 28 Kapitel │ +├─────────────────────────────────────────────────────────────────┤ +│ Audio-Spuren │ +├──────┬─────────────────────────────┬───────────────────────────┤ +│ ☑ │ Track 1: English (AC3, 5.1) │ Copy (ac3) │ +│ ☑ │ Track 2: Deutsch (DTS, 5.1) │ Fallback Transcode (av_aac)│ +│ ☐ │ Track 3: Français (AC3, 2.0) │ Nicht übernommen │ +├──────┴─────────────────────────────┴───────────────────────────┤ +│ Untertitel-Spuren │ +├──────┬─────────────────────────────┬────────┬──────┬──────────┤ +│ ☑ │ Track 1: Deutsch │ Einbr.☐ │Forc.☐│Default☑ │ +│ ☐ │ Track 2: English │ Einbr.☐ │Forc.☐│Default☐ │ +├──────┴─────────────────────────────┴────────┴──────┴──────────┤ +│ [Encode bestätigen] │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### Audio-Track-Aktionen verstehen + +| Symbol/Text | Bedeutung | +|------------|-----------| +| `Copy (ac3)` | Track wird **verlustfrei** direkt übernommen | +| `Copy (truehd)` | TrueHD-Track wird direkt übernommen | +| `Transcode (av_aac)` | Track wird zu AAC umgewandelt | +| `Fallback Transcode (av_aac)` | Copy nicht möglich → automatisch zu AAC | +| `Preset-Default (HandBrake)` | HandBrake-Preset entscheidet | +| `Nicht übernommen` | Track ist nicht ausgewählt | + +### Untertitel-Flags + +| Flag | Bedeutung | +|------|-----------| +| **Einbrennen** | Untertitel werden fest ins Video gebrannt (nur ein Track möglich) | +| **Forced** | Nur erzwungene Untertitel-Einblendungen übernehmen | +| **Default** | Diese Spur wird beim Abspielen automatisch aktiviert | + +### Vorauswahl-Regeln + +Die Tracks mit `☑` wurden nach der Regel aus den Einstellungen automatisch vorausgewählt (`selectedByRule: true`). Die Auswahl kann frei geändert werden. + +Klicke **"Encode bestätigen"** um fortzufahren. --- -## 8. Encoding +## Schritt 8 – Encoding → `ENCODING` -HandBrake encodiert die Datei mit dem konfigurierten Preset. +HandBrake startet mit dem finalisierten Plan: -**Fortschrittsanzeige:** -- Aktueller Prozentsatz +```bash +HandBrakeCLI \ + -i /dev/sr0 \ + -o "/mnt/movies/Inception (2010).mkv" \ + -t 1 \ + --preset "H.265 MKV 1080p30" \ + -a 1,2 \ + -E copy:ac3,av_aac \ + -s 1 \ + --subtitle-default 1 +``` + +**Live-Fortschritt** wird aus HandBrake-stderr geparst: + +``` +Encoding: task 1 of 1, 73.50 % (45.23 fps, avg 44.12 fps, ETA 00h12m34s) +``` + +Das Dashboard zeigt: +- Fortschrittsbalken (0–100 %) +- Aktuelle Encoding-Geschwindigkeit (FPS) - Geschätzte Restzeit (ETA) -- Encoding-Geschwindigkeit (FPS) + +**Typische Dauer (abhängig von CPU/GPU und Preset):** +- Schnelles Preset (`fast`): 0.5× Echtzeit +- Standard-Preset: 1–3× Echtzeit +- Langsames Preset (`slow`): 5–10× Echtzeit --- -## 9. Fertig! - -Status wechselt zu **FINISHED**. Die encodierte Datei liegt im konfigurierten `movie_dir`. +## Schritt 9 – Fertig! → `FINISHED` ``` /mnt/nas/movies/ -└── Inception (2010).mkv ← Fertige Datei +└── Inception (2010).mkv ✓ Encodierung abgeschlossen ``` -!!! success "PushOver-Benachrichtigung" - Falls PushOver konfiguriert ist, erhältst du eine Push-Benachrichtigung auf dein Mobilgerät. +- Job-Status in der Datenbank: `FINISHED` +- PushOver-Benachrichtigung (falls konfiguriert) +- Eintrag in der [History](http://localhost:5173/history) mit vollständigen Logs --- -## Workflow-Zusammenfassung +## Fehlerbehandlung -``` -Disc einlegen - ↓ -ANALYZING (MakeMKV analysiert) - ↓ -METADATA_SELECTION (Titel & Playlist wählen) - ↓ -READY_TO_START → [Starten] - ↓ -RIPPING (MakeMKV rippt) - ↓ -MEDIAINFO_CHECK (Track-Analyse) - ↓ -READY_TO_ENCODE → [Bestätigen] - ↓ -ENCODING (HandBrake encodiert) - ↓ -FINISHED ✓ -``` +### Job im Status `ERROR` + +1. **Dashboard**: Details-Button → Log-Ausgabe prüfen +2. **Retry**: Job vom Fehlerzustand neu starten (behält Metadaten) +3. **History**: Vollständige Logs und Fehlerdetails + +### Häufige Fehlerursachen + +| Fehler | Ursache | Lösung | +|-------|---------|--------| +| MakeMKV: Lizenzfehler | Abgelaufene Beta-Lizenz | Neue Lizenz im [MakeMKV-Forum](https://www.makemkv.com/forum/viewtopic.php?t=1053) | +| HandBrake: Preset nicht gefunden | Preset-Name falsch | `HandBrakeCLI --preset-list` prüfen | +| Keine Disc erkannt | Laufwerk-Berechtigungen | `sudo chmod a+rw /dev/sr0` | +| Falsches Video (zerstückelt) | Falsche Playlist | Job re-encodieren mit anderer Playlist | +| OMDb: Keine Ergebnisse | API-Key fehlt oder Titel nicht gefunden | Einstellungen prüfen; manuell eingeben | --- -## Was tun bei Fehlern? +## Kurzübersicht aller Schritte -Falls ein Job in den Status **ERROR** wechselt: - -1. Klicke auf **"Details"** im Dashboard -2. Prüfe die Log-Ausgabe -3. Klicke auf **"Retry"** um den Job erneut zu versuchen - -Logs findest du auch in der [History-Seite](http://localhost:5173/history). +| # | Status | Benutzeraktion | Was Ripster tut | +|--|--------|---------------|----------------| +| 1 | `IDLE` | Disc einlegen | Disc-Polling erkennt Disc | +| 2 | `DISC_DETECTED` | "Analyse starten" klicken | Job anlegen, OMDb vorsuchen | +| 3 | `METADATA_SELECTION` | Film im Dialog auswählen | Playlist-Analyse durchführen | +| 4a | `READY_TO_START` | — | Empfehlung automatisch übernommen | +| 4b | `WAITING_FOR_USER_DECISION` | Playlist manuell wählen | Auf Bestätigung warten | +| 5 | `READY_TO_START` | "Starten" klicken | MakeMKV-Ripping starten | +| 6 | `RIPPING` | Warten | MakeMKV rippt, Fortschritt streamen | +| 7 | `MEDIAINFO_CHECK` | Warten | HandBrake-Scan, Encode-Plan bauen | +| 8 | `READY_TO_ENCODE` | Tracks prüfen + bestätigen | Auswahl in Plan übernehmen | +| 9 | `ENCODING` | Warten | HandBrake encodiert, Fortschritt streamen | +| 10 | `FINISHED` | — | Datei fertig, Benachrichtigung senden | diff --git a/docs/pipeline/encoding.md b/docs/pipeline/encoding.md index 179e18d..a182ebc 100644 --- a/docs/pipeline/encoding.md +++ b/docs/pipeline/encoding.md @@ -1,64 +1,221 @@ -# Encode-Planung +# Encode-Planung & Track-Auswahl -`encodePlan.js` analysiert die MediaInfo-Ausgabe und erstellt einen strukturierten Encode-Plan mit Track-Auswahl. +`encodePlan.js` analysiert die HandBrake-Scan-Ausgabe, wählt Audio- und Untertitelspuren anhand von Regeln vor und erstellt einen vollständigen Encode-Plan für die Benutzer-Review. --- -## Ablauf +## Ablauf im Pipeline-Kontext ``` -MediaInfo-JSON - ↓ -Track-Parsing (Video, Audio, Untertitel) - ↓ -Sprach-Normalisierung (ISO 639-1 → 639-3) - ↓ -Codec-Klassifizierung (copy-kompatibel / transcode) - ↓ -Encode-Plan generieren - ↓ -Benutzer-Review im Frontend - ↓ -HandBrake-CLI-Argumente aufbauen +RIPPING abgeschlossen (oder Pre-Rip-Scan) + ↓ +HandBrake --scan (alle Titel & Tracks einlesen) + ↓ +buildTrackSelectors() ← Regeln aus Einstellungen ableiten + ↓ +selectTrackIds() ← Tracks anhand Regeln vorauswählen + ↓ +resolveAudioEncoderAction() ← Encoder-Aktion pro Track bestimmen + ↓ +buildDiscScanReview() ← Vollständigen Encode-Plan erstellen + ↓ +READY_TO_ENCODE ← Benutzer-Review im Frontend + ↓ +applyManualTrackSelectionToPlan() ← Benutzer-Auswahl anwenden + ↓ +ENCODING ← HandBrake-CLI mit finalem Plan starten ``` --- -## Encode-Plan-Format +## Phase 1: Pre-Rip Track-Scan -Der generierte Plan wird als JSON im Job-Datensatz gespeichert: +Ripster führt einen **HandBrake-Scan** bereits **vor dem eigentlichen Ripping** durch: + +```bash +HandBrakeCLI --scan -i /dev/sr0 -t 0 +``` + +Dieser Scan liest alle Titel und deren Tracks aus der Disc (ohne zu encodieren). So kann der Benutzer die Track-Auswahl bereits vor dem zeitintensiven Rip-Prozess bestätigen. + +!!! info "Pre-Rip vs. Post-Rip" + Ob der Scan vor oder nach dem Ripping passiert, hängt vom konfigurierten Modus ab. Bei direktem Disc-Zugriff ist Pre-Rip möglich; nach einem MakeMKV-Backup wird die entstandene `.mkv`-Datei gescannt. + +--- + +## Phase 2: Track-Selektor-Regeln (`buildTrackSelectors`) + +Die Regeln werden aus den HandBrake-Einstellungen abgeleitet. Es gibt fünf **Selektionsmodi**: + +| Modus | Beschreibung | +|------|-------------| +| `none` | Keine Tracks dieser Art übernehmen | +| `first` | Nur den ersten Track übernehmen | +| `all` | Alle Tracks übernehmen | +| `language` | Nur Tracks in bestimmten Sprachen | +| `explicit` | Bestimmte Track-IDs explizit angeben | + +Der aktive Modus wird aus den `handbrake_*`-Einstellungen und `handbrake_extra_args` abgeleitet. Explizite CLI-Argumente (`--audio`, `--audio-lang-list`) überschreiben die Basis-Konfiguration. + +--- + +## Phase 3: Automatische Vorauswahl (`selectTrackIds`) + +### Audio-Tracks + +``` +Modus 'none' → Keine Audio-Tracks +Modus 'all' → Alle Tracks (oder nur erster, wenn firstOnly) +Modus 'language' → Alle Tracks in den konfigurierten Sprachen +Modus 'explicit' → Nur die angegebenen Track-IDs +Modus 'first' → Nur Track 1 +``` + +Jeder Audio-Track erhält das Feld `selectedByRule: true/false` – dieses zeigt dem Benutzer, welche Tracks automatisch vorausgewählt wurden. + +**Sprach-Normalisierung (`normalizeLanguage`):** + +Alle Sprachcodes werden auf **ISO 639-2** (3-Buchstaben) normalisiert: + +| Eingabe | Normalisiert | +|--------|-------------| +| `de`, `ger` | `deu` | +| `German` | `deu` | +| `en`, `eng` | `eng` | +| `English` | `eng` | +| `fr`, `fre` | `fra` | +| `ja`, `jpn` | `jpn` | +| Unbekannt | `und` | + +### Untertitel-Tracks + +Gleiche Modus-Logik wie Audio, aber mit **zusätzlichen Flags** pro Track: + +| Flag | Bedeutung | +|------|-----------| +| `burnIn` | Untertitel in Video einbrennen (`--subtitle-burned`) | +| `forced` | Nur erzwungene Untertitel übernehmen (`--subtitle-forced`) | +| `defaultTrack` | Als Standard-Untertitelspur markieren (`--subtitle-default`) | + +Diese Flags werden im Encode-Review als Checkboxen angezeigt. + +--- + +## Phase 4: Encoder-Aktion bestimmen (`resolveAudioEncoderAction`) + +Für jeden vorausgewählten Audio-Track bestimmt Ripster die Encoder-Aktion: + +``` +Encoder-Einstellung Codec-Support in Copy-Mask? Aktion +───────────────────────────────────────────────────────────────────── +Kein Encoder / 'preset-default' → preset-default HandBrake-Preset entscheidet +encoder.startsWith('copy') + UND Codec in audioCopyMask → copy Direktkopie (verlustfrei) + UND Codec NICHT in audioCopyMask→ fallback Transcode mit Fallback-Encoder +sonstiger Encoder → transcode Transcode mit explizitem Encoder +``` + +**Encoder-Aktionstypen:** + +| Typ | Label (UI) | Qualität | +|----|-----------|---------| +| `preset-default` | `Preset-Default (HandBrake)` | HandBrake entscheidet | +| `copy` | `Copy (ac3)` | Verlustfrei | +| `fallback` | `Fallback Transcode (av_aac)` | Mit Qualitätsverlust | +| `transcode` | `Transcode (av_aac)` | Mit Qualitätsverlust | + +**Copy-kompatible Codecs (Standard Copy-Mask):** + +| Codec | Encoder-String | +|-------|---------------| +| AC-3 | `copy:ac3` | +| E-AC-3 | `copy:eac3` | +| AAC | `copy:aac` | +| MP3 | `copy:mp3` | +| TrueHD | `copy:truehd` | +| DTS | `copy:dts` *(nur mit spez. HandBrake-Build)* | +| DTS-HD | `copy:dtshd` *(nur mit spez. HandBrake-Build)* | + +!!! warning "DTS im Standard-HandBrake" + Standard-HandBrake-Builds unterstützen kein DTS-Passthrough. DTS-Tracks werden dann automatisch auf den Fallback-Encoder umgestellt (Standard: `av_aac`). + +--- + +## Phase 5: Encode-Plan-Struktur + +Der vollständige Plan wird im Job-Datensatz als `encode_plan_json` gespeichert: ```json { - "inputFile": "/mnt/raw/Inception_t00.mkv", - "outputFile": "/mnt/movies/Inception (2010).mkv", - "preset": "H.265 MKV 1080p30", - "audioTracks": [ + "mode": "pre_rip", + "preRip": true, + "encodeInputTitleId": 1, + "encodeInputPath": "disc-track-scan://title-1", + "selectors": { + "audio": { "mode": "language", "languages": ["deu", "eng"], "copyMask": ["copy:ac3", "copy:eac3"] }, + "subtitle": { "mode": "none" } + }, + "titles": [ { - "index": 1, - "codec": "dts", - "language": "deu", - "channels": 6, - "label": "Deutsch (DTS, 5.1)", - "copyCompatible": false, - "selected": true - }, - { - "index": 2, - "codec": "truehd", - "language": "eng", - "channels": 8, - "label": "English (TrueHD, 7.1)", - "copyCompatible": true, - "selected": true - } - ], - "subtitleTracks": [ - { - "index": 1, - "language": "deu", - "label": "Deutsch", - "selected": true + "id": 1, + "fileName": "Disc Title 1", + "durationSeconds": 8885, + "selectedByMinLength": true, + "isEncodeInput": true, + "audioTracks": [ + { + "id": 1, + "sourceTrackId": 1, + "language": "eng", + "languageLabel": "English", + "title": "5.1 Surround", + "format": "AC3", + "codecToken": "ac3", + "channels": "6", + "selectedByRule": true, + "selectedForEncode": true, + "encodePreviewActions": [ + { "type": "copy", "encoder": "copy:ac3", "label": "Copy (ac3)" } + ], + "encodePreviewSummary": "Copy (ac3)" + }, + { + "id": 2, + "sourceTrackId": 2, + "language": "deu", + "languageLabel": "Deutsch", + "format": "DTS", + "codecToken": "dts", + "channels": "6", + "selectedByRule": true, + "selectedForEncode": true, + "encodePreviewActions": [ + { "type": "fallback", "encoder": "av_aac", "label": "Fallback Transcode (av_aac)" } + ], + "encodePreviewSummary": "Fallback Transcode (av_aac)" + }, + { + "id": 3, + "language": "fra", + "languageLabel": "Français", + "selectedByRule": false, + "selectedForEncode": false, + "encodePreviewSummary": "Nicht übernommen" + } + ], + "subtitleTracks": [ + { + "id": 1, + "language": "deu", + "selectedByRule": true, + "selectedForEncode": true, + "burnIn": false, + "forced": false, + "defaultTrack": true, + "subtitlePreviewSummary": "Übernehmen", + "subtitlePreviewFlags": ["default"] + } + ] } ] } @@ -66,94 +223,107 @@ Der generierte Plan wird als JSON im Job-Datensatz gespeichert: --- -## Sprach-Normalisierung +## Phase 6: Benutzer-Review im Frontend (`MediaInfoReviewPanel`) -MediaInfo liefert Sprachcodes in verschiedenen Formaten. `encodePlan.js` normalisiert diese auf **ISO 639-3**: +Das Review-Panel zeigt: -| MediaInfo-Output | Normalisiert | -|----------------|-------------| -| `de` | `deu` | -| `German` | `deu` | -| `en` | `eng` | -| `English` | `eng` | -| `fr` | `fra` | -| `ja` | `jpn` | +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Encode-Review Titel: Disc Title 1 │ +│ Laufzeit: 2:28:05 │ +├─────────────────────────────────────────────────────────────────┤ +│ Audio-Spuren │ +├──────┬──────────────────────────┬──────────────────────────────┤ +│ [✓] │ Track 1: English (AC3) │ Copy (ac3) │ +│ [✓] │ Track 2: Deutsch (DTS) │ Fallback Transcode (av_aac) │ +│ [ ] │ Track 3: Français (DTS) │ Nicht übernommen │ +├──────┴──────────────────────────┴──────────────────────────────┤ +│ Untertitel-Spuren │ +├──────┬──────────────────────────┬────────┬────────┬────────────┤ +│ [✓] │ Track 1: Deutsch │Einbr.[ ]│Forced[ ]│Default[✓]│ +│ [ ] │ Track 2: English │Einbr.[ ]│Forced[ ]│Default[ ]│ +├──────┴──────────────────────────┴────────┴────────┴────────────┤ +│ [Encode bestätigen] │ +└─────────────────────────────────────────────────────────────────┘ +``` + +Der Benutzer kann: +- **Audio-Tracks** per Checkbox aktivieren/deaktivieren +- **Untertitel-Flags** (Einbrennen, Forced, Default) setzen +- **Mehrere Titel** bei der Titleauswahl wechseln (für Discs mit mehreren Haupttiteln) --- -## Codec-Klassifizierung +## Phase 7: Benutzer-Auswahl anwenden (`applyManualTrackSelectionToPlan`) -HandBrake kann einige Codecs direkt kopieren (ohne Transcoding): +Nach "Encode bestätigen" wird die Benutzer-Auswahl auf den Plan angewendet: -| Codec | Copy-kompatibel | HandBrake-Encoder | -|-------|----------------|------------------| -| `ac3` | ✅ Ja | `copy:ac3` | -| `aac` | ✅ Ja | `copy:aac` | -| `mp3` | ✅ Ja | `copy:mp3` | -| `truehd` | ✅ Ja | `copy:truehd` | -| `eac3` | ✅ Ja | `copy:eac3` | -| `dts` | ❌ Nein | `ffaac` (transcode) | -| `dtshd` | ❌ Nein | `ffaac` (transcode) | +```json +Payload: { + "selectedEncodeTitleId": 1, + "selectedTrackSelection": { + "1": { + "audioTrackIds": [1, 2], + "subtitleTrackIds": [1] + } + } +} +``` -!!! info "DTS-Transcoding" - HandBrake unterstützt kein DTS-Passthrough in den Standard-Builds. DTS-Tracks werden zu AAC transcodiert, es sei denn, du verwendest einen speziellen HandBrake-Build mit DTS-Unterstützung. +Jeder Track erhält `selectedForEncode: true/false` entsprechend der Auswahl. Die Encoder-Aktionen (`encodeActions`) der nicht gewählten Tracks werden geleert. --- -## HandBrake-CLI-Argumente +## Phase 8: HandBrake-CLI-Befehl -Aus dem Encode-Plan generiert `commandLine.js` die HandBrake-Argumente: +Aus dem finalisierten Plan baut Ripster den HandBrake-Aufruf: ```bash HandBrakeCLI \ - --input "/mnt/raw/Inception_t00.mkv" \ - --output "/mnt/movies/Inception (2010).mkv" \ + -i /dev/sr0 \ + -o "/mnt/movies/Inception (2010).mkv" \ + -t 1 \ --preset "H.265 MKV 1080p30" \ - --audio 1,2 \ - --aencoder copy:truehd,ffaac \ - --subtitle 1 \ + -a 1,2 \ + -E copy:ac3,av_aac \ + -s 1 \ --subtitle-default 1 ``` -### Zusätzliche Argumente - -Über die Einstellung `handbrake_extra_args` können beliebige HandBrake-Argumente ergänzt werden: - -``` ---crop 0:0:0:0 --loose-anamorphic -``` +| Argument | Quelle | +|---------|--------| +| `-i` | `encode_input_path` aus Job | +| `-o` | Ausgabepfad aus `filename_template` + `movie_dir` | +| `-t` | Gewählter Titel-Index | +| `-a` | Kommagetrennte Audio-Track-IDs der ausgewählten Tracks | +| `-E` | Kommagetrennte Encoder-Aktionen (eine pro Track, gleiche Reihenfolge wie `-a`) | +| `-s` | Kommagetrennte Untertitel-Track-IDs | +| `--subtitle-default` | Track-ID der als Default markierten Untertitelspur | +| `--preset` | `handbrake_preset`-Einstellung | +| Extras | `handbrake_extra_args`-Einstellung | --- ## Dateiname-Template -Die Ausgabedatei wird über das konfigurierte Template benannt: +| Platzhalter | Wert | Beispiel | +|------------|------|---------| +| `{title}` | Filmtitel von OMDb | `Inception` | +| `{year}` | Erscheinungsjahr | `2010` | +| `{imdb_id}` | IMDb-ID | `tt1375666` | +| `{type}` | `movie` oder `series` | `movie` | -``` -Template: {title} ({year}) -Ergebnis: Inception (2010).mkv -``` - -Verfügbare Platzhalter: - -| Platzhalter | Wert | -|------------|------| -| `{title}` | Filmtitel von OMDb | -| `{year}` | Erscheinungsjahr | -| `{imdb_id}` | IMDb-ID (z.B. `tt1375666`) | -| `{type}` | `movie` oder `series` | - -Sonderzeichen im Dateinamen werden automatisch sanitisiert (`:`, `/`, `?` etc. werden entfernt oder ersetzt). +Sonderzeichen (`:`, `/`, `?`, `*` etc.) werden automatisch aus dem Dateinamen entfernt. --- ## Re-Encoding -Abgeschlossene Jobs können mit geänderten Einstellungen neu encodiert werden: +Ein abgeschlossener Job kann ohne erneutes Ripping neu encodiert werden: -1. Job in der History auswählen -2. "Re-Encode" klicken -3. Neue Track-Auswahl treffen (oder bestehende übernehmen) -4. Encoding startet mit aktuellen Einstellungen +1. Job in der **History** öffnen +2. **"Re-Encode"** klicken +3. Track-Auswahl anpassen (oder bestehende übernehmen) +4. Encoding startet mit den aktuellen `handbrake_*`-Einstellungen -Dies ist nützlich, wenn sich das HandBrake-Preset oder die Track-Auswahl geändert hat, ohne die zeitintensive Ripping-Phase zu wiederholen. +Nützlich bei geänderten Presets, anderen Sprach-Präferenzen oder nach einem Einstellungs-Update. diff --git a/docs/pipeline/playlist-analysis.md b/docs/pipeline/playlist-analysis.md index 2768081..7397d4b 100644 --- a/docs/pipeline/playlist-analysis.md +++ b/docs/pipeline/playlist-analysis.md @@ -1,119 +1,209 @@ # Playlist-Analyse -Einige Blu-rays verwenden **Playlist-Obfuskierung** als Kopierschutz-Mechanismus. Ripster erkennt dieses Muster und hilft bei der Auswahl der korrekten Playlist. +Einige Blu-rays verwenden **Playlist-Obfuskierung** als Kopierschutz. Ripster analysiert automatisch alle MakeMKV-Titel und empfiehlt die korrekte Playlist – auf Basis eines Segment-Scoring-Algorithmus aus `playlistAnalysis.js`. --- ## Das Problem: Playlist-Obfuskierung -Moderne Blu-rays können Hunderte von Playlists enthalten, von denen nur eine den eigentlichen Film enthält. Die anderen sind: +Moderne Blu-rays können Dutzende bis Hunderte von Titeln/Playlists enthalten. Der eigentliche Film steckt in genau einer davon – alle anderen sind: -- **Kurze Dummy-Playlists** (wenige Sekunden bis Minuten) -- **Umgeordnete Segmente** (falsche Reihenfolge der Film-Segmente) -- **Duplizierte Inhalte** (mehrere Playlists mit gleichem Inhalt, verschiedenen Timestamps) +- **Kurze Dummy-Titel** (wenige Sekunden bis Minuten) +- **Titel mit verschachtelten Segmenten** (absichtlich versetzte Reihenfolge, sodass der Film falsch gerippt wird) +- **Titel gleicher Länge** (mehrere Playlists mit identischer Laufzeit, aber unterschiedlicher Segment-Reihenfolge) -Dies macht es schwierig, die korrekte Playlist manuell zu identifizieren. +Das Ziel der Obfuskierung: Ein einfacher Ripper wählt den erstbesten langen Titel – und bekommt ein zerstückeltes, unbrauchbares Video. --- -## Ripsters Analyse-Algorithmus +## Wann wird die Analyse ausgelöst? -`playlistAnalysis.js` analysiert alle von MakeMKV erkannten Playlists nach mehreren Kriterien: - -### 1. Laufzeit-Matching - -Die erwartete Laufzeit (aus OMDb-Metadaten) wird mit der Playlist-Laufzeit verglichen: +Die Playlist-Analyse wird automatisch gestartet **sobald der Benutzer Metadaten bestätigt** (nach dem Metadaten-Dialog). Ripster ruft `makemkvcon` im Info-Modus auf und parst die TINFO-Ausgabe. ``` -Filmtitel: Inception (2010) -OMDb-Laufzeit: 148 Minuten - -Playlist 00800.mpls: 148:22 → ✅ Match -Playlist 00801.mpls: 1:23 → ❌ Zu kurz -Playlist 00900.mpls: 148:25 → ✅ Match (Duplikat?) +TINFO:,26,"" ``` -### 2. Titel-Ähnlichkeit - -Playlists mit Namen, die dem Filmtitel ähneln, werden bevorzugt. - -### 3. Segment-Validierung - -Die Playlist-Segmente werden auf logische Reihenfolge geprüft. - -### 4. Häufigkeits-Analyse - -Bei mehreren Kandidaten: Welche Segment-Kombination kommt am häufigsten vor? +Feld **26** enthält die kommagetrennte Liste der Segment-Nummern in der Abspielreihenfolge des Titels. --- -## Benutzer-Interface +## Algorithmus im Detail (`playlistAnalysis.js`) -Wenn Playlist-Obfuskierung erkannt wird, zeigt Ripster im `MetadataSelectionDialog` eine Playlist-Auswahl: +### Schritt 1 – Segment-Nummern parsen ``` -┌─────────────────────────────────────────────────────┐ -│ Playlist auswählen │ -├─────────────────────────────────────────────────────┤ -│ ★ 00800.mpls 2:28:05 ✓ Empfohlen (Laufzeit passt) │ -│ 00801.mpls 0:01:23 Kurz (wahrscheinlich Menü) │ -│ 00900.mpls 2:28:12 Mögliche Alternative │ -│ 00901.mpls 0:00:45 Kurz │ -│ ... ... ... │ -├─────────────────────────────────────────────────────┤ -│ Hinweis: 847 Playlists gefunden – Analyse empfiehlt │ -│ Playlist 00800.mpls als Hauptfilm. │ -└─────────────────────────────────────────────────────┘ +TINFO:1,26,"00000,00001,00002,00003" → [0, 1, 2, 3] linearer Film +TINFO:2,26,"00100,00050,00100,00051" → [100, 50, 100, 51] Fake-Playlist ``` +### Schritt 2 – Metriken berechnen (`computeSegmentMetrics`) + +Für jedes aufeinanderfolgende Segment-Paar `[a, b]` wird `diff = b − a` berechnet: + +| Metrik | Bedingung | Bedeutung | +|--------|----------|-----------| +| `directSequenceSteps` | `diff == 1` | Aufeinanderfolgende Segmente → linearer Film | +| `backwardJumps` | `b < a` | Rückwärtssprünge → verdächtig | +| `largeJumps` | `\|diff\| > 20` | Große Sprünge → verdächtig | +| `alternatingPairs` | Große Sprünge mit **wechselndem Vorzeichen** | Hin-und-her-Muster → starker Fake-Indikator | + +**Score-Formel:** + +``` +score = (directSequenceSteps × 2) − (backwardJumps × 3) − (largeJumps × 2) +``` + +**Konkrete Beispiele:** + +| Segmentfolge | directSeq | backward | large | score | Ergebnis | +|-------------|-----------|----------|-------|-------|---------| +| `0,1,2,3,4,5` | 5 | 0 | 0 | +10 | Echter Film | +| `0,1,100,2,101,3` | 2 | 0 | 4 | -4 | Verdächtig | +| `50,10,60,11,70,12` | 0 | 3 | 3 | -15 | Fake | + +### Schritt 3 – Bewertungslabel vergeben (`buildEvaluationLabel`) + +``` +alternatingRatio = alternatingPairs / largeJumps + +if alternatingRatio >= 0.55 AND alternatingPairs >= 3: + → "Fake-Struktur (alternierendes Sprungmuster)" + +else if backwardJumps > 0 OR largeJumps > 0: + → "Auffällige Segmentreihenfolge" + +else: + → "wahrscheinlich korrekt (lineare Segmentfolge)" +``` + +### Schritt 4 – Duplikat-Gruppen bilden (`buildSimilarityGroups`) + +Alle Titel werden nach **ähnlicher Laufzeit** gruppiert (±90 Sekunden Toleranz). Gibt es mehrere Kandidaten mit ähnlicher Laufzeit, ist das ein klares Zeichen für Obfuskierung: + +``` +8 Titel mit ~148 Minuten Laufzeit → Duplikat-Gruppe +→ obfuscationDetected = true +→ manualDecisionRequired = true +``` + +### Schritt 5 – Besten Kandidaten empfehlen (`scoreCandidates`) + +Innerhalb der größten Duplikat-Gruppe werden alle Kandidaten sortiert nach: + +1. `score` (höher = besser) +2. `sequenceCoherence` (Anteil linearer Segmentschritte) +3. Laufzeit (länger = besser) +4. Dateigröße (größer = besser als Tiebreaker) + +Der **erste Kandidat** der sortierten Liste ist die Empfehlung. + --- -## Analyse-Ergebnis-Format +## Wann greift der Benutzer ein? + +``` +obfuscationDetected = duplicateDurationGroups.length > 0 +manualDecisionRequired = obfuscationDetected +``` + +| Ergebnis | Nächster Pipeline-Zustand | Aktion | +|---------|--------------------------|--------| +| Keine Duplikat-Gruppen | `READY_TO_START` | Empfehlung wird automatisch übernommen | +| Duplikat-Gruppen gefunden | `WAITING_FOR_USER_DECISION` | Benutzer muss Playlist auswählen | + +--- + +## Benutzeroberfläche: Playlist-Auswahl-Dialog + +Wenn `manualDecisionRequired = true`, öffnet sich der Playlist-Dialog **nach** dem Metadaten-Dialog: + +``` +┌───────────────────────────────────────────────────────────────────┐ +│ Playlist-Auswahl │ +├──────────┬──────────┬──────────┬────────────────────────────────┤ +│ Playlist │ Laufzeit │ Score │ Bewertung │ +├──────────┼──────────┼──────────┼────────────────────────────────┤ +│ ★ 00800 │ 2:28:05 │ +18 │ wahrscheinlich korrekt │ +│ │ │ │ (lineare Segmentfolge) │ +├──────────┼──────────┼──────────┼────────────────────────────────┤ +│ 00801 │ 2:28:12 │ −4 │ Auffällige Segmentreihenfolge │ +├──────────┼──────────┼──────────┼────────────────────────────────┤ +│ 00900 │ 2:28:05 │ −32 │ Fake-Struktur │ +│ │ │ │ (alternierendes Sprungmuster) │ +└──────────┴──────────┴──────────┴────────────────────────────────┘ + Hinweis: 847 Playlists insgesamt. 3 relevante Kandidaten (≥ 15 min). + Empfehlung: 00800 (★) +``` + +- **★** markiert die empfohlene Playlist (vorausgewählt) +- Nur Titel ≥ `makemkv_min_length_minutes` erscheinen in der Liste +- Der Benutzer wählt per Radio-Button und klickt "Bestätigen" +- Erst nach dieser Bestätigung wechselt die Pipeline zu `READY_TO_START` + +--- + +## Vollständige Datenstruktur (`analyzeContext.playlistAnalysis`) ```json { + "titles": [ + { "titleId": 1, "playlistId": "00800", "durationSeconds": 8885, "durationLabel": "2:28:05", "chapters": 28 } + ], "candidates": [ + { "titleId": 1, "playlistId": "00800", "durationSeconds": 8885 }, + { "titleId": 2, "playlistId": "00801", "durationSeconds": 8892 } + ], + "evaluatedCandidates": [ { - "playlist": "00800.mpls", - "duration": "2:28:05", - "durationSeconds": 8885, - "score": 0.95, - "recommended": true, - "reasons": ["Laufzeit stimmt mit OMDb überein", "Häufigste Segment-Kombination"] - }, - { - "playlist": "00900.mpls", - "duration": "2:28:12", - "durationSeconds": 8892, - "score": 0.72, - "recommended": false, - "reasons": ["Ähnliche Laufzeit", "Seltene Segment-Kombination"] + "titleId": 1, + "playlistId": "00800", + "score": 18, + "sequenceCoherence": 0.95, + "evaluationLabel": "wahrscheinlich korrekt (lineare Segmentfolge)", + "metrics": { + "directSequenceSteps": 12, + "backwardJumps": 0, + "largeJumps": 1, + "alternatingPairs": 0 + } } ], - "totalPlaylists": 847, - "recommendation": "00800.mpls" + "duplicateDurationGroups": [ + [ + { "titleId": 1, "playlistId": "00800" }, + { "titleId": 2, "playlistId": "00801" } + ] + ], + "recommendation": { + "titleId": 1, + "playlistId": "00800", + "score": 18, + "reason": "Höchster Segment-Score in der größten Laufzeit-Gruppe" + }, + "obfuscationDetected": true, + "manualDecisionRequired": true } ``` --- -## Manuelle Auswahl +## Konfiguration -Falls die automatische Empfehlung nicht korrekt ist: - -1. Wähle eine andere Playlist aus der Liste -2. Beachte die Laufzeit-Angabe -3. Vergleiche mit der erwarteten Filmlänge (aus OMDb oder Disc-Hülle) - -!!! tip "Tipp" - Bei Blu-rays von bekannten Filmen kannst du die korrekte Playlist oft über Foren wie [MakeMKV-Forum](https://www.makemkv.com/forum/) verifizieren. +| Einstellung | Standard | Wirkung | +|------------|---------|---------| +| `makemkv_min_length_minutes` | `15` | Titel kürzer als dieser Wert werden als Kandidaten ignoriert | --- -## Konfiguration +## Tipps bei Fehlempfehlung -Die Playlist-Analyse ist automatisch aktiv. Einstellbar ist: +!!! tip "Falsche Playlist gewählt?" + Wenn das resultierende Video zerstückelt ist: -| Parameter | Beschreibung | -|----------|-------------| -| `makemkv_min_length_minutes` | Mindestlänge, um als Hauptfilm-Kandidat zu gelten (Standard: 15 Min) | + 1. Job in der **History** öffnen + 2. **Re-Encode** starten – diesmal eine andere Playlist wählen + 3. Alternativ: Korrekte Playlist im [MakeMKV-Forum](https://www.makemkv.com/forum/) recherchieren + +!!! info "Keine Segment-Daten verfügbar" + Bei DVDs oder älteren Blu-rays liefert MakeMKV manchmal keine Segmentinfos (TINFO-Feld 26 fehlt). In diesem Fall entfällt die Analyse und der erste Titel über der Mindestlänge wird automatisch verwendet. diff --git a/frontend/src/components/JobDetailDialog.jsx b/frontend/src/components/JobDetailDialog.jsx index 57ce7d7..7405bfc 100644 --- a/frontend/src/components/JobDetailDialog.jsx +++ b/frontend/src/components/JobDetailDialog.jsx @@ -20,6 +20,7 @@ export default function JobDetailDialog({ onLoadLog, logLoadingMode = null, onAssignOmdb, + onRestartEncode, onReencode, onDeleteFiles, onDeleteEntry, @@ -32,7 +33,15 @@ export default function JobDetailDialog({ const running = ['ANALYZING', 'RIPPING', 'MEDIAINFO_CHECK', 'ENCODING'].includes(job?.status); const showFinalLog = !running; const canReencode = !!(job?.rawStatus?.exists && job?.rawStatus?.isEmpty !== true && mkDone && !running); - const canDeleteEntry = !running; + const hasConfirmedPlan = Boolean( + job?.encodePlan + && Array.isArray(job?.encodePlan?.titles) + && job?.encodePlan?.titles.length > 0 + && Number(job?.encode_review_confirmed || 0) === 1 + ); + const hasRestartInput = Boolean(job?.encode_input_path || job?.raw_path || job?.encodePlan?.encodeInputPath); + const canRestartEncode = Boolean(hasConfirmedPlan && hasRestartInput && !running); + const canDeleteEntry = !running && typeof onDeleteEntry === 'function'; const logCount = Number(job?.log_count || 0); const logMeta = job?.logMeta && typeof job.logMeta === 'object' ? job.logMeta : null; const logLoaded = Boolean(logMeta?.loaded) || Boolean(job?.log); @@ -106,6 +115,14 @@ export default function JobDetailDialog({
Movie-Dir leer: {job.movieDirStatus?.isEmpty === null ? '-' : job.movieDirStatus?.isEmpty ? 'ja' : 'nein'}
+
+ Backup erfolgreich:{' '} + {job?.backupSuccess ?