Files
ripster/site/search/search_index.json
2026-03-05 19:34:01 +00:00

1 line
172 KiB
JSON

{"config":{"lang":["de"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"],"fields":{"title":{"boost":1000.0},"text":{"boost":1.0},"tags":{"boost":1000000.0}}},"docs":[{"location":"","title":"Ripster","text":"<p>Halbautomatische Disc-Ripping-Plattform f\u00fcr DVDs und Blu-rays</p> <ul> <li> <p> Automatisiertes Ripping</p> <p>Disc einlegen \u2013 Ripster erkennt sie automatisch und startet den Analyse-Workflow mit MakeMKV.</p> <p> Workflow verstehen</p> </li> <li> <p> Metadata-Integration</p> <p>Automatische Suche in der OMDb-Datenbank f\u00fcr Filmtitel, Poster und IMDb-IDs.</p> <p> Konfiguration</p> </li> <li> <p> Flexibles Encoding</p> <p>HandBrake-Encoding mit individueller Track-Auswahl f\u00fcr Audio- und Untertitelspuren.</p> <p> Encode-Planung</p> </li> <li> <p> Job-Historie</p> <p>Vollst\u00e4ndiges Audit-Trail aller Ripping-Jobs mit Logs und Re-Encode-Funktion.</p> <p> History API</p> </li> </ul>"},{"location":"#was-ist-ripster","title":"Was ist Ripster?","text":"<p>Ripster ist eine webbasierte Anwendung zur halbautomatischen Digitalisierung von DVDs und Blu-rays. Die Anwendung kombiniert bew\u00e4hrte Open-Source-Tools zu einem durchg\u00e4ngigen, komfortablen Workflow:</p> <pre><code>Disc einlegen \u2192 Erkennung \u2192 Analyse \u2192 Metadaten w\u00e4hlen \u2192 Rippen \u2192 Encodieren \u2192 Fertig\n</code></pre>"},{"location":"#kernfunktionen","title":"Kernfunktionen","text":"Feature Beschreibung Echtzeit-Updates WebSocket-basierte Live-Statusanzeige ohne Reload Intelligente Playlist-Analyse Erkennt Blu-ray Playlist-Verschleierung (Fake-Playlists) Track-Auswahl Individuelle Auswahl von Audio- und Untertitelspuren Orphan-Recovery Import von bereits gerippten Dateien als Jobs PushOver-Benachrichtigungen Mobile Alerts bei Fertigstellung oder Fehlern DB-Korruptions-Recovery Automatische Quarant\u00e4ne bei korrupten SQLite-Dateien Re-Encoding Erneutes Encodieren ohne neu rippen"},{"location":"#technologie-stack","title":"Technologie-Stack","text":"BackendFrontendExterne Tools <ul> <li>Node.js &gt;= 20.19.0 mit Express.js</li> <li>SQLite3 mit automatischen Schema-Migrationen</li> <li>WebSocket (<code>ws</code>) f\u00fcr Echtzeit-Kommunikation</li> <li>Externe CLI-Tools: <code>makemkvcon</code>, <code>HandBrakeCLI</code>, <code>mediainfo</code></li> </ul> <ul> <li>React 18.3.1 mit React Router</li> <li>Vite 5.4.12 als Build-Tool</li> <li>PrimeReact 10.9.2 als UI-Bibliothek</li> <li>WebSocket-Client f\u00fcr Live-Updates</li> </ul> Tool Zweck <code>makemkvcon</code> Disc-Analyse &amp; MKV/Backup-Ripping <code>HandBrakeCLI</code> Video-Encoding <code>mediainfo</code> Track-Informationen aus gerippten Dateien OMDb API Filmmetadaten (Titel, Poster, IMDb-ID)"},{"location":"#schnellstart","title":"Schnellstart","text":"<pre><code># 1. Repository klonen\ngit clone https://github.com/YOUR_GITHUB_USERNAME/ripster.git\ncd ripster\n\n# 2. Starten (Node.js &gt;= 20 erforderlich)\n./start.sh\n\n# 3. Browser \u00f6ffnen\nopen http://localhost:5173\n</code></pre> <p>Erste Schritte</p> <p>Die vollst\u00e4ndige Installationsanleitung mit allen Voraussetzungen findest du unter Erste Schritte.</p>"},{"location":"#pipeline-uberblick","title":"Pipeline-\u00dcberblick","text":"<pre><code>flowchart LR\n IDLE --&gt; DD[DISC_DETECTED]\n DD --&gt; META[METADATA\\nSELECTION]\n META --&gt; RTS[READY_TO\\nSTART]\n RTS --&gt;|Auto-Start| RIP[RIPPING]\n RTS --&gt;|Auto-Start mit RAW| MIC\n RIP --&gt; MIC[MEDIAINFO\\nCHECK]\n MIC --&gt;|Playlist offen (Backup)| WUD[WAITING_FOR\\nUSER_DECISION]\n WUD --&gt; MIC\n MIC --&gt; RTE[READY_TO\\nENCODE]\n RTE --&gt; ENC[ENCODING]\n ENC --&gt;|inkl. Post-Skripte| FIN([FINISHED])\n ENC --&gt; ERR([ERROR])\n RIP --&gt; ERR\n\n style FIN fill:#e8f5e9,stroke:#66bb6a,color:#2e7d32\n style ERR fill:#ffebee,stroke:#ef5350,color:#c62828\n style WUD fill:#fff8e1,stroke:#ffa726,color:#e65100\n style ENC fill:#f3e5f5,stroke:#ab47bc,color:#6a1b9a</code></pre> <p><code>READY_TO_START</code> ist in der Praxis meist ein kurzer \u00dcbergangszustand: der Job wird nach Metadaten-Auswahl automatisch gestartet oder in die Queue eingeplant.</p>"},{"location":"api/","title":"API-Referenz","text":"<p>Ripster bietet eine REST-API f\u00fcr alle Operationen sowie einen WebSocket-Endpunkt f\u00fcr Echtzeit-Updates.</p>"},{"location":"api/#basis-url","title":"Basis-URL","text":"<pre><code>http://localhost:3001\n</code></pre> <p>Konfigurierbar \u00fcber die Umgebungsvariable <code>PORT</code>.</p>"},{"location":"api/#api-gruppen","title":"API-Gruppen","text":"<ul> <li> <p> Pipeline API</p> <p>Pipeline-Steuerung: Analyse starten, Metadaten setzen, Ripping und Encoding steuern.</p> <p> Pipeline API</p> </li> <li> <p> Settings API</p> <p>Einstellungen lesen und schreiben.</p> <p> Settings API</p> </li> <li> <p> History API</p> <p>Job-Geschichte abfragen, Jobs l\u00f6schen, Orphan-Ordner importieren.</p> <p> History API</p> </li> <li> <p> WebSocket Events</p> <p>Echtzeit-Events f\u00fcr Pipeline-Status, Fortschritt und Disc-Erkennung.</p> <p> WebSocket</p> </li> </ul>"},{"location":"api/#authentifizierung","title":"Authentifizierung","text":"<p>Die API hat keine Authentifizierung. Sie ist f\u00fcr den Einsatz im lokalen Netzwerk konzipiert.</p> <p>Produktionsbetrieb</p> <p>Falls Ripster \u00f6ffentlich erreichbar sein soll, sch\u00fctze die API mit einem Reverse-Proxy (z. B. nginx mit Basic Auth oder OAuth).</p>"},{"location":"api/#fehlerformat","title":"Fehlerformat","text":"<p>Alle API-Fehler werden im folgenden Format zur\u00fcckgegeben:</p> <pre><code>{\n \"error\": \"Job nicht gefunden\",\n \"details\": \"Kein Job mit ID 999 vorhanden\"\n}\n</code></pre> <p>HTTP-Statuscodes:</p> Code Bedeutung <code>200</code> Erfolg <code>400</code> Ung\u00fcltige Anfrage <code>404</code> Ressource nicht gefunden <code>409</code> Konflikt (z.B. Pipeline bereits aktiv) <code>500</code> Interner Serverfehler"},{"location":"api/history/","title":"History API","text":"<p>Endpunkte f\u00fcr die Job-Histoire, Dateimanagement und Orphan-Import.</p>"},{"location":"api/history/#get-apihistory","title":"GET /api/history","text":"<p>Gibt eine Liste aller Jobs zur\u00fcck, optional gefiltert.</p> <p>Query-Parameter:</p> Parameter Typ Beschreibung <code>status</code> string Filtert nach Status (z.B. <code>FINISHED</code>, <code>ERROR</code>) <code>search</code> string Sucht in Filmtiteln <p>Beispiel:</p> <pre><code>GET /api/history?status=FINISHED&amp;search=Inception\n</code></pre> <p>Response:</p> <pre><code>{\n \"jobs\": [\n {\n \"id\": 42,\n \"status\": \"FINISHED\",\n \"title\": \"Inception\",\n \"imdb_id\": \"tt1375666\",\n \"omdb_year\": \"2010\",\n \"omdb_type\": \"movie\",\n \"omdb_poster\": \"https://...\",\n \"raw_path\": \"/mnt/nas/raw/Inception_t00.mkv\",\n \"output_path\": \"/mnt/nas/movies/Inception (2010).mkv\",\n \"created_at\": \"2024-01-15T10:00:00.000Z\",\n \"updated_at\": \"2024-01-15T12:30:00.000Z\"\n }\n ],\n \"total\": 1\n}\n</code></pre>"},{"location":"api/history/#get-apihistoryid","title":"GET /api/history/:id","text":"<p>Gibt Detail-Informationen f\u00fcr einen einzelnen Job zur\u00fcck.</p> <p>URL-Parameter: <code>id</code> \u2013 Job-ID</p> <p>Query-Parameter:</p> Parameter Typ Standard Beschreibung <code>includeLogs</code> boolean <code>false</code> Log-Inhalte einschlie\u00dfen <code>includeLiveLog</code> boolean <code>false</code> Aktuellen Live-Log einschlie\u00dfen <p>Response:</p> <pre><code>{\n \"id\": 42,\n \"status\": \"FINISHED\",\n \"title\": \"Inception\",\n \"imdb_id\": \"tt1375666\",\n \"encode_plan\": { ... },\n \"makemkv_output\": { ... },\n \"mediainfo_output\": { ... },\n \"handbrake_log\": \"/path/to/log\",\n \"logs\": {\n \"handbrake\": \"Encoding: task 1 of 1, 100.0%\\n...\"\n },\n \"created_at\": \"2024-01-15T10:00:00.000Z\",\n \"updated_at\": \"2024-01-15T12:30:00.000Z\"\n}\n</code></pre>"},{"location":"api/history/#get-apihistorydatabase","title":"GET /api/history/database","text":"<p>Gibt alle rohen Datenbankzeilen zur\u00fcck (Debug-Ansicht).</p> <p>Response:</p> <pre><code>{\n \"jobs\": [ { \"id\": 1, \"status\": \"FINISHED\", ... } ],\n \"total\": 15\n}\n</code></pre>"},{"location":"api/history/#get-apihistoryorphan-raw","title":"GET /api/history/orphan-raw","text":"<p>Findet Raw-Ordner, die nicht als Jobs in der Datenbank registriert sind.</p> <p>Response:</p> <pre><code>{\n \"orphans\": [\n {\n \"path\": \"/mnt/nas/raw/UnknownMovie_2023-12-01\",\n \"size\": \"45.2 GB\",\n \"modifiedAt\": \"2023-12-01T15:00:00.000Z\",\n \"files\": [\"t00.mkv\", \"t01.mkv\"]\n }\n ]\n}\n</code></pre>"},{"location":"api/history/#post-apihistoryorphan-rawimport","title":"POST /api/history/orphan-raw/import","text":"<p>Importiert einen Orphan-Raw-Ordner als Job in die Datenbank.</p> <p>Request:</p> <pre><code>{\n \"path\": \"/mnt/nas/raw/UnknownMovie_2023-12-01\"\n}\n</code></pre> <p>Response:</p> <pre><code>{\n \"ok\": true,\n \"jobId\": 99,\n \"message\": \"Orphan-Ordner als Job importiert\"\n}\n</code></pre> <p>Nach dem Import kann dem Job \u00fcber <code>/api/history/:id/omdb/assign</code> Metadaten zugewiesen werden.</p>"},{"location":"api/history/#post-apihistoryidomdbassign","title":"POST /api/history/:id/omdb/assign","text":"<p>Weist einem bestehenden Job OMDb-Metadaten nachtr\u00e4glich zu.</p> <p>URL-Parameter: <code>id</code> \u2013 Job-ID</p> <p>Request:</p> <pre><code>{\n \"imdbId\": \"tt1375666\",\n \"title\": \"Inception\",\n \"year\": \"2010\",\n \"type\": \"movie\",\n \"poster\": \"https://...\"\n}\n</code></pre> <p>Response:</p> <pre><code>{ \"ok\": true }\n</code></pre>"},{"location":"api/history/#post-apihistoryiddelete-files","title":"POST /api/history/:id/delete-files","text":"<p>L\u00f6scht die Dateien eines Jobs (Raw und/oder Output), beh\u00e4lt den Job-Eintrag.</p> <p>URL-Parameter: <code>id</code> \u2013 Job-ID</p> <p>Request:</p> <pre><code>{\n \"deleteRaw\": true,\n \"deleteOutput\": false\n}\n</code></pre> <p>Response:</p> <pre><code>{\n \"ok\": true,\n \"deleted\": {\n \"raw\": \"/mnt/nas/raw/Inception_t00.mkv\",\n \"output\": null\n }\n}\n</code></pre>"},{"location":"api/history/#post-apihistoryiddelete","title":"POST /api/history/:id/delete","text":"<p>L\u00f6scht den Job-Eintrag aus der Datenbank, optional auch die Dateien.</p> <p>URL-Parameter: <code>id</code> \u2013 Job-ID</p> <p>Request:</p> <pre><code>{\n \"deleteFiles\": true\n}\n</code></pre> <p>Response:</p> <pre><code>{ \"ok\": true, \"message\": \"Job gel\u00f6scht\" }\n</code></pre> <p>Unwiderruflich</p> <p>Das L\u00f6schen von Jobs und Dateien ist nicht r\u00fcckg\u00e4ngig zu machen.</p>"},{"location":"api/pipeline/","title":"Pipeline API","text":"<p>Alle Endpunkte zur Steuerung des Ripster-Workflows.</p>"},{"location":"api/pipeline/#get-apipipelinestate","title":"GET /api/pipeline/state","text":"<p>Liefert den aktuellen Pipeline-Snapshot.</p> <p>Response:</p> <pre><code>{\n \"pipeline\": {\n \"state\": \"READY_TO_ENCODE\",\n \"activeJobId\": 42,\n \"progress\": 0,\n \"eta\": null,\n \"statusText\": \"Mediainfo geladen - bitte best\u00e4tigen\",\n \"context\": {\n \"jobId\": 42\n },\n \"queue\": {\n \"maxParallelJobs\": 1,\n \"runningCount\": 0,\n \"queuedCount\": 0,\n \"runningJobs\": [],\n \"queuedJobs\": []\n }\n }\n}\n</code></pre> <p>Pipeline-Zust\u00e4nde:</p> Wert Beschreibung <code>IDLE</code> Wartet auf Medium <code>DISC_DETECTED</code> Medium erkannt, wartet auf Analyse-Start <code>METADATA_SELECTION</code> Metadaten-Dialog aktiv <code>WAITING_FOR_USER_DECISION</code> Manuelle Playlist-Auswahl erforderlich <code>READY_TO_START</code> \u00dcbergang/Fallback vor Start <code>RIPPING</code> MakeMKV l\u00e4uft <code>MEDIAINFO_CHECK</code> HandBrake-Scan + Plan-Erstellung <code>READY_TO_ENCODE</code> Review bereit <code>ENCODING</code> HandBrake-Encoding l\u00e4uft (inkl. Post-Skripte) <code>FINISHED</code> Abgeschlossen <code>CANCELLED</code> Vom Benutzer abgebrochen <code>ERROR</code> Fehler"},{"location":"api/pipeline/#post-apipipelineanalyze","title":"POST /api/pipeline/analyze","text":"<p>Startet die Analyse f\u00fcr die aktuell erkannte Disc.</p> <p>Request: kein Body</p> <p>Response:</p> <pre><code>{\n \"result\": {\n \"jobId\": 42,\n \"detectedTitle\": \"INCEPTION\",\n \"omdbCandidates\": []\n }\n}\n</code></pre>"},{"location":"api/pipeline/#post-apipipelinerescan-disc","title":"POST /api/pipeline/rescan-disc","text":"<p>Erzwingt eine erneute Laufwerkspr\u00fcfung.</p> <p>Response (Beispiel):</p> <pre><code>{\n \"result\": {\n \"emitted\": \"discInserted\"\n }\n}\n</code></pre>"},{"location":"api/pipeline/#get-apipipelineomdbsearchq","title":"GET /api/pipeline/omdb/search?q= <p>Sucht OMDb-Titel.</p> <p>Response:</p> <pre><code>{\n \"results\": [\n {\n \"imdbId\": \"tt1375666\",\n \"title\": \"Inception\",\n \"year\": \"2010\",\n \"type\": \"movie\",\n \"poster\": \"https://...\"\n }\n ]\n}\n</code></pre>","text":""},{"location":"api/pipeline/#post-apipipelineselect-metadata","title":"POST /api/pipeline/select-metadata <p>Setzt Metadaten (und optional Playlist-Entscheidung).</p> <p>Request:</p> <pre><code>{\n \"jobId\": 42,\n \"title\": \"Inception\",\n \"year\": 2010,\n \"imdbId\": \"tt1375666\",\n \"poster\": \"https://...\",\n \"fromOmdb\": true,\n \"selectedPlaylist\": \"00800\"\n}\n</code></pre> <p>Response: <code>{ \"job\": { ... } }</code></p> <p>Startlogik</p> <p>Nach Metadaten-Best\u00e4tigung wird der n\u00e4chste Schritt automatisch ausgel\u00f6st (<code>startPreparedJob</code>). Der Job startet direkt oder wird in die Queue eingereiht.</p>","text":""},{"location":"api/pipeline/#post-apipipelinestartjobid","title":"POST /api/pipeline/start/:jobId <p>Startet einen vorbereiteten Job manuell (z. B. Fallback/Queue-Szenario).</p> <p>Response (Beispiel):</p> <pre><code>{\n \"result\": {\n \"started\": true,\n \"stage\": \"RIPPING\"\n }\n}\n</code></pre> <p>M\u00f6gliche <code>stage</code>-Werte sind u. a. <code>RIPPING</code>, <code>MEDIAINFO_CHECK</code>, <code>ENCODING</code>.</p>","text":""},{"location":"api/pipeline/#post-apipipelineconfirm-encodejobid","title":"POST /api/pipeline/confirm-encode/:jobId <p>Best\u00e4tigt Review-Auswahl (Titel/Tracks/Post-Skripte).</p> <p>Request:</p> <pre><code>{\n \"selectedEncodeTitleId\": 1,\n \"selectedTrackSelection\": {\n \"1\": {\n \"audioTrackIds\": [1, 2],\n \"subtitleTrackIds\": [3]\n }\n },\n \"selectedPostEncodeScriptIds\": [2, 7],\n \"skipPipelineStateUpdate\": false\n}\n</code></pre> <p>Response: <code>{ \"job\": { ... } }</code></p>","text":""},{"location":"api/pipeline/#post-apipipelinecancel","title":"POST /api/pipeline/cancel <p>Bricht laufenden Job ab oder entfernt einen Queue-Eintrag.</p> <p>Request (optional):</p> <pre><code>{\n \"jobId\": 42\n}\n</code></pre> <p>Response (Beispiel):</p> <pre><code>{\n \"result\": {\n \"cancelled\": true,\n \"queuedOnly\": false,\n \"jobId\": 42\n }\n}\n</code></pre>","text":""},{"location":"api/pipeline/#post-apipipelineretryjobid","title":"POST /api/pipeline/retry/:jobId <p>Startet einen Job aus <code>ERROR</code>/<code>CANCELLED</code> erneut (oder reiht ihn in die Queue ein).</p> <p>Response: <code>{ \"result\": { ... } }</code></p>","text":""},{"location":"api/pipeline/#post-apipipelineresume-readyjobid","title":"POST /api/pipeline/resume-ready/:jobId <p>L\u00e4dt einen <code>READY_TO_ENCODE</code>-Job nach Neustart wieder in die aktive Session.</p> <p>Response: <code>{ \"job\": { ... } }</code></p>","text":""},{"location":"api/pipeline/#post-apipipelinereencodejobid","title":"POST /api/pipeline/reencode/:jobId <p>Startet Re-Encode aus bestehendem RAW.</p> <p>Response: <code>{ \"result\": { ... } }</code></p>","text":""},{"location":"api/pipeline/#post-apipipelinerestart-reviewjobid","title":"POST /api/pipeline/restart-review/:jobId <p>Berechnet die Review aus vorhandenem RAW neu.</p> <p>Response: <code>{ \"result\": { ... } }</code></p>","text":""},{"location":"api/pipeline/#post-apipipelinerestart-encodejobid","title":"POST /api/pipeline/restart-encode/:jobId <p>Startet Encoding mit der zuletzt best\u00e4tigten Auswahl neu.</p> <p>Response: <code>{ \"result\": { ... } }</code></p>","text":""},{"location":"api/pipeline/#queue-endpunkte","title":"Queue-Endpunkte","text":""},{"location":"api/pipeline/#get-apipipelinequeue","title":"GET /api/pipeline/queue","text":"<p>Liefert den aktuellen Queue-Status.</p> <p>Response: <code>{ \"queue\": { ... } }</code></p>"},{"location":"api/pipeline/#post-apipipelinequeuereorder","title":"POST /api/pipeline/queue/reorder","text":"<p>Sortiert Queue-Eintr\u00e4ge neu.</p> <p>Request:</p> <pre><code>{\n \"orderedJobIds\": [42, 43, 41]\n}\n</code></pre> <p>Response: <code>{ \"queue\": { ... } }</code></p>"},{"location":"api/settings/","title":"Settings API","text":"<p>Endpunkte zum Lesen und Schreiben der Anwendungseinstellungen.</p>"},{"location":"api/settings/#get-apisettings","title":"GET /api/settings","text":"<p>Gibt alle Einstellungen kategorisiert zur\u00fcck.</p> <p>Response:</p> <pre><code>{\n \"paths\": {\n \"raw_dir\": {\n \"value\": \"/mnt/nas/raw\",\n \"schema\": {\n \"type\": \"string\",\n \"label\": \"Raw-Verzeichnis\",\n \"description\": \"Speicherort f\u00fcr rohe MKV-Dateien\",\n \"required\": true\n }\n },\n \"movie_dir\": {\n \"value\": \"/mnt/nas/movies\",\n \"schema\": { ... }\n }\n },\n \"tools\": { ... },\n \"encoding\": { ... },\n \"drive\": { ... },\n \"makemkv\": { ... },\n \"omdb\": { ... },\n \"notifications\": { ... }\n}\n</code></pre>"},{"location":"api/settings/#put-apisettingskey","title":"PUT /api/settings/:key","text":"<p>Aktualisiert eine einzelne Einstellung.</p> <p>URL-Parameter: <code>key</code> \u2013 Einstellungs-Schl\u00fcssel</p> <p>Request:</p> <pre><code>{\n \"value\": \"/mnt/storage/raw\"\n}\n</code></pre> <p>Response:</p> <pre><code>{ \"ok\": true, \"key\": \"raw_dir\", \"value\": \"/mnt/storage/raw\" }\n</code></pre> <p>Fehlerf\u00e4lle: - <code>400</code> \u2013 Ung\u00fcltiger Wert (Validierungsfehler) - <code>404</code> \u2013 Einstellung nicht gefunden</p> <p>Encode-Review-Refresh</p> <p>Wenn eine encoding-relevante Einstellung ge\u00e4ndert wird (z.B. <code>handbrake_preset</code>), wird der Encode-Plan f\u00fcr den aktuell wartenden Job automatisch neu berechnet.</p>"},{"location":"api/settings/#put-apisettings","title":"PUT /api/settings","text":"<p>Aktualisiert mehrere Einstellungen auf einmal.</p> <p>Request:</p> <pre><code>{\n \"raw_dir\": \"/mnt/storage/raw\",\n \"movie_dir\": \"/mnt/storage/movies\",\n \"handbrake_preset\": \"H.265 MKV 720p30\"\n}\n</code></pre> <p>Response:</p> <pre><code>{\n \"ok\": true,\n \"updated\": [\"raw_dir\", \"movie_dir\", \"handbrake_preset\"],\n \"errors\": []\n}\n</code></pre>"},{"location":"api/settings/#post-apisettingspushovertest","title":"POST /api/settings/pushover/test","text":"<p>Sendet eine Test-Benachrichtigung \u00fcber PushOver.</p> <p>Request: Kein Body erforderlich (verwendet gespeicherte Zugangsdaten)</p> <p>Response (Erfolg):</p> <pre><code>{ \"ok\": true, \"message\": \"Test-Benachrichtigung gesendet\" }\n</code></pre> <p>Response (Fehler):</p> <pre><code>{ \"ok\": false, \"error\": \"Ung\u00fcltiger API-Token\" }\n</code></pre>"},{"location":"api/settings/#skript-verwaltung","title":"Skript-Verwaltung","text":"<p>Post-Encode-Skripte werden \u00fcber eigene Endpunkte unter <code>/api/settings/scripts</code> verwaltet.</p>"},{"location":"api/settings/#get-apisettingsscripts","title":"GET /api/settings/scripts","text":"<p>Gibt alle konfigurierten Skripte zur\u00fcck.</p> <p>Response:</p> <pre><code>{\n \"scripts\": [\n {\n \"id\": \"script-abc123\",\n \"name\": \"Zu Plex verschieben\",\n \"command\": \"/home/michael/scripts/move-to-plex.sh\",\n \"description\": \"Verschiebt die fertige Datei ins Plex-Verzeichnis\",\n \"createdAt\": \"2024-01-15T10:00:00.000Z\"\n }\n ]\n}\n</code></pre>"},{"location":"api/settings/#post-apisettingsscripts","title":"POST /api/settings/scripts","text":"<p>Legt ein neues Post-Encode-Skript an.</p> <p>Request:</p> <pre><code>{\n \"name\": \"Zu Plex verschieben\",\n \"command\": \"/home/michael/scripts/move-to-plex.sh\",\n \"description\": \"Verschiebt die fertige Datei ins Plex-Verzeichnis\"\n}\n</code></pre> Feld Typ Pflicht Beschreibung <code>name</code> string \u2705 Anzeigename <code>command</code> string \u2705 Shell-Befehl oder absoluter Skriptpfad <code>description</code> string \u2014 Optionale Beschreibung <p>Response:</p> <pre><code>{\n \"ok\": true,\n \"script\": {\n \"id\": \"script-abc123\",\n \"name\": \"Zu Plex verschieben\",\n \"command\": \"/home/michael/scripts/move-to-plex.sh\"\n }\n}\n</code></pre>"},{"location":"api/settings/#put-apisettingsscriptsscriptid","title":"PUT /api/settings/scripts/:scriptId","text":"<p>Aktualisiert ein vorhandenes Skript.</p> <p>URL-Parameter: <code>scriptId</code></p> <p>Request: Gleiche Felder wie beim Anlegen (alle optional).</p> <pre><code>{ \"name\": \"Zu Jellyfin verschieben\", \"command\": \"/home/michael/scripts/move-to-jellyfin.sh\" }\n</code></pre> <p>Response: <code>{ \"ok\": true }</code></p>"},{"location":"api/settings/#delete-apisettingsscriptsscriptid","title":"DELETE /api/settings/scripts/:scriptId","text":"<p>L\u00f6scht ein Skript.</p> <p>URL-Parameter: <code>scriptId</code></p> <p>Response: <code>{ \"ok\": true }</code></p> <p>Referenzen in Jobs</p> <p>Wenn das Skript in laufenden oder abgeschlossenen Jobs referenziert wird, wird es trotzdem gel\u00f6scht. In zuk\u00fcnftigen Encode-Reviews erscheint es nicht mehr.</p>"},{"location":"api/settings/#post-apisettingsscriptsscriptidtest","title":"POST /api/settings/scripts/:scriptId/test","text":"<p>F\u00fchrt ein Skript mit Platzhalter-Umgebungsvariablen aus (Testlauf).</p> <p>URL-Parameter: <code>scriptId</code></p> <p>Response (Erfolg):</p> <pre><code>{\n \"ok\": true,\n \"exitCode\": 0,\n \"stdout\": \"Testausgabe des Skripts\",\n \"stderr\": \"\",\n \"durationMs\": 245\n}\n</code></pre> <p>Response (Fehler):</p> <pre><code>{\n \"ok\": false,\n \"exitCode\": 1,\n \"stdout\": \"\",\n \"stderr\": \"Datei nicht gefunden: /home/michael/scripts/move-to-plex.sh\",\n \"durationMs\": 12\n}\n</code></pre> <p>Platzhalter-Werte beim Testlauf:</p> Variable Testwert <code>RIPSTER_OUTPUT_PATH</code> <code>/tmp/ripster-test-output.mkv</code> <code>RIPSTER_JOB_ID</code> <code>0</code> <code>RIPSTER_TITLE</code> <code>Test Film</code> <code>RIPSTER_YEAR</code> <code>2024</code> <code>RIPSTER_IMDB_ID</code> <code>tt0000000</code> <code>RIPSTER_RAW_PATH</code> <code>/tmp/ripster-test-raw.mkv</code>"},{"location":"api/settings/#einstellungs-schlussel-referenz","title":"Einstellungs-Schl\u00fcssel Referenz","text":"<p>Eine vollst\u00e4ndige Liste aller Einstellungs-Schl\u00fcssel:</p> Schl\u00fcssel Kategorie Typ Beschreibung <code>raw_dir</code> paths string Raw-MKV Verzeichnis <code>movie_dir</code> paths string Ausgabe-Verzeichnis <code>log_dir</code> paths string Log-Verzeichnis <code>makemkv_command</code> tools string MakeMKV-Befehl <code>handbrake_command</code> tools string HandBrake-Befehl <code>mediainfo_command</code> tools string MediaInfo-Befehl <code>handbrake_preset</code> encoding string HandBrake-Preset-Name <code>handbrake_extra_args</code> encoding string Zusatz-Argumente <code>output_extension</code> encoding string Dateiendung (z.B. <code>mkv</code>) <code>filename_template</code> encoding string Dateiname-Template <code>drive_mode</code> drive select <code>auto</code> oder <code>explicit</code> <code>drive_device</code> drive string Ger\u00e4te-Pfad <code>disc_poll_interval_ms</code> drive number Polling-Intervall (ms) <code>makemkv_min_length_minutes</code> makemkv number Min. Titell\u00e4nge (Minuten) <code>makemkv_backup_mode</code> makemkv boolean Backup-Modus aktivieren <code>omdb_api_key</code> omdb string OMDb API-Key <code>omdb_default_type</code> omdb select Standard-Suchtyp <code>pushover_user_key</code> notifications string PushOver User-Key <code>pushover_api_token</code> notifications string PushOver API-Token"},{"location":"api/websocket/","title":"WebSocket Events","text":"<p>Ripster sendet Echtzeit-Updates \u00fcber WebSocket unter <code>/ws</code>.</p>"},{"location":"api/websocket/#verbindung","title":"Verbindung","text":"<pre><code>const ws = new WebSocket('ws://localhost:3001/ws');\n\nws.onmessage = (event) =&gt; {\n const message = JSON.parse(event.data);\n console.log(message.type, message.payload);\n};\n</code></pre>"},{"location":"api/websocket/#nachrichtenformat","title":"Nachrichtenformat","text":"<p>Alle Broadcasts haben dieses Schema:</p> <pre><code>{\n \"type\": \"EVENT_TYPE\",\n \"payload\": { },\n \"timestamp\": \"2026-03-05T10:00:00.000Z\"\n}\n</code></pre>"},{"location":"api/websocket/#event-typen","title":"Event-Typen","text":""},{"location":"api/websocket/#ws_connected","title":"WS_CONNECTED","text":"<p>Wird direkt nach Verbindungsaufbau gesendet.</p> <pre><code>{\n \"type\": \"WS_CONNECTED\",\n \"payload\": {\n \"connectedAt\": \"2026-03-05T10:00:00.000Z\"\n }\n}\n</code></pre>"},{"location":"api/websocket/#pipeline_state_changed","title":"PIPELINE_STATE_CHANGED","text":"<p>Snapshot bei Zustandswechsel.</p> <pre><code>{\n \"type\": \"PIPELINE_STATE_CHANGED\",\n \"payload\": {\n \"state\": \"ENCODING\",\n \"activeJobId\": 42,\n \"progress\": 73.5,\n \"eta\": \"00:12:34\",\n \"statusText\": \"Encoding mit HandBrake\",\n \"context\": {},\n \"queue\": {\n \"maxParallelJobs\": 1,\n \"runningCount\": 1,\n \"queuedCount\": 0\n }\n }\n}\n</code></pre>"},{"location":"api/websocket/#pipeline_progress","title":"PIPELINE_PROGRESS","text":"<p>Laufende Fortschrittsupdates w\u00e4hrend aktiver Phasen.</p> <pre><code>{\n \"type\": \"PIPELINE_PROGRESS\",\n \"payload\": {\n \"state\": \"ENCODING\",\n \"activeJobId\": 42,\n \"progress\": 73.5,\n \"eta\": \"00:12:34\",\n \"statusText\": \"ENCODING 73.50% - task 1 of 1\"\n }\n}\n</code></pre>"},{"location":"api/websocket/#pipeline_queue_changed","title":"PIPELINE_QUEUE_CHANGED","text":"<p>Aktualisierung der Job-Queue.</p> <pre><code>{\n \"type\": \"PIPELINE_QUEUE_CHANGED\",\n \"payload\": {\n \"maxParallelJobs\": 1,\n \"runningCount\": 1,\n \"queuedCount\": 2,\n \"runningJobs\": [],\n \"queuedJobs\": []\n }\n}\n</code></pre>"},{"location":"api/websocket/#disc_detected","title":"DISC_DETECTED","text":"<p>Disc erkannt.</p> <pre><code>{\n \"type\": \"DISC_DETECTED\",\n \"payload\": {\n \"device\": {\n \"path\": \"/dev/sr0\",\n \"discLabel\": \"INCEPTION\"\n }\n }\n}\n</code></pre>"},{"location":"api/websocket/#disc_removed","title":"DISC_REMOVED","text":"<p>Disc entfernt.</p> <pre><code>{\n \"type\": \"DISC_REMOVED\",\n \"payload\": {\n \"device\": {\n \"path\": \"/dev/sr0\"\n }\n }\n}\n</code></pre>"},{"location":"api/websocket/#pipeline_error","title":"PIPELINE_ERROR","text":"<p>Fehler bei Pipeline-Disc-Events im Backend.</p> <pre><code>{\n \"type\": \"PIPELINE_ERROR\",\n \"payload\": {\n \"message\": \"...\"\n }\n}\n</code></pre>"},{"location":"api/websocket/#disk_detection_error","title":"DISK_DETECTION_ERROR","text":"<p>Fehler im Laufwerkserkennungsdienst.</p> <pre><code>{\n \"type\": \"DISK_DETECTION_ERROR\",\n \"payload\": {\n \"message\": \"...\"\n }\n}\n</code></pre>"},{"location":"api/websocket/#reconnect-verhalten","title":"Reconnect-Verhalten","text":"<p><code>useWebSocket.js</code> versucht bei Verbindungsabbruch automatisch erneut zu verbinden.</p> <ul> <li>fester Retry-Intervall: <code>1500ms</code></li> <li>erneuter Versuch bis zum Unmount der Komponente</li> </ul>"},{"location":"api/websocket/#react-beispiel","title":"React-Beispiel","text":"<pre><code>import { useWebSocket } from './hooks/useWebSocket';\n\nuseWebSocket({\n onMessage: (msg) =&gt; {\n if (msg.type === 'PIPELINE_STATE_CHANGED') {\n setPipeline(msg.payload);\n }\n }\n});\n</code></pre>"},{"location":"architecture/","title":"Architektur","text":"<p>Ripster ist als klassische Client-Server-Anwendung mit Echtzeit-Kommunikation \u00fcber WebSockets aufgebaut.</p>"},{"location":"architecture/#systemuberblick","title":"System\u00fcberblick","text":"<pre><code>graph TB\n subgraph Browser[\"Browser (React)\"]\n Dashboard[\"Dashboard\"]\n Settings[\"Einstellungen\"]\n History[\"History\"]\n end\n\n subgraph Backend[\"Node.js Backend\"]\n API[\"REST API\\n(Express)\"]\n WS[\"WebSocket\\nServer\"]\n Pipeline[\"Pipeline\\nService\"]\n DB[\"SQLite\\nDatenbank\"]\n end\n\n subgraph ExternalTools[\"Externe Tools\"]\n MakeMKV[\"makemkvcon\"]\n HandBrake[\"HandBrakeCLI\"]\n MediaInfo[\"mediainfo\"]\n end\n\n subgraph ExternalAPIs[\"Externe APIs\"]\n OMDb[\"OMDb API\"]\n PushOver[\"PushOver\"]\n end\n\n Browser &lt;--&gt;|HTTP REST| API\n Browser &lt;--&gt;|WebSocket| WS\n Pipeline --&gt; MakeMKV\n Pipeline --&gt; HandBrake\n Pipeline --&gt; MediaInfo\n Pipeline &lt;--&gt;|Metadaten| OMDb\n Pipeline --&gt;|Benachrichtigungen| PushOver\n API --&gt; DB\n Pipeline --&gt; DB</code></pre>"},{"location":"architecture/#schichten-architektur","title":"Schichten-Architektur","text":""},{"location":"architecture/#backend","title":"Backend","text":"<pre><code>index.js (Express Server)\n\u251c\u2500\u2500 Routes (API-Endpunkte)\n\u2502 \u251c\u2500\u2500 pipelineRoutes.js\n\u2502 \u251c\u2500\u2500 settingsRoutes.js\n\u2502 \u2514\u2500\u2500 historyRoutes.js\n\u251c\u2500\u2500 Services (Business Logic)\n\u2502 \u251c\u2500\u2500 pipelineService.js \u2190 Kern-Orchestrierung\n\u2502 \u251c\u2500\u2500 diskDetectionService.js\n\u2502 \u251c\u2500\u2500 processRunner.js\n\u2502 \u251c\u2500\u2500 websocketService.js\n\u2502 \u251c\u2500\u2500 omdbService.js\n\u2502 \u251c\u2500\u2500 settingsService.js\n\u2502 \u251c\u2500\u2500 notificationService.js\n\u2502 \u251c\u2500\u2500 historyService.js\n\u2502 \u2514\u2500\u2500 logger.js\n\u251c\u2500\u2500 Database\n\u2502 \u251c\u2500\u2500 database.js\n\u2502 \u2514\u2500\u2500 defaultSettings.js\n\u2514\u2500\u2500 Utils\n \u251c\u2500\u2500 encodePlan.js\n \u251c\u2500\u2500 playlistAnalysis.js\n \u251c\u2500\u2500 progressParsers.js\n \u2514\u2500\u2500 files.js\n</code></pre>"},{"location":"architecture/#frontend","title":"Frontend","text":"<pre><code>App.jsx (React Router)\n\u251c\u2500\u2500 Pages\n\u2502 \u251c\u2500\u2500 DashboardPage.jsx \u2190 Haupt-Interface\n\u2502 \u251c\u2500\u2500 SettingsPage.jsx\n\u2502 \u2514\u2500\u2500 DatabasePage.jsx \u2190 Historie/DB-Ansicht\n\u251c\u2500\u2500 Components\n\u2502 \u251c\u2500\u2500 PipelineStatusCard.jsx\n\u2502 \u251c\u2500\u2500 MetadataSelectionDialog.jsx\n\u2502 \u251c\u2500\u2500 MediaInfoReviewPanel.jsx\n\u2502 \u251c\u2500\u2500 DynamicSettingsForm.jsx\n\u2502 \u2514\u2500\u2500 JobDetailDialog.jsx\n\u251c\u2500\u2500 Hooks\n\u2502 \u2514\u2500\u2500 useWebSocket.js\n\u2514\u2500\u2500 API\n \u2514\u2500\u2500 client.js\n</code></pre>"},{"location":"architecture/#weiterfuhrende-dokumentation","title":"Weiterf\u00fchrende Dokumentation","text":"<ul> <li> <p> \u00dcbersicht</p> </li> <li> <p> Backend-Services</p> </li> <li> <p> Frontend-Komponenten</p> </li> <li> <p> Datenbank</p> </li> </ul>"},{"location":"architecture/backend/","title":"Backend-Services","text":"<p>Das Backend ist in Node.js/Express geschrieben und in Services aufgeteilt, die jeweils eine klar abgegrenzte Verantwortlichkeit haben.</p>"},{"location":"architecture/backend/#pipelineservicejs","title":"pipelineService.js","text":"<p>Der Kern von Ripster \u2013 orchestriert den gesamten Ripping-Workflow.</p>"},{"location":"architecture/backend/#zustandigkeiten","title":"Zust\u00e4ndigkeiten","text":"<ul> <li>Verwaltung des Pipeline-Zustands als State Machine</li> <li>Koordination zwischen allen externen Tools</li> <li>Generierung von Encode-Pl\u00e4nen</li> <li>Fehlerbehandlung und Recovery</li> </ul>"},{"location":"architecture/backend/#haupt-methoden","title":"Haupt-Methoden","text":"Methode Beschreibung <code>analyzeDisc()</code> Legt Job an und \u00f6ffnet Metadaten-Auswahl <code>selectMetadata({...})</code> Setzt Metadaten/Playlist und triggert Auto-Start <code>startPreparedJob(jobId)</code> Startet vorbereiteten Job (oder Queue) <code>confirmEncodeReview(jobId, options)</code> Best\u00e4tigt Review inkl. Track/Skript-Auswahl <code>cancel(jobId)</code> Bricht laufenden Job ab oder entfernt Queue-Eintrag <code>retry(jobId)</code> Startet fehlgeschlagenen/abgebrochenen Job neu <code>reencodeFromRaw(jobId)</code> Encodiert aus vorhandenem RAW neu <code>restartReviewFromRaw(jobId)</code> Berechnet Review aus RAW neu <code>restartEncodeWithLastSettings(jobId)</code> Neustart mit letzter best\u00e4tigter Auswahl <code>resumeReadyToEncodeJob(jobId)</code> L\u00e4dt READY_TO_ENCODE nach Neustart in die Session"},{"location":"architecture/backend/#zustandsubergange","title":"Zustands\u00fcberg\u00e4nge","text":"<pre><code>flowchart LR\n START(( )) --&gt; IDLE\n IDLE --&gt;|analyzeDisc()| META[METADATA\\nSELECTION]\n META --&gt;|selectMetadata()| RTS[READY_TO\\nSTART]\n RTS --&gt;|Auto-Start/Queue| RIP[RIPPING]\n RTS --&gt;|Auto-Start mit RAW| MIC[MEDIAINFO\\nCHECK]\n RIP --&gt;|MKV erstellt| MIC[MEDIAINFO\\nCHECK]\n MIC --&gt;|Playlist offen| WUD[WAITING_FOR\\nUSER_DECISION]\n WUD --&gt;|selectMetadata(selectedPlaylist)| MIC\n MIC --&gt;|Tracks analysiert| RTE[READY_TO\\nENCODE]\n RTE --&gt;|confirmEncodeReview() + startPreparedJob()| ENC[ENCODING]\n ENC --&gt;|HandBrake + Post-Skripte fertig| FIN([FINISHED])\n ENC --&gt;|Abbruch| CAN([CANCELLED])\n ENC --&gt;|Fehler| ERR([ERROR])\n RIP --&gt;|Fehler| ERR\n RIP --&gt;|Abbruch| CAN\n ERR --&gt;|retry() / cancel()| IDLE\n CAN --&gt;|retry() / analyzeDisc()| IDLE\n FIN --&gt;|cancel / neue Disc| IDLE\n\n style FIN fill:#e8f5e9,stroke:#66bb6a,color:#2e7d32\n style CAN fill:#fff3e0,stroke:#fb8c00,color:#e65100\n style ERR fill:#ffebee,stroke:#ef5350,color:#c62828\n style ENC fill:#f3e5f5,stroke:#ab47bc,color:#6a1b9a\n style RIP fill:#e3f2fd,stroke:#42a5f5,color:#1565c0\n style MIC fill:#e3f2fd,stroke:#42a5f5,color:#1565c0</code></pre>"},{"location":"architecture/backend/#diskdetectionservicejs","title":"diskDetectionService.js","text":"<p>\u00dcberwacht das Disc-Laufwerk auf Disc-Einleger- und Auswurf-Ereignisse.</p>"},{"location":"architecture/backend/#modi","title":"Modi","text":"Modus Beschreibung <code>auto</code> Erkennt verf\u00fcgbare Laufwerke automatisch <code>explicit</code> \u00dcberwacht ein bestimmtes Ger\u00e4t (z.B. <code>/dev/sr0</code>)"},{"location":"architecture/backend/#polling","title":"Polling","text":"<p>Der Service pollt das Laufwerk im konfigurierten Intervall (<code>disc_poll_interval_ms</code>, Standard: 4000ms) und emittiert Events:</p> <pre><code>// Ereignisse\nemit('discInserted', { path: '/dev/sr0' })\nemit('discRemoved', { path: '/dev/sr0' })\n</code></pre>"},{"location":"architecture/backend/#processrunnerjs","title":"processRunner.js","text":"<p>Verwaltet externe CLI-Prozesse.</p>"},{"location":"architecture/backend/#features","title":"Features","text":"<ul> <li>Streaming: stdout/stderr werden zeilenweise gelesen</li> <li>Progress-Callbacks: Erm\u00f6glicht Echtzeit-Fortschrittsanzeige</li> <li>Graceful Shutdown: SIGINT \u2192 Warte-Timeout \u2192 SIGKILL</li> <li>Prozess-Registry: Verfolgt aktive Prozesse f\u00fcr sauberes Beenden</li> </ul>"},{"location":"architecture/backend/#nutzung","title":"Nutzung","text":"<pre><code>const result = await runProcess(\n 'HandBrakeCLI',\n ['--input', rawFile, '--output', outputFile, '--preset', preset],\n {\n onStderr: (line) =&gt; parseHandBrakeProgress(line),\n onStdout: (line) =&gt; logger.debug(line)\n }\n);\n</code></pre>"},{"location":"architecture/backend/#websocketservicejs","title":"websocketService.js","text":"<p>WebSocket-Server f\u00fcr Echtzeit-Client-Kommunikation.</p>"},{"location":"architecture/backend/#betrieb","title":"Betrieb","text":"<ul> <li>L\u00e4uft auf Pfad <code>/ws</code> des Express-Servers</li> <li>H\u00e4lt eine Registry aller verbundenen Clients</li> <li>Erm\u00f6glicht Broadcast an alle Clients oder gezieltes Senden</li> </ul>"},{"location":"architecture/backend/#api","title":"API","text":"<pre><code>broadcast('PIPELINE_STATE_CHANGED', { state, activeJobId });\nbroadcast('PIPELINE_PROGRESS', { state, progress, eta, statusText });\nbroadcast('PIPELINE_QUEUE_CHANGED', queueSnapshot);\n</code></pre>"},{"location":"architecture/backend/#omdbservicejs","title":"omdbService.js","text":"<p>Integration mit der OMDb API.</p>"},{"location":"architecture/backend/#methoden","title":"Methoden","text":"Methode Beschreibung <code>searchByTitle(title, type)</code> Suche nach Titel (movie/series) <code>fetchById(imdbId)</code> Vollst\u00e4ndige Metadaten per IMDb-ID"},{"location":"architecture/backend/#zuruckgegebene-daten","title":"Zur\u00fcckgegebene Daten","text":"<pre><code>{\n \"imdbId\": \"tt1375666\",\n \"title\": \"Inception\",\n \"year\": \"2010\",\n \"type\": \"movie\",\n \"poster\": \"https://...\",\n \"plot\": \"...\",\n \"director\": \"Christopher Nolan\"\n}\n</code></pre>"},{"location":"architecture/backend/#settingsservicejs","title":"settingsService.js","text":"<p>Verwaltet alle Anwendungseinstellungen.</p>"},{"location":"architecture/backend/#features_1","title":"Features","text":"<ul> <li>Schema-getriebene Validierung: Jede Einstellung hat Typ, Grenzen und Pflichtfeld-Flag</li> <li>Kategorisierung: Einstellungen sind in Kategorien gruppiert (Paths, Tools, Encoding, ...)</li> <li>Persistenz: Werte in SQLite, Schema ebenfalls in SQLite</li> <li>Defaults: <code>defaultSettings.js</code> definiert Standardwerte</li> </ul>"},{"location":"architecture/backend/#einstellungs-kategorien","title":"Einstellungs-Kategorien","text":"Kategorie Einstellungen <code>Pfade</code> <code>raw_dir</code>, <code>movie_dir</code>, <code>log_dir</code> <code>Laufwerk</code> <code>drive_mode</code>, <code>drive_device</code>, <code>disc_poll_interval_ms</code>, <code>makemkv_source_index</code> <code>Monitoring</code> <code>hardware_monitoring_enabled</code>, <code>hardware_monitoring_interval_ms</code> <code>Tools</code> <code>makemkv_command</code>, <code>handbrake_command</code>, <code>mediainfo_command</code>, <code>pipeline_max_parallel_jobs</code> <code>Metadaten</code> <code>omdb_api_key</code>, <code>omdb_default_type</code> <code>Benachrichtigungen</code> <code>pushover_user_key</code>, <code>pushover_api_token</code>"},{"location":"architecture/backend/#historyservicejs","title":"historyService.js","text":"<p>Datenbankoperationen f\u00fcr Job-Historie.</p>"},{"location":"architecture/backend/#hauptoperationen","title":"Hauptoperationen","text":"Operation Beschreibung <code>listJobs(filters)</code> Jobs nach Status/Titel filtern <code>getJob(id)</code> Job-Details mit Logs abrufen <code>findOrphanRawFolders()</code> Nicht-getrackte Raw-Ordner finden <code>importOrphanRaw(path)</code> Orphan-Ordner als Job importieren <code>assignOmdb(id, omdbData)</code> OMDb-Metadaten nachtr\u00e4glich zuweisen <code>deleteJob(id, deleteFiles)</code> Job und optional Dateien l\u00f6schen"},{"location":"architecture/backend/#notificationservicejs","title":"notificationService.js","text":"<p>PushOver-Push-Benachrichtigungen.</p> <pre><code>await notify({\n title: 'Ripster: Job abgeschlossen',\n message: 'Inception (2010) wurde erfolgreich encodiert'\n});\n</code></pre>"},{"location":"architecture/backend/#loggerjs","title":"logger.js","text":"<p>Strukturiertes Logging mit t\u00e4glicher Log-Rotation.</p>"},{"location":"architecture/backend/#log-level","title":"Log-Level","text":"Level Verwendung <code>debug</code> Detaillierte Entwicklungs-Informationen <code>info</code> Normale Betriebsereignisse <code>warn</code> Warnungen, die Aufmerksamkeit ben\u00f6tigen <code>error</code> Fehler, die den Betrieb beeintr\u00e4chtigen"},{"location":"architecture/backend/#log-dateien","title":"Log-Dateien","text":"<pre><code>logs/\n\u251c\u2500\u2500 ripster-2024-01-15.log \u2190 Tages-Log\n\u2514\u2500\u2500 jobs/\n \u2514\u2500\u2500 job-42-handbrake.log \u2190 Prozess-spezifische Logs\n</code></pre>"},{"location":"architecture/database/","title":"Datenbank","text":"<p>Ripster verwendet SQLite3 als Datenbank. Die Datenbankdatei liegt unter <code>backend/data/ripster.db</code>.</p>"},{"location":"architecture/database/#schema-ubersicht","title":"Schema-\u00dcbersicht","text":"<pre><code>-- Vier Haupt-Tabellen\nsettings_schema -- Einstellungs-Definitionen\nsettings_values -- Benutzer-Werte\njobs -- Rip-Job-Datens\u00e4tze\npipeline_state -- Aktueller Pipeline-Zustand (Singleton)\n</code></pre>"},{"location":"architecture/database/#tabelle-jobs","title":"Tabelle: jobs","text":"<p>Die wichtigste Tabelle \u2013 speichert alle Ripping-Jobs.</p> <pre><code>CREATE TABLE jobs (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n status TEXT NOT NULL, -- Aktueller Status\n title TEXT, -- Filmtitel (von OMDb)\n imdb_id TEXT, -- IMDb-ID\n omdb_year TEXT, -- Erscheinungsjahr\n omdb_type TEXT, -- movie/series\n omdb_poster TEXT, -- Poster-URL\n raw_path TEXT, -- Pfad zur Raw-MKV\n output_path TEXT, -- Pfad zur Ausgabedatei\n playlist TEXT, -- Gew\u00e4hlte Blu-ray Playlist\n makemkv_output TEXT, -- MakeMKV-Ausgabe (JSON)\n mediainfo_output TEXT, -- MediaInfo-Ausgabe (JSON)\n encode_plan TEXT, -- Encode-Plan (JSON)\n handbrake_log TEXT, -- HandBrake Log-Pfad\n error_message TEXT, -- Fehlermeldung bei ERROR\n error_details TEXT -- Detaillierte Fehler-Infos\n);\n</code></pre>"},{"location":"architecture/database/#job-status-werte","title":"Job-Status-Werte","text":"Status Beschreibung <code>ANALYZING</code> MakeMKV analysiert die Disc <code>METADATA_SELECTION</code> Wartet auf Benutzer-Metadaten-Auswahl <code>READY_TO_START</code> Bereit zum Starten <code>RIPPING</code> MakeMKV rippt die Disc <code>MEDIAINFO_CHECK</code> MediaInfo analysiert die Raw-Datei <code>READY_TO_ENCODE</code> Wartet auf Encode-Best\u00e4tigung <code>ENCODING</code> HandBrake encodiert <code>FINISHED</code> Erfolgreich abgeschlossen <code>ERROR</code> Fehler aufgetreten"},{"location":"architecture/database/#tabelle-pipeline_state","title":"Tabelle: pipeline_state","text":"<p>Singleton-Tabelle f\u00fcr den aktuellen Pipeline-Zustand (immer genau 1 Zeile).</p> <pre><code>CREATE TABLE pipeline_state (\n id INTEGER PRIMARY KEY CHECK(id = 1),\n state TEXT NOT NULL DEFAULT 'IDLE',\n job_id INTEGER, -- Aktiver Job (NULL wenn IDLE)\n progress REAL, -- Fortschritt 0-100\n eta TEXT, -- Gesch\u00e4tzte Restzeit\n updated_at TEXT NOT NULL\n);\n</code></pre>"},{"location":"architecture/database/#tabelle-settings_schema","title":"Tabelle: settings_schema","text":"<p>Definiert alle verf\u00fcgbaren Einstellungen mit Metadaten.</p> <pre><code>CREATE TABLE settings_schema (\n key TEXT PRIMARY KEY,\n category TEXT NOT NULL, -- paths, tools, encoding, ...\n type TEXT NOT NULL, -- string, number, boolean, select\n label TEXT NOT NULL, -- Anzeigename\n description TEXT, -- Hilfetext\n default_val TEXT, -- Standardwert\n required INTEGER, -- 1 = Pflichtfeld\n min_val REAL, -- Minimalwert (f\u00fcr number)\n max_val REAL, -- Maximalwert (f\u00fcr number)\n options TEXT -- JSON-Array f\u00fcr select-Typ\n);\n</code></pre>"},{"location":"architecture/database/#tabelle-settings_values","title":"Tabelle: settings_values","text":"<p>Speichert benutzer-konfigurierte Werte.</p> <pre><code>CREATE TABLE settings_values (\n key TEXT PRIMARY KEY REFERENCES settings_schema(key),\n value TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\n</code></pre>"},{"location":"architecture/database/#schema-migrationen","title":"Schema-Migrationen","text":"<p><code>database.js</code> implementiert automatische Migrationen:</p> <ol> <li>Beim Start wird das aktuelle Schema gepr\u00fcft</li> <li>Fehlende Tabellen werden erstellt</li> <li>Fehlende Spalten werden hinzugef\u00fcgt</li> <li>Neue Default-Einstellungen werden eingef\u00fcgt</li> </ol>"},{"location":"architecture/database/#korruptions-recovery","title":"Korruptions-Recovery","text":"<p>Falls die Datenbankdatei korrupt ist:</p> <pre><code>1. Korrupte Datei wird erkannt (Verbindungsfehler / Integrit\u00e4tspr\u00fcfung)\n2. Datei wird in /backend/data/quarantine/ verschoben\n3. Neue, leere Datenbank wird erstellt\n4. Schema wird neu initialisiert\n5. Log-Eintrag mit Warnung\n</code></pre>"},{"location":"architecture/database/#datenbankpfad-konfigurieren","title":"Datenbankpfad konfigurieren","text":"<p>Standard: <code>./data/ripster.db</code> (relativ zum Backend-Verzeichnis)</p> <p>\u00dcber Umgebungsvariable anpassen:</p> <pre><code>DB_PATH=/var/lib/ripster/ripster.db\n</code></pre>"},{"location":"architecture/database/#direkte-datenbankinspektion","title":"Direkte Datenbankinspektion","text":"<pre><code># SQLite3-CLI\nsqlite3 backend/data/ripster.db\n\n# Alle Jobs anzeigen\n.mode table\nSELECT id, status, title, created_at FROM jobs ORDER BY created_at DESC;\n\n# Einstellungen anzeigen\nSELECT key, value FROM settings_values;\n</code></pre>"},{"location":"architecture/frontend/","title":"Frontend-Komponenten","text":"<p>Das Frontend ist mit React 18 und PrimeReact gebaut und kommuniziert \u00fcber REST-API und WebSocket mit dem Backend.</p>"},{"location":"architecture/frontend/#seiten-pages","title":"Seiten (Pages)","text":""},{"location":"architecture/frontend/#dashboardpagejsx","title":"DashboardPage.jsx","text":"<p>Die Hauptseite von Ripster \u2013 zeigt den aktuellen Pipeline-Status und erm\u00f6glicht alle Workflow-Aktionen.</p> <p>Funktionen: - Anzeige des aktuellen Pipeline-Zustands (IDLE, DISC_DETECTED, METADATA_SELECTION, RIPPING, MEDIAINFO_CHECK, READY_TO_ENCODE, ENCODING, ...) - Live-Fortschrittsbalken mit ETA - Trigger f\u00fcr Metadaten-Dialog - Playlist-Entscheidungs-UI (bei Blu-ray Obfuskierung) - Encode-Review mit Track-Auswahl - Job-Steuerung (Start, Abbruch, Retry, Queue-Interaktion)</p> <p>Zugeh\u00f6rige Komponenten: - <code>PipelineStatusCard</code> \u2013 Status-Widget - <code>MetadataSelectionDialog</code> \u2013 OMDb-Suche und Playlist-Auswahl - <code>MediaInfoReviewPanel</code> \u2013 Track-Auswahl vor dem Encoding - Queue- und Job-Karten-UI direkt in <code>DashboardPage</code></p>"},{"location":"architecture/frontend/#settingspagejsx","title":"SettingsPage.jsx","text":"<p>Konfigurationsoberfl\u00e4che f\u00fcr alle Ripster-Einstellungen.</p> <p>Funktionen: - Dynamisch generiertes Formular aus dem Settings-Schema - Echtzeit-Validierungsfeedback - PushOver-Verbindungstest - Automatische Aktualisierung des Encode-Reviews bei relevanten \u00c4nderungen</p>"},{"location":"architecture/frontend/#databasepagejsx-history","title":"DatabasePage.jsx (<code>/history</code>)","text":"<p>Job-Historie und Datenbankansicht mit vollst\u00e4ndigem Audit-Trail.</p> <p>Funktionen: - Sortier- und filterbares Job-Verzeichnis - Statusfilter (FINISHED, ERROR, WAITING_FOR_USER_DECISION, ...) - Job-Detail-Dialog mit vollst\u00e4ndigen Logs - Re-Encode, L\u00f6schen und Metadaten-Zuweisung - Import von Orphan-Raw-Ordnern</p>"},{"location":"architecture/frontend/#komponenten-components","title":"Komponenten (Components)","text":""},{"location":"architecture/frontend/#metadataselectiondialogjsx","title":"MetadataSelectionDialog.jsx","text":"<p>Dialog f\u00fcr die Metadaten-Auswahl nach der Disc-Analyse.</p> <pre><code>\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Metadaten ausw\u00e4hlen \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 Suche: [Inception ] \ud83d\udd0d \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 Ergebnisse: \u2502\n\u2502 \u25b6 Inception (2010) \u2013 Movie \u2502\n\u2502 Inception: ... (2011) \u2013 Series \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 Playlist (nur Blu-ray): \u2502\n\u2502 \u25b6 00800.mpls (2:30:15) \u2713 Empfohlen \u2502\n\u2502 00801.mpls (0:01:23) \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 [Best\u00e4tigen] \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n</code></pre>"},{"location":"architecture/frontend/#mediainforeviewpaneljsx","title":"MediaInfoReviewPanel.jsx","text":"<p>Track-Auswahl-Panel vor dem Encoding.</p> <pre><code>\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Encode-Review \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 Audio-Tracks: \u2502\n\u2502 \u2611 Track 1: Deutsch (AC-3, 5.1) \u2502\n\u2502 \u2611 Track 2: English (TrueHD, 7.1) \u2502\n\u2502 \u2610 Track 3: Fran\u00e7ais (AC-3, 2.0) \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 Untertitel: \u2502\n\u2502 \u2611 Track 1: Deutsch \u2502\n\u2502 \u2610 Track 2: English \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 [Encoding starten] \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n</code></pre>"},{"location":"architecture/frontend/#dynamicsettingsformjsx","title":"DynamicSettingsForm.jsx","text":"<p>Wiederverwendbares Formular, das aus dem Settings-Schema generiert wird.</p> <p>Unterst\u00fctzte Feldtypen:</p> Typ UI-Element <code>string</code> Text-Input <code>number</code> Zahlen-Input mit Min/Max <code>boolean</code> Toggle/Checkbox <code>select</code> Dropdown <code>password</code> Passwort-Input"},{"location":"architecture/frontend/#pipelinestatuscardjsx","title":"PipelineStatusCard.jsx","text":"<p>Status-Anzeige-Widget f\u00fcr die Dashboard-Seite.</p>"},{"location":"architecture/frontend/#jobdetaildialogjsx","title":"JobDetailDialog.jsx","text":"<p>Vollst\u00e4ndiger Job-Detail-Dialog mit Logs-Viewer.</p>"},{"location":"architecture/frontend/#hooks","title":"Hooks","text":""},{"location":"architecture/frontend/#usewebsocketjs","title":"useWebSocket.js","text":"<p>Zentraler Custom-Hook f\u00fcr die WebSocket-Verbindung.</p> <pre><code>useWebSocket({\n onMessage: (msg) =&gt; {\n if (msg.type === 'PIPELINE_STATE_CHANGED') {\n setPipelineState(msg.payload);\n }\n }\n});\n</code></pre> <p>Features: - Automatische Verbindung zu <code>/ws</code> - Reconnect mit festem Intervall (<code>1500ms</code>) - Message-Parsing (JSON)</p>"},{"location":"architecture/frontend/#api-client-clientjs","title":"API-Client (client.js)","text":"<p>Zentraler HTTP-Client f\u00fcr alle Backend-Anfragen.</p> <pre><code>// Beispiel-Aufrufe\nconst state = await api.getPipelineState();\nconst results = await api.searchOmdb('Inception');\nawait api.selectMetadata({ jobId, title, year, imdbId, selectedPlaylist });\nawait api.confirmEncodeReview(jobId, {\n selectedEncodeTitleId: 1,\n selectedTrackSelection: { 1: { audioTrackIds: [1], subtitleTrackIds: [3] } }\n});\n</code></pre> <p>Features: - Zentralisierte Fehlerbehandlung - Automatische JSON-Serialisierung - Basis-URL aus Umgebungsvariable (<code>VITE_API_BASE</code>)</p>"},{"location":"architecture/frontend/#build-entwicklung","title":"Build &amp; Entwicklung","text":""},{"location":"architecture/frontend/#entwicklungsserver","title":"Entwicklungsserver","text":"<pre><code>cd frontend\nnpm run dev\n# \u2192 http://localhost:5173\n</code></pre>"},{"location":"architecture/frontend/#vite-proxy-konfiguration","title":"Vite-Proxy-Konfiguration","text":"<p>In der Entwicklungsumgebung proxied Vite API-Anfragen zum Backend:</p> <pre><code>// vite.config.js\nproxy: {\n '/api': 'http://localhost:3001',\n '/ws': { target: 'ws://localhost:3001', ws: true }\n}\n</code></pre>"},{"location":"architecture/frontend/#production-build","title":"Production-Build","text":"<pre><code>cd frontend\nnpm run build\n# \u2192 frontend/dist/\n</code></pre>"},{"location":"architecture/overview/","title":"Architektur-\u00dcbersicht","text":""},{"location":"architecture/overview/#kern-designprinzipien","title":"Kern-Designprinzipien","text":""},{"location":"architecture/overview/#event-driven-pipeline","title":"Event-Driven Pipeline","text":"<p>Der gesamte Ripping-Workflow ist als State Machine implementiert. Der <code>pipelineService</code> verwaltet den aktuellen Zustand und emittiert Ereignisse bei jedem Zustandswechsel. Der WebSocket-Service \u00fcbertr\u00e4gt diese Ereignisse sofort an alle verbundenen Clients.</p> <pre><code>Zustandswechsel \u2192 Event \u2192 WebSocket \u2192 Frontend-Update\n</code></pre>"},{"location":"architecture/overview/#service-layer-muster","title":"Service-Layer-Muster","text":"<pre><code>HTTP-Route \u2192 Service \u2192 Datenbank\n</code></pre> <p>Routes delegieren die gesamte Business-Logik an Services. Services sind voneinander unabh\u00e4ngig und k\u00f6nnen einzeln getestet werden.</p>"},{"location":"architecture/overview/#schema-getriebene-einstellungen","title":"Schema-getriebene Einstellungen","text":"<p>Die Settings-Konfiguration definiert sowohl die Validierungsregeln als auch die UI-Struktur in einer einzigen Quelle (<code>settings_schema</code>-Tabelle). Die <code>DynamicSettingsForm</code>-Komponente rendert das Formular dynamisch aus dem Schema.</p>"},{"location":"architecture/overview/#echtzeit-kommunikation","title":"Echtzeit-Kommunikation","text":""},{"location":"architecture/overview/#websocket-protokoll","title":"WebSocket-Protokoll","text":"<p>Der WebSocket-Server l\u00e4uft unter dem Pfad <code>/ws</code>. Nachrichten werden als JSON \u00fcbertragen:</p> <pre><code>{\n \"type\": \"PIPELINE_STATE_CHANGED\",\n \"payload\": {\n \"state\": \"ENCODING\",\n \"activeJobId\": 42,\n \"progress\": 73.5,\n \"eta\": \"00:12:34\"\n }\n}\n</code></pre> <p>Nachrichtentypen:</p> Typ Beschreibung <code>PIPELINE_STATE_CHANGED</code> Pipeline-Zustand hat gewechselt <code>PIPELINE_PROGRESS</code> Fortschritt (% und ETA) <code>PIPELINE_QUEUE_CHANGED</code> Queue-Status ge\u00e4ndert <code>DISC_DETECTED</code> Disc wurde erkannt <code>DISC_REMOVED</code> Disc wurde entfernt <code>PIPELINE_ERROR</code> Pipeline-Fehler aufgetreten <code>DISK_DETECTION_ERROR</code> Laufwerkserkennung-Fehler"},{"location":"architecture/overview/#reconnect-logik","title":"Reconnect-Logik","text":"<p>Der Frontend-Hook <code>useWebSocket.js</code> implementiert automatisches Reconnect mit festem Intervall von 1500ms bei Verbindungsabbr\u00fcchen.</p>"},{"location":"architecture/overview/#prozess-management","title":"Prozess-Management","text":""},{"location":"architecture/overview/#processrunnerjs","title":"processRunner.js","text":"<p>Externe Tools (MakeMKV, HandBrake, MediaInfo) werden als Child Processes gestartet:</p> <pre><code>spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'] })\n</code></pre> <ul> <li>stdout/stderr werden zeilenweise gelesen und in Echtzeit verarbeitet</li> <li>Progress-Parsing erfolgt \u00fcber regul\u00e4re Ausdr\u00fccke in <code>progressParsers.js</code></li> <li>Graceful Shutdown: SIGINT \u2192 Timeout \u2192 SIGKILL</li> <li>Prozess-Tracking: Aktive Prozesse werden registriert f\u00fcr sauberes Beenden</li> </ul>"},{"location":"architecture/overview/#datenpersistenz","title":"Datenpersistenz","text":""},{"location":"architecture/overview/#sqlite-datenbank","title":"SQLite-Datenbank","text":"<p>Ripster verwendet eine einzige SQLite-Datei f\u00fcr alle persistenten Daten:</p> <pre><code>backend/data/ripster.db\n</code></pre> <p>Tabellen:</p> Tabelle Inhalt <code>jobs</code> Alle Rip-Jobs mit Status, Logs, Metadaten <code>pipeline_state</code> Aktueller Pipeline-Zustand (Singleton) <code>settings_schema</code> Schema aller verf\u00fcgbaren Einstellungen <code>settings_values</code> Benutzer-konfigurierte Werte"},{"location":"architecture/overview/#migrations-strategie","title":"Migrations-Strategie","text":"<p>Beim Start pr\u00fcft <code>database.js</code> automatisch, ob das Schema aktuell ist, und f\u00fchrt fehlende Migrationen aus. Korrupte Datenbankdateien werden in ein Quarant\u00e4ne-Verzeichnis verschoben und eine neue Datenbank erstellt.</p>"},{"location":"architecture/overview/#fehlerbehandlung","title":"Fehlerbehandlung","text":""},{"location":"architecture/overview/#strukturierte-fehler","title":"Strukturierte Fehler","text":"<p>Alle Fehler werden mit Kontext-Metadaten protokolliert:</p> <pre><code>logger.error('Encoding fehlgeschlagen', {\n jobId: job.id,\n command: cmd,\n exitCode: code,\n stderr: lastLines\n});\n</code></pre>"},{"location":"architecture/overview/#job-fehler-recovery","title":"Job-Fehler-Recovery","text":"<ul> <li>Fehlgeschlagene Jobs bleiben in der Datenbank (Status <code>ERROR</code>)</li> <li>Vollst\u00e4ndige Fehler-Logs werden im Job-Datensatz gespeichert</li> <li>Retry-Funktion erm\u00f6glicht Neustart von einem Fehler-Zustand</li> <li>Re-Encode erlaubt erneutes Encodieren ohne neu zu rippen</li> </ul>"},{"location":"architecture/overview/#sicherheit","title":"Sicherheit","text":""},{"location":"architecture/overview/#eingabe-validierung","title":"Eingabe-Validierung","text":"<ul> <li>Alle Benutzer-Eingaben werden in <code>validators.js</code> validiert</li> <li>CLI-Argumente werden sicher \u00fcber <code>commandLine.js</code> konstruiert (kein Shell-Injection-Risiko)</li> <li>Pfade werden sanitisiert bevor sie an externe Prozesse \u00fcbergeben werden</li> </ul>"},{"location":"architecture/overview/#cors-konfiguration","title":"CORS-Konfiguration","text":"<pre><code>CORS_ORIGIN=http://localhost:5173\n</code></pre> <p>In Produktion sollte dieser Wert auf die tats\u00e4chliche Frontend-URL gesetzt werden.</p>"},{"location":"configuration/","title":"Konfiguration","text":"<ul> <li> <p> Einstellungsreferenz</p> <p>Alle verf\u00fcgbaren Einstellungen mit Typen, Standardwerten und Beschreibungen.</p> <p> Einstellungsreferenz</p> </li> <li> <p> Umgebungsvariablen</p> <p>Umgebungsvariablen f\u00fcr Backend und Frontend.</p> <p> Umgebungsvariablen</p> </li> </ul>"},{"location":"configuration/environment/","title":"Umgebungsvariablen","text":"<p>Umgebungsvariablen \u00fcberschreiben die Standardwerte und eignen sich f\u00fcr Server-Deployments.</p>"},{"location":"configuration/environment/#backend-umgebungsvariablen","title":"Backend-Umgebungsvariablen","text":"<p>Konfigurationsdatei: <code>backend/.env</code></p> Variable Standard Beschreibung <code>PORT</code> <code>3001</code> Port des Express-Servers <code>DB_PATH</code> <code>./data/ripster.db</code> Pfad zur SQLite-Datenbankdatei <code>CORS_ORIGIN</code> <code>http://localhost:5173</code> Erlaubter CORS-Origin <code>LOG_DIR</code> <code>./logs</code> Verzeichnis f\u00fcr Log-Dateien <code>LOG_LEVEL</code> <code>info</code> Log-Level (<code>debug</code>, <code>info</code>, <code>warn</code>, <code>error</code>)"},{"location":"configuration/environment/#beispiel-backendenv","title":"Beispiel: backend/.env","text":"<pre><code>PORT=3001\nDB_PATH=/var/lib/ripster/ripster.db\nCORS_ORIGIN=http://192.168.1.100:5173\nLOG_DIR=/var/log/ripster\nLOG_LEVEL=info\n</code></pre>"},{"location":"configuration/environment/#frontend-umgebungsvariablen","title":"Frontend-Umgebungsvariablen","text":"<p>Konfigurationsdatei: <code>frontend/.env</code></p> Variable Standard Beschreibung <code>VITE_API_BASE</code> <code>http://localhost:3001</code> Backend-API-URL <code>VITE_WS_URL</code> <code>ws://localhost:3001</code> WebSocket-URL <code>VITE_PUBLIC_ORIGIN</code> \u2014 \u00d6ffentliche Origin-URL (f\u00fcr CORS) <code>VITE_HMR_HOST</code> \u2014 Vite HMR-Host (f\u00fcr Remote-Entwicklung) <code>VITE_HMR_PORT</code> \u2014 Vite HMR-Port"},{"location":"configuration/environment/#beispiel-frontendenv-entwicklung","title":"Beispiel: frontend/.env (Entwicklung)","text":"<pre><code>VITE_API_BASE=http://localhost:3001\nVITE_WS_URL=ws://localhost:3001\n</code></pre>"},{"location":"configuration/environment/#beispiel-frontendenv-netzwerk-zugriff","title":"Beispiel: frontend/.env (Netzwerk-Zugriff)","text":"<pre><code>VITE_API_BASE=http://192.168.1.100:3001\nVITE_WS_URL=ws://192.168.1.100:3001\nVITE_PUBLIC_ORIGIN=http://192.168.1.100:5173\n</code></pre>"},{"location":"configuration/environment/#envexample-dateien","title":".env.example Dateien","text":"<p>Das Repository enth\u00e4lt Vorlagen f\u00fcr beide Konfigurationsdateien:</p> <pre><code># Backend\ncp backend/.env.example backend/.env\n\n# Frontend\ncp frontend/.env.example frontend/.env\n</code></pre>"},{"location":"configuration/environment/#prioritat-der-konfiguration","title":"Priorit\u00e4t der Konfiguration","text":"<p>Einstellungen werden in folgender Reihenfolge geladen (h\u00f6here Priorit\u00e4t \u00fcberschreibt niedrigere):</p> <pre><code>1. Systemumgebungsvariablen (export VAR=value)\n2. .env-Datei\n3. Hardcodierte Standardwerte in config.js\n</code></pre>"},{"location":"configuration/environment/#log_level","title":"LOG_LEVEL","text":"Level Ausgabe <code>debug</code> Alle Meldungen inkl. Debugging <code>info</code> Normale Betriebsinformationen <code>warn</code> Warnungen + Fehler <code>error</code> Nur Fehler <p>Produktionsempfehlung</p> <p>F\u00fcr Produktionsumgebungen <code>LOG_LEVEL=info</code> oder <code>LOG_LEVEL=warn</code> verwenden. <code>debug</code> erzeugt sehr viele Log-Eintr\u00e4ge.</p>"},{"location":"configuration/settings-reference/","title":"Einstellungsreferenz","text":"<p>Vollst\u00e4ndige \u00dcbersicht aller Ripster-Einstellungen. Alle Einstellungen werden \u00fcber die Web-Oberfl\u00e4che unter Einstellungen verwaltet.</p>"},{"location":"configuration/settings-reference/#kategorie-pfade-paths","title":"Kategorie: Pfade (paths)","text":"Schl\u00fcssel Typ Standard Pflicht Beschreibung <code>raw_dir</code> string \u2014 \u2705 Verzeichnis f\u00fcr rohe MKV-Dateien nach dem Ripping <code>movie_dir</code> string \u2014 \u2705 Ausgabeverzeichnis f\u00fcr encodierte Filme <code>log_dir</code> string <code>./logs</code> \u2014 Verzeichnis f\u00fcr Log-Dateien <p>Beispielkonfiguration</p> <pre><code>raw_dir = /mnt/nas/raw\nmovie_dir = /mnt/nas/movies\nlog_dir = /var/log/ripster\n</code></pre>"},{"location":"configuration/settings-reference/#kategorie-tools-tools","title":"Kategorie: Tools (tools)","text":"Schl\u00fcssel Typ Standard Beschreibung <code>makemkv_command</code> string <code>makemkvcon</code> Befehl oder absoluter Pfad zu MakeMKV <code>handbrake_command</code> string <code>HandBrakeCLI</code> Befehl oder absoluter Pfad zu HandBrake <code>mediainfo_command</code> string <code>mediainfo</code> Befehl oder absoluter Pfad zu MediaInfo <p>Absolute Pfade verwenden</p> <p>Falls die Tools nicht im <code>PATH</code> des Systems sind: <pre><code>makemkv_command = /usr/local/bin/makemkvcon\nhandbrake_command = /usr/local/bin/HandBrakeCLI\nmediainfo_command = /usr/bin/mediainfo\n</code></pre></p>"},{"location":"configuration/settings-reference/#kategorie-encoding-encoding","title":"Kategorie: Encoding (encoding)","text":"Schl\u00fcssel Typ Standard Beschreibung <code>handbrake_preset</code> string <code>H.265 MKV 1080p30</code> Name des HandBrake-Presets <code>handbrake_extra_args</code> string (leer) Zus\u00e4tzliche HandBrake CLI-Argumente <code>output_extension</code> string <code>mkv</code> Dateiendung der Ausgabedatei <code>filename_template</code> string <code>{title} ({year})</code> Template f\u00fcr den Dateinamen"},{"location":"configuration/settings-reference/#verfugbare-handbrake-presets","title":"Verf\u00fcgbare HandBrake-Presets","text":"<p>Eine vollst\u00e4ndige Liste der verf\u00fcgbaren Presets:</p> <pre><code>HandBrakeCLI --preset-list\n</code></pre> <p>H\u00e4ufig verwendete Presets:</p> Preset Beschreibung <code>H.265 MKV 1080p30</code> H.265/HEVC, Full-HD, 30fps <code>H.265 MKV 720p30</code> H.265/HEVC, HD, 30fps <code>H.264 MKV 1080p30</code> H.264/AVC, Full-HD, 30fps <code>HQ 1080p30 Surround</code> Hohe Qualit\u00e4t, Full-HD mit Surround"},{"location":"configuration/settings-reference/#dateiname-template-platzhalter","title":"Dateiname-Template-Platzhalter","text":"Platzhalter Beispiel <code>{title}</code> <code>Inception</code> <code>{year}</code> <code>2010</code> <code>{imdb_id}</code> <code>tt1375666</code> <code>{type}</code> <code>movie</code>"},{"location":"configuration/settings-reference/#kategorie-laufwerk-drive","title":"Kategorie: Laufwerk (drive)","text":"Schl\u00fcssel Typ Standard Optionen Beschreibung <code>drive_mode</code> select <code>auto</code> <code>auto</code>, <code>explicit</code> Laufwerk-Erkennungsmodus <code>drive_device</code> string <code>/dev/sr0</code> \u2014 Ger\u00e4te-Pfad (nur bei <code>explicit</code>) <code>disc_poll_interval_ms</code> number <code>4000</code> 1000\u201360000 Polling-Intervall in Millisekunden <p><code>drive_mode</code> Optionen:</p> Modus Beschreibung <code>auto</code> Ripster erkennt das Laufwerk automatisch <code>explicit</code> Verwendet das in <code>drive_device</code> konfigurierte Ger\u00e4t"},{"location":"configuration/settings-reference/#kategorie-makemkv-makemkv","title":"Kategorie: MakeMKV (makemkv)","text":"Schl\u00fcssel Typ Standard Min Max Beschreibung <code>makemkv_min_length_minutes</code> number <code>15</code> <code>0</code> <code>999</code> Mindest-Titell\u00e4nge in Minuten <code>makemkv_backup_mode</code> boolean <code>false</code> \u2014 \u2014 Backup-Modus statt MKV-Modus <p><code>makemkv_min_length_minutes</code>: Titel k\u00fcrzer als dieser Wert werden von MakeMKV ignoriert. Verhindert das Rippen von Men\u00fc-Schleifen und kurzen Extra-Clips.</p> <p><code>makemkv_backup_mode</code>: Im Backup-Modus erstellt MakeMKV eine vollst\u00e4ndige Disc-Kopie mit Men\u00fcs. Im Standard-Modus werden direkt MKV-Dateien erstellt.</p>"},{"location":"configuration/settings-reference/#kategorie-omdb-omdb","title":"Kategorie: OMDb (omdb)","text":"Schl\u00fcssel Typ Standard Pflicht Beschreibung <code>omdb_api_key</code> string \u2014 \u2705 API-Key von omdbapi.com <code>omdb_default_type</code> select <code>movie</code> \u2014 Standard-Suchtyp: <code>movie</code> oder <code>series</code>"},{"location":"configuration/settings-reference/#kategorie-benachrichtigungen-notifications","title":"Kategorie: Benachrichtigungen (notifications)","text":"Schl\u00fcssel Typ Standard Beschreibung <code>pushover_user_key</code> string \u2014 PushOver User-Key <code>pushover_api_token</code> string \u2014 PushOver API-Token <p>Beide Felder m\u00fcssen konfiguriert sein, um PushOver-Benachrichtigungen zu aktivieren. Die Verbindung kann mit dem Test-Button in den Einstellungen gepr\u00fcft werden.</p>"},{"location":"configuration/settings-reference/#standard-einstellungen-zurucksetzen","title":"Standard-Einstellungen zur\u00fccksetzen","text":"<p>\u00dcber die Datenbank k\u00f6nnen Einstellungen auf Standardwerte zur\u00fcckgesetzt werden:</p> <pre><code>sqlite3 backend/data/ripster.db \\\n \"DELETE FROM settings_values WHERE key = 'handbrake_preset';\"\n</code></pre> <p>Beim n\u00e4chsten Laden der Einstellungen wird der Standardwert verwendet.</p>"},{"location":"deployment/","title":"Deployment","text":"<ul> <li> <p> Entwicklungsumgebung</p> <p>Lokale Entwicklungsumgebung einrichten.</p> <p> Entwicklung</p> </li> <li> <p> Produktion</p> <p>Ripster auf einem Server dauerhaft betreiben.</p> <p> Produktion</p> </li> </ul>"},{"location":"deployment/development/","title":"Entwicklungsumgebung","text":""},{"location":"deployment/development/#voraussetzungen","title":"Voraussetzungen","text":"<ul> <li>Node.js &gt;= 20.19.0</li> <li>Alle externen Tools installiert</li> </ul>"},{"location":"deployment/development/#schnellstart","title":"Schnellstart","text":"<pre><code>./start.sh\n</code></pre> <p>Das Skript startet automatisch: - Backend auf Port 3001 (mit Nodemon f\u00fcr Hot-Reload) - Frontend auf Port 5173 (mit Vite HMR)</p>"},{"location":"deployment/development/#manuelle-entwicklungsumgebung","title":"Manuelle Entwicklungsumgebung","text":""},{"location":"deployment/development/#terminal-1-backend","title":"Terminal 1 \u2013 Backend","text":"<pre><code>cd backend\nnpm install\nnpm run dev\n</code></pre> <p>Backend l\u00e4uft auf <code>http://localhost:3001</code> mit Nodemon \u2013 Neustart bei Datei\u00e4nderungen.</p>"},{"location":"deployment/development/#terminal-2-frontend","title":"Terminal 2 \u2013 Frontend","text":"<pre><code>cd frontend\nnpm install\nnpm run dev\n</code></pre> <p>Frontend l\u00e4uft auf <code>http://localhost:5173</code> mit Vite HMR \u2013 sofortige Browser-Updates.</p>"},{"location":"deployment/development/#vite-proxy","title":"Vite-Proxy","text":"<p>Im Entwicklungsmodus proxied Vite alle API- und WebSocket-Anfragen zum Backend:</p> <pre><code>// frontend/vite.config.js\nserver: {\n proxy: {\n '/api': {\n target: 'http://localhost:3001',\n changeOrigin: true\n },\n '/ws': {\n target: 'ws://localhost:3001',\n ws: true\n }\n }\n}\n</code></pre> <p>Das bedeutet: Im Browser macht das Frontend Anfragen an <code>localhost:5173/api/...</code> \u2013 Vite leitet diese an <code>localhost:3001/api/...</code> weiter.</p>"},{"location":"deployment/development/#remote-entwicklung","title":"Remote-Entwicklung","text":"<p>Falls Ripster auf einem entfernten Server entwickelt wird (z.B. Homeserver), muss die Vite-Konfiguration angepasst werden:</p> <pre><code># frontend/.env.local\nVITE_API_BASE=http://192.168.1.100:3001\nVITE_WS_URL=ws://192.168.1.100:3001\nVITE_HMR_HOST=192.168.1.100\nVITE_HMR_PORT=5173\n</code></pre>"},{"location":"deployment/development/#log-level-fur-entwicklung","title":"Log-Level f\u00fcr Entwicklung","text":"<pre><code># backend/.env\nLOG_LEVEL=debug\n</code></pre> <p>Im Debug-Modus werden alle Ausgaben der externen Tools (MakeMKV, HandBrake) vollst\u00e4ndig geloggt.</p>"},{"location":"deployment/development/#stoppen","title":"Stoppen","text":"<pre><code>./kill.sh\n</code></pre>"},{"location":"deployment/development/#linting-type-checking","title":"Linting &amp; Type-Checking","text":"<pre><code># Frontend (ESLint)\ncd frontend &amp;&amp; npm run lint\n\n# Backend hat keine separaten Lint-Scripts,\n# nutze direkt eslint falls konfiguriert\n</code></pre>"},{"location":"deployment/development/#deployment-script","title":"Deployment-Script","text":"<p>Das <code>deploy-ripster.sh</code>-Script \u00fcbertr\u00e4gt Code auf einen Remote-Server per SSH:</p> <pre><code>./deploy-ripster.sh\n</code></pre> <p>Was das Script tut: 1. <code>rsync</code> synchronisiert den Code (Backend-Quellcode ohne <code>data/</code>) 2. Die Datenbank (<code>backend/data/</code>) wird nicht \u00fcberschrieben 3. Verbindung via SSH (konfigurierbar im Script)</p> <p>Anpassung des Scripts:</p> <pre><code># deploy-ripster.sh\nREMOTE_HOST=\"192.168.1.100\"\nREMOTE_USER=\"michael\"\nREMOTE_PATH=\"/home/michael/ripster\"\n</code></pre>"},{"location":"deployment/production/","title":"Produktions-Deployment","text":""},{"location":"deployment/production/#empfohlene-architektur","title":"Empfohlene Architektur","text":"<pre><code>Internet / Heimnetz\n \u2193\n nginx (Reverse Proxy)\n \u2193\n \u250c\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2510\n \u2502 \u2502\nBackend Frontend\n :3001 (statische Dateien)\n</code></pre>"},{"location":"deployment/production/#systemd-service","title":"systemd-Service","text":"<p>F\u00fcr ein dauerhaftes Betreiben als systemd-Service:</p> <pre><code>sudo nano /etc/systemd/system/ripster.service\n</code></pre> <pre><code>[Unit]\nDescription=Ripster - Disc Ripping Service\nAfter=network.target\n\n[Service]\nType=simple\nUser=michael\nWorkingDirectory=/home/michael/ripster\nExecStart=/bin/bash /home/michael/ripster/start.sh\nExecStop=/bin/bash /home/michael/ripster/kill.sh\nRestart=on-failure\nRestartSec=10s\n\n# Umgebungsvariablen\nEnvironment=NODE_ENV=production\nEnvironment=PORT=3001\nEnvironment=LOG_LEVEL=info\n\n[Install]\nWantedBy=multi-user.target\n</code></pre> <pre><code># Service aktivieren und starten\nsudo systemctl daemon-reload\nsudo systemctl enable ripster\nsudo systemctl start ripster\n\n# Status pr\u00fcfen\nsudo systemctl status ripster\n\n# Logs anzeigen\njournalctl -u ripster -f\n</code></pre>"},{"location":"deployment/production/#frontend-build","title":"Frontend-Build","text":"<p>F\u00fcr Produktion das Frontend bauen:</p> <pre><code>cd frontend\nnpm run build\n</code></pre> <p>Die statischen Dateien landen in <code>frontend/dist/</code>.</p>"},{"location":"deployment/production/#nginx-konfiguration","title":"nginx-Konfiguration","text":"<pre><code># /etc/nginx/sites-available/ripster\nserver {\n listen 80;\n server_name ripster.local;\n\n # Statisches Frontend\n root /home/michael/ripster/frontend/dist;\n index index.html;\n\n # SPA Fallback (React Router)\n location / {\n try_files $uri $uri/ /index.html;\n }\n\n # API-Proxy zum Backend\n location /api/ {\n proxy_pass http://localhost:3001;\n proxy_set_header Host $host;\n proxy_set_header X-Real-IP $remote_addr;\n }\n\n # WebSocket-Proxy\n location /ws {\n proxy_pass http://localhost:3001;\n proxy_http_version 1.1;\n proxy_set_header Upgrade $http_upgrade;\n proxy_set_header Connection \"upgrade\";\n proxy_set_header Host $host;\n }\n}\n</code></pre> <pre><code>sudo ln -s /etc/nginx/sites-available/ripster /etc/nginx/sites-enabled/\nsudo nginx -t &amp;&amp; sudo systemctl reload nginx\n</code></pre>"},{"location":"deployment/production/#nur-backend-produktion-ohne-nginx","title":"Nur-Backend-Produktion (ohne nginx)","text":"<p>Falls kein Reverse Proxy gew\u00fcnscht ist, kann das Backend die Frontend-Dateien direkt ausliefern:</p> <pre><code># Frontend bauen\ncd frontend &amp;&amp; npm run build\n\n# Backend startet und serviert frontend/dist/\ncd backend &amp;&amp; NODE_ENV=production npm start\n</code></pre> <p>Das Backend ist so konfiguriert, dass es im Produktionsmodus die <code>frontend/dist/</code>-Dateien als statische Assets ausliefert.</p>"},{"location":"deployment/production/#datenbank-backup","title":"Datenbank-Backup","text":"<pre><code># Datenbank sichern\ncp backend/data/ripster.db backend/data/ripster.db.backup.$(date +%Y%m%d)\n\n# Oder mit SQLite-eigenem Backup-Befehl\nsqlite3 backend/data/ripster.db \".backup '/mnt/backup/ripster.db'\"\n</code></pre> <p>Automatisches Backup</p> <p>Cron-Job f\u00fcr t\u00e4gliches Backup: <pre><code>0 3 * * * sqlite3 /home/michael/ripster/backend/data/ripster.db \".backup '/mnt/backup/ripster-$(date +\\%Y\\%m\\%d).db'\"\n</code></pre></p>"},{"location":"deployment/production/#log-rotation","title":"Log-Rotation","text":"<p>Ripster rotiert Logs automatisch t\u00e4glich. Falls zus\u00e4tzlich systemd-Journal-Rotation gew\u00fcnscht ist:</p> <pre><code># /etc/logrotate.d/ripster\n/home/michael/ripster/backend/logs/*.log {\n daily\n rotate 14\n compress\n missingok\n notifempty\n}\n</code></pre>"},{"location":"deployment/production/#sicherheitshinweise","title":"Sicherheitshinweise","text":"<p>Heimnetz-Einsatz</p> <p>Ripster ist f\u00fcr den Einsatz im lokalen Heimnetz konzipiert und enth\u00e4lt keine Authentifizierung. Stelle sicher, dass der Dienst nicht \u00f6ffentlich erreichbar ist.</p> <p>Falls \u00f6ffentlicher Zugang ben\u00f6tigt wird:</p> <ol> <li> <p>Basic Auth via nginx: <pre><code>sudo htpasswd -c /etc/nginx/.htpasswd michael\n</code></pre> <pre><code>location / {\n auth_basic \"Ripster\";\n auth_basic_user_file /etc/nginx/.htpasswd;\n # ...\n}\n</code></pre></p> </li> <li> <p>VPN-Zugang (empfohlen): Zugriff nur \u00fcber WireGuard/OpenVPN</p> </li> <li> <p>SSL/TLS: Let's Encrypt mit certbot f\u00fcr HTTPS</p> </li> </ol>"},{"location":"getting-started/","title":"Erste Schritte","text":"<p>Dieser Abschnitt f\u00fchrt dich durch die Installation und Einrichtung von Ripster.</p>"},{"location":"getting-started/#uberblick","title":"\u00dcberblick","text":"<ul> <li> <p>:material-list-check: Voraussetzungen</p> <p>Systemanforderungen und externe Tools, die vor der Installation ben\u00f6tigt werden.</p> <p> Voraussetzungen pr\u00fcfen</p> </li> <li> <p> Installation</p> <p>Schritt-f\u00fcr-Schritt-Anleitung zur Installation von Ripster.</p> <p> Installation starten</p> </li> <li> <p> Konfiguration</p> <p>Einrichten von Pfaden, API-Keys und Encoding-Presets.</p> <p> Konfigurieren</p> </li> <li> <p> Schnellstart</p> <p>Rippe deinen ersten Film in wenigen Minuten.</p> <p> Loslegen</p> </li> </ul>"},{"location":"getting-started/configuration/","title":"Konfiguration","text":"<p>Alle Einstellungen werden \u00fcber die Web-Oberfl\u00e4che unter Einstellungen verwaltet und in der SQLite-Datenbank gespeichert.</p>"},{"location":"getting-started/configuration/#pflichteinstellungen","title":"Pflichteinstellungen","text":"<p>Diese Einstellungen m\u00fcssen vor dem ersten Rip konfiguriert werden:</p>"},{"location":"getting-started/configuration/#pfade","title":"Pfade","text":"Einstellung Beschreibung Beispiel <code>raw_dir</code> Verzeichnis f\u00fcr rohe MKV-Dateien <code>/mnt/nas/raw</code> <code>movie_dir</code> Ausgabeverzeichnis f\u00fcr kodierte Filme <code>/mnt/nas/movies</code> <code>log_dir</code> Verzeichnis f\u00fcr Log-Dateien <code>/var/log/ripster</code> <p>Berechtigungen</p> <p>Der Ripster-Prozess ben\u00f6tigt Schreibrechte auf alle konfigurierten Verzeichnisse.</p> <pre><code># Verzeichnisse erstellen und Berechtigungen setzen\nsudo mkdir -p /mnt/nas/{raw,movies}\nsudo chown $USER:$USER /mnt/nas/{raw,movies}\n</code></pre>"},{"location":"getting-started/configuration/#omdb-api","title":"OMDb API","text":"Einstellung Beschreibung <code>omdb_api_key</code> API-Key von omdbapi.com <code>omdb_default_type</code> Standard-Suchtyp: <code>movie</code> oder <code>series</code>"},{"location":"getting-started/configuration/#tool-konfiguration","title":"Tool-Konfiguration","text":"Einstellung Standard Beschreibung <code>makemkv_command</code> <code>makemkvcon</code> Pfad oder Befehl f\u00fcr MakeMKV <code>handbrake_command</code> <code>HandBrakeCLI</code> Pfad oder Befehl f\u00fcr HandBrake <code>mediainfo_command</code> <code>mediainfo</code> Pfad oder Befehl f\u00fcr MediaInfo <p>Absolute Pfade</p> <p>Falls die Tools nicht im <code>PATH</code> sind, verwende absolute Pfade: <pre><code>/usr/local/bin/HandBrakeCLI\n</code></pre></p>"},{"location":"getting-started/configuration/#encoding-konfiguration","title":"Encoding-Konfiguration","text":"Einstellung Standard Beschreibung <code>handbrake_preset</code> <code>H.265 MKV 1080p30</code> HandBrake-Preset-Name <code>handbrake_extra_args</code> (leer) Zus\u00e4tzliche HandBrake-Argumente <code>output_extension</code> <code>mkv</code> Dateiendung der Ausgabedatei <code>filename_template</code> <code>{title} ({year})</code> Template f\u00fcr Dateinamen"},{"location":"getting-started/configuration/#dateiname-template","title":"Dateiname-Template","text":"<p>Das Template unterst\u00fctzt folgende Platzhalter:</p> Platzhalter Beschreibung Beispiel <code>{title}</code> Filmtitel <code>Inception</code> <code>{year}</code> Erscheinungsjahr <code>2010</code> <code>{imdb_id}</code> IMDb-ID <code>tt1375666</code> <code>{type}</code> <code>movie</code> oder <code>series</code> <code>movie</code> <p>Beispiel-Template: <pre><code>{title} ({year})\n\u2192 Inception (2010).mkv\n</code></pre></p>"},{"location":"getting-started/configuration/#laufwerk-konfiguration","title":"Laufwerk-Konfiguration","text":"Einstellung Standard Beschreibung <code>drive_mode</code> <code>auto</code> <code>auto</code> (automatisch erkennen) oder <code>explicit</code> (festes Ger\u00e4t) <code>drive_device</code> <code>/dev/sr0</code> Ger\u00e4te-Pfad (nur bei <code>explicit</code>) <code>disc_poll_interval_ms</code> <code>4000</code> Polling-Intervall in Millisekunden"},{"location":"getting-started/configuration/#makemkv-konfiguration","title":"MakeMKV-Konfiguration","text":"Einstellung Standard Beschreibung <code>makemkv_min_length_minutes</code> <code>15</code> Mindestl\u00e4nge f\u00fcr Titel in Minuten <code>makemkv_backup_mode</code> <code>false</code> Backup-Modus statt MKV-Modus <p>Backup-Modus</p> <p>Im Backup-Modus erstellt MakeMKV eine vollst\u00e4ndige Kopie der Disc (inkl. Men\u00fcs). Der Standardmodus erstellt direkt MKV-Dateien.</p>"},{"location":"getting-started/configuration/#benachrichtigungen-pushover","title":"Benachrichtigungen (PushOver)","text":"Einstellung Beschreibung <code>pushover_user_key</code> Dein PushOver User-Key <code>pushover_api_token</code> API-Token deiner PushOver-App <p>Nach der Eingabe kann die Verbindung mit dem Test-Button gepr\u00fcft werden.</p>"},{"location":"getting-started/configuration/#vollstandige-einstellungsreferenz","title":"Vollst\u00e4ndige Einstellungsreferenz","text":"<p>Eine vollst\u00e4ndige Liste aller Einstellungen mit Typen, Validierung und Standardwerten findest du unter:</p> <p> Einstellungsreferenz</p>"},{"location":"getting-started/installation/","title":"Installation","text":""},{"location":"getting-started/installation/#repository-klonen","title":"Repository klonen","text":"<pre><code>git clone https://github.com/YOUR_GITHUB_USERNAME/ripster.git\ncd ripster\n</code></pre>"},{"location":"getting-started/installation/#automatischer-start","title":"Automatischer Start","text":"<p>Ripster enth\u00e4lt ein <code>start.sh</code>-Skript, das alle Abh\u00e4ngigkeiten installiert und Backend + Frontend gleichzeitig startet:</p> <pre><code>./start.sh\n</code></pre> <p>Das Skript f\u00fchrt automatisch folgende Schritte durch:</p> <ol> <li>Node.js-Versionscheck \u2013 pr\u00fcft ob &gt;= 20.19.0 verf\u00fcgbar ist (mit nvm/npx-Fallback)</li> <li>Abh\u00e4ngigkeiten installieren \u2013 <code>npm install</code> f\u00fcr Root, Backend und Frontend</li> <li>Dienste starten \u2013 Backend und Frontend werden parallel gestartet</li> </ol> <p>Erfolgreich gestartet</p> <ul> <li>Backend l\u00e4uft auf <code>http://localhost:3001</code></li> <li>Frontend l\u00e4uft auf <code>http://localhost:5173</code></li> </ul>"},{"location":"getting-started/installation/#manuelle-installation","title":"Manuelle Installation","text":"<p>Falls du mehr Kontrolle ben\u00f6tigst:</p> <pre><code># Root-Abh\u00e4ngigkeiten\nnpm install\n\n# Backend-Abh\u00e4ngigkeiten\ncd backend &amp;&amp; npm install &amp;&amp; cd ..\n\n# Frontend-Abh\u00e4ngigkeiten\ncd frontend &amp;&amp; npm install &amp;&amp; cd ..\n\n# Backend starten (Terminal 1)\ncd backend &amp;&amp; npm run dev\n\n# Frontend starten (Terminal 2)\ncd frontend &amp;&amp; npm run dev\n</code></pre>"},{"location":"getting-started/installation/#umgebungsvariablen-konfigurieren","title":"Umgebungsvariablen konfigurieren","text":""},{"location":"getting-started/installation/#backend","title":"Backend","text":"<pre><code>cp backend/.env.example backend/.env\n</code></pre> <p>Bearbeite <code>backend/.env</code>:</p> <pre><code>PORT=3001\nDB_PATH=./data/ripster.db\nCORS_ORIGIN=http://localhost:5173\nLOG_DIR=./logs\nLOG_LEVEL=info\n</code></pre>"},{"location":"getting-started/installation/#frontend","title":"Frontend","text":"<pre><code>cp frontend/.env.example frontend/.env\n</code></pre> <p>Bearbeite <code>frontend/.env</code>:</p> <pre><code>VITE_API_BASE=http://localhost:3001\nVITE_WS_URL=ws://localhost:3001\n</code></pre> <p>Alle Umgebungsvariablen</p> <p>Eine vollst\u00e4ndige \u00dcbersicht aller Umgebungsvariablen findest du unter Umgebungsvariablen.</p>"},{"location":"getting-started/installation/#datenbank-initialisieren","title":"Datenbank initialisieren","text":"<p>Die SQLite-Datenbank wird automatisch beim ersten Start erstellt und mit dem Schema aus <code>db/schema.sql</code> initialisiert. Es sind keine manuellen Datenbankschritte erforderlich.</p> <pre><code>backend/data/\n\u2514\u2500\u2500 ripster.db \u2190 Wird automatisch angelegt\n</code></pre>"},{"location":"getting-started/installation/#stoppen","title":"Stoppen","text":"<pre><code>./kill.sh\n</code></pre> <p>Das Skript beendet Backend- und Frontend-Prozesse graceful.</p>"},{"location":"getting-started/installation/#verzeichnisstruktur-nach-installation","title":"Verzeichnisstruktur nach Installation","text":"<pre><code>ripster/\n\u251c\u2500\u2500 backend/\n\u2502 \u251c\u2500\u2500 data/ \u2190 SQLite-Datenbank (nach erstem Start)\n\u2502 \u251c\u2500\u2500 logs/ \u2190 Log-Dateien\n\u2502 \u251c\u2500\u2500 node_modules/ \u2190 Backend-Abh\u00e4ngigkeiten\n\u2502 \u2514\u2500\u2500 .env \u2190 Backend-Konfiguration\n\u251c\u2500\u2500 frontend/\n\u2502 \u251c\u2500\u2500 node_modules/ \u2190 Frontend-Abh\u00e4ngigkeiten\n\u2502 \u251c\u2500\u2500 dist/ \u2190 Production-Build (nach npm run build)\n\u2502 \u2514\u2500\u2500 .env \u2190 Frontend-Konfiguration\n\u2514\u2500\u2500 node_modules/ \u2190 Root-Abh\u00e4ngigkeiten (concurrently etc.)\n</code></pre>"},{"location":"getting-started/installation/#nachste-schritte","title":"N\u00e4chste Schritte","text":"<p>Nach erfolgreicher Installation:</p> <ol> <li>\u00d6ffne http://localhost:5173</li> <li>Navigiere zu Einstellungen</li> <li>Konfiguriere Pfade, API-Keys und Encoding-Presets</li> </ol> <p> Zur Konfiguration</p>"},{"location":"getting-started/prerequisites/","title":"Voraussetzungen","text":"<p>Bevor du Ripster installierst, stelle sicher, dass folgende Software auf deinem System verf\u00fcgbar ist.</p>"},{"location":"getting-started/prerequisites/#system-anforderungen","title":"System-Anforderungen","text":"Anforderung Mindestversion Empfohlen Betriebssystem Linux / macOS Ubuntu 22.04+ Node.js 20.19.0 20.x LTS RAM 4 GB 8 GB+ Festplatte 50 GB frei 500 GB+ (f\u00fcr Roh-MKVs)"},{"location":"getting-started/prerequisites/#nodejs","title":"Node.js","text":"<p>Ripster ben\u00f6tigt Node.js &gt;= 20.19.0.</p> nvm (empfohlen)Ubuntu/DebianmacOS (Homebrew) <pre><code># nvm installieren\ncurl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash\n\n# Node.js 20 installieren\nnvm install 20\nnvm use 20\n\n# Version pr\u00fcfen\nnode --version # v20.x.x\n</code></pre> <pre><code>curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -\nsudo apt-get install -y nodejs\n\nnode --version # v20.x.x\n</code></pre> <pre><code>brew install node@20\nnode --version # v20.x.x\n</code></pre>"},{"location":"getting-started/prerequisites/#externe-tools","title":"Externe Tools","text":""},{"location":"getting-started/prerequisites/#makemkv","title":"MakeMKV","text":"<p>Lizenz erforderlich</p> <p>MakeMKV ist f\u00fcr den pers\u00f6nlichen Gebrauch kostenlos (Beta-Lizenz), ben\u00f6tigt aber eine g\u00fcltige Lizenz.</p> <pre><code># Ubuntu/Debian - PPA verwenden\nsudo add-apt-repository ppa:heyarje/makemkv-beta\nsudo apt-get update\nsudo apt-get install makemkv-bin makemkv-oss\n\n# Installierte Version pr\u00fcfen\nmakemkvcon --version\n</code></pre> <p> MakeMKV Download</p>"},{"location":"getting-started/prerequisites/#handbrake-cli","title":"HandBrake CLI","text":"<pre><code># Ubuntu/Debian\nsudo add-apt-repository ppa:stebbins/handbrake-releases\nsudo apt-get update\nsudo apt-get install handbrake-cli\n\n# Version pr\u00fcfen\nHandBrakeCLI --version\n\n# macOS\nbrew install handbrake\n</code></pre> <p> HandBrake Download</p>"},{"location":"getting-started/prerequisites/#mediainfo","title":"MediaInfo","text":"<pre><code># Ubuntu/Debian\nsudo apt-get install mediainfo\n\n# macOS\nbrew install mediainfo\n\n# Version pr\u00fcfen\nmediainfo --Version\n</code></pre>"},{"location":"getting-started/prerequisites/#disc-laufwerk","title":"Disc-Laufwerk","text":"<p>Ripster ben\u00f6tigt ein physisches DVD- oder Blu-ray-Laufwerk.</p> <p>Blu-ray unter Linux</p> <p>F\u00fcr Blu-ray-Ripping unter Linux wird zus\u00e4tzlich <code>libaacs</code> ben\u00f6tigt. MakeMKV bringt jedoch eine eigene Entschl\u00fcsselung mit, daher ist dies in den meisten F\u00e4llen nicht erforderlich.</p> <pre><code># Laufwerk pr\u00fcfen\nls /dev/sr*\n# oder\nlsblk | grep rom\n</code></pre>"},{"location":"getting-started/prerequisites/#omdb-api-key","title":"OMDb API-Key","text":"<p>Ripster verwendet die OMDb API f\u00fcr Filmmetadaten.</p> <ol> <li>Registriere dich kostenlos auf omdbapi.com</li> <li>Best\u00e4tige deine E-Mail-Adresse</li> <li>Notiere deinen API-Key \u2013 du gibst ihn sp\u00e4ter in den Einstellungen ein</li> </ol>"},{"location":"getting-started/prerequisites/#optionale-voraussetzungen","title":"Optionale Voraussetzungen","text":""},{"location":"getting-started/prerequisites/#pushover-benachrichtigungen","title":"PushOver (Benachrichtigungen)","text":"<p>F\u00fcr mobile Push-Benachrichtigungen bei Fertigstellung oder Fehlern:</p> <ul> <li>App kaufen auf pushover.net (~5 USD einmalig)</li> <li>User Key und API Token notieren</li> </ul>"},{"location":"getting-started/prerequisites/#ssh-zugang-deployment","title":"SSH-Zugang (Deployment)","text":"<p>F\u00fcr Remote-Deployment via <code>deploy-ripster.sh</code>:</p> <pre><code># sshpass installieren\nsudo apt-get install sshpass\n</code></pre>"},{"location":"getting-started/prerequisites/#checkliste","title":"Checkliste","text":"<ul> <li>[ ] Node.js &gt;= 20.19.0 installiert (<code>node --version</code>)</li> <li>[ ] <code>makemkvcon</code> installiert (<code>makemkvcon --version</code>)</li> <li>[ ] <code>HandBrakeCLI</code> installiert (<code>HandBrakeCLI --version</code>)</li> <li>[ ] <code>mediainfo</code> installiert (<code>mediainfo --Version</code>)</li> <li>[ ] DVD/Blu-ray Laufwerk vorhanden (<code>ls /dev/sr*</code>)</li> <li>[ ] OMDb API-Key beschafft</li> </ul>"},{"location":"getting-started/quickstart/","title":"Schnellstart \u2013 Vollst\u00e4ndiger Workflow","text":"<p>Nach der Installation und Konfiguration f\u00fchrt diese Seite Schritt f\u00fcr Schritt durch den ersten Rip \u2013 mit allen Details aus dem Code.</p>"},{"location":"getting-started/quickstart/#ubersicht-pipeline-ablauf","title":"\u00dcbersicht: Pipeline-Ablauf","text":"\u25cf IDLE Warten 1 DISC_DETECTED Disc erkannt 2 METADATA_SELECTION OMDb &amp; Dialog \u26a0 WAITING_FOR_USER_DECISION Playlist w\u00e4hlen(nur bei Obfusk.) 3 READY_TO_START Bereit 4 RIPPING MakeMKV 5 MEDIAINFO_CHECK HandBrake-Scan 6 READY_TO_ENCODE Track-Review 7 ENCODING HandBrake 8* POST-ENCODE Skripte(innerhalb ENCODING) \u2713 FINISHED Fertig <p>Legende: \u25cf Warten \u00a0|\u00a0 \u25a0 L\u00e4uft automatisch \u00a0|\u00a0 \u25a0 Benutzeraktion \u00a0|\u00a0 \u26a0 Optional \u00a0|\u00a0 \u25a0 Encodierung \u00a0|\u00a0 \u2713 Fertig</p> Vollst\u00e4ndiges Zustandsdiagramm (inkl. Fehler- &amp; Alternativpfade) <pre><code>flowchart LR\n START(( )) --&gt; IDLE\n\n IDLE --&gt;|Disc erkannt| DD[DISC_DETECTED]\n DD --&gt;|Analyse starten| META[METADATA\\nSELECTION]\n\n META --&gt;|Metadaten \u00fcbernommen| RTS[READY_TO\\nSTART]\n META --&gt;|vorhandenes RAW +\\nPlaylist offen| WUD[WAITING_FOR\\nUSER_DECISION]\n RTS --&gt;|Auto-Start| RIP[RIPPING]\n RTS --&gt;|Auto-Start mit RAW| MIC[MEDIAINFO\\nCHECK]\n\n RIP --&gt;|MKV fertig| MIC\n RIP --&gt;|Fehler| ERR\n\n MIC --&gt;|Playlist offen (Backup)| WUD\n WUD --&gt;|Playlist best\u00e4tigt| MIC\n WUD --&gt;|Playlist best\u00e4tigt,\\nnoch kein RAW| RTS\n\n MIC --&gt; RTE[READY_TO\\nENCODE]\n RTE --&gt;|Encoding starten| ENC[ENCODING]\n\n ENC --&gt;|inkl. Post-Skripte| FIN([FINISHED])\n ENC --&gt;|Fehler| ERR\n\n ERR([ERROR]) --&gt;|Retry / Cancel| IDLE\n\n style FIN fill:#e8f5e9,stroke:#66bb6a,color:#2e7d32\n style ERR fill:#ffebee,stroke:#ef5350,color:#c62828\n style WUD fill:#fff8e1,stroke:#ffa726,color:#e65100\n style ENC fill:#f3e5f5,stroke:#ab47bc,color:#6a1b9a</code></pre>"},{"location":"getting-started/quickstart/#schritt-1-ripster-starten","title":"Schritt 1 \u2013 Ripster starten","text":"<pre><code>cd ripster\n./start.sh\n</code></pre> <p>\u00d6ffne http://localhost:5173 im Browser. Das Dashboard zeigt <code>IDLE</code>.</p>"},{"location":"getting-started/quickstart/#schritt-2-disc-einlegen-disc_detected","title":"Schritt 2 \u2013 Disc einlegen \u2192 <code>DISC_DETECTED</code>","text":"<p>Lege eine DVD oder Blu-ray ein. Der <code>diskDetectionService</code> pollt das Laufwerk alle <code>disc_poll_interval_ms</code> Millisekunden (Standard: 4 Sekunden).</p> <p>Was passiert im Code:</p> <ul> <li><code>diskDetectionService</code> emittiert <code>discInserted</code> mit Ger\u00e4teinformationen</li> <li><code>pipelineService.onDiscInserted()</code> wird aufgerufen</li> <li>Dashboard-Status-Badge zeigt \"Medium erkannt\"</li> <li>Status-Text zeigt \"Neue Disk erkannt\"</li> <li>Der \"Analyse starten\"-Button wird aktiv</li> </ul> <p>Manuelle Ausl\u00f6sung</p> <p>Falls die automatische Erkennung nicht greift: <pre><code>curl -X POST http://localhost:3001/api/pipeline/analyze\n</code></pre></p>"},{"location":"getting-started/quickstart/#schritt-3-analyse-starten-metadata_selection","title":"Schritt 3 \u2013 Analyse starten \u2192 <code>METADATA_SELECTION</code>","text":"<p>Klicke auf \"Analyse starten\".</p> <p>Was passiert im Code:</p> <ol> <li>Ein neuer Job-Datensatz wird in der Datenbank angelegt (<code>status: METADATA_SELECTION</code>)</li> <li>Ripster versucht, den Titel automatisch aus dem Disc-Label/Modell zu ermitteln</li> <li>Mit diesem erkannten Titel wird sofort eine OMDb-Suche ausgel\u00f6st</li> <li>Der <code>MetadataSelectionDialog</code> \u00f6ffnet sich im Frontend mit den vorgeladenen Suchergebnissen</li> </ol> <p>Erkannter Titel: Der Disc-Label (z. B. <code>INCEPTION</code>) wird als Suchbegriff verwendet. Falls kein Label vorhanden, bleibt das Suchfeld leer.</p>"},{"location":"getting-started/quickstart/#schritt-4-metadaten-auswahlen-metadataselectiondialog","title":"Schritt 4 \u2013 Metadaten ausw\u00e4hlen (<code>MetadataSelectionDialog</code>)","text":"<p>Der Dialog zeigt vorgeladene OMDb-Suchergebnisse. Du kannst:</p>"},{"location":"getting-started/quickstart/#4a-omdb-suchergebnis-wahlen","title":"4a) OMDb-Suchergebnis w\u00e4hlen","text":"<pre><code>\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Suche: [Inception ] \ud83d\udd0d \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 \u25b6 Inception (2010) \u00b7 Movie \u00b7 tt1375666 \u2502\n\u2502 Inception: ... \u00b7 Series \u00b7 ... \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 [Auswahl \u00fcbernehmen] \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n</code></pre> <ul> <li>Suche durch Titel anpassen und Enter dr\u00fccken</li> <li>Typ-Filter: <code>movie</code> / <code>series</code> umschalten m\u00f6glich</li> <li>Einen Eintrag anklicken, dann \"Auswahl \u00fcbernehmen\"</li> </ul>"},{"location":"getting-started/quickstart/#4b-manuelle-eingabe-ohne-omdb","title":"4b) Manuelle Eingabe (ohne OMDb)","text":"<p>Falls kein passendes Ergebnis gefunden wird: - Titel, Jahr und IMDb-ID manuell eingeben - OMDb-Poster wird \u00fcbersprungen</p> <p>Was passiert nach Best\u00e4tigung:</p> <p>Ripster ruft <code>pipelineService.selectMetadata()</code> auf und startet den n\u00e4chsten Schritt automatisch:</p> <ul> <li>Job wird auf <code>READY_TO_START</code> gesetzt (kurzer \u00dcbergangszustand)</li> <li>Falls bereits RAW vorhanden: direkter Sprung zu <code>MEDIAINFO_CHECK</code></li> <li>Falls kein RAW vorhanden: automatischer Start von <code>RIPPING</code></li> <li>Wenn bereits andere Jobs laufen, landet der Start stattdessen in der Queue</li> </ul>"},{"location":"getting-started/quickstart/#schritt-5-optional-playlist-auswahl-waiting_for_user_decision","title":"Schritt 5 \u2013 Optional: Playlist-Auswahl \u2192 <code>WAITING_FOR_USER_DECISION</code>","text":"<p>Dieser Zustand erscheint nur bei mehrdeutigen Blu-ray-Playlists (typisch nach RAW-Analyse im Backup-Modus).</p> <p>Der Playlist-Auswahl-Dialog erscheint zus\u00e4tzlich (nach dem Metadaten-Dialog):</p> <pre><code>\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Playlist-Auswahl \u2502\n\u2502 Es wurden mehrere Titel mit \u00e4hnlicher Laufzeit gefunden. \u2502\n\u2502 Bitte w\u00e4hle die korrekte Playlist: \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 Playlist \u2502 Laufzeit \u2502 Score \u2502 Bewertung \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 \u25cf 00800 \u2502 2:28:05 \u2502 +18 \u2502 wahrscheinlich korrekt \u2502\n\u2502 \u2502 \u2502 \u2502 (lineare Segmentfolge) \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 \u25cb 00801 \u2502 2:28:12 \u2502 \u22124 \u2502 Auff\u00e4llige Segmentreihenfolge \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 \u25cb 00900 \u2502 2:28:05 \u2502 \u221232 \u2502 Fake-Struktur \u2502\n\u2502 \u2502 \u2502 \u2502 (alternierendes Sprungmuster) \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n 847 Playlists insgesamt \u00b7 3 relevante Kandidaten (\u2265 15 min)\n Empfehlung: 00800 (vorausgew\u00e4hlt)\n [Playlist \u00fcbernehmen]\n</code></pre> <ul> <li>Die empfohlene Playlist ist vorausgew\u00e4hlt (Checkbox)</li> <li>Score und Bewertungslabel helfen bei der Entscheidung</li> <li>Nach \"Playlist \u00fcbernehmen\" setzt Ripster automatisch fort:</li> <li>mit vorhandenem RAW in <code>MEDIAINFO_CHECK</code></li> <li>ohne RAW \u00fcber <code>READY_TO_START</code> weiter Richtung <code>RIPPING</code></li> </ul> <p>Scoring-Details</p> <p>Wie die Scores berechnet werden, erkl\u00e4rt die Playlist-Analyse-Seite.</p>"},{"location":"getting-started/quickstart/#schritt-6-ripping-ripping","title":"Schritt 6 \u2013 Ripping \u2192 <code>RIPPING</code>","text":"<p>Vorher pr\u00fcft Ripster: Existiert bereits eine Raw-Datei f\u00fcr diesen Job?</p> <ul> <li>Ja, Raw-Datei vorhanden \u2192 Direkt zu Schritt 7 (Track-Review), kein erneutes Ripping</li> <li>Nein \u2192 MakeMKV-Ripping startet</li> </ul> <p>Im Standardfall startet Ripster diesen Schritt automatisch nach der Metadaten-Auswahl. Der Button \"Job starten\" ist haupts\u00e4chlich f\u00fcr Sonderf\u00e4lle sichtbar (z. B. Fallback/Queue).</p> <p>Was MakeMKV ausf\u00fchrt (MKV-Modus):</p> <pre><code>makemkvcon mkv disc:0 all /mnt/raw/Inception-2010/ \\\n --minlength=900 -r\n</code></pre> <p>Was MakeMKV ausf\u00fchrt (Backup-Modus):</p> <pre><code>makemkvcon backup disc:0 /mnt/raw/Inception-2010-backup/ \\\n --decrypt -r\n</code></pre> <p>Live-Fortschritt wird aus der MakeMKV-Ausgabe geparst:</p> <pre><code>PRGV:2048,0,65536 \u2192 Fortschritt wird berechnet und per WebSocket gesendet\nPRGT:5011,0,\"Sichern...\" \u2192 Aktueller Task-Name\n</code></pre> <p>Typische Dauer: - DVD: 20\u201345 Minuten - Blu-ray: 45\u2013120 Minuten</p>"},{"location":"getting-started/quickstart/#schritt-7-track-review-ready_to_encode","title":"Schritt 7 \u2013 Track-Review \u2192 <code>READY_TO_ENCODE</code>","text":"<p>Nach dem Ripping, nach Playlist-\u00dcbernahme oder direkt bei vorhandenem RAW startet der HandBrake-Scan:</p> <pre><code>HandBrakeCLI --scan -i &lt;quelle&gt; -t 0\n</code></pre> <p>Dieser Scan liest alle Tracks aus ohne zu encodieren. Ripster baut daraus den Encode-Plan mit automatischer Vorauswahl:</p> <p>Status: <code>MEDIAINFO_CHECK</code> \u2013 l\u00e4uft automatisch, kein Benutzereingriff</p> <p>Danach \u00f6ffnet sich das Encode-Review-Panel (<code>READY_TO_ENCODE</code>):</p> <pre><code>\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Encode-Review \u2502\n\u2502 Titel: Disc Title 1 \u00b7 Laufzeit: 2:28:05 \u00b7 28 Kapitel \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 Audio-Spuren \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 \u2611 \u2502 Track 1: English (AC3, 5.1) \u2502 Copy (ac3) \u2502\n\u2502 \u2611 \u2502 Track 2: Deutsch (DTS, 5.1) \u2502 Fallback Transcode (av_aac)\u2502\n\u2502 \u2610 \u2502 Track 3: Fran\u00e7ais (AC3, 2.0) \u2502 Nicht \u00fcbernommen \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 Untertitel-Spuren \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 \u2611 \u2502 Track 1: Deutsch \u2502 Einbr.\u2610 \u2502Forc.\u2610\u2502Default\u2611 \u2502\n\u2502 \u2610 \u2502 Track 2: English \u2502 Einbr.\u2610 \u2502Forc.\u2610\u2502Default\u2610 \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 [Encoding starten] \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n</code></pre>"},{"location":"getting-started/quickstart/#audio-track-aktionen-verstehen","title":"Audio-Track-Aktionen verstehen","text":"Symbol/Text Bedeutung <code>Copy (ac3)</code> Track wird verlustfrei direkt \u00fcbernommen <code>Copy (truehd)</code> TrueHD-Track wird direkt \u00fcbernommen <code>Transcode (av_aac)</code> Track wird zu AAC umgewandelt <code>Fallback Transcode (av_aac)</code> Copy nicht m\u00f6glich \u2192 automatisch zu AAC <code>Preset-Default (HandBrake)</code> HandBrake-Preset entscheidet <code>Nicht \u00fcbernommen</code> Track ist nicht ausgew\u00e4hlt"},{"location":"getting-started/quickstart/#untertitel-flags","title":"Untertitel-Flags","text":"Flag Bedeutung Einbrennen Untertitel werden fest ins Video gebrannt (nur ein Track m\u00f6glich) Forced Nur erzwungene Untertitel-Einblendungen \u00fcbernehmen Default Diese Spur wird beim Abspielen automatisch aktiviert"},{"location":"getting-started/quickstart/#vorauswahl-regeln","title":"Vorauswahl-Regeln","text":"<p>Die Tracks mit <code>\u2611</code> wurden nach der Regel aus den Einstellungen automatisch vorausgew\u00e4hlt (<code>selectedByRule: true</code>). Die Auswahl kann frei ge\u00e4ndert werden.</p> <p>Klicke \"Encoding starten\" (bzw. im Pre-Rip-Modus \"Backup + Encoding starten\"), um fortzufahren. Falls die Auswahl noch nicht best\u00e4tigt wurde, \u00fcbernimmt das Frontend die Best\u00e4tigung automatisch beim Start.</p>"},{"location":"getting-started/quickstart/#schritt-8-encoding-encoding","title":"Schritt 8 \u2013 Encoding \u2192 <code>ENCODING</code>","text":"<p>HandBrake startet mit dem finalisierten Plan:</p> <pre><code>HandBrakeCLI \\\n -i /dev/sr0 \\\n -o \"/mnt/movies/Inception (2010).mkv\" \\\n -t 1 \\\n --preset \"H.265 MKV 1080p30\" \\\n -a 1,2 \\\n -E copy:ac3,av_aac \\\n -s 1 \\\n --subtitle-default 1\n</code></pre> <p>Live-Fortschritt wird aus HandBrake-stderr geparst:</p> <pre><code>Encoding: task 1 of 1, 73.50 % (45.23 fps, avg 44.12 fps, ETA 00h12m34s)\n</code></pre> <p>Das Dashboard zeigt: - Fortschrittsbalken (0\u2013100 %) - Aktuelle Encoding-Geschwindigkeit (FPS) - Gesch\u00e4tzte Restzeit (ETA)</p> <p>Typische Dauer (abh\u00e4ngig von CPU/GPU und Preset): - Schnelles Preset (<code>fast</code>): 0.5\u00d7 Echtzeit - Standard-Preset: 1\u20133\u00d7 Echtzeit - Langsames Preset (<code>slow</code>): 5\u201310\u00d7 Echtzeit</p>"},{"location":"getting-started/quickstart/#schritt-9-fertig-finished","title":"Schritt 9 \u2013 Fertig! \u2192 <code>FINISHED</code>","text":"<pre><code>/mnt/nas/movies/\n\u2514\u2500\u2500 Inception (2010).mkv \u2713 Encodierung abgeschlossen\n</code></pre> <ul> <li>Job-Status in der Datenbank: <code>FINISHED</code></li> <li>PushOver-Benachrichtigung (falls konfiguriert)</li> <li>Eintrag in der History mit vollst\u00e4ndigen Logs</li> </ul>"},{"location":"getting-started/quickstart/#fehlerbehandlung","title":"Fehlerbehandlung","text":""},{"location":"getting-started/quickstart/#job-im-status-error","title":"Job im Status <code>ERROR</code>","text":"<ol> <li>Dashboard: Details-Button \u2192 Log-Ausgabe pr\u00fcfen</li> <li>Retry: Job vom Fehlerzustand neu starten (beh\u00e4lt Metadaten)</li> <li>History: Vollst\u00e4ndige Logs und Fehlerdetails</li> </ol>"},{"location":"getting-started/quickstart/#haufige-fehlerursachen","title":"H\u00e4ufige Fehlerursachen","text":"Fehler Ursache L\u00f6sung MakeMKV: Lizenzfehler Abgelaufene Beta-Lizenz Neue Lizenz im MakeMKV-Forum HandBrake: Preset nicht gefunden Preset-Name falsch <code>HandBrakeCLI --preset-list</code> pr\u00fcfen Keine Disc erkannt Laufwerk-Berechtigungen <code>sudo chmod a+rw /dev/sr0</code> Falsches Video (zerst\u00fcckelt) Falsche Playlist Job re-encodieren mit anderer Playlist OMDb: Keine Ergebnisse API-Key fehlt oder Titel nicht gefunden Einstellungen pr\u00fcfen; manuell eingeben"},{"location":"getting-started/quickstart/#kurzubersicht-aller-schritte","title":"Kurz\u00fcbersicht aller Schritte","text":"# Status Benutzeraktion Was Ripster tut 1 <code>IDLE</code> Disc einlegen Disc-Polling erkennt Disc 2 <code>DISC_DETECTED</code> \"Analyse starten\" klicken Job anlegen, OMDb vorsuchen 3 <code>METADATA_SELECTION</code> Film im Dialog ausw\u00e4hlen Start automatisch einplanen/ausl\u00f6sen 4 <code>READY_TO_START</code> meist keine \u00dcbergangszustand vor Auto-Start 5 <code>RIPPING</code> Warten MakeMKV rippt, Fortschritt streamen 6 <code>MEDIAINFO_CHECK</code> Warten HandBrake-Scan, Encode-Plan bauen 7 <code>WAITING_FOR_USER_DECISION</code> (optional) Playlist manuell w\u00e4hlen Auf Best\u00e4tigung warten 8 <code>READY_TO_ENCODE</code> Tracks pr\u00fcfen + \"Encoding starten\" Auswahl \u00fcbernehmen, Start ausl\u00f6sen 9 <code>ENCODING</code> Warten HandBrake encodiert, inkl. Post-Skripte 10 <code>FINISHED</code> \u2014 Datei fertig, Benachrichtigung senden"},{"location":"pipeline/","title":"Pipeline","text":"<p>Der Pipeline-Abschnitt beschreibt den Kern-Workflow von Ripster.</p> <ul> <li> <p> Workflow &amp; Zust\u00e4nde</p> <p>Der vollst\u00e4ndige Ripping-Workflow mit allen Zustands\u00fcberg\u00e4ngen.</p> <p> Workflow</p> </li> <li> <p> Encode-Planung</p> <p>Wie Ripster Audio- und Untertitel-Tracks analysiert und Encode-Pl\u00e4ne erstellt.</p> <p> Encoding</p> </li> <li> <p> Playlist-Analyse</p> <p>Erkennung von Blu-ray Playlist-Obfuskierung und Auswahl der korrekten Playlist.</p> <p> Playlist-Analyse</p> </li> <li> <p> Post-Encode-Skripte</p> <p>Automatische Ausf\u00fchrung von Shell-Skripten nach erfolgreichem Encoding \u2013 z. B. zum Verschieben oder Benachrichtigen.</p> <p> Post-Encode-Skripte</p> </li> </ul>"},{"location":"pipeline/encoding/","title":"Encode-Planung &amp; Track-Auswahl","text":"<p><code>encodePlan.js</code> analysiert die HandBrake-Scan-Ausgabe, w\u00e4hlt Audio- und Untertitelspuren anhand von Regeln vor und erstellt einen vollst\u00e4ndigen Encode-Plan f\u00fcr die Benutzer-Review.</p>"},{"location":"pipeline/encoding/#ablauf-im-pipeline-kontext","title":"Ablauf im Pipeline-Kontext","text":"<pre><code>RIPPING abgeschlossen (oder Pre-Rip-Scan)\n \u2193\nHandBrake --scan (alle Titel &amp; Tracks einlesen)\n \u2193\nbuildTrackSelectors() \u2190 Regeln aus Einstellungen ableiten\n \u2193\nselectTrackIds() \u2190 Tracks anhand Regeln vorausw\u00e4hlen\n \u2193\nresolveAudioEncoderAction() \u2190 Encoder-Aktion pro Track bestimmen\n \u2193\nbuildDiscScanReview() \u2190 Vollst\u00e4ndigen Encode-Plan erstellen\n \u2193\nREADY_TO_ENCODE \u2190 Benutzer-Review im Frontend\n \u2193\napplyManualTrackSelectionToPlan() \u2190 Benutzer-Auswahl anwenden\n \u2193\nENCODING \u2190 HandBrake-CLI mit finalem Plan starten\n</code></pre>"},{"location":"pipeline/encoding/#phase-1-pre-rip-track-scan","title":"Phase 1: Pre-Rip Track-Scan","text":"<p>Ripster f\u00fchrt einen HandBrake-Scan bereits vor dem eigentlichen Ripping durch:</p> <pre><code>HandBrakeCLI --scan -i /dev/sr0 -t 0\n</code></pre> <p>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\u00e4tigen.</p> <p>Pre-Rip vs. Post-Rip</p> <p>Ob der Scan vor oder nach dem Ripping passiert, h\u00e4ngt vom konfigurierten Modus ab. Bei direktem Disc-Zugriff ist Pre-Rip m\u00f6glich; nach einem MakeMKV-Backup wird die entstandene <code>.mkv</code>-Datei gescannt.</p>"},{"location":"pipeline/encoding/#phase-2-track-selektor-regeln-buildtrackselectors","title":"Phase 2: Track-Selektor-Regeln (<code>buildTrackSelectors</code>)","text":"<p>Die Regeln werden aus den HandBrake-Einstellungen abgeleitet. Es gibt f\u00fcnf Selektionsmodi:</p> Modus Beschreibung <code>none</code> Keine Tracks dieser Art \u00fcbernehmen <code>first</code> Nur den ersten Track \u00fcbernehmen <code>all</code> Alle Tracks \u00fcbernehmen <code>language</code> Nur Tracks in bestimmten Sprachen <code>explicit</code> Bestimmte Track-IDs explizit angeben <p>Der aktive Modus wird aus den <code>handbrake_*</code>-Einstellungen und <code>handbrake_extra_args</code> abgeleitet. Explizite CLI-Argumente (<code>--audio</code>, <code>--audio-lang-list</code>) \u00fcberschreiben die Basis-Konfiguration.</p>"},{"location":"pipeline/encoding/#phase-3-automatische-vorauswahl-selecttrackids","title":"Phase 3: Automatische Vorauswahl (<code>selectTrackIds</code>)","text":""},{"location":"pipeline/encoding/#audio-tracks","title":"Audio-Tracks","text":"<pre><code>Modus 'none' \u2192 Keine Audio-Tracks\nModus 'all' \u2192 Alle Tracks (oder nur erster, wenn firstOnly)\nModus 'language' \u2192 Alle Tracks in den konfigurierten Sprachen\nModus 'explicit' \u2192 Nur die angegebenen Track-IDs\nModus 'first' \u2192 Nur Track 1\n</code></pre> <p>Jeder Audio-Track erh\u00e4lt das Feld <code>selectedByRule: true/false</code> \u2013 dieses zeigt dem Benutzer, welche Tracks automatisch vorausgew\u00e4hlt wurden.</p> <p>Sprach-Normalisierung (<code>normalizeLanguage</code>):</p> <p>Alle Sprachcodes werden auf ISO 639-2 (3-Buchstaben) normalisiert:</p> Eingabe Normalisiert <code>de</code>, <code>ger</code> <code>deu</code> <code>German</code> <code>deu</code> <code>en</code>, <code>eng</code> <code>eng</code> <code>English</code> <code>eng</code> <code>fr</code>, <code>fre</code> <code>fra</code> <code>ja</code>, <code>jpn</code> <code>jpn</code> Unbekannt <code>und</code>"},{"location":"pipeline/encoding/#untertitel-tracks","title":"Untertitel-Tracks","text":"<p>Gleiche Modus-Logik wie Audio, aber mit zus\u00e4tzlichen Flags pro Track:</p> Flag Bedeutung <code>burnIn</code> Untertitel in Video einbrennen (<code>--subtitle-burned</code>) <code>forced</code> Nur erzwungene Untertitel \u00fcbernehmen (<code>--subtitle-forced</code>) <code>defaultTrack</code> Als Standard-Untertitelspur markieren (<code>--subtitle-default</code>) <p>Diese Flags werden im Encode-Review als Checkboxen angezeigt.</p>"},{"location":"pipeline/encoding/#phase-4-encoder-aktion-bestimmen-resolveaudioencoderaction","title":"Phase 4: Encoder-Aktion bestimmen (<code>resolveAudioEncoderAction</code>)","text":"<p>F\u00fcr jeden vorausgew\u00e4hlten Audio-Track bestimmt Ripster die Encoder-Aktion:</p> <pre><code>Encoder-Einstellung Codec-Support in Copy-Mask? Aktion\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nKein Encoder / 'preset-default' \u2192 preset-default HandBrake-Preset entscheidet\nencoder.startsWith('copy')\n UND Codec in audioCopyMask \u2192 copy Direktkopie (verlustfrei)\n UND Codec NICHT in audioCopyMask\u2192 fallback Transcode mit Fallback-Encoder\nsonstiger Encoder \u2192 transcode Transcode mit explizitem Encoder\n</code></pre> <p>Encoder-Aktionstypen:</p> Typ Label (UI) Qualit\u00e4t <code>preset-default</code> <code>Preset-Default (HandBrake)</code> HandBrake entscheidet <code>copy</code> <code>Copy (ac3)</code> Verlustfrei <code>fallback</code> <code>Fallback Transcode (av_aac)</code> Mit Qualit\u00e4tsverlust <code>transcode</code> <code>Transcode (av_aac)</code> Mit Qualit\u00e4tsverlust <p>Copy-kompatible Codecs (Standard Copy-Mask):</p> Codec Encoder-String AC-3 <code>copy:ac3</code> E-AC-3 <code>copy:eac3</code> AAC <code>copy:aac</code> MP3 <code>copy:mp3</code> TrueHD <code>copy:truehd</code> DTS <code>copy:dts</code> (nur mit spez. HandBrake-Build) DTS-HD <code>copy:dtshd</code> (nur mit spez. HandBrake-Build) <p>DTS im Standard-HandBrake</p> <p>Standard-HandBrake-Builds unterst\u00fctzen kein DTS-Passthrough. DTS-Tracks werden dann automatisch auf den Fallback-Encoder umgestellt (Standard: <code>av_aac</code>).</p>"},{"location":"pipeline/encoding/#phase-5-encode-plan-struktur","title":"Phase 5: Encode-Plan-Struktur","text":"<p>Der vollst\u00e4ndige Plan wird im Job-Datensatz als <code>encode_plan_json</code> gespeichert:</p> <pre><code>{\n \"mode\": \"pre_rip\",\n \"preRip\": true,\n \"encodeInputTitleId\": 1,\n \"encodeInputPath\": \"disc-track-scan://title-1\",\n \"selectors\": {\n \"audio\": { \"mode\": \"language\", \"languages\": [\"deu\", \"eng\"], \"copyMask\": [\"copy:ac3\", \"copy:eac3\"] },\n \"subtitle\": { \"mode\": \"none\" }\n },\n \"titles\": [\n {\n \"id\": 1,\n \"fileName\": \"Disc Title 1\",\n \"durationSeconds\": 8885,\n \"selectedByMinLength\": true,\n \"isEncodeInput\": true,\n \"audioTracks\": [\n {\n \"id\": 1,\n \"sourceTrackId\": 1,\n \"language\": \"eng\",\n \"languageLabel\": \"English\",\n \"title\": \"5.1 Surround\",\n \"format\": \"AC3\",\n \"codecToken\": \"ac3\",\n \"channels\": \"6\",\n \"selectedByRule\": true,\n \"selectedForEncode\": true,\n \"encodePreviewActions\": [\n { \"type\": \"copy\", \"encoder\": \"copy:ac3\", \"label\": \"Copy (ac3)\" }\n ],\n \"encodePreviewSummary\": \"Copy (ac3)\"\n },\n {\n \"id\": 2,\n \"sourceTrackId\": 2,\n \"language\": \"deu\",\n \"languageLabel\": \"Deutsch\",\n \"format\": \"DTS\",\n \"codecToken\": \"dts\",\n \"channels\": \"6\",\n \"selectedByRule\": true,\n \"selectedForEncode\": true,\n \"encodePreviewActions\": [\n { \"type\": \"fallback\", \"encoder\": \"av_aac\", \"label\": \"Fallback Transcode (av_aac)\" }\n ],\n \"encodePreviewSummary\": \"Fallback Transcode (av_aac)\"\n },\n {\n \"id\": 3,\n \"language\": \"fra\",\n \"languageLabel\": \"Fran\u00e7ais\",\n \"selectedByRule\": false,\n \"selectedForEncode\": false,\n \"encodePreviewSummary\": \"Nicht \u00fcbernommen\"\n }\n ],\n \"subtitleTracks\": [\n {\n \"id\": 1,\n \"language\": \"deu\",\n \"selectedByRule\": true,\n \"selectedForEncode\": true,\n \"burnIn\": false,\n \"forced\": false,\n \"defaultTrack\": true,\n \"subtitlePreviewSummary\": \"\u00dcbernehmen\",\n \"subtitlePreviewFlags\": [\"default\"]\n }\n ]\n }\n ]\n}\n</code></pre>"},{"location":"pipeline/encoding/#phase-6-benutzer-review-im-frontend-mediainforeviewpanel","title":"Phase 6: Benutzer-Review im Frontend (<code>MediaInfoReviewPanel</code>)","text":"<p>Das Review-Panel zeigt:</p> <pre><code>\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Encode-Review Titel: Disc Title 1 \u2502\n\u2502 Laufzeit: 2:28:05 \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 Audio-Spuren \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 [\u2713] \u2502 Track 1: English (AC3) \u2502 Copy (ac3) \u2502\n\u2502 [\u2713] \u2502 Track 2: Deutsch (DTS) \u2502 Fallback Transcode (av_aac) \u2502\n\u2502 [ ] \u2502 Track 3: Fran\u00e7ais (DTS) \u2502 Nicht \u00fcbernommen \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 Untertitel-Spuren \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 [\u2713] \u2502 Track 1: Deutsch \u2502Einbr.[ ]\u2502Forced[ ]\u2502Default[\u2713]\u2502\n\u2502 [ ] \u2502 Track 2: English \u2502Einbr.[ ]\u2502Forced[ ]\u2502Default[ ]\u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 [Encoding starten] \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n</code></pre> <p>Der Benutzer kann: - Audio-Tracks per Checkbox aktivieren/deaktivieren - Untertitel-Flags (Einbrennen, Forced, Default) setzen - Mehrere Titel bei der Titleauswahl wechseln (f\u00fcr Discs mit mehreren Haupttiteln)</p>"},{"location":"pipeline/encoding/#phase-7-benutzer-auswahl-anwenden-applymanualtrackselectiontoplan","title":"Phase 7: Benutzer-Auswahl anwenden (<code>applyManualTrackSelectionToPlan</code>)","text":"<p>Im Frontend wird die Benutzer-Auswahl beim Klick auf \"Encoding starten\" (ggf. automatisch) best\u00e4tigt und dann auf den Plan angewendet:</p> <pre><code>Payload: {\n \"selectedEncodeTitleId\": 1,\n \"selectedTrackSelection\": {\n \"1\": {\n \"audioTrackIds\": [1, 2],\n \"subtitleTrackIds\": [1]\n }\n }\n}\n</code></pre> <p>Jeder Track erh\u00e4lt <code>selectedForEncode: true/false</code> entsprechend der Auswahl. Die Encoder-Aktionen (<code>encodeActions</code>) der nicht gew\u00e4hlten Tracks werden geleert.</p>"},{"location":"pipeline/encoding/#phase-8-handbrake-cli-befehl","title":"Phase 8: HandBrake-CLI-Befehl","text":"<p>Aus dem finalisierten Plan baut Ripster den HandBrake-Aufruf:</p> <pre><code>HandBrakeCLI \\\n -i /dev/sr0 \\\n -o \"/mnt/movies/Inception (2010).mkv\" \\\n -t 1 \\\n --preset \"H.265 MKV 1080p30\" \\\n -a 1,2 \\\n -E copy:ac3,av_aac \\\n -s 1 \\\n --subtitle-default 1\n</code></pre> Argument Quelle <code>-i</code> <code>encode_input_path</code> aus Job <code>-o</code> Ausgabepfad aus <code>filename_template</code> + <code>movie_dir</code> <code>-t</code> Gew\u00e4hlter Titel-Index <code>-a</code> Kommagetrennte Audio-Track-IDs der ausgew\u00e4hlten Tracks <code>-E</code> Kommagetrennte Encoder-Aktionen (eine pro Track, gleiche Reihenfolge wie <code>-a</code>) <code>-s</code> Kommagetrennte Untertitel-Track-IDs <code>--subtitle-default</code> Track-ID der als Default markierten Untertitelspur <code>--preset</code> <code>handbrake_preset</code>-Einstellung Extras <code>handbrake_extra_args</code>-Einstellung"},{"location":"pipeline/encoding/#dateiname-template","title":"Dateiname-Template","text":"Platzhalter Wert Beispiel <code>{title}</code> Filmtitel von OMDb <code>Inception</code> <code>{year}</code> Erscheinungsjahr <code>2010</code> <code>{imdb_id}</code> IMDb-ID <code>tt1375666</code> <code>{type}</code> <code>movie</code> oder <code>series</code> <code>movie</code> <p>Sonderzeichen (<code>:</code>, <code>/</code>, <code>?</code>, <code>*</code> etc.) werden automatisch aus dem Dateinamen entfernt.</p>"},{"location":"pipeline/encoding/#re-encoding","title":"Re-Encoding","text":"<p>Ein abgeschlossener Job kann ohne erneutes Ripping neu encodiert werden:</p> <ol> <li>Job in der History \u00f6ffnen</li> <li>\"Re-Encode\" klicken</li> <li>Track-Auswahl anpassen (oder bestehende \u00fcbernehmen)</li> <li>Encoding startet mit den aktuellen <code>handbrake_*</code>-Einstellungen</li> </ol> <p>N\u00fctzlich bei ge\u00e4nderten Presets, anderen Sprach-Pr\u00e4ferenzen oder nach einem Einstellungs-Update.</p>"},{"location":"pipeline/playlist-analysis/","title":"Playlist-Analyse","text":"<p>Einige Blu-rays verwenden Playlist-Obfuskierung als Kopierschutz. Ripster analysiert automatisch alle MakeMKV-Titel und empfiehlt die korrekte Playlist \u2013 auf Basis eines Segment-Scoring-Algorithmus aus <code>playlistAnalysis.js</code>.</p>"},{"location":"pipeline/playlist-analysis/#das-problem-playlist-obfuskierung","title":"Das Problem: Playlist-Obfuskierung","text":"<p>Moderne Blu-rays k\u00f6nnen Dutzende bis Hunderte von Titeln/Playlists enthalten. Der eigentliche Film steckt in genau einer davon \u2013 alle anderen sind:</p> <ul> <li>Kurze Dummy-Titel (wenige Sekunden bis Minuten)</li> <li>Titel mit verschachtelten Segmenten (absichtlich versetzte Reihenfolge, sodass der Film falsch gerippt wird)</li> <li>Titel gleicher L\u00e4nge (mehrere Playlists mit identischer Laufzeit, aber unterschiedlicher Segment-Reihenfolge)</li> </ul> <p>Das Ziel der Obfuskierung: Ein einfacher Ripper w\u00e4hlt den erstbesten langen Titel \u2013 und bekommt ein zerst\u00fcckeltes, unbrauchbares Video.</p>"},{"location":"pipeline/playlist-analysis/#wann-wird-die-analyse-ausgelost","title":"Wann wird die Analyse ausgel\u00f6st?","text":"<p>Die Playlist-Analyse wird automatisch gestartet sobald der Benutzer Metadaten best\u00e4tigt (nach dem Metadaten-Dialog). Ripster ruft <code>makemkvcon</code> im Info-Modus auf und parst die TINFO-Ausgabe.</p> <pre><code>TINFO:&lt;titleId&gt;,26,\"&lt;segment-list&gt;\"\n</code></pre> <p>Feld 26 enth\u00e4lt die kommagetrennte Liste der Segment-Nummern in der Abspielreihenfolge des Titels.</p>"},{"location":"pipeline/playlist-analysis/#algorithmus-im-detail-playlistanalysisjs","title":"Algorithmus im Detail (<code>playlistAnalysis.js</code>)","text":""},{"location":"pipeline/playlist-analysis/#schritt-1-segment-nummern-parsen","title":"Schritt 1 \u2013 Segment-Nummern parsen","text":"<pre><code>TINFO:1,26,\"00000,00001,00002,00003\" \u2192 [0, 1, 2, 3] linearer Film\nTINFO:2,26,\"00100,00050,00100,00051\" \u2192 [100, 50, 100, 51] Fake-Playlist\n</code></pre>"},{"location":"pipeline/playlist-analysis/#schritt-2-metriken-berechnen-computesegmentmetrics","title":"Schritt 2 \u2013 Metriken berechnen (<code>computeSegmentMetrics</code>)","text":"<p>F\u00fcr jedes aufeinanderfolgende Segment-Paar <code>[a, b]</code> wird <code>diff = b \u2212 a</code> berechnet:</p> Metrik Bedingung Bedeutung <code>directSequenceSteps</code> <code>diff == 1</code> Aufeinanderfolgende Segmente \u2192 linearer Film <code>backwardJumps</code> <code>b &lt; a</code> R\u00fcckw\u00e4rtsspr\u00fcnge \u2192 verd\u00e4chtig <code>largeJumps</code> <code>\\|diff\\| &gt; 20</code> Gro\u00dfe Spr\u00fcnge \u2192 verd\u00e4chtig <code>alternatingPairs</code> Gro\u00dfe Spr\u00fcnge mit wechselndem Vorzeichen Hin-und-her-Muster \u2192 starker Fake-Indikator <p>Score-Formel:</p> <pre><code>score = (directSequenceSteps \u00d7 2) \u2212 (backwardJumps \u00d7 3) \u2212 (largeJumps \u00d7 2)\n</code></pre> <p>Konkrete Beispiele:</p> Segmentfolge directSeq backward large score Ergebnis <code>0,1,2,3,4,5</code> 5 0 0 +10 Echter Film <code>0,1,100,2,101,3</code> 2 0 4 -4 Verd\u00e4chtig <code>50,10,60,11,70,12</code> 0 3 3 -15 Fake"},{"location":"pipeline/playlist-analysis/#schritt-3-bewertungslabel-vergeben-buildevaluationlabel","title":"Schritt 3 \u2013 Bewertungslabel vergeben (<code>buildEvaluationLabel</code>)","text":"<pre><code>alternatingRatio = alternatingPairs / largeJumps\n\nif alternatingRatio &gt;= 0.55 AND alternatingPairs &gt;= 3:\n \u2192 \"Fake-Struktur (alternierendes Sprungmuster)\"\n\nelse if backwardJumps &gt; 0 OR largeJumps &gt; 0:\n \u2192 \"Auff\u00e4llige Segmentreihenfolge\"\n\nelse:\n \u2192 \"wahrscheinlich korrekt (lineare Segmentfolge)\"\n</code></pre>"},{"location":"pipeline/playlist-analysis/#schritt-4-duplikat-gruppen-bilden-buildsimilaritygroups","title":"Schritt 4 \u2013 Duplikat-Gruppen bilden (<code>buildSimilarityGroups</code>)","text":"<p>Alle Titel werden nach \u00e4hnlicher Laufzeit gruppiert (\u00b190 Sekunden Toleranz). Gibt es mehrere Kandidaten mit \u00e4hnlicher Laufzeit, ist das ein klares Zeichen f\u00fcr Obfuskierung:</p> <pre><code>8 Titel mit ~148 Minuten Laufzeit \u2192 Duplikat-Gruppe\n\u2192 obfuscationDetected = true\n</code></pre>"},{"location":"pipeline/playlist-analysis/#schritt-5-besten-kandidaten-empfehlen-scorecandidates","title":"Schritt 5 \u2013 Besten Kandidaten empfehlen (<code>scoreCandidates</code>)","text":"<p>Innerhalb der gr\u00f6\u00dften Duplikat-Gruppe werden alle Kandidaten sortiert nach:</p> <ol> <li><code>score</code> (h\u00f6her = besser)</li> <li><code>sequenceCoherence</code> (Anteil linearer Segmentschritte)</li> <li>Laufzeit (l\u00e4nger = besser)</li> <li>Dateigr\u00f6\u00dfe (gr\u00f6\u00dfer = besser als Tiebreaker)</li> </ol> <p>Der erste Kandidat der sortierten Liste ist die Empfehlung.</p>"},{"location":"pipeline/playlist-analysis/#schritt-6-entscheidung-erzwingen-bei-mehreren-kandidaten","title":"Schritt 6 \u2013 Entscheidung erzwingen bei mehreren Kandidaten","text":"<p>Sobald nach <code>MIN_LENGTH_MINUTES</code> mehr als eine Playlist \u00fcbrig bleibt, wird immer eine manuelle Auswahl verlangt:</p> <pre><code>candidateCount &gt; 1 \u2192 manualDecisionRequired = true\ncandidateCount &lt;= 1 \u2192 manualDecisionRequired = false\n</code></pre>"},{"location":"pipeline/playlist-analysis/#wann-greift-der-benutzer-ein","title":"Wann greift der Benutzer ein?","text":"<pre><code>obfuscationDetected = duplicateDurationGroups.length &gt; 0\nmanualDecisionRequired = candidates.length &gt; 1\n</code></pre> Ergebnis N\u00e4chster Pipeline-Zustand Aktion Nur ein Kandidat nach Mindestl\u00e4nge <code>READY_TO_START</code> Automatische \u00dcbernahme m\u00f6glich Mehrere Kandidaten nach Mindestl\u00e4nge <code>WAITING_FOR_USER_DECISION</code> Benutzer muss Playlist ausw\u00e4hlen"},{"location":"pipeline/playlist-analysis/#benutzeroberflache-playlist-auswahl-dialog","title":"Benutzeroberfl\u00e4che: Playlist-Auswahl-Dialog","text":"<p>Wenn <code>manualDecisionRequired = true</code>, \u00f6ffnet sich der Playlist-Dialog nach dem Metadaten-Dialog:</p> <pre><code>\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Playlist-Auswahl \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 Playlist \u2502 Laufzeit \u2502 Score \u2502 Bewertung \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 \u2605 00800 \u2502 2:28:05 \u2502 +18 \u2502 wahrscheinlich korrekt \u2502\n\u2502 \u2502 \u2502 \u2502 (lineare Segmentfolge) \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 00801 \u2502 2:28:12 \u2502 \u22124 \u2502 Auff\u00e4llige Segmentreihenfolge \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 00900 \u2502 2:28:05 \u2502 \u221232 \u2502 Fake-Struktur \u2502\n\u2502 \u2502 \u2502 \u2502 (alternierendes Sprungmuster) \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n Hinweis: 847 Playlists insgesamt. 3 relevante Kandidaten (\u2265 15 min).\n Empfehlung: 00800 (\u2605)\n</code></pre> <ul> <li>\u2605 markiert die empfohlene Playlist (vorausgew\u00e4hlt)</li> <li>Nur Titel \u2265 <code>makemkv_min_length_minutes</code> erscheinen in der Liste</li> <li>Der Benutzer w\u00e4hlt per Radio-Button und klickt \"Best\u00e4tigen\"</li> <li>Erst nach dieser Best\u00e4tigung wechselt die Pipeline zu <code>READY_TO_START</code></li> </ul>"},{"location":"pipeline/playlist-analysis/#vollstandige-datenstruktur-analyzecontextplaylistanalysis","title":"Vollst\u00e4ndige Datenstruktur (<code>analyzeContext.playlistAnalysis</code>)","text":"<pre><code>{\n \"titles\": [\n { \"titleId\": 1, \"playlistId\": \"00800\", \"durationSeconds\": 8885, \"durationLabel\": \"2:28:05\", \"chapters\": 28 }\n ],\n \"candidates\": [\n { \"titleId\": 1, \"playlistId\": \"00800\", \"durationSeconds\": 8885 },\n { \"titleId\": 2, \"playlistId\": \"00801\", \"durationSeconds\": 8892 }\n ],\n \"evaluatedCandidates\": [\n {\n \"titleId\": 1,\n \"playlistId\": \"00800\",\n \"score\": 18,\n \"sequenceCoherence\": 0.95,\n \"evaluationLabel\": \"wahrscheinlich korrekt (lineare Segmentfolge)\",\n \"metrics\": {\n \"directSequenceSteps\": 12,\n \"backwardJumps\": 0,\n \"largeJumps\": 1,\n \"alternatingPairs\": 0\n }\n }\n ],\n \"duplicateDurationGroups\": [\n [\n { \"titleId\": 1, \"playlistId\": \"00800\" },\n { \"titleId\": 2, \"playlistId\": \"00801\" }\n ]\n ],\n \"recommendation\": {\n \"titleId\": 1,\n \"playlistId\": \"00800\",\n \"score\": 18,\n \"reason\": \"H\u00f6chster Segment-Score in der gr\u00f6\u00dften Laufzeit-Gruppe\"\n },\n \"obfuscationDetected\": true,\n \"manualDecisionRequired\": true\n}\n</code></pre>"},{"location":"pipeline/playlist-analysis/#konfiguration","title":"Konfiguration","text":"Einstellung Standard Wirkung <code>makemkv_min_length_minutes</code> <code>15</code> Titel k\u00fcrzer als dieser Wert werden als Kandidaten ignoriert"},{"location":"pipeline/playlist-analysis/#tipps-bei-fehlempfehlung","title":"Tipps bei Fehlempfehlung","text":"<p>Falsche Playlist gew\u00e4hlt?</p> <p>Wenn das resultierende Video zerst\u00fcckelt ist:</p> <ol> <li>Job in der History \u00f6ffnen</li> <li>Re-Encode starten \u2013 diesmal eine andere Playlist w\u00e4hlen</li> <li>Alternativ: Korrekte Playlist im MakeMKV-Forum recherchieren</li> </ol> <p>Keine Segment-Daten verf\u00fcgbar</p> <p>Bei DVDs oder \u00e4lteren Blu-rays liefert MakeMKV manchmal keine Segmentinfos (TINFO-Feld 26 fehlt). In diesem Fall entf\u00e4llt die Analyse und der erste Titel \u00fcber der Mindestl\u00e4nge wird automatisch verwendet.</p>"},{"location":"pipeline/post-encode-scripts/","title":"Post-Encode-Skripte","text":"<p>Post-Encode-Skripte erm\u00f6glichen es, nach erfolgreichem Encoding automatisch beliebige Shell-Befehle oder Programme auszuf\u00fchren \u2013 z. B. zum Verschieben von Dateien, Benachrichtigen externer Dienste oder Ausl\u00f6sen weiterer Verarbeitungsschritte.</p>"},{"location":"pipeline/post-encode-scripts/#funktionsweise","title":"Funktionsweise","text":"<p>Nach einem erfolgreich abgeschlossenen Encoding-Schritt f\u00fchrt Ripster die konfigurierten Skripte sequenziell in der festgelegten Reihenfolge aus:</p> <pre><code>ENCODING abgeschlossen\n \u2193\nSkript 1 ausf\u00fchren \u2190 Fehler? \u2192 Abbruch\n \u2193\nSkript 2 ausf\u00fchren \u2190 Fehler? \u2192 Abbruch\n \u2193\n ...\n \u2193\nFINISHED\n</code></pre> <p>Abbruch bei Fehler</p> <p>Schl\u00e4gt ein Skript fehl (Exit-Code \u2260 0), werden alle nachfolgenden Skripte nicht mehr ausgef\u00fchrt. Der Job bleibt im Abschlusszustand <code>FINISHED</code>; der Fehler wird in Log/Status-Text und im <code>postEncodeScripts</code>-Summary festgehalten.</p>"},{"location":"pipeline/post-encode-scripts/#skript-verwaltung","title":"Skript-Verwaltung","text":"<p>Skripte werden \u00fcber die Einstellungen-Seite angelegt und verwaltet. Sie stehen danach in jedem Encode-Review zur Auswahl.</p>"},{"location":"pipeline/post-encode-scripts/#skript-anlegen","title":"Skript anlegen","text":"<p>Navigiere zu Einstellungen \u2192 Skripte und klicke \"Neues Skript\":</p> Feld Beschreibung Name Anzeigename des Skripts (z. B. <code>Zu Plex verschieben</code>) Befehl Shell-Befehl oder Skriptpfad (z. B. <code>/home/michael/scripts/move-to-plex.sh</code>) Beschreibung Optionale Erkl\u00e4rung"},{"location":"pipeline/post-encode-scripts/#verfugbare-umgebungsvariablen","title":"Verf\u00fcgbare Umgebungsvariablen","text":"<p>Jedes Skript wird mit folgenden Umgebungsvariablen aufgerufen:</p> Variable Inhalt Beispiel <code>RIPSTER_OUTPUT_PATH</code> Absoluter Pfad der encodierten Datei <code>/mnt/movies/Inception (2010).mkv</code> <code>RIPSTER_JOB_ID</code> Job-ID in der Datenbank <code>42</code> <code>RIPSTER_TITLE</code> Filmtitel <code>Inception</code> <code>RIPSTER_YEAR</code> Erscheinungsjahr <code>2010</code> <code>RIPSTER_IMDB_ID</code> IMDb-ID <code>tt1375666</code> <code>RIPSTER_RAW_PATH</code> Pfad zur Raw-MKV-Datei <code>/mnt/raw/Inception-2010/t00.mkv</code>"},{"location":"pipeline/post-encode-scripts/#beispiel-skript-datei-nach-jellyfin-verschieben","title":"Beispiel-Skript: Datei nach Jellyfin verschieben","text":"<pre><code>#!/bin/bash\n# /home/michael/scripts/move-to-jellyfin.sh\n\nTARGET_DIR=\"/mnt/media/movies\"\nmkdir -p \"$TARGET_DIR\"\nmv \"$RIPSTER_OUTPUT_PATH\" \"$TARGET_DIR/\"\necho \"Verschoben: $RIPSTER_TITLE nach $TARGET_DIR\"\n</code></pre>"},{"location":"pipeline/post-encode-scripts/#beispiel-skript-webhook-auslosen","title":"Beispiel-Skript: Webhook ausl\u00f6sen","text":"<pre><code>#!/bin/bash\n# /home/michael/scripts/notify-webhook.sh\n\ncurl -s -X POST https://mein-webhook.example.com/ripster \\\n -H \"Content-Type: application/json\" \\\n -d \"{\\\"title\\\": \\\"$RIPSTER_TITLE\\\", \\\"year\\\": \\\"$RIPSTER_YEAR\\\", \\\"path\\\": \\\"$RIPSTER_OUTPUT_PATH\\\"}\"\n</code></pre>"},{"location":"pipeline/post-encode-scripts/#skript-im-encode-review-auswahlen","title":"Skript im Encode-Review ausw\u00e4hlen","text":"<p>Im <code>READY_TO_ENCODE</code>-Zustand zeigt das MediaInfoReviewPanel einen Skript-Abschnitt:</p> <pre><code>\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Post-Encode-Skripte \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 Ausgew\u00e4hlte Skripte (Reihenfolge per Drag &amp; Drop): \u2502\n\u2502 \u2261 1. Zu Plex verschieben [Entfernen]\u2502\n\u2502 \u2261 2. Webhook ausl\u00f6sen [Entfernen]\u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 Skript hinzuf\u00fcgen: [Zu Jellyfin verschieben \u25be] [+ Hinzuf.]\u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n</code></pre> <ul> <li>Reihenfolge per Drag &amp; Drop \u00e4ndern</li> <li>Hinzuf\u00fcgen aus der Dropdown-Liste aller konfigurierten Skripte</li> <li>Entfernen einzelner Skripte aus der aktuellen Auswahl</li> <li>Skripte k\u00f6nnen pro Job unterschiedlich gew\u00e4hlt werden</li> </ul>"},{"location":"pipeline/post-encode-scripts/#skript-testen","title":"Skript testen","text":"<p>\u00dcber die Einstellungen kann jedes Skript mit einem Test-Job ausgef\u00fchrt werden:</p> <pre><code>POST /api/settings/scripts/:scriptId/test\n</code></pre> <p>Der Test-Aufruf bef\u00fcllt die Umgebungsvariablen mit Platzhalter-Werten.</p>"},{"location":"pipeline/post-encode-scripts/#ausfuhrungs-ergebnis","title":"Ausf\u00fchrungs-Ergebnis","text":"<p>Das Ergebnis der Skript-Ausf\u00fchrung wird im Job-Datensatz gespeichert und in der History angezeigt:</p> <pre><code>{\n \"postEncodeScripts\": {\n \"configured\": 2,\n \"attempted\": 2,\n \"succeeded\": 2,\n \"failed\": 0,\n \"skipped\": 0,\n \"aborted\": false,\n \"results\": [\n {\n \"scriptId\": 1,\n \"scriptName\": \"Zu Plex verschieben\",\n \"status\": \"SUCCESS\"\n },\n {\n \"scriptId\": 2,\n \"scriptName\": \"Webhook ausl\u00f6sen\",\n \"status\": \"SUCCESS\"\n }\n ]\n }\n}\n</code></pre> Feld Beschreibung <code>configured</code> Anzahl ausgew\u00e4hlter Skripte <code>attempted</code> Anzahl tats\u00e4chlich gestarteter Skripte <code>succeeded</code> Erfolgreich ausgef\u00fchrt (Exit-Code 0) <code>failed</code> Fehlgeschlagen <code>skipped</code> Nicht ausgef\u00fchrt (wegen vorherigem Fehler) <code>aborted</code> <code>true</code>, wenn die Kette abgebrochen wurde"},{"location":"pipeline/post-encode-scripts/#api-referenz","title":"API-Referenz","text":"<p>Eine vollst\u00e4ndige API-Dokumentation der Skript-Endpunkte findest du unter:</p> <p> Settings API \u2013 Skripte</p>"},{"location":"pipeline/workflow/","title":"Workflow &amp; Zust\u00e4nde","text":"<p>Der Ripping-Workflow von Ripster ist als State Machine implementiert. Jeder Zustand hat klar definierte \u00dcbergangsbedingungen und Aktionen.</p>"},{"location":"pipeline/workflow/#zustandsdiagramm","title":"Zustandsdiagramm","text":"<pre><code>flowchart LR\n START(( )) --&gt; IDLE\n\n IDLE --&gt;|Disc erkannt| DD[DISC_DETECTED]\n DD --&gt;|Analyse starten| META[METADATA\\nSELECTION]\n\n META --&gt;|Metadaten \u00fcbernommen| RTS[READY_TO\\nSTART]\n META --&gt;|vorhandenes RAW +\\nPlaylist offen| WUD[WAITING_FOR\\nUSER_DECISION]\n\n RTS --&gt;|Auto-Start| RIP[RIPPING]\n RTS --&gt;|Auto-Start mit RAW| MIC[MEDIAINFO\\nCHECK]\n RIP --&gt;|MKV fertig| MIC\n RIP --&gt;|Fehler| ERR\n RIP --&gt;|Abbruch| CAN([CANCELLED])\n\n MIC --&gt;|Playlist offen (Backup)| WUD\n WUD --&gt;|Playlist best\u00e4tigt| MIC\n WUD --&gt;|Playlist best\u00e4tigt,\\nnoch kein RAW| RTS\n MIC --&gt; RTE[READY_TO\\nENCODE]\n RTE --&gt;|Encoding starten\\n(best\u00e4tigt bei Bedarf automatisch)| ENC[ENCODING]\n\n ENC --&gt;|inkl. Post-Skripte| FIN([FINISHED])\n ENC --&gt;|Fehler| ERR\n ENC --&gt;|Abbruch| CAN\n\n ERR([ERROR]) --&gt;|Retry / Cancel| IDLE\n CAN --&gt;|Retry / Neu-Analyse| IDLE\n FIN --&gt;|Neue Disc| IDLE\n\n style FIN fill:#e8f5e9,stroke:#66bb6a,color:#2e7d32\n style ERR fill:#ffebee,stroke:#ef5350,color:#c62828\n style CAN fill:#fff3e0,stroke:#fb8c00,color:#e65100\n style WUD fill:#fff8e1,stroke:#ffa726,color:#e65100\n style ENC fill:#f3e5f5,stroke:#ab47bc,color:#6a1b9a\n style RIP fill:#e3f2fd,stroke:#42a5f5,color:#1565c0\n style MIC fill:#e3f2fd,stroke:#42a5f5,color:#1565c0</code></pre>"},{"location":"pipeline/workflow/#ui-badge-bezeichnungen","title":"UI-Badge-Bezeichnungen","text":"<p>Die Status-Badges im Dashboard verwenden diese Labels:</p> State Badge-Label <code>IDLE</code> <code>Bereit</code> <code>DISC_DETECTED</code> <code>Medium erkannt</code> <code>METADATA_SELECTION</code> <code>Metadatenauswahl</code> <code>WAITING_FOR_USER_DECISION</code> <code>Warte auf Auswahl</code> <code>READY_TO_START</code> <code>Startbereit</code> <code>RIPPING</code> <code>Rippen</code> <code>MEDIAINFO_CHECK</code> <code>Mediainfo-Pruefung</code> <code>READY_TO_ENCODE</code> <code>Bereit zum Encodieren</code> <code>ENCODING</code> <code>Encodieren</code> <code>FINISHED</code> <code>Fertig</code> <code>CANCELLED</code> <code>Abgebrochen</code> <code>ERROR</code> <code>Fehler</code> Queue (kein eigener State) <code>In der Queue</code>"},{"location":"pipeline/workflow/#zustandsbeschreibungen","title":"Zustandsbeschreibungen","text":""},{"location":"pipeline/workflow/#idle","title":"IDLE","text":"<p>Ausgangszustand. Ripster wartet auf eine Disc.</p> <ul> <li><code>diskDetectionService</code> pollt das Laufwerk im konfigurierten Intervall</li> <li>Bei Disc-Erkennung: automatischer \u00dcbergang zu <code>DISC_DETECTED</code></li> <li>WebSocket-Event: <code>DISC_DETECTED</code></li> </ul>"},{"location":"pipeline/workflow/#disc_detected","title":"DISC_DETECTED","text":"<p>Disc erkannt, wartet auf Benutzeraktion.</p> <ul> <li>Dashboard-Badge: \"Medium erkannt\"</li> <li>Status-Text: \"Neue Disk erkannt\"</li> <li>\"Analyse starten\"-Button wird aktiv</li> <li>Kein Prozess l\u00e4uft noch</li> </ul> <p>\u00dcbergang: Benutzer klickt \"Analyse starten\" \u2192 <code>METADATA_SELECTION</code></p>"},{"location":"pipeline/workflow/#metadata_selection","title":"METADATA_SELECTION","text":"<p>Metadaten-Auswahl l\u00e4uft.</p> <ol> <li>Job wird erstellt (<code>status = METADATA_SELECTION</code>)</li> <li>OMDb-Vorsuche mit erkanntem Disc-Label</li> <li><code>MetadataSelectionDialog</code> \u00f6ffnet sich mit vorgeladenen Ergebnissen</li> <li>Benutzer w\u00e4hlt Filmtitel (oder gibt manuell ein)</li> <li>Nach Best\u00e4tigung wird der Job automatisch f\u00fcr Start/Queue vorbereitet (<code>selectMetadata</code> + <code>startPreparedJob</code>)</li> </ol> <p>\u00dcbergang (automatisch nach Metadaten-Best\u00e4tigung):</p> Ergebnis N\u00e4chster Zustand Kein verwertbares RAW vorhanden <code>READY_TO_START</code> \u2192 automatisch <code>RIPPING</code> (oder Queue) Verwertbares RAW vorhanden <code>READY_TO_START</code> \u2192 automatisch <code>MEDIAINFO_CHECK</code> (oder Queue) Vorhandenes RAW + offene Playlist-Entscheidung <code>WAITING_FOR_USER_DECISION</code>"},{"location":"pipeline/workflow/#waiting_for_user_decision","title":"WAITING_FOR_USER_DECISION","text":"<p>Playlist-Obfuskierung erkannt \u2013 manuelle Auswahl erforderlich.</p> <p>Neu seit \u201eSkript Integration + UI Anpassungen\"</p> <p>Dieser Zustand wurde eingef\u00fchrt, um Blu-rays mit mehreren Playlists \u00e4hnlicher L\u00e4nge korrekt zu behandeln.</p> <ul> <li>Playlist-Auswahl-Dialog wird im Dashboard angezeigt</li> <li>Alle Kandidaten mit Score, Laufzeit und Bewertungslabel</li> <li>Empfohlene Playlist ist vorausgew\u00e4hlt</li> <li>Benutzer best\u00e4tigt mit \"Playlist \u00fcbernehmen\"</li> <li>Tritt h\u00e4ufig nach <code>MEDIAINFO_CHECK</code> auf (Backup-Analyse), seltener direkt nach <code>METADATA_SELECTION</code> bei vorhandenem RAW</li> </ul> <p>Darstellung im Dashboard:</p> <pre><code>\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Playlist-Auswahl erforderlich \u2502\n\u2502 Es wurden mehrere Titel mit \u00e4hnlicher Laufzeit gefunden. \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 Playlist \u2502 Laufzeit \u2502 Score \u2502 Bewertung \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 \u25cf 00800 \u2502 2:28:05 \u2502 +18 \u2502 wahrscheinlich korrekt \u2502\n\u2502 \u25cb 00801 \u2502 2:28:12 \u2502 \u22124 \u2502 Auff\u00e4llige Segmentfolge \u2502\n\u2502 \u25cb 00900 \u2502 2:28:05 \u2502 \u221232 \u2502 Fake-Struktur \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n [Playlist \u00fcbernehmen]\n</code></pre> <p>\u00dcbergang: <code>selectMetadata(jobId, { selectedPlaylist })</code> setzt die Pipeline automatisch fort:</p> <ul> <li>mit vorhandenem RAW nach <code>MEDIAINFO_CHECK</code></li> <li>ohne RAW \u00fcber <code>READY_TO_START</code> weiter Richtung <code>RIPPING</code></li> </ul> <p>Mehr Details: Playlist-Analyse</p>"},{"location":"pipeline/workflow/#ready_to_start","title":"READY_TO_START","text":"<p>\u00dcbergangs-/Fallback-Zustand vor dem eigentlichen Start.</p> <ul> <li>Wird nach Metadaten-Best\u00e4tigung kurz gesetzt</li> <li><code>startPreparedJob()</code> wird danach automatisch ausgef\u00fchrt</li> <li>Wenn Parallel-Limit erreicht ist, wird der Start stattdessen in die Queue eingereiht</li> <li>\"Job starten\" ist prim\u00e4r f\u00fcr Sonderf\u00e4lle/Fallback sichtbar</li> </ul> <p>Sonderfall \u2013 RAW-Datei bereits vorhanden: Wenn f\u00fcr diesen Job bereits ein verwertbares RAW unter <code>raw_dir</code> existiert, wird Ripping \u00fcbersprungen und direkt <code>MEDIAINFO_CHECK</code> gestartet.</p> <p>\u00dcbergang: <code>startPreparedJob(jobId)</code> \u2192 <code>RIPPING</code> oder direkt <code>MEDIAINFO_CHECK</code></p>"},{"location":"pipeline/workflow/#ripping","title":"RIPPING","text":"<p>MakeMKV rippt die Disc.</p> MKV-Modus (Standard)Backup-Modus <pre><code>makemkvcon mkv disc:0 all /path/to/raw/ --minlength=900 -r\n</code></pre> <p>Erstellt MKV-Datei(en) direkt aus den gew\u00e4hlten Titeln.</p> <pre><code>makemkvcon backup disc:0 /path/to/raw/backup/ --decrypt -r\n</code></pre> <p>Erstellt vollst\u00e4ndiges Disc-Backup inkl. Men\u00fcs.</p> <p>Live-Updates aus MakeMKV-Ausgabe:</p> <pre><code>PRGV:2048,0,65536 \u2192 Fortschritt-Berechnung\nPRGT:5011,0,\"...\" \u2192 Aktueller Task-Name\n</code></pre> <p>Typische Dauer: DVD 20\u201345 min \u00b7 Blu-ray 45\u2013120 min</p>"},{"location":"pipeline/workflow/#mediainfo_check","title":"MEDIAINFO_CHECK","text":"<p>HandBrake-Scan und Encode-Plan-Erstellung.</p> <p>Dieser Zustand umfasst je nach Quelle mehrere Phasen:</p> <ol> <li>Optional: Playlist-Aufl\u00f6sung bei Blu-ray-Backup (inkl. MakeMKV/HandBrake-Zuordnung)</li> <li>HandBrake-Scan (<code>HandBrakeCLI --scan</code>) auf RAW-Input</li> <li>Encode-Plan-Erstellung mit automatischer Track-Vorauswahl</li> </ol> <p>Kein Benutzereingriff \u2013 l\u00e4uft automatisch durch.</p> <p>\u00dcberg\u00e4nge:</p> <ul> <li>Eindeutige Quelle/Titelwahl m\u00f6glich \u2192 <code>READY_TO_ENCODE</code></li> <li>Mehrdeutige Playlist erkannt \u2192 <code>WAITING_FOR_USER_DECISION</code></li> </ul>"},{"location":"pipeline/workflow/#ready_to_encode","title":"READY_TO_ENCODE","text":"<p>Encode-Plan bereit.</p> <p>Das <code>MediaInfoReviewPanel</code> zeigt:</p> <ul> <li>Titel-Auswahl (bei Discs mit mehreren langen Titeln)</li> <li>Audio-Tracks mit Encoder-Vorschau (Copy/Transcode/Fallback)</li> <li>Untertitel-Tracks mit Flags (Einbrennen, Forced, Default)</li> <li>Post-Encode-Skripte \u2013 Auswahl und Reihenfolge der auszuf\u00fchrenden Skripte</li> </ul> <p>Im Frontend startet \"Encoding starten\" (bzw. \"Backup + Encoding starten\" im Pre-Rip-Modus) den n\u00e4chsten Schritt. Falls die Review noch nicht best\u00e4tigt wurde, wird <code>confirmEncodeReview(...)</code> automatisch vor dem Start aufgerufen.</p> <p>\u00dcbergang: <code>startPreparedJob(jobId)</code> \u2192 <code>ENCODING</code> (oder im Pre-Rip-Fall zuerst <code>RIPPING</code>)</p>"},{"location":"pipeline/workflow/#encoding","title":"ENCODING","text":"<p>HandBrake encodiert die Datei.</p> <pre><code>HandBrakeCLI \\\n -i &lt;quelle&gt; -o &lt;ziel&gt; \\\n -t &lt;titelId&gt; \\\n --preset \"H.265 MKV 1080p30\" \\\n -a 1,2 -E copy:ac3,av_aac \\\n -s 1 --subtitle-default 1\n</code></pre> <p>Live-Updates aus HandBrake-stderr:</p> <pre><code>Encoding: task 1 of 1, 73.50 % (45.23 fps, avg 44.12 fps, ETA 00h12m34s)\n</code></pre> <p>Post-Encode-Skripte werden innerhalb dieses Zustands sequenziell ausgef\u00fchrt (kein separater Pipeline-State).</p> <p>Skriptfehler</p> <p>Skriptfehler f\u00fchren zum Abbruch der Skriptkette, der Job bleibt jedoch im Abschlusszustand <code>FINISHED</code> mit entsprechendem Hinweis im Status-Text/Log.</p>"},{"location":"pipeline/workflow/#finished","title":"FINISHED","text":"<p>Job erfolgreich abgeschlossen.</p> <ul> <li>Ausgabedatei liegt im konfigurierten <code>movie_dir</code></li> <li>Job-Status in Datenbank: <code>FINISHED</code></li> <li>PushOver-Benachrichtigung (falls konfiguriert)</li> <li>WebSocket-Event: <code>PIPELINE_STATE_CHANGED</code> (State <code>FINISHED</code>)</li> </ul>"},{"location":"pipeline/workflow/#cancelled","title":"CANCELLED","text":"<p>Job wurde vom Benutzer abgebrochen.</p> <ul> <li>Entsteht bei aktivem Abbruch (<code>/api/pipeline/cancel</code>) w\u00e4hrend laufender Phase</li> <li>Job-Status in Datenbank: <code>CANCELLED</code></li> <li>Im Dashboard stehen danach u. a. <code>Retry Rippen</code>, <code>Review neu starten</code> oder <code>Encode neu starten</code> (kontextabh\u00e4ngig) zur Verf\u00fcgung</li> </ul>"},{"location":"pipeline/workflow/#error","title":"ERROR","text":"<p>Fehler aufgetreten.</p> <ul> <li>Fehlerdetails im Job-Datensatz gespeichert</li> <li>Fehler-Logs in History abrufbar</li> <li>Retry: Neustart vom Fehlerzustand</li> <li>Neu analysieren: Disc erneut als neuer Job starten</li> </ul>"},{"location":"pipeline/workflow/#abbrechen-retry","title":"Abbrechen &amp; Retry","text":""},{"location":"pipeline/workflow/#pipeline-abbrechen","title":"Pipeline abbrechen","text":"<pre><code>POST /api/pipeline/cancel\n</code></pre> <ul> <li>SIGINT \u2192 graceful exit (Timeout: 10 s) \u2192 SIGKILL</li> <li>Laufender Job landet in <code>CANCELLED</code> (oder Queue-Eintrag wird entfernt, falls noch nicht gestartet)</li> </ul>"},{"location":"pipeline/workflow/#job-wiederholen","title":"Job wiederholen","text":"<pre><code>POST /api/pipeline/retry/:jobId\n</code></pre> <ul> <li>Startet den Job neu in <code>RIPPING</code> (oder reiht den Retry in die Queue ein)</li> <li>Metadaten bleiben erhalten; Encode-/Scan-Daten werden neu erzeugt</li> </ul>"},{"location":"pipeline/workflow/#re-encode","title":"Re-Encode","text":"<pre><code>POST /api/pipeline/reencode/:jobId\n</code></pre> <ul> <li>Encodiert bestehende Raw-MKV neu</li> <li>Erm\u00f6glicht neue Track-Auswahl und andere Skripte</li> <li>Kein Ripping erforderlich</li> </ul>"},{"location":"tools/","title":"Externe Tools","text":"<p>Ripster ist ein Orchestrator \u2013 die eigentliche Arbeit erledigen diese bew\u00e4hrten Open-Source-Tools:</p> <ul> <li> <p> MakeMKV</p> <p>Disc-Analyse und Ripping. Erstellt MKV-Dateien oder vollst\u00e4ndige Backups.</p> <p> MakeMKV</p> </li> <li> <p> HandBrake</p> <p>Video-Encoding mit umfangreichen Preset-Optionen.</p> <p> HandBrake</p> </li> <li> <p> MediaInfo</p> <p>Analyse von Track-Informationen in Mediendateien.</p> <p> MediaInfo</p> </li> </ul>"},{"location":"tools/handbrake/","title":"HandBrake","text":"<p>HandBrake encodiert die rohen MKV-Dateien in das gew\u00fcnschte Format. Ripster nutzt <code>HandBrakeCLI</code>.</p>"},{"location":"tools/handbrake/#verwendeter-befehl","title":"Verwendeter Befehl","text":"<pre><code>HandBrakeCLI \\\n --input \"/mnt/raw/Film_t00.mkv\" \\\n --output \"/mnt/movies/Film (2010).mkv\" \\\n --preset \"H.265 MKV 1080p30\" \\\n --audio 1,2 \\\n --aencoder copy:ac3,ffaac \\\n --subtitle 1 \\\n --subtitle-default 1\n</code></pre>"},{"location":"tools/handbrake/#presets","title":"Presets","text":"<p>HandBrake verwendet Presets f\u00fcr vorkonfigurierte Encoding-Einstellungen.</p>"},{"location":"tools/handbrake/#empfohlene-presets","title":"Empfohlene Presets","text":"Preset Codec Aufl\u00f6sung F\u00fcr <code>H.265 MKV 1080p30</code> HEVC/H.265 1080p Beste Qualit\u00e4t/Gr\u00f6\u00dfe <code>H.265 MKV 720p30</code> HEVC/H.265 720p Kleinere Dateien <code>H.264 MKV 1080p30</code> AVC/H.264 1080p Breiteste Kompatibilit\u00e4t <code>HQ 1080p30 Surround</code> HEVC/H.265 1080p Hohe Qualit\u00e4t mit Surround"},{"location":"tools/handbrake/#alle-presets-anzeigen","title":"Alle Presets anzeigen","text":"<pre><code>HandBrakeCLI --preset-list\n</code></pre>"},{"location":"tools/handbrake/#audio-encoding","title":"Audio-Encoding","text":""},{"location":"tools/handbrake/#copy-kompatible-codecs","title":"Copy-kompatible Codecs","text":"<p>HandBrake kann folgende Codecs direkt kopieren (kein Qualit\u00e4tsverlust):</p> Codec <code>--aencoder</code> Wert AC-3 <code>copy:ac3</code> AAC <code>copy:aac</code> MP3 <code>copy:mp3</code> TrueHD <code>copy:truehd</code> E-AC-3 <code>copy:eac3</code>"},{"location":"tools/handbrake/#transcoding","title":"Transcoding","text":"<p>Codecs die nicht kopiert werden k\u00f6nnen, werden zu AAC transcodiert:</p> Original Transcodiert zu DTS AAC (<code>ffaac</code>) DTS-HD AAC (<code>ffaac</code>)"},{"location":"tools/handbrake/#extra-argumente","title":"Extra-Argumente","text":"<p>\u00dcber die Einstellung <code>handbrake_extra_args</code> k\u00f6nnen beliebige HandBrake-Argumente hinzugef\u00fcgt werden:</p> <pre><code># Cropping deaktivieren\n--crop 0:0:0:0\n\n# Loose Anamorphic\n--loose-anamorphic\n\n# Bestimmte Qualit\u00e4t setzen\n--quality 20\n</code></pre>"},{"location":"tools/handbrake/#fortschritts-parsing","title":"Fortschritts-Parsing","text":"<p>Ripster parst die HandBrake-Ausgabe auf stderr f\u00fcr die Fortschrittsanzeige:</p> <pre><code>Encoding: task 1 of 1, 73.50 % (45.23 fps, avg 44.12 fps, ETA 00h12m34s)\n</code></pre> <p><code>progressParsers.js</code> extrahiert: - Prozentzahl - Aktuelle FPS - ETA</p>"},{"location":"tools/handbrake/#konfiguration-in-ripster","title":"Konfiguration in Ripster","text":"Einstellung Beschreibung <code>handbrake_command</code> Pfad/Befehl f\u00fcr <code>HandBrakeCLI</code> <code>handbrake_preset</code> Preset-Name <code>handbrake_extra_args</code> Zus\u00e4tzliche CLI-Argumente <code>output_extension</code> Dateiendung der Ausgabe"},{"location":"tools/handbrake/#troubleshooting","title":"Troubleshooting","text":""},{"location":"tools/handbrake/#handbrake-findet-preset-nicht","title":"HandBrake findet Preset nicht","text":"<pre><code># Preset-Liste anzeigen\nHandBrakeCLI --preset-list 2&gt;&amp;1 | grep -i \"h.265\"\n</code></pre> <p>Preset-Namen sind case-sensitive!</p>"},{"location":"tools/handbrake/#encoding-sehr-langsam","title":"Encoding sehr langsam","text":"<pre><code># CPU-Encoding-Preset anpassen (schneller = schlechtere Qualit\u00e4t)\nhandbrake_extra_args = --encoder-preset fast\n</code></pre> <p>Verf\u00fcgbare Presets: <code>ultrafast</code>, <code>superfast</code>, <code>veryfast</code>, <code>faster</code>, <code>fast</code>, <code>medium</code>, <code>slow</code>, <code>slower</code>, <code>veryslow</code></p>"},{"location":"tools/handbrake/#gpu-encoding-nutzen-nvidia","title":"GPU-Encoding nutzen (NVIDIA)","text":"<pre><code>handbrake_preset = H.265 NVENC 1080p\n</code></pre> <p>Erfordert HandBrake-Build mit NVENC-Unterst\u00fctzung und NVIDIA-GPU.</p>"},{"location":"tools/makemkv/","title":"MakeMKV","text":"<p>MakeMKV analysiert und rippt DVDs und Blu-rays. Ripster nutzt <code>makemkvcon</code> (die CLI-Version).</p>"},{"location":"tools/makemkv/#verwendete-befehle","title":"Verwendete Befehle","text":""},{"location":"tools/makemkv/#disc-analyse","title":"Disc-Analyse","text":"<pre><code>makemkvcon -r --cache=1 info disc:0\n</code></pre> <p>Gibt alle Titel und Playlists der eingelegten Disc aus. Ripster parst diese Ausgabe um die verf\u00fcgbaren Tracks und Playlists zu bestimmen.</p> <p>Parameter: - <code>-r</code> \u2013 Maschinen-lesbares Ausgabeformat - <code>--cache=1</code> \u2013 Minimaler Disc-Cache - <code>info disc:0</code> \u2013 Informationsabfrage f\u00fcr erstes Laufwerk</p>"},{"location":"tools/makemkv/#mkv-modus-standard","title":"MKV-Modus (Standard)","text":"<pre><code>makemkvcon mkv disc:0 all /path/to/raw/ \\\n --minlength=900 \\\n -r\n</code></pre> <p>Erstellt MKV-Dateien aus allen Titeln, die l\u00e4nger als 15 Minuten sind.</p> <p>Parameter: - <code>mkv</code> \u2013 MKV-Ausgabemodus - <code>disc:0</code> \u2013 Erstes Disc-Laufwerk - <code>all</code> \u2013 Alle passenden Titel (nicht nur einen bestimmten) - <code>--minlength=900</code> \u2013 Mindestl\u00e4nge in Sekunden (entspricht 15 Minuten)</p>"},{"location":"tools/makemkv/#backup-modus","title":"Backup-Modus","text":"<pre><code>makemkvcon backup disc:0 /path/to/raw/backup/ \\\n --decrypt \\\n -r\n</code></pre> <p>Erstellt ein vollst\u00e4ndiges Disc-Backup mit Men\u00fcs.</p> <p>Parameter: - <code>backup</code> \u2013 Backup-Modus - <code>--decrypt</code> \u2013 Verschl\u00fcsselung entfernen</p>"},{"location":"tools/makemkv/#ausgabeformat","title":"Ausgabeformat","text":"<p>MakeMKV gibt Fortschritt und Status in einem strukturierten Format aus:</p> <pre><code>PRGV:current,total,max \u2192 Fortschrittsbalken-Werte\nPRGT:code,id,\"Beschreibung\" \u2192 Aktueller Task\nPRGC:code,id,\"Beschreibung\" \u2192 Aktueller Sub-Task\nMSG:code,flags,count,\"Text\" \u2192 Nachricht\n</code></pre> <p>Ripster's <code>progressParsers.js</code> parst diese Ausgabe f\u00fcr die Live-Fortschrittsanzeige.</p>"},{"location":"tools/makemkv/#makemkv-lizenz","title":"MakeMKV-Lizenz","text":"<p>MakeMKV ist Beta-Software und kostenlos f\u00fcr den pers\u00f6nlichen Gebrauch w\u00e4hrend der Beta-Phase. Eine Beta-Lizenz ist regelm\u00e4\u00dfig im MakeMKV-Forum verf\u00fcgbar.</p> <p>Ohne g\u00fcltige Lizenz k\u00f6nnen Blu-rays nicht entschl\u00fcsselt werden.</p>"},{"location":"tools/makemkv/#lizenz-eintragen","title":"Lizenz eintragen","text":"<p>Die Lizenz wird in den MakeMKV-Einstellungen eingetragen (GUI) oder direkt in:</p> <pre><code>~/.MakeMKV/settings.conf\n</code></pre> <pre><code>app_Key = \"XXXX-XXXX-XXXX-XXXX-XXXX\"\n</code></pre>"},{"location":"tools/makemkv/#konfiguration-in-ripster","title":"Konfiguration in Ripster","text":"Einstellung Beschreibung <code>makemkv_command</code> Pfad/Befehl f\u00fcr <code>makemkvcon</code> <code>makemkv_min_length_minutes</code> Mindest-Titell\u00e4nge (Standard: 15 Min) <code>makemkv_backup_mode</code> Backup-Modus statt MKV"},{"location":"tools/makemkv/#troubleshooting","title":"Troubleshooting","text":""},{"location":"tools/makemkv/#makemkv-erkennt-disc-nicht","title":"MakeMKV erkennt Disc nicht","text":"<pre><code># Laufwerk-Berechtigungen pr\u00fcfen\nls -la /dev/sr0\nsudo chmod a+rw /dev/sr0\n\n# Oder Benutzer zur Gruppe cdrom hinzuf\u00fcgen\nsudo usermod -a -G cdrom $USER\n</code></pre>"},{"location":"tools/makemkv/#langer-analyseprozess","title":"Langer Analyseprozess","text":"<p>Blu-ray-Analyse kann bei Discs mit vielen Playlists 5+ Minuten dauern. Dies ist normal.</p>"},{"location":"tools/makemkv/#fehlermeldung-libmmbd","title":"Fehlermeldung: \"LibMMBD\"","text":"<p>LibMMBD ist MakeMKVs interne Verschl\u00fcsselungsbibliothek. Bei Fehlern die MakeMKV-Version aktualisieren.</p>"},{"location":"tools/mediainfo/","title":"MediaInfo","text":"<p>MediaInfo analysiert die Track-Struktur von Mediendateien. Ripster nutzt es nach dem Ripping um Audio- und Untertitelspuren zu identifizieren.</p>"},{"location":"tools/mediainfo/#verwendeter-befehl","title":"Verwendeter Befehl","text":"<pre><code>mediainfo --Output=JSON /path/to/raw/film.mkv\n</code></pre> <p>Gibt vollst\u00e4ndige Track-Informationen als JSON zur\u00fcck.</p>"},{"location":"tools/mediainfo/#ausgabe-struktur","title":"Ausgabe-Struktur","text":"<pre><code>{\n \"media\": {\n \"track\": [\n {\n \"@type\": \"General\",\n \"Duration\": \"8885.042\",\n \"Format\": \"Matroska\"\n },\n {\n \"@type\": \"Video\",\n \"Format\": \"HEVC\",\n \"Width\": \"1920\",\n \"Height\": \"1080\",\n \"FrameRate\": \"23.976\"\n },\n {\n \"@type\": \"Audio\",\n \"StreamOrder\": \"1\",\n \"Format\": \"TrueHD\",\n \"Channels\": \"8\",\n \"Language\": \"en\"\n },\n {\n \"@type\": \"Audio\",\n \"StreamOrder\": \"2\",\n \"Format\": \"AC-3\",\n \"Channels\": \"6\",\n \"Language\": \"de\"\n },\n {\n \"@type\": \"Text\",\n \"StreamOrder\": \"1\",\n \"Format\": \"UTF-8\",\n \"Language\": \"de\"\n }\n ]\n }\n}\n</code></pre>"},{"location":"tools/mediainfo/#verarbeitung-in-ripster","title":"Verarbeitung in Ripster","text":"<p><code>encodePlan.js</code> verarbeitet die MediaInfo-Ausgabe:</p> <ol> <li>Track-Extraktion: Alle Audio- und Untertitel-Tracks werden extrahiert</li> <li>Sprach-Normalisierung: Sprachcodes werden auf ISO 639-3 normalisiert</li> <li>Codec-Klassifizierung: Bestimmt ob Codec kopiert oder transcodiert werden kann</li> <li>Track-Labels: Benutzerfreundliche Bezeichnungen (z.B. \"Deutsch (AC-3, 5.1)\")</li> </ol>"},{"location":"tools/mediainfo/#track-label-format","title":"Track-Label-Format","text":"<pre><code>{Sprache} ({Format}, {Kan\u00e4le})\n</code></pre> <p>Beispiele: - <code>Deutsch (AC-3, 5.1)</code> - <code>English (TrueHD, 7.1)</code> - <code>Fran\u00e7ais (AC-3, 2.0)</code></p>"},{"location":"tools/mediainfo/#konfiguration-in-ripster","title":"Konfiguration in Ripster","text":"Einstellung Beschreibung <code>mediainfo_command</code> Pfad/Befehl f\u00fcr <code>mediainfo</code>"},{"location":"tools/mediainfo/#troubleshooting","title":"Troubleshooting","text":""},{"location":"tools/mediainfo/#mediainfo-gibt-kein-json-aus","title":"MediaInfo gibt kein JSON aus","text":"<pre><code># Version pr\u00fcfen\nmediainfo --Version\n\n# JSON-Ausgabe testen\nmediainfo --Output=JSON /path/to/test.mkv\n</code></pre> <p>MediaInfo &gt;= 17.10 wird empfohlen.</p>"},{"location":"tools/mediainfo/#sprache-als-und-angezeigt","title":"Sprache als \"und\" angezeigt","text":"<p><code>und</code> steht f\u00fcr \"undetermined\" \u2013 die Sprache ist in der MKV-Datei nicht getaggt. Dies ist bei manchen Rips normal. Der Track wird trotzdem angezeigt und kann manuell ausgew\u00e4hlt werden.</p>"}]}