diff --git a/.gitignore b/.gitignore index 3bdae91..14da8fa 100644 --- a/.gitignore +++ b/.gitignore @@ -74,3 +74,9 @@ frontend/.env.* Thumbs.db .idea/ .vscode/ + +# ---------------------------- +# Scripts +# ---------------------------- +deploy-ripster.sh +build-handbrake-nvdec.sh \ No newline at end of file diff --git a/README.md b/README.md index 0d95ad6..8a020b1 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,21 @@ # Ripster -Ripster ist eine lokale Web-Anwendung für halbautomatisches Disc-Ripping mit MakeMKV + HandBrake inklusive Metadaten-Auswahl, Titel-/Spurprüfung und Job-Historie. - ---- - -> **Neu seit letztem Release** -> -> - **Profil-spezifische Einstellungen** – Separate Konfiguration für Blu-ray und DVD: eigene Pfade, HandBrake-Presets, Rip-Modi, Extra-Args, Dateinamen-Templates; automatische Auflösung anhand des erkannten Medientyps -> - **Cron-Job-System** – Skripte und Skript-Ketten zeitgesteuert ausführen; eigener Expression-Parser, Ausführungs-Logs, manuelle Auslösung, PushOver-Integration pro Job -> - **User-Presets** – benannte HandBrake-Preset-Sammlungen (Preset + Extra-Args) pro Medientyp anlegen und im Review-Panel auswählen -> - **DVD-/Blu-ray-Erkennung verbessert** – robuste Media-Profil-Erkennung aus UDF/ISO9660-Dateisystemtyp, Laufwerk-Modell und Disc-Label; Medientyp-Indikator in der UI -> - **Pre-Encode-Ausführungen** – Skripte und Ketten können nun auch *vor* dem Encode-Schritt ausgeführt werden (zusätzlich zu Post-Encode) -> - **Sortierbare Skripte & Ketten** – Reihenfolge über Drag & Drop festlegen; wird persistent gespeichert -> - **Granulares PushOver** – je Event konfigurierbar (Metadaten bereit, Rip-Start, Encode-Start, Fertig, Fehler, Abbruch, Re-Encode) -> - **`rip_successful`-Flag in Jobs** – separates Feld zur Nachverfolgung ob der Rip-Schritt abgeschlossen wurde (unabhängig vom Encode-Status) -> - **`handbrake_restart_delete_incomplete_output`** – unvollständige Ausgabe wird beim Encode-Neustart automatisch gelöscht +Ripster ist eine lokale Web-Anwendung für halbautomatisches Disc-Ripping mit MakeMKV + HandBrake inklusive Metadaten-Auswahl, Track-Review, Queue, Skripten/Ketten und Job-Historie. --- ## Was Ripster kann - Disc-Erkennung mit Pipeline-Status in Echtzeit (WebSocket) -- Robuste Erkennung von Blu-ray, DVD und CD (UDF/ISO9660-Heuristik + Laufwerk-Modell) +- Medienprofil-Erkennung (`bluray`/`dvd`/`other`) aus Device-/Filesystem-Heuristik - Metadaten-Suche und Zuordnung über OMDb -- MakeMKV-Analyse und Rip (MKV oder Backup-Modus) -- HandBrake-Encode mit Preset + Extra-Args + Track-Override -- Manuelle Playlist-/Titel-Auswahl bei komplexen Blu-rays -- Pre- und Post-Encode-Skripte & Skript-Ketten (inkl. Drag-and-Drop-Sortierung) -- Cron-Jobs: Skripte und Ketten zeitgesteuert ausführen (eigener Expression-Parser, Logs, PushOver) -- Historie mit Re-Encode, Löschfunktionen und Detailansicht -- Dateibasierte Logs (Backend + Job-Prozesslogs) +- MakeMKV-Analyse und Rip (`mkv` oder `backup`) mit profilspezifischen Settings +- HandBrake-Review und Encoding mit Track-Auswahl, User-Presets, Extra-Args +- Pre- und Post-Encode-Ausführungen (Skripte und/oder Skript-Ketten) +- Pipeline-Queue mit Job- und Nicht-Job-Einträgen (`script`, `chain`, `wait`) +- Cron-Jobs für Skripte/Ketten (inkl. Logs und manueller Auslösung) +- Historie mit Re-Encode, Review-Neustart, File-/Job-Löschung und Orphan-Import +- Hardware-Monitoring (CPU/RAM/GPU/Storage) im Dashboard ## Tech-Stack @@ -39,7 +25,7 @@ Ripster ist eine lokale Web-Anwendung für halbautomatisches Disc-Ripping mit Ma ## Voraussetzungen -- Linux-System mit optischem Laufwerk (oder gemountete Quelle) +- Linux-System mit optischem Laufwerk (oder gemounteter Quelle) - Node.js `>= 20.19.0` (siehe [.nvmrc](.nvmrc)) - Installierte CLI-Tools im `PATH`: - `makemkvcon` @@ -54,7 +40,7 @@ Ripster ist eine lokale Web-Anwendung für halbautomatisches Disc-Ripping mit Ma `start.sh` erledigt: -1. Node-Version prüfen/umschalten (inkl. `nvm`/`node@20`-Fallback) +1. Node-Version prüfen/umschalten (`nvm`/`npx node@20` Fallback) 2. Dependencies installieren (Root, Backend, Frontend) 3. Dev-Umgebung starten (`backend` + `frontend`) @@ -63,11 +49,7 @@ Danach: - Frontend: `http://localhost:5173` - Backend API: `http://localhost:3001/api` -Stoppen: - -```bash -./kill.sh -``` +Stoppen: laufenden Prozess mit `Ctrl+C` im Terminal beenden. ## Manueller Start @@ -99,23 +81,23 @@ npm run start ## Konfiguration -### 1) UI-Settings (empfohlen) +### UI-Settings (empfohlen) Die meisten Einstellungen werden in der App unter `Settings` gepflegt und in SQLite gespeichert: -- Pfade: `raw_dir`, `movie_dir`, `log_dir` +- Pfade: `raw_dir[_bluray/_dvd/_other]`, `movie_dir[_bluray/_dvd/_other]`, `log_dir` - Tools: `makemkv_command`, `handbrake_command`, `mediainfo_command` -- Encode: `handbrake_preset`, `handbrake_extra_args`, `output_extension`, `filename_template` -- Laufwerk/Scan: `drive_mode`, `drive_device`, Polling +- Profile: `*_bluray` / `*_dvd` Varianten für Rip-/Encode-Verhalten +- Queue/Monitoring: `pipeline_max_parallel_jobs`, `hardware_monitoring_*` - Benachrichtigungen: PushOver -### 2) Umgebungsvariablen +### Umgebungsvariablen Backend (`backend/src/config.js`): - `PORT` (Default: `3001`) - `DB_PATH` (Default: `backend/data/ripster.db`) -- `LOG_DIR` (Fallback-Logpfad, Default: `backend/logs`) +- `LOG_DIR` (Default: `backend/logs`) - `CORS_ORIGIN` (Default: `*`) - `LOG_LEVEL` (`debug|info|warn|error`, Default: `info`) @@ -123,7 +105,7 @@ Frontend (Vite): - `VITE_API_BASE` (Default: `/api`) - `VITE_WS_URL` (optional, überschreibt automatische WS-URL) -- `VITE_PUBLIC_ORIGIN`, `VITE_ALLOWED_HOSTS`, `VITE_HMR_*` (Remote-Dev/HMR) +- optional für Remote-Dev: `VITE_PUBLIC_ORIGIN`, `VITE_ALLOWED_HOSTS`, `VITE_HMR_PROTOCOL`, `VITE_HMR_HOST`, `VITE_HMR_CLIENT_PORT` ## Logs und Daten @@ -131,9 +113,9 @@ Log-Ziel ist primär der in den Settings gepflegte `log_dir`. - Backend-Logs: `/backend/backend-latest.log` und Tagesdateien - Job-Logs: `/job-.process.log` -- DB: `backend/data/ripster.db` (inkl. Job-/Settings-Daten) +- DB: `backend/data/ripster.db` -Hinweis: Beim DB-Init wird das Schema gegen die Soll-Struktur abgeglichen und migriert. +Hinweis: Beim DB-Init wird das Schema geprüft und fehlende Elemente werden migriert. ## Projektstruktur @@ -150,28 +132,55 @@ ripster/ pages/ components/ api/ + db/schema.sql start.sh - kill.sh - deploy-ripster.sh + install.sh + install-dev.sh ``` ## API-Überblick +**Health** +- `GET /api/health` + **Pipeline** - `GET /api/pipeline/state` - `POST /api/pipeline/analyze` +- `POST /api/pipeline/rescan-disc` +- `POST /api/pipeline/select-metadata` - `POST /api/pipeline/start/:jobId` - `POST /api/pipeline/confirm-encode/:jobId` +- `POST /api/pipeline/cancel` +- `POST /api/pipeline/retry/:jobId` +- `POST /api/pipeline/reencode/:jobId` +- `POST /api/pipeline/restart-review/:jobId` +- `POST /api/pipeline/restart-encode/:jobId` +- `POST /api/pipeline/resume-ready/:jobId` +- `GET /api/pipeline/queue` +- `POST /api/pipeline/queue/reorder` +- `POST /api/pipeline/queue/entry` +- `DELETE /api/pipeline/queue/entry/:entryId` **History** - `GET /api/history` - `GET /api/history/:id` +- `GET /api/history/database` +- `GET /api/history/orphan-raw` +- `POST /api/history/orphan-raw/import` +- `POST /api/history/:id/omdb/assign` +- `POST /api/history/:id/delete-files` +- `POST /api/history/:id/delete` **Settings** - `GET /api/settings` +- `PUT /api/settings/:key` - `PUT /api/settings` +- `GET/POST/PUT/DELETE /api/settings/scripts...` +- `GET/POST/PUT/DELETE /api/settings/script-chains...` +- `GET/POST/PUT/DELETE /api/settings/user-presets...` +- `POST /api/settings/pushover/test` -**Cron-Jobs** _(neu)_ +**Cron-Jobs** - `GET /api/crons` - `POST /api/crons` - `GET /api/crons/:id` @@ -185,17 +194,16 @@ ripster/ - WebSocket verbindet nicht: - prüfen, ob Frontend über Vite-Proxy läuft (`/ws` -> Backend) - - bei Reverse-Proxy `VITE_PUBLIC_ORIGIN`/HMR korrekt setzen + - bei Reverse-Proxy Upgrade-Header für `/ws` setzen - Keine Disc erkannt: - `drive_mode=explicit` testen und `drive_device` setzen (z. B. `/dev/sr0`) - HandBrake/MakeMKV Fehler: - CLI-Binaries im `PATH` prüfen - - Preset-Name exakt wie in `HandBrakeCLI -z` hinterlegen + - Preset-Name mit `HandBrakeCLI -z` prüfen - Startfehler wegen Schema: - - sicherstellen, dass die erwartete Schema-Datei vorhanden ist (`db/schema.sql`) + - `db/schema.sql` vorhanden halten ## Sicherheit - Keine echten Tokens/Passwörter ins Repository committen. - Lokale Secrets in `.env` oder in Settings pflegen, aber nicht versionieren. - diff --git a/backend/src/services/historyService.js b/backend/src/services/historyService.js index a103599..357db06 100644 --- a/backend/src/services/historyService.js +++ b/backend/src/services/historyService.js @@ -21,6 +21,8 @@ function parseJsonSafe(raw, fallback = null) { const PROCESS_LOG_TAIL_MAX_BYTES = 1024 * 1024; const processLogStreams = new Map(); const PROFILE_PATH_SUFFIXES = ['bluray', 'dvd', 'other']; +const RAW_INCOMPLETE_PREFIX = 'Incomplete_'; +const RAW_RIP_COMPLETE_PREFIX = 'Rip_Complete_'; function inspectDirectory(dirPath) { if (!dirPath) { @@ -430,9 +432,29 @@ function normalizeComparablePath(inputPath) { return resolveSafe(String(inputPath || '')).replace(/[\\/]+$/, ''); } +function stripRawFolderStatePrefix(folderName) { + const rawName = String(folderName || '').trim(); + if (!rawName) { + return ''; + } + return rawName + .replace(new RegExp(`^${RAW_INCOMPLETE_PREFIX}`, 'i'), '') + .replace(new RegExp(`^${RAW_RIP_COMPLETE_PREFIX}`, 'i'), '') + .trim(); +} + +function applyRawFolderPrefix(folderName, prefix = '') { + const normalized = stripRawFolderStatePrefix(folderName); + if (!normalized) { + return normalized; + } + const safePrefix = String(prefix || '').trim(); + return safePrefix ? `${safePrefix}${normalized}` : normalized; +} + function parseRawFolderMetadata(folderName) { const rawName = String(folderName || '').trim(); - const normalizedRawName = rawName.replace(/^Incomplete_/i, '').trim(); + const normalizedRawName = stripRawFolderStatePrefix(rawName); const folderJobIdMatch = normalizedRawName.match(/-\s*RAW\s*-\s*job-(\d+)\s*$/i); const folderJobId = folderJobIdMatch ? Number(folderJobIdMatch[1]) : null; let working = normalizedRawName.replace(/\s*-\s*RAW\s*-\s*job-\d+\s*$/i, '').trim(); @@ -1053,6 +1075,7 @@ class HistoryService { detectedTitle: effectiveTitle }); + const renameSteps = []; let finalRawPath = absRawPath; const renamedRawPath = buildRawPathForJobId(absRawPath, created.id); const shouldRenameRawFolder = normalizeComparablePath(renamedRawPath) !== absRawPath; @@ -1067,6 +1090,7 @@ class HistoryService { try { fs.renameSync(absRawPath, renamedRawPath); finalRawPath = normalizeComparablePath(renamedRawPath); + renameSteps.push({ from: absRawPath, to: finalRawPath }); } catch (error) { await db.run('DELETE FROM jobs WHERE id = ?', [created.id]); const wrapped = new Error(`RAW-Ordner konnte nicht auf neue Job-ID umbenannt werden: ${error.message}`); @@ -1075,6 +1099,30 @@ class HistoryService { } } + const ripCompleteFolderName = applyRawFolderPrefix(path.basename(finalRawPath), RAW_RIP_COMPLETE_PREFIX); + const ripCompleteRawPath = path.join(path.dirname(finalRawPath), ripCompleteFolderName); + const shouldMarkRipComplete = normalizeComparablePath(ripCompleteRawPath) !== normalizeComparablePath(finalRawPath); + if (shouldMarkRipComplete) { + if (fs.existsSync(ripCompleteRawPath)) { + await db.run('DELETE FROM jobs WHERE id = ?', [created.id]); + const error = new Error(`RAW-Ordner für Rip_Complete-Zustand existiert bereits: ${ripCompleteRawPath}`); + error.statusCode = 409; + throw error; + } + + try { + const previousRawPath = finalRawPath; + fs.renameSync(previousRawPath, ripCompleteRawPath); + finalRawPath = normalizeComparablePath(ripCompleteRawPath); + renameSteps.push({ from: previousRawPath, to: finalRawPath }); + } catch (error) { + await db.run('DELETE FROM jobs WHERE id = ?', [created.id]); + const wrapped = new Error(`RAW-Ordner konnte nicht als Rip_Complete markiert werden: ${error.message}`); + wrapped.statusCode = 500; + throw wrapped; + } + } + await this.updateJob(created.id, { status: 'FINISHED', last_state: 'FINISHED', @@ -1105,8 +1153,8 @@ class HistoryService { await this.appendLog( created.id, 'SYSTEM', - shouldRenameRawFolder - ? `Historieneintrag aus RAW erstellt. Ordner umbenannt: ${absRawPath} -> ${finalRawPath}` + renameSteps.length > 0 + ? `Historieneintrag aus RAW erstellt. Ordner umbenannt: ${renameSteps.map((step) => `${step.from} -> ${step.to}`).join(' | ')}` : `Historieneintrag aus bestehendem RAW-Ordner erstellt: ${finalRawPath}` ); if (metadata.imdbId) { diff --git a/backend/src/services/pipelineService.js b/backend/src/services/pipelineService.js index 13daac0..d5f148d 100644 --- a/backend/src/services/pipelineService.js +++ b/backend/src/services/pipelineService.js @@ -60,6 +60,12 @@ const POST_ENCODE_PROGRESS_RESERVE = 10; const POST_ENCODE_FINISH_BUFFER = 1; const MIN_EXTENSIONLESS_DISC_IMAGE_BYTES = 256 * 1024 * 1024; const RAW_INCOMPLETE_PREFIX = 'Incomplete_'; +const RAW_RIP_COMPLETE_PREFIX = 'Rip_Complete_'; +const RAW_FOLDER_STATES = Object.freeze({ + INCOMPLETE: 'incomplete', + RIP_COMPLETE: 'rip_complete', + COMPLETE: 'complete' +}); function nowIso() { return new Date().toISOString(); @@ -929,6 +935,18 @@ function parseHandBrakeDurationSeconds(rawDuration) { return 0; } +function formatDurationClock(seconds) { + const total = Number(seconds || 0); + if (!Number.isFinite(total) || total <= 0) { + return null; + } + const rounded = Math.max(0, Math.trunc(total)); + const h = Math.floor(rounded / 3600); + const m = Math.floor((rounded % 3600) / 60); + const s = rounded % 60; + return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; +} + function normalizeTrackLanguage(raw) { const value = String(raw || '').trim(); if (!value) { @@ -937,6 +955,91 @@ function normalizeTrackLanguage(raw) { return value.toLowerCase().slice(0, 3); } +function normalizePositiveTrackId(rawValue) { + const parsed = Number(rawValue); + if (!Number.isFinite(parsed) || parsed <= 0) { + return null; + } + return Math.trunc(parsed); +} + +function isLikelyForcedSubtitleTrack(track) { + const text = [ + track?.title, + track?.description, + track?.name, + track?.format, + track?.label + ] + .map((value) => String(value || '').trim().toLowerCase()) + .filter(Boolean) + .join(' '); + if (!text) { + return false; + } + if (/\bnot forced\b/.test(text)) { + return false; + } + return ( + /\bforced(?:\s+only)?\b/.test(text) + || /nur\s+erzwungen/.test(text) + || /\berzwungen\b/.test(text) + ); +} + +function annotateSubtitleForcedAvailability(handBrakeSubtitleTracks, makeMkvSubtitleTracks) { + const hbTracks = Array.isArray(handBrakeSubtitleTracks) ? handBrakeSubtitleTracks : []; + if (hbTracks.length === 0) { + return []; + } + + const mkTracks = Array.isArray(makeMkvSubtitleTracks) ? makeMkvSubtitleTracks : []; + const forcedSourceIdsByLanguage = new Map(); + + for (const track of mkTracks) { + if (!isLikelyForcedSubtitleTrack(track)) { + continue; + } + const language = normalizeTrackLanguage(track?.language || track?.languageLabel || 'und'); + const sourceTrackId = normalizePositiveTrackId(track?.sourceTrackId ?? track?.id); + if (!sourceTrackId) { + continue; + } + if (!forcedSourceIdsByLanguage.has(language)) { + forcedSourceIdsByLanguage.set(language, []); + } + const list = forcedSourceIdsByLanguage.get(language); + if (!list.includes(sourceTrackId)) { + list.push(sourceTrackId); + } + } + + return hbTracks.map((track) => { + const language = normalizeTrackLanguage(track?.language || track?.languageLabel || 'und'); + const forcedSourceTrackIds = normalizeTrackIdList(forcedSourceIdsByLanguage.get(language) || []); + const forcedTrack = isLikelyForcedSubtitleTrack(track); + return { + ...track, + forcedTrack, + forcedAvailable: forcedTrack || forcedSourceTrackIds.length > 0, + forcedSourceTrackIds + }; + }); +} + +function enrichTitleInfoWithForcedSubtitleAvailability(titleInfo, makeMkvSubtitleTracks) { + if (!titleInfo || typeof titleInfo !== 'object') { + return titleInfo; + } + return { + ...titleInfo, + subtitleTracks: annotateSubtitleForcedAvailability( + Array.isArray(titleInfo?.subtitleTracks) ? titleInfo.subtitleTracks : [], + makeMkvSubtitleTracks + ) + }; +} + function pickScanTitleList(scanJson) { if (!scanJson || typeof scanJson !== 'object') { return []; @@ -1138,43 +1241,249 @@ function remapReviewTrackIdsToSourceIds(review) { }; } -function resolveHandBrakeTitleIdForPlaylist(scanJson, playlistIdRaw) { - const playlistId = normalizePlaylistId(playlistIdRaw); - if (!playlistId) { - return null; +function extractPlaylistIdFromHandBrakeTitle(title) { + const directCandidates = [ + title?.Playlist, + title?.playlist, + title?.PlaylistName, + title?.playlistName, + title?.SourcePlaylist, + title?.sourcePlaylist + ]; + for (const candidate of directCandidates) { + const normalized = normalizePlaylistId(candidate); + if (normalized) { + return normalized; + } } + const textCandidates = [ + title?.Path, + title?.path, + title?.Name, + title?.name, + title?.File, + title?.file, + title?.TitleName, + title?.titleName, + title?.SourceName, + title?.sourceName + ]; + for (const candidate of textCandidates) { + const text = String(candidate || '').trim(); + if (!text) { + continue; + } + const match = text.match(/(\d{1,5})\.mpls\b/i); + if (!match) { + continue; + } + const normalized = normalizePlaylistId(match[1]); + if (normalized) { + return normalized; + } + } + + return null; +} + +function parseHandBrakeScanSizeBytes(title) { + const numeric = Number(title?.Size?.Bytes ?? title?.Bytes ?? 0); + if (!Number.isFinite(numeric) || numeric <= 0) { + return 0; + } + return Math.trunc(numeric); +} + +function buildHandBrakeScanTitleRows(scanJson) { const titleList = pickScanTitleList(scanJson); - const matches = titleList + return titleList .map((title, idx) => { const handBrakeTitleId = normalizeScanTrackId( title?.Index ?? title?.index ?? title?.Title ?? title?.title, idx ); - const playlist = normalizePlaylistId( - title?.Playlist - || title?.playlist - || title?.PlaylistName - || title?.playlistName - || null - ); + const playlist = extractPlaylistIdFromHandBrakeTitle(title); const durationSeconds = parseHandBrakeDurationSeconds( title?.Duration ?? title?.duration ?? title?.Length ?? title?.length ); + const sizeBytes = parseHandBrakeScanSizeBytes(title); + const audioTrackCount = Array.isArray(title?.AudioList) ? title.AudioList.length : 0; + const subtitleTrackCount = Array.isArray(title?.SubtitleList) ? title.SubtitleList.length : 0; return { handBrakeTitleId, playlist, - durationSeconds + durationSeconds, + sizeBytes, + audioTrackCount, + subtitleTrackCount }; }) - .filter((item) => item.playlist === playlistId); + .filter((item) => Number.isFinite(item.handBrakeTitleId) && item.handBrakeTitleId > 0); +} - if (matches.length === 0) { +function listAvailableHandBrakePlaylists(scanJson) { + const rows = buildHandBrakeScanTitleRows(scanJson); + return Array.from(new Set( + rows + .map((item) => normalizePlaylistId(item?.playlist)) + .filter(Boolean) + )).sort(); +} + +function resolveHandBrakeTitleIdForPlaylist(scanJson, playlistIdRaw, options = {}) { + const playlistId = normalizePlaylistId(playlistIdRaw); + if (!playlistId) { return null; } - const best = matches.sort((a, b) => b.durationSeconds - a.durationSeconds || a.handBrakeTitleId - b.handBrakeTitleId)[0]; - return best?.handBrakeTitleId || null; + const expectedMakemkvTitleIdRaw = Number(options?.expectedMakemkvTitleId); + const expectedMakemkvTitleId = Number.isFinite(expectedMakemkvTitleIdRaw) && expectedMakemkvTitleIdRaw >= 0 + ? Math.trunc(expectedMakemkvTitleIdRaw) + : null; + const expectedDurationRaw = Number(options?.expectedDurationSeconds); + const expectedDurationSeconds = Number.isFinite(expectedDurationRaw) && expectedDurationRaw > 0 + ? Math.trunc(expectedDurationRaw) + : null; + const expectedSizeRaw = Number(options?.expectedSizeBytes); + const expectedSizeBytes = Number.isFinite(expectedSizeRaw) && expectedSizeRaw > 0 + ? Math.trunc(expectedSizeRaw) + : null; + const durationToleranceRaw = Number(options?.durationToleranceSeconds); + const durationToleranceSeconds = Number.isFinite(durationToleranceRaw) && durationToleranceRaw >= 0 + ? Math.trunc(durationToleranceRaw) + : 5; + + const rows = buildHandBrakeScanTitleRows(scanJson); + const matches = rows.filter((item) => item.playlist === playlistId); + + const scoreForExpected = (row) => { + const durationDelta = expectedDurationSeconds !== null + ? Math.abs(Number(row?.durationSeconds || 0) - expectedDurationSeconds) + : Number.MAX_SAFE_INTEGER; + const sizeDelta = expectedSizeBytes !== null + ? Math.abs(Number(row?.sizeBytes || 0) - expectedSizeBytes) + : Number.MAX_SAFE_INTEGER; + const trackRichness = Number(row?.audioTrackCount || 0) + Number(row?.subtitleTrackCount || 0); + return { + row, + durationDelta, + sizeDelta, + trackRichness + }; + }; + + const sortByExpectedScore = (a, b) => + a.durationDelta - b.durationDelta + || a.sizeDelta - b.sizeDelta + || b.trackRichness - a.trackRichness + || b.row.durationSeconds - a.row.durationSeconds + || b.row.sizeBytes - a.row.sizeBytes + || a.row.handBrakeTitleId - b.row.handBrakeTitleId; + + if (matches.length > 0) { + if (expectedDurationSeconds !== null || expectedSizeBytes !== null) { + const scored = matches.map(scoreForExpected).sort(sortByExpectedScore); + if (expectedDurationSeconds !== null) { + const withinTolerance = scored.filter((item) => item.durationDelta <= durationToleranceSeconds); + if (withinTolerance.length > 0) { + return withinTolerance[0].row.handBrakeTitleId; + } + } + return scored[0].row.handBrakeTitleId; + } + const best = matches.sort((a, b) => + b.durationSeconds - a.durationSeconds + || b.sizeBytes - a.sizeBytes + || a.handBrakeTitleId - b.handBrakeTitleId + )[0]; + return best?.handBrakeTitleId || null; + } + + // Fallback 1: choose closest duration/size if playlist metadata is absent in scan JSON. + if ((expectedDurationSeconds !== null || expectedSizeBytes !== null) && rows.length > 0) { + const scored = rows.map(scoreForExpected).sort(sortByExpectedScore); + if (expectedDurationSeconds !== null) { + const withinTolerance = scored.filter((item) => item.durationDelta <= durationToleranceSeconds); + if (withinTolerance.length > 0) { + return withinTolerance[0].row.handBrakeTitleId; + } + } + return scored[0].row.handBrakeTitleId; + } + + // Fallback 2: map MakeMKV title-id to HandBrake title-id if ordering matches. + if (expectedMakemkvTitleId !== null) { + const byPlusOne = rows.find((item) => item.handBrakeTitleId === (expectedMakemkvTitleId + 1)); + if (byPlusOne) { + return byPlusOne.handBrakeTitleId; + } + const byEqual = rows.find((item) => item.handBrakeTitleId === expectedMakemkvTitleId); + if (byEqual) { + return byEqual.handBrakeTitleId; + } + } + + if (rows.length === 1) { + return rows[0].handBrakeTitleId; + } + + return null; +} + +function isHandBrakePlaylistCacheEntryCompatible(entry, playlistIdRaw, options = {}) { + const playlistId = normalizePlaylistId(playlistIdRaw); + if (!playlistId) { + return false; + } + if (!entry || typeof entry !== 'object') { + return false; + } + const handBrakeTitleId = Number(entry?.handBrakeTitleId); + if (!Number.isFinite(handBrakeTitleId) || handBrakeTitleId <= 0) { + return false; + } + const titleInfo = entry?.titleInfo && typeof entry.titleInfo === 'object' ? entry.titleInfo : null; + if (!titleInfo) { + return false; + } + + const cachedPlaylistId = normalizePlaylistId(titleInfo?.playlistId || null); + if (cachedPlaylistId && cachedPlaylistId !== playlistId) { + return false; + } + + const expectedDurationRaw = Number(options?.expectedDurationSeconds); + const expectedDurationSeconds = Number.isFinite(expectedDurationRaw) && expectedDurationRaw > 0 + ? Math.trunc(expectedDurationRaw) + : null; + const cachedDurationRaw = Number(titleInfo?.durationSeconds); + const cachedDurationSeconds = Number.isFinite(cachedDurationRaw) && cachedDurationRaw > 0 + ? Math.trunc(cachedDurationRaw) + : null; + if (expectedDurationSeconds !== null && cachedDurationSeconds !== null) { + // Reject clearly wrong cache mappings (e.g. 30s instead of 6681s movie title). + if (Math.abs(expectedDurationSeconds - cachedDurationSeconds) > 120) { + return false; + } + } + + const expectedSizeRaw = Number(options?.expectedSizeBytes); + const expectedSizeBytes = Number.isFinite(expectedSizeRaw) && expectedSizeRaw > 0 + ? Math.trunc(expectedSizeRaw) + : null; + const cachedSizeRaw = Number(titleInfo?.sizeBytes); + const cachedSizeBytes = Number.isFinite(cachedSizeRaw) && cachedSizeRaw > 0 + ? Math.trunc(cachedSizeRaw) + : null; + if (expectedSizeBytes !== null && cachedSizeBytes !== null) { + const delta = Math.abs(expectedSizeBytes - cachedSizeBytes); + if (delta > (512 * 1024 * 1024)) { + return false; + } + } + + return true; } function normalizeCodecNumber(value) { @@ -1245,6 +1554,9 @@ function parseHandBrakeSelectedTitleInfo(scanJson, options = {}) { const preferredHandBrakeTitleId = Number.isFinite(rawPreferredHandBrakeTitleId) && rawPreferredHandBrakeTitleId > 0 ? Math.trunc(rawPreferredHandBrakeTitleId) : null; + const makeMkvSubtitleTracks = Array.isArray(options?.makeMkvSubtitleTracks) + ? options.makeMkvSubtitleTracks + : []; const parsedTitles = titleList.map((title, idx) => { const handBrakeTitleId = normalizeScanTrackId( @@ -1313,7 +1625,7 @@ function parseHandBrakeSelectedTitleInfo(scanJson, options = {}) { .filter((track) => Number.isFinite(Number(track?.sourceTrackId)) && Number(track.sourceTrackId) > 0); const audioTracks = filterDtsCoreFallbackTracks(audioTracksRaw); - const subtitleTracks = (Array.isArray(title?.SubtitleList) ? title.SubtitleList : []) + const subtitleTracksRaw = (Array.isArray(title?.SubtitleList) ? title.SubtitleList : []) .map((track, trackIndex) => { const sourceTrackId = normalizeScanTrackId( track?.TrackNumber @@ -1348,6 +1660,7 @@ function parseHandBrakeSelectedTitleInfo(scanJson, options = {}) { }; }) .filter((track) => Number.isFinite(Number(track?.sourceTrackId)) && Number(track.sourceTrackId) > 0); + const subtitleTracks = annotateSubtitleForcedAvailability(subtitleTracksRaw, makeMkvSubtitleTracks); return { handBrakeTitleId, @@ -1405,7 +1718,9 @@ function pickTitleIdForTrackReview(playlistAnalysis, selectedTitleId = null) { const candidates = Array.isArray(playlistAnalysis?.candidates) ? playlistAnalysis.candidates : []; if (candidates.length > 0) { - const sortedCandidates = [...candidates].sort((a, b) => + const candidatesWithPlaylist = candidates.filter((item) => normalizePlaylistId(item?.playlistId)); + const sortPool = candidatesWithPlaylist.length > 0 ? candidatesWithPlaylist : candidates; + const sortedCandidates = [...sortPool].sort((a, b) => Number(b?.durationSeconds || 0) - Number(a?.durationSeconds || 0) || Number(b?.sizeBytes || 0) - Number(a?.sizeBytes || 0) || Number(a?.titleId || 0) - Number(b?.titleId || 0) @@ -1508,7 +1823,7 @@ function buildDiscScanReview({ }); const audioTracks = filterDtsCoreFallbackTracks(audioTracksRaw); - const subtitleTracks = subtitleList.map((item, trackIndex) => { + const subtitleTracksRaw = subtitleList.map((item, trackIndex) => { const trackId = normalizeScanTrackId(item?.TrackNumber ?? item?.Track ?? item?.id, trackIndex); const languageLabel = String(item?.Language || item?.LanguageCode || item?.language || 'und'); return { @@ -1527,6 +1842,10 @@ function buildDiscScanReview({ subtitlePreviewDefaultTrack: false }; }); + const subtitleTracks = annotateSubtitleForcedAvailability( + subtitleTracksRaw, + Array.isArray(mappedMakemkvTitle?.subtitleTracks) ? mappedMakemkvTitle.subtitleTracks : [] + ); return { id: reviewTitleId, @@ -1680,8 +1999,9 @@ function findExistingRawDirectory(rawBaseDir, metadataBase) { const normalizedBase = sanitizeFileName(metadataBase); const escapedBase = normalizedBase.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const escapedIncompletePrefix = RAW_INCOMPLETE_PREFIX.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const escapedRipCompletePrefix = RAW_RIP_COMPLETE_PREFIX.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const folderPattern = new RegExp( - `^(?:${escapedIncompletePrefix})?${escapedBase}(?:\\s\\[tt\\d{6,12}\\])?\\s-\\sRAW\\s-\\sjob-\\d+\\s*$`, + `^(?:(?:${escapedIncompletePrefix}|${escapedRipCompletePrefix}))?${escapedBase}(?:\\s\\[tt\\d{6,12}\\])?\\s-\\sRAW\\s-\\sjob-\\d+\\s*$`, 'i' ); const candidates = entries @@ -1723,26 +2043,102 @@ function buildRawMetadataBase(jobLike = {}, fallbackJobId = null) { ); } -function buildRawDirName(metadataBase, jobId, options = {}) { - const incomplete = options?.incomplete !== undefined ? Boolean(options.incomplete) : true; - const baseName = sanitizeFileName(`${metadataBase} - RAW - job-${jobId}`); - return incomplete ? sanitizeFileName(`${RAW_INCOMPLETE_PREFIX}${baseName}`) : baseName; +function normalizeRawFolderState(rawState, fallback = RAW_FOLDER_STATES.INCOMPLETE) { + const state = String(rawState || '').trim().toLowerCase(); + if (!state) { + return fallback; + } + if (state === RAW_FOLDER_STATES.INCOMPLETE) { + return RAW_FOLDER_STATES.INCOMPLETE; + } + if (state === RAW_FOLDER_STATES.RIP_COMPLETE || state === 'ripcomplete' || state === 'rip-complete') { + return RAW_FOLDER_STATES.RIP_COMPLETE; + } + if (state === RAW_FOLDER_STATES.COMPLETE || state === 'none' || state === 'final') { + return RAW_FOLDER_STATES.COMPLETE; + } + return fallback; } -function buildCompletedRawPath(rawPath) { +function stripRawStatePrefix(folderName) { + const rawName = String(folderName || '').trim(); + if (!rawName) { + return ''; + } + return rawName + .replace(/^Incomplete_/i, '') + .replace(/^Rip_Complete_/i, '') + .trim(); +} + +function applyRawFolderStateToName(folderName, state) { + const baseName = stripRawStatePrefix(folderName); + if (!baseName) { + return baseName; + } + const normalizedState = normalizeRawFolderState(state, RAW_FOLDER_STATES.COMPLETE); + if (normalizedState === RAW_FOLDER_STATES.INCOMPLETE) { + return `${RAW_INCOMPLETE_PREFIX}${baseName}`; + } + if (normalizedState === RAW_FOLDER_STATES.RIP_COMPLETE) { + return `${RAW_RIP_COMPLETE_PREFIX}${baseName}`; + } + return baseName; +} + +function resolveRawFolderStateFromPath(rawPath) { + const sourcePath = String(rawPath || '').trim(); + if (!sourcePath) { + return RAW_FOLDER_STATES.COMPLETE; + } + const folderName = path.basename(sourcePath); + if (/^Incomplete_/i.test(folderName)) { + return RAW_FOLDER_STATES.INCOMPLETE; + } + if (/^Rip_Complete_/i.test(folderName)) { + return RAW_FOLDER_STATES.RIP_COMPLETE; + } + return RAW_FOLDER_STATES.COMPLETE; +} + +function resolveRawFolderStateFromOptions(options = {}) { + if (options && Object.prototype.hasOwnProperty.call(options, 'state')) { + return normalizeRawFolderState(options.state, RAW_FOLDER_STATES.INCOMPLETE); + } + if (options && options.ripComplete) { + return RAW_FOLDER_STATES.RIP_COMPLETE; + } + if (options && Object.prototype.hasOwnProperty.call(options, 'incomplete')) { + return options.incomplete ? RAW_FOLDER_STATES.INCOMPLETE : RAW_FOLDER_STATES.COMPLETE; + } + return RAW_FOLDER_STATES.INCOMPLETE; +} + +function buildRawDirName(metadataBase, jobId, options = {}) { + const state = resolveRawFolderStateFromOptions(options); + const baseName = sanitizeFileName(`${metadataBase} - RAW - job-${jobId}`); + return sanitizeFileName(applyRawFolderStateToName(baseName, state)); +} + +function buildRawPathForState(rawPath, state) { const sourcePath = String(rawPath || '').trim(); if (!sourcePath) { return null; } const folderName = path.basename(sourcePath); - if (!new RegExp(`^${RAW_INCOMPLETE_PREFIX}`, 'i').test(folderName)) { + const nextFolderName = applyRawFolderStateToName(folderName, state); + if (!nextFolderName) { return sourcePath; } - const completedFolderName = folderName.replace(new RegExp(`^${RAW_INCOMPLETE_PREFIX}`, 'i'), ''); - if (!completedFolderName) { - return sourcePath; - } - return path.join(path.dirname(sourcePath), completedFolderName); + return path.join(path.dirname(sourcePath), nextFolderName); +} + +function buildRipCompleteRawPath(rawPath) { + return buildRawPathForState(rawPath, RAW_FOLDER_STATES.RIP_COMPLETE); +} + +function buildCompletedRawPath(rawPath) { + return buildRawPathForState(rawPath, RAW_FOLDER_STATES.COMPLETE); } function normalizeComparablePath(inputPath) { @@ -1812,11 +2208,54 @@ function buildPlaylistCandidates(playlistAnalysis) { const sequenceCoherence = Number(source?.structuralMetrics?.sequenceCoherence); const titleId = Number(source?.titleId ?? source?.id); const handBrakeTitleId = Number(source?.handBrakeTitleId); + const durationSecondsRaw = Number(source?.durationSeconds ?? source?.duration ?? 0); + const durationSeconds = Number.isFinite(durationSecondsRaw) && durationSecondsRaw > 0 + ? Math.trunc(durationSecondsRaw) + : 0; + const sizeBytesRaw = Number(source?.sizeBytes ?? source?.size ?? 0); + const sizeBytes = Number.isFinite(sizeBytesRaw) && sizeBytesRaw > 0 + ? Math.trunc(sizeBytesRaw) + : 0; + const durationLabelRaw = String(source?.durationLabel || '').trim(); + const durationLabel = durationLabelRaw || formatDurationClock(durationSeconds); + const sourceAudioTracks = Array.isArray(source?.audioTracks) ? source.audioTracks : []; + const fallbackAudioTrackPreview = sourceAudioTracks + .slice(0, 8) + .map((track) => { + const rawTrackId = Number(track?.sourceTrackId ?? track?.id); + const trackId = Number.isFinite(rawTrackId) && rawTrackId > 0 ? Math.trunc(rawTrackId) : null; + const language = normalizeTrackLanguage(track?.language || track?.languageLabel || 'und'); + const languageLabel = String(track?.languageLabel || track?.language || language).trim() || language; + const format = String(track?.format || '').trim(); + const channels = String(track?.channels || '').trim(); + const parts = []; + if (trackId !== null) { + parts.push(`#${trackId}`); + } + parts.push(language); + parts.push(languageLabel); + if (format) { + parts.push(format); + } + if (channels) { + parts.push(channels); + } + return parts.join(' | '); + }) + .filter((line) => line.length > 0); + const sourceAudioTrackPreview = Array.isArray(source?.audioTrackPreview) + ? source.audioTrackPreview.map((line) => String(line || '').trim()).filter((line) => line.length > 0) + : []; + const audioTrackPreview = sourceAudioTrackPreview.length > 0 ? sourceAudioTrackPreview : fallbackAudioTrackPreview; + const audioSummary = String(source?.audioSummary || '').trim() || buildHandBrakeAudioSummary(audioTrackPreview); return { playlistId, playlistFile: toPlaylistFile(playlistId), titleId: Number.isFinite(titleId) ? Math.trunc(titleId) : null, + durationSeconds, + durationLabel: durationLabel || null, + sizeBytes, score: Number.isFinite(score) ? score : null, recommended: Boolean(source?.recommended), evaluationLabel: source?.evaluationLabel || null, @@ -1830,8 +2269,8 @@ function buildPlaylistCandidates(playlistAnalysis) { handBrakeTitleId: Number.isFinite(handBrakeTitleId) && handBrakeTitleId > 0 ? Math.trunc(handBrakeTitleId) : null, - audioSummary: source?.audioSummary || null, - audioTrackPreview: Array.isArray(source?.audioTrackPreview) ? source.audioTrackPreview : [] + audioSummary: audioSummary || null, + audioTrackPreview }; }); } @@ -1967,6 +2406,19 @@ function hasCachedHandBrakeDataForPlaylistCandidates(scanCache, playlistCandidat } function buildHandBrakePlaylistScanCache(scanJson, playlistCandidates = [], rawPath = null) { + const candidateMetaByPlaylist = new Map(); + for (const row of (Array.isArray(playlistCandidates) ? playlistCandidates : [])) { + const playlistId = normalizePlaylistId(row?.playlistId || row?.playlistFile || row); + if (!playlistId || candidateMetaByPlaylist.has(playlistId)) { + continue; + } + candidateMetaByPlaylist.set(playlistId, { + expectedMakemkvTitleId: normalizeNonNegativeInteger(row?.titleId), + expectedDurationSeconds: Number(row?.durationSeconds || 0) || null, + expectedSizeBytes: Number(row?.sizeBytes || 0) || null + }); + } + const candidateIds = Array.from(new Set( (Array.isArray(playlistCandidates) ? playlistCandidates : []) .map((item) => normalizePlaylistId(item?.playlistId || item?.playlistFile || item)) @@ -1975,7 +2427,8 @@ function buildHandBrakePlaylistScanCache(scanJson, playlistCandidates = [], rawP const byPlaylist = {}; for (const playlistId of candidateIds) { - const handBrakeTitleId = resolveHandBrakeTitleIdForPlaylist(scanJson, playlistId); + const expected = candidateMetaByPlaylist.get(playlistId) || {}; + const handBrakeTitleId = resolveHandBrakeTitleIdForPlaylist(scanJson, playlistId, expected); if (!handBrakeTitleId) { continue; } @@ -1986,6 +2439,13 @@ function buildHandBrakePlaylistScanCache(scanJson, playlistCandidates = [], rawP if (!titleInfo) { continue; } + if (!isHandBrakePlaylistCacheEntryCompatible({ + playlistId, + handBrakeTitleId, + titleInfo + }, playlistId, expected)) { + continue; + } const audioTrackPreview = buildHandBrakeAudioTrackPreview(titleInfo); byPlaylist[playlistId] = { playlistId, @@ -2634,6 +3094,21 @@ class PipelineService extends EventEmitter { return String(mkInfo?.status || '').trim().toUpperCase() === 'SUCCESS'; } + isEncodeSuccessful(job = null) { + const handBrakeInfo = this.safeParseJson(job?.handbrake_info_json); + return String(handBrakeInfo?.status || '').trim().toUpperCase() === 'SUCCESS'; + } + + resolveDesiredRawFolderState(job = null) { + if (!this.isRipSuccessful(job)) { + return RAW_FOLDER_STATES.INCOMPLETE; + } + if (this.isEncodeSuccessful(job)) { + return RAW_FOLDER_STATES.COMPLETE; + } + return RAW_FOLDER_STATES.RIP_COMPLETE; + } + resolveCurrentRawPath(rawBaseDir, storedRawPath, extraBaseDirs = []) { const stored = String(storedRawPath || '').trim(); if (!stored) { @@ -2674,7 +3149,7 @@ class PipelineService extends EventEmitter { } const rows = await db.all(` - SELECT id, title, year, detected_title, raw_path, status, last_state, rip_successful, makemkv_info_json + SELECT id, title, year, detected_title, raw_path, status, last_state, rip_successful, makemkv_info_json, handbrake_info_json FROM jobs WHERE raw_path IS NOT NULL AND TRIM(raw_path) <> '' `); @@ -2749,7 +3224,7 @@ class PipelineService extends EventEmitter { // Keep renamed folder in the same base dir as the current path const currentBaseDir = path.dirname(currentRawPath); - const currentFolderName = path.basename(currentRawPath).replace(/^Incomplete_/i, '').trim(); + const currentFolderName = stripRawStatePrefix(path.basename(currentRawPath)); const folderYearMatch = currentFolderName.match(/\((19|20)\d{2}\)/); const fallbackYear = folderYearMatch ? Number(String(folderYearMatch[0]).replace(/[()]/g, '')) @@ -2759,10 +3234,10 @@ class PipelineService extends EventEmitter { year: row.year || null, fallbackYear }, jobId); - const shouldBeIncomplete = !isJobFinished(row); + const desiredRawFolderState = this.resolveDesiredRawFolderState(row); const desiredRawPath = path.join( currentBaseDir, - buildRawDirName(metadataBase, jobId, { incomplete: shouldBeIncomplete }) + buildRawDirName(metadataBase, jobId, { state: desiredRawFolderState }) ); let finalRawPath = currentRawPath; @@ -4279,6 +4754,7 @@ class PipelineService extends EventEmitter { const mode = String(options?.mode || 'rip').trim().toLowerCase() || 'rip'; const forcePlaylistReselection = Boolean(options?.forcePlaylistReselection); + const forceFreshAnalyze = Boolean(options?.forceFreshAnalyze); const mkInfo = this.safeParseJson(job.makemkv_info_json); const mediaProfile = this.resolveMediaProfileForJob(job, { mediaProfile: options?.mediaProfile, @@ -4287,18 +4763,22 @@ class PipelineService extends EventEmitter { }); const settings = await settingsService.getEffectiveSettingsMap(mediaProfile); const analyzeContext = mkInfo?.analyzeContext || {}; - let playlistAnalysis = analyzeContext.playlistAnalysis || this.snapshot.context?.playlistAnalysis || null; - let handBrakePlaylistScan = normalizeHandBrakePlaylistScanCache(analyzeContext.handBrakePlaylistScan || null); + let playlistAnalysis = forceFreshAnalyze + ? null + : (analyzeContext.playlistAnalysis || this.snapshot.context?.playlistAnalysis || null); + let handBrakePlaylistScan = forceFreshAnalyze + ? null + : normalizeHandBrakePlaylistScanCache(analyzeContext.handBrakePlaylistScan || null); if (playlistAnalysis && handBrakePlaylistScan) { playlistAnalysis = enrichPlaylistAnalysisWithHandBrakeCache(playlistAnalysis, handBrakePlaylistScan); } - const selectedPlaylistSource = forcePlaylistReselection + const selectedPlaylistSource = (forcePlaylistReselection || forceFreshAnalyze) ? (options?.selectedPlaylist || null) : (options?.selectedPlaylist || analyzeContext.selectedPlaylist || this.snapshot.context?.selectedPlaylist || null); const selectedPlaylistId = normalizePlaylistId( selectedPlaylistSource ); - const selectedTitleSource = forcePlaylistReselection + const selectedTitleSource = (forcePlaylistReselection || forceFreshAnalyze) ? (options?.selectedTitleId ?? null) : (options?.selectedTitleId ?? analyzeContext.selectedTitleId ?? this.snapshot.context?.selectedTitleId ?? null); const selectedMakemkvTitleId = normalizeNonNegativeInteger(selectedTitleSource); @@ -4348,6 +4828,13 @@ class PipelineService extends EventEmitter { 'Re-Encode: gespeicherte Playlist-Auswahl wird ignoriert. Bitte Playlist manuell neu auswählen.' ); } + if (forceFreshAnalyze) { + await historyService.appendLog( + jobId, + 'SYSTEM', + 'Review-Neustart erzwingt frische MakeMKV Full-Analyse (kein Reuse von Playlist-/HandBrake-Cache).' + ); + } // Build playlist->TITLE_ID mapping once from MakeMKV full robot scan on RAW backup. let makeMkvAnalyzeRunInfo = null; @@ -4556,12 +5043,13 @@ class PipelineService extends EventEmitter { const playlistFile = toPlaylistFile(candidate?.playlistId) || `Titel #${candidate?.titleId || '-'}`; const score = Number(candidate?.score); const scoreLabel = Number.isFinite(score) ? score.toFixed(0) : '-'; + const durationLabel = String(candidate?.durationLabel || '').trim() || formatDurationClock(candidate?.durationSeconds) || '-'; const recommendedLabel = candidate?.recommended ? ' (empfohlen)' : ''; const evaluationLabel = candidate?.evaluationLabel ? ` | ${candidate.evaluationLabel}` : ''; await historyService.appendLog( jobId, 'SYSTEM', - `${playlistFile} -> Score ${scoreLabel}${recommendedLabel}${evaluationLabel}` + `${playlistFile} -> Dauer ${durationLabel} | Score ${scoreLabel}${recommendedLabel}${evaluationLabel}` ); } await historyService.appendLog( @@ -4661,12 +5149,25 @@ class PipelineService extends EventEmitter { } const cachedHandBrakePlaylistEntry = getCachedHandBrakePlaylistEntry(handBrakePlaylistScan, resolvedPlaylistId); + const expectedDurationForCache = Number(selectedTitleFromAnalysis?.durationSeconds || 0) || null; + const expectedSizeForCache = Number(selectedTitleFromAnalysis?.sizeBytes || 0) || null; const hasCachedHandBrakeEntry = Boolean( - cachedHandBrakePlaylistEntry - && cachedHandBrakePlaylistEntry.titleInfo - && Number.isFinite(Number(cachedHandBrakePlaylistEntry.handBrakeTitleId)) - && Number(cachedHandBrakePlaylistEntry.handBrakeTitleId) > 0 + isHandBrakePlaylistCacheEntryCompatible( + cachedHandBrakePlaylistEntry, + resolvedPlaylistId, + { + expectedDurationSeconds: expectedDurationForCache, + expectedSizeBytes: expectedSizeForCache + } + ) ); + if (cachedHandBrakePlaylistEntry && !hasCachedHandBrakeEntry) { + await historyService.appendLog( + jobId, + 'SYSTEM', + `HandBrake Cache für ${toPlaylistFile(resolvedPlaylistId)} verworfen (inkompatible Playlist-/Dauerdaten).` + ); + } if (this.isPrimaryJob(jobId)) { await this.updateProgress( @@ -4683,10 +5184,16 @@ class PipelineService extends EventEmitter { let handBrakeTitleRunInfo = null; let resolvedHandBrakeTitleId = null; const reviewTitleSource = 'handbrake'; + const makeMkvSubtitleTracksForSelection = Array.isArray(selectedTitleFromAnalysis?.subtitleTracks) + ? selectedTitleFromAnalysis.subtitleTracks + : []; let reviewTitleInfo = null; if (hasCachedHandBrakeEntry) { resolvedHandBrakeTitleId = Math.trunc(Number(cachedHandBrakePlaylistEntry.handBrakeTitleId)); - reviewTitleInfo = cachedHandBrakePlaylistEntry.titleInfo; + reviewTitleInfo = enrichTitleInfoWithForcedSubtitleAvailability( + cachedHandBrakePlaylistEntry.titleInfo, + makeMkvSubtitleTracksForSelection + ); handBrakeResolveRunInfo = { source: 'HANDBRAKE_SCAN_PLAYLIST_MAP_CACHE', stage: 'MEDIAINFO_CHECK', @@ -4738,9 +5245,17 @@ class PipelineService extends EventEmitter { throw error; } - resolvedHandBrakeTitleId = resolveHandBrakeTitleIdForPlaylist(resolveScanJson, resolvedPlaylistId); + resolvedHandBrakeTitleId = resolveHandBrakeTitleIdForPlaylist(resolveScanJson, resolvedPlaylistId, { + expectedMakemkvTitleId: selectedTitleForReview, + expectedDurationSeconds: expectedDurationForCache, + expectedSizeBytes: expectedSizeForCache + }); if (!resolvedHandBrakeTitleId) { - const error = new Error(`Kein HandBrake-Titel für ${toPlaylistFile(resolvedPlaylistId)} gefunden.`); + const knownPlaylists = listAvailableHandBrakePlaylists(resolveScanJson); + const error = new Error( + `Kein HandBrake-Titel für ${toPlaylistFile(resolvedPlaylistId)} gefunden.` + + ` ${knownPlaylists.length > 0 ? `Scan-Playlists: ${knownPlaylists.map((id) => `${id}.mpls`).join(', ')}` : 'Scan enthält keine erkennbaren Playlist-IDs.'}` + ); error.statusCode = 400; error.runInfo = handBrakeResolveRunInfo; throw error; @@ -4748,7 +5263,8 @@ class PipelineService extends EventEmitter { reviewTitleInfo = parseHandBrakeSelectedTitleInfo(resolveScanJson, { playlistId: resolvedPlaylistId, - handBrakeTitleId: resolvedHandBrakeTitleId + handBrakeTitleId: resolvedHandBrakeTitleId, + makeMkvSubtitleTracks: makeMkvSubtitleTracksForSelection }); if (!reviewTitleInfo) { const error = new Error( @@ -4758,6 +5274,21 @@ class PipelineService extends EventEmitter { error.runInfo = handBrakeResolveRunInfo; throw error; } + if (!isHandBrakePlaylistCacheEntryCompatible({ + playlistId: resolvedPlaylistId, + handBrakeTitleId: resolvedHandBrakeTitleId, + titleInfo: reviewTitleInfo + }, resolvedPlaylistId, { + expectedDurationSeconds: expectedDurationForCache, + expectedSizeBytes: expectedSizeForCache + })) { + const error = new Error( + `HandBrake Titel-Mapping inkonsistent für ${toPlaylistFile(resolvedPlaylistId)} (-t ${resolvedHandBrakeTitleId}).` + ); + error.statusCode = 400; + error.runInfo = handBrakeResolveRunInfo; + throw error; + } handBrakeTitleRunInfo = handBrakeResolveRunInfo; await historyService.appendLog( @@ -4840,24 +5371,52 @@ class PipelineService extends EventEmitter { review = remapReviewTrackIdsToSourceIds(review); const resolvedPlaylistInfo = resolvePlaylistInfoFromAnalysis(playlistAnalysis, resolvedPlaylistId); + const subtitleTrackMetaBySourceId = new Map( + (Array.isArray(reviewTitleInfo?.subtitleTracks) ? reviewTitleInfo.subtitleTracks : []) + .map((track) => { + const sourceTrackId = normalizeTrackIdList([track?.sourceTrackId ?? track?.id])[0] || null; + return sourceTrackId ? [sourceTrackId, track] : null; + }) + .filter(Boolean) + ); const normalizedTitles = (Array.isArray(review.titles) ? review.titles : []) .slice(0, 1) - .map((title) => ({ - ...title, - filePath: rawPath, - fileName: reviewTitleInfo?.fileName || title?.fileName || `Title #${selectedTitleForReview}`, - durationSeconds: Number(reviewTitleInfo?.durationSeconds || title?.durationSeconds || 0), - durationMinutes: Number((((reviewTitleInfo?.durationSeconds || title?.durationSeconds || 0) / 60)).toFixed(2)), - sizeBytes: Number(reviewTitleInfo?.sizeBytes || title?.sizeBytes || 0), - playlistId: resolvedPlaylistInfo.playlistId || title?.playlistId || null, - playlistFile: resolvedPlaylistInfo.playlistFile || title?.playlistFile || null, - playlistRecommended: Boolean(resolvedPlaylistInfo.recommended || title?.playlistRecommended), - playlistEvaluationLabel: resolvedPlaylistInfo.evaluationLabel || title?.playlistEvaluationLabel || null, - playlistSegmentCommand: resolvedPlaylistInfo.segmentCommand || title?.playlistSegmentCommand || null, - playlistSegmentFiles: Array.isArray(resolvedPlaylistInfo.segmentFiles) && resolvedPlaylistInfo.segmentFiles.length > 0 - ? resolvedPlaylistInfo.segmentFiles - : (Array.isArray(title?.playlistSegmentFiles) ? title.playlistSegmentFiles : []) - })); + .map((title) => { + const subtitleTracks = (Array.isArray(title?.subtitleTracks) ? title.subtitleTracks : []).map((track) => { + const sourceTrackId = normalizeTrackIdList([track?.sourceTrackId ?? track?.id])[0] || null; + const sourceMeta = sourceTrackId ? (subtitleTrackMetaBySourceId.get(sourceTrackId) || null) : null; + return { + ...track, + id: sourceTrackId || track?.id || null, + sourceTrackId: sourceTrackId || track?.sourceTrackId || track?.id || null, + language: sourceMeta?.language || track?.language || 'und', + languageLabel: sourceMeta?.languageLabel || track?.languageLabel || track?.language || 'und', + title: sourceMeta?.title ?? track?.title ?? null, + format: sourceMeta?.format || track?.format || null, + forcedTrack: Boolean(sourceMeta?.forcedTrack), + forcedAvailable: Boolean(sourceMeta?.forcedAvailable), + forcedSourceTrackIds: normalizeTrackIdList(sourceMeta?.forcedSourceTrackIds || []) + }; + }); + + return { + ...title, + filePath: rawPath, + fileName: reviewTitleInfo?.fileName || title?.fileName || `Title #${selectedTitleForReview}`, + durationSeconds: Number(reviewTitleInfo?.durationSeconds || title?.durationSeconds || 0), + durationMinutes: Number((((reviewTitleInfo?.durationSeconds || title?.durationSeconds || 0) / 60)).toFixed(2)), + sizeBytes: Number(reviewTitleInfo?.sizeBytes || title?.sizeBytes || 0), + playlistId: resolvedPlaylistInfo.playlistId || title?.playlistId || null, + playlistFile: resolvedPlaylistInfo.playlistFile || title?.playlistFile || null, + playlistRecommended: Boolean(resolvedPlaylistInfo.recommended || title?.playlistRecommended), + playlistEvaluationLabel: resolvedPlaylistInfo.evaluationLabel || title?.playlistEvaluationLabel || null, + playlistSegmentCommand: resolvedPlaylistInfo.segmentCommand || title?.playlistSegmentCommand || null, + playlistSegmentFiles: Array.isArray(resolvedPlaylistInfo.segmentFiles) && resolvedPlaylistInfo.segmentFiles.length > 0 + ? resolvedPlaylistInfo.segmentFiles + : (Array.isArray(title?.playlistSegmentFiles) ? title.playlistSegmentFiles : []), + subtitleTracks + }; + }); const encodeInputTitleId = Number(normalizedTitles[0]?.id || review.encodeInputTitleId || null) || null; review = { @@ -5115,14 +5674,18 @@ class PipelineService extends EventEmitter { const existingRawPath = findExistingRawDirectory(settings.raw_dir, metadataBase); let updatedRawPath = existingRawPath || null; if (existingRawPath) { - const renamedDirName = buildRawDirName(metadataBase, jobId, { incomplete: true }); + const existingRawState = resolveRawFolderStateFromPath(existingRawPath); + const renameState = existingRawState === RAW_FOLDER_STATES.INCOMPLETE + ? RAW_FOLDER_STATES.INCOMPLETE + : RAW_FOLDER_STATES.RIP_COMPLETE; + const renamedDirName = buildRawDirName(metadataBase, jobId, { state: renameState }); const renamedRawPath = path.join(settings.raw_dir, renamedDirName); if (existingRawPath !== renamedRawPath && !fs.existsSync(renamedRawPath)) { try { fs.renameSync(existingRawPath, renamedRawPath); updatedRawPath = renamedRawPath; await historyService.updateRawPathByOldPath(existingRawPath, renamedRawPath); - logger.info('metadata:raw-dir-renamed', { from: existingRawPath, to: renamedRawPath, jobId }); + logger.info('metadata:raw-dir-renamed', { from: existingRawPath, to: renamedRawPath, jobId, state: renameState }); } catch (renameError) { logger.warn('metadata:raw-dir-rename-failed', { existingRawPath, renamedRawPath, error: errorToMeta(renameError) }); } @@ -6661,6 +7224,23 @@ class PipelineService extends EventEmitter { || this.snapshot.context?.selectedPlaylist || null ); + const selectedEncodeTitle = Array.isArray(encodePlan?.titles) + ? ( + encodePlan.titles.find((title) => + Boolean(title?.selectedForEncode) && normalizePlaylistId(title?.playlistId) === selectedPlaylistId + ) + || encodePlan.titles.find((title) => Boolean(title?.selectedForEncode)) + || null + ) + : null; + const expectedMakemkvTitleIdForResolve = normalizeNonNegativeInteger( + selectedEncodeTitle?.makemkvTitleId + ?? encodePlan?.playlistRecommendation?.makemkvTitleId + ?? this.snapshot.context?.selectedTitleId + ?? null + ); + const expectedDurationSecondsForResolve = Number(selectedEncodeTitle?.durationSeconds || 0) || null; + const expectedSizeBytesForResolve = Number(selectedEncodeTitle?.sizeBytes || 0) || null; if (!handBrakeTitleId && selectedPlaylistId) { const titleResolveScanLines = []; const titleResolveScanConfig = await settingsService.buildHandBrakeScanConfigForInput(inputPath, { @@ -6689,9 +7269,17 @@ class PipelineService extends EventEmitter { error.runInfo = titleResolveRunInfo; throw error; } - handBrakeTitleId = resolveHandBrakeTitleIdForPlaylist(titleResolveParsed, selectedPlaylistId); + handBrakeTitleId = resolveHandBrakeTitleIdForPlaylist(titleResolveParsed, selectedPlaylistId, { + expectedMakemkvTitleId: expectedMakemkvTitleIdForResolve, + expectedDurationSeconds: expectedDurationSecondsForResolve, + expectedSizeBytes: expectedSizeBytesForResolve + }); if (!handBrakeTitleId) { - const error = new Error(`Kein HandBrake-Titel für Playlist ${selectedPlaylistId}.mpls gefunden.`); + const knownPlaylists = listAvailableHandBrakePlaylists(titleResolveParsed); + const error = new Error( + `Kein HandBrake-Titel für Playlist ${selectedPlaylistId}.mpls gefunden.` + + ` ${knownPlaylists.length > 0 ? `Scan-Playlists: ${knownPlaylists.map((id) => `${id}.mpls`).join(', ')}` : 'Scan enthält keine erkennbaren Playlist-IDs.'}` + ); error.statusCode = 400; throw error; } @@ -6812,7 +7400,7 @@ class PipelineService extends EventEmitter { await historyService.appendLog( jobId, 'SYSTEM', - `RAW-Ordner konnte nicht als abgeschlossen markiert werden (Ziel existiert bereits): ${completedRawPath}` + `RAW-Ordner konnte nicht finalisiert werden (Ziel existiert bereits): ${completedRawPath}` ); } else { try { @@ -6822,7 +7410,7 @@ class PipelineService extends EventEmitter { await historyService.appendLog( jobId, 'SYSTEM', - `RAW-Ordner als abgeschlossen markiert: ${currentRawPath} -> ${completedRawPath}` + `RAW-Ordner nach erfolgreichem Encode finalisiert (Prefix entfernt): ${currentRawPath} -> ${completedRawPath}` ); } catch (rawRenameError) { logger.warn('encoding:raw-dir-finalize:rename-failed', { @@ -6834,7 +7422,7 @@ class PipelineService extends EventEmitter { await historyService.appendLog( jobId, 'SYSTEM', - `RAW-Ordner konnte nicht als abgeschlossen markiert werden: ${rawRenameError.message}` + `RAW-Ordner konnte nach Encode nicht finalisiert werden: ${rawRenameError.message}` ); } } @@ -6982,7 +7570,7 @@ class PipelineService extends EventEmitter { title: job.title || job.detected_title || null, year: job.year || null }, jobId); - const rawDirName = buildRawDirName(metadataBase, jobId, { incomplete: true }); + const rawDirName = buildRawDirName(metadataBase, jobId, { state: RAW_FOLDER_STATES.INCOMPLETE }); const rawJobDir = path.join(rawBaseDir, rawDirName); ensureDir(rawJobDir); chownRecursive(rawJobDir, settings.raw_dir_owner); @@ -7120,6 +7708,18 @@ class PipelineService extends EventEmitter { } } + // Check for MakeMKV backup failure even when exit code is 0. + // MakeMKV can exit 0 but still output "Backup failed" in stdout. + const backupFailed = Array.isArray(makemkvInfo?.highlights) && + makemkvInfo.highlights.some(line => /backup failed/i.test(line)); + if (backupFailed) { + const failMsg = makemkvInfo.highlights.find(line => /backup failed/i.test(line)) || 'Backup failed'; + throw Object.assign( + new Error(`MakeMKV Backup fehlgeschlagen (Exit Code 0): ${failMsg}`), + { runInfo: makemkvInfo } + ); + } + const mkInfoBeforeRip = this.safeParseJson(job.makemkv_info_json); await historyService.updateJob(jobId, { makemkv_info_json: JSON.stringify(this.withAnalyzeContextMediaProfile({ @@ -7129,39 +7729,39 @@ class PipelineService extends EventEmitter { rip_successful: 1 }); - // Rename Incomplete_ prefix away now that the rip is complete and successful. + // Mark RAW as rip-complete until encode succeeds. let activeRawJobDir = rawJobDir; - const completedRawJobDir = buildCompletedRawPath(rawJobDir); - if (completedRawJobDir && completedRawJobDir !== rawJobDir) { - if (fs.existsSync(completedRawJobDir)) { - logger.warn('rip:raw-complete:rename-skip', { jobId, rawJobDir, completedRawJobDir }); + const ripCompleteRawJobDir = buildRipCompleteRawPath(rawJobDir); + if (ripCompleteRawJobDir && ripCompleteRawJobDir !== rawJobDir) { + if (fs.existsSync(ripCompleteRawJobDir)) { + logger.warn('rip:raw-complete:rename-skip', { jobId, rawJobDir, ripCompleteRawJobDir }); await historyService.appendLog( jobId, 'SYSTEM', - `RAW-Ordner konnte nach Rip nicht umbenannt werden (Zielordner existiert): ${completedRawJobDir}` + `RAW-Ordner konnte nach Rip nicht als Rip_Complete markiert werden (Zielordner existiert): ${ripCompleteRawJobDir}` ); } else { try { - fs.renameSync(rawJobDir, completedRawJobDir); - activeRawJobDir = completedRawJobDir; + fs.renameSync(rawJobDir, ripCompleteRawJobDir); + activeRawJobDir = ripCompleteRawJobDir; chownRecursive(activeRawJobDir, settings.raw_dir_owner); - await historyService.updateRawPathByOldPath(rawJobDir, completedRawJobDir); + await historyService.updateRawPathByOldPath(rawJobDir, ripCompleteRawJobDir); await historyService.appendLog( jobId, 'SYSTEM', - `RAW-Ordner nach erfolgreichem Rip umbenannt: ${rawJobDir} → ${completedRawJobDir}` + `RAW-Ordner nach erfolgreichem Rip als Rip_Complete markiert: ${rawJobDir} → ${ripCompleteRawJobDir}` ); } catch (renameError) { logger.warn('rip:raw-complete:rename-failed', { jobId, rawJobDir, - completedRawJobDir, + ripCompleteRawJobDir, error: errorToMeta(renameError) }); await historyService.appendLog( jobId, 'SYSTEM', - `RAW-Ordner konnte nach Rip nicht umbenannt werden: ${renameError.message}` + `RAW-Ordner konnte nach Rip nicht als Rip_Complete markiert werden: ${renameError.message}` ); } } @@ -7525,13 +8125,15 @@ class PipelineService extends EventEmitter { const nextMakemkvInfoJson = mkInfo && typeof mkInfo === 'object' ? JSON.stringify({ ...mkInfo, - analyzeContext: forcePlaylistReselection - ? { - ...(mkInfo?.analyzeContext || {}), - selectedPlaylist: null, - selectedTitleId: null - } - : (mkInfo?.analyzeContext || null) + analyzeContext: { + ...(mkInfo?.analyzeContext || {}), + playlistAnalysis: null, + playlistDecisionRequired: false, + selectedPlaylist: null, + selectedTitleId: null, + handBrakePlaylistScan: null + }, + postBackupAnalyze: null }) : sourceJob.makemkv_info_json; @@ -7556,7 +8158,7 @@ class PipelineService extends EventEmitter { await historyService.appendLog( jobId, 'USER_ACTION', - `Review-Neustart aus RAW angefordert.${forcePlaylistReselection ? ' Playlist-Auswahl wird zurückgesetzt.' : ''}` + `Review-Neustart aus RAW angefordert.${forcePlaylistReselection ? ' Playlist-Auswahl wird zurückgesetzt.' : ''} MakeMKV Full-Analyse wird vollständig neu ausgeführt.` ); await this.setState('MEDIAINFO_CHECK', { @@ -7575,7 +8177,8 @@ class PipelineService extends EventEmitter { this.runReviewForRawJob(jobId, resolvedReviewRawPath, { mode: options?.mode || 'reencode', sourceJobId: jobId, - forcePlaylistReselection + forcePlaylistReselection, + forceFreshAnalyze: true }).catch((error) => { logger.error('restartReviewFromRaw:background-failed', { jobId, error: errorToMeta(error) }); this.failJob(jobId, 'MEDIAINFO_CHECK', error).catch((failError) => { diff --git a/backend/src/services/settingsService.js b/backend/src/services/settingsService.js index 868b3aa..503abcc 100644 --- a/backend/src/services/settingsService.js +++ b/backend/src/services/settingsService.js @@ -716,7 +716,16 @@ class SettingsService { options?.mediaProfile || deviceInfo?.mediaProfile || null ); const cmd = map.makemkv_command; - const args = ['-r', 'info', this.resolveSourceArg(map, deviceInfo), ...splitArgs(map.makemkv_analyze_extra_args)]; + const extraArgs = splitArgs(map.makemkv_analyze_extra_args); + const hasExplicitMinLength = extraArgs.some((arg) => /^--minlength(?:=|$)/i.test(String(arg || '').trim())); + const minLengthMinutes = Number(map.makemkv_min_length_minutes || 0); + const minLengthSeconds = Number.isFinite(minLengthMinutes) && minLengthMinutes > 0 + ? Math.round(minLengthMinutes * 60) + : 0; + const minLengthArgs = (!hasExplicitMinLength && minLengthSeconds > 0) + ? [`--minlength=${minLengthSeconds}`] + : []; + const args = ['-r', ...minLengthArgs, ...extraArgs, 'info', this.resolveSourceArg(map, deviceInfo)]; logger.debug('cli:makemkv:analyze', { cmd, args, deviceInfo }); return { cmd, args }; } @@ -726,7 +735,16 @@ class SettingsService { const map = this.resolveEffectiveToolSettings(rawMap, options?.mediaProfile || null); const cmd = map.makemkv_command; const sourceArg = `file:${sourcePath}`; - const args = ['-r', 'info', sourceArg, ...splitArgs(map.makemkv_analyze_extra_args)]; + const extraArgs = splitArgs(map.makemkv_analyze_extra_args); + const hasExplicitMinLength = extraArgs.some((arg) => /^--minlength(?:=|$)/i.test(String(arg || '').trim())); + const minLengthMinutes = Number(map.makemkv_min_length_minutes || 0); + const minLengthSeconds = Number.isFinite(minLengthMinutes) && minLengthMinutes > 0 + ? Math.round(minLengthMinutes * 60) + : 0; + const minLengthArgs = (!hasExplicitMinLength && minLengthSeconds > 0) + ? [`--minlength=${minLengthSeconds}`] + : []; + const args = ['-r', ...minLengthArgs, ...extraArgs, 'info', sourceArg]; const titleIdRaw = Number(options?.titleId); // "makemkvcon info" supports only ; title filtering is done in app parser. logger.debug('cli:makemkv:analyze:path', { diff --git a/backend/src/utils/encodePlan.js b/backend/src/utils/encodePlan.js index d00ac13..0e1061f 100644 --- a/backend/src/utils/encodePlan.js +++ b/backend/src/utils/encodePlan.js @@ -444,7 +444,9 @@ function buildBaseTrackSelectors(settings, presetProfile = null) { explicitIds: [], firstOnly: baseSubtitleMode === 'first', selectionSource: profile.source === 'preset-export' ? 'preset' : 'default', - burnBehavior: normalizeBurnBehavior(profile.subtitleBurnBehavior), + // Do not auto-burn subtitle tracks from exported preset metadata. + // Burn-in should only be activated via explicit CLI args/selection. + burnBehavior: 'none', burnedTrackId: null, defaultTrackId: null, forcedTrackId: null, diff --git a/backend/src/utils/playlistAnalysis.js b/backend/src/utils/playlistAnalysis.js index cd8e78f..3210775 100644 --- a/backend/src/utils/playlistAnalysis.js +++ b/backend/src/utils/playlistAnalysis.js @@ -1,5 +1,7 @@ const LARGE_JUMP_THRESHOLD = 20; const DEFAULT_DURATION_SIMILARITY_SECONDS = 90; +const RAW_MIRROR_DURATION_TOLERANCE_SECONDS = 2; +const RAW_MIRROR_SIZE_TOLERANCE_BYTES = 64 * 1024 * 1024; function parseDurationSeconds(raw) { const text = String(raw || '').trim(); @@ -151,6 +153,7 @@ function parseAnalyzeTitles(lines) { chapters: 0, segmentNumbers: [], segmentFiles: [], + streams: {}, fields: {} }); } @@ -164,6 +167,57 @@ function parseAnalyzeTitles(lines) { title.playlistIdFromMap = normalizePlaylistId(mapping.playlistId); } + const sinfo = String(line || '').match(/^SINFO:(\d+),(\d+),(\d+),\d+,"([^"]*)"/i); + if (sinfo) { + const titleId = Number(sinfo[1]); + const streamIndex = Number(sinfo[2]); + const fieldId = Number(sinfo[3]); + const value = String(sinfo[4] || '').trim(); + if ( + Number.isFinite(titleId) && titleId >= 0 + && Number.isFinite(streamIndex) && streamIndex >= 0 + && Number.isFinite(fieldId) + ) { + const title = ensureTitle(titleId); + const streamKey = String(Math.trunc(streamIndex)); + if (!title.streams[streamKey]) { + title.streams[streamKey] = { + index: Math.trunc(streamIndex), + type: null, + language: null, + languageLabel: null, + format: null, + channels: null, + description: null + }; + } + const stream = title.streams[streamKey]; + if (fieldId === 1) { + const lowered = value.toLowerCase(); + if (lowered.includes('audio')) { + stream.type = 'audio'; + } else if (lowered.includes('subtitle') || lowered.includes('untertitel') || lowered.includes('text')) { + stream.type = 'subtitle'; + } + } else if (fieldId === 3) { + stream.language = value ? value.toLowerCase() : null; + } else if (fieldId === 4) { + stream.languageLabel = value || null; + } else if (fieldId === 6 || fieldId === 7) { + if (!stream.format || fieldId === 6) { + stream.format = value || null; + } + } else if (fieldId === 14 || fieldId === 40) { + if (!stream.channels || fieldId === 40) { + stream.channels = value || null; + } + } else if (fieldId === 30) { + stream.description = value || null; + } + } + continue; + } + const tinfo = String(line || '').match(/^TINFO:(\d+),(\d+),\d+,"([^"]*)"/i); if (!tinfo) { continue; @@ -242,20 +296,64 @@ function parseAnalyzeTitles(lines) { const playlistId = normalizePlaylistId(item.playlistId); const playlistIdFromMap = normalizePlaylistId(item.playlistIdFromMap); const playlistIdFromField16 = normalizePlaylistId(item.playlistIdFromField16); - // Prefer explicit title<->playlist map lines from MakeMKV (MSG:3016). - const resolvedPlaylistId = playlistIdFromMap || playlistIdFromField16 || playlistId; + const field16Raw = String(item?.fields?.[16] || '').trim(); + const hasField16 = field16Raw.length > 0; + const field16LooksPlaylist = /\.mpls$/i.test(field16Raw) || /^\d{1,5}$/i.test(field16Raw); + const field16LooksClip = /\.(?:m2ts|m2t|mts)$/i.test(field16Raw); + let resolvedPlaylistId = null; + + // TINFO:16 is part of the final title block and is more reliable than MSG:3307 + // lines, which can include pre-dedup title ids. + if (field16LooksPlaylist && playlistIdFromField16) { + resolvedPlaylistId = playlistIdFromField16; + } else if (!hasField16) { + resolvedPlaylistId = playlistIdFromField16 || playlistIdFromMap || playlistId; + } else if (!field16LooksClip && playlistIdFromField16) { + resolvedPlaylistId = playlistIdFromField16; + } const segmentNumbers = Array.isArray(item.segmentNumbers) ? item.segmentNumbers : []; const segmentFiles = segmentNumbers .map((number) => toSegmentFile(number)) .filter(Boolean); + const streams = item?.streams && typeof item.streams === 'object' ? Object.values(item.streams) : []; + const sortedStreams = streams + .filter((stream) => Number.isFinite(Number(stream?.index))) + .sort((a, b) => Number(a.index) - Number(b.index)); + const audioTracks = sortedStreams + .filter((stream) => String(stream?.type || '').toLowerCase() === 'audio') + .map((stream) => ({ + id: Number(stream.index) + 1, + sourceTrackId: Number(stream.index) + 1, + language: stream.language || 'und', + languageLabel: stream.languageLabel || stream.language || 'und', + title: stream.description || null, + format: stream.format || null, + channels: stream.channels || null + })); + const subtitleTracks = sortedStreams + .filter((stream) => String(stream?.type || '').toLowerCase() === 'subtitle') + .map((stream) => ({ + id: Number(stream.index) + 1, + sourceTrackId: Number(stream.index) + 1, + language: stream.language || 'und', + languageLabel: stream.languageLabel || stream.language || 'und', + title: stream.description || null, + format: stream.format || null, + channels: null + })); + const { streams: _omitStreams, ...restItem } = item; return { - ...item, + ...restItem, playlistId: resolvedPlaylistId, playlistIdFromMap, playlistIdFromField16, playlistFile: resolvedPlaylistId ? `${resolvedPlaylistId}.mpls` : null, durationLabel: item.durationLabel || formatDuration(item.durationSeconds), + audioTracks, + subtitleTracks, + audioTrackCount: audioTracks.length, + subtitleTrackCount: subtitleTracks.length, segmentNumbers, segmentFiles }; @@ -277,6 +375,58 @@ function uniqueOrdered(values) { return output; } +function parseReportedTitleCount(lines) { + for (let index = (Array.isArray(lines) ? lines.length : 0) - 1; index >= 0; index -= 1) { + const line = String(lines[index] || '').trim(); + const match = line.match(/^TCOUNT:(\d+)/i); + if (!match) { + continue; + } + const value = Number(match[1]); + if (Number.isFinite(value) && value >= 0) { + return Math.trunc(value); + } + } + return null; +} + +function likelyRawMirrorOfPlaylist(rawTitle, playlistTitle) { + const rawDuration = Number(rawTitle?.durationSeconds || 0); + const playlistDuration = Number(playlistTitle?.durationSeconds || 0); + const rawSize = Number(rawTitle?.sizeBytes || 0); + const playlistSize = Number(playlistTitle?.sizeBytes || 0); + if (!Number.isFinite(rawDuration) || !Number.isFinite(playlistDuration) || rawDuration <= 0 || playlistDuration <= 0) { + return false; + } + if (Math.abs(rawDuration - playlistDuration) > RAW_MIRROR_DURATION_TOLERANCE_SECONDS) { + return false; + } + + if (rawSize > 0 && playlistSize > 0) { + return Math.abs(rawSize - playlistSize) <= RAW_MIRROR_SIZE_TOLERANCE_BYTES; + } + return true; +} + +function suppressRawMirrorCandidates(candidates) { + const rows = Array.isArray(candidates) ? candidates : []; + if (rows.length <= 1) { + return rows; + } + + const playlistRows = rows.filter((item) => normalizePlaylistId(item?.playlistId)); + if (playlistRows.length === 0) { + return rows; + } + + return rows.filter((item) => { + if (normalizePlaylistId(item?.playlistId)) { + return true; + } + return !playlistRows.some((playlistRow) => likelyRawMirrorOfPlaylist(item, playlistRow)); + }); +} + function buildSimilarityGroups(candidates, durationSimilaritySeconds) { const list = Array.isArray(candidates) ? [...candidates] : []; const tolerance = Math.max(0, Math.round(Number(durationSimilaritySeconds || 0))); @@ -506,37 +656,45 @@ function extractPlaylistMismatchWarnings(titles) { .filter((title) => String(title.playlistIdFromMap) !== String(title.playlistIdFromField16)) .slice(0, 25) .map((title) => - `Titel #${title.titleId}: MSG-Playlist=${title.playlistIdFromMap}.mpls, TINFO16=${title.playlistIdFromField16}.mpls (MSG bevorzugt)` + `Titel #${title.titleId}: MSG-Playlist=${title.playlistIdFromMap}.mpls, TINFO16=${title.playlistIdFromField16}.mpls (TINFO16 bevorzugt)` ); } function analyzePlaylistObfuscation(lines, minLengthMinutes = 60, options = {}) { const parsedTitles = parseAnalyzeTitles(lines); + const reportedTitleCount = parseReportedTitleCount(lines); const minSeconds = Math.max(0, Math.round(Number(minLengthMinutes || 0) * 60)); const durationSimilaritySeconds = Math.max( 0, Math.round(Number(options.durationSimilaritySeconds || DEFAULT_DURATION_SIMILARITY_SECONDS)) ); - const candidates = parsedTitles + const candidatesRaw = parsedTitles .filter((item) => Number(item.durationSeconds || 0) >= minSeconds) .sort((a, b) => b.durationSeconds - a.durationSeconds || b.sizeBytes - a.sizeBytes || a.titleId - b.titleId); + const candidates = suppressRawMirrorCandidates(candidatesRaw) + .slice() + .sort((a, b) => b.durationSeconds - a.durationSeconds || b.sizeBytes - a.sizeBytes || a.titleId - b.titleId); + const playlistBackedCandidates = candidates + .filter((item) => normalizePlaylistId(item?.playlistId)); + const candidatePlaylistsAll = uniqueOrdered( + playlistBackedCandidates.map((item) => item.playlistId).filter(Boolean) + ); - const similarityGroups = buildSimilarityGroups(candidates, durationSimilaritySeconds); + const similarityGroups = buildSimilarityGroups(playlistBackedCandidates, durationSimilaritySeconds); const obfuscationDetected = similarityGroups.length > 0; - const multipleCandidatesDetected = candidates.length > 1; + const multipleCandidatesDetected = candidatePlaylistsAll.length > 1; const manualDecisionRequired = multipleCandidatesDetected; - const decisionPool = manualDecisionRequired ? candidates : []; + const decisionPool = manualDecisionRequired ? playlistBackedCandidates : []; const evaluatedCandidates = decisionPool.length > 0 ? scoreCandidates(decisionPool) : []; const recommendation = evaluatedCandidates[0] || null; - const candidatePlaylists = manualDecisionRequired - ? uniqueOrdered(decisionPool.map((item) => item.playlistId).filter(Boolean)) - : []; + const candidatePlaylists = manualDecisionRequired ? candidatePlaylistsAll : []; const playlistSegments = buildPlaylistSegmentMap(decisionPool); const playlistToTitleId = buildPlaylistToTitleIdMap(parsedTitles); return { generatedAt: new Date().toISOString(), + reportedTitleCount, minLengthMinutes: Number(minLengthMinutes || 0), minLengthSeconds: minSeconds, durationSimilaritySeconds, @@ -570,6 +728,9 @@ function analyzePlaylistObfuscation(lines, minLengthMinutes = 60, options = {}) }, warningLines: [ ...extractWarningLines(lines), + ...(reportedTitleCount !== null && reportedTitleCount !== parsedTitles.length + ? [`Titel-Anzahl abweichend: TCOUNT=${reportedTitleCount}, geparst=${parsedTitles.length}`] + : []), ...extractPlaylistMismatchWarnings(parsedTitles) ].slice(0, 60) }; diff --git a/backend/src/utils/progressParsers.js b/backend/src/utils/progressParsers.js index 684b699..110aca5 100644 --- a/backend/src/utils/progressParsers.js +++ b/backend/src/utils/progressParsers.js @@ -33,12 +33,12 @@ function parseMakeMkvProgress(line) { const prgv = line.match(/PRGV:(\d+),(\d+),(\d+)/); if (prgv) { // Format: PRGV:current,total,max (official makemkv docs) - // progress = current / max - const current = Number(prgv[1]); + // current = per-file progress, total = overall progress across all files + const total = Number(prgv[2]); const max = Number(prgv[3]); if (max > 0) { - return { percent: clampPercent((current / max) * 100), eta: null }; + return { percent: clampPercent((total / max) * 100), eta: null }; } } diff --git a/deploy-ripster.sh b/deploy-ripster.sh index acacaaa..56aa6ee 100755 --- a/deploy-ripster.sh +++ b/deploy-ripster.sh @@ -31,6 +31,8 @@ sshpass -p "$SSH_PASSWORD" ssh $SSH_OPTS "${REMOTE_USER}@${REMOTE_HOST}" "set -e echo "Uebertrage lokalen Ordner ${LOCAL_PATH} nach ${REMOTE_TARGET} ..." echo "backend/data wird weder uebertragen noch auf dem Ziel geloescht: ${DATA_RELATIVE_DIR}" sshpass -p "$SSH_PASSWORD" rsync -az --progress --delete \ + --exclude "${DATA_RELATIVE_DIR}" \ + --filter "protect ${DATA_RELATIVE_DIR}" \ --filter "protect debug" \ -e "ssh $SSH_OPTS" \ "${LOCAL_PATH}/" "${REMOTE_TARGET}/" diff --git a/docs/api/crons.md b/docs/api/crons.md index e17a7be..df22460 100644 --- a/docs/api/crons.md +++ b/docs/api/crons.md @@ -1,16 +1,12 @@ # Cron API -Ripster enthält ein eingebautes Cron-System, mit dem **Skripte** und **Skript-Ketten** zeitgesteuert oder manuell ausgeführt werden können. Der Cron-Dienst benötigt keine externen Pakete – der Cron-Expression-Parser ist vollständig im Backend implementiert. +Ripster enthält ein eingebautes Cron-System für Skripte und Skript-Ketten (`sourceType: script|chain`). --- -## Endpunkte +## GET /api/crons -### `GET /api/crons` - -Alle konfigurierten Cron-Jobs auflisten. - -**Antwort:** +Listet alle Cron-Jobs. ```json { @@ -21,13 +17,14 @@ Alle konfigurierten Cron-Jobs auflisten. "cronExpression": "0 2 * * *", "sourceType": "script", "sourceId": 3, + "sourceName": "Backup-Skript", "enabled": true, "pushoverEnabled": true, - "lastRunAt": "2026-03-09T02:00:00.000Z", + "lastRunAt": "2026-03-10T02:00:00.000Z", "lastRunStatus": "success", - "nextRunAt": "2026-03-10T02:00:00.000Z", + "nextRunAt": "2026-03-11T02:00:00.000Z", "createdAt": "2026-03-01T10:00:00.000Z", - "updatedAt": "2026-03-09T02:00:00.000Z" + "updatedAt": "2026-03-10T02:00:05.000Z" } ] } @@ -35,11 +32,9 @@ Alle konfigurierten Cron-Jobs auflisten. --- -### `POST /api/crons` +## POST /api/crons -Neuen Cron-Job anlegen. - -**Body:** +Erstellt Cron-Job. ```json { @@ -52,16 +47,25 @@ Neuen Cron-Job anlegen. } ``` -| Feld | Typ | Pflicht | Beschreibung | -|------|-----|---------|-------------| -| `name` | string | ✓ | Anzeigename | -| `cronExpression` | string | ✓ | 5-Felder-Cron-Ausdruck (Minute Stunde Tag Monat Wochentag) | -| `sourceType` | string | ✓ | `"script"` oder `"chain"` | -| `sourceId` | number | ✓ | ID des Skripts bzw. der Kette | -| `enabled` | boolean | – | Aktiviert (default: `true`) | -| `pushoverEnabled` | boolean | – | PushOver-Benachrichtigung nach Ausführung (default: `true`) | +Response: `201` mit `{ "job": { ... } }` -**Antwort:** `201 Created` +--- + +## GET /api/crons/:id + +Response: + +```json +{ "job": { "id": 1, "name": "..." } } +``` + +--- + +## PUT /api/crons/:id + +Aktualisiert Cron-Job. Felder wie bei `POST`. + +Response: ```json { "job": { ... } } @@ -69,53 +73,27 @@ Neuen Cron-Job anlegen. --- -### `GET /api/crons/:id` +## DELETE /api/crons/:id -Einzelnen Cron-Job abrufen. - -**Antwort:** +Response: ```json -{ "job": { ... } } +{ "removed": { "id": 1, "name": "Nachtlauf Backup" } } ``` --- -### `PUT /api/crons/:id` +## GET /api/crons/:id/logs -Cron-Job aktualisieren. Body-Felder entsprechen `POST /api/crons`. - -**Antwort:** - -```json -{ "job": { ... } } -``` - ---- - -### `DELETE /api/crons/:id` - -Cron-Job löschen. - -**Antwort:** - -```json -{ "removed": { "id": 1 } } -``` - ---- - -### `GET /api/crons/:id/logs` - -Ausführungs-Logs eines Cron-Jobs abrufen. +Liefert Ausführungs-Logs. **Query-Parameter:** | Parameter | Typ | Default | Beschreibung | |-----------|-----|---------|-------------| -| `limit` | number | 20 | Anzahl Einträge (max. 100) | +| `limit` | number | `20` | Anzahl Einträge, max. `100` | -**Antwort:** +**Response:** ```json { @@ -123,62 +101,54 @@ Ausführungs-Logs eines Cron-Jobs abrufen. { "id": 42, "cronJobId": 1, - "startedAt": "2026-03-09T02:00:01.000Z", - "finishedAt": "2026-03-09T02:00:05.000Z", + "startedAt": "2026-03-10T02:00:01.000Z", + "finishedAt": "2026-03-10T02:00:05.000Z", "status": "success", - "exitCode": 0, - "stdout": "Backup abgeschlossen.", - "stderr": "", - "triggeredBy": "cron" + "output": "Backup abgeschlossen.", + "errorMessage": null } ] } ``` -| Feld | Beschreibung | -|------|-------------| -| `status` | `"success"`, `"error"` oder `"running"` | -| `triggeredBy` | `"cron"` (zeitgesteuert) oder `"manual"` (manuell ausgelöst) | +`status`: `running` | `success` | `error` --- -### `POST /api/crons/:id/run` +## POST /api/crons/:id/run -Cron-Job sofort manuell auslösen (unabhängig vom Zeitplan). +Triggert Job manuell (asynchron). -**Antwort:** +**Response:** ```json -{ - "status": "success", - "exitCode": 0, - "stdout": "...", - "stderr": "" -} +{ "triggered": true, "cronJobId": 1 } ``` +Wenn Job bereits läuft: `409`. + --- -### `POST /api/crons/validate-expression` +## POST /api/crons/validate-expression -Cron-Ausdruck validieren und nächsten Ausführungszeitpunkt berechnen. +Validiert 5-Felder-Cron-Ausdruck und berechnet nächsten Lauf. -**Body:** +**Request:** ```json { "cronExpression": "*/15 * * * *" } ``` -**Antwort (gültig):** +**Gültige Response:** ```json { "valid": true, - "nextRunAt": "2026-03-09T14:15:00.000Z" + "nextRunAt": "2026-03-10T14:15:00.000Z" } ``` -**Antwort (ungültig):** +**Ungültige Response:** ```json { @@ -190,54 +160,23 @@ Cron-Ausdruck validieren und nächsten Ausführungszeitpunkt berechnen. --- -## Cron-Expression-Format +## Cron-Format -Ripster verwendet **5-Felder-Cron-Ausdrücke** (kein Sekunden-Feld): +Ripster unterstützt 5 Felder: -``` -┌───────────── Minute (0-59) -│ ┌────────── Stunde (0-23) -│ │ ┌─────── Tag (1-31) -│ │ │ ┌──── Monat (1-12) -│ │ │ │ ┌─ Wochentag (0-7, 0 und 7 = Sonntag) -│ │ │ │ │ -* * * * * +```text +Minute Stunde Tag Monat Wochentag ``` -### Beispiele +Beispiele: -| Ausdruck | Beschreibung | -|----------|-------------| -| `0 2 * * *` | Täglich um 02:00 Uhr | -| `*/15 * * * *` | Alle 15 Minuten | -| `0 6 * * 1-5` | Montag–Freitag um 06:00 Uhr | -| `30 23 * * 0` | Sonntags um 23:30 Uhr | -| `0 0 1 * *` | Erster Tag des Monats um Mitternacht | - -### Unterstützte Syntax - -| Syntax | Bedeutung | -|--------|----------| -| `*` | Jeder Wert | -| `*/n` | Jeder n-te Wert (Step) | -| `a-b` | Bereich von a bis b | -| `a,b,c` | Liste von Werten | -| Kombinierbar | z. B. `1,5-10,*/3` | +- `0 2 * * *` täglich 02:00 +- `*/15 * * * *` alle 15 Minuten +- `0 6 * * 1-5` Mo-Fr 06:00 --- -## WebSocket-Event +## WebSocket-Events zu Cron -Bei Änderungen an Cron-Jobs (Anlegen, Aktualisieren, Löschen) wird ein `CRON_JOBS_UPDATED`-Event gesendet: - -```json -{ - "type": "CRON_JOBS_UPDATED", - "payload": { - "action": "created", - "id": 1 - } -} -``` - -`action` ist `"created"`, `"updated"` oder `"deleted"`. +- `CRON_JOBS_UPDATED` bei Create/Update/Delete +- `CRON_JOB_UPDATED` bei Laufzeitstatus (`running` -> `success|error`) diff --git a/docs/api/history.md b/docs/api/history.md index df6457b..fc861b6 100644 --- a/docs/api/history.md +++ b/docs/api/history.md @@ -1,23 +1,23 @@ # History API -Endpunkte für die Job-Histoire, Dateimanagement und Orphan-Import. +Endpunkte für Job-Historie, Orphan-Import und Löschoperationen. --- ## GET /api/history -Gibt eine Liste aller Jobs zurück, optional gefiltert. +Liefert Jobs (optionale Filter). **Query-Parameter:** | Parameter | Typ | Beschreibung | |----------|-----|-------------| -| `status` | string | Filtert nach Status (z.B. `FINISHED`, `ERROR`) | -| `search` | string | Sucht in Filmtiteln | +| `status` | string | Filter nach Job-Status | +| `search` | string | Suche in Titel-Feldern | **Beispiel:** -``` +```text GET /api/history?status=FINISHED&search=Inception ``` @@ -30,17 +30,15 @@ GET /api/history?status=FINISHED&search=Inception "id": 42, "status": "FINISHED", "title": "Inception", - "imdb_id": "tt1375666", - "omdb_year": "2010", - "omdb_type": "movie", - "omdb_poster": "https://...", - "raw_path": "/mnt/nas/raw/Inception_t00.mkv", - "output_path": "/mnt/nas/movies/Inception (2010).mkv", - "created_at": "2024-01-15T10:00:00.000Z", - "updated_at": "2024-01-15T12:30:00.000Z" + "raw_path": "/mnt/raw/Inception - RAW - job-42", + "output_path": "/mnt/movies/Inception (2010)/Inception (2010).mkv", + "mediaType": "bluray", + "ripSuccessful": true, + "encodeSuccess": true, + "created_at": "2026-03-10T08:00:00.000Z", + "updated_at": "2026-03-10T10:00:00.000Z" } - ], - "total": 1 + ] } ``` @@ -48,34 +46,37 @@ GET /api/history?status=FINISHED&search=Inception ## GET /api/history/:id -Gibt Detail-Informationen für einen einzelnen Job zurück. - -**URL-Parameter:** `id` – Job-ID +Liefert Job-Detail. **Query-Parameter:** | Parameter | Typ | Standard | Beschreibung | |----------|-----|---------|-------------| -| `includeLogs` | boolean | `false` | Log-Inhalte einschließen | -| `includeLiveLog` | boolean | `false` | Aktuellen Live-Log einschließen | +| `includeLogs` | bool | `false` | Prozesslog laden | +| `includeLiveLog` | bool | `false` | alias-artig ebenfalls Prozesslog laden | +| `includeAllLogs` | bool | `false` | vollständiges Log statt Tail | +| `logTailLines` | number | `800` | Tail-Länge falls nicht `includeAllLogs` | **Response:** ```json { - "id": 42, - "status": "FINISHED", - "title": "Inception", - "imdb_id": "tt1375666", - "encode_plan": { ... }, - "makemkv_output": { ... }, - "mediainfo_output": { ... }, - "handbrake_log": "/path/to/log", - "logs": { - "handbrake": "Encoding: task 1 of 1, 100.0%\n..." - }, - "created_at": "2024-01-15T10:00:00.000Z", - "updated_at": "2024-01-15T12:30:00.000Z" + "job": { + "id": 42, + "status": "FINISHED", + "makemkvInfo": {}, + "mediainfoInfo": {}, + "handbrakeInfo": {}, + "encodePlan": {}, + "log": "...", + "log_count": 1, + "logMeta": { + "loaded": true, + "total": 800, + "returned": 800, + "truncated": true + } + } } ``` @@ -83,14 +84,19 @@ Gibt Detail-Informationen für einen einzelnen Job zurück. ## GET /api/history/database -Gibt alle rohen Datenbankzeilen zurück (Debug-Ansicht). +Debug-Ansicht der DB-Zeilen (angereichert). **Response:** ```json { - "jobs": [ { "id": 1, "status": "FINISHED", ... } ], - "total": 15 + "rows": [ + { + "id": 42, + "status": "FINISHED", + "rawFolderName": "Inception - RAW - job-42" + } + ] } ``` @@ -98,18 +104,25 @@ Gibt alle rohen Datenbankzeilen zurück (Debug-Ansicht). ## GET /api/history/orphan-raw -Findet Raw-Ordner, die nicht als Jobs in der Datenbank registriert sind. +Sucht RAW-Ordner ohne zugehörigen Job. **Response:** ```json { - "orphans": [ + "rawDir": "/mnt/raw", + "rawDirs": ["/mnt/raw", "/mnt/raw-bluray"], + "rows": [ { - "path": "/mnt/nas/raw/UnknownMovie_2023-12-01", - "size": "45.2 GB", - "modifiedAt": "2023-12-01T15:00:00.000Z", - "files": ["t00.mkv", "t01.mkv"] + "rawPath": "/mnt/raw/Inception (2010) [tt1375666] - RAW - job-99", + "folderName": "Inception (2010) [tt1375666] - RAW - job-99", + "title": "Inception", + "year": 2010, + "imdbId": "tt1375666", + "folderJobId": 99, + "entryCount": 4, + "hasBlurayStructure": true, + "lastModifiedAt": "2026-03-10T09:00:00.000Z" } ] } @@ -119,35 +132,28 @@ Findet Raw-Ordner, die nicht als Jobs in der Datenbank registriert sind. ## POST /api/history/orphan-raw/import -Importiert einen Orphan-Raw-Ordner als Job in die Datenbank. +Importiert RAW-Ordner als FINISHED-Job. **Request:** ```json -{ - "path": "/mnt/nas/raw/UnknownMovie_2023-12-01" -} +{ "rawPath": "/mnt/raw/Inception (2010) [tt1375666] - RAW - job-99" } ``` **Response:** ```json { - "ok": true, - "jobId": 99, - "message": "Orphan-Ordner als Job importiert" + "job": { "id": 77, "status": "FINISHED" }, + "uiReset": { "reset": true, "state": "IDLE" } } ``` -Nach dem Import kann dem Job über `/api/history/:id/omdb/assign` Metadaten zugewiesen werden. - --- ## POST /api/history/:id/omdb/assign -Weist einem bestehenden Job OMDb-Metadaten nachträglich zu. - -**URL-Parameter:** `id` – Job-ID +Weist OMDb-/Metadaten nachträglich zu. **Request:** @@ -155,44 +161,42 @@ Weist einem bestehenden Job OMDb-Metadaten nachträglich zu. { "imdbId": "tt1375666", "title": "Inception", - "year": "2010", - "type": "movie", - "poster": "https://..." + "year": 2010, + "poster": "https://...", + "fromOmdb": true } ``` **Response:** ```json -{ "ok": true } +{ "job": { "id": 42, "imdb_id": "tt1375666" } } ``` --- ## POST /api/history/:id/delete-files -Löscht die Dateien eines Jobs (Raw und/oder Output), behält den Job-Eintrag. - -**URL-Parameter:** `id` – Job-ID +Löscht Dateien eines Jobs, behält DB-Eintrag. **Request:** ```json -{ - "deleteRaw": true, - "deleteOutput": false -} +{ "target": "both" } ``` +`target`: `raw` | `movie` | `both` + **Response:** ```json { - "ok": true, - "deleted": { - "raw": "/mnt/nas/raw/Inception_t00.mkv", - "output": null - } + "summary": { + "target": "both", + "raw": { "attempted": true, "deleted": true, "filesDeleted": 12, "dirsRemoved": 3, "reason": null }, + "movie": { "attempted": true, "deleted": false, "filesDeleted": 0, "dirsRemoved": 0, "reason": "Movie-Datei/Pfad existiert nicht." } + }, + "job": { "id": 42 } } ``` @@ -200,23 +204,38 @@ Löscht die Dateien eines Jobs (Raw und/oder Output), behält den Job-Eintrag. ## POST /api/history/:id/delete -Löscht den Job-Eintrag aus der Datenbank, optional auch die Dateien. - -**URL-Parameter:** `id` – Job-ID +Löscht Job aus DB; optional auch Dateien. **Request:** ```json -{ - "deleteFiles": true -} +{ "target": "none" } ``` +`target`: `none` | `raw` | `movie` | `both` + **Response:** ```json -{ "ok": true, "message": "Job gelöscht" } +{ + "deleted": true, + "jobId": 42, + "fileTarget": "both", + "fileSummary": { + "target": "both", + "raw": { "filesDeleted": 10 }, + "movie": { "filesDeleted": 1 } + }, + "uiReset": { + "reset": true, + "state": "IDLE" + } +} ``` -!!! warning "Unwiderruflich" - Das Löschen von Jobs und Dateien ist nicht rückgängig zu machen. +--- + +## Hinweise + +- Ein aktiver Pipeline-Job kann nicht gelöscht werden (`409`). +- Alle Löschoperationen sind irreversibel. diff --git a/docs/api/index.md b/docs/api/index.md index c446edf..66a1c03 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -1,16 +1,21 @@ # API-Referenz -Ripster bietet eine **REST-API** für alle Operationen sowie einen **WebSocket-Endpunkt** für Echtzeit-Updates. +Ripster bietet eine REST-API für Steuerung/Verwaltung sowie einen WebSocket-Endpunkt für Echtzeit-Updates. --- ## Basis-URL -``` +```text http://localhost:3001 ``` -Konfigurierbar über die Umgebungsvariable `PORT`. +API-Prefix: `/api` + +Beispiele: + +- `GET /api/health` +- `GET /api/pipeline/state` --- @@ -18,11 +23,19 @@ Konfigurierbar über die Umgebungsvariable `PORT`.
+- :material-heart-pulse: **Health** + + --- + + Service-Liveness. + + `GET /api/health` + - :material-pipe: **Pipeline API** --- - Pipeline-Steuerung: Analyse starten, Metadaten setzen, Ripping und Encoding steuern. + Analyse, Start/Retry/Cancel, Queue, Re-Encode. [:octicons-arrow-right-24: Pipeline API](pipeline.md) @@ -30,7 +43,7 @@ Konfigurierbar über die Umgebungsvariable `PORT`. --- - Einstellungen lesen und schreiben. + Einstellungen, Skripte/Ketten, User-Presets. [:octicons-arrow-right-24: Settings API](settings.md) @@ -38,7 +51,7 @@ Konfigurierbar über die Umgebungsvariable `PORT`. --- - Job-Geschichte abfragen, Jobs löschen, Orphan-Ordner importieren. + Job-Historie, Orphan-Import, Löschoperationen. [:octicons-arrow-right-24: History API](history.md) @@ -46,7 +59,7 @@ Konfigurierbar über die Umgebungsvariable `PORT`. --- - Cron-Jobs verwalten, manuell auslösen und Ausführungs-Logs abrufen. + Zeitgesteuerte Skript-/Kettenausführung. [:octicons-arrow-right-24: Cron API](crons.md) @@ -54,7 +67,7 @@ Konfigurierbar über die Umgebungsvariable `PORT`. --- - Echtzeit-Events für Pipeline-Status, Fortschritt und Disc-Erkennung. + Pipeline-, Queue-, Disk-, Settings-, Cron- und Monitoring-Events. [:octicons-arrow-right-24: WebSocket](websocket.md) @@ -64,30 +77,41 @@ Konfigurierbar über die Umgebungsvariable `PORT`. ## Authentifizierung -Die API hat **keine Authentifizierung**. Sie ist für den Einsatz im lokalen Netzwerk konzipiert. - -!!! warning "Produktionsbetrieb" - Falls Ripster öffentlich erreichbar sein soll, schütze die API mit einem Reverse-Proxy (z. B. nginx mit Basic Auth oder OAuth). +Es gibt keine eingebaute Authentifizierung. Ripster ist für lokalen Betrieb gedacht. --- ## Fehlerformat -Alle API-Fehler werden im folgenden Format zurückgegeben: +Fehler werden zentral als JSON geliefert: ```json { - "error": "Job nicht gefunden", - "details": "Kein Job mit ID 999 vorhanden" + "error": { + "message": "Job nicht gefunden.", + "statusCode": 404, + "reqId": "req_...", + "details": [ + { + "field": "name", + "message": "Name darf nicht leer sein." + } + ] + } } ``` -HTTP-Statuscodes: +`details` ist optional (z. B. bei Validierungsfehlern). + +--- + +## Häufige Statuscodes | Code | Bedeutung | -|-----|-----------| +|------|-----------| | `200` | Erfolg | -| `400` | Ungültige Anfrage | +| `201` | Ressource erstellt | +| `400` | Ungültige Anfrage / Validierungsfehler | | `404` | Ressource nicht gefunden | -| `409` | Konflikt (z.B. Pipeline bereits aktiv) | -| `500` | Interner Serverfehler | +| `409` | Konflikt (z. B. falscher Pipeline-Zustand, Job läuft bereits) | +| `500` | Interner Fehler | diff --git a/docs/api/pipeline.md b/docs/api/pipeline.md index 9f7e989..e6a26fd 100644 --- a/docs/api/pipeline.md +++ b/docs/api/pipeline.md @@ -1,14 +1,14 @@ # Pipeline API -Alle Endpunkte zur Steuerung des Ripster-Workflows. +Endpunkte zur Steuerung des Pipeline-Workflows. --- ## GET /api/pipeline/state -Liefert den aktuellen Pipeline-Snapshot. +Liefert aktuellen Pipeline- und Hardware-Monitoring-Snapshot. -**Response:** +**Response (Beispiel):** ```json { @@ -17,45 +17,46 @@ Liefert den aktuellen Pipeline-Snapshot. "activeJobId": 42, "progress": 0, "eta": null, - "statusText": "Mediainfo geladen - bitte bestätigen", + "statusText": "Mediainfo bestätigt - Encode manuell starten", "context": { "jobId": 42 }, + "jobProgress": { + "42": { + "state": "MEDIAINFO_CHECK", + "progress": 68.5, + "eta": null, + "statusText": "MEDIAINFO_CHECK 68.50%" + } + }, "queue": { "maxParallelJobs": 1, - "runningCount": 0, - "queuedCount": 0, + "runningCount": 1, + "queuedCount": 2, "runningJobs": [], "queuedJobs": [] } + }, + "hardwareMonitoring": { + "enabled": true, + "intervalMs": 5000, + "updatedAt": "2026-03-10T09:00:00.000Z", + "sample": { + "cpu": {}, + "memory": {}, + "gpu": {}, + "storage": {} + }, + "error": null } } ``` -**Pipeline-Zustände:** - -| Wert | Beschreibung | -|------|-------------| -| `IDLE` | Wartet auf Medium | -| `DISC_DETECTED` | Medium erkannt, wartet auf Analyse-Start | -| `METADATA_SELECTION` | Metadaten-Dialog aktiv | -| `WAITING_FOR_USER_DECISION` | Manuelle Playlist-Auswahl erforderlich | -| `READY_TO_START` | Übergang/Fallback vor Start | -| `RIPPING` | MakeMKV läuft | -| `MEDIAINFO_CHECK` | HandBrake-Scan + Plan-Erstellung | -| `READY_TO_ENCODE` | Review bereit | -| `ENCODING` | HandBrake-Encoding läuft (inkl. Post-Skripte) | -| `FINISHED` | Abgeschlossen | -| `CANCELLED` | Vom Benutzer abgebrochen | -| `ERROR` | Fehler | - --- ## POST /api/pipeline/analyze -Startet die Analyse für die aktuell erkannte Disc. - -**Request:** kein Body +Startet Disc-Analyse und legt Job an. **Response:** @@ -73,14 +74,21 @@ Startet die Analyse für die aktuell erkannte Disc. ## POST /api/pipeline/rescan-disc -Erzwingt eine erneute Laufwerksprüfung. +Erzwingt erneute Laufwerksprüfung. **Response (Beispiel):** ```json { "result": { - "emitted": "discInserted" + "present": true, + "changed": true, + "emitted": "discInserted", + "device": { + "path": "/dev/sr0", + "discLabel": "INCEPTION", + "mediaProfile": "bluray" + } } } ``` @@ -89,7 +97,7 @@ Erzwingt eine erneute Laufwerksprüfung. ## GET /api/pipeline/omdb/search?q= -Sucht OMDb-Titel. +OMDb-Titelsuche. **Response:** @@ -111,7 +119,7 @@ Sucht OMDb-Titel. ## POST /api/pipeline/select-metadata -Setzt Metadaten (und optional Playlist-Entscheidung). +Setzt Metadaten (und optional Playlist) für einen Job. **Request:** @@ -127,38 +135,35 @@ Setzt Metadaten (und optional Playlist-Entscheidung). } ``` -**Response:** `{ "job": { ... } }` +**Response:** -!!! note "Startlogik" - Nach Metadaten-Bestätigung wird der nächste Schritt automatisch ausgelöst (`startPreparedJob`). - Der Job startet direkt oder wird in die Queue eingereiht. +```json +{ "job": { "id": 42, "status": "READY_TO_START" } } +``` --- ## POST /api/pipeline/start/:jobId -Startet einen vorbereiteten Job manuell (z. B. Fallback/Queue-Szenario). +Startet vorbereiteten Job oder queued ihn (je nach Parallel-Limit). -**Response (Beispiel):** +**Mögliche Responses:** ```json -{ - "result": { - "started": true, - "stage": "RIPPING" - } -} +{ "result": { "started": true, "stage": "RIPPING" } } ``` -Mögliche `stage`-Werte sind u. a. `RIPPING`, `MEDIAINFO_CHECK`, `ENCODING`. +```json +{ "result": { "queued": true, "started": false, "queuePosition": 2, "action": "START_PREPARED" } } +``` --- ## POST /api/pipeline/confirm-encode/:jobId -Bestätigt Review-Auswahl (Titel/Tracks/Post-Skripte). +Bestätigt Review-Auswahl (Tracks, Pre/Post-Skripte/Ketten, User-Preset). -**Request:** +**Request (typisch):** ```json { @@ -169,78 +174,70 @@ Bestätigt Review-Auswahl (Titel/Tracks/Post-Skripte). "subtitleTrackIds": [3] } }, + "selectedPreEncodeScriptIds": [1], "selectedPostEncodeScriptIds": [2, 7], + "selectedPreEncodeChainIds": [3], + "selectedPostEncodeChainIds": [4], + "selectedUserPresetId": 5, "skipPipelineStateUpdate": false } ``` -**Response:** `{ "job": { ... } }` +**Response:** + +```json +{ "job": { "id": 42, "encode_review_confirmed": 1 } } +``` --- ## POST /api/pipeline/cancel -Bricht laufenden Job ab oder entfernt einen Queue-Eintrag. +Bricht laufenden Job ab oder entfernt Queue-Eintrag. **Request (optional):** ```json -{ - "jobId": 42 -} +{ "jobId": 42 } ``` -**Response (Beispiel):** +**Mögliche Responses:** ```json -{ - "result": { - "cancelled": true, - "queuedOnly": false, - "jobId": 42 - } -} +{ "result": { "cancelled": true, "queuedOnly": true, "jobId": 42 } } +``` + +```json +{ "result": { "cancelled": true, "queuedOnly": false, "jobId": 42 } } +``` + +```json +{ "result": { "cancelled": true, "queuedOnly": false, "pending": true, "jobId": 42 } } ``` --- ## POST /api/pipeline/retry/:jobId -Startet einen Job aus `ERROR`/`CANCELLED` erneut (oder reiht ihn in die Queue ein). - -**Response:** `{ "result": { ... } }` - ---- - -## POST /api/pipeline/resume-ready/:jobId - -Lädt einen `READY_TO_ENCODE`-Job nach Neustart wieder in die aktive Session. - -**Response:** `{ "job": { ... } }` - ---- +Retry für `ERROR`/`CANCELLED`-Jobs (oder Queue-Einreihung). ## POST /api/pipeline/reencode/:jobId Startet Re-Encode aus bestehendem RAW. -**Response:** `{ "result": { ... } }` - ---- - ## POST /api/pipeline/restart-review/:jobId -Berechnet die Review aus vorhandenem RAW neu. - -**Response:** `{ "result": { ... } }` - ---- +Berechnet Review aus RAW neu. ## POST /api/pipeline/restart-encode/:jobId -Startet Encoding mit der zuletzt bestätigten Auswahl neu. +Startet Encoding mit letzter bestätigter Review neu. -**Response:** `{ "result": { ... } }` +## POST /api/pipeline/resume-ready/:jobId + +Lädt `READY_TO_ENCODE`-Job nach Neustart wieder in aktive Session. + +Alle Endpunkte liefern `{ result: ... }` bzw. `{ job: ... }`. --- @@ -248,9 +245,51 @@ Startet Encoding mit der zuletzt bestätigten Auswahl neu. ### GET /api/pipeline/queue -Liefert den aktuellen Queue-Status. +Liefert Queue-Snapshot. -**Response:** `{ "queue": { ... } }` +```json +{ + "queue": { + "maxParallelJobs": 1, + "runningCount": 1, + "queuedCount": 3, + "runningJobs": [ + { + "jobId": 41, + "title": "Inception", + "status": "ENCODING", + "lastState": "ENCODING" + } + ], + "queuedJobs": [ + { + "entryId": 11, + "position": 1, + "type": "job", + "jobId": 42, + "action": "START_PREPARED", + "actionLabel": "Start", + "title": "Matrix", + "status": "READY_TO_ENCODE", + "lastState": "READY_TO_ENCODE", + "hasScripts": true, + "hasChains": false, + "enqueuedAt": "2026-03-10T09:00:00.000Z" + }, + { + "entryId": 12, + "position": 2, + "type": "wait", + "waitSeconds": 30, + "title": "Warten 30s", + "status": "QUEUED", + "enqueuedAt": "2026-03-10T09:01:00.000Z" + } + ], + "updatedAt": "2026-03-10T09:01:02.000Z" + } +} +``` ### POST /api/pipeline/queue/reorder @@ -260,8 +299,71 @@ Sortiert Queue-Einträge neu. ```json { - "orderedJobIds": [42, 43, 41] + "orderedEntryIds": [12, 11] } ``` -**Response:** `{ "queue": { ... } }` +Legacy fallback wird akzeptiert: + +```json +{ + "orderedJobIds": [42, 43] +} +``` + +### POST /api/pipeline/queue/entry + +Fügt Nicht-Job-Queue-Eintrag hinzu (`script`, `chain`, `wait`). + +**Request-Beispiele:** + +```json +{ "type": "script", "scriptId": 3 } +``` + +```json +{ "type": "chain", "chainId": 2, "insertAfterEntryId": 11 } +``` + +```json +{ "type": "wait", "waitSeconds": 45 } +``` + +**Response:** + +```json +{ + "result": { "entryId": 12, "type": "wait", "position": 2 }, + "queue": { "...": "..." } +} +``` + +### DELETE /api/pipeline/queue/entry/:entryId + +Entfernt Queue-Eintrag. + +**Response:** + +```json +{ "queue": { "...": "..." } } +``` + +--- + +## Pipeline-Zustände + +| State | Bedeutung | +|------|-----------| +| `IDLE` | Wartet auf Medium | +| `DISC_DETECTED` | Medium erkannt | +| `ANALYZING` | MakeMKV-Analyse läuft | +| `METADATA_SELECTION` | Metadaten-Auswahl | +| `WAITING_FOR_USER_DECISION` | Playlist-Entscheidung erforderlich | +| `READY_TO_START` | Übergang vor Start | +| `RIPPING` | MakeMKV-Rip läuft | +| `MEDIAINFO_CHECK` | Titel-/Track-Auswertung | +| `READY_TO_ENCODE` | Review bereit | +| `ENCODING` | HandBrake-Encoding läuft | +| `FINISHED` | Abgeschlossen | +| `CANCELLED` | Abgebrochen | +| `ERROR` | Fehler | diff --git a/docs/api/settings.md b/docs/api/settings.md index 57aa3a2..a893b02 100644 --- a/docs/api/settings.md +++ b/docs/api/settings.md @@ -1,38 +1,36 @@ # Settings API -Endpunkte zum Lesen und Schreiben der Anwendungseinstellungen. +Endpunkte für Einstellungen, Skripte, Skript-Ketten und User-Presets. --- ## GET /api/settings -Gibt alle Einstellungen kategorisiert zurück. +Liefert alle Einstellungen kategorisiert. -**Response:** +**Response (Struktur):** ```json { - "paths": { - "raw_dir": { - "value": "/mnt/nas/raw", - "schema": { - "type": "string", - "label": "Raw-Verzeichnis", - "description": "Speicherort für rohe MKV-Dateien", - "required": true - } - }, - "movie_dir": { - "value": "/mnt/nas/movies", - "schema": { ... } + "categories": [ + { + "category": "Pfade", + "settings": [ + { + "key": "raw_dir", + "label": "Raw Ausgabeordner", + "type": "path", + "required": true, + "description": "...", + "defaultValue": "data/output/raw", + "options": [], + "validation": { "minLength": 1 }, + "value": "data/output/raw", + "orderIndex": 100 + } + ] } - }, - "tools": { ... }, - "encoding": { ... }, - "drive": { ... }, - "makemkv": { ... }, - "omdb": { ... }, - "notifications": { ... } + ] } ``` @@ -42,42 +40,44 @@ Gibt alle Einstellungen kategorisiert zurück. Aktualisiert eine einzelne Einstellung. -**URL-Parameter:** `key` – Einstellungs-Schlüssel - **Request:** ```json -{ - "value": "/mnt/storage/raw" -} +{ "value": "/mnt/storage/raw" } ``` **Response:** ```json -{ "ok": true, "key": "raw_dir", "value": "/mnt/storage/raw" } +{ + "setting": { + "key": "raw_dir", + "value": "/mnt/storage/raw" + }, + "reviewRefresh": { + "triggered": false, + "reason": "not_ready" + } +} ``` -**Fehlerfälle:** -- `400` – Ungültiger Wert (Validierungsfehler) -- `404` – Einstellung nicht gefunden - -!!! note "Encode-Review-Refresh" - Wenn eine encoding-relevante Einstellung geändert wird (z.B. `handbrake_preset`), wird der Encode-Plan für den aktuell wartenden Job automatisch neu berechnet. +`reviewRefresh` ist `null` oder ein Objekt mit Status der optionalen Review-Neuberechnung. --- ## PUT /api/settings -Aktualisiert mehrere Einstellungen auf einmal. +Aktualisiert mehrere Einstellungen atomar. **Request:** ```json { - "raw_dir": "/mnt/storage/raw", - "movie_dir": "/mnt/storage/movies", - "handbrake_preset": "H.265 MKV 720p30" + "settings": { + "raw_dir": "/mnt/storage/raw", + "movie_dir": "/mnt/storage/movies", + "handbrake_preset_bluray": "H.264 MKV 1080p30" + } } ``` @@ -85,9 +85,36 @@ Aktualisiert mehrere Einstellungen auf einmal. ```json { - "ok": true, - "updated": ["raw_dir", "movie_dir", "handbrake_preset"], - "errors": [] + "changes": [ + { "key": "raw_dir", "value": "/mnt/storage/raw" }, + { "key": "movie_dir", "value": "/mnt/storage/movies" } + ], + "reviewRefresh": { + "triggered": true, + "jobId": 42, + "relevantKeys": ["handbrake_preset_bluray"] + } +} +``` + +Bei Validierungsfehlern kommt `400` mit `error.details[]`. + +--- + +## GET /api/settings/handbrake-presets + +Liest Preset-Liste via `HandBrakeCLI -z` (mit Fallback auf konfigurierte Presets). + +**Response (Beispiel):** + +```json +{ + "source": "handbrake-cli", + "message": null, + "options": [ + { "label": "General/", "value": "__group__general", "disabled": true, "category": "General" }, + { "label": " Fast 1080p30", "value": "Fast 1080p30", "category": "General" } + ] } ``` @@ -95,260 +122,217 @@ Aktualisiert mehrere Einstellungen auf einmal. ## POST /api/settings/pushover/test -Sendet eine Test-Benachrichtigung über PushOver. +Sendet Testnachricht über aktuelle PushOver-Settings. -**Request:** Kein Body erforderlich (verwendet gespeicherte Zugangsdaten) - -**Response (Erfolg):** - -```json -{ "ok": true, "message": "Test-Benachrichtigung gesendet" } -``` - -**Response (Fehler):** - -```json -{ "ok": false, "error": "Ungültiger API-Token" } -``` - ---- - -## Skript-Verwaltung - -Skripte werden über eigene Endpunkte unter `/api/settings/scripts` verwaltet. Jedes Skript hat eine `scriptBody`-Property (der Shell-Befehl oder mehrzeiliges Skript) und einen `orderIndex` für die Sortierung. - -### GET /api/settings/scripts - -Gibt alle Skripte zurück, sortiert nach `orderIndex`. - -**Response:** +**Request (optional):** ```json { - "scripts": [ - { - "id": 1, - "name": "Zu Plex verschieben", - "scriptBody": "mv \"$RIPSTER_OUTPUT_PATH\" /mnt/plex/movies/", - "orderIndex": 1, - "createdAt": "2026-01-15T10:00:00.000Z", - "updatedAt": "2026-01-15T10:00:00.000Z" - } - ] + "title": "Test", + "message": "Ripster Test" } ``` ---- - -### POST /api/settings/scripts - -Legt ein neues Skript an. - -**Request:** - -```json -{ - "name": "Zu Plex verschieben", - "scriptBody": "mv \"$RIPSTER_OUTPUT_PATH\" /mnt/plex/movies/" -} -``` - -| Feld | Typ | Pflicht | Beschreibung | -|------|-----|---------|-------------| -| `name` | string | ✅ | Anzeigename (eindeutig) | -| `scriptBody` | string | ✅ | Shell-Befehl oder mehrzeiliges Skript | - -**Response:** `201 Created` – `{ "script": { ... } }` - ---- - -### PUT /api/settings/scripts/:id - -Aktualisiert ein vorhandenes Skript. Alle Felder optional. - ---- - -### DELETE /api/settings/scripts/:id - -Löscht ein Skript. - -!!! warning "Referenzen" - Das Skript wird gelöscht, auch wenn es in Job-Historien referenziert ist. In zukünftigen Reviews erscheint es nicht mehr. - ---- - -### POST /api/settings/scripts/:id/test - -Führt ein Skript mit Platzhalter-Umgebungsvariablen aus (Testlauf). - -**Response:** - -```json -{ - "ok": true, - "exitCode": 0, - "stdout": "Testausgabe des Skripts", - "stderr": "", - "durationMs": 245 -} -``` - -**Platzhalter-Werte beim Testlauf:** - -| Variable | Testwert | -|---------|---------| -| `RIPSTER_OUTPUT_PATH` | `/tmp/ripster-test-output.mkv` | -| `RIPSTER_JOB_ID` | `0` | -| `RIPSTER_TITLE` | `Test Film` | -| `RIPSTER_YEAR` | `2024` | -| `RIPSTER_IMDB_ID` | `tt0000000` | -| `RIPSTER_RAW_PATH` | `/tmp/ripster-test-raw.mkv` | - ---- - -### POST /api/settings/scripts/reorder - -Ändert die Reihenfolge der Skripte (persistiert in `order_index`). - -**Request:** - -```json -{ "orderedScriptIds": [3, 1, 2] } -``` - -**Response:** `{ "scripts": [ ... ] }` – alle Skripte in neuer Reihenfolge. - ---- - -## Skript-Ketten-Verwaltung - -Skript-Ketten werden unter `/api/settings/script-chains` verwaltet. - -### GET /api/settings/script-chains - -Gibt alle Ketten zurück (inkl. Schritte). - -### POST /api/settings/script-chains - -Legt eine neue Kette an. - -```json -{ "name": "Nach Jellyfin deployen" } -``` - -### PUT /api/settings/script-chains/:id - -Aktualisiert eine Kette (Name, Schritte). - -### DELETE /api/settings/script-chains/:id - -Löscht eine Kette und alle ihre Schritte. - -### POST /api/settings/script-chains/:id/test - -Führt eine Kette mit Platzhalter-Umgebungsvariablen aus (Testlauf). - **Response:** ```json { "result": { - "success": true, - "steps": [ - { "scriptId": 1, "scriptName": "Zu Plex verschieben", "success": true, "exitCode": 0 } - ] + "sent": true, + "eventKey": "test", + "requestId": "..." } } ``` +Wenn PushOver deaktiviert ist oder Credentials fehlen, kommt i. d. R. ebenfalls `200` mit `sent: false` + `reason`. + +--- + +## Skripte + +Basis: `/api/settings/scripts` + +### GET /api/settings/scripts + +```json +{ "scripts": [ { "id": 1, "name": "...", "scriptBody": "...", "orderIndex": 1, "createdAt": "...", "updatedAt": "..." } ] } +``` + +### POST /api/settings/scripts + +```json +{ "name": "Move", "scriptBody": "mv \"$RIPSTER_OUTPUT_PATH\" /mnt/movies/" } +``` + +Response: `201` mit `{ "script": { ... } }` + +### PUT /api/settings/scripts/:id + +Body wie `POST`, Response `{ "script": { ... } }`. + +### DELETE /api/settings/scripts/:id + +Response `{ "removed": { ... } }`. + +### POST /api/settings/scripts/reorder + +```json +{ "orderedScriptIds": [3, 1, 2] } +``` + +Response `{ "scripts": [ ... ] }`. + +### POST /api/settings/scripts/:id/test + +Führt Skript als Testlauf aus. + +```json +{ + "result": { + "scriptId": 1, + "scriptName": "Move", + "success": true, + "exitCode": 0, + "signal": null, + "timedOut": false, + "durationMs": 120, + "stdout": "...", + "stderr": "...", + "stdoutTruncated": false, + "stderrTruncated": false + } +} +``` + +### Umgebungsvariablen für Skripte + +Diese Variablen werden beim Ausführen gesetzt: + +- `RIPSTER_SCRIPT_RUN_AT` +- `RIPSTER_JOB_ID` +- `RIPSTER_JOB_TITLE` +- `RIPSTER_MODE` +- `RIPSTER_INPUT_PATH` +- `RIPSTER_OUTPUT_PATH` +- `RIPSTER_RAW_PATH` +- `RIPSTER_SCRIPT_ID` +- `RIPSTER_SCRIPT_NAME` +- `RIPSTER_SCRIPT_SOURCE` + +--- + +## Skript-Ketten + +Basis: `/api/settings/script-chains` + +Eine Kette hat Schritte vom Typ: + +- `script` (`scriptId` erforderlich) +- `wait` (`waitSeconds` 1..3600) + +### GET /api/settings/script-chains + +Response `{ "chains": [ ... ] }` (inkl. `steps[]`). + +### GET /api/settings/script-chains/:id + +Response `{ "chain": { ... } }`. + +### POST /api/settings/script-chains + +```json +{ + "name": "After Encode", + "steps": [ + { "stepType": "script", "scriptId": 1 }, + { "stepType": "wait", "waitSeconds": 15 }, + { "stepType": "script", "scriptId": 2 } + ] +} +``` + +Response: `201` mit `{ "chain": { ... } }` + +### PUT /api/settings/script-chains/:id + +Body wie `POST`, Response `{ "chain": { ... } }`. + +### DELETE /api/settings/script-chains/:id + +Response `{ "removed": { ... } }`. + ### POST /api/settings/script-chains/reorder -Ändert die Reihenfolge der Ketten (persistiert in `order_index`). - -**Request:** - ```json { "orderedChainIds": [2, 1, 3] } ``` +Response `{ "chains": [ ... ] }`. + +### POST /api/settings/script-chains/:id/test + +Response: + +```json +{ + "result": { + "chainId": 2, + "chainName": "After Encode", + "steps": 3, + "succeeded": 3, + "failed": 0, + "aborted": false, + "results": [] + } +} +``` + --- ## User-Presets -Benannte HandBrake-Preset-Sammlungen, die im Encode-Review schnell angewendet werden können. Unter `/api/settings/user-presets` verwaltet. +Basis: `/api/settings/user-presets` ### GET /api/settings/user-presets -Gibt alle User-Presets zurück. Optional gefiltert per Query-Parameter `mediaType`. - -**Query-Parameter:** - -| Parameter | Werte | Beschreibung | -|-----------|-------|-------------| -| `mediaType` | `bluray`, `dvd`, `other`, `all` | Filtert Presets nach Medientyp | - -**Response:** +Optionaler Query-Parameter: `media_type=bluray|dvd|other|all` ```json { "presets": [ { "id": 1, - "name": "Blu-ray High Quality", + "name": "Blu-ray HQ", "mediaType": "bluray", - "handbrakePreset": "H.265 MKV 1080p30", + "handbrakePreset": "H.264 MKV 1080p30", "extraArgs": "--encoder-preset slow", - "description": "Langsam, aber beste Qualität", - "createdAt": "2026-01-15T10:00:00.000Z", - "updatedAt": "2026-01-15T10:00:00.000Z" + "description": "...", + "createdAt": "...", + "updatedAt": "..." } ] } ``` ---- - ### POST /api/settings/user-presets -Legt ein neues User-Preset an. - -**Request:** - ```json { - "name": "Blu-ray High Quality", + "name": "Blu-ray HQ", "mediaType": "bluray", - "handbrakePreset": "H.265 MKV 1080p30", + "handbrakePreset": "H.264 MKV 1080p30", "extraArgs": "--encoder-preset slow", - "description": "Langsam, aber beste Qualität" + "description": "optional" } ``` -| Feld | Typ | Pflicht | Beschreibung | -|------|-----|---------|-------------| -| `name` | string | ✅ | Anzeigename | -| `mediaType` | string | — | `bluray`, `dvd`, `other`, `all` (Standard: `all`) | -| `handbrakePreset` | string | — | HandBrake-Preset-Name (`-Z`) | -| `extraArgs` | string | — | Zusatz-CLI-Argumente | -| `description` | string | — | Optionale Beschreibung | - -**Response:** `201 Created` – `{ "preset": { ... } }` - ---- +Response: `201` mit `{ "preset": { ... } }` ### PUT /api/settings/user-presets/:id -Aktualisiert ein User-Preset. Alle Felder optional. - ---- +Body mit beliebigen Feldern aus `POST`, Response `{ "preset": { ... } }`. ### DELETE /api/settings/user-presets/:id -Löscht ein User-Preset. - ---- - -## Einstellungs-Schlüssel Referenz - -Eine vollständige Übersicht aller Schlüssel: -[:octicons-arrow-right-24: Einstellungsreferenz](../configuration/settings-reference.md) +Response `{ "removed": { ... } }`. diff --git a/docs/api/websocket.md b/docs/api/websocket.md index cb52704..13b3b4c 100644 --- a/docs/api/websocket.md +++ b/docs/api/websocket.md @@ -1,6 +1,6 @@ # WebSocket Events -Ripster sendet Echtzeit-Updates über WebSocket unter `/ws`. +Ripster sendet Echtzeit-Updates über `/ws`. --- @@ -10,8 +10,8 @@ Ripster sendet Echtzeit-Updates über WebSocket unter `/ws`. const ws = new WebSocket('ws://localhost:3001/ws'); ws.onmessage = (event) => { - const message = JSON.parse(event.data); - console.log(message.type, message.payload); + const msg = JSON.parse(event.data); + console.log(msg.type, msg.payload); }; ``` @@ -19,36 +19,38 @@ ws.onmessage = (event) => { ## Nachrichtenformat -Alle Broadcasts haben dieses Schema: +Die meisten Broadcasts haben dieses Schema: ```json { "type": "EVENT_TYPE", - "payload": { }, - "timestamp": "2026-03-05T10:00:00.000Z" + "payload": {}, + "timestamp": "2026-03-10T09:00:00.000Z" } ``` +Ausnahme: `WS_CONNECTED` beim Verbindungsaufbau enthält kein `timestamp`. + --- ## Event-Typen ### WS_CONNECTED -Wird direkt nach Verbindungsaufbau gesendet. +Sofort nach erfolgreicher Verbindung. ```json { "type": "WS_CONNECTED", "payload": { - "connectedAt": "2026-03-05T10:00:00.000Z" + "connectedAt": "2026-03-10T09:00:00.000Z" } } ``` ### PIPELINE_STATE_CHANGED -Snapshot bei Zustandswechsel. +Neuer Pipeline-Snapshot. ```json { @@ -56,14 +58,24 @@ Snapshot bei Zustandswechsel. "payload": { "state": "ENCODING", "activeJobId": 42, - "progress": 73.5, + "progress": 62.5, "eta": "00:12:34", - "statusText": "Encoding mit HandBrake", + "statusText": "ENCODING 62.50%", "context": {}, + "jobProgress": { + "42": { + "state": "ENCODING", + "progress": 62.5, + "eta": "00:12:34", + "statusText": "ENCODING 62.50%" + } + }, "queue": { "maxParallelJobs": 1, "runningCount": 1, - "queuedCount": 0 + "queuedCount": 2, + "runningJobs": [], + "queuedJobs": [] } } } @@ -71,7 +83,7 @@ Snapshot bei Zustandswechsel. ### PIPELINE_PROGRESS -Laufende Fortschrittsupdates während aktiver Phasen. +Laufende Fortschrittsupdates. ```json { @@ -79,33 +91,20 @@ Laufende Fortschrittsupdates während aktiver Phasen. "payload": { "state": "ENCODING", "activeJobId": 42, - "progress": 73.5, + "progress": 62.5, "eta": "00:12:34", - "statusText": "ENCODING 73.50% - task 1 of 1" + "statusText": "ENCODING 62.50%" } } ``` ### PIPELINE_QUEUE_CHANGED -Aktualisierung der Job-Queue. +Queue-Snapshot aktualisiert. -```json -{ - "type": "PIPELINE_QUEUE_CHANGED", - "payload": { - "maxParallelJobs": 1, - "runningCount": 1, - "queuedCount": 2, - "runningJobs": [], - "queuedJobs": [] - } -} -``` +### DISC_DETECTED / DISC_REMOVED -### DISC_DETECTED - -Disc erkannt. +Disc-Insertion/-Removal. ```json { @@ -114,7 +113,6 @@ Disc erkannt. "device": { "path": "/dev/sr0", "discLabel": "INCEPTION", - "label": "INCEPTION", "model": "ASUS BW-16D1HT", "fstype": "udf", "mountpoint": null, @@ -124,132 +122,93 @@ Disc erkannt. } ``` -`mediaProfile` ist `"bluray"`, `"dvd"`, `"other"` oder `null` (wenn nicht bestimmbar). Der Wert wird aus Dateisystemtyp (UDF/ISO9660), Laufwerk-Modell und Disc-Label abgeleitet. +`mediaProfile`: `bluray` | `dvd` | `other` | `null` -### DISC_REMOVED +### HARDWARE_MONITOR_UPDATE -Disc entfernt. +Snapshot aus Hardware-Monitoring. ```json { - "type": "DISC_REMOVED", + "type": "HARDWARE_MONITOR_UPDATE", "payload": { - "device": { - "path": "/dev/sr0" - } + "enabled": true, + "intervalMs": 5000, + "updatedAt": "2026-03-10T09:00:00.000Z", + "sample": { + "cpu": {}, + "memory": {}, + "gpu": {}, + "storage": {} + }, + "error": null } } ``` ### PIPELINE_ERROR -Fehler bei Pipeline-Disc-Events im Backend. - -```json -{ - "type": "PIPELINE_ERROR", - "payload": { - "message": "..." - } -} -``` +Fehler bei Disc-Event-Verarbeitung in Pipeline. ### DISK_DETECTION_ERROR -Fehler im Laufwerkserkennungsdienst. +Fehler in Laufwerkserkennung. + +### SETTINGS_UPDATED + +Einzelnes Setting wurde gespeichert. + +### SETTINGS_BULK_UPDATED + +Bulk-Settings gespeichert. ```json { - "type": "DISK_DETECTION_ERROR", + "type": "SETTINGS_BULK_UPDATED", "payload": { - "message": "..." + "count": 3, + "keys": ["raw_dir", "movie_dir", "handbrake_preset_bluray"] } } ``` ### SETTINGS_SCRIPTS_UPDATED -Wird gesendet, wenn ein Skript angelegt, aktualisiert, gelöscht oder umsortiert wurde. - -```json -{ - "type": "SETTINGS_SCRIPTS_UPDATED", - "payload": { - "action": "reordered", - "count": 3 - } -} -``` - -`action` ist `"created"`, `"updated"`, `"deleted"` oder `"reordered"`. +Skript geändert (`created|updated|deleted|reordered`). ### SETTINGS_SCRIPT_CHAINS_UPDATED -Wird gesendet bei Änderungen an Skript-Ketten. - -```json -{ - "type": "SETTINGS_SCRIPT_CHAINS_UPDATED", - "payload": { - "action": "created", - "id": 2 - } -} -``` +Skript-Kette geändert (`created|updated|deleted|reordered`). ### USER_PRESETS_UPDATED -Wird gesendet, wenn ein User-Preset angelegt, aktualisiert oder gelöscht wurde. - -```json -{ - "type": "USER_PRESETS_UPDATED", - "payload": { - "action": "created", - "id": 1 - } -} -``` - -`action` ist `"created"`, `"updated"` oder `"deleted"`. +User-Preset geändert (`created|updated|deleted`). ### CRON_JOBS_UPDATED -Wird gesendet, wenn ein Cron-Job angelegt, aktualisiert oder gelöscht wurde. +Cron-Config geändert (`created|updated|deleted`). + +### CRON_JOB_UPDATED + +Laufzeitstatus eines Cron-Jobs geändert. ```json { - "type": "CRON_JOBS_UPDATED", + "type": "CRON_JOB_UPDATED", "payload": { - "action": "created", - "id": 1 + "id": 1, + "lastRunStatus": "running", + "lastRunAt": "2026-03-10T10:00:00.000Z", + "nextRunAt": null } } ``` -`action` ist `"created"`, `"updated"` oder `"deleted"`. - --- ## Reconnect-Verhalten -`useWebSocket.js` versucht bei Verbindungsabbruch automatisch erneut zu verbinden. +`useWebSocket` verbindet bei Abbruch automatisch neu: -- fester Retry-Intervall: `1500ms` -- erneuter Versuch bis zum Unmount der Komponente - ---- - -## React-Beispiel - -```js -import { useWebSocket } from './hooks/useWebSocket'; - -useWebSocket({ - onMessage: (msg) => { - if (msg.type === 'PIPELINE_STATE_CHANGED') { - setPipeline(msg.payload); - } - } -}); -``` +- Retry-Intervall: `1500ms` +- Wiederverbindung bis Komponente unmounted wird diff --git a/docs/architecture/backend.md b/docs/architecture/backend.md index d0a63b2..02963de 100644 --- a/docs/architecture/backend.md +++ b/docs/architecture/backend.md @@ -1,305 +1,116 @@ # Backend-Services -Das Backend ist in Node.js/Express geschrieben und in **Services** aufgeteilt, die jeweils eine klar abgegrenzte Verantwortlichkeit haben. +Das Backend ist in Services aufgeteilt, die von Express-Routen orchestriert werden. --- -## pipelineService.js +## `pipelineService.js` -**Der Kern von Ripster** – orchestriert den gesamten Ripping-Workflow. +Zentrale Workflow-Orchestrierung. -### Zuständigkeiten +Aufgaben: -- Verwaltung des Pipeline-Zustands als State Machine -- Koordination zwischen allen externen Tools -- Generierung von Encode-Plänen -- Fehlerbehandlung und Recovery +- Pipeline-State-Machine + Persistenz (`pipeline_state`) +- Disc-Analyse/Rip/Review/Encode +- Queue-Management (Jobs + `script|chain|wait` Einträge) +- Retry/Re-Encode/Restart-Flows +- WebSocket-Broadcasts für State/Progress/Queue -### Haupt-Methoden +Wichtige Methoden: -| Methode | Beschreibung | -|---------|-------------| -| `analyzeDisc()` | Legt Job an und öffnet Metadaten-Auswahl | -| `selectMetadata({...})` | Setzt Metadaten/Playlist und triggert Auto-Start | -| `startPreparedJob(jobId)` | Startet vorbereiteten Job (oder Queue) | -| `confirmEncodeReview(jobId, options)` | Bestätigt Review inkl. Track/Skript-Auswahl | -| `cancel(jobId)` | Bricht laufenden Job ab oder entfernt Queue-Eintrag | -| `retry(jobId)` | Startet fehlgeschlagenen/abgebrochenen Job neu | -| `reencodeFromRaw(jobId)` | Encodiert aus vorhandenem RAW neu | -| `restartReviewFromRaw(jobId)` | Berechnet Review aus RAW neu | -| `restartEncodeWithLastSettings(jobId)` | Neustart mit letzter bestätigter Auswahl | -| `resumeReadyToEncodeJob(jobId)` | Lädt READY_TO_ENCODE nach Neustart in die Session | - -### Zustandsübergänge - -
- -```mermaid -flowchart LR - START(( )) --> IDLE - IDLE -->|analyzeDisc()| META[METADATA\nSELECTION] - META -->|selectMetadata()| RTS[READY_TO\nSTART] - RTS -->|Auto-Start/Queue| RIP[RIPPING] - RTS -->|Auto-Start mit RAW| MIC[MEDIAINFO\nCHECK] - RIP -->|MKV erstellt| MIC[MEDIAINFO\nCHECK] - MIC -->|Playlist offen| WUD[WAITING_FOR\nUSER_DECISION] - WUD -->|selectMetadata(selectedPlaylist)| MIC - MIC -->|Tracks analysiert| RTE[READY_TO\nENCODE] - RTE -->|confirmEncodeReview() + startPreparedJob()| ENC[ENCODING] - ENC -->|Pre-Encode → HandBrake → Post-Encode fertig| FIN([FINISHED]) - ENC -->|Abbruch| CAN([CANCELLED]) - ENC -->|Fehler| ERR([ERROR]) - RIP -->|Fehler| ERR - RIP -->|Abbruch| CAN - ERR -->|retry() / cancel()| IDLE - CAN -->|retry() / analyzeDisc()| IDLE - FIN -->|cancel / neue Disc| IDLE - - style FIN fill:#e8f5e9,stroke:#66bb6a,color:#2e7d32 - style CAN fill:#fff3e0,stroke:#fb8c00,color:#e65100 - style ERR fill:#ffebee,stroke:#ef5350,color:#c62828 - style ENC fill:#f3e5f5,stroke:#ab47bc,color:#6a1b9a - style RIP fill:#e3f2fd,stroke:#42a5f5,color:#1565c0 - style MIC fill:#e3f2fd,stroke:#42a5f5,color:#1565c0 -``` - -
+- `analyzeDisc()` +- `selectMetadata()` +- `startPreparedJob()` +- `confirmEncodeReview()` +- `cancel()` +- `retry()` +- `reencodeFromRaw()` +- `restartReviewFromRaw()` +- `restartEncodeWithLastSettings()` +- `resumeReadyToEncodeJob()` +- `enqueueNonJobEntry()`, `reorderQueue()`, `removeQueueEntry()` --- -## diskDetectionService.js +## `diskDetectionService.js` -Überwacht das Disc-Laufwerk auf Disc-Einleger- und Auswurf-Ereignisse. +Pollt Laufwerk(e) und emittiert: -### Modi +- `discInserted` +- `discRemoved` +- `error` -| Modus | Beschreibung | -|------|-------------| -| `auto` | Erkennt verfügbare Laufwerke automatisch | -| `explicit` | Überwacht ein bestimmtes Gerät (z.B. `/dev/sr0`) | +Zusatz: -### Polling - -Der Service pollt das Laufwerk im konfigurierten Intervall (`disc_poll_interval_ms`, Standard: 4000ms) und emittiert Events: - -```js -// Ereignisse -emit('discInserted', { path: '/dev/sr0', mediaProfile: 'bluray', ... }) -emit('discRemoved', { path: '/dev/sr0' }) -``` - -### Media-Profil-Erkennung - -Das erkannte Gerät enthält ein `mediaProfile`-Feld (`"bluray"`, `"dvd"`, `"other"` oder `null`). Die Erkennung nutzt eine Heuristik aus drei Quellen (absteigend nach Priorität): - -1. Explizit gesetztes `media_profile` aus den Settings -2. Disc-Label und Laufwerks-Modell (Regex gegen bekannte Begriffe) -3. Dateisystemtyp: `udf` → bevorzugt DVD, kombiniert mit Modell; `iso9660/cdfs` → DVD oder CD +- Modus `auto` oder `explicit` +- heuristische `mediaProfile`-Erkennung (`bluray`/`dvd`/`other`) +- `rescanAndEmit()` für manuellen Trigger --- -## processRunner.js +## `settingsService.js` -Verwaltet externe CLI-Prozesse. +Settings-Layer mit Validation/Serialisierung. -### Features +Features: -- **Streaming**: stdout/stderr werden zeilenweise gelesen -- **Progress-Callbacks**: Ermöglicht Echtzeit-Fortschrittsanzeige -- **Graceful Shutdown**: SIGINT → Warte-Timeout → SIGKILL -- **Prozess-Registry**: Verfolgt aktive Prozesse für sauberes Beenden - -### Nutzung - -```js -const result = await runProcess( - 'HandBrakeCLI', - ['--input', rawFile, '--output', outputFile, '--preset', preset], - { - onStderr: (line) => parseHandBrakeProgress(line), - onStdout: (line) => logger.debug(line) - } -); -``` +- `getCategorizedSettings()` für UI-Form +- `setSettingValue()` / `setSettingsBulk()` +- profilspezifische Auflösung (`resolveEffectiveToolSettings`) +- CLI-Config-Building für MakeMKV/HandBrake/MediaInfo +- HandBrake-Preset-Liste via `HandBrakeCLI -z` +- MakeMKV-Registration-Command aus `makemkv_registration_key` --- -## websocketService.js +## `historyService.js` -WebSocket-Server für Echtzeit-Client-Kommunikation. +Historie + Dateioperationen. -### Betrieb +Features: -- Läuft auf Pfad `/ws` des Express-Servers -- Hält eine Registry aller verbundenen Clients -- Ermöglicht Broadcast an alle Clients oder gezieltes Senden - -### API - -```js -broadcast('PIPELINE_STATE_CHANGED', { state, activeJobId }); -broadcast('PIPELINE_PROGRESS', { state, progress, eta, statusText }); -broadcast('PIPELINE_QUEUE_CHANGED', queueSnapshot); -``` +- Job-Liste/Detail inkl. Log-Tail +- Orphan-RAW-Erkennung und Import +- OMDb-Nachzuweisung +- Dateilöschung (`raw|movie|both`) +- Job-Löschung (`none|raw|movie|both`) --- -## omdbService.js +## `cronService.js` -Integration mit der [OMDb API](https://www.omdbapi.com/). +Integriertes Cron-System ohne externe Parser-Library. -### Methoden +Features: -| Methode | Beschreibung | -|---------|-------------| -| `searchByTitle(title, type)` | Suche nach Titel (movie/series) | -| `fetchById(imdbId)` | Vollständige Metadaten per IMDb-ID | - -### Zurückgegebene Daten - -```json -{ - "imdbId": "tt1375666", - "title": "Inception", - "year": "2010", - "type": "movie", - "poster": "https://...", - "plot": "...", - "director": "Christopher Nolan" -} -``` +- 5-Feld-Cron-Parser + `nextRun`-Berechnung +- Quellen: `script` oder `chain` +- Laufzeitlogs (`cron_run_logs`) +- manuelles Triggern +- WebSocket-Events: `CRON_JOBS_UPDATED`, `CRON_JOB_UPDATED` --- -## settingsService.js +## Weitere Services -Verwaltet alle Anwendungseinstellungen. - -### Features - -- **Schema-getriebene Validierung**: Jede Einstellung hat Typ, Grenzen und Pflichtfeld-Flag -- **Kategorisierung**: Einstellungen sind in Kategorien gruppiert (Pfade, Tools, Metadaten, …) -- **Persistenz**: Werte in SQLite, Schema ebenfalls in SQLite -- **Profil-Auflösung**: `resolveEffectiveToolSettings(settingsMap, mediaProfile)` wählt automatisch die profil-spezifischen Werte (`_bluray`/`_dvd`) und fällt auf den globalen Wert zurück - -### Profil-Auflösung - -```js -// Löst alle profil-spezifischen Keys auf und gibt einen effektiven Einstellungs-Map zurück -const effective = await settingsService.getEffectiveSettingsMap('bluray'); -// effective.handbrake_preset → Wert aus handbrake_preset_bluray (falls gesetzt) -// effective.raw_dir → Wert aus raw_dir_bluray (kein Fallback bei Pfaden) -``` - -### Einstellungs-Kategorien - -| Kategorie | Ausgewählte Schlüssel | -|-----------|----------------------| -| `Pfade` | `raw_dir[_bluray/_dvd/_other]`, `movie_dir[_bluray/_dvd/_other]`, `log_dir` | -| `Laufwerk` | `drive_mode`, `drive_device`, `disc_poll_interval_ms`, `makemkv_source_index` | -| `Monitoring` | `hardware_monitoring_enabled`, `hardware_monitoring_interval_ms` | -| `Tools` | `makemkv_command`, `handbrake_command`, `mediainfo_command`, `pipeline_max_parallel_jobs` | -| `Tools – Blu-ray` | `handbrake_preset_bluray`, `makemkv_rip_mode_bluray`, … | -| `Tools – DVD` | `handbrake_preset_dvd`, `makemkv_rip_mode_dvd`, … | -| `Metadaten` | `omdb_api_key`, `omdb_default_type` | -| `Benachrichtigungen` | `pushover_enabled`, `pushover_token`, `pushover_notify_*` | +- `scriptService.js` (CRUD + Test + Wrapper-Ausführung) +- `scriptChainService.js` (CRUD + Step-Execution) +- `userPresetService.js` (HandBrake User-Presets) +- `hardwareMonitorService.js` (CPU/RAM/GPU/Storage) +- `websocketService.js` (Client-Registry + Broadcast) +- `notificationService.js` (PushOver) +- `logger.js` (rotierende Datei-Logs) --- -## userPresetService.js +## Bootstrapping (`src/index.js`) -Verwaltet benannte HandBrake-Preset-Sammlungen pro Medientyp. +Beim Start: -### Methoden - -| Methode | Beschreibung | -|---------|-------------| -| `listPresets(mediaType?)` | Alle Presets; optional nach Medientyp filtern (`bluray`/`dvd`/`other`/`all`) | -| `getPresetById(id)` | Einzelnes Preset | -| `createPreset(payload)` | Neues Preset anlegen | -| `updatePreset(id, payload)` | Preset aktualisieren | -| `deletePreset(id)` | Preset löschen | - -!!! info "mediaType = 'all'" - Presets mit `mediaType = 'all'` erscheinen bei Filterung nach jedem Medientyp. - ---- - -## historyService.js - -Datenbankoperationen für Job-Historie. - -### Hauptoperationen - -| Operation | Beschreibung | -|-----------|-------------| -| `listJobs(filters)` | Jobs nach Status/Titel filtern | -| `getJob(id)` | Job-Details mit Logs abrufen | -| `findOrphanRawFolders()` | Nicht-getrackte Raw-Ordner finden | -| `importOrphanRaw(path)` | Orphan-Ordner als Job importieren | -| `assignOmdb(id, omdbData)` | OMDb-Metadaten nachträglich zuweisen | -| `deleteJob(id, deleteFiles)` | Job und optional Dateien löschen | - ---- - -## cronService.js - -Eingebautes Cron-System ohne externe Abhängigkeiten. - -### Features - -- **Eigener Expression-Parser**: Unterstützt alle Standard-5-Felder-Cron-Ausdrücke (`* /n`, Bereiche, Listen) -- **Skripte und Ketten**: Cron-Jobs können ein Skript (`sourceType: "script"`) oder eine Kette (`sourceType: "chain"`) ausführen -- **Log-Rotation**: Max. 50 Logs pro Job, Ausgabe auf 100.000 Zeichen begrenzt -- **PushOver-Integration**: Optionale Benachrichtigung nach jeder Ausführung -- **Manuelle Auslösung**: `triggerJobManually(id)` – läuft unabhängig vom Zeitplan - -### Methoden - -| Methode | Beschreibung | -|---------|-------------| -| `listJobs()` | Alle Cron-Jobs | -| `createJob(payload)` | Neuen Job anlegen | -| `updateJob(id, payload)` | Job aktualisieren | -| `deleteJob(id)` | Job löschen | -| `getJobLogs(id, limit)` | Ausführungs-Logs | -| `triggerJobManually(id)` | Sofortige Ausführung | -| `validateExpression(expr)` | Ausdruck validieren | -| `getNextRunTime(expr)` | Nächsten Ausführungszeitpunkt berechnen | - ---- - -## notificationService.js - -PushOver-Push-Benachrichtigungen. - -```js -await notify({ - title: 'Ripster: Job abgeschlossen', - message: 'Inception (2010) wurde erfolgreich encodiert' -}); -``` - ---- - -## logger.js - -Strukturiertes Logging mit täglicher Log-Rotation. - -### Log-Level - -| Level | Verwendung | -|-------|-----------| -| `debug` | Detaillierte Entwicklungs-Informationen | -| `info` | Normale Betriebsereignisse | -| `warn` | Warnungen, die Aufmerksamkeit benötigen | -| `error` | Fehler, die den Betrieb beeinträchtigen | - -### Log-Dateien - -``` -logs/ -├── ripster-2024-01-15.log ← Tages-Log -└── jobs/ - └── job-42-handbrake.log ← Prozess-spezifische Logs -``` +1. DB init/migrate +2. Pipeline-Init +3. Cron-Init +4. Express-Routes + Error-Handler +5. WebSocket-Server auf `/ws` +6. Hardware-Monitoring-Init +7. Disk-Detection-Start diff --git a/docs/architecture/database.md b/docs/architecture/database.md index ac29785..ccf406c 100644 --- a/docs/architecture/database.md +++ b/docs/architecture/database.md @@ -1,273 +1,112 @@ # Datenbank -Ripster verwendet **SQLite3** als Datenbank. Die Datenbankdatei liegt unter `backend/data/ripster.db`. +Ripster verwendet SQLite (`backend/data/ripster.db`). --- -## Schema-Übersicht +## Tabellen -```sql -settings_schema -- Einstellungs-Definitionen -settings_values -- Benutzer-Werte -jobs -- Rip-Job-Datensätze -pipeline_state -- Aktueller Pipeline-Zustand (Singleton) -scripts -- Shell-Skripte für Pre-/Post-Encode-Ausführung -script_chains -- Geordnete Ketten aus mehreren Skripten -script_chain_steps -- Einzelschritte einer Skript-Kette -user_presets -- Benannte HandBrake-Preset-Sammlungen pro Medientyp -cron_jobs -- Zeitgesteuerte Aufgaben (eigener Cron-Parser) -cron_run_logs -- Ausführungs-Protokolle für Cron-Jobs +```text +settings_schema +settings_values +jobs +pipeline_state +scripts +script_chains +script_chain_steps +user_presets +cron_jobs +cron_run_logs ``` --- -## Tabelle: jobs +## `jobs` -Die wichtigste Tabelle – speichert alle Ripping-Jobs. +Speichert Pipeline-Lifecycle und Artefakte pro Job. -```sql -CREATE TABLE jobs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL, - status TEXT NOT NULL, -- Aktueller Status - title TEXT, -- Filmtitel (von OMDb) - imdb_id TEXT, -- IMDb-ID - omdb_year TEXT, -- Erscheinungsjahr - omdb_type TEXT, -- movie/series - omdb_poster TEXT, -- Poster-URL - raw_path TEXT, -- Pfad zur Raw-MKV - output_path TEXT, -- Pfad zur Ausgabedatei - playlist TEXT, -- Gewählte Blu-ray Playlist - rip_successful INTEGER NOT NULL DEFAULT 0, -- 1 wenn Rip abgeschlossen - makemkv_output TEXT, -- MakeMKV-Ausgabe (JSON) - mediainfo_output TEXT, -- MediaInfo-Ausgabe (JSON) - encode_plan TEXT, -- Encode-Plan (JSON) - handbrake_log TEXT, -- HandBrake Log-Pfad - error_message TEXT, -- Fehlermeldung bei ERROR - error_details TEXT -- Detaillierte Fehler-Infos -); -``` +Zentrale Felder: -!!! info "rip_successful" - Das Feld `rip_successful` wird auf `1` gesetzt, sobald MakeMKV den Rip-Schritt erfolgreich abgeschlossen hat – unabhängig davon, ob danach ein Encode-Fehler auftritt. Damit lässt sich in der History unterscheiden, ob eine Raw-Datei vorhanden ist. - -### Job-Status-Werte - -| Status | Beschreibung | -|--------|-------------| -| `ANALYZING` | MakeMKV analysiert die Disc | -| `METADATA_SELECTION` | Wartet auf Benutzer-Metadaten-Auswahl | -| `READY_TO_START` | Bereit zum Starten | -| `RIPPING` | MakeMKV rippt die Disc | -| `MEDIAINFO_CHECK` | MediaInfo analysiert die Raw-Datei | -| `READY_TO_ENCODE` | Wartet auf Encode-Bestätigung | -| `ENCODING` | HandBrake encodiert | -| `FINISHED` | Erfolgreich abgeschlossen | -| `ERROR` | Fehler aufgetreten | +- Metadaten: `title`, `year`, `imdb_id`, `poster_url`, `omdb_json`, `selected_from_omdb` +- Laufzeit: `start_time`, `end_time`, `status`, `last_state` +- Pfade: `raw_path`, `output_path`, `encode_input_path` +- Tool-Ausgaben: `makemkv_info_json`, `handbrake_info_json`, `mediainfo_info_json`, `encode_plan_json` +- Kontrolle: `encode_review_confirmed`, `rip_successful`, `error_message` +- Audit: `created_at`, `updated_at` --- -## Tabelle: pipeline_state +## `pipeline_state` -Singleton-Tabelle für den aktuellen Pipeline-Zustand (immer genau 1 Zeile). +Singleton-Tabelle (`id = 1`) für aktiven Snapshot: -```sql -CREATE TABLE pipeline_state ( - id INTEGER PRIMARY KEY CHECK(id = 1), - state TEXT NOT NULL DEFAULT 'IDLE', - job_id INTEGER, -- Aktiver Job (NULL wenn IDLE) - progress REAL, -- Fortschritt 0-100 - eta TEXT, -- Geschätzte Restzeit - updated_at TEXT NOT NULL -); -``` +- `state` +- `active_job_id` +- `progress` +- `eta` +- `status_text` +- `context_json` +- `updated_at` --- -## Tabelle: settings_schema +## `settings_schema` + `settings_values` -Definiert alle verfügbaren Einstellungen mit Metadaten. - -```sql -CREATE TABLE settings_schema ( - key TEXT PRIMARY KEY, - category TEXT NOT NULL, -- paths, tools, encoding, ... - type TEXT NOT NULL, -- string, number, boolean, select - label TEXT NOT NULL, -- Anzeigename - description TEXT, -- Hilfetext - default_val TEXT, -- Standardwert - required INTEGER, -- 1 = Pflichtfeld - min_val REAL, -- Minimalwert (für number) - max_val REAL, -- Maximalwert (für number) - options TEXT -- JSON-Array für select-Typ -); -``` +- `settings_schema`: Definition (Typ, Default, Validation, Reihenfolge) +- `settings_values`: aktueller Wert pro Key --- -## Tabelle: settings_values +## `scripts`, `script_chains`, `script_chain_steps` -Speichert benutzer-konfigurierte Werte. - -```sql -CREATE TABLE settings_values ( - key TEXT PRIMARY KEY REFERENCES settings_schema(key), - value TEXT NOT NULL, - updated_at TEXT NOT NULL -); -``` +- `scripts`: Shell-Skripte (`name`, `script_body`, `order_index`) +- `script_chains`: Ketten (`name`, `order_index`) +- `script_chain_steps`: Schritte je Kette + - `step_type`: `script` oder `wait` + - `script_id` oder `wait_seconds` --- -## Tabelle: scripts +## `user_presets` -Verwaltet Shell-Skripte, die vor oder nach dem Encode-Schritt ausgeführt werden können. +Benannte HandBrake-Preset-Sets: -```sql -CREATE TABLE scripts ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL UNIQUE, - script_body TEXT NOT NULL, - order_index INTEGER NOT NULL DEFAULT 0, -- Sortierposition in der UI - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP -); -``` - -## Tabelle: script_chains - -Geordnete Ketten, die mehrere Skripte sequenziell zusammenfassen. - -```sql -CREATE TABLE script_chains ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL UNIQUE, - order_index INTEGER NOT NULL DEFAULT 0, -- Sortierposition in der UI - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP -); - -CREATE TABLE script_chain_steps ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - chain_id INTEGER NOT NULL REFERENCES script_chains(id) ON DELETE CASCADE, - script_id INTEGER NOT NULL REFERENCES scripts(id) ON DELETE CASCADE, - step_order INTEGER NOT NULL DEFAULT 0, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP -); -``` - -!!! info "Sortierung" - `order_index` in `scripts` und `script_chains` wird über die API (`reorderScripts` / `reorderChains`) per Drag & Drop in der UI gesetzt und bleibt persistent gespeichert. +- `name` +- `media_type` (`bluray|dvd|other|all`) +- `handbrake_preset` +- `extra_args` +- `description` --- -## Tabelle: user_presets +## `cron_jobs` + `cron_run_logs` -Speichert benannte HandBrake-Preset-Sammlungen pro Medientyp. - -```sql -CREATE TABLE user_presets ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - media_type TEXT NOT NULL DEFAULT 'all', -- 'bluray', 'dvd', 'other', 'all' - handbrake_preset TEXT, - extra_args TEXT, - description TEXT, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP -); -``` - -!!! info "Medientyp-Filter" - `GET /api/settings/user-presets?mediaType=bluray` gibt Presets mit `media_type = 'bluray'` **und** `media_type = 'all'` zurück. +- `cron_jobs`: Zeitplan + Status +- `cron_run_logs`: einzelne Läufe + - `status`: `running|success|error` + - `output` + - `error_message` --- -## Tabellen: cron_jobs & cron_run_logs +## Migration/Recovery -Speichern den Zeitplan und die Ausführungs-Historie des eingebauten Cron-Systems. +Beim Start werden Schema und Settings-Metadaten automatisch abgeglichen. -```sql -CREATE TABLE cron_jobs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - cron_expression TEXT NOT NULL, -- 5-Felder-Ausdruck (min h d m wd) - source_type TEXT NOT NULL, -- "script" oder "chain" - source_id INTEGER NOT NULL, -- ID des Skripts/der Kette - enabled INTEGER NOT NULL DEFAULT 1, - pushover_enabled INTEGER NOT NULL DEFAULT 1, - last_run_at TEXT, - last_run_status TEXT, -- "success", "error", "running" - next_run_at TEXT, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP -); +Bei korruptem SQLite-File: -CREATE TABLE cron_run_logs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - cron_job_id INTEGER NOT NULL REFERENCES cron_jobs(id) ON DELETE CASCADE, - started_at TEXT NOT NULL, - finished_at TEXT, - status TEXT NOT NULL, -- "success", "error", "running" - exit_code INTEGER, - stdout TEXT, - stderr TEXT, - triggered_by TEXT NOT NULL DEFAULT 'cron', -- "cron" oder "manual" - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP -); -``` - -!!! info "Log-Rotation" - Pro Cron-Job werden maximal **50 Log-Einträge** gespeichert; ältere Einträge werden automatisch gelöscht. Stdout/Stderr werden auf **100.000 Zeichen** begrenzt. +1. Datei wird nach `backend/data/corrupt-backups/` verschoben +2. neue DB wird initialisiert +3. Schema wird neu aufgebaut --- -## Schema-Migrationen - -`database.js` implementiert **automatische Migrationen**: - -1. Beim Start wird das aktuelle Schema geprüft -2. Fehlende Tabellen werden erstellt -3. Fehlende Spalten werden hinzugefügt -4. Neue Default-Einstellungen werden eingefügt - -### Korruptions-Recovery - -Falls die Datenbankdatei korrupt ist: - -``` -1. Korrupte Datei wird erkannt (Verbindungsfehler / Integritätsprüfung) -2. Datei wird in backend/data/corrupt-backups/ verschoben -3. Neue, leere Datenbank wird erstellt -4. Schema wird neu initialisiert -5. Log-Eintrag mit Warnung -``` - ---- - -## Datenbankpfad konfigurieren - -Standard: `./data/ripster.db` (relativ zum Backend-Verzeichnis) - -Über Umgebungsvariable anpassen: - -```env -DB_PATH=/var/lib/ripster/ripster.db -``` - ---- - -## Direkte Datenbankinspektion +## Direkte Inspektion ```bash -# SQLite3-CLI sqlite3 backend/data/ripster.db -# Alle Jobs anzeigen .mode table SELECT id, status, title, created_at FROM jobs ORDER BY created_at DESC; - -# Einstellungen anzeigen -SELECT key, value FROM settings_values; +SELECT key, value FROM settings_values ORDER BY key; ``` diff --git a/docs/architecture/frontend.md b/docs/architecture/frontend.md index de20935..9ab4d47 100644 --- a/docs/architecture/frontend.md +++ b/docs/architecture/frontend.md @@ -1,192 +1,82 @@ # Frontend-Komponenten -Das Frontend ist mit **React 18** und **PrimeReact** gebaut und kommuniziert über REST-API und WebSocket mit dem Backend. +Frontend: React + PrimeReact + Vite. --- -## Seiten (Pages) +## Hauptseiten -### DashboardPage.jsx +### `DashboardPage.jsx` -Die Hauptseite von Ripster – zeigt den aktuellen Pipeline-Status und ermöglicht alle Workflow-Aktionen. +Pipeline-Steuerung: -**Funktionen:** -- Anzeige des aktuellen Pipeline-Zustands (IDLE, DISC_DETECTED, METADATA_SELECTION, RIPPING, MEDIAINFO_CHECK, READY_TO_ENCODE, ENCODING, ...) -- Live-Fortschrittsbalken mit ETA -- Trigger für Metadaten-Dialog -- Playlist-Entscheidungs-UI (bei Blu-ray Obfuskierung) -- Encode-Review mit Track-Auswahl -- Job-Steuerung (Start, Abbruch, Retry, Queue-Interaktion) +- Status/Progress/ETA +- Metadaten-Dialog +- Playlist-Entscheidung +- Review-Panel +- Queue-Interaktion (reorder/add/remove) +- Job-Aktionen (Start/Cancel/Retry/Re-Encode) +- Hardware-Monitoring-Anzeige -**Zugehörige Komponenten:** -- `PipelineStatusCard` – Status-Widget -- `MetadataSelectionDialog` – OMDb-Suche und Playlist-Auswahl -- `MediaInfoReviewPanel` – Track-Auswahl vor dem Encoding -- Queue- und Job-Karten-UI direkt in `DashboardPage` +### `SettingsPage.jsx` -### SettingsPage.jsx +Konfiguration: -Konfigurationsoberfläche für alle Ripster-Einstellungen. +- dynamisches Settings-Formular (`DynamicSettingsForm`) +- Skripte/Ketten inkl. Reorder/Test +- User-Presets +- Cron-Jobs (`CronJobsTab`) -**Funktionen:** -- Dynamisch generiertes Formular aus dem Settings-Schema -- Echtzeit-Validierungsfeedback -- PushOver-Verbindungstest -- Automatische Aktualisierung des Encode-Reviews bei relevanten Änderungen +### `HistoryPage.jsx` -### DatabasePage.jsx (`/history`) +Historie: -Job-Historie und Datenbankansicht mit vollständigem Audit-Trail. - -**Funktionen:** -- Sortier- und filterbares Job-Verzeichnis -- Statusfilter (FINISHED, ERROR, WAITING_FOR_USER_DECISION, ...) -- Job-Detail-Dialog mit vollständigen Logs -- Re-Encode, Löschen und Metadaten-Zuweisung -- Import von Orphan-Raw-Ordnern +- Job-Liste/Filter +- Job-Details + Logs +- OMDb-Nachzuweisung +- Re-Encode/Restart-Workflows --- -## Komponenten (Components) +## Wichtige Komponenten -### MetadataSelectionDialog.jsx - -Dialog für die Metadaten-Auswahl nach der Disc-Analyse. - -``` -┌─────────────────────────────────────┐ -│ Metadaten auswählen │ -├─────────────────────────────────────┤ -│ Suche: [Inception ] 🔍 │ -├─────────────────────────────────────┤ -│ Ergebnisse: │ -│ ▶ Inception (2010) – Movie │ -│ Inception: ... (2011) – Series │ -├─────────────────────────────────────┤ -│ Playlist (nur Blu-ray): │ -│ ▶ 00800.mpls (2:30:15) ✓ Empfohlen │ -│ 00801.mpls (0:01:23) │ -├─────────────────────────────────────┤ -│ [Bestätigen] │ -└─────────────────────────────────────┘ -``` - -### MediaInfoReviewPanel.jsx - -Track-Auswahl-Panel vor dem Encoding. - -``` -┌─────────────────────────────────────┐ -│ Encode-Review │ -├─────────────────────────────────────┤ -│ Audio-Tracks: │ -│ ☑ Track 1: Deutsch (AC-3, 5.1) │ -│ ☑ Track 2: English (TrueHD, 7.1) │ -│ ☐ Track 3: Français (AC-3, 2.0) │ -├─────────────────────────────────────┤ -│ Untertitel: │ -│ ☑ Track 1: Deutsch │ -│ ☐ Track 2: English │ -├─────────────────────────────────────┤ -│ [Encoding starten] │ -└─────────────────────────────────────┘ -``` - -### DynamicSettingsForm.jsx - -Wiederverwendbares Formular, das aus dem Settings-Schema generiert wird. - -**Unterstützte Feldtypen:** - -| Typ | UI-Element | -|----|-----------| -| `string` | Text-Input | -| `number` | Zahlen-Input mit Min/Max | -| `boolean` | Toggle/Checkbox | -| `select` | Dropdown | -| `password` | Passwort-Input | - -### PipelineStatusCard.jsx - -Status-Anzeige-Widget für die Dashboard-Seite. - -### JobDetailDialog.jsx - -Vollständiger Job-Detail-Dialog mit Logs-Viewer. +- `PipelineStatusCard.jsx` +- `MetadataSelectionDialog.jsx` +- `MediaInfoReviewPanel.jsx` +- `JobDetailDialog.jsx` +- `CronJobsTab.jsx` --- -## Hooks +## API-Client (`api/client.js`) -### useWebSocket.js - -Zentraler Custom-Hook für die WebSocket-Verbindung. - -```js -useWebSocket({ - onMessage: (msg) => { - if (msg.type === 'PIPELINE_STATE_CHANGED') { - setPipelineState(msg.payload); - } - } -}); -``` - -**Features:** -- Automatische Verbindung zu `/ws` -- Reconnect mit festem Intervall (`1500ms`) -- Message-Parsing (JSON) +- zentraler `request()` mit JSON-Handling +- Fehlerobjekt aus API wird auf `Error(message)` gemappt +- `VITE_API_BASE` default `/api` --- -## API-Client (client.js) +## WebSocket (`hooks/useWebSocket.js`) -Zentraler HTTP-Client für alle Backend-Anfragen. +- URL: `VITE_WS_URL` oder automatisch `ws(s):///ws` +- Auto-Reconnect mit 1500ms Intervall -```js -// Beispiel-Aufrufe -const state = await api.getPipelineState(); -const results = await api.searchOmdb('Inception'); -await api.selectMetadata({ jobId, title, year, imdbId, selectedPlaylist }); -await api.confirmEncodeReview(jobId, { - selectedEncodeTitleId: 1, - selectedTrackSelection: { 1: { audioTrackIds: [1], subtitleTrackIds: [3] } } -}); -``` +In `App.jsx` werden u. a. verarbeitet: -**Features:** -- Zentralisierte Fehlerbehandlung -- Automatische JSON-Serialisierung -- Basis-URL aus Umgebungsvariable (`VITE_API_BASE`) +- `PIPELINE_STATE_CHANGED` +- `PIPELINE_PROGRESS` +- `PIPELINE_QUEUE_CHANGED` +- `DISC_DETECTED` / `DISC_REMOVED` +- `HARDWARE_MONITOR_UPDATE` --- -## Build & Entwicklung - -### Entwicklungsserver +## Build/Run ```bash -cd frontend -npm run dev -# → http://localhost:5173 -``` - -### Vite-Proxy-Konfiguration - -In der Entwicklungsumgebung proxied Vite API-Anfragen zum Backend: - -```js -// vite.config.js -proxy: { - '/api': 'http://localhost:3001', - '/ws': { target: 'ws://localhost:3001', ws: true } -} -``` - -### Production-Build - -```bash -cd frontend -npm run build -# → frontend/dist/ +# dev +npm run dev --prefix frontend + +# prod build +npm run build --prefix frontend ``` diff --git a/docs/architecture/index.md b/docs/architecture/index.md index 7b06375..436b338 100644 --- a/docs/architecture/index.md +++ b/docs/architecture/index.md @@ -1,6 +1,6 @@ # Architektur -Ripster ist als klassische **Client-Server-Anwendung** mit Echtzeit-Kommunikation über WebSockets aufgebaut. +Ripster ist eine Client-Server-Anwendung mit REST + WebSocket. --- @@ -9,104 +9,63 @@ Ripster ist als klassische **Client-Server-Anwendung** mit Echtzeit-Kommunikatio ```mermaid graph TB subgraph Browser["Browser (React)"] - Dashboard["Dashboard"] - Settings["Einstellungen"] - History["History"] + Dashboard[Dashboard] + Settings[Einstellungen] + History[Historie] end subgraph Backend["Node.js Backend"] - API["REST API\n(Express)"] - WS["WebSocket\nServer"] - Pipeline["Pipeline\nService"] - DB["SQLite\nDatenbank"] + API[REST API\nExpress] + WS[WebSocket\n/ws] + Pipeline[pipelineService] + Cron[cronService] + DB[(SQLite)] end - subgraph ExternalTools["Externe Tools"] - MakeMKV["makemkvcon"] - HandBrake["HandBrakeCLI"] - MediaInfo["mediainfo"] + subgraph Tools["Externe Tools"] + MakeMKV[makemkvcon] + HandBrake[HandBrakeCLI] + MediaInfo[mediainfo] end - subgraph ExternalAPIs["Externe APIs"] - OMDb["OMDb API"] - PushOver["PushOver"] - end - - Browser <-->|HTTP REST| API + Browser <-->|HTTP| API Browser <-->|WebSocket| WS Pipeline --> MakeMKV Pipeline --> HandBrake Pipeline --> MediaInfo - Pipeline <-->|Metadaten| OMDb - Pipeline -->|Benachrichtigungen| PushOver API --> DB Pipeline --> DB + Cron --> DB ``` --- -## Schichten-Architektur +## Schichten ### Backend -``` -index.js (Express Server) -├── Routes (API-Endpunkte) -│ ├── pipelineRoutes.js -│ ├── settingsRoutes.js -│ └── historyRoutes.js -├── Services (Business Logic) -│ ├── pipelineService.js ← Kern-Orchestrierung -│ ├── diskDetectionService.js -│ ├── processRunner.js -│ ├── websocketService.js -│ ├── omdbService.js -│ ├── settingsService.js -│ ├── notificationService.js -│ ├── historyService.js -│ └── logger.js -├── Database -│ ├── database.js -│ └── defaultSettings.js -└── Utils - ├── encodePlan.js - ├── playlistAnalysis.js - ├── progressParsers.js - └── files.js -``` +- `src/index.js` (Bootstrapping, Routes, WS, Services) +- `src/routes/*` (Pipeline, Settings, History, Crons) +- `src/services/*` (Business-Logik) +- `src/db/database.js` (Init/Migration) +- `src/utils/*` (Parser, Dateifunktionen, Validierung) ### Frontend -``` -App.jsx (React Router) -├── Pages -│ ├── DashboardPage.jsx ← Haupt-Interface -│ ├── SettingsPage.jsx -│ └── DatabasePage.jsx ← Historie/DB-Ansicht -├── Components -│ ├── PipelineStatusCard.jsx -│ ├── MetadataSelectionDialog.jsx -│ ├── MediaInfoReviewPanel.jsx -│ ├── DynamicSettingsForm.jsx -│ └── JobDetailDialog.jsx -├── Hooks -│ └── useWebSocket.js -└── API - └── client.js -``` +- `App.jsx` + `pages/*` (Dashboard, Settings, History) +- `components/*` (Status-/Review-/Dialog-Komponenten) +- `api/client.js` (REST-Client) +- `hooks/useWebSocket.js` (WS-Reconnect) --- -## Weiterführende Dokumentation +## Weiterführend
-- [:octicons-arrow-right-24: Übersicht](overview.md) - -- [:octicons-arrow-right-24: Backend-Services](backend.md) - -- [:octicons-arrow-right-24: Frontend-Komponenten](frontend.md) - -- [:octicons-arrow-right-24: Datenbank](database.md) +- [:octicons-arrow-right-24: Übersicht](overview.md) +- [:octicons-arrow-right-24: Backend-Services](backend.md) +- [:octicons-arrow-right-24: Frontend-Komponenten](frontend.md) +- [:octicons-arrow-right-24: Datenbank](database.md)
diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index 3c000a7..fbae11a 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -2,144 +2,93 @@ --- -## Kern-Designprinzipien +## Kernprinzipien -### Event-Driven Pipeline +### Event-getriebene Pipeline -Der gesamte Ripping-Workflow ist als **State Machine** implementiert. Der `pipelineService` verwaltet den aktuellen Zustand und emittiert Ereignisse bei jedem Zustandswechsel. Der WebSocket-Service überträgt diese Ereignisse sofort an alle verbundenen Clients. +`pipelineService` hält einen Snapshot der State-Machine und broadcastet Änderungen sofort via WebSocket. -``` -Zustandswechsel → Event → WebSocket → Frontend-Update +```text +State-Änderung -> PIPELINE_STATE_CHANGED/PIPELINE_PROGRESS -> Frontend-Update ``` -### Service-Layer-Muster +### Service-Layer -``` -HTTP-Route → Service → Datenbank +```text +Route -> Service -> DB/Tool-Execution ``` -Routes delegieren die gesamte Business-Logik an Services. Services sind voneinander unabhängig und können einzeln getestet werden. +Routes enthalten kaum Business-Logik. -### Schema-getriebene Einstellungen +### Schema-getriebene Settings -Die Settings-Konfiguration definiert **sowohl** die Validierungsregeln als auch die UI-Struktur in einer einzigen Quelle (`settings_schema`-Tabelle). Die `DynamicSettingsForm`-Komponente rendert das Formular dynamisch aus dem Schema. +Settings sind DB-schema-getrieben (`settings_schema` + `settings_values`), UI rendert dynamisch aus diesen Daten. --- ## Echtzeit-Kommunikation -### WebSocket-Protokoll +WebSocket läuft auf `/ws`. -Der WebSocket-Server läuft unter dem Pfad `/ws`. Nachrichten werden als JSON übertragen: +Wichtige Events: -```json -{ - "type": "PIPELINE_STATE_CHANGED", - "payload": { - "state": "ENCODING", - "activeJobId": 42, - "progress": 73.5, - "eta": "00:12:34" - } -} -``` - -**Nachrichtentypen:** - -| Typ | Beschreibung | -|----|-------------| -| `PIPELINE_STATE_CHANGED` | Pipeline-Zustand hat gewechselt | -| `PIPELINE_PROGRESS` | Fortschritt (% und ETA) | -| `PIPELINE_QUEUE_CHANGED` | Queue-Status geändert | -| `DISC_DETECTED` | Disc wurde erkannt | -| `DISC_REMOVED` | Disc wurde entfernt | -| `PIPELINE_ERROR` | Pipeline-Fehler aufgetreten | -| `DISK_DETECTION_ERROR` | Laufwerkserkennung-Fehler | - -### Reconnect-Logik - -Der Frontend-Hook `useWebSocket.js` implementiert automatisches Reconnect mit festem Intervall von 1500ms bei Verbindungsabbrüchen. +- `PIPELINE_STATE_CHANGED`, `PIPELINE_PROGRESS`, `PIPELINE_QUEUE_CHANGED` +- `DISC_DETECTED`, `DISC_REMOVED` +- `HARDWARE_MONITOR_UPDATE` +- `SETTINGS_UPDATED`, `SETTINGS_BULK_UPDATED` +- `SETTINGS_SCRIPTS_UPDATED`, `SETTINGS_SCRIPT_CHAINS_UPDATED`, `USER_PRESETS_UPDATED` +- `CRON_JOBS_UPDATED`, `CRON_JOB_UPDATED` +- `PIPELINE_ERROR`, `DISK_DETECTION_ERROR` --- -## Prozess-Management +## Prozessausführung -### processRunner.js +Externe Tools werden als Child-Processes gestartet (`processRunner`): -Externe Tools (MakeMKV, HandBrake, MediaInfo) werden als **Child Processes** gestartet: - -```js -spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'] }) -``` - -- **stdout/stderr** werden zeilenweise gelesen und in Echtzeit verarbeitet -- **Progress-Parsing** erfolgt über reguläre Ausdrücke in `progressParsers.js` -- **Graceful Shutdown**: SIGINT → Timeout → SIGKILL -- **Prozess-Tracking**: Aktive Prozesse werden registriert für sauberes Beenden +- Streaming von stdout/stderr +- Progress-Parsing (`progressParsers.js`) +- kontrollierter Abbruch (SIGINT/SIGKILL-Fallback) --- -## Datenpersistenz +## Persistenz -### SQLite-Datenbank +SQLite-Datei: `backend/data/ripster.db` -Ripster verwendet eine **einzige SQLite-Datei** für alle persistenten Daten: +Kern-Tabellen: -``` -backend/data/ripster.db -``` +- `jobs`, `pipeline_state` +- `settings_schema`, `settings_values` +- `scripts`, `script_chains`, `script_chain_steps` +- `user_presets` +- `cron_jobs`, `cron_run_logs` -**Tabellen:** - -| Tabelle | Inhalt | -|---------|--------| -| `jobs` | Alle Rip-Jobs mit Status, Logs, Metadaten | -| `pipeline_state` | Aktueller Pipeline-Zustand (Singleton) | -| `settings_schema` | Schema aller verfügbaren Einstellungen | -| `settings_values` | Benutzer-konfigurierte Werte | - -### Migrations-Strategie - -Beim Start prüft `database.js` automatisch, ob das Schema aktuell ist, und führt fehlende Migrationen aus. Korrupte Datenbankdateien werden in ein Quarantäne-Verzeichnis verschoben und eine neue Datenbank erstellt. +Beim Start werden Schema und Settings-Migrationen automatisch ausgeführt. --- ## Fehlerbehandlung -### Strukturierte Fehler +Zentrales Error-Handling liefert: -Alle Fehler werden mit Kontext-Metadaten protokolliert: - -```js -logger.error('Encoding fehlgeschlagen', { - jobId: job.id, - command: cmd, - exitCode: code, - stderr: lastLines -}); +```json +{ + "error": { + "message": "...", + "statusCode": 400, + "reqId": "...", + "details": [] + } +} ``` -### Job-Fehler-Recovery - -- Fehlgeschlagene Jobs bleiben in der Datenbank (Status `ERROR`) -- Vollständige Fehler-Logs werden im Job-Datensatz gespeichert -- **Retry-Funktion** ermöglicht Neustart von einem Fehler-Zustand -- **Re-Encode** erlaubt erneutes Encodieren ohne neu zu rippen +Fehlgeschlagene Jobs bleiben in der Historie (`ERROR` oder `CANCELLED`) und können erneut gestartet werden. --- -## Sicherheit +## CORS & Runtime-Konfig -### Eingabe-Validierung - -- Alle Benutzer-Eingaben werden in `validators.js` validiert -- CLI-Argumente werden sicher über `commandLine.js` konstruiert (kein Shell-Injection-Risiko) -- Pfade werden sanitisiert bevor sie an externe Prozesse übergeben werden - -### CORS-Konfiguration - -```env -CORS_ORIGIN=http://localhost:5173 -``` - -In Produktion sollte dieser Wert auf die tatsächliche Frontend-URL gesetzt werden. +- `CORS_ORIGIN` default: `*` +- `LOG_LEVEL` default: `info` +- DB-/Log-Pfade über `DB_PATH`/`LOG_DIR` konfigurierbar diff --git a/docs/configuration/environment.md b/docs/configuration/environment.md index fe9a0bf..61a5b92 100644 --- a/docs/configuration/environment.md +++ b/docs/configuration/environment.md @@ -1,96 +1,67 @@ # Umgebungsvariablen -Umgebungsvariablen überschreiben die Standardwerte und eignen sich für Server-Deployments. +Umgebungsvariablen steuern Backend/Vite außerhalb der DB-basierten UI-Settings. --- -## Backend-Umgebungsvariablen +## Backend (`backend/.env`) -Konfigurationsdatei: `backend/.env` +| Variable | Default (Code) | Beschreibung | +|---------|------------------|-------------| +| `PORT` | `3001` | Express-Port | +| `DB_PATH` | `backend/data/ripster.db` | SQLite-Datei (relativ zu `backend/`) | +| `LOG_DIR` | `backend/logs` | Fallback-Logverzeichnis (wenn `log_dir`-Setting nicht gesetzt/lesbar) | +| `CORS_ORIGIN` | `*` | CORS-Origin für API | +| `LOG_LEVEL` | `info` | `debug`, `info`, `warn`, `error` | -| Variable | Standard | Beschreibung | -|---------|---------|-------------| -| `PORT` | `3001` | Port des Express-Servers | -| `DB_PATH` | `./data/ripster.db` | Pfad zur SQLite-Datenbankdatei | -| `CORS_ORIGIN` | `http://localhost:5173` | Erlaubter CORS-Origin | -| `LOG_DIR` | `./logs` | Verzeichnis für Log-Dateien | -| `LOG_LEVEL` | `info` | Log-Level (`debug`, `info`, `warn`, `error`) | - -### Beispiel: backend/.env +Beispiel: ```env PORT=3001 DB_PATH=/var/lib/ripster/ripster.db -CORS_ORIGIN=http://192.168.1.100:5173 LOG_DIR=/var/log/ripster +CORS_ORIGIN=http://192.168.1.50:5173 LOG_LEVEL=info ``` +Hinweis: `backend/.env.example` enthält bewusst dev-freundliche Werte (z. B. lokaler `CORS_ORIGIN`). + --- -## Frontend-Umgebungsvariablen +## Frontend (`frontend/.env`) -Konfigurationsdatei: `frontend/.env` - -| Variable | Standard | Beschreibung | +| Variable | Default | Beschreibung | |---------|---------|-------------| -| `VITE_API_BASE` | `http://localhost:3001` | Backend-API-URL | -| `VITE_WS_URL` | `ws://localhost:3001` | WebSocket-URL | -| `VITE_PUBLIC_ORIGIN` | — | Öffentliche Origin-URL (für CORS) | -| `VITE_HMR_HOST` | — | Vite HMR-Host (für Remote-Entwicklung) | -| `VITE_HMR_PORT` | — | Vite HMR-Port | +| `VITE_API_BASE` | `/api` | API-Basis für Fetch-Client | +| `VITE_WS_URL` | automatisch aus `window.location` + `/ws` | Optional explizite WebSocket-URL | +| `VITE_PUBLIC_ORIGIN` | leer | Öffentliche Vite-Origin (Remote-Dev) | +| `VITE_ALLOWED_HOSTS` | `true` | Komma-separierte Hostliste für Vite `allowedHosts` | +| `VITE_HMR_PROTOCOL` | abgeleitet aus `VITE_PUBLIC_ORIGIN` | HMR-Protokoll (`ws`/`wss`) | +| `VITE_HMR_HOST` | abgeleitet aus `VITE_PUBLIC_ORIGIN` | HMR-Host | +| `VITE_HMR_CLIENT_PORT` | abgeleitet aus `VITE_PUBLIC_ORIGIN` | HMR-Client-Port | -### Beispiel: frontend/.env (Entwicklung) +Beispiele: ```env -VITE_API_BASE=http://localhost:3001 -VITE_WS_URL=ws://localhost:3001 +# lokal (mit Vite-Proxy) +VITE_API_BASE=/api ``` -### Beispiel: frontend/.env (Netzwerk-Zugriff) - ```env -VITE_API_BASE=http://192.168.1.100:3001 -VITE_WS_URL=ws://192.168.1.100:3001 -VITE_PUBLIC_ORIGIN=http://192.168.1.100:5173 +# remote dev +VITE_API_BASE=http://192.168.1.50:3001/api +VITE_WS_URL=ws://192.168.1.50:3001/ws +VITE_PUBLIC_ORIGIN=http://192.168.1.50:5173 +VITE_ALLOWED_HOSTS=192.168.1.50,ripster.local +VITE_HMR_PROTOCOL=ws +VITE_HMR_HOST=192.168.1.50 +VITE_HMR_CLIENT_PORT=5173 ``` --- -## .env.example Dateien +## Priorität -Das Repository enthält Vorlagen für beide Konfigurationsdateien: - -```bash -# Backend -cp backend/.env.example backend/.env - -# Frontend -cp frontend/.env.example frontend/.env -``` - ---- - -## Priorität der Konfiguration - -Einstellungen werden in folgender Reihenfolge geladen (höhere Priorität überschreibt niedrigere): - -``` -1. Systemumgebungsvariablen (export VAR=value) -2. .env-Datei -3. Hardcodierte Standardwerte in config.js -``` - ---- - -## LOG_LEVEL - -| Level | Ausgabe | -|-------|---------| -| `debug` | Alle Meldungen inkl. Debugging | -| `info` | Normale Betriebsinformationen | -| `warn` | Warnungen + Fehler | -| `error` | Nur Fehler | - -!!! tip "Produktionsempfehlung" - Für Produktionsumgebungen `LOG_LEVEL=info` oder `LOG_LEVEL=warn` verwenden. `debug` erzeugt sehr viele Log-Einträge. +1. Prozess-Umgebungsvariablen +2. `.env` +3. Code-Defaults diff --git a/docs/configuration/settings-reference.md b/docs/configuration/settings-reference.md index 8f4dd8c..d8f1bc6 100644 --- a/docs/configuration/settings-reference.md +++ b/docs/configuration/settings-reference.md @@ -1,180 +1,165 @@ # Einstellungsreferenz -Vollständige Übersicht aller Ripster-Einstellungen. Alle Einstellungen werden über die Web-Oberfläche unter **Einstellungen** verwaltet und in SQLite gespeichert. +Alle Settings liegen in `settings_schema`/`settings_values` und werden über die UI verwaltet. --- ## Profil-System -Ripster erkennt den Medientyp einer eingelegten Disc (Blu-ray / DVD / CD) und wählt automatisch die passenden profil-spezifischen Einstellungen. Für viele Schlüssel gibt es zusätzlich zur globalen Einstellung eine Variante pro Profil: +Ripster arbeitet mit Media-Profilen: -| Profil | Erkennungsmerkmale | -|--------|--------------------| -| `bluray` | UDF-Dateisystem, Laufwerk-Modell enthält „Blu-ray", Disc-Label wie BDMV | -| `dvd` | ISO9660/UDF, Laufwerk-Modell enthält „DVD", VIDEO_TS-Struktur | -| `other` | Alles andere (CD, unbekannt) | +- `bluray` +- `dvd` +- `other` -**Auflösungsreihenfolge für profil-spezifische Einstellungen:** +Viele Tool-/Pfad-Settings existieren als Profil-Varianten (`*_bluray`, `*_dvd`, `*_other`). -1. Profil-spezifischer Wert (`_bluray` / `_dvd`) – wenn gesetzt, hat dieser Vorrang -2. Alternativ-Profil als Fallback (Blu-ray → DVD-Wert als Fallback und umgekehrt) +Wichtig: -Pfad-Einstellungen (`raw_dir`, `movie_dir`) und Besitz-Einstellungen (`raw_dir_owner`, `movie_dir_owner`) werden **ausschließlich** aus dem passenden Profil bezogen – kein Cross-Profil-Fallback. +- Für `raw_dir`, `movie_dir` und die zugehörigen `*_owner`-Keys gibt es **kein Cross-Profil-Fallback**. +- Für viele Tool-Keys werden profilspezifische Varianten bevorzugt. + +--- + +## Template-Platzhalter + +Datei-/Ordner-Templates unterstützen: + +- `${title}` +- `${year}` +- `${imdbId}` + +Nicht gesetzte Werte werden zu `unknown`. --- ## Kategorie: Pfade -| Schlüssel | Typ | Pflicht | Beschreibung | -|-----------|-----|---------|-------------| -| `raw_dir` | string | ✅ | Verzeichnis für rohe MKV-Dateien (Fallback wenn kein Profil-Wert) | -| `raw_dir_bluray` | string | — | Raw-Verzeichnis für Blu-rays | -| `raw_dir_dvd` | string | — | Raw-Verzeichnis für DVDs | -| `raw_dir_other` | string | — | Raw-Verzeichnis für sonstige Medien | -| `raw_dir_owner` | string | — | Besitzer für Raw-Verzeichnis (`user:group`, Fallback) | -| `raw_dir_bluray_owner` | string | — | Besitzer für Raw-Verzeichnis (Blu-ray) | -| `raw_dir_dvd_owner` | string | — | Besitzer für Raw-Verzeichnis (DVD) | -| `raw_dir_other_owner` | string | — | Besitzer für Raw-Verzeichnis (Sonstiges) | -| `movie_dir` | string | ✅ | Ausgabeverzeichnis für Filme (Fallback) | -| `movie_dir_bluray` | string | — | Ausgabeverzeichnis für Blu-rays | -| `movie_dir_dvd` | string | — | Ausgabeverzeichnis für DVDs | -| `movie_dir_other` | string | — | Ausgabeverzeichnis für sonstige Medien | -| `movie_dir_owner` | string | — | Besitzer für Ausgabeverzeichnis (Fallback) | -| `movie_dir_bluray_owner` | string | — | Besitzer für Ausgabeverzeichnis (Blu-ray) | -| `movie_dir_dvd_owner` | string | — | Besitzer für Ausgabeverzeichnis (DVD) | -| `movie_dir_other_owner` | string | — | Besitzer für Ausgabeverzeichnis (Sonstiges) | -| `log_dir` | string | — | Verzeichnis für Log-Dateien (Standard: `./logs`) | +| Key | Typ | Default | +|-----|-----|---------| +| `raw_dir` | path | `data/output/raw` | +| `raw_dir_bluray` | path | `null` | +| `raw_dir_dvd` | path | `null` | +| `raw_dir_other` | path | `null` | +| `raw_dir_bluray_owner` | string | `null` | +| `raw_dir_dvd_owner` | string | `null` | +| `raw_dir_other_owner` | string | `null` | +| `movie_dir` | path | `data/output/movies` | +| `movie_dir_bluray` | path | `null` | +| `movie_dir_dvd` | path | `null` | +| `movie_dir_other` | path | `null` | +| `movie_dir_bluray_owner` | string | `null` | +| `movie_dir_dvd_owner` | string | `null` | +| `movie_dir_other_owner` | string | `null` | +| `log_dir` | path | `data/logs` | --- ## Kategorie: Laufwerk -| Schlüssel | Typ | Standard | Beschreibung | -|-----------|-----|---------|-------------| -| `drive_mode` | select | `auto` | `auto` = automatisch erkennen, `explicit` = festes Gerät | -| `drive_device` | string | `/dev/sr0` | Geräte-Pfad (nur bei `explicit`) | -| `disc_poll_interval_ms` | number | `4000` | Polling-Intervall in Millisekunden (1000–60000) | -| `makemkv_source_index` | number | `0` | Laufwerk-Index für MakeMKV (bei mehreren Laufwerken) | - ---- - -## Kategorie: Tools (global) - -| Schlüssel | Typ | Standard | Beschreibung | -|-----------|-----|---------|-------------| -| `makemkv_command` | string | `makemkvcon` | Befehl oder absoluter Pfad zu MakeMKV | -| `handbrake_command` | string | `HandBrakeCLI` | Befehl oder absoluter Pfad zu HandBrake | -| `mediainfo_command` | string | `mediainfo` | Befehl oder absoluter Pfad zu MediaInfo | -| `makemkv_min_length_minutes` | number | `15` | Mindest-Titellänge in Minuten (0–999) | -| `pipeline_max_parallel_jobs` | number | `1` | Maximale Anzahl parallel laufender Jobs (1–12) | -| `handbrake_restart_delete_incomplete_output` | boolean | `true` | Unvollständige Ausgabedatei beim Encode-Neustart löschen | - -### Kategorie: Tools – Blu-ray - -| Schlüssel | Typ | Standard | Beschreibung | -|-----------|-----|---------|-------------| -| `makemkv_rip_mode_bluray` | select | `backup` | Rip-Modus: `mkv` oder `backup` | -| `makemkv_analyze_extra_args_bluray` | string | — | Zusatz-CLI-Parameter für Analyse (Blu-ray) | -| `makemkv_rip_extra_args_bluray` | string | — | Zusatz-CLI-Parameter für Rip (Blu-ray) | -| `mediainfo_extra_args_bluray` | string | — | Zusatz-CLI-Parameter für mediainfo (Blu-ray) | -| `handbrake_preset_bluray` | string | `H.264 MKV 1080p30` | HandBrake-Preset für Blu-rays | -| `handbrake_extra_args_bluray` | string | — | Zusatz-CLI-Argumente für HandBrake (Blu-ray) | -| `output_extension_bluray` | select | `mkv` | Ausgabeformat: `mkv` oder `mp4` | -| `filename_template_bluray` | string | `${title} (${year})` | Dateiname-Template (Blu-ray) | -| `output_folder_template_bluray` | string | — | Ordnername-Template (Blu-ray, leer = Dateiname-Template) | - -### Kategorie: Tools – DVD - -| Schlüssel | Typ | Standard | Beschreibung | -|-----------|-----|---------|-------------| -| `makemkv_rip_mode_dvd` | select | `mkv` | Rip-Modus: `mkv` oder `backup` | -| `makemkv_analyze_extra_args_dvd` | string | — | Zusatz-CLI-Parameter für Analyse (DVD) | -| `makemkv_rip_extra_args_dvd` | string | — | Zusatz-CLI-Parameter für Rip (DVD) | -| `mediainfo_extra_args_dvd` | string | — | Zusatz-CLI-Parameter für mediainfo (DVD) | -| `handbrake_preset_dvd` | string | `H.264 MKV 480p30` | HandBrake-Preset für DVDs | -| `handbrake_extra_args_dvd` | string | — | Zusatz-CLI-Argumente für HandBrake (DVD) | -| `output_extension_dvd` | select | `mkv` | Ausgabeformat: `mkv` oder `mp4` | -| `filename_template_dvd` | string | `${title} (${year})` | Dateiname-Template (DVD) | -| `output_folder_template_dvd` | string | — | Ordnername-Template (DVD, leer = Dateiname-Template) | - -### Globale Fallback-Einstellungen für Encode - -Diese Werte werden verwendet, wenn kein profil-spezifischer Wert konfiguriert ist: - -| Schlüssel | Typ | Standard | Beschreibung | -|-----------|-----|---------|-------------| -| `handbrake_preset` | string | `H.265 MKV 1080p30` | Fallback HandBrake-Preset | -| `handbrake_extra_args` | string | — | Fallback Extra-Args | -| `makemkv_rip_mode` | select | `mkv` | Fallback Rip-Modus | -| `makemkv_analyze_extra_args` | string | — | Fallback Analyse-Args | -| `makemkv_rip_extra_args` | string | — | Fallback Rip-Args | -| `mediainfo_extra_args` | string | — | Fallback MediaInfo-Args | -| `output_extension` | select | `mkv` | Fallback Ausgabeformat | -| `filename_template` | string | `${title} (${year})` | Fallback Dateiname-Template | -| `output_folder_template` | string | — | Fallback Ordnername-Template | - -### Template-Platzhalter - -| Platzhalter | Beispiel | -|------------|---------| -| `${title}` | `Inception` | -| `${year}` | `2010` | -| `${imdbId}` | `tt1375666` | - ---- - -## Kategorie: Metadaten - -| Schlüssel | Typ | Standard | Beschreibung | -|-----------|-----|---------|-------------| -| `omdb_api_key` | string | — | API-Key von [omdbapi.com](https://www.omdbapi.com/) | -| `omdb_default_type` | select | `movie` | Vorauswahl für OMDb-Suche: `movie`, `series`, `episode` | - ---- - -## Kategorie: Benachrichtigungen (PushOver) - -| Schlüssel | Typ | Standard | Beschreibung | -|-----------|-----|---------|-------------| -| `pushover_enabled` | boolean | `false` | Master-Schalter für PushOver | -| `pushover_token` | string | — | Application-Token | -| `pushover_user` | string | — | User-Key | -| `pushover_device` | string | — | Optionales Ziel-Device | -| `pushover_title_prefix` | string | `Ripster` | Präfix im Benachrichtigungstitel | -| `pushover_priority` | number | `0` | Priorität (-2 bis 2) | -| `pushover_timeout_ms` | number | `7000` | HTTP-Timeout für PushOver-Requests (ms) | - -### Granulare Event-Schalter - -| Schlüssel | Standard | Beschreibung | -|-----------|---------|-------------| -| `pushover_notify_metadata_ready` | `true` | Bei Metadaten-Auswahl benachrichtigen | -| `pushover_notify_rip_started` | `true` | Bei MakeMKV-Rip-Start | -| `pushover_notify_encoding_started` | `true` | Bei HandBrake-Start | -| `pushover_notify_job_finished` | `true` | Bei erfolgreichem Abschluss | -| `pushover_notify_job_error` | `true` | Bei Fehler | -| `pushover_notify_job_cancelled` | `true` | Bei manuellem Abbruch | -| `pushover_notify_reencode_started` | `true` | Bei Re-Encode-Start | -| `pushover_notify_reencode_finished` | `true` | Bei erfolgreichem Re-Encode | +| Key | Typ | Default | Hinweis | +|-----|-----|---------|--------| +| `drive_mode` | select | `auto` | `auto` oder `explicit` | +| `drive_device` | path | `/dev/sr0` | bei `explicit` relevant | +| `makemkv_source_index` | number | `0` | MakeMKV Source-Index | +| `disc_poll_interval_ms` | number | `4000` | 1000..60000 | --- ## Kategorie: Monitoring -| Schlüssel | Typ | Standard | Beschreibung | -|-----------|-----|---------|-------------| -| `hardware_monitoring_enabled` | boolean | `false` | Hardware-Monitoring aktivieren (CPU, RAM, Temp.) | -| `hardware_monitoring_interval_ms` | number | `5000` | Monitoring-Polling-Intervall (ms) | +| Key | Typ | Default | +|-----|-----|---------| +| `hardware_monitoring_enabled` | boolean | `true` | +| `hardware_monitoring_interval_ms` | number | `5000` | --- -## Standard-Einstellungen zurücksetzen +## Kategorie: Tools (global) -Einen einzelnen Wert über die Datenbank zurücksetzen: +| Key | Typ | Default | +|-----|-----|---------| +| `makemkv_command` | string | `makemkvcon` | +| `makemkv_registration_key` | string | `null` | +| `mediainfo_command` | string | `mediainfo` | +| `makemkv_min_length_minutes` | number | `60` | +| `handbrake_command` | string | `HandBrakeCLI` | +| `handbrake_restart_delete_incomplete_output` | boolean | `true` | +| `pipeline_max_parallel_jobs` | number | `1` | -```bash -sqlite3 backend/data/ripster.db \ - "DELETE FROM settings_values WHERE key = 'handbrake_preset_bluray';" -``` +### Blu-ray-spezifisch -Beim nächsten Laden wird der Standardwert aus `settings_schema.default_value` verwendet. +| Key | Typ | Default | +|-----|-----|---------| +| `mediainfo_extra_args_bluray` | string | `null` | +| `makemkv_rip_mode_bluray` | select | `backup` | +| `makemkv_analyze_extra_args_bluray` | string | `null` | +| `makemkv_rip_extra_args_bluray` | string | `null` | +| `handbrake_preset_bluray` | string | `H.264 MKV 1080p30` | +| `handbrake_extra_args_bluray` | string | `null` | +| `output_extension_bluray` | select | `mkv` | +| `filename_template_bluray` | string | `${title} (${year})` | +| `output_folder_template_bluray` | string | `null` | + +### DVD-spezifisch + +| Key | Typ | Default | +|-----|-----|---------| +| `mediainfo_extra_args_dvd` | string | `null` | +| `makemkv_rip_mode_dvd` | select | `mkv` | +| `makemkv_analyze_extra_args_dvd` | string | `null` | +| `makemkv_rip_extra_args_dvd` | string | `null` | +| `handbrake_preset_dvd` | string | `H.264 MKV 480p30` | +| `handbrake_extra_args_dvd` | string | `null` | +| `output_extension_dvd` | select | `mkv` | +| `filename_template_dvd` | string | `${title} (${year})` | +| `output_folder_template_dvd` | string | `null` | + +--- + +## Kategorie: Metadaten + +| Key | Typ | Default | +|-----|-----|---------| +| `omdb_api_key` | string | `null` | +| `omdb_default_type` | select | `movie` | + +--- + +## Kategorie: Benachrichtigungen (PushOver) + +| Key | Typ | Default | +|-----|-----|---------| +| `pushover_enabled` | boolean | `false` | +| `pushover_token` | string | `null` | +| `pushover_user` | string | `null` | +| `pushover_device` | string | `null` | +| `pushover_title_prefix` | string | `Ripster` | +| `pushover_priority` | number | `0` | +| `pushover_timeout_ms` | number | `7000` | +| `pushover_notify_metadata_ready` | boolean | `true` | +| `pushover_notify_rip_started` | boolean | `true` | +| `pushover_notify_encoding_started` | boolean | `true` | +| `pushover_notify_job_finished` | boolean | `true` | +| `pushover_notify_job_error` | boolean | `true` | +| `pushover_notify_job_cancelled` | boolean | `true` | +| `pushover_notify_reencode_started` | boolean | `true` | +| `pushover_notify_reencode_finished` | boolean | `true` | + +--- + +## Entfernte Legacy-Keys + +Diese Legacy-Keys werden bei Migration entfernt und sollten nicht mehr genutzt werden: + +- `makemkv_backup_mode` +- `mediainfo_extra_args` +- `makemkv_rip_mode` +- `makemkv_analyze_extra_args` +- `makemkv_rip_extra_args` +- `handbrake_preset` +- `handbrake_extra_args` +- `output_extension` +- `filename_template` +- `output_folder_template` +- `pushover_notify_disc_detected` diff --git a/docs/deployment/development.md b/docs/deployment/development.md index 7fed80b..3a1eaf3 100644 --- a/docs/deployment/development.md +++ b/docs/deployment/development.md @@ -5,7 +5,7 @@ ## Voraussetzungen - Node.js >= 20.19.0 -- Alle [externen Tools](../getting-started/prerequisites.md) installiert +- externe Tools installiert (`makemkvcon`, `HandBrakeCLI`, `mediainfo`) --- @@ -15,15 +15,18 @@ ./start.sh ``` -Das Skript startet automatisch: -- **Backend** auf Port 3001 (mit Nodemon für Hot-Reload) -- **Frontend** auf Port 5173 (mit Vite HMR) +Startet: + +- Backend (`http://localhost:3001`, mit nodemon) +- Frontend (`http://localhost:5173`, mit Vite HMR) + +Stoppen: `Ctrl+C`. --- -## Manuelle Entwicklungsumgebung +## Manuell -### Terminal 1 – Backend +### Backend ```bash cd backend @@ -31,9 +34,7 @@ npm install npm run dev ``` -Backend läuft auf `http://localhost:3001` mit **Nodemon** – Neustart bei Dateiänderungen. - -### Terminal 2 – Frontend +### Frontend ```bash cd frontend @@ -41,97 +42,44 @@ npm install npm run dev ``` -Frontend läuft auf `http://localhost:5173` mit **Vite HMR** – sofortige Browser-Updates. +--- + +## Vite-Proxy (Dev) + +`frontend/vite.config.js` proxied standardmäßig: + +- `/api` -> `http://127.0.0.1:3001` +- `/ws` -> `ws://127.0.0.1:3001` --- -## Vite-Proxy +## Remote-Dev (optional) -Im Entwicklungsmodus proxied Vite alle API- und WebSocket-Anfragen zum Backend: - -```js -// frontend/vite.config.js -server: { - proxy: { - '/api': { - target: 'http://localhost:3001', - changeOrigin: true - }, - '/ws': { - target: 'ws://localhost:3001', - ws: true - } - } -} -``` - -Das bedeutet: Im Browser macht das Frontend Anfragen an `localhost:5173/api/...` – Vite leitet diese an `localhost:3001/api/...` weiter. - ---- - -## Remote-Entwicklung - -Falls Ripster auf einem entfernten Server entwickelt wird (z.B. Homeserver), muss die Vite-Konfiguration angepasst werden: +Beispiel `frontend/.env.local`: ```env -# frontend/.env.local -VITE_API_BASE=http://192.168.1.100:3001 -VITE_WS_URL=ws://192.168.1.100:3001 +VITE_API_BASE=http://192.168.1.100:3001/api +VITE_WS_URL=ws://192.168.1.100:3001/ws +VITE_PUBLIC_ORIGIN=http://192.168.1.100:5173 +VITE_ALLOWED_HOSTS=192.168.1.100,ripster.local +VITE_HMR_PROTOCOL=ws VITE_HMR_HOST=192.168.1.100 -VITE_HMR_PORT=5173 +VITE_HMR_CLIENT_PORT=5173 ``` --- -## Log-Level für Entwicklung - -```env -# backend/.env -LOG_LEVEL=debug -``` - -Im Debug-Modus werden alle Ausgaben der externen Tools (MakeMKV, HandBrake) vollständig geloggt. - ---- - -## Stoppen +## Nützliche Kommandos ```bash -./kill.sh +# Root dev (backend + frontend) +npm run dev + +# einzeln +npm run dev:backend +npm run dev:frontend + +# Frontend Build +npm run build:frontend ``` ---- - -## Linting & Type-Checking - -```bash -# Frontend (ESLint) -cd frontend && npm run lint - -# Backend hat keine separaten Lint-Scripts, -# nutze direkt eslint falls konfiguriert -``` - ---- - -## Deployment-Script - -Das `deploy-ripster.sh`-Script überträgt Code auf einen Remote-Server per SSH: - -```bash -./deploy-ripster.sh -``` - -**Was das Script tut:** -1. `rsync` synchronisiert den Code (Backend-Quellcode ohne `data/`) -2. Die Datenbank (`backend/data/`) wird **nicht** überschrieben -3. Verbindung via SSH (konfigurierbar im Script) - -**Anpassung des Scripts:** - -```bash -# deploy-ripster.sh -REMOTE_HOST="192.168.1.100" -REMOTE_USER="michael" -REMOTE_PATH="/home/michael/ripster" -``` diff --git a/docs/deployment/production.md b/docs/deployment/production.md index e66fae4..df96913 100644 --- a/docs/deployment/production.md +++ b/docs/deployment/production.md @@ -4,42 +4,44 @@ ## Empfohlene Architektur +```text +Client + -> nginx (Reverse Proxy + statisches Frontend) + -> Backend API/WebSocket (Node.js, Port 3001) ``` -Internet / Heimnetz - ↓ - nginx (Reverse Proxy) - ↓ - ┌────┴────┐ - │ │ -Backend Frontend - :3001 (statische Dateien) -``` + +Wichtig: Das Backend serviert im aktuellen Stand keine `frontend/dist`-Dateien automatisch. --- -## systemd-Service - -Für ein dauerhaftes Betreiben als systemd-Service: +## 1) Frontend builden ```bash -sudo nano /etc/systemd/system/ripster.service +cd frontend +npm install +npm run build ``` +Artefakte liegen in `frontend/dist/`. + +--- + +## 2) Backend als systemd-Service + +Beispiel `/etc/systemd/system/ripster-backend.service`: + ```ini [Unit] -Description=Ripster - Disc Ripping Service +Description=Ripster Backend After=network.target [Service] Type=simple -User=michael -WorkingDirectory=/home/michael/ripster -ExecStart=/bin/bash /home/michael/ripster/start.sh -ExecStop=/bin/bash /home/michael/ripster/kill.sh +User=ripster +WorkingDirectory=/opt/ripster/backend +ExecStart=/usr/bin/env node src/index.js Restart=on-failure -RestartSec=10s - -# Umgebungsvariablen +RestartSec=5 Environment=NODE_ENV=production Environment=PORT=3001 Environment=LOG_LEVEL=info @@ -48,61 +50,40 @@ Environment=LOG_LEVEL=info WantedBy=multi-user.target ``` +Aktivieren: + ```bash -# Service aktivieren und starten sudo systemctl daemon-reload -sudo systemctl enable ripster -sudo systemctl start ripster - -# Status prüfen -sudo systemctl status ripster - -# Logs anzeigen -journalctl -u ripster -f +sudo systemctl enable --now ripster-backend +sudo systemctl status ripster-backend ``` --- -## Frontend-Build +## 3) nginx konfigurieren -Für Produktion das Frontend bauen: - -```bash -cd frontend -npm run build -``` - -Die statischen Dateien landen in `frontend/dist/`. - ---- - -## nginx-Konfiguration +Beispiel `/etc/nginx/sites-available/ripster`: ```nginx -# /etc/nginx/sites-available/ripster server { listen 80; server_name ripster.local; - # Statisches Frontend - root /home/michael/ripster/frontend/dist; + root /opt/ripster/frontend/dist; index index.html; - # SPA Fallback (React Router) location / { try_files $uri $uri/ /index.html; } - # API-Proxy zum Backend location /api/ { - proxy_pass http://localhost:3001; + proxy_pass http://127.0.0.1:3001; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } - # WebSocket-Proxy location /ws { - proxy_pass http://localhost:3001; + proxy_pass http://127.0.0.1:3001; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; @@ -111,83 +92,27 @@ server { } ``` +Aktivieren: + ```bash sudo ln -s /etc/nginx/sites-available/ripster /etc/nginx/sites-enabled/ -sudo nginx -t && sudo systemctl reload nginx +sudo nginx -t +sudo systemctl reload nginx ``` --- -## Nur-Backend-Produktion (ohne nginx) - -Falls kein Reverse Proxy gewünscht ist, kann das Backend die Frontend-Dateien direkt ausliefern: - -```bash -# Frontend bauen -cd frontend && npm run build - -# Backend startet und serviert frontend/dist/ -cd backend && NODE_ENV=production npm start -``` - -Das Backend ist so konfiguriert, dass es im Produktionsmodus die `frontend/dist/`-Dateien als statische Assets ausliefert. - ---- - ## Datenbank-Backup ```bash -# Datenbank sichern -cp backend/data/ripster.db backend/data/ripster.db.backup.$(date +%Y%m%d) - -# Oder mit SQLite-eigenem Backup-Befehl -sqlite3 backend/data/ripster.db ".backup '/mnt/backup/ripster.db'" -``` - -!!! tip "Automatisches Backup" - Cron-Job für tägliches Backup: - ```cron - 0 3 * * * sqlite3 /home/michael/ripster/backend/data/ripster.db ".backup '/mnt/backup/ripster-$(date +\%Y\%m\%d).db'" - ``` - ---- - -## Log-Rotation - -Ripster rotiert Logs automatisch täglich. Falls zusätzlich systemd-Journal-Rotation gewünscht ist: - -```bash -# /etc/logrotate.d/ripster -/home/michael/ripster/backend/logs/*.log { - daily - rotate 14 - compress - missingok - notifempty -} +sqlite3 /opt/ripster/backend/data/ripster.db \ + ".backup '/var/backups/ripster-$(date +%Y%m%d).db'" ``` --- -## Sicherheitshinweise +## Sicherheit -!!! warning "Heimnetz-Einsatz" - Ripster ist für den Einsatz im **lokalen Heimnetz** konzipiert und enthält **keine Authentifizierung**. Stelle sicher, dass der Dienst nicht öffentlich erreichbar ist. - -Falls öffentlicher Zugang benötigt wird: - -1. **Basic Auth** via nginx: - ```bash - sudo htpasswd -c /etc/nginx/.htpasswd michael - ``` - ```nginx - location / { - auth_basic "Ripster"; - auth_basic_user_file /etc/nginx/.htpasswd; - # ... - } - ``` - -2. **VPN-Zugang** (empfohlen): Zugriff nur über WireGuard/OpenVPN - -3. **SSL/TLS**: Let's Encrypt mit certbot für HTTPS +- Ripster hat keine eingebaute Authentifizierung. +- Für externen Zugriff mindestens Basic Auth + TLS + Netzwerksegmentierung/VPN einsetzen. +- Secrets nicht ins Repo committen (`.env`, Settings-Felder). diff --git a/docs/getting-started/configuration.md b/docs/getting-started/configuration.md index 4a7fe4d..21f72f3 100644 --- a/docs/getting-started/configuration.md +++ b/docs/getting-started/configuration.md @@ -1,118 +1,99 @@ # Konfiguration -Alle Einstellungen werden über die Web-Oberfläche unter **Einstellungen** verwaltet und in der SQLite-Datenbank gespeichert. +Die Hauptkonfiguration erfolgt über die UI (`Settings`) und wird in SQLite gespeichert. --- -## Pflichteinstellungen +## Pflichteinstellungen vor dem ersten Rip -Diese Einstellungen müssen vor dem ersten Rip konfiguriert werden: - -### Pfade +### 1) Pfade | Einstellung | Beschreibung | Beispiel | |------------|-------------|---------| -| `raw_dir` | Verzeichnis für rohe MKV-Dateien | `/mnt/nas/raw` | -| `movie_dir` | Ausgabeverzeichnis für kodierte Filme | `/mnt/nas/movies` | -| `log_dir` | Verzeichnis für Log-Dateien | `/var/log/ripster` | +| `raw_dir` | Basisverzeichnis für RAW-Rips | `/mnt/ripster/raw` | +| `movie_dir` | Basisverzeichnis für finale Encodes | `/mnt/ripster/movies` | +| `log_dir` | Verzeichnis für Prozess-/Backend-Logs | `/mnt/ripster/logs` | -!!! warning "Berechtigungen" - Der Ripster-Prozess benötigt **Schreibrechte** auf alle konfigurierten Verzeichnisse. +Optional profilspezifisch: - ```bash - # Verzeichnisse erstellen und Berechtigungen setzen - sudo mkdir -p /mnt/nas/{raw,movies} - sudo chown $USER:$USER /mnt/nas/{raw,movies} - ``` +- `raw_dir_bluray`, `raw_dir_dvd`, `raw_dir_other` +- `movie_dir_bluray`, `movie_dir_dvd`, `movie_dir_other` -### OMDb API +### 2) Tools + +| Einstellung | Standard | +|------------|---------| +| `makemkv_command` | `makemkvcon` | +| `handbrake_command` | `HandBrakeCLI` | +| `mediainfo_command` | `mediainfo` | + +### 3) OMDb | Einstellung | Beschreibung | |------------|-------------| | `omdb_api_key` | API-Key von omdbapi.com | -| `omdb_default_type` | Standard-Suchtyp: `movie` oder `series` | +| `omdb_default_type` | `movie`, `series`, `episode` | --- -## Tool-Konfiguration +## Encode-Konfiguration (wichtig) -| Einstellung | Standard | Beschreibung | -|------------|---------|-------------| -| `makemkv_command` | `makemkvcon` | Pfad oder Befehl für MakeMKV | -| `handbrake_command` | `HandBrakeCLI` | Pfad oder Befehl für HandBrake | -| `mediainfo_command` | `mediainfo` | Pfad oder Befehl für MediaInfo | +Ripster arbeitet profilspezifisch, typischerweise über: -!!! tip "Absolute Pfade" - Falls die Tools nicht im `PATH` sind, verwende absolute Pfade: - ``` - /usr/local/bin/HandBrakeCLI - ``` +- Blu-ray: `handbrake_preset_bluray`, `handbrake_extra_args_bluray`, `output_extension_bluray`, `filename_template_bluray` +- DVD: `handbrake_preset_dvd`, `handbrake_extra_args_dvd`, `output_extension_dvd`, `filename_template_dvd` ---- +### Template-Platzhalter -## Encoding-Konfiguration +Verfügbar in `filename_template_*` und `output_folder_template_*`: -| Einstellung | Standard | Beschreibung | -|------------|---------|-------------| -| `handbrake_preset` | `H.265 MKV 1080p30` | HandBrake-Preset-Name | -| `handbrake_extra_args` | _(leer)_ | Zusätzliche HandBrake-Argumente | -| `output_extension` | `mkv` | Dateiendung der Ausgabedatei | -| `filename_template` | `{title} ({year})` | Template für Dateinamen | +- `${title}` +- `${year}` +- `${imdbId}` -### Dateiname-Template +Beispiel: -Das Template unterstützt folgende Platzhalter: - -| Platzhalter | Beschreibung | Beispiel | -|------------|-------------|---------| -| `{title}` | Filmtitel | `Inception` | -| `{year}` | Erscheinungsjahr | `2010` | -| `{imdb_id}` | IMDb-ID | `tt1375666` | -| `{type}` | `movie` oder `series` | `movie` | - -**Beispiel-Template:** -``` -{title} ({year}) -→ Inception (2010).mkv +```text +${title} (${year}) +-> Inception (2010).mkv ``` --- -## Laufwerk-Konfiguration +## MakeMKV-spezifisch -| Einstellung | Standard | Beschreibung | -|------------|---------|-------------| -| `drive_mode` | `auto` | `auto` (automatisch erkennen) oder `explicit` (festes Gerät) | -| `drive_device` | `/dev/sr0` | Geräte-Pfad (nur bei `explicit`) | -| `disc_poll_interval_ms` | `4000` | Polling-Intervall in Millisekunden | +| Einstellung | Standard | Hinweis | +|------------|---------|--------| +| `makemkv_min_length_minutes` | `60` | Kandidaten-Filter | +| `makemkv_rip_mode_bluray` | `backup` | `mkv` oder `backup` | +| `makemkv_rip_mode_dvd` | `mkv` | `mkv` oder `backup` | +| `makemkv_registration_key` | leer | optional, wird via `makemkvcon reg` gesetzt | --- -## MakeMKV-Konfiguration +## Monitoring & Queue -| Einstellung | Standard | Beschreibung | -|------------|---------|-------------| -| `makemkv_min_length_minutes` | `15` | Mindestlänge für Titel in Minuten | -| `makemkv_backup_mode` | `false` | Backup-Modus statt MKV-Modus | - -!!! info "Backup-Modus" - Im Backup-Modus erstellt MakeMKV eine vollständige Kopie der Disc (inkl. Menüs). Der Standardmodus erstellt direkt MKV-Dateien. +| Einstellung | Standard | +|------------|---------| +| `hardware_monitoring_enabled` | `true` | +| `hardware_monitoring_interval_ms` | `5000` | +| `pipeline_max_parallel_jobs` | `1` | --- -## Benachrichtigungen (PushOver) +## PushOver (optional) -| Einstellung | Beschreibung | -|------------|-------------| -| `pushover_user_key` | Dein PushOver User-Key | -| `pushover_api_token` | API-Token deiner PushOver-App | +Basis: -Nach der Eingabe kann die Verbindung mit dem **Test-Button** geprüft werden. +- `pushover_enabled` +- `pushover_token` +- `pushover_user` + +Zusätzlich pro Event ein/aus (z. B. `pushover_notify_job_finished`). --- -## Vollständige Einstellungsreferenz +## Verwandte Doku -Eine vollständige Liste aller Einstellungen mit Typen, Validierung und Standardwerten findest du unter: - -[:octicons-arrow-right-24: Einstellungsreferenz](../configuration/settings-reference.md) +- [Einstellungsreferenz](../configuration/settings-reference.md) +- [Umgebungsvariablen](../configuration/environment.md) diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 63d2423..1f1cd91 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -11,50 +11,46 @@ cd ripster --- -## Automatischer Start - -Ripster enthält ein `start.sh`-Skript, das alle Abhängigkeiten installiert und Backend + Frontend gleichzeitig startet: +## Dev-Start (empfohlen) ```bash ./start.sh ``` -Das Skript führt automatisch folgende Schritte durch: +`start.sh`: -1. **Node.js-Versionscheck** – prüft ob >= 20.19.0 verfügbar ist (mit nvm/npx-Fallback) -2. **Abhängigkeiten installieren** – `npm install` für Root, Backend und Frontend -3. **Dienste starten** – Backend und Frontend werden parallel gestartet +1. prüft Node-Version (`>= 20.19.0`) +2. installiert Dependencies (Root/Backend/Frontend) +3. startet Backend + Frontend parallel -!!! success "Erfolgreich gestartet" - - Backend läuft auf `http://localhost:3001` - - Frontend läuft auf `http://localhost:5173` +Danach: + +- Backend: `http://localhost:3001` +- Frontend: `http://localhost:5173` + +Stoppen: mit `Ctrl+C` im laufenden Terminal. --- -## Manuelle Installation - -Falls du mehr Kontrolle benötigst: +## Manuell starten ```bash -# Root-Abhängigkeiten npm install +npm --prefix backend install +npm --prefix frontend install +npm run dev +``` -# Backend-Abhängigkeiten -cd backend && npm install && cd .. +Oder getrennt: -# Frontend-Abhängigkeiten -cd frontend && npm install && cd .. - -# Backend starten (Terminal 1) -cd backend && npm run dev - -# Frontend starten (Terminal 2) -cd frontend && npm run dev +```bash +npm run dev:backend +npm run dev:frontend ``` --- -## Umgebungsvariablen konfigurieren +## Optional: .env-Dateien anlegen ### Backend @@ -62,13 +58,13 @@ cd frontend && npm run dev cp backend/.env.example backend/.env ``` -Bearbeite `backend/.env`: +Beispiel: ```env PORT=3001 DB_PATH=./data/ripster.db -CORS_ORIGIN=http://localhost:5173 LOG_DIR=./logs +CORS_ORIGIN=http://localhost:5173 LOG_LEVEL=info ``` @@ -78,63 +74,30 @@ LOG_LEVEL=info cp frontend/.env.example frontend/.env ``` -Bearbeite `frontend/.env`: +Beispiel: ```env -VITE_API_BASE=http://localhost:3001 -VITE_WS_URL=ws://localhost:3001 -``` - -!!! tip "Alle Umgebungsvariablen" - Eine vollständige Übersicht aller Umgebungsvariablen findest du unter [Umgebungsvariablen](../configuration/environment.md). - ---- - -## Datenbank initialisieren - -Die SQLite-Datenbank wird **automatisch** beim ersten Start erstellt und mit dem Schema aus `db/schema.sql` initialisiert. Es sind keine manuellen Datenbankschritte erforderlich. - -``` -backend/data/ -└── ripster.db ← Wird automatisch angelegt +VITE_API_BASE=/api +# optional: +# VITE_WS_URL=ws://localhost:3001/ws ``` --- -## Stoppen +## Datenbank -```bash -./kill.sh +SQLite wird automatisch beim Backend-Start initialisiert: + +```text +backend/data/ripster.db ``` -Das Skript beendet Backend- und Frontend-Prozesse graceful. - ---- - -## Verzeichnisstruktur nach Installation - -``` -ripster/ -├── backend/ -│ ├── data/ ← SQLite-Datenbank (nach erstem Start) -│ ├── logs/ ← Log-Dateien -│ ├── node_modules/ ← Backend-Abhängigkeiten -│ └── .env ← Backend-Konfiguration -├── frontend/ -│ ├── node_modules/ ← Frontend-Abhängigkeiten -│ ├── dist/ ← Production-Build (nach npm run build) -│ └── .env ← Frontend-Konfiguration -└── node_modules/ ← Root-Abhängigkeiten (concurrently etc.) -``` +Schema-Quelle: `db/schema.sql` --- ## Nächste Schritte -Nach erfolgreicher Installation: - -1. Öffne [http://localhost:5173](http://localhost:5173) -2. Navigiere zu **Einstellungen** -3. Konfiguriere Pfade, API-Keys und Encoding-Presets - -[:octicons-arrow-right-24: Zur Konfiguration](configuration.md) +1. Browser öffnen: `http://localhost:5173` +2. In `Settings` Pfade/Tools/API-Keys prüfen +3. Erste Disc einlegen und Workflow starten diff --git a/docs/getting-started/prerequisites.md b/docs/getting-started/prerequisites.md index 0d3b9e0..5bb7fef 100644 --- a/docs/getting-started/prerequisites.md +++ b/docs/getting-started/prerequisites.md @@ -145,15 +145,6 @@ Für mobile Push-Benachrichtigungen bei Fertigstellung oder Fehlern: - App kaufen auf [pushover.net](https://pushover.net) (~5 USD einmalig) - **User Key** und **API Token** notieren -### SSH-Zugang (Deployment) - -Für Remote-Deployment via `deploy-ripster.sh`: - -```bash -# sshpass installieren -sudo apt-get install sshpass -``` - --- ## Checkliste diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md index d7ead5a..449cb3e 100644 --- a/docs/getting-started/quickstart.md +++ b/docs/getting-started/quickstart.md @@ -1,411 +1,114 @@ -# Schnellstart – Vollständiger Workflow +# Schnellstart – Erster kompletter Job -Nach der [Installation](installation.md) und [Konfiguration](configuration.md) führt diese Seite Schritt für Schritt durch den ersten Rip – mit allen Details aus dem Code. +Diese Seite führt durch den typischen ersten Lauf. --- -## Übersicht: Pipeline-Ablauf - -
-
-
-
IDLE
-
Warten
-
-
-
1
-
DISC_DETECTED
-
Disc erkannt
-
-
-
2
-
METADATA_SELECTION
-
OMDb & Dialog
-
-
-
-
WAITING_FOR_USER_DECISION
-
Playlist wählen
(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)
-
-
-
-
FINISHED
-
Fertig
-
-
- -**Legende:** ● Warten  |  ■ Läuft automatisch  |  ■ Benutzeraktion  |  ⚠ Optional  |  ■ Encodierung  |  ✓ Fertig - -??? note "Vollständiges Zustandsdiagramm (inkl. Fehler- & Alternativpfade)" - -
- - ```mermaid - flowchart LR - START(( )) --> IDLE - - IDLE -->|Disc erkannt| DD[DISC_DETECTED] - DD -->|Analyse starten| META[METADATA\nSELECTION] - - META -->|Metadaten übernommen| RTS[READY_TO\nSTART] - META -->|vorhandenes RAW +\nPlaylist offen| WUD[WAITING_FOR\nUSER_DECISION] - RTS -->|Auto-Start| RIP[RIPPING] - RTS -->|Auto-Start mit RAW| MIC[MEDIAINFO\nCHECK] - - RIP -->|MKV fertig| MIC - RIP -->|Fehler| ERR - - MIC -->|Playlist offen (Backup)| WUD - WUD -->|Playlist bestätigt| MIC - WUD -->|Playlist bestätigt,\nnoch kein RAW| RTS - - MIC --> RTE[READY_TO\nENCODE] - RTE -->|Encoding starten| ENC[ENCODING] - - ENC -->|inkl. Post-Skripte| FIN([FINISHED]) - ENC -->|Fehler| ERR - - ERR([ERROR]) -->|Retry / Cancel| IDLE - - style FIN fill:#e8f5e9,stroke:#66bb6a,color:#2e7d32 - style ERR fill:#ffebee,stroke:#ef5350,color:#c62828 - style WUD fill:#fff8e1,stroke:#ffa726,color:#e65100 - style ENC fill:#f3e5f5,stroke:#ab47bc,color:#6a1b9a - ``` - -
- ---- - -## Schritt 1 – Ripster starten +## 1) Starten ```bash cd ripster ./start.sh ``` -Öffne [http://localhost:5173](http://localhost:5173) im Browser. Das Dashboard zeigt `IDLE`. +Öffne `http://localhost:5173`. --- -## Schritt 2 – Disc einlegen → `DISC_DETECTED` +## 2) Disc einlegen -Lege eine DVD oder Blu-ray ein. Der `diskDetectionService` pollt das Laufwerk alle `disc_poll_interval_ms` Millisekunden (Standard: 4 Sekunden). +Pipeline wechselt auf `DISC_DETECTED`. -**Was passiert im Code:** - -- `diskDetectionService` emittiert `discInserted` mit Geräteinformationen -- `pipelineService.onDiscInserted()` wird aufgerufen -- Dashboard-Status-Badge zeigt **"Medium erkannt"** -- Status-Text zeigt **"Neue Disk erkannt"** -- Der **"Analyse starten"**-Button wird aktiv - -!!! tip "Manuelle Auslösung" - Falls die automatische Erkennung nicht greift: - ```bash - curl -X POST http://localhost:3001/api/pipeline/analyze - ``` - ---- - -## Schritt 3 – Analyse starten → `METADATA_SELECTION` - -Klicke auf **"Analyse starten"**. - -**Was passiert im Code:** - -1. Ein neuer Job-Datensatz wird in der Datenbank angelegt (`status: METADATA_SELECTION`) -2. Ripster versucht, den Titel automatisch aus dem Disc-Label/Modell zu ermitteln -3. Mit diesem erkannten Titel wird sofort eine **OMDb-Suche** ausgelöst -4. Der `MetadataSelectionDialog` öffnet sich im Frontend mit den vorgeladenen Suchergebnissen - -**Erkannter Titel:** Der Disc-Label (z. B. `INCEPTION`) wird als Suchbegriff verwendet. Falls kein Label vorhanden, bleibt das Suchfeld leer. - ---- - -## Schritt 4 – Metadaten auswählen (`MetadataSelectionDialog`) - -Der Dialog zeigt vorgeladene OMDb-Suchergebnisse. Du kannst: - -### 4a) OMDb-Suchergebnis wählen - -``` -┌─────────────────────────────────────────────────┐ -│ Suche: [Inception ] 🔍 │ -├─────────────────────────────────────────────────┤ -│ ▶ Inception (2010) · Movie · tt1375666 │ -│ Inception: ... · Series · ... │ -├─────────────────────────────────────────────────┤ -│ [Auswahl übernehmen] │ -└─────────────────────────────────────────────────┘ -``` - -- Suche durch Titel anpassen und Enter drücken -- Typ-Filter: `movie` / `series` umschalten möglich -- Einen Eintrag anklicken, dann **"Auswahl übernehmen"** - -### 4b) Manuelle Eingabe (ohne OMDb) - -Falls kein passendes Ergebnis gefunden wird: -- Titel, Jahr und IMDb-ID manuell eingeben -- OMDb-Poster wird übersprungen - -**Was passiert nach Bestätigung:** - -Ripster ruft `pipelineService.selectMetadata()` auf und startet den nächsten Schritt automatisch: - -- Job wird auf `READY_TO_START` gesetzt (kurzer Übergangszustand) -- Falls bereits RAW vorhanden: direkter Sprung zu `MEDIAINFO_CHECK` -- Falls kein RAW vorhanden: automatischer Start von `RIPPING` -- Wenn bereits andere Jobs laufen, landet der Start stattdessen in der Queue - ---- - -## Schritt 5 – Optional: Playlist-Auswahl → `WAITING_FOR_USER_DECISION` - -Dieser Zustand erscheint nur bei mehrdeutigen Blu-ray-Playlists (typisch nach RAW-Analyse im Backup-Modus). - -Der **Playlist-Auswahl-Dialog** erscheint **zusätzlich** (nach dem Metadaten-Dialog): - -``` -┌───────────────────────────────────────────────────────────────┐ -│ Playlist-Auswahl │ -│ Es wurden mehrere Titel mit ähnlicher Laufzeit gefunden. │ -│ Bitte wähle die korrekte Playlist: │ -├───────────┬──────────┬────────┬──────────────────────────────┤ -│ Playlist │ Laufzeit │ Score │ Bewertung │ -├───────────┼──────────┼────────┼──────────────────────────────┤ -│ ● 00800 │ 2:28:05 │ +18 │ wahrscheinlich korrekt │ -│ │ │ │ (lineare Segmentfolge) │ -├───────────┼──────────┼────────┼──────────────────────────────┤ -│ ○ 00801 │ 2:28:12 │ −4 │ Auffällige Segmentreihenfolge │ -├───────────┼──────────┼────────┼──────────────────────────────┤ -│ ○ 00900 │ 2:28:05 │ −32 │ Fake-Struktur │ -│ │ │ │ (alternierendes Sprungmuster) │ -└───────────┴──────────┴────────┴──────────────────────────────┘ - 847 Playlists insgesamt · 3 relevante Kandidaten (≥ 15 min) - Empfehlung: 00800 (vorausgewählt) - [Playlist übernehmen] -``` - -- Die empfohlene Playlist ist **vorausgewählt** (Checkbox) -- Score und Bewertungslabel helfen bei der Entscheidung -- Nach **"Playlist übernehmen"** setzt Ripster automatisch fort: - - mit vorhandenem RAW in `MEDIAINFO_CHECK` - - ohne RAW über `READY_TO_START` weiter Richtung `RIPPING` - -!!! info "Scoring-Details" - Wie die Scores berechnet werden, erklärt die [Playlist-Analyse](../pipeline/playlist-analysis.md)-Seite. - ---- - -## Schritt 6 – Ripping → `RIPPING` - -**Vorher prüft Ripster:** Existiert bereits eine Raw-Datei für diesen Job? - -- **Ja, Raw-Datei vorhanden** → Direkt zu Schritt 7 (Track-Review), kein erneutes Ripping -- **Nein** → MakeMKV-Ripping startet - -Im Standardfall startet Ripster diesen Schritt automatisch nach der Metadaten-Auswahl. -Der Button **"Job starten"** ist hauptsächlich für Sonderfälle sichtbar (z. B. Fallback/Queue). - -**Was MakeMKV ausführt (MKV-Modus):** +Falls nötig manuell neu scannen: ```bash -makemkvcon mkv disc:0 all /mnt/raw/Inception-2010/ \ - --minlength=900 -r +curl -X POST http://localhost:3001/api/pipeline/rescan-disc ``` -**Was MakeMKV ausführt (Backup-Modus):** +--- + +## 3) Analyse starten + +Klicke im Dashboard auf `Analyse starten`. + +Intern: + +- Job wird angelegt +- MakeMKV-Analyse läuft (`ANALYZING`) +- UI wechselt in Metadatenauswahl (`METADATA_SELECTION`) + +--- + +## 4) Metadaten bestätigen + +Im Dialog: + +- OMDb-Ergebnis wählen oder manuell eintragen +- bei Playlist-Abfrage ggf. `selectedPlaylist` wählen + +Nach Bestätigung startet Ripster automatisch weiter. + +--- + +## 5) Pipeline-Pfade + +Abhängig von Job/RAW-Situation: + +- **kein RAW vorhanden** -> `RIPPING` +- **RAW vorhanden** -> `MEDIAINFO_CHECK` +- **mehrdeutige Playlist** -> `WAITING_FOR_USER_DECISION` + +Wenn Parallel-Limit erreicht ist, wird der Job in die Queue eingereiht. + +--- + +## 6) Review (`READY_TO_ENCODE`) + +Im Review-Panel: + +- Titel auswählen (falls mehrere) +- Audio-/Subtitle-Tracks auswählen +- optional User-Preset anwenden +- optional Pre-/Post-Skripte und Ketten hinzufügen + +Mit `Encoding starten` wird `confirm-encode` + Start ausgelöst. + +--- + +## 7) Encoding (`ENCODING`) + +Während Encoding: + +- Live-Fortschritt/ETA über WebSocket +- Pre-Encode-Ausführungen laufen vor HandBrake +- Post-Encode-Ausführungen laufen nach HandBrake + +Wichtig: + +- Pre-Encode-Fehler -> Job endet in `ERROR` +- Post-Encode-Fehler -> Job kann `FINISHED` bleiben, aber mit Fehlerhinweis im Status/Log + +--- + +## 8) Abschluss (`FINISHED`) + +Ergebnis: + +- Ausgabe in `movie_dir` (ggf. profilspezifisch) +- Job in Historie sichtbar +- Logs im konfigurierten `log_dir` + +--- + +## Nützliche API-Shortcuts ```bash -makemkvcon backup disc:0 /mnt/raw/Inception-2010-backup/ \ - --decrypt -r +# Pipeline-Snapshot +curl http://localhost:3001/api/pipeline/state + +# Queue-Snapshot +curl http://localhost:3001/api/pipeline/queue + +# Jobs +curl http://localhost:3001/api/history ``` - -**Live-Fortschritt** wird aus der MakeMKV-Ausgabe geparst: - -``` -PRGV:2048,0,65536 → Fortschritt wird berechnet und per WebSocket gesendet -PRGT:5011,0,"Sichern..." → Aktueller Task-Name -``` - -**Typische Dauer:** -- DVD: 20–45 Minuten -- Blu-ray: 45–120 Minuten - ---- - -## Schritt 7 – Track-Review → `READY_TO_ENCODE` - -Nach dem Ripping, nach Playlist-Übernahme oder direkt bei vorhandenem RAW startet der **HandBrake-Scan**: - -```bash -HandBrakeCLI --scan -i -t 0 -``` - -Dieser Scan liest alle Tracks aus ohne zu encodieren. Ripster baut daraus den Encode-Plan mit automatischer Vorauswahl: - -**Status: `MEDIAINFO_CHECK`** – läuft automatisch, kein Benutzereingriff - -Danach öffnet sich das **Encode-Review-Panel** (`READY_TO_ENCODE`): - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ Encode-Review │ -│ Titel: Disc Title 1 · Laufzeit: 2:28:05 · 28 Kapitel │ -├─────────────────────────────────────────────────────────────────┤ -│ Audio-Spuren │ -├──────┬─────────────────────────────┬───────────────────────────┤ -│ ☑ │ Track 1: English (AC3, 5.1) │ Copy (ac3) │ -│ ☑ │ Track 2: Deutsch (DTS, 5.1) │ Fallback Transcode (av_aac)│ -│ ☐ │ Track 3: Français (AC3, 2.0) │ Nicht übernommen │ -├──────┴─────────────────────────────┴───────────────────────────┤ -│ Untertitel-Spuren │ -├──────┬─────────────────────────────┬────────┬──────┬──────────┤ -│ ☑ │ Track 1: Deutsch │ Einbr.☐ │Forc.☐│Default☑ │ -│ ☐ │ Track 2: English │ Einbr.☐ │Forc.☐│Default☐ │ -├──────┴─────────────────────────────┴────────┴──────┴──────────┤ -│ [Encoding starten] │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### Audio-Track-Aktionen verstehen - -| Symbol/Text | Bedeutung | -|------------|-----------| -| `Copy (ac3)` | Track wird **verlustfrei** direkt übernommen | -| `Copy (truehd)` | TrueHD-Track wird direkt übernommen | -| `Transcode (av_aac)` | Track wird zu AAC umgewandelt | -| `Fallback Transcode (av_aac)` | Copy nicht möglich → automatisch zu AAC | -| `Preset-Default (HandBrake)` | HandBrake-Preset entscheidet | -| `Nicht übernommen` | Track ist nicht ausgewählt | - -### Untertitel-Flags - -| Flag | Bedeutung | -|------|-----------| -| **Einbrennen** | Untertitel werden fest ins Video gebrannt (nur ein Track möglich) | -| **Forced** | Nur erzwungene Untertitel-Einblendungen übernehmen | -| **Default** | Diese Spur wird beim Abspielen automatisch aktiviert | - -### Vorauswahl-Regeln - -Die Tracks mit `☑` wurden nach der Regel aus den Einstellungen automatisch vorausgewählt (`selectedByRule: true`). Die Auswahl kann frei geändert werden. - -Klicke **"Encoding starten"** (bzw. im Pre-Rip-Modus **"Backup + Encoding starten"**), um fortzufahren. -Falls die Auswahl noch nicht bestätigt wurde, übernimmt das Frontend die Bestätigung automatisch beim Start. - ---- - -## Schritt 8 – Encoding → `ENCODING` - -HandBrake startet mit dem finalisierten Plan: - -```bash -HandBrakeCLI \ - -i /dev/sr0 \ - -o "/mnt/movies/Inception (2010).mkv" \ - -t 1 \ - --preset "H.265 MKV 1080p30" \ - -a 1,2 \ - -E copy:ac3,av_aac \ - -s 1 \ - --subtitle-default 1 -``` - -**Live-Fortschritt** wird aus HandBrake-stderr geparst: - -``` -Encoding: task 1 of 1, 73.50 % (45.23 fps, avg 44.12 fps, ETA 00h12m34s) -``` - -Das Dashboard zeigt: -- Fortschrittsbalken (0–100 %) -- Aktuelle Encoding-Geschwindigkeit (FPS) -- Geschätzte Restzeit (ETA) - -**Typische Dauer (abhängig von CPU/GPU und Preset):** -- Schnelles Preset (`fast`): 0.5× Echtzeit -- Standard-Preset: 1–3× Echtzeit -- Langsames Preset (`slow`): 5–10× Echtzeit - ---- - -## Schritt 9 – Fertig! → `FINISHED` - -``` -/mnt/nas/movies/ -└── Inception (2010).mkv ✓ Encodierung abgeschlossen -``` - -- Job-Status in der Datenbank: `FINISHED` -- PushOver-Benachrichtigung (falls konfiguriert) -- Eintrag in der [History](http://localhost:5173/history) mit vollständigen Logs - ---- - -## Fehlerbehandlung - -### Job im Status `ERROR` - -1. **Dashboard**: Details-Button → Log-Ausgabe prüfen -2. **Retry**: Job vom Fehlerzustand neu starten (behält Metadaten) -3. **History**: Vollständige Logs und Fehlerdetails - -### Häufige Fehlerursachen - -| Fehler | Ursache | Lösung | -|-------|---------|--------| -| MakeMKV: Lizenzfehler | Abgelaufene Beta-Lizenz | Neue Lizenz im [MakeMKV-Forum](https://www.makemkv.com/forum/viewtopic.php?t=1053) | -| HandBrake: Preset nicht gefunden | Preset-Name falsch | `HandBrakeCLI --preset-list` prüfen | -| Keine Disc erkannt | Laufwerk-Berechtigungen | `sudo chmod a+rw /dev/sr0` | -| Falsches Video (zerstückelt) | Falsche Playlist | Job re-encodieren mit anderer Playlist | -| OMDb: Keine Ergebnisse | API-Key fehlt oder Titel nicht gefunden | Einstellungen prüfen; manuell eingeben | - ---- - -## Kurzübersicht aller Schritte - -| # | Status | Benutzeraktion | Was Ripster tut | -|--|--------|---------------|----------------| -| 1 | `IDLE` | Disc einlegen | Disc-Polling erkennt Disc | -| 2 | `DISC_DETECTED` | "Analyse starten" klicken | Job anlegen, OMDb vorsuchen | -| 3 | `METADATA_SELECTION` | Film im Dialog auswählen | Start automatisch einplanen/auslösen | -| 4 | `READY_TO_START` | meist keine | Übergangszustand vor Auto-Start | -| 5 | `RIPPING` | Warten | MakeMKV rippt, Fortschritt streamen | -| 6 | `MEDIAINFO_CHECK` | Warten | HandBrake-Scan, Encode-Plan bauen | -| 7 | `WAITING_FOR_USER_DECISION` (optional) | Playlist manuell wählen | Auf Bestätigung warten | -| 8 | `READY_TO_ENCODE` | Tracks prüfen + "Encoding starten" | Auswahl übernehmen, Start auslösen | -| 9 | `ENCODING` | Warten | HandBrake encodiert, inkl. Post-Skripte | -| 10 | `FINISHED` | — | Datei fertig, Benachrichtigung senden | diff --git a/docs/pipeline/encoding.md b/docs/pipeline/encoding.md index 9a8fa8f..040603f 100644 --- a/docs/pipeline/encoding.md +++ b/docs/pipeline/encoding.md @@ -1,329 +1,103 @@ # Encode-Planung & Track-Auswahl -`encodePlan.js` analysiert die HandBrake-Scan-Ausgabe, wählt Audio- und Untertitelspuren anhand von Regeln vor und erstellt einen vollständigen Encode-Plan für die Benutzer-Review. +Ripster erzeugt vor dem Encode einen `encodePlan` und lässt ihn im Review-Panel bestätigen. --- -## Ablauf im Pipeline-Kontext +## Ablauf -``` -RIPPING abgeschlossen (oder Pre-Rip-Scan) - ↓ -HandBrake --scan (alle Titel & Tracks einlesen) - ↓ -buildTrackSelectors() ← Regeln aus Einstellungen ableiten - ↓ -selectTrackIds() ← Tracks anhand Regeln vorauswählen - ↓ -resolveAudioEncoderAction() ← Encoder-Aktion pro Track bestimmen - ↓ -buildDiscScanReview() ← Vollständigen Encode-Plan erstellen - ↓ -READY_TO_ENCODE ← Benutzer-Review im Frontend - ↓ -applyManualTrackSelectionToPlan() ← Benutzer-Auswahl anwenden - ↓ -ENCODING ← HandBrake-CLI mit finalem Plan starten +```text +Quelle bestimmen (Disc/RAW) + -> HandBrake-Scan (--scan --json) + -> Plan erstellen (Titel, Audio, Untertitel) + -> READY_TO_ENCODE + -> Benutzer bestätigt Auswahl + -> finaler HandBrake-Aufruf ``` --- -## Phase 1: Pre-Rip Track-Scan +## Review-Inhalt (`READY_TO_ENCODE`) -Ripster führt einen **HandBrake-Scan** bereits **vor dem eigentlichen Ripping** durch: - -```bash -HandBrakeCLI --scan -i /dev/sr0 -t 0 -``` - -Dieser Scan liest alle Titel und deren Tracks aus der Disc (ohne zu encodieren). So kann der Benutzer die Track-Auswahl bereits vor dem zeitintensiven Rip-Prozess bestätigen. - -!!! info "Pre-Rip vs. Post-Rip" - Ob der Scan vor oder nach dem Ripping passiert, hängt vom konfigurierten Modus ab. Bei direktem Disc-Zugriff ist Pre-Rip möglich; nach einem MakeMKV-Backup wird die entstandene `.mkv`-Datei gescannt. +- auswählbarer Encode-Titel +- Audio-Track-Selektion +- Untertitel-Track-Selektion inkl. Flags + - `burnIn` + - `forced` + - `defaultTrack` +- optionale User-Presets (HandBrake-Preset + Extra-Args) +- optionale Pre-/Post-Skripte und Ketten --- -## Phase 2: Track-Selektor-Regeln (`buildTrackSelectors`) +## Bestätigung (`confirm-encode`) -Die Regeln werden aus den HandBrake-Einstellungen abgeleitet. Es gibt fünf **Selektionsmodi**: - -| Modus | Beschreibung | -|------|-------------| -| `none` | Keine Tracks dieser Art übernehmen | -| `first` | Nur den ersten Track übernehmen | -| `all` | Alle Tracks übernehmen | -| `language` | Nur Tracks in bestimmten Sprachen | -| `explicit` | Bestimmte Track-IDs explizit angeben | - -Der aktive Modus wird aus den `handbrake_*`-Einstellungen und `handbrake_extra_args` abgeleitet. Explizite CLI-Argumente (`--audio`, `--audio-lang-list`) überschreiben die Basis-Konfiguration. - ---- - -## Phase 3: Automatische Vorauswahl (`selectTrackIds`) - -### Audio-Tracks - -``` -Modus 'none' → Keine Audio-Tracks -Modus 'all' → Alle Tracks (oder nur erster, wenn firstOnly) -Modus 'language' → Alle Tracks in den konfigurierten Sprachen -Modus 'explicit' → Nur die angegebenen Track-IDs -Modus 'first' → Nur Track 1 -``` - -Jeder Audio-Track erhält das Feld `selectedByRule: true/false` – dieses zeigt dem Benutzer, welche Tracks automatisch vorausgewählt wurden. - -**Sprach-Normalisierung (`normalizeLanguage`):** - -Alle Sprachcodes werden auf **ISO 639-2** (3-Buchstaben) normalisiert: - -| Eingabe | Normalisiert | -|--------|-------------| -| `de`, `ger` | `deu` | -| `German` | `deu` | -| `en`, `eng` | `eng` | -| `English` | `eng` | -| `fr`, `fre` | `fra` | -| `ja`, `jpn` | `jpn` | -| Unbekannt | `und` | - -### Untertitel-Tracks - -Gleiche Modus-Logik wie Audio, aber mit **zusätzlichen Flags** pro Track: - -| Flag | Bedeutung | -|------|-----------| -| `burnIn` | Untertitel in Video einbrennen (`--subtitle-burned`) | -| `forced` | Nur erzwungene Untertitel übernehmen (`--subtitle-forced`) | -| `defaultTrack` | Als Standard-Untertitelspur markieren (`--subtitle-default`) | - -Diese Flags werden im Encode-Review als Checkboxen angezeigt. - ---- - -## Phase 4: Encoder-Aktion bestimmen (`resolveAudioEncoderAction`) - -Für jeden vorausgewählten Audio-Track bestimmt Ripster die Encoder-Aktion: - -``` -Encoder-Einstellung Codec-Support in Copy-Mask? Aktion -───────────────────────────────────────────────────────────────────── -Kein Encoder / 'preset-default' → preset-default HandBrake-Preset entscheidet -encoder.startsWith('copy') - UND Codec in audioCopyMask → copy Direktkopie (verlustfrei) - UND Codec NICHT in audioCopyMask→ fallback Transcode mit Fallback-Encoder -sonstiger Encoder → transcode Transcode mit explizitem Encoder -``` - -**Encoder-Aktionstypen:** - -| Typ | Label (UI) | Qualität | -|----|-----------|---------| -| `preset-default` | `Preset-Default (HandBrake)` | HandBrake entscheidet | -| `copy` | `Copy (ac3)` | Verlustfrei | -| `fallback` | `Fallback Transcode (av_aac)` | Mit Qualitätsverlust | -| `transcode` | `Transcode (av_aac)` | Mit Qualitätsverlust | - -**Copy-kompatible Codecs (Standard Copy-Mask):** - -| Codec | Encoder-String | -|-------|---------------| -| AC-3 | `copy:ac3` | -| E-AC-3 | `copy:eac3` | -| AAC | `copy:aac` | -| MP3 | `copy:mp3` | -| TrueHD | `copy:truehd` | -| DTS | `copy:dts` *(nur mit spez. HandBrake-Build)* | -| DTS-HD | `copy:dtshd` *(nur mit spez. HandBrake-Build)* | - -!!! warning "DTS im Standard-HandBrake" - Standard-HandBrake-Builds unterstützen kein DTS-Passthrough. DTS-Tracks werden dann automatisch auf den Fallback-Encoder umgestellt (Standard: `av_aac`). - ---- - -## Phase 5: Encode-Plan-Struktur - -Der vollständige Plan wird im Job-Datensatz als `encode_plan_json` gespeichert: +Typischer Payload: ```json { - "mode": "pre_rip", - "preRip": true, - "encodeInputTitleId": 1, - "encodeInputPath": "disc-track-scan://title-1", - "selectors": { - "audio": { "mode": "language", "languages": ["deu", "eng"], "copyMask": ["copy:ac3", "copy:eac3"] }, - "subtitle": { "mode": "none" } - }, - "titles": [ - { - "id": 1, - "fileName": "Disc Title 1", - "durationSeconds": 8885, - "selectedByMinLength": true, - "isEncodeInput": true, - "audioTracks": [ - { - "id": 1, - "sourceTrackId": 1, - "language": "eng", - "languageLabel": "English", - "title": "5.1 Surround", - "format": "AC3", - "codecToken": "ac3", - "channels": "6", - "selectedByRule": true, - "selectedForEncode": true, - "encodePreviewActions": [ - { "type": "copy", "encoder": "copy:ac3", "label": "Copy (ac3)" } - ], - "encodePreviewSummary": "Copy (ac3)" - }, - { - "id": 2, - "sourceTrackId": 2, - "language": "deu", - "languageLabel": "Deutsch", - "format": "DTS", - "codecToken": "dts", - "channels": "6", - "selectedByRule": true, - "selectedForEncode": true, - "encodePreviewActions": [ - { "type": "fallback", "encoder": "av_aac", "label": "Fallback Transcode (av_aac)" } - ], - "encodePreviewSummary": "Fallback Transcode (av_aac)" - }, - { - "id": 3, - "language": "fra", - "languageLabel": "Français", - "selectedByRule": false, - "selectedForEncode": false, - "encodePreviewSummary": "Nicht übernommen" - } - ], - "subtitleTracks": [ - { - "id": 1, - "language": "deu", - "selectedByRule": true, - "selectedForEncode": true, - "burnIn": false, - "forced": false, - "defaultTrack": true, - "subtitlePreviewSummary": "Übernehmen", - "subtitlePreviewFlags": ["default"] - } - ] - } - ] -} -``` - ---- - -## Phase 6: Benutzer-Review im Frontend (`MediaInfoReviewPanel`) - -Das Review-Panel zeigt: - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ Encode-Review Titel: Disc Title 1 │ -│ Laufzeit: 2:28:05 │ -├─────────────────────────────────────────────────────────────────┤ -│ Audio-Spuren │ -├──────┬──────────────────────────┬──────────────────────────────┤ -│ [✓] │ Track 1: English (AC3) │ Copy (ac3) │ -│ [✓] │ Track 2: Deutsch (DTS) │ Fallback Transcode (av_aac) │ -│ [ ] │ Track 3: Français (DTS) │ Nicht übernommen │ -├──────┴──────────────────────────┴──────────────────────────────┤ -│ Untertitel-Spuren │ -├──────┬──────────────────────────┬────────┬────────┬────────────┤ -│ [✓] │ Track 1: Deutsch │Einbr.[ ]│Forced[ ]│Default[✓]│ -│ [ ] │ Track 2: English │Einbr.[ ]│Forced[ ]│Default[ ]│ -├──────┴──────────────────────────┴────────┴────────┴────────────┤ -│ [Encoding starten] │ -└─────────────────────────────────────────────────────────────────┘ -``` - -Der Benutzer kann: -- **Audio-Tracks** per Checkbox aktivieren/deaktivieren -- **Untertitel-Flags** (Einbrennen, Forced, Default) setzen -- **Mehrere Titel** bei der Titleauswahl wechseln (für Discs mit mehreren Haupttiteln) - ---- - -## Phase 7: Benutzer-Auswahl anwenden (`applyManualTrackSelectionToPlan`) - -Im Frontend wird die Benutzer-Auswahl beim Klick auf **"Encoding starten"** (ggf. automatisch) bestätigt und dann auf den Plan angewendet: - -```json -Payload: { "selectedEncodeTitleId": 1, "selectedTrackSelection": { "1": { "audioTrackIds": [1, 2], - "subtitleTrackIds": [1] + "subtitleTrackIds": [3] } - } + }, + "selectedPreEncodeScriptIds": [1], + "selectedPostEncodeScriptIds": [2], + "selectedPreEncodeChainIds": [3], + "selectedPostEncodeChainIds": [4], + "selectedUserPresetId": 5 } ``` -Jeder Track erhält `selectedForEncode: true/false` entsprechend der Auswahl. Die Encoder-Aktionen (`encodeActions`) der nicht gewählten Tracks werden geleert. +Ripster speichert die bestätigte Auswahl in `jobs.encode_plan_json` und markiert `encode_review_confirmed = 1`. --- -## Phase 8: HandBrake-CLI-Befehl +## HandBrake-Aufruf -Aus dem finalisierten Plan baut Ripster den HandBrake-Aufruf: +Grundstruktur: ```bash HandBrakeCLI \ - -i /dev/sr0 \ - -o "/mnt/movies/Inception (2010).mkv" \ - -t 1 \ - --preset "H.265 MKV 1080p30" \ - -a 1,2 \ - -E copy:ac3,av_aac \ - -s 1 \ - --subtitle-default 1 + -i \ + -o \ + -t \ + -Z "" \ + \ + -a \ + -s ``` -| Argument | Quelle | -|---------|--------| -| `-i` | `encode_input_path` aus Job | -| `-o` | Ausgabepfad aus `filename_template` + `movie_dir` | -| `-t` | Gewählter Titel-Index | -| `-a` | Kommagetrennte Audio-Track-IDs der ausgewählten Tracks | -| `-E` | Kommagetrennte Encoder-Aktionen (eine pro Track, gleiche Reihenfolge wie `-a`) | -| `-s` | Kommagetrennte Untertitel-Track-IDs | -| `--subtitle-default` | Track-ID der als Default markierten Untertitelspur | -| `--preset` | `handbrake_preset`-Einstellung | -| Extras | `handbrake_extra_args`-Einstellung | +Untertitel-Flags werden bei Bedarf ergänzt: + +- `--subtitle-burned=` +- `--subtitle-default=` +- `--subtitle-forced=` oder `--subtitle-forced` --- -## Dateiname-Template +## Pre-/Post-Encode-Ausführungen -| Platzhalter | Wert | Beispiel | -|------------|------|---------| -| `{title}` | Filmtitel von OMDb | `Inception` | -| `{year}` | Erscheinungsjahr | `2010` | -| `{imdb_id}` | IMDb-ID | `tt1375666` | -| `{type}` | `movie` oder `series` | `movie` | +- Pre-Encode läuft vor HandBrake +- Post-Encode läuft nach HandBrake -Sonderzeichen (`:`, `/`, `?`, `*` etc.) werden automatisch aus dem Dateinamen entfernt. +Verhalten bei Fehlern: + +- Pre-Encode-Fehler: Job wird als `ERROR` beendet (Encode startet nicht) +- Post-Encode-Fehler: Job kann `FINISHED` bleiben, enthält aber Fehlerhinweis/Script-Summary --- -## Re-Encoding +## Dateinamen/Ordner -Ein abgeschlossener Job kann ohne erneutes Ripping neu encodiert werden: +Der finale Outputpfad wird aus Settings-Templates aufgebaut. -1. Job in der **History** öffnen -2. **"Re-Encode"** klicken -3. Track-Auswahl anpassen (oder bestehende übernehmen) -4. Encoding startet mit den aktuellen `handbrake_*`-Einstellungen +Platzhalter: -Nützlich bei geänderten Presets, anderen Sprach-Präferenzen oder nach einem Einstellungs-Update. +- `${title}` +- `${year}` +- `${imdbId}` + +Ungültige Dateizeichen werden sanitisiert. diff --git a/docs/pipeline/index.md b/docs/pipeline/index.md index 0844339..e18c035 100644 --- a/docs/pipeline/index.md +++ b/docs/pipeline/index.md @@ -1,6 +1,6 @@ # Pipeline -Der Pipeline-Abschnitt beschreibt den Kern-Workflow von Ripster. +Der Pipeline-Bereich beschreibt den Kern-Workflow von Ripster.
@@ -8,7 +8,7 @@ Der Pipeline-Abschnitt beschreibt den Kern-Workflow von Ripster. --- - Der vollständige Ripping-Workflow mit allen Zustandsübergängen. + Zustände, Übergänge und Queue-Verhalten. [:octicons-arrow-right-24: Workflow](workflow.md) @@ -16,7 +16,7 @@ Der Pipeline-Abschnitt beschreibt den Kern-Workflow von Ripster. --- - Wie Ripster Audio- und Untertitel-Tracks analysiert und Encode-Pläne erstellt. + Wie Titel/Tracks für HandBrake vorbereitet und bestätigt werden. [:octicons-arrow-right-24: Encoding](encoding.md) @@ -24,16 +24,16 @@ Der Pipeline-Abschnitt beschreibt den Kern-Workflow von Ripster. --- - Erkennung von Blu-ray Playlist-Obfuskierung und Auswahl der korrekten Playlist. + Bewertung mehrdeutiger Blu-ray-Playlists und manuelle Entscheidung. [:octicons-arrow-right-24: Playlist-Analyse](playlist-analysis.md) -- :material-script-text: **Post-Encode-Skripte** +- :material-script-text: **Encode-Skripte (Pre & Post)** --- - Automatische Ausführung von Shell-Skripten nach erfolgreichem Encoding – z. B. zum Verschieben oder Benachrichtigen. + Skripte/Ketten vor und nach dem Encode ausführen. - [:octicons-arrow-right-24: Post-Encode-Skripte](post-encode-scripts.md) + [:octicons-arrow-right-24: Encode-Skripte](post-encode-scripts.md)
diff --git a/docs/pipeline/playlist-analysis.md b/docs/pipeline/playlist-analysis.md index 6f2324c..555549b 100644 --- a/docs/pipeline/playlist-analysis.md +++ b/docs/pipeline/playlist-analysis.md @@ -1,217 +1,65 @@ # Playlist-Analyse -Einige Blu-rays verwenden **Playlist-Obfuskierung** als Kopierschutz. Ripster analysiert automatisch alle MakeMKV-Titel und empfiehlt die korrekte Playlist – auf Basis eines Segment-Scoring-Algorithmus aus `playlistAnalysis.js`. +Ripster analysiert bei Blu-ray-ähnlichen Quellen Playlists und fordert bei Mehrdeutigkeit eine manuelle Auswahl an. --- -## Das Problem: Playlist-Obfuskierung +## Ziel -Moderne Blu-rays können Dutzende bis Hunderte von Titeln/Playlists enthalten. Der eigentliche Film steckt in genau einer davon – alle anderen sind: - -- **Kurze Dummy-Titel** (wenige Sekunden bis Minuten) -- **Titel mit verschachtelten Segmenten** (absichtlich versetzte Reihenfolge, sodass der Film falsch gerippt wird) -- **Titel gleicher Länge** (mehrere Playlists mit identischer Laufzeit, aber unterschiedlicher Segment-Reihenfolge) - -Das Ziel der Obfuskierung: Ein einfacher Ripper wählt den erstbesten langen Titel – und bekommt ein zerstückeltes, unbrauchbares Video. +Erkennen, welche Playlist wahrscheinlich der Hauptfilm ist, statt versehentlich eine Fake-/Dummy-Playlist zu verwenden. --- -## Wann wird die Analyse ausgelöst? +## Eingabedaten -Die Playlist-Analyse wird automatisch gestartet **sobald der Benutzer Metadaten bestätigt** (nach dem Metadaten-Dialog). Ripster ruft `makemkvcon` im Info-Modus auf und parst die TINFO-Ausgabe. - -``` -TINFO:,26,"" -``` - -Feld **26** enthält die kommagetrennte Liste der Segment-Nummern in der Abspielreihenfolge des Titels. +Die Analyse basiert auf MakeMKV-Infos (u. a. Playlist-/Segment-Struktur, Laufzeiten, Titelzuordnung). --- -## Algorithmus im Detail (`playlistAnalysis.js`) +## Auswertung (vereinfacht) -### Schritt 1 – Segment-Nummern parsen +Für Kandidaten werden u. a. berücksichtigt: -``` -TINFO:1,26,"00000,00001,00002,00003" → [0, 1, 2, 3] linearer Film -TINFO:2,26,"00100,00050,00100,00051" → [100, 50, 100, 51] Fake-Playlist -``` +- Laufzeit +- Segment-Reihenfolge +- Rückwärtssprünge/große Sprünge +- Kohärenz linearer Segmentfolgen +- Duplikatgruppen mit ähnlicher Laufzeit -### Schritt 2 – Metriken berechnen (`computeSegmentMetrics`) +Daraus entstehen: -Für jedes aufeinanderfolgende Segment-Paar `[a, b]` wird `diff = b − a` berechnet: - -| Metrik | Bedingung | Bedeutung | -|--------|----------|-----------| -| `directSequenceSteps` | `diff == 1` | Aufeinanderfolgende Segmente → linearer Film | -| `backwardJumps` | `b < a` | Rückwärtssprünge → verdächtig | -| `largeJumps` | `\|diff\| > 20` | Große Sprünge → verdächtig | -| `alternatingPairs` | Große Sprünge mit **wechselndem Vorzeichen** | Hin-und-her-Muster → starker Fake-Indikator | - -**Score-Formel:** - -``` -score = (directSequenceSteps × 2) − (backwardJumps × 3) − (largeJumps × 2) -``` - -**Konkrete Beispiele:** - -| Segmentfolge | directSeq | backward | large | score | Ergebnis | -|-------------|-----------|----------|-------|-------|---------| -| `0,1,2,3,4,5` | 5 | 0 | 0 | +10 | Echter Film | -| `0,1,100,2,101,3` | 2 | 0 | 4 | -4 | Verdächtig | -| `50,10,60,11,70,12` | 0 | 3 | 3 | -15 | Fake | - -### Schritt 3 – Bewertungslabel vergeben (`buildEvaluationLabel`) - -``` -alternatingRatio = alternatingPairs / largeJumps - -if alternatingRatio >= 0.55 AND alternatingPairs >= 3: - → "Fake-Struktur (alternierendes Sprungmuster)" - -else if backwardJumps > 0 OR largeJumps > 0: - → "Auffällige Segmentreihenfolge" - -else: - → "wahrscheinlich korrekt (lineare Segmentfolge)" -``` - -### Schritt 4 – Duplikat-Gruppen bilden (`buildSimilarityGroups`) - -Alle Titel werden nach **ähnlicher Laufzeit** gruppiert (±90 Sekunden Toleranz). Gibt es mehrere Kandidaten mit ähnlicher Laufzeit, ist das ein klares Zeichen für Obfuskierung: - -``` -8 Titel mit ~148 Minuten Laufzeit → Duplikat-Gruppe -→ obfuscationDetected = true -``` - -### Schritt 5 – Besten Kandidaten empfehlen (`scoreCandidates`) - -Innerhalb der größten Duplikat-Gruppe werden alle Kandidaten sortiert nach: - -1. `score` (höher = besser) -2. `sequenceCoherence` (Anteil linearer Segmentschritte) -3. Laufzeit (länger = besser) -4. Dateigröße (größer = besser als Tiebreaker) - -Der **erste Kandidat** der sortierten Liste ist die Empfehlung. - -### Schritt 6 – Entscheidung erzwingen bei mehreren Kandidaten - -Sobald nach `MIN_LENGTH_MINUTES` **mehr als eine** Playlist übrig bleibt, wird immer eine manuelle Auswahl verlangt: - -``` -candidateCount > 1 → manualDecisionRequired = true -candidateCount <= 1 → manualDecisionRequired = false -``` +- `candidates` +- `evaluatedCandidates` (inkl. Score/Label) +- `recommendation` +- `manualDecisionRequired` --- -## Wann greift der Benutzer ein? +## Wann muss der Benutzer entscheiden? -``` -obfuscationDetected = duplicateDurationGroups.length > 0 -manualDecisionRequired = candidates.length > 1 -``` +Wenn nach Filterung mehr als ein relevanter Kandidat übrig bleibt, setzt Ripster `manualDecisionRequired = true` und wechselt auf: -| Ergebnis | Nächster Pipeline-Zustand | Aktion | -|---------|--------------------------|--------| -| Nur ein Kandidat nach Mindestlänge | `READY_TO_START` | Automatische Übernahme möglich | -| Mehrere Kandidaten nach Mindestlänge | `WAITING_FOR_USER_DECISION` | Benutzer muss Playlist auswählen | +- `WAITING_FOR_USER_DECISION` + +Dann muss eine Playlist bestätigt werden, bevor der Workflow weiterläuft. --- -## Benutzeroberfläche: Playlist-Auswahl-Dialog +## Konfigurationseinfluss -Wenn `manualDecisionRequired = true`, öffnet sich der Playlist-Dialog **nach** dem Metadaten-Dialog: +| Key | Wirkung | +|-----|---------| +| `makemkv_min_length_minutes` | Mindestlaufzeit für Kandidaten | -``` -┌───────────────────────────────────────────────────────────────────┐ -│ Playlist-Auswahl │ -├──────────┬──────────┬──────────┬────────────────────────────────┤ -│ Playlist │ Laufzeit │ Score │ Bewertung │ -├──────────┼──────────┼──────────┼────────────────────────────────┤ -│ ★ 00800 │ 2:28:05 │ +18 │ wahrscheinlich korrekt │ -│ │ │ │ (lineare Segmentfolge) │ -├──────────┼──────────┼──────────┼────────────────────────────────┤ -│ 00801 │ 2:28:12 │ −4 │ Auffällige Segmentreihenfolge │ -├──────────┼──────────┼──────────┼────────────────────────────────┤ -│ 00900 │ 2:28:05 │ −32 │ Fake-Struktur │ -│ │ │ │ (alternierendes Sprungmuster) │ -└──────────┴──────────┴──────────┴────────────────────────────────┘ - Hinweis: 847 Playlists insgesamt. 3 relevante Kandidaten (≥ 15 min). - Empfehlung: 00800 (★) -``` - -- **★** markiert die empfohlene Playlist (vorausgewählt) -- Nur Titel ≥ `makemkv_min_length_minutes` erscheinen in der Liste -- Der Benutzer wählt per Radio-Button und klickt "Bestätigen" -- Erst nach dieser Bestätigung wechselt die Pipeline zu `READY_TO_START` +Default ist aktuell `60` Minuten. --- -## Vollständige Datenstruktur (`analyzeContext.playlistAnalysis`) +## UI-Verhalten -```json -{ - "titles": [ - { "titleId": 1, "playlistId": "00800", "durationSeconds": 8885, "durationLabel": "2:28:05", "chapters": 28 } - ], - "candidates": [ - { "titleId": 1, "playlistId": "00800", "durationSeconds": 8885 }, - { "titleId": 2, "playlistId": "00801", "durationSeconds": 8892 } - ], - "evaluatedCandidates": [ - { - "titleId": 1, - "playlistId": "00800", - "score": 18, - "sequenceCoherence": 0.95, - "evaluationLabel": "wahrscheinlich korrekt (lineare Segmentfolge)", - "metrics": { - "directSequenceSteps": 12, - "backwardJumps": 0, - "largeJumps": 1, - "alternatingPairs": 0 - } - } - ], - "duplicateDurationGroups": [ - [ - { "titleId": 1, "playlistId": "00800" }, - { "titleId": 2, "playlistId": "00801" } - ] - ], - "recommendation": { - "titleId": 1, - "playlistId": "00800", - "score": 18, - "reason": "Höchster Segment-Score in der größten Laufzeit-Gruppe" - }, - "obfuscationDetected": true, - "manualDecisionRequired": true -} -``` +Bei manueller Entscheidung zeigt das Dashboard Kandidaten inkl. Score/Bewertung und markiert eine Empfehlung. ---- +Nach Bestätigung: -## Konfiguration - -| Einstellung | Standard | Wirkung | -|------------|---------|---------| -| `makemkv_min_length_minutes` | `15` | Titel kürzer als dieser Wert werden als Kandidaten ignoriert | - ---- - -## Tipps bei Fehlempfehlung - -!!! tip "Falsche Playlist gewählt?" - Wenn das resultierende Video zerstückelt ist: - - 1. Job in der **History** öffnen - 2. **Re-Encode** starten – diesmal eine andere Playlist wählen - 3. Alternativ: Korrekte Playlist im [MakeMKV-Forum](https://www.makemkv.com/forum/) recherchieren - -!!! info "Keine Segment-Daten verfügbar" - Bei DVDs oder älteren Blu-rays liefert MakeMKV manchmal keine Segmentinfos (TINFO-Feld 26 fehlt). In diesem Fall entfällt die Analyse und der erste Titel über der Mindestlänge wird automatisch verwendet. +- mit vorhandenem RAW -> zurück zu `MEDIAINFO_CHECK` +- ohne RAW -> Startpfad über `READY_TO_START`/`RIPPING` diff --git a/docs/pipeline/post-encode-scripts.md b/docs/pipeline/post-encode-scripts.md index 5da7a67..10137fa 100644 --- a/docs/pipeline/post-encode-scripts.md +++ b/docs/pipeline/post-encode-scripts.md @@ -1,173 +1,70 @@ # Encode-Skripte (Pre & Post) -Ripster unterstützt **Pre-Encode-** und **Post-Encode-Ausführungen**: Beliebige Shell-Skripte oder Skript-Ketten können automatisch vor und/oder nach dem Encoding-Schritt laufen – z. B. zum Vorbereiten von Verzeichnissen, Verschieben von Dateien oder Benachrichtigen externer Dienste. +Ripster kann Skripte und Skript-Ketten vor und nach dem Encode ausführen. --- -## Funktionsweise +## Ablauf -``` +```text READY_TO_ENCODE - ↓ -[Pre-Encode-Ausführungen] ← Fehler? → Abbruch - Skript/Kette 1, 2, … - ↓ -ENCODING - ↓ -[Post-Encode-Ausführungen] ← Fehler? → Abbruch - Skript/Kette 1, 2, … - ↓ -FINISHED -``` - -!!! warning "Abbruch bei Fehler" - Schlägt eine Ausführung fehl (Exit-Code ≠ 0), werden alle nachfolgenden Ausführungen der gleichen Phase **nicht mehr ausgeführt**. - Der Job bleibt im Abschlusszustand `FINISHED`; der Fehler wird in Log/Status-Text und im Summary festgehalten. - ---- - -## Skript- und Ketten-Verwaltung - -Skripte und Skript-Ketten werden über die **Einstellungen-Seite** angelegt und verwaltet. Die Reihenfolge in der Liste kann per **Drag & Drop** geändert werden und bleibt persistent gespeichert. - -### Skript anlegen - -Navigiere zu **Einstellungen → Skripte** und klicke **"Neues Skript"**: - -| Feld | Beschreibung | -|------|-------------| -| **Name** | Anzeigename des Skripts (z. B. `Zu Plex verschieben`) | -| **Befehl** | Shell-Befehl oder Skriptpfad (z. B. `/home/michael/scripts/move-to-plex.sh`) | -| **Beschreibung** | Optionale Erklärung | - -### Skript-Ketten - -Eine **Skript-Kette** fasst mehrere Skripte zu einer benannten Einheit zusammen, die als ganzes ausgewählt werden kann. Nützlich für wiederkehrende Kombinationen (z. B. „Move + Notify Plex + Webhook"). Ketten werden genauso wie einzelne Skripte im Review-Panel ausgewählt. - -### Verfügbare Umgebungsvariablen - -Jedes Skript wird mit folgenden Umgebungsvariablen aufgerufen: - -| Variable | Inhalt | Beispiel | -|---------|--------|---------| -| `RIPSTER_OUTPUT_PATH` | Absoluter Pfad der encodierten Datei | `/mnt/movies/Inception (2010).mkv` | -| `RIPSTER_JOB_ID` | Job-ID in der Datenbank | `42` | -| `RIPSTER_TITLE` | Filmtitel | `Inception` | -| `RIPSTER_YEAR` | Erscheinungsjahr | `2010` | -| `RIPSTER_IMDB_ID` | IMDb-ID | `tt1375666` | -| `RIPSTER_RAW_PATH` | Pfad zur Raw-MKV-Datei | `/mnt/raw/Inception-2010/t00.mkv` | - -### Beispiel-Skript: Datei nach Jellyfin verschieben - -```bash -#!/bin/bash -# /home/michael/scripts/move-to-jellyfin.sh - -TARGET_DIR="/mnt/media/movies" -mkdir -p "$TARGET_DIR" -mv "$RIPSTER_OUTPUT_PATH" "$TARGET_DIR/" -echo "Verschoben: $RIPSTER_TITLE nach $TARGET_DIR" -``` - -### Beispiel-Skript: Webhook auslösen - -```bash -#!/bin/bash -# /home/michael/scripts/notify-webhook.sh - -curl -s -X POST https://mein-webhook.example.com/ripster \ - -H "Content-Type: application/json" \ - -d "{\"title\": \"$RIPSTER_TITLE\", \"year\": \"$RIPSTER_YEAR\", \"path\": \"$RIPSTER_OUTPUT_PATH\"}" + -> Pre-Encode Skripte/Ketten + -> HandBrake Encoding + -> Post-Encode Skripte/Ketten + -> FINISHED oder ERROR ``` --- -## Im Encode-Review auswählen +## Auswahl im Review -Im `READY_TO_ENCODE`-Zustand zeigt das **MediaInfoReviewPanel** zwei Abschnitte: +Im Review-Panel kannst du getrennt wählen: -``` -┌──────────────────────────────────────────────────────────┐ -│ Pre-Encode Ausführungen (optional) │ -├──────────────────────────────────────────────────────────┤ -│ ≡ 1. Verzeichnis vorbereiten (Skript) [Entfernen]│ -├──────────────────────────────────────────────────────────┤ -│ Hinzufügen: [Skript/Kette auswählen ▾] [+ Hinzuf.]│ -└──────────────────────────────────────────────────────────┘ - -┌──────────────────────────────────────────────────────────┐ -│ Post-Encode Ausführungen (optional) │ -├──────────────────────────────────────────────────────────┤ -│ ≡ 1. Zu Plex verschieben (Skript) [Entfernen]│ -│ ≡ 2. Notify-Kette (Kette) [Entfernen]│ -├──────────────────────────────────────────────────────────┤ -│ Hinzufügen: [Skript/Kette auswählen ▾] [+ Hinzuf.]│ -└──────────────────────────────────────────────────────────┘ -``` - -- **Pre-Encode** und **Post-Encode** werden separat konfiguriert -- Sowohl **einzelne Skripte** als auch **Skript-Ketten** können in beiden Phasen ausgewählt werden -- **Reihenfolge** per Drag & Drop innerhalb jeder Phase ändern -- **Hinzufügen** aus der Dropdown-Liste aller konfigurierten Skripte und Ketten -- **Entfernen** einzelner Einträge -- Auswahl kann pro Job frei variiert werden +- `selectedPreEncodeScriptIds` +- `selectedPostEncodeScriptIds` +- `selectedPreEncodeChainIds` +- `selectedPostEncodeChainIds` --- -## Skript testen +## Fehlerverhalten -Über die Einstellungen kann jedes Skript mit einem Test-Job ausgeführt werden: - -```http -POST /api/settings/scripts/:scriptId/test -``` - -Der Test-Aufruf befüllt die Umgebungsvariablen mit Platzhalter-Werten. +- Pre-Encode-Fehler stoppen die Kette und führen zu `ERROR`. +- Post-Encode-Fehler stoppen die restlichen Post-Schritte; Job kann dennoch `FINISHED` sein (mit Fehlerzusatz im Status/Log). --- -## Ausführungs-Ergebnis +## Verfügbare Umgebungsvariablen -Das Ergebnis der Skript-Ausführung wird im Job-Datensatz gespeichert und in der History angezeigt: +Beim Script-Run werden gesetzt: -```json -{ - "postEncodeScripts": { - "configured": 2, - "attempted": 2, - "succeeded": 2, - "failed": 0, - "skipped": 0, - "aborted": false, - "results": [ - { - "scriptId": 1, - "scriptName": "Zu Plex verschieben", - "status": "SUCCESS" - }, - { - "scriptId": 2, - "scriptName": "Webhook auslösen", - "status": "SUCCESS" - } - ] - } -} -``` - -| Feld | Beschreibung | -|------|-------------| -| `configured` | Anzahl ausgewählter Skripte | -| `attempted` | Anzahl tatsächlich gestarteter Skripte | -| `succeeded` | Erfolgreich ausgeführt (Exit-Code 0) | -| `failed` | Fehlgeschlagen | -| `skipped` | Nicht ausgeführt (wegen vorherigem Fehler) | -| `aborted` | `true`, wenn die Kette abgebrochen wurde | +- `RIPSTER_SCRIPT_RUN_AT` +- `RIPSTER_JOB_ID` +- `RIPSTER_JOB_TITLE` +- `RIPSTER_MODE` +- `RIPSTER_INPUT_PATH` +- `RIPSTER_OUTPUT_PATH` +- `RIPSTER_RAW_PATH` +- `RIPSTER_SCRIPT_ID` +- `RIPSTER_SCRIPT_NAME` +- `RIPSTER_SCRIPT_SOURCE` --- -## API-Referenz +## Skript-Ketten -Eine vollständige API-Dokumentation der Skript-Endpunkte findest du unter: +Ketten unterstützen zwei Step-Typen: -[:octicons-arrow-right-24: Settings API – Skripte](../api/settings.md#skript-verwaltung) +- `script` (führt ein hinterlegtes Skript aus) +- `wait` (wartet `waitSeconds`) + +Bei Fehler in einem Script-Step wird die Kette abgebrochen. + +--- + +## Testläufe + +- Skript testen: `POST /api/settings/scripts/:id/test` +- Kette testen: `POST /api/settings/script-chains/:id/test` + +Ergebnisse enthalten Erfolg/Exit-Code, Laufzeit und stdout/stderr. diff --git a/docs/pipeline/workflow.md b/docs/pipeline/workflow.md index 7899b35..f98e766 100644 --- a/docs/pipeline/workflow.md +++ b/docs/pipeline/workflow.md @@ -1,329 +1,87 @@ # Workflow & Zustände -Der Ripping-Workflow von Ripster ist als **State Machine** implementiert. Jeder Zustand hat klar definierte Übergangsbedingungen und Aktionen. +Ripster steuert den Ablauf als State-Machine im `pipelineService`. --- -## Zustandsdiagramm - -
+## Zustandsdiagramm (vereinfacht) ```mermaid flowchart LR - START(( )) --> IDLE - - IDLE -->|Disc erkannt| DD[DISC_DETECTED] - DD -->|Analyse starten| META[METADATA\nSELECTION] - - META -->|Metadaten übernommen| RTS[READY_TO\nSTART] - META -->|vorhandenes RAW +\nPlaylist offen| WUD[WAITING_FOR\nUSER_DECISION] - - RTS -->|Auto-Start| RIP[RIPPING] - RTS -->|Auto-Start mit RAW| MIC[MEDIAINFO\nCHECK] - RIP -->|MKV fertig| MIC - RIP -->|Fehler| ERR - RIP -->|Abbruch| CAN([CANCELLED]) - - MIC -->|Playlist offen (Backup)| WUD - WUD -->|Playlist bestätigt| MIC - WUD -->|Playlist bestätigt,\nnoch kein RAW| RTS - MIC --> RTE[READY_TO\nENCODE] - RTE -->|Encoding starten\n(bestätigt bei Bedarf automatisch)| ENC[ENCODING] - - ENC -->|inkl. Post-Skripte| FIN([FINISHED]) - ENC -->|Fehler| ERR - ENC -->|Abbruch| CAN - - ERR([ERROR]) -->|Retry / Cancel| IDLE - CAN -->|Retry / Neu-Analyse| IDLE - FIN -->|Neue Disc| IDLE - - style FIN fill:#e8f5e9,stroke:#66bb6a,color:#2e7d32 - style ERR fill:#ffebee,stroke:#ef5350,color:#c62828 - style CAN fill:#fff3e0,stroke:#fb8c00,color:#e65100 - style WUD fill:#fff8e1,stroke:#ffa726,color:#e65100 - style ENC fill:#f3e5f5,stroke:#ab47bc,color:#6a1b9a - style RIP fill:#e3f2fd,stroke:#42a5f5,color:#1565c0 - style MIC fill:#e3f2fd,stroke:#42a5f5,color:#1565c0 + IDLE --> DISC_DETECTED + DISC_DETECTED --> ANALYZING + ANALYZING --> METADATA_SELECTION + METADATA_SELECTION --> READY_TO_START + READY_TO_START --> RIPPING + READY_TO_START --> MEDIAINFO_CHECK + MEDIAINFO_CHECK --> WAITING_FOR_USER_DECISION + WAITING_FOR_USER_DECISION --> MEDIAINFO_CHECK + MEDIAINFO_CHECK --> READY_TO_ENCODE + READY_TO_ENCODE --> ENCODING + ENCODING --> FINISHED + ENCODING --> ERROR + RIPPING --> ERROR + RIPPING --> CANCELLED ``` -
+--- + +## State-Liste + +| State | Bedeutung | +|------|-----------| +| `IDLE` | Wartet auf Disc | +| `DISC_DETECTED` | Disc erkannt | +| `ANALYZING` | MakeMKV-Analyse läuft | +| `METADATA_SELECTION` | Benutzer wählt Metadaten | +| `WAITING_FOR_USER_DECISION` | Playlist-Auswahl nötig | +| `READY_TO_START` | Übergangszustand vor Start | +| `RIPPING` | MakeMKV-Rip läuft | +| `MEDIAINFO_CHECK` | Quelle/Tracks werden ausgewertet | +| `READY_TO_ENCODE` | Review ist bereit | +| `ENCODING` | HandBrake läuft | +| `FINISHED` | erfolgreich abgeschlossen | +| `CANCELLED` | abgebrochen | +| `ERROR` | fehlgeschlagen | --- -## UI-Badge-Bezeichnungen +## Typische Pfade -Die Status-Badges im Dashboard verwenden diese Labels: +### Standardfall (kein vorhandenes RAW) -| State | Badge-Label | -|------|-------------| -| `IDLE` | `Bereit` | -| `DISC_DETECTED` | `Medium erkannt` | -| `METADATA_SELECTION` | `Metadatenauswahl` | -| `WAITING_FOR_USER_DECISION` | `Warte auf Auswahl` | -| `READY_TO_START` | `Startbereit` | -| `RIPPING` | `Rippen` | -| `MEDIAINFO_CHECK` | `Mediainfo-Pruefung` | -| `READY_TO_ENCODE` | `Bereit zum Encodieren` | -| `ENCODING` | `Encodieren` | -| `FINISHED` | `Fertig` | -| `CANCELLED` | `Abgebrochen` | -| `ERROR` | `Fehler` | -| Queue (kein eigener State) | `In der Queue` | +1. Disc erkannt +2. Analyse + Metadaten +3. `RIPPING` +4. `MEDIAINFO_CHECK` +5. `READY_TO_ENCODE` +6. `ENCODING` +7. `FINISHED` + +### Vorhandenes RAW + +`READY_TO_START` springt direkt zu `MEDIAINFO_CHECK` (kein neuer Rip). + +### Mehrdeutige Blu-ray-Playlist + +`MEDIAINFO_CHECK` -> `WAITING_FOR_USER_DECISION` bis Benutzer Playlist bestätigt. --- -## Zustandsbeschreibungen +## Queue-Verhalten -### IDLE +Wenn `pipeline_max_parallel_jobs` erreicht ist: -**Ausgangszustand.** Ripster wartet auf eine Disc. - -- `diskDetectionService` pollt das Laufwerk im konfigurierten Intervall -- Bei Disc-Erkennung: automatischer Übergang zu `DISC_DETECTED` -- WebSocket-Event: `DISC_DETECTED` +- Job-Aktionen werden als Queue-Einträge abgelegt +- Queue kann zusätzlich Nicht-Job-Einträge enthalten (`script`, `chain`, `wait`) +- Reihenfolge ist per API/UI änderbar --- -### DISC_DETECTED +## Abbruch, Retry, Restart -**Disc erkannt, wartet auf Benutzeraktion.** - -- Dashboard-Badge: **"Medium erkannt"** -- Status-Text: **"Neue Disk erkannt"** -- **"Analyse starten"**-Button wird aktiv -- Kein Prozess läuft noch - -**Übergang:** Benutzer klickt "Analyse starten" → `METADATA_SELECTION` - ---- - -### METADATA_SELECTION - -**Metadaten-Auswahl läuft.** - -1. Job wird erstellt (`status = METADATA_SELECTION`) -2. OMDb-Vorsuche mit erkanntem Disc-Label -3. `MetadataSelectionDialog` öffnet sich mit vorgeladenen Ergebnissen -4. Benutzer wählt Filmtitel (oder gibt manuell ein) -5. Nach Bestätigung wird der Job automatisch für Start/Queue vorbereitet (`selectMetadata` + `startPreparedJob`) - -**Übergang (automatisch nach Metadaten-Bestätigung):** - -| Ergebnis | Nächster Zustand | -|--------------------|-----------------| -| Kein verwertbares RAW vorhanden | `READY_TO_START` → automatisch `RIPPING` (oder Queue) | -| Verwertbares RAW vorhanden | `READY_TO_START` → automatisch `MEDIAINFO_CHECK` (oder Queue) | -| Vorhandenes RAW + offene Playlist-Entscheidung | `WAITING_FOR_USER_DECISION` | - ---- - -### WAITING_FOR_USER_DECISION - -**Playlist-Obfuskierung erkannt – manuelle Auswahl erforderlich.** - -!!! info "Neu seit „Skript Integration + UI Anpassungen"" - Dieser Zustand wurde eingeführt, um Blu-rays mit mehreren Playlists ähnlicher Länge korrekt zu behandeln. - -- Playlist-Auswahl-Dialog wird im Dashboard angezeigt -- Alle Kandidaten mit Score, Laufzeit und Bewertungslabel -- Empfohlene Playlist ist vorausgewählt -- Benutzer bestätigt mit **"Playlist übernehmen"** -- Tritt häufig nach `MEDIAINFO_CHECK` auf (Backup-Analyse), seltener direkt nach `METADATA_SELECTION` bei vorhandenem RAW - -**Darstellung im Dashboard:** - -``` -┌──────────────────────────────────────────────────────────┐ -│ Playlist-Auswahl erforderlich │ -│ Es wurden mehrere Titel mit ähnlicher Laufzeit gefunden. │ -├──────────┬──────────┬────────┬──────────────────────────┤ -│ Playlist │ Laufzeit │ Score │ Bewertung │ -├──────────┼──────────┼────────┼──────────────────────────┤ -│ ● 00800 │ 2:28:05 │ +18 │ wahrscheinlich korrekt │ -│ ○ 00801 │ 2:28:12 │ −4 │ Auffällige Segmentfolge │ -│ ○ 00900 │ 2:28:05 │ −32 │ Fake-Struktur │ -└──────────┴──────────┴────────┴──────────────────────────┘ - [Playlist übernehmen] -``` - -**Übergang:** `selectMetadata(jobId, { selectedPlaylist })` setzt die Pipeline automatisch fort: - -- mit vorhandenem RAW nach `MEDIAINFO_CHECK` -- ohne RAW über `READY_TO_START` weiter Richtung `RIPPING` - -Mehr Details: [Playlist-Analyse](playlist-analysis.md) - ---- - -### READY_TO_START - -**Übergangs-/Fallback-Zustand vor dem eigentlichen Start.** - -- Wird nach Metadaten-Bestätigung kurz gesetzt -- `startPreparedJob()` wird danach automatisch ausgeführt -- Wenn Parallel-Limit erreicht ist, wird der Start stattdessen in die Queue eingereiht -- **"Job starten"** ist primär für Sonderfälle/Fallback sichtbar - -**Sonderfall – RAW-Datei bereits vorhanden:** -Wenn für diesen Job bereits ein verwertbares RAW unter `raw_dir` existiert, wird Ripping übersprungen und direkt `MEDIAINFO_CHECK` gestartet. - -**Übergang:** `startPreparedJob(jobId)` → `RIPPING` oder direkt `MEDIAINFO_CHECK` - ---- - -### RIPPING - -**MakeMKV rippt die Disc.** - -=== "MKV-Modus (Standard)" - - ```bash - makemkvcon mkv disc:0 all /path/to/raw/ --minlength=900 -r - ``` - - Erstellt MKV-Datei(en) direkt aus den gewählten Titeln. - -=== "Backup-Modus" - - ```bash - makemkvcon backup disc:0 /path/to/raw/backup/ --decrypt -r - ``` - - Erstellt vollständiges Disc-Backup inkl. Menüs. - -**Live-Updates** aus MakeMKV-Ausgabe: - -``` -PRGV:2048,0,65536 → Fortschritt-Berechnung -PRGT:5011,0,"..." → Aktueller Task-Name -``` - -**Typische Dauer:** DVD 20–45 min · Blu-ray 45–120 min - ---- - -### MEDIAINFO_CHECK - -**HandBrake-Scan und Encode-Plan-Erstellung.** - -Dieser Zustand umfasst je nach Quelle mehrere Phasen: - -1. Optional: Playlist-Auflösung bei Blu-ray-Backup (inkl. MakeMKV/HandBrake-Zuordnung) -2. **HandBrake-Scan** (`HandBrakeCLI --scan`) auf RAW-Input -3. **Encode-Plan-Erstellung** mit automatischer Track-Vorauswahl - -Kein Benutzereingriff – läuft automatisch durch. - -**Übergänge:** - -- Eindeutige Quelle/Titelwahl möglich → `READY_TO_ENCODE` -- Mehrdeutige Playlist erkannt → `WAITING_FOR_USER_DECISION` - ---- - -### READY_TO_ENCODE - -**Encode-Plan bereit.** - -Das `MediaInfoReviewPanel` zeigt: - -- **Titel-Auswahl** (bei Discs mit mehreren langen Titeln) -- **Audio-Tracks** mit Encoder-Vorschau (Copy/Transcode/Fallback) -- **Untertitel-Tracks** mit Flags (Einbrennen, Forced, Default) -- **Post-Encode-Skripte** – Auswahl und Reihenfolge der auszuführenden Skripte - -Im Frontend startet **"Encoding starten"** (bzw. **"Backup + Encoding starten"** im Pre-Rip-Modus) den nächsten Schritt. -Falls die Review noch nicht bestätigt wurde, wird `confirmEncodeReview(...)` automatisch vor dem Start aufgerufen. - -**Übergang:** `startPreparedJob(jobId)` → `ENCODING` (oder im Pre-Rip-Fall zuerst `RIPPING`) - ---- - -### ENCODING - -**HandBrake encodiert die Datei.** - -```bash -HandBrakeCLI \ - -i -o \ - -t \ - --preset "H.265 MKV 1080p30" \ - -a 1,2 -E copy:ac3,av_aac \ - -s 1 --subtitle-default 1 -``` - -**Live-Updates** aus HandBrake-stderr: - -``` -Encoding: task 1 of 1, 73.50 % (45.23 fps, avg 44.12 fps, ETA 00h12m34s) -``` - -Post-Encode-Skripte werden innerhalb dieses Zustands sequenziell ausgeführt (kein separater Pipeline-State). - -!!! note "Skriptfehler" - Skriptfehler führen zum Abbruch der Skriptkette, der Job bleibt jedoch im Abschlusszustand `FINISHED` mit entsprechendem Hinweis im Status-Text/Log. - ---- - -### FINISHED - -**Job erfolgreich abgeschlossen.** - -- Ausgabedatei liegt im konfigurierten `movie_dir` -- Job-Status in Datenbank: `FINISHED` -- PushOver-Benachrichtigung (falls konfiguriert) -- WebSocket-Event: `PIPELINE_STATE_CHANGED` (State `FINISHED`) - ---- - -### CANCELLED - -**Job wurde vom Benutzer abgebrochen.** - -- Entsteht bei aktivem Abbruch (`/api/pipeline/cancel`) während laufender Phase -- Job-Status in Datenbank: `CANCELLED` -- Im Dashboard stehen danach u. a. `Retry Rippen`, `Review neu starten` oder `Encode neu starten` (kontextabhängig) zur Verfügung - ---- - -### ERROR - -**Fehler aufgetreten.** - -- Fehlerdetails im Job-Datensatz gespeichert -- Fehler-Logs in History abrufbar -- **Retry**: Neustart vom Fehlerzustand -- **Neu analysieren**: Disc erneut als neuer Job starten - ---- - -## Abbrechen & Retry - -### Pipeline abbrechen - -```http -POST /api/pipeline/cancel -``` - -- SIGINT → graceful exit (Timeout: 10 s) → SIGKILL -- Laufender Job landet in `CANCELLED` (oder Queue-Eintrag wird entfernt, falls noch nicht gestartet) - -### Job wiederholen - -```http -POST /api/pipeline/retry/:jobId -``` - -- Startet den Job neu in `RIPPING` (oder reiht den Retry in die Queue ein) -- Metadaten bleiben erhalten; Encode-/Scan-Daten werden neu erzeugt - -### Re-Encode - -```http -POST /api/pipeline/reencode/:jobId -``` - -- Encodiert bestehende Raw-MKV neu -- Ermöglicht neue Track-Auswahl und andere Skripte -- Kein Ripping erforderlich +- `cancel`: laufenden Job abbrechen oder Queue-Eintrag entfernen +- `retry`: Fehler-/Abbruch-Job neu starten +- `reencode`: aus vorhandenem RAW neu encodieren +- `restart-review`: Review aus RAW neu aufbauen +- `restart-encode`: Encoding mit letzter bestätigter Auswahl neu starten diff --git a/docs/tools/handbrake.md b/docs/tools/handbrake.md index 257d583..0d6c0d4 100644 --- a/docs/tools/handbrake.md +++ b/docs/tools/handbrake.md @@ -1,137 +1,69 @@ # HandBrake -HandBrake encodiert die rohen MKV-Dateien in das gewünschte Format. Ripster nutzt `HandBrakeCLI`. +Ripster verwendet `HandBrakeCLI` für Scan und Encode. --- -## Verwendeter Befehl +## Verwendete Aufrufe + +### Scan (Review-Aufbau) + +```bash +HandBrakeCLI --scan --json -i -t 0 +``` + +### Encode (vereinfacht) ```bash HandBrakeCLI \ - --input "/mnt/raw/Film_t00.mkv" \ - --output "/mnt/movies/Film (2010).mkv" \ - --preset "H.265 MKV 1080p30" \ - --audio 1,2 \ - --aencoder copy:ac3,ffaac \ - --subtitle 1 \ - --subtitle-default 1 + -i \ + -o \ + -t \ + -Z "" \ + \ + -a \ + -s ``` +Optional ergänzt Ripster: + +- `--subtitle-burned=` +- `--subtitle-default=` +- `--subtitle-forced=` oder `--subtitle-forced` + --- -## Presets +## Presets auslesen -HandBrake verwendet **Presets** für vorkonfigurierte Encoding-Einstellungen. - -### Empfohlene Presets - -| Preset | Codec | Auflösung | Für | -|--------|-------|----------|-----| -| `H.265 MKV 1080p30` | HEVC/H.265 | 1080p | Beste Qualität/Größe | -| `H.265 MKV 720p30` | HEVC/H.265 | 720p | Kleinere Dateien | -| `H.264 MKV 1080p30` | AVC/H.264 | 1080p | Breiteste Kompatibilität | -| `HQ 1080p30 Surround` | HEVC/H.265 | 1080p | Hohe Qualität mit Surround | - -### Alle Presets anzeigen +Ripster liest Presets mit: ```bash -HandBrakeCLI --preset-list +HandBrakeCLI -z ``` --- -## Audio-Encoding +## Relevante Settings -### Copy-kompatible Codecs - -HandBrake kann folgende Codecs direkt kopieren (kein Qualitätsverlust): - -| Codec | `--aencoder` Wert | -|-------|-----------------| -| AC-3 | `copy:ac3` | -| AAC | `copy:aac` | -| MP3 | `copy:mp3` | -| TrueHD | `copy:truehd` | -| E-AC-3 | `copy:eac3` | - -### Transcoding - -Codecs die nicht kopiert werden können, werden zu AAC transcodiert: - -| Original | Transcodiert zu | -|---------|----------------| -| DTS | AAC (`ffaac`) | -| DTS-HD | AAC (`ffaac`) | - ---- - -## Extra-Argumente - -Über die Einstellung `handbrake_extra_args` können beliebige HandBrake-Argumente hinzugefügt werden: - -``` -# Cropping deaktivieren ---crop 0:0:0:0 - -# Loose Anamorphic ---loose-anamorphic - -# Bestimmte Qualität setzen ---quality 20 -``` +| Key | Bedeutung | +|-----|-----------| +| `handbrake_command` | CLI-Binary | +| `handbrake_preset_bluray` / `handbrake_preset_dvd` | profilspezifisches Preset | +| `handbrake_extra_args_bluray` / `handbrake_extra_args_dvd` | profilspezifische Zusatzargumente | +| `output_extension_bluray` / `output_extension_dvd` | Ausgabeformat | +| `handbrake_restart_delete_incomplete_output` | unvollständige Ausgabe bei Neustart löschen | --- ## Fortschritts-Parsing -Ripster parst die HandBrake-Ausgabe auf stderr für die Fortschrittsanzeige: - -``` -Encoding: task 1 of 1, 73.50 % (45.23 fps, avg 44.12 fps, ETA 00h12m34s) -``` - -`progressParsers.js` extrahiert: -- Prozentzahl -- Aktuelle FPS -- ETA - ---- - -## Konfiguration in Ripster - -| Einstellung | Beschreibung | -|------------|-------------| -| `handbrake_command` | Pfad/Befehl für `HandBrakeCLI` | -| `handbrake_preset` | Preset-Name | -| `handbrake_extra_args` | Zusätzliche CLI-Argumente | -| `output_extension` | Dateiendung der Ausgabe | +Ripster parst HandBrake-Stderr (Prozent/ETA/Detail) und sendet WebSocket-Progress (`PIPELINE_PROGRESS`). --- ## Troubleshooting -### HandBrake findet Preset nicht +- Preset nicht gefunden: Preset-Namen mit `HandBrakeCLI -z` prüfen +- sehr langsames Encoding: Preset/Extra-Args prüfen (z. B. `--encoder-preset`) -```bash -# Preset-Liste anzeigen -HandBrakeCLI --preset-list 2>&1 | grep -i "h.265" -``` - -Preset-Namen sind case-sensitive! - -### Encoding sehr langsam - -```bash -# CPU-Encoding-Preset anpassen (schneller = schlechtere Qualität) -handbrake_extra_args = --encoder-preset fast -``` - -Verfügbare Presets: `ultrafast`, `superfast`, `veryfast`, `faster`, `fast`, `medium`, `slow`, `slower`, `veryslow` - -### GPU-Encoding nutzen (NVIDIA) - -``` -handbrake_preset = H.265 NVENC 1080p -``` - -Erfordert HandBrake-Build mit NVENC-Unterstützung und NVIDIA-GPU. +Das Produktions-Installer-Script `install.sh` bietet eine Option zur Installation eines gebündelten HandBrakeCLI-Binaries mit NVDEC-Unterstützung (NVIDIA GPU-Dekodierung). Diese Option erscheint interaktiv während der Installation. diff --git a/docs/tools/makemkv.md b/docs/tools/makemkv.md index 646b4b5..faa31f0 100644 --- a/docs/tools/makemkv.md +++ b/docs/tools/makemkv.md @@ -1,160 +1,61 @@ # MakeMKV -MakeMKV analysiert und rippt DVDs und Blu-rays. Ripster nutzt `makemkvcon` (die CLI-Version). +Ripster nutzt `makemkvcon` für Disc-Analyse und Rip. --- -## Verwendete Befehle +## Verwendete Aufrufe -### Disc-Analyse +### Analyse ```bash -makemkvcon -r --cache=1 info disc:0 +makemkvcon -r info ``` -Gibt alle Titel und Playlists der eingelegten Disc aus. Ripster parst diese Ausgabe um die verfügbaren Tracks und Playlists zu bestimmen. +`` ist typischerweise: -**Parameter:** -- `-r` – Maschinen-lesbares Ausgabeformat -- `--cache=1` – Minimaler Disc-Cache -- `info disc:0` – Informationsabfrage für erstes Laufwerk +- `disc:` (Auto-Modus) +- `dev:/dev/sr0` (explicit) +- `file:` (Datei/Ordner-Analyse) -### MKV-Modus (Standard) +### Rip (MKV-Modus) ```bash -makemkvcon mkv disc:0 all /path/to/raw/ \ - --minlength=900 \ - -r +makemkvcon mkv [--minlength=...] [...extraArgs] ``` -Erstellt MKV-Dateien aus allen Titeln, die länger als 15 Minuten sind. - -**Parameter:** -- `mkv` – MKV-Ausgabemodus -- `disc:0` – Erstes Disc-Laufwerk -- `all` – Alle passenden Titel (nicht nur einen bestimmten) -- `--minlength=900` – Mindestlänge in Sekunden (entspricht 15 Minuten) - -### Backup-Modus +### Rip (Backup-Modus) ```bash -makemkvcon backup disc:0 /path/to/raw/backup/ \ - --decrypt \ - -r -``` - -Erstellt ein vollständiges Disc-Backup mit Menüs. - -**Parameter:** -- `backup` – Backup-Modus -- `--decrypt` – Verschlüsselung entfernen - ---- - -## Ausgabeformat - -MakeMKV gibt Fortschritt und Status in einem strukturierten Format aus: - -``` -PRGV:current,total,max → Fortschrittsbalken-Werte -PRGT:code,id,"Beschreibung" → Aktueller Task -PRGC:code,id,"Beschreibung" → Aktueller Sub-Task -MSG:code,flags,count,"Text" → Nachricht -``` - -Ripster's `progressParsers.js` parst diese Ausgabe für die Live-Fortschrittsanzeige. - ---- - -## LibDriveIO-Modus (Pflicht) - -!!! danger "Laufwerk muss im LibDriveIO-Modus betrieben werden" - MakeMKV greift auf Discs über **LibDriveIO** zu – eine Bibliothek, die direkt auf Rohdaten des Laufwerks zugreift und den Standard-OS-Treiber umgeht. Ohne diesen Modus kann MakeMKV verschlüsselte Blu-rays (insbesondere UHD) **nicht lesen**. - -### Was ist LibDriveIO? - -LibDriveIO ist MakeMKVs interne Treiberschicht für den direkten Laufwerkszugriff. Sie ermöglicht: - -- Lesen von verschlüsselten Blu-ray-Sektoren (AACS, BD+, AACS2) -- Zugriff auf Disc-Strukturen, die über Standard-OS-APIs nicht erreichbar sind -- UHD-Blu-ray-Entschlüsselung ohne externe Bibliotheken - -### Voraussetzungen für den LibDriveIO-Modus - -Das Laufwerk muss **LibDriveIO-kompatibel** sein und entsprechend betrieben werden: - -1. **Kompatibles Laufwerk** – Nicht alle Laufwerke unterstützen den Rohdatenzugriff. UHD-kompatible Laufwerke (z. B. LG, Pioneer bestimmter Firmware-Versionen) sind erforderlich. - -2. **Laufwerk-Berechtigungen** – Der Prozess benötigt direkten Zugriff auf das Blockdevice: - ```bash - sudo chmod a+rw /dev/sr0 - # oder dauerhaft über udev-Regel - ``` - -3. **Kein OS-seitiger Disc-Mount** – Das Laufwerk darf beim Ripping **nicht** durch das OS automatisch gemountet sein (AutoMount deaktivieren): - ```bash - # Automount temporär deaktivieren (GNOME) - gsettings set org.gnome.desktop.media-handling automount false - ``` - -### How-To: LibDriveIO einrichten - -Die vollständige Anleitung zur Einrichtung und zu kompatiblen Laufwerken findet sich im offiziellen MakeMKV-Forum: - -[:octicons-link-external-24: MakeMKV Forum – LibDriveIO How-To](https://www.makemkv.com/forum/viewtopic.php?t=18856){ .md-button } - -!!! tip "Prüfen ob LibDriveIO aktiv ist" - In der MakeMKV-Ausgabe erscheint beim Laufwerkszugriff `LibDriveIO` statt `LibMMMBD`, wenn der direkte Modus aktiv ist. - ---- - -## MakeMKV-Lizenz - -MakeMKV ist **Beta-Software** und kostenlos für den persönlichen Gebrauch während der Beta-Phase. Eine Beta-Lizenz ist regelmäßig im [MakeMKV-Forum](https://www.makemkv.com/forum/viewtopic.php?t=1053) verfügbar. - -Ohne gültige Lizenz können Blu-rays nicht entschlüsselt werden. - -### Lizenz eintragen - -Die Lizenz wird in den MakeMKV-Einstellungen eingetragen (GUI) oder direkt in: - -``` -~/.MakeMKV/settings.conf -``` - -``` -app_Key = "XXXX-XXXX-XXXX-XXXX-XXXX" +makemkvcon backup --decrypt ``` --- -## Konfiguration in Ripster +## Registrierungsschlüssel (optional) -| Einstellung | Beschreibung | -|------------|-------------| -| `makemkv_command` | Pfad/Befehl für `makemkvcon` | -| `makemkv_min_length_minutes` | Mindest-Titellänge (Standard: 15 Min) | -| `makemkv_backup_mode` | Backup-Modus statt MKV | - ---- - -## Troubleshooting - -### MakeMKV erkennt Disc nicht +Wenn `makemkv_registration_key` gesetzt ist, führt Ripster vor Analyse/Rip aus: ```bash -# Laufwerk-Berechtigungen prüfen -ls -la /dev/sr0 -sudo chmod a+rw /dev/sr0 - -# Oder Benutzer zur Gruppe cdrom hinzufügen -sudo usermod -a -G cdrom $USER +makemkvcon reg ``` -### Langer Analyseprozess +--- -Blu-ray-Analyse kann bei Discs mit vielen Playlists 5+ Minuten dauern. Dies ist normal. +## Relevante Settings -### Fehlermeldung: "LibMMBD" +| Key | Bedeutung | +|-----|-----------| +| `makemkv_command` | CLI-Binary | +| `makemkv_source_index` | Source-Index im Auto-Modus | +| `makemkv_min_length_minutes` | Mindestlaufzeitfilter | +| `makemkv_rip_mode_bluray` / `makemkv_rip_mode_dvd` | `mkv` oder `backup` | +| `makemkv_analyze_extra_args_bluray` / `_dvd` | Zusatzargs Analyse | +| `makemkv_rip_extra_args_bluray` / `_dvd` | Zusatzargs Rip | -LibMMBD ist MakeMKVs interne Verschlüsselungsbibliothek. Bei Fehlern die MakeMKV-Version aktualisieren. +--- + +## Hinweise + +- Blu-ray-Backups werden oft für robuste Playlist-Analyse genutzt. +- MakeMKV-Ausgaben werden geparst und als `makemkvInfo` im Job gespeichert. diff --git a/docs/tools/mediainfo.md b/docs/tools/mediainfo.md index 89ca3ab..cfddb1b 100644 --- a/docs/tools/mediainfo.md +++ b/docs/tools/mediainfo.md @@ -1,108 +1,37 @@ # MediaInfo -MediaInfo analysiert die Track-Struktur von Mediendateien. Ripster nutzt es nach dem Ripping um Audio- und Untertitelspuren zu identifizieren. +Ripster nutzt `mediainfo` zur JSON-Analyse von Medien-Dateien. --- -## Verwendeter Befehl +## Aufruf ```bash -mediainfo --Output=JSON /path/to/raw/film.mkv +mediainfo --Output=JSON ``` -Gibt vollständige Track-Informationen als JSON zurück. +Der Input ist typischerweise eine RAW-Datei oder ein vom Workflow gewählter Inputpfad. --- -## Ausgabe-Struktur +## Verwendung in Ripster -```json -{ - "media": { - "track": [ - { - "@type": "General", - "Duration": "8885.042", - "Format": "Matroska" - }, - { - "@type": "Video", - "Format": "HEVC", - "Width": "1920", - "Height": "1080", - "FrameRate": "23.976" - }, - { - "@type": "Audio", - "StreamOrder": "1", - "Format": "TrueHD", - "Channels": "8", - "Language": "en" - }, - { - "@type": "Audio", - "StreamOrder": "2", - "Format": "AC-3", - "Channels": "6", - "Language": "de" - }, - { - "@type": "Text", - "StreamOrder": "1", - "Format": "UTF-8", - "Language": "de" - } - ] - } -} -``` +- Track-/Codec-Metadaten für Review-Plan +- Fallback-Informationen in bestimmten Analysepfaden +- Persistenz als `mediainfoInfo` im Job --- -## Verarbeitung in Ripster +## Relevante Settings -`encodePlan.js` verarbeitet die MediaInfo-Ausgabe: - -1. **Track-Extraktion**: Alle Audio- und Untertitel-Tracks werden extrahiert -2. **Sprach-Normalisierung**: Sprachcodes werden auf ISO 639-3 normalisiert -3. **Codec-Klassifizierung**: Bestimmt ob Codec kopiert oder transcodiert werden kann -4. **Track-Labels**: Benutzerfreundliche Bezeichnungen (z.B. "Deutsch (AC-3, 5.1)") - -### Track-Label-Format - -``` -{Sprache} ({Format}, {Kanäle}) -``` - -Beispiele: -- `Deutsch (AC-3, 5.1)` -- `English (TrueHD, 7.1)` -- `Français (AC-3, 2.0)` - ---- - -## Konfiguration in Ripster - -| Einstellung | Beschreibung | -|------------|-------------| -| `mediainfo_command` | Pfad/Befehl für `mediainfo` | +| Key | Bedeutung | +|-----|-----------| +| `mediainfo_command` | CLI-Binary | +| `mediainfo_extra_args_bluray` / `_dvd` | profilspezifische Zusatzargumente | --- ## Troubleshooting -### MediaInfo gibt kein JSON aus - -```bash -# Version prüfen -mediainfo --Version - -# JSON-Ausgabe testen -mediainfo --Output=JSON /path/to/test.mkv -``` - -MediaInfo >= 17.10 wird empfohlen. - -### Sprache als "und" angezeigt - -`und` steht für "undetermined" – die Sprache ist in der MKV-Datei nicht getaggt. Dies ist bei manchen Rips normal. Der Track wird trotzdem angezeigt und kann manuell ausgewählt werden. +- JSON-Test: `mediainfo --Output=JSON ` +- unbekannte Sprache erscheint oft als `und` (undetermined) diff --git a/frontend/src/components/MediaInfoReviewPanel.jsx b/frontend/src/components/MediaInfoReviewPanel.jsx index 5c6bcce..c9ecc69 100644 --- a/frontend/src/components/MediaInfoReviewPanel.jsx +++ b/frontend/src/components/MediaInfoReviewPanel.jsx @@ -65,6 +65,23 @@ function isBurnedSubtitleTrack(track) { ); } +function isForcedOnlySubtitleTrack(track) { + const summary = `${track?.title || ''} ${track?.description || ''} ${track?.languageLabel || ''}`.toLowerCase(); + return Boolean( + track?.forcedTrack + || /forced only/.test(summary) + || /nur erzwungen/.test(summary) + || /\berzwungen\b/.test(summary) + ); +} + +function hasForcedSubtitleAvailable(track) { + const sourceTrackIds = normalizeTrackIdList( + Array.isArray(track?.forcedSourceTrackIds) ? track.forcedSourceTrackIds : [] + ); + return Boolean(track?.forcedAvailable || sourceTrackIds.length > 0); +} + function splitArgs(input) { if (!input || typeof input !== 'string') { return []; @@ -601,6 +618,8 @@ function TrackList({ const displayAudioTitle = audioChannelLabel(track.channels); const audioVariant = type === 'audio' ? extractAudioVariant(displayHint) : ''; const disabled = !allowSelection || (type === 'subtitle' && burned); + const forcedOnlyTrack = type === 'subtitle' ? isForcedOnlySubtitleTrack(track) : false; + const forcedAvailable = type === 'subtitle' ? hasForcedSubtitleAvailable(track) : false; let displayText = `#${track.id} | ${displayLanguage} | ${displayCodec}`; if (type === 'audio') { @@ -616,6 +635,10 @@ function TrackList({ } if (type === 'subtitle' && burned) { displayText += ' | burned'; + } else if (type === 'subtitle' && forcedOnlyTrack) { + displayText += ' | forced-only'; + } else if (type === 'subtitle' && forcedAvailable) { + displayText += ' | forced verfügbar'; } return ( @@ -1074,7 +1097,6 @@ export default function MediaInfoReviewPanel({ allowTrackSelection && allowTitleSelection && titleChecked - && titleEligible ); return ( @@ -1090,7 +1112,7 @@ export default function MediaInfoReviewPanel({ onSelectEncodeTitle(normalizeTitleId(title.id)); }} readOnly={!allowTitleSelection} - disabled={!allowTitleSelection || !titleEligible} + disabled={!allowTitleSelection} /> #{title.id} | {title.fileName} | {formatDuration(title.durationMinutes)} | {formatBytes(title.sizeBytes)} diff --git a/frontend/src/components/PipelineStatusCard.jsx b/frontend/src/components/PipelineStatusCard.jsx index c72b60d..fc9df16 100644 --- a/frontend/src/components/PipelineStatusCard.jsx +++ b/frontend/src/components/PipelineStatusCard.jsx @@ -24,6 +24,18 @@ function normalizePlaylistId(value) { return match ? String(match[1]).padStart(5, '0') : null; } +function formatDurationClock(seconds) { + const total = Number(seconds || 0); + if (!Number.isFinite(total) || total <= 0) { + return null; + } + const rounded = Math.max(0, Math.trunc(total)); + const h = Math.floor(rounded / 3600); + const m = Math.floor((rounded % 3600) / 60); + const s = rounded % 60; + return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; +} + function normalizeTrackId(value) { const parsed = Number(value); if (!Number.isFinite(parsed) || parsed <= 0) { @@ -376,10 +388,18 @@ export default function PipelineStatusCard({ item?.structuralMetrics?.sequenceCoherence ?? item?.sequenceCoherence ); const handBrakeTitleId = Number(item?.handBrakeTitleId); + const durationSecondsRaw = Number(item?.durationSeconds ?? item?.duration ?? 0); + const durationSeconds = Number.isFinite(durationSecondsRaw) && durationSecondsRaw > 0 + ? Math.trunc(durationSecondsRaw) + : 0; + const durationLabelRaw = String(item?.durationLabel || '').trim(); + const durationLabel = durationLabelRaw || formatDurationClock(durationSeconds); return { playlistId, playlistFile, titleId: Number.isFinite(Number(item?.titleId)) ? Number(item.titleId) : null, + durationSeconds, + durationLabel: durationLabel || null, score: Number.isFinite(score) ? score : null, evaluationLabel: item?.evaluationLabel || null, segmentCommand: item?.segmentCommand @@ -688,6 +708,7 @@ export default function PipelineStatusCard({ {row.playlistFile} {row.titleId !== null ? ` | Titel #${row.titleId}` : ''} + {row.durationLabel ? ` | Dauer ${row.durationLabel}` : ''} {row.score !== null ? ` | Score ${row.score}` : ''} {row.recommended ? ' | empfohlen' : ''} @@ -757,7 +778,7 @@ export default function PipelineStatusCard({ {(state === 'READY_TO_ENCODE' || state === 'MEDIAINFO_CHECK' || mediaInfoReview) ? (

Titel-/Spurprüfung

- {state === 'READY_TO_ENCODE' && !reviewConfirmed && !queueLocked ? ( + {state === 'READY_TO_ENCODE' && !queueLocked ? ( {isPreRipReview ? 'Spurauswahl kann direkt übernommen werden. Beim Klick auf "Backup + Encoding starten" wird automatisch bestätigt und gestartet.' @@ -770,9 +791,9 @@ export default function PipelineStatusCard({ presetDisplayValue={presetDisplayValue} commandOutputPath={commandOutputPath} selectedEncodeTitleId={normalizeTitleId(selectedEncodeTitleId)} - allowTitleSelection={state === 'READY_TO_ENCODE' && !reviewConfirmed && !queueLocked} + allowTitleSelection={state === 'READY_TO_ENCODE' && !queueLocked} onSelectEncodeTitle={(titleId) => setSelectedEncodeTitleId(normalizeTitleId(titleId))} - allowTrackSelection={state === 'READY_TO_ENCODE' && !reviewConfirmed && !queueLocked} + allowTrackSelection={state === 'READY_TO_ENCODE' && !queueLocked} trackSelectionByTitle={trackSelectionByTitle} onTrackSelectionChange={(titleId, trackType, trackId, checked) => { const normalizedTitleId = normalizeTitleId(titleId); @@ -808,7 +829,7 @@ export default function PipelineStatusCard({ userPresets={filteredUserPresets} selectedUserPresetId={selectedUserPresetId} onUserPresetChange={(presetId) => setSelectedUserPresetId(presetId)} - allowEncodeItemSelection={state === 'READY_TO_ENCODE' && !reviewConfirmed && !queueLocked} + allowEncodeItemSelection={state === 'READY_TO_ENCODE' && !queueLocked} onAddPreEncodeItem={(itemType) => { setPreEncodeItems((prev) => { const current = Array.isArray(prev) ? prev : []; diff --git a/install.sh b/install.sh index 868fecc..787ee26 100755 --- a/install.sh +++ b/install.sh @@ -392,7 +392,7 @@ else ok "Benutzer '$SERVICE_USER' angelegt" fi -for grp in cdrom optical disk; do +for grp in cdrom optical disk video render; do if getent group "$grp" &>/dev/null; then usermod -aG "$grp" "$SERVICE_USER" 2>/dev/null || true info "Benutzer '$SERVICE_USER' zur Gruppe '$grp' hinzugefügt" @@ -541,6 +541,16 @@ StandardOutput=journal StandardError=journal SyslogIdentifier=ripster-backend +# Device-Zugriff fuer GPU und CD-ROM +DeviceAllow=/dev/sr0 rw +DeviceAllow=/dev/nvidia0 rw +DeviceAllow=/dev/nvidiactl rw +DeviceAllow=/dev/nvidia-uvm rw +DeviceAllow=/dev/nvidia-uvm-tools rw +DeviceAllow=/dev/dri/renderD128 rw +DeviceAllow=/dev/dri/renderD129 rw +SupplementaryGroups=video render cdrom disk + NoNewPrivileges=true ProtectSystem=full ProtectHome=read-only diff --git a/site/404.html b/site/404.html index a8d4da4..23596d9 100644 --- a/site/404.html +++ b/site/404.html @@ -1 +1 @@ - Ripster

404 - Not found

\ No newline at end of file + Ripster

404 - Not found

\ No newline at end of file diff --git a/site/api/crons/index.html b/site/api/crons/index.html new file mode 100644 index 0000000..47214d2 --- /dev/null +++ b/site/api/crons/index.html @@ -0,0 +1,56 @@ + Cron API - Ripster

Cron API

Ripster enthält ein eingebautes Cron-System für Skripte und Skript-Ketten (sourceType: script|chain).


GET /api/crons

Listet alle Cron-Jobs.

{
+  "jobs": [
+    {
+      "id": 1,
+      "name": "Nachtlauf Backup",
+      "cronExpression": "0 2 * * *",
+      "sourceType": "script",
+      "sourceId": 3,
+      "sourceName": "Backup-Skript",
+      "enabled": true,
+      "pushoverEnabled": true,
+      "lastRunAt": "2026-03-10T02:00:00.000Z",
+      "lastRunStatus": "success",
+      "nextRunAt": "2026-03-11T02:00:00.000Z",
+      "createdAt": "2026-03-01T10:00:00.000Z",
+      "updatedAt": "2026-03-10T02:00:05.000Z"
+    }
+  ]
+}
+

POST /api/crons

Erstellt Cron-Job.

{
+  "name": "Nachtlauf Backup",
+  "cronExpression": "0 2 * * *",
+  "sourceType": "script",
+  "sourceId": 3,
+  "enabled": true,
+  "pushoverEnabled": true
+}
+

Response: 201 mit { "job": { ... } }


GET /api/crons/:id

Response:

{ "job": { "id": 1, "name": "..." } }
+

PUT /api/crons/:id

Aktualisiert Cron-Job. Felder wie bei POST.

Response:

{ "job": { ... } }
+

DELETE /api/crons/:id

Response:

{ "removed": { "id": 1, "name": "Nachtlauf Backup" } }
+

GET /api/crons/:id/logs

Liefert Ausführungs-Logs.

Query-Parameter:

Parameter Typ Default Beschreibung
limit number 20 Anzahl Einträge, max. 100

Response:

{
+  "logs": [
+    {
+      "id": 42,
+      "cronJobId": 1,
+      "startedAt": "2026-03-10T02:00:01.000Z",
+      "finishedAt": "2026-03-10T02:00:05.000Z",
+      "status": "success",
+      "output": "Backup abgeschlossen.",
+      "errorMessage": null
+    }
+  ]
+}
+

status: running | success | error


POST /api/crons/:id/run

Triggert Job manuell (asynchron).

Response:

{ "triggered": true, "cronJobId": 1 }
+

Wenn Job bereits läuft: 409.


POST /api/crons/validate-expression

Validiert 5-Felder-Cron-Ausdruck und berechnet nächsten Lauf.

Request:

{ "cronExpression": "*/15 * * * *" }
+

Gültige Response:

{
+  "valid": true,
+  "nextRunAt": "2026-03-10T14:15:00.000Z"
+}
+

Ungültige Response:

{
+  "valid": false,
+  "error": "Cron-Ausdruck muss genau 5 Felder haben (Minute Stunde Tag Monat Wochentag).",
+  "nextRunAt": null
+}
+

Cron-Format

Ripster unterstützt 5 Felder:

Minute Stunde Tag Monat Wochentag
+

Beispiele:

  • 0 2 * * * täglich 02:00
  • */15 * * * * alle 15 Minuten
  • 0 6 * * 1-5 Mo-Fr 06:00

WebSocket-Events zu Cron

  • CRON_JOBS_UPDATED bei Create/Update/Delete
  • CRON_JOB_UPDATED bei Laufzeitstatus (running -> success|error)
\ No newline at end of file diff --git a/site/api/history/index.html b/site/api/history/index.html index 4e41492..c337cc4 100644 --- a/site/api/history/index.html +++ b/site/api/history/index.html @@ -1,80 +1,99 @@ - History API - Ripster

History API

Endpunkte für die Job-Histoire, Dateimanagement und Orphan-Import.


GET /api/history

Gibt eine Liste aller Jobs zurück, optional gefiltert.

Query-Parameter:

Parameter Typ Beschreibung
status string Filtert nach Status (z.B. FINISHED, ERROR)
search string Sucht in Filmtiteln

Beispiel:

GET /api/history?status=FINISHED&search=Inception
+ History API - Ripster      

History API

Endpunkte für Job-Historie, Orphan-Import und Löschoperationen.


GET /api/history

Liefert Jobs (optionale Filter).

Query-Parameter:

Parameter Typ Beschreibung
status string Filter nach Job-Status
search string Suche in Titel-Feldern

Beispiel:

GET /api/history?status=FINISHED&search=Inception
 

Response:

{
   "jobs": [
     {
       "id": 42,
       "status": "FINISHED",
       "title": "Inception",
-      "imdb_id": "tt1375666",
-      "omdb_year": "2010",
-      "omdb_type": "movie",
-      "omdb_poster": "https://...",
-      "raw_path": "/mnt/nas/raw/Inception_t00.mkv",
-      "output_path": "/mnt/nas/movies/Inception (2010).mkv",
-      "created_at": "2024-01-15T10:00:00.000Z",
-      "updated_at": "2024-01-15T12:30:00.000Z"
-    }
-  ],
-  "total": 1
-}
-

GET /api/history/:id

Gibt Detail-Informationen für einen einzelnen Job zurück.

URL-Parameter: id – Job-ID

Query-Parameter:

Parameter Typ Standard Beschreibung
includeLogs boolean false Log-Inhalte einschließen
includeLiveLog boolean false Aktuellen Live-Log einschließen

Response:

{
-  "id": 42,
-  "status": "FINISHED",
-  "title": "Inception",
-  "imdb_id": "tt1375666",
-  "encode_plan": { ... },
-  "makemkv_output": { ... },
-  "mediainfo_output": { ... },
-  "handbrake_log": "/path/to/log",
-  "logs": {
-    "handbrake": "Encoding: task 1 of 1, 100.0%\n..."
-  },
-  "created_at": "2024-01-15T10:00:00.000Z",
-  "updated_at": "2024-01-15T12:30:00.000Z"
-}
-

GET /api/history/database

Gibt alle rohen Datenbankzeilen zurück (Debug-Ansicht).

Response:

{
-  "jobs": [ { "id": 1, "status": "FINISHED", ... } ],
-  "total": 15
-}
-

GET /api/history/orphan-raw

Findet Raw-Ordner, die nicht als Jobs in der Datenbank registriert sind.

Response:

{
-  "orphans": [
-    {
-      "path": "/mnt/nas/raw/UnknownMovie_2023-12-01",
-      "size": "45.2 GB",
-      "modifiedAt": "2023-12-01T15:00:00.000Z",
-      "files": ["t00.mkv", "t01.mkv"]
-    }
-  ]
-}
-

POST /api/history/orphan-raw/import

Importiert einen Orphan-Raw-Ordner als Job in die Datenbank.

Request:

{
-  "path": "/mnt/nas/raw/UnknownMovie_2023-12-01"
-}
+      "raw_path": "/mnt/raw/Inception - RAW - job-42",
+      "output_path": "/mnt/movies/Inception (2010)/Inception (2010).mkv",
+      "mediaType": "bluray",
+      "ripSuccessful": true,
+      "encodeSuccess": true,
+      "created_at": "2026-03-10T08:00:00.000Z",
+      "updated_at": "2026-03-10T10:00:00.000Z"
+    }
+  ]
+}
+

GET /api/history/:id

Liefert Job-Detail.

Query-Parameter:

Parameter Typ Standard Beschreibung
includeLogs bool false Prozesslog laden
includeLiveLog bool false alias-artig ebenfalls Prozesslog laden
includeAllLogs bool false vollständiges Log statt Tail
logTailLines number 800 Tail-Länge falls nicht includeAllLogs

Response:

{
+  "job": {
+    "id": 42,
+    "status": "FINISHED",
+    "makemkvInfo": {},
+    "mediainfoInfo": {},
+    "handbrakeInfo": {},
+    "encodePlan": {},
+    "log": "...",
+    "log_count": 1,
+    "logMeta": {
+      "loaded": true,
+      "total": 800,
+      "returned": 800,
+      "truncated": true
+    }
+  }
+}
+

GET /api/history/database

Debug-Ansicht der DB-Zeilen (angereichert).

Response:

{
+  "rows": [
+    {
+      "id": 42,
+      "status": "FINISHED",
+      "rawFolderName": "Inception - RAW - job-42"
+    }
+  ]
+}
+

GET /api/history/orphan-raw

Sucht RAW-Ordner ohne zugehörigen Job.

Response:

{
+  "rawDir": "/mnt/raw",
+  "rawDirs": ["/mnt/raw", "/mnt/raw-bluray"],
+  "rows": [
+    {
+      "rawPath": "/mnt/raw/Inception (2010) [tt1375666] - RAW - job-99",
+      "folderName": "Inception (2010) [tt1375666] - RAW - job-99",
+      "title": "Inception",
+      "year": 2010,
+      "imdbId": "tt1375666",
+      "folderJobId": 99,
+      "entryCount": 4,
+      "hasBlurayStructure": true,
+      "lastModifiedAt": "2026-03-10T09:00:00.000Z"
+    }
+  ]
+}
+

POST /api/history/orphan-raw/import

Importiert RAW-Ordner als FINISHED-Job.

Request:

{ "rawPath": "/mnt/raw/Inception (2010) [tt1375666] - RAW - job-99" }
 

Response:

{
-  "ok": true,
-  "jobId": 99,
-  "message": "Orphan-Ordner als Job importiert"
-}
-

Nach dem Import kann dem Job über /api/history/:id/omdb/assign Metadaten zugewiesen werden.


POST /api/history/:id/omdb/assign

Weist einem bestehenden Job OMDb-Metadaten nachträglich zu.

URL-Parameter: id – Job-ID

Request:

{
+  "job": { "id": 77, "status": "FINISHED" },
+  "uiReset": { "reset": true, "state": "IDLE" }
+}
+

POST /api/history/:id/omdb/assign

Weist OMDb-/Metadaten nachträglich zu.

Request:

{
   "imdbId": "tt1375666",
   "title": "Inception",
-  "year": "2010",
-  "type": "movie",
-  "poster": "https://..."
+  "year": 2010,
+  "poster": "https://...",
+  "fromOmdb": true
 }
-

Response:

{ "ok": true }
-

POST /api/history/:id/delete-files

Löscht die Dateien eines Jobs (Raw und/oder Output), behält den Job-Eintrag.

URL-Parameter: id – Job-ID

Request:

{
-  "deleteRaw": true,
-  "deleteOutput": false
-}
-

Response:

{
-  "ok": true,
-  "deleted": {
-    "raw": "/mnt/nas/raw/Inception_t00.mkv",
-    "output": null
-  }
-}
-

POST /api/history/:id/delete

Löscht den Job-Eintrag aus der Datenbank, optional auch die Dateien.

URL-Parameter: id – Job-ID

Request:

{
-  "deleteFiles": true
-}
-

Response:

{ "ok": true, "message": "Job gelöscht" }
-

Unwiderruflich

Das Löschen von Jobs und Dateien ist nicht rückgängig zu machen.

\ No newline at end of file +

Response:

{ "job": { "id": 42, "imdb_id": "tt1375666" } }
+

POST /api/history/:id/delete-files

Löscht Dateien eines Jobs, behält DB-Eintrag.

Request:

{ "target": "both" }
+

target: raw | movie | both

Response:

{
+  "summary": {
+    "target": "both",
+    "raw": { "attempted": true, "deleted": true, "filesDeleted": 12, "dirsRemoved": 3, "reason": null },
+    "movie": { "attempted": true, "deleted": false, "filesDeleted": 0, "dirsRemoved": 0, "reason": "Movie-Datei/Pfad existiert nicht." }
+  },
+  "job": { "id": 42 }
+}
+

POST /api/history/:id/delete

Löscht Job aus DB; optional auch Dateien.

Request:

{ "target": "none" }
+

target: none | raw | movie | both

Response:

{
+  "deleted": true,
+  "jobId": 42,
+  "fileTarget": "both",
+  "fileSummary": {
+    "target": "both",
+    "raw": { "filesDeleted": 10 },
+    "movie": { "filesDeleted": 1 }
+  },
+  "uiReset": {
+    "reset": true,
+    "state": "IDLE"
+  }
+}
+

Hinweise

  • Ein aktiver Pipeline-Job kann nicht gelöscht werden (409).
  • Alle Löschoperationen sind irreversibel.
\ No newline at end of file diff --git a/site/api/index.html b/site/api/index.html index 2ec274d..eed6d63 100644 --- a/site/api/index.html +++ b/site/api/index.html @@ -1,6 +1,15 @@ - API-Referenz - Ripster

API-Referenz

Ripster bietet eine REST-API für alle Operationen sowie einen WebSocket-Endpunkt für Echtzeit-Updates.


Basis-URL

http://localhost:3001
-

Konfigurierbar über die Umgebungsvariable PORT.


API-Gruppen

  • Pipeline API


    Pipeline-Steuerung: Analyse starten, Metadaten setzen, Ripping und Encoding steuern.

    Pipeline API

  • Settings API


    Einstellungen lesen und schreiben.

    Settings API

  • History API


    Job-Geschichte abfragen, Jobs löschen, Orphan-Ordner importieren.

    History API

  • WebSocket Events


    Echtzeit-Events für Pipeline-Status, Fortschritt und Disc-Erkennung.

    WebSocket


Authentifizierung

Die API hat keine Authentifizierung. Sie ist für den Einsatz im lokalen Netzwerk konzipiert.

Produktionsbetrieb

Falls Ripster öffentlich erreichbar sein soll, schütze die API mit einem Reverse-Proxy (z. B. nginx mit Basic Auth oder OAuth).


Fehlerformat

Alle API-Fehler werden im folgenden Format zurückgegeben:

{
-  "error": "Job nicht gefunden",
-  "details": "Kein Job mit ID 999 vorhanden"
-}
-

HTTP-Statuscodes:

Code Bedeutung
200 Erfolg
400 Ungültige Anfrage
404 Ressource nicht gefunden
409 Konflikt (z.B. Pipeline bereits aktiv)
500 Interner Serverfehler
\ No newline at end of file + API-Referenz - Ripster

API-Referenz

Ripster bietet eine REST-API für Steuerung/Verwaltung sowie einen WebSocket-Endpunkt für Echtzeit-Updates.


Basis-URL

http://localhost:3001
+

API-Prefix: /api

Beispiele:

  • GET /api/health
  • GET /api/pipeline/state

API-Gruppen

  • Health


    Service-Liveness.

    GET /api/health

  • Pipeline API


    Analyse, Start/Retry/Cancel, Queue, Re-Encode.

    Pipeline API

  • Settings API


    Einstellungen, Skripte/Ketten, User-Presets.

    Settings API

  • History API


    Job-Historie, Orphan-Import, Löschoperationen.

    History API

  • Cron API


    Zeitgesteuerte Skript-/Kettenausführung.

    Cron API

  • WebSocket Events


    Pipeline-, Queue-, Disk-, Settings-, Cron- und Monitoring-Events.

    WebSocket


Authentifizierung

Es gibt keine eingebaute Authentifizierung. Ripster ist für lokalen Betrieb gedacht.


Fehlerformat

Fehler werden zentral als JSON geliefert:

{
+  "error": {
+    "message": "Job nicht gefunden.",
+    "statusCode": 404,
+    "reqId": "req_...",
+    "details": [
+      {
+        "field": "name",
+        "message": "Name darf nicht leer sein."
+      }
+    ]
+  }
+}
+

details ist optional (z. B. bei Validierungsfehlern).


Häufige Statuscodes

Code Bedeutung
200 Erfolg
201 Ressource erstellt
400 Ungültige Anfrage / Validierungsfehler
404 Ressource nicht gefunden
409 Konflikt (z. B. falscher Pipeline-Zustand, Job läuft bereits)
500 Interner Fehler
\ No newline at end of file diff --git a/site/api/pipeline/index.html b/site/api/pipeline/index.html index ddd20cd..1830878 100644 --- a/site/api/pipeline/index.html +++ b/site/api/pipeline/index.html @@ -1,35 +1,62 @@ - Pipeline API - Ripster

Pipeline API

Alle Endpunkte zur Steuerung des Ripster-Workflows.


GET /api/pipeline/state

Liefert den aktuellen Pipeline-Snapshot.

Response:

{
+ Pipeline API - Ripster      

Pipeline API

Endpunkte zur Steuerung des Pipeline-Workflows.


GET /api/pipeline/state

Liefert aktuellen Pipeline- und Hardware-Monitoring-Snapshot.

Response (Beispiel):

{
   "pipeline": {
     "state": "READY_TO_ENCODE",
     "activeJobId": 42,
     "progress": 0,
     "eta": null,
-    "statusText": "Mediainfo geladen - bitte bestätigen",
+    "statusText": "Mediainfo bestätigt - Encode manuell starten",
     "context": {
       "jobId": 42
     },
-    "queue": {
-      "maxParallelJobs": 1,
-      "runningCount": 0,
-      "queuedCount": 0,
-      "runningJobs": [],
-      "queuedJobs": []
-    }
-  }
-}
-

Pipeline-Zustände:

Wert Beschreibung
IDLE Wartet auf Medium
DISC_DETECTED Medium erkannt, wartet auf Analyse-Start
METADATA_SELECTION Metadaten-Dialog aktiv
WAITING_FOR_USER_DECISION Manuelle Playlist-Auswahl erforderlich
READY_TO_START Übergang/Fallback vor Start
RIPPING MakeMKV läuft
MEDIAINFO_CHECK HandBrake-Scan + Plan-Erstellung
READY_TO_ENCODE Review bereit
ENCODING HandBrake-Encoding läuft (inkl. Post-Skripte)
FINISHED Abgeschlossen
CANCELLED Vom Benutzer abgebrochen
ERROR Fehler

POST /api/pipeline/analyze

Startet die Analyse für die aktuell erkannte Disc.

Request: kein Body

Response:

{
+    "jobProgress": {
+      "42": {
+        "state": "MEDIAINFO_CHECK",
+        "progress": 68.5,
+        "eta": null,
+        "statusText": "MEDIAINFO_CHECK 68.50%"
+      }
+    },
+    "queue": {
+      "maxParallelJobs": 1,
+      "runningCount": 1,
+      "queuedCount": 2,
+      "runningJobs": [],
+      "queuedJobs": []
+    }
+  },
+  "hardwareMonitoring": {
+    "enabled": true,
+    "intervalMs": 5000,
+    "updatedAt": "2026-03-10T09:00:00.000Z",
+    "sample": {
+      "cpu": {},
+      "memory": {},
+      "gpu": {},
+      "storage": {}
+    },
+    "error": null
+  }
+}
+

POST /api/pipeline/analyze

Startet Disc-Analyse und legt Job an.

Response:

{
   "result": {
     "jobId": 42,
     "detectedTitle": "INCEPTION",
     "omdbCandidates": []
   }
 }
-

POST /api/pipeline/rescan-disc

Erzwingt eine erneute Laufwerksprüfung.

Response (Beispiel):

{
+

POST /api/pipeline/rescan-disc

Erzwingt erneute Laufwerksprüfung.

Response (Beispiel):

{
   "result": {
-    "emitted": "discInserted"
-  }
-}
-

GET /api/pipeline/omdb/search?q=

Sucht OMDb-Titel.

Response:

{
+    "present": true,
+    "changed": true,
+    "emitted": "discInserted",
+    "device": {
+      "path": "/dev/sr0",
+      "discLabel": "INCEPTION",
+      "mediaProfile": "bluray"
+    }
+  }
+}
+

GET /api/pipeline/omdb/search?q=

OMDb-Titelsuche.

Response:

{
   "results": [
     {
       "imdbId": "tt1375666",
@@ -40,7 +67,7 @@
     }
   ]
 }
-

POST /api/pipeline/select-metadata

Setzt Metadaten (und optional Playlist-Entscheidung).

Request:

{
+

POST /api/pipeline/select-metadata

Setzt Metadaten (und optional Playlist) für einen Job.

Request:

{
   "jobId": 42,
   "title": "Inception",
   "year": 2010,
@@ -49,34 +76,82 @@
   "fromOmdb": true,
   "selectedPlaylist": "00800"
 }
-

Response: { "job": { ... } }

Startlogik

Nach Metadaten-Bestätigung wird der nächste Schritt automatisch ausgelöst (startPreparedJob). Der Job startet direkt oder wird in die Queue eingereiht.


POST /api/pipeline/start/:jobId

Startet einen vorbereiteten Job manuell (z. B. Fallback/Queue-Szenario).

Response (Beispiel):

{
-  "result": {
-    "started": true,
-    "stage": "RIPPING"
-  }
-}
-

Mögliche stage-Werte sind u. a. RIPPING, MEDIAINFO_CHECK, ENCODING.


POST /api/pipeline/confirm-encode/:jobId

Bestätigt Review-Auswahl (Titel/Tracks/Post-Skripte).

Request:

{
-  "selectedEncodeTitleId": 1,
-  "selectedTrackSelection": {
-    "1": {
-      "audioTrackIds": [1, 2],
-      "subtitleTrackIds": [3]
-    }
-  },
-  "selectedPostEncodeScriptIds": [2, 7],
-  "skipPipelineStateUpdate": false
-}
-

Response: { "job": { ... } }


POST /api/pipeline/cancel

Bricht laufenden Job ab oder entfernt einen Queue-Eintrag.

Request (optional):

{
-  "jobId": 42
-}
-

Response (Beispiel):

{
-  "result": {
-    "cancelled": true,
-    "queuedOnly": false,
-    "jobId": 42
-  }
-}
-

POST /api/pipeline/retry/:jobId

Startet einen Job aus ERROR/CANCELLED erneut (oder reiht ihn in die Queue ein).

Response: { "result": { ... } }


POST /api/pipeline/resume-ready/:jobId

Lädt einen READY_TO_ENCODE-Job nach Neustart wieder in die aktive Session.

Response: { "job": { ... } }


POST /api/pipeline/reencode/:jobId

Startet Re-Encode aus bestehendem RAW.

Response: { "result": { ... } }


POST /api/pipeline/restart-review/:jobId

Berechnet die Review aus vorhandenem RAW neu.

Response: { "result": { ... } }


POST /api/pipeline/restart-encode/:jobId

Startet Encoding mit der zuletzt bestätigten Auswahl neu.

Response: { "result": { ... } }


Queue-Endpunkte

GET /api/pipeline/queue

Liefert den aktuellen Queue-Status.

Response: { "queue": { ... } }

POST /api/pipeline/queue/reorder

Sortiert Queue-Einträge neu.

Request:

{
-  "orderedJobIds": [42, 43, 41]
-}
-

Response: { "queue": { ... } }

\ No newline at end of file +

Response:

{ "job": { "id": 42, "status": "READY_TO_START" } }
+

POST /api/pipeline/start/:jobId

Startet vorbereiteten Job oder queued ihn (je nach Parallel-Limit).

Mögliche Responses:

{ "result": { "started": true, "stage": "RIPPING" } }
+
{ "result": { "queued": true, "started": false, "queuePosition": 2, "action": "START_PREPARED" } }
+

POST /api/pipeline/confirm-encode/:jobId

Bestätigt Review-Auswahl (Tracks, Pre/Post-Skripte/Ketten, User-Preset).

Request (typisch):

{
+  "selectedEncodeTitleId": 1,
+  "selectedTrackSelection": {
+    "1": {
+      "audioTrackIds": [1, 2],
+      "subtitleTrackIds": [3]
+    }
+  },
+  "selectedPreEncodeScriptIds": [1],
+  "selectedPostEncodeScriptIds": [2, 7],
+  "selectedPreEncodeChainIds": [3],
+  "selectedPostEncodeChainIds": [4],
+  "selectedUserPresetId": 5,
+  "skipPipelineStateUpdate": false
+}
+

Response:

{ "job": { "id": 42, "encode_review_confirmed": 1 } }
+

POST /api/pipeline/cancel

Bricht laufenden Job ab oder entfernt Queue-Eintrag.

Request (optional):

{ "jobId": 42 }
+

Mögliche Responses:

{ "result": { "cancelled": true, "queuedOnly": true, "jobId": 42 } }
+
{ "result": { "cancelled": true, "queuedOnly": false, "jobId": 42 } }
+
{ "result": { "cancelled": true, "queuedOnly": false, "pending": true, "jobId": 42 } }
+

POST /api/pipeline/retry/:jobId

Retry für ERROR/CANCELLED-Jobs (oder Queue-Einreihung).

POST /api/pipeline/reencode/:jobId

Startet Re-Encode aus bestehendem RAW.

POST /api/pipeline/restart-review/:jobId

Berechnet Review aus RAW neu.

POST /api/pipeline/restart-encode/:jobId

Startet Encoding mit letzter bestätigter Review neu.

POST /api/pipeline/resume-ready/:jobId

Lädt READY_TO_ENCODE-Job nach Neustart wieder in aktive Session.

Alle Endpunkte liefern { result: ... } bzw. { job: ... }.


Queue-Endpunkte

GET /api/pipeline/queue

Liefert Queue-Snapshot.

{
+  "queue": {
+    "maxParallelJobs": 1,
+    "runningCount": 1,
+    "queuedCount": 3,
+    "runningJobs": [
+      {
+        "jobId": 41,
+        "title": "Inception",
+        "status": "ENCODING",
+        "lastState": "ENCODING"
+      }
+    ],
+    "queuedJobs": [
+      {
+        "entryId": 11,
+        "position": 1,
+        "type": "job",
+        "jobId": 42,
+        "action": "START_PREPARED",
+        "actionLabel": "Start",
+        "title": "Matrix",
+        "status": "READY_TO_ENCODE",
+        "lastState": "READY_TO_ENCODE",
+        "hasScripts": true,
+        "hasChains": false,
+        "enqueuedAt": "2026-03-10T09:00:00.000Z"
+      },
+      {
+        "entryId": 12,
+        "position": 2,
+        "type": "wait",
+        "waitSeconds": 30,
+        "title": "Warten 30s",
+        "status": "QUEUED",
+        "enqueuedAt": "2026-03-10T09:01:00.000Z"
+      }
+    ],
+    "updatedAt": "2026-03-10T09:01:02.000Z"
+  }
+}
+

POST /api/pipeline/queue/reorder

Sortiert Queue-Einträge neu.

Request:

{
+  "orderedEntryIds": [12, 11]
+}
+

Legacy fallback wird akzeptiert:

{
+  "orderedJobIds": [42, 43]
+}
+

POST /api/pipeline/queue/entry

Fügt Nicht-Job-Queue-Eintrag hinzu (script, chain, wait).

Request-Beispiele:

{ "type": "script", "scriptId": 3 }
+
{ "type": "chain", "chainId": 2, "insertAfterEntryId": 11 }
+
{ "type": "wait", "waitSeconds": 45 }
+

Response:

{
+  "result": { "entryId": 12, "type": "wait", "position": 2 },
+  "queue": { "...": "..." }
+}
+

DELETE /api/pipeline/queue/entry/:entryId

Entfernt Queue-Eintrag.

Response:

{ "queue": { "...": "..." } }
+

Pipeline-Zustände

State Bedeutung
IDLE Wartet auf Medium
DISC_DETECTED Medium erkannt
ANALYZING MakeMKV-Analyse läuft
METADATA_SELECTION Metadaten-Auswahl
WAITING_FOR_USER_DECISION Playlist-Entscheidung erforderlich
READY_TO_START Übergang vor Start
RIPPING MakeMKV-Rip läuft
MEDIAINFO_CHECK Titel-/Track-Auswertung
READY_TO_ENCODE Review bereit
ENCODING HandBrake-Encoding läuft
FINISHED Abgeschlossen
CANCELLED Abgebrochen
ERROR Fehler
\ No newline at end of file diff --git a/site/api/settings/index.html b/site/api/settings/index.html index c0a94c3..adbb6b7 100644 --- a/site/api/settings/index.html +++ b/site/api/settings/index.html @@ -1,79 +1,129 @@ - Settings API - Ripster

Settings API

Endpunkte zum Lesen und Schreiben der Anwendungseinstellungen.


GET /api/settings

Gibt alle Einstellungen kategorisiert zurück.

Response:

{
-  "paths": {
-    "raw_dir": {
-      "value": "/mnt/nas/raw",
-      "schema": {
-        "type": "string",
-        "label": "Raw-Verzeichnis",
-        "description": "Speicherort für rohe MKV-Dateien",
-        "required": true
-      }
-    },
-    "movie_dir": {
-      "value": "/mnt/nas/movies",
-      "schema": { ... }
-    }
-  },
-  "tools": { ... },
-  "encoding": { ... },
-  "drive": { ... },
-  "makemkv": { ... },
-  "omdb": { ... },
-  "notifications": { ... }
-}
-

PUT /api/settings/:key

Aktualisiert eine einzelne Einstellung.

URL-Parameter: key – Einstellungs-Schlüssel

Request:

{
-  "value": "/mnt/storage/raw"
-}
-

Response:

{ "ok": true, "key": "raw_dir", "value": "/mnt/storage/raw" }
-

Fehlerfälle: - 400 – Ungültiger Wert (Validierungsfehler) - 404 – Einstellung nicht gefunden

Encode-Review-Refresh

Wenn eine encoding-relevante Einstellung geändert wird (z.B. handbrake_preset), wird der Encode-Plan für den aktuell wartenden Job automatisch neu berechnet.


PUT /api/settings

Aktualisiert mehrere Einstellungen auf einmal.

Request:

{
-  "raw_dir": "/mnt/storage/raw",
-  "movie_dir": "/mnt/storage/movies",
-  "handbrake_preset": "H.265 MKV 720p30"
-}
+ Settings API - Ripster      

Settings API

Endpunkte für Einstellungen, Skripte, Skript-Ketten und User-Presets.


GET /api/settings

Liefert alle Einstellungen kategorisiert.

Response (Struktur):

{
+  "categories": [
+    {
+      "category": "Pfade",
+      "settings": [
+        {
+          "key": "raw_dir",
+          "label": "Raw Ausgabeordner",
+          "type": "path",
+          "required": true,
+          "description": "...",
+          "defaultValue": "data/output/raw",
+          "options": [],
+          "validation": { "minLength": 1 },
+          "value": "data/output/raw",
+          "orderIndex": 100
+        }
+      ]
+    }
+  ]
+}
+

PUT /api/settings/:key

Aktualisiert eine einzelne Einstellung.

Request:

{ "value": "/mnt/storage/raw" }
+

Response:

{
+  "setting": {
+    "key": "raw_dir",
+    "value": "/mnt/storage/raw"
+  },
+  "reviewRefresh": {
+    "triggered": false,
+    "reason": "not_ready"
+  }
+}
+

reviewRefresh ist null oder ein Objekt mit Status der optionalen Review-Neuberechnung.


PUT /api/settings

Aktualisiert mehrere Einstellungen atomar.

Request:

{
+  "settings": {
+    "raw_dir": "/mnt/storage/raw",
+    "movie_dir": "/mnt/storage/movies",
+    "handbrake_preset_bluray": "H.264 MKV 1080p30"
+  }
+}
 

Response:

{
-  "ok": true,
-  "updated": ["raw_dir", "movie_dir", "handbrake_preset"],
-  "errors": []
-}
-

POST /api/settings/pushover/test

Sendet eine Test-Benachrichtigung über PushOver.

Request: Kein Body erforderlich (verwendet gespeicherte Zugangsdaten)

Response (Erfolg):

{ "ok": true, "message": "Test-Benachrichtigung gesendet" }
-

Response (Fehler):

{ "ok": false, "error": "Ungültiger API-Token" }
-

Skript-Verwaltung

Post-Encode-Skripte werden über eigene Endpunkte unter /api/settings/scripts verwaltet.

GET /api/settings/scripts

Gibt alle konfigurierten Skripte zurück.

Response:

{
-  "scripts": [
-    {
-      "id": "script-abc123",
-      "name": "Zu Plex verschieben",
-      "command": "/home/michael/scripts/move-to-plex.sh",
-      "description": "Verschiebt die fertige Datei ins Plex-Verzeichnis",
-      "createdAt": "2024-01-15T10:00:00.000Z"
-    }
-  ]
-}
-

POST /api/settings/scripts

Legt ein neues Post-Encode-Skript an.

Request:

{
-  "name": "Zu Plex verschieben",
-  "command": "/home/michael/scripts/move-to-plex.sh",
-  "description": "Verschiebt die fertige Datei ins Plex-Verzeichnis"
-}
-
Feld Typ Pflicht Beschreibung
name string Anzeigename
command string Shell-Befehl oder absoluter Skriptpfad
description string Optionale Beschreibung

Response:

{
-  "ok": true,
-  "script": {
-    "id": "script-abc123",
-    "name": "Zu Plex verschieben",
-    "command": "/home/michael/scripts/move-to-plex.sh"
-  }
-}
-

PUT /api/settings/scripts/:scriptId

Aktualisiert ein vorhandenes Skript.

URL-Parameter: scriptId

Request: Gleiche Felder wie beim Anlegen (alle optional).

{ "name": "Zu Jellyfin verschieben", "command": "/home/michael/scripts/move-to-jellyfin.sh" }
-

Response: { "ok": true }


DELETE /api/settings/scripts/:scriptId

Löscht ein Skript.

URL-Parameter: scriptId

Response: { "ok": true }

Referenzen in Jobs

Wenn das Skript in laufenden oder abgeschlossenen Jobs referenziert wird, wird es trotzdem gelöscht. In zukünftigen Encode-Reviews erscheint es nicht mehr.


POST /api/settings/scripts/:scriptId/test

Führt ein Skript mit Platzhalter-Umgebungsvariablen aus (Testlauf).

URL-Parameter: scriptId

Response (Erfolg):

{
-  "ok": true,
-  "exitCode": 0,
-  "stdout": "Testausgabe des Skripts",
-  "stderr": "",
-  "durationMs": 245
-}
-

Response (Fehler):

{
-  "ok": false,
-  "exitCode": 1,
-  "stdout": "",
-  "stderr": "Datei nicht gefunden: /home/michael/scripts/move-to-plex.sh",
-  "durationMs": 12
-}
-

Platzhalter-Werte beim Testlauf:

Variable Testwert
RIPSTER_OUTPUT_PATH /tmp/ripster-test-output.mkv
RIPSTER_JOB_ID 0
RIPSTER_TITLE Test Film
RIPSTER_YEAR 2024
RIPSTER_IMDB_ID tt0000000
RIPSTER_RAW_PATH /tmp/ripster-test-raw.mkv

Einstellungs-Schlüssel Referenz

Eine vollständige Liste aller Einstellungs-Schlüssel:

Schlüssel Kategorie Typ Beschreibung
raw_dir paths string Raw-MKV Verzeichnis
movie_dir paths string Ausgabe-Verzeichnis
log_dir paths string Log-Verzeichnis
makemkv_command tools string MakeMKV-Befehl
handbrake_command tools string HandBrake-Befehl
mediainfo_command tools string MediaInfo-Befehl
handbrake_preset encoding string HandBrake-Preset-Name
handbrake_extra_args encoding string Zusatz-Argumente
output_extension encoding string Dateiendung (z.B. mkv)
filename_template encoding string Dateiname-Template
drive_mode drive select auto oder explicit
drive_device drive string Geräte-Pfad
disc_poll_interval_ms drive number Polling-Intervall (ms)
makemkv_min_length_minutes makemkv number Min. Titellänge (Minuten)
makemkv_backup_mode makemkv boolean Backup-Modus aktivieren
omdb_api_key omdb string OMDb API-Key
omdb_default_type omdb select Standard-Suchtyp
pushover_user_key notifications string PushOver User-Key
pushover_api_token notifications string PushOver API-Token
\ No newline at end of file +
"changes": [ + { "key": "raw_dir", "value": "/mnt/storage/raw" }, + { "key": "movie_dir", "value": "/mnt/storage/movies" } + ], + "reviewRefresh": { + "triggered": true, + "jobId": 42, + "relevantKeys": ["handbrake_preset_bluray"] + } +} +

Bei Validierungsfehlern kommt 400 mit error.details[].


GET /api/settings/handbrake-presets

Liest Preset-Liste via HandBrakeCLI -z (mit Fallback auf konfigurierte Presets).

Response (Beispiel):

{
+  "source": "handbrake-cli",
+  "message": null,
+  "options": [
+    { "label": "General/", "value": "__group__general", "disabled": true, "category": "General" },
+    { "label": "   Fast 1080p30", "value": "Fast 1080p30", "category": "General" }
+  ]
+}
+

POST /api/settings/pushover/test

Sendet Testnachricht über aktuelle PushOver-Settings.

Request (optional):

{
+  "title": "Test",
+  "message": "Ripster Test"
+}
+

Response:

{
+  "result": {
+    "sent": true,
+    "eventKey": "test",
+    "requestId": "..."
+  }
+}
+

Wenn PushOver deaktiviert ist oder Credentials fehlen, kommt i. d. R. ebenfalls 200 mit sent: false + reason.


Skripte

Basis: /api/settings/scripts

GET /api/settings/scripts

{ "scripts": [ { "id": 1, "name": "...", "scriptBody": "...", "orderIndex": 1, "createdAt": "...", "updatedAt": "..." } ] }
+

POST /api/settings/scripts

{ "name": "Move", "scriptBody": "mv \"$RIPSTER_OUTPUT_PATH\" /mnt/movies/" }
+

Response: 201 mit { "script": { ... } }

PUT /api/settings/scripts/:id

Body wie POST, Response { "script": { ... } }.

DELETE /api/settings/scripts/:id

Response { "removed": { ... } }.

POST /api/settings/scripts/reorder

{ "orderedScriptIds": [3, 1, 2] }
+

Response { "scripts": [ ... ] }.

POST /api/settings/scripts/:id/test

Führt Skript als Testlauf aus.

{
+  "result": {
+    "scriptId": 1,
+    "scriptName": "Move",
+    "success": true,
+    "exitCode": 0,
+    "signal": null,
+    "timedOut": false,
+    "durationMs": 120,
+    "stdout": "...",
+    "stderr": "...",
+    "stdoutTruncated": false,
+    "stderrTruncated": false
+  }
+}
+

Umgebungsvariablen für Skripte

Diese Variablen werden beim Ausführen gesetzt:

  • RIPSTER_SCRIPT_RUN_AT
  • RIPSTER_JOB_ID
  • RIPSTER_JOB_TITLE
  • RIPSTER_MODE
  • RIPSTER_INPUT_PATH
  • RIPSTER_OUTPUT_PATH
  • RIPSTER_RAW_PATH
  • RIPSTER_SCRIPT_ID
  • RIPSTER_SCRIPT_NAME
  • RIPSTER_SCRIPT_SOURCE

Skript-Ketten

Basis: /api/settings/script-chains

Eine Kette hat Schritte vom Typ:

  • script (scriptId erforderlich)
  • wait (waitSeconds 1..3600)

GET /api/settings/script-chains

Response { "chains": [ ... ] } (inkl. steps[]).

GET /api/settings/script-chains/:id

Response { "chain": { ... } }.

POST /api/settings/script-chains

{
+  "name": "After Encode",
+  "steps": [
+    { "stepType": "script", "scriptId": 1 },
+    { "stepType": "wait", "waitSeconds": 15 },
+    { "stepType": "script", "scriptId": 2 }
+  ]
+}
+

Response: 201 mit { "chain": { ... } }

PUT /api/settings/script-chains/:id

Body wie POST, Response { "chain": { ... } }.

DELETE /api/settings/script-chains/:id

Response { "removed": { ... } }.

POST /api/settings/script-chains/reorder

{ "orderedChainIds": [2, 1, 3] }
+

Response { "chains": [ ... ] }.

POST /api/settings/script-chains/:id/test

Response:

{
+  "result": {
+    "chainId": 2,
+    "chainName": "After Encode",
+    "steps": 3,
+    "succeeded": 3,
+    "failed": 0,
+    "aborted": false,
+    "results": []
+  }
+}
+

User-Presets

Basis: /api/settings/user-presets

GET /api/settings/user-presets

Optionaler Query-Parameter: media_type=bluray|dvd|other|all

{
+  "presets": [
+    {
+      "id": 1,
+      "name": "Blu-ray HQ",
+      "mediaType": "bluray",
+      "handbrakePreset": "H.264 MKV 1080p30",
+      "extraArgs": "--encoder-preset slow",
+      "description": "...",
+      "createdAt": "...",
+      "updatedAt": "..."
+    }
+  ]
+}
+

POST /api/settings/user-presets

{
+  "name": "Blu-ray HQ",
+  "mediaType": "bluray",
+  "handbrakePreset": "H.264 MKV 1080p30",
+  "extraArgs": "--encoder-preset slow",
+  "description": "optional"
+}
+

Response: 201 mit { "preset": { ... } }

PUT /api/settings/user-presets/:id

Body mit beliebigen Feldern aus POST, Response { "preset": { ... } }.

DELETE /api/settings/user-presets/:id

Response { "removed": { ... } }.

\ No newline at end of file diff --git a/site/api/websocket/index.html b/site/api/websocket/index.html index 19ad4d5..401f26f 100644 --- a/site/api/websocket/index.html +++ b/site/api/websocket/index.html @@ -1,92 +1,98 @@ - WebSocket Events - Ripster

WebSocket Events

Ripster sendet Echtzeit-Updates über WebSocket unter /ws.


Verbindung

const ws = new WebSocket('ws://localhost:3001/ws');
+ WebSocket Events - Ripster      

WebSocket Events

Ripster sendet Echtzeit-Updates über /ws.


Verbindung

const ws = new WebSocket('ws://localhost:3001/ws');
 
 ws.onmessage = (event) => {
-  const message = JSON.parse(event.data);
-  console.log(message.type, message.payload);
+  const msg = JSON.parse(event.data);
+  console.log(msg.type, msg.payload);
 };
-

Nachrichtenformat

Alle Broadcasts haben dieses Schema:

{
+

Nachrichtenformat

Die meisten Broadcasts haben dieses Schema:

{
   "type": "EVENT_TYPE",
-  "payload": { },
-  "timestamp": "2026-03-05T10:00:00.000Z"
+  "payload": {},
+  "timestamp": "2026-03-10T09:00:00.000Z"
 }
-

Event-Typen

WS_CONNECTED

Wird direkt nach Verbindungsaufbau gesendet.

{
+

Ausnahme: WS_CONNECTED beim Verbindungsaufbau enthält kein timestamp.


Event-Typen

WS_CONNECTED

Sofort nach erfolgreicher Verbindung.

{
   "type": "WS_CONNECTED",
   "payload": {
-    "connectedAt": "2026-03-05T10:00:00.000Z"
+    "connectedAt": "2026-03-10T09:00:00.000Z"
   }
 }
-

PIPELINE_STATE_CHANGED

Snapshot bei Zustandswechsel.

{
+

PIPELINE_STATE_CHANGED

Neuer Pipeline-Snapshot.

{
   "type": "PIPELINE_STATE_CHANGED",
   "payload": {
     "state": "ENCODING",
     "activeJobId": 42,
-    "progress": 73.5,
+    "progress": 62.5,
     "eta": "00:12:34",
-    "statusText": "Encoding mit HandBrake",
+    "statusText": "ENCODING 62.50%",
     "context": {},
-    "queue": {
-      "maxParallelJobs": 1,
-      "runningCount": 1,
-      "queuedCount": 0
-    }
-  }
-}
-

PIPELINE_PROGRESS

Laufende Fortschrittsupdates während aktiver Phasen.

{
+    "jobProgress": {
+      "42": {
+        "state": "ENCODING",
+        "progress": 62.5,
+        "eta": "00:12:34",
+        "statusText": "ENCODING 62.50%"
+      }
+    },
+    "queue": {
+      "maxParallelJobs": 1,
+      "runningCount": 1,
+      "queuedCount": 2,
+      "runningJobs": [],
+      "queuedJobs": []
+    }
+  }
+}
+

PIPELINE_PROGRESS

Laufende Fortschrittsupdates.

{
   "type": "PIPELINE_PROGRESS",
   "payload": {
     "state": "ENCODING",
     "activeJobId": 42,
-    "progress": 73.5,
+    "progress": 62.5,
     "eta": "00:12:34",
-    "statusText": "ENCODING 73.50% - task 1 of 1"
+    "statusText": "ENCODING 62.50%"
   }
 }
-

PIPELINE_QUEUE_CHANGED

Aktualisierung der Job-Queue.

{
-  "type": "PIPELINE_QUEUE_CHANGED",
+

PIPELINE_QUEUE_CHANGED

Queue-Snapshot aktualisiert.

DISC_DETECTED / DISC_REMOVED

Disc-Insertion/-Removal.

{
+  "type": "DISC_DETECTED",
   "payload": {
-    "maxParallelJobs": 1,
-    "runningCount": 1,
-    "queuedCount": 2,
-    "runningJobs": [],
-    "queuedJobs": []
-  }
-}
-

DISC_DETECTED

Disc erkannt.

{
-  "type": "DISC_DETECTED",
+    "device": {
+      "path": "/dev/sr0",
+      "discLabel": "INCEPTION",
+      "model": "ASUS BW-16D1HT",
+      "fstype": "udf",
+      "mountpoint": null,
+      "mediaProfile": "bluray"
+    }
+  }
+}
+

mediaProfile: bluray | dvd | other | null

HARDWARE_MONITOR_UPDATE

Snapshot aus Hardware-Monitoring.

{
+  "type": "HARDWARE_MONITOR_UPDATE",
   "payload": {
-    "device": {
-      "path": "/dev/sr0",
-      "discLabel": "INCEPTION"
-    }
-  }
-}
-

DISC_REMOVED

Disc entfernt.

{
-  "type": "DISC_REMOVED",
+    "enabled": true,
+    "intervalMs": 5000,
+    "updatedAt": "2026-03-10T09:00:00.000Z",
+    "sample": {
+      "cpu": {},
+      "memory": {},
+      "gpu": {},
+      "storage": {}
+    },
+    "error": null
+  }
+}
+

PIPELINE_ERROR

Fehler bei Disc-Event-Verarbeitung in Pipeline.

DISK_DETECTION_ERROR

Fehler in Laufwerkserkennung.

SETTINGS_UPDATED

Einzelnes Setting wurde gespeichert.

SETTINGS_BULK_UPDATED

Bulk-Settings gespeichert.

{
+  "type": "SETTINGS_BULK_UPDATED",
   "payload": {
-    "device": {
-      "path": "/dev/sr0"
-    }
-  }
-}
-

PIPELINE_ERROR

Fehler bei Pipeline-Disc-Events im Backend.

{
-  "type": "PIPELINE_ERROR",
+    "count": 3,
+    "keys": ["raw_dir", "movie_dir", "handbrake_preset_bluray"]
+  }
+}
+

SETTINGS_SCRIPTS_UPDATED

Skript geändert (created|updated|deleted|reordered).

SETTINGS_SCRIPT_CHAINS_UPDATED

Skript-Kette geändert (created|updated|deleted|reordered).

USER_PRESETS_UPDATED

User-Preset geändert (created|updated|deleted).

CRON_JOBS_UPDATED

Cron-Config geändert (created|updated|deleted).

CRON_JOB_UPDATED

Laufzeitstatus eines Cron-Jobs geändert.

{
+  "type": "CRON_JOB_UPDATED",
   "payload": {
-    "message": "..."
-  }
-}
-

DISK_DETECTION_ERROR

Fehler im Laufwerkserkennungsdienst.

{
-  "type": "DISK_DETECTION_ERROR",
-  "payload": {
-    "message": "..."
-  }
-}
-

Reconnect-Verhalten

useWebSocket.js versucht bei Verbindungsabbruch automatisch erneut zu verbinden.

  • fester Retry-Intervall: 1500ms
  • erneuter Versuch bis zum Unmount der Komponente

React-Beispiel

import { useWebSocket } from './hooks/useWebSocket';
-
-useWebSocket({
-  onMessage: (msg) => {
-    if (msg.type === 'PIPELINE_STATE_CHANGED') {
-      setPipeline(msg.payload);
-    }
-  }
-});
-
\ No newline at end of file +
"id": 1, + "lastRunStatus": "running", + "lastRunAt": "2026-03-10T10:00:00.000Z", + "nextRunAt": null + } +} +

Reconnect-Verhalten

useWebSocket verbindet bei Abbruch automatisch neu:

  • Retry-Intervall: 1500ms
  • Wiederverbindung bis Komponente unmounted wird
\ No newline at end of file diff --git a/site/architecture/backend/index.html b/site/architecture/backend/index.html index 06c1618..d80227f 100644 --- a/site/architecture/backend/index.html +++ b/site/architecture/backend/index.html @@ -1,57 +1 @@ - Backend-Services - Ripster

Backend-Services

Das Backend ist in Node.js/Express geschrieben und in Services aufgeteilt, die jeweils eine klar abgegrenzte Verantwortlichkeit haben.


pipelineService.js

Der Kern von Ripster – orchestriert den gesamten Ripping-Workflow.

Zuständigkeiten

  • Verwaltung des Pipeline-Zustands als State Machine
  • Koordination zwischen allen externen Tools
  • Generierung von Encode-Plänen
  • Fehlerbehandlung und Recovery

Haupt-Methoden

Methode Beschreibung
analyzeDisc() Legt Job an und öffnet Metadaten-Auswahl
selectMetadata({...}) Setzt Metadaten/Playlist und triggert Auto-Start
startPreparedJob(jobId) Startet vorbereiteten Job (oder Queue)
confirmEncodeReview(jobId, options) Bestätigt Review inkl. Track/Skript-Auswahl
cancel(jobId) Bricht laufenden Job ab oder entfernt Queue-Eintrag
retry(jobId) Startet fehlgeschlagenen/abgebrochenen Job neu
reencodeFromRaw(jobId) Encodiert aus vorhandenem RAW neu
restartReviewFromRaw(jobId) Berechnet Review aus RAW neu
restartEncodeWithLastSettings(jobId) Neustart mit letzter bestätigter Auswahl
resumeReadyToEncodeJob(jobId) Lädt READY_TO_ENCODE nach Neustart in die Session

Zustandsübergänge

flowchart LR
-    START(( )) --> IDLE
-    IDLE -->|analyzeDisc()| META[METADATA\nSELECTION]
-    META -->|selectMetadata()| RTS[READY_TO\nSTART]
-    RTS -->|Auto-Start/Queue| RIP[RIPPING]
-    RTS -->|Auto-Start mit RAW| MIC[MEDIAINFO\nCHECK]
-    RIP -->|MKV erstellt| MIC[MEDIAINFO\nCHECK]
-    MIC -->|Playlist offen| WUD[WAITING_FOR\nUSER_DECISION]
-    WUD -->|selectMetadata(selectedPlaylist)| MIC
-    MIC -->|Tracks analysiert| RTE[READY_TO\nENCODE]
-    RTE -->|confirmEncodeReview() + startPreparedJob()| ENC[ENCODING]
-    ENC -->|HandBrake + Post-Skripte fertig| FIN([FINISHED])
-    ENC -->|Abbruch| CAN([CANCELLED])
-    ENC -->|Fehler| ERR([ERROR])
-    RIP -->|Fehler| ERR
-    RIP -->|Abbruch| CAN
-    ERR -->|retry() / cancel()| IDLE
-    CAN -->|retry() / analyzeDisc()| IDLE
-    FIN -->|cancel / neue Disc| IDLE
-
-    style FIN fill:#e8f5e9,stroke:#66bb6a,color:#2e7d32
-    style CAN fill:#fff3e0,stroke:#fb8c00,color:#e65100
-    style ERR fill:#ffebee,stroke:#ef5350,color:#c62828
-    style ENC fill:#f3e5f5,stroke:#ab47bc,color:#6a1b9a
-    style RIP fill:#e3f2fd,stroke:#42a5f5,color:#1565c0
-    style MIC fill:#e3f2fd,stroke:#42a5f5,color:#1565c0

diskDetectionService.js

Überwacht das Disc-Laufwerk auf Disc-Einleger- und Auswurf-Ereignisse.

Modi

Modus Beschreibung
auto Erkennt verfügbare Laufwerke automatisch
explicit Überwacht ein bestimmtes Gerät (z.B. /dev/sr0)

Polling

Der Service pollt das Laufwerk im konfigurierten Intervall (disc_poll_interval_ms, Standard: 4000ms) und emittiert Events:

// Ereignisse
-emit('discInserted', { path: '/dev/sr0' })
-emit('discRemoved', { path: '/dev/sr0' })
-

processRunner.js

Verwaltet externe CLI-Prozesse.

Features

  • Streaming: stdout/stderr werden zeilenweise gelesen
  • Progress-Callbacks: Ermöglicht Echtzeit-Fortschrittsanzeige
  • Graceful Shutdown: SIGINT → Warte-Timeout → SIGKILL
  • Prozess-Registry: Verfolgt aktive Prozesse für sauberes Beenden

Nutzung

const result = await runProcess(
-  'HandBrakeCLI',
-  ['--input', rawFile, '--output', outputFile, '--preset', preset],
-  {
-    onStderr: (line) => parseHandBrakeProgress(line),
-    onStdout: (line) => logger.debug(line)
-  }
-);
-

websocketService.js

WebSocket-Server für Echtzeit-Client-Kommunikation.

Betrieb

  • Läuft auf Pfad /ws des Express-Servers
  • Hält eine Registry aller verbundenen Clients
  • Ermöglicht Broadcast an alle Clients oder gezieltes Senden

API

broadcast('PIPELINE_STATE_CHANGED', { state, activeJobId });
-broadcast('PIPELINE_PROGRESS', { state, progress, eta, statusText });
-broadcast('PIPELINE_QUEUE_CHANGED', queueSnapshot);
-

omdbService.js

Integration mit der OMDb API.

Methoden

Methode Beschreibung
searchByTitle(title, type) Suche nach Titel (movie/series)
fetchById(imdbId) Vollständige Metadaten per IMDb-ID

Zurückgegebene Daten

{
-  "imdbId": "tt1375666",
-  "title": "Inception",
-  "year": "2010",
-  "type": "movie",
-  "poster": "https://...",
-  "plot": "...",
-  "director": "Christopher Nolan"
-}
-

settingsService.js

Verwaltet alle Anwendungseinstellungen.

Features

  • Schema-getriebene Validierung: Jede Einstellung hat Typ, Grenzen und Pflichtfeld-Flag
  • Kategorisierung: Einstellungen sind in Kategorien gruppiert (Paths, Tools, Encoding, ...)
  • Persistenz: Werte in SQLite, Schema ebenfalls in SQLite
  • Defaults: defaultSettings.js definiert Standardwerte

Einstellungs-Kategorien

Kategorie Einstellungen
Pfade raw_dir, movie_dir, log_dir
Laufwerk drive_mode, drive_device, disc_poll_interval_ms, makemkv_source_index
Monitoring hardware_monitoring_enabled, hardware_monitoring_interval_ms
Tools makemkv_command, handbrake_command, mediainfo_command, pipeline_max_parallel_jobs
Metadaten omdb_api_key, omdb_default_type
Benachrichtigungen pushover_user_key, pushover_api_token

historyService.js

Datenbankoperationen für Job-Historie.

Hauptoperationen

Operation Beschreibung
listJobs(filters) Jobs nach Status/Titel filtern
getJob(id) Job-Details mit Logs abrufen
findOrphanRawFolders() Nicht-getrackte Raw-Ordner finden
importOrphanRaw(path) Orphan-Ordner als Job importieren
assignOmdb(id, omdbData) OMDb-Metadaten nachträglich zuweisen
deleteJob(id, deleteFiles) Job und optional Dateien löschen

notificationService.js

PushOver-Push-Benachrichtigungen.

await notify({
-  title: 'Ripster: Job abgeschlossen',
-  message: 'Inception (2010) wurde erfolgreich encodiert'
-});
-

logger.js

Strukturiertes Logging mit täglicher Log-Rotation.

Log-Level

Level Verwendung
debug Detaillierte Entwicklungs-Informationen
info Normale Betriebsereignisse
warn Warnungen, die Aufmerksamkeit benötigen
error Fehler, die den Betrieb beeinträchtigen

Log-Dateien

logs/
-├── ripster-2024-01-15.log    ← Tages-Log
-└── jobs/
-    └── job-42-handbrake.log  ← Prozess-spezifische Logs
-
\ No newline at end of file + Backend-Services - Ripster

Backend-Services

Das Backend ist in Services aufgeteilt, die von Express-Routen orchestriert werden.


pipelineService.js

Zentrale Workflow-Orchestrierung.

Aufgaben:

  • Pipeline-State-Machine + Persistenz (pipeline_state)
  • Disc-Analyse/Rip/Review/Encode
  • Queue-Management (Jobs + script|chain|wait Einträge)
  • Retry/Re-Encode/Restart-Flows
  • WebSocket-Broadcasts für State/Progress/Queue

Wichtige Methoden:

  • analyzeDisc()
  • selectMetadata()
  • startPreparedJob()
  • confirmEncodeReview()
  • cancel()
  • retry()
  • reencodeFromRaw()
  • restartReviewFromRaw()
  • restartEncodeWithLastSettings()
  • resumeReadyToEncodeJob()
  • enqueueNonJobEntry(), reorderQueue(), removeQueueEntry()

diskDetectionService.js

Pollt Laufwerk(e) und emittiert:

  • discInserted
  • discRemoved
  • error

Zusatz:

  • Modus auto oder explicit
  • heuristische mediaProfile-Erkennung (bluray/dvd/other)
  • rescanAndEmit() für manuellen Trigger

settingsService.js

Settings-Layer mit Validation/Serialisierung.

Features:

  • getCategorizedSettings() für UI-Form
  • setSettingValue() / setSettingsBulk()
  • profilspezifische Auflösung (resolveEffectiveToolSettings)
  • CLI-Config-Building für MakeMKV/HandBrake/MediaInfo
  • HandBrake-Preset-Liste via HandBrakeCLI -z
  • MakeMKV-Registration-Command aus makemkv_registration_key

historyService.js

Historie + Dateioperationen.

Features:

  • Job-Liste/Detail inkl. Log-Tail
  • Orphan-RAW-Erkennung und Import
  • OMDb-Nachzuweisung
  • Dateilöschung (raw|movie|both)
  • Job-Löschung (none|raw|movie|both)

cronService.js

Integriertes Cron-System ohne externe Parser-Library.

Features:

  • 5-Feld-Cron-Parser + nextRun-Berechnung
  • Quellen: script oder chain
  • Laufzeitlogs (cron_run_logs)
  • manuelles Triggern
  • WebSocket-Events: CRON_JOBS_UPDATED, CRON_JOB_UPDATED

Weitere Services

  • scriptService.js (CRUD + Test + Wrapper-Ausführung)
  • scriptChainService.js (CRUD + Step-Execution)
  • userPresetService.js (HandBrake User-Presets)
  • hardwareMonitorService.js (CPU/RAM/GPU/Storage)
  • websocketService.js (Client-Registry + Broadcast)
  • notificationService.js (PushOver)
  • logger.js (rotierende Datei-Logs)

Bootstrapping (src/index.js)

Beim Start:

  1. DB init/migrate
  2. Pipeline-Init
  3. Cron-Init
  4. Express-Routes + Error-Handler
  5. WebSocket-Server auf /ws
  6. Hardware-Monitoring-Init
  7. Disk-Detection-Start
\ No newline at end of file diff --git a/site/architecture/database/index.html b/site/architecture/database/index.html index bc51d9f..2d1502d 100644 --- a/site/architecture/database/index.html +++ b/site/architecture/database/index.html @@ -1,66 +1,16 @@ - Datenbank - Ripster

Datenbank

Ripster verwendet SQLite3 als Datenbank. Die Datenbankdatei liegt unter backend/data/ripster.db.


Schema-Übersicht

-- Vier Haupt-Tabellen
-settings_schema    -- Einstellungs-Definitionen
-settings_values    -- Benutzer-Werte
-jobs               -- Rip-Job-Datensätze
-pipeline_state     -- Aktueller Pipeline-Zustand (Singleton)
-

Tabelle: jobs

Die wichtigste Tabelle – speichert alle Ripping-Jobs.

CREATE TABLE jobs (
-  id              INTEGER PRIMARY KEY AUTOINCREMENT,
-  created_at      TEXT NOT NULL,
-  updated_at      TEXT NOT NULL,
-  status          TEXT NOT NULL,        -- Aktueller Status
-  title           TEXT,                 -- Filmtitel (von OMDb)
-  imdb_id         TEXT,                 -- IMDb-ID
-  omdb_year       TEXT,                 -- Erscheinungsjahr
-  omdb_type       TEXT,                 -- movie/series
-  omdb_poster     TEXT,                 -- Poster-URL
-  raw_path        TEXT,                 -- Pfad zur Raw-MKV
-  output_path     TEXT,                 -- Pfad zur Ausgabedatei
-  playlist        TEXT,                 -- Gewählte Blu-ray Playlist
-  makemkv_output  TEXT,                 -- MakeMKV-Ausgabe (JSON)
-  mediainfo_output TEXT,                -- MediaInfo-Ausgabe (JSON)
-  encode_plan     TEXT,                 -- Encode-Plan (JSON)
-  handbrake_log   TEXT,                 -- HandBrake Log-Pfad
-  error_message   TEXT,                 -- Fehlermeldung bei ERROR
-  error_details   TEXT                  -- Detaillierte Fehler-Infos
-);
-

Job-Status-Werte

Status Beschreibung
ANALYZING MakeMKV analysiert die Disc
METADATA_SELECTION Wartet auf Benutzer-Metadaten-Auswahl
READY_TO_START Bereit zum Starten
RIPPING MakeMKV rippt die Disc
MEDIAINFO_CHECK MediaInfo analysiert die Raw-Datei
READY_TO_ENCODE Wartet auf Encode-Bestätigung
ENCODING HandBrake encodiert
FINISHED Erfolgreich abgeschlossen
ERROR Fehler aufgetreten

Tabelle: pipeline_state

Singleton-Tabelle für den aktuellen Pipeline-Zustand (immer genau 1 Zeile).

CREATE TABLE pipeline_state (
-  id          INTEGER PRIMARY KEY CHECK(id = 1),
-  state       TEXT NOT NULL DEFAULT 'IDLE',
-  job_id      INTEGER,                -- Aktiver Job (NULL wenn IDLE)
-  progress    REAL,                   -- Fortschritt 0-100
-  eta         TEXT,                   -- Geschätzte Restzeit
-  updated_at  TEXT NOT NULL
-);
-

Tabelle: settings_schema

Definiert alle verfügbaren Einstellungen mit Metadaten.

CREATE TABLE settings_schema (
-  key          TEXT PRIMARY KEY,
-  category     TEXT NOT NULL,      -- paths, tools, encoding, ...
-  type         TEXT NOT NULL,      -- string, number, boolean, select
-  label        TEXT NOT NULL,      -- Anzeigename
-  description  TEXT,               -- Hilfetext
-  default_val  TEXT,               -- Standardwert
-  required     INTEGER,            -- 1 = Pflichtfeld
-  min_val      REAL,               -- Minimalwert (für number)
-  max_val      REAL,               -- Maximalwert (für number)
-  options      TEXT                -- JSON-Array für select-Typ
-);
-

Tabelle: settings_values

Speichert benutzer-konfigurierte Werte.

CREATE TABLE settings_values (
-  key        TEXT PRIMARY KEY REFERENCES settings_schema(key),
-  value      TEXT NOT NULL,
-  updated_at TEXT NOT NULL
-);
-

Schema-Migrationen

database.js implementiert automatische Migrationen:

  1. Beim Start wird das aktuelle Schema geprüft
  2. Fehlende Tabellen werden erstellt
  3. Fehlende Spalten werden hinzugefügt
  4. Neue Default-Einstellungen werden eingefügt

Korruptions-Recovery

Falls die Datenbankdatei korrupt ist:

1. Korrupte Datei wird erkannt (Verbindungsfehler / Integritätsprüfung)
-2. Datei wird in /backend/data/quarantine/ verschoben
-3. Neue, leere Datenbank wird erstellt
-4. Schema wird neu initialisiert
-5. Log-Eintrag mit Warnung
-

Datenbankpfad konfigurieren

Standard: ./data/ripster.db (relativ zum Backend-Verzeichnis)

Über Umgebungsvariable anpassen:

DB_PATH=/var/lib/ripster/ripster.db
-

Direkte Datenbankinspektion

# SQLite3-CLI
-sqlite3 backend/data/ripster.db
-
-# Alle Jobs anzeigen
-.mode table
-SELECT id, status, title, created_at FROM jobs ORDER BY created_at DESC;
-
-# Einstellungen anzeigen
-SELECT key, value FROM settings_values;
+ Datenbank - Ripster      

Datenbank

Ripster verwendet SQLite (backend/data/ripster.db).


Tabellen

settings_schema
+settings_values
+jobs
+pipeline_state
+scripts
+script_chains
+script_chain_steps
+user_presets
+cron_jobs
+cron_run_logs
+

jobs

Speichert Pipeline-Lifecycle und Artefakte pro Job.

Zentrale Felder:

  • Metadaten: title, year, imdb_id, poster_url, omdb_json, selected_from_omdb
  • Laufzeit: start_time, end_time, status, last_state
  • Pfade: raw_path, output_path, encode_input_path
  • Tool-Ausgaben: makemkv_info_json, handbrake_info_json, mediainfo_info_json, encode_plan_json
  • Kontrolle: encode_review_confirmed, rip_successful, error_message
  • Audit: created_at, updated_at

pipeline_state

Singleton-Tabelle (id = 1) für aktiven Snapshot:

  • state
  • active_job_id
  • progress
  • eta
  • status_text
  • context_json
  • updated_at

settings_schema + settings_values

  • settings_schema: Definition (Typ, Default, Validation, Reihenfolge)
  • settings_values: aktueller Wert pro Key

scripts, script_chains, script_chain_steps

  • scripts: Shell-Skripte (name, script_body, order_index)
  • script_chains: Ketten (name, order_index)
  • script_chain_steps: Schritte je Kette
  • step_type: script oder wait
  • script_id oder wait_seconds

user_presets

Benannte HandBrake-Preset-Sets:

  • name
  • media_type (bluray|dvd|other|all)
  • handbrake_preset
  • extra_args
  • description

cron_jobs + cron_run_logs

  • cron_jobs: Zeitplan + Status
  • cron_run_logs: einzelne Läufe
  • status: running|success|error
  • output
  • error_message

Migration/Recovery

Beim Start werden Schema und Settings-Metadaten automatisch abgeglichen.

Bei korruptem SQLite-File:

  1. Datei wird nach backend/data/corrupt-backups/ verschoben
  2. neue DB wird initialisiert
  3. Schema wird neu aufgebaut

Direkte Inspektion

sqlite3 backend/data/ripster.db
+
+.mode table
+SELECT id, status, title, created_at FROM jobs ORDER BY created_at DESC;
+SELECT key, value FROM settings_values ORDER BY key;
 
\ No newline at end of file diff --git a/site/architecture/frontend/index.html b/site/architecture/frontend/index.html index 4a7fea2..4906e6d 100644 --- a/site/architecture/frontend/index.html +++ b/site/architecture/frontend/index.html @@ -1,56 +1,6 @@ - Frontend-Komponenten - Ripster

Frontend-Komponenten

Das Frontend ist mit React 18 und PrimeReact gebaut und kommuniziert über REST-API und WebSocket mit dem Backend.


Seiten (Pages)

DashboardPage.jsx

Die Hauptseite von Ripster – zeigt den aktuellen Pipeline-Status und ermöglicht alle Workflow-Aktionen.

Funktionen: - Anzeige des aktuellen Pipeline-Zustands (IDLE, DISC_DETECTED, METADATA_SELECTION, RIPPING, MEDIAINFO_CHECK, READY_TO_ENCODE, ENCODING, ...) - Live-Fortschrittsbalken mit ETA - Trigger für Metadaten-Dialog - Playlist-Entscheidungs-UI (bei Blu-ray Obfuskierung) - Encode-Review mit Track-Auswahl - Job-Steuerung (Start, Abbruch, Retry, Queue-Interaktion)

Zugehörige Komponenten: - PipelineStatusCard – Status-Widget - MetadataSelectionDialog – OMDb-Suche und Playlist-Auswahl - MediaInfoReviewPanel – Track-Auswahl vor dem Encoding - Queue- und Job-Karten-UI direkt in DashboardPage

SettingsPage.jsx

Konfigurationsoberfläche für alle Ripster-Einstellungen.

Funktionen: - Dynamisch generiertes Formular aus dem Settings-Schema - Echtzeit-Validierungsfeedback - PushOver-Verbindungstest - Automatische Aktualisierung des Encode-Reviews bei relevanten Änderungen

DatabasePage.jsx (/history)

Job-Historie und Datenbankansicht mit vollständigem Audit-Trail.

Funktionen: - Sortier- und filterbares Job-Verzeichnis - Statusfilter (FINISHED, ERROR, WAITING_FOR_USER_DECISION, ...) - Job-Detail-Dialog mit vollständigen Logs - Re-Encode, Löschen und Metadaten-Zuweisung - Import von Orphan-Raw-Ordnern


Komponenten (Components)

MetadataSelectionDialog.jsx

Dialog für die Metadaten-Auswahl nach der Disc-Analyse.

┌─────────────────────────────────────┐
-│ Metadaten auswählen                 │
-├─────────────────────────────────────┤
-│ Suche: [Inception              ] 🔍 │
-├─────────────────────────────────────┤
-│ Ergebnisse:                         │
-│ ▶ Inception (2010) – Movie          │
-│   Inception: ... (2011) – Series    │
-├─────────────────────────────────────┤
-│ Playlist (nur Blu-ray):             │
-│ ▶ 00800.mpls (2:30:15) ✓ Empfohlen  │
-│   00801.mpls (0:01:23)              │
-├─────────────────────────────────────┤
-│                   [Bestätigen]      │
-└─────────────────────────────────────┘
-

MediaInfoReviewPanel.jsx

Track-Auswahl-Panel vor dem Encoding.

┌─────────────────────────────────────┐
-│ Encode-Review                       │
-├─────────────────────────────────────┤
-│ Audio-Tracks:                       │
-│ ☑ Track 1: Deutsch (AC-3, 5.1)     │
-│ ☑ Track 2: English (TrueHD, 7.1)   │
-│ ☐ Track 3: Français (AC-3, 2.0)    │
-├─────────────────────────────────────┤
-│ Untertitel:                         │
-│ ☑ Track 1: Deutsch                  │
-│ ☐ Track 2: English                  │
-├─────────────────────────────────────┤
-│                [Encoding starten]   │
-└─────────────────────────────────────┘
-

DynamicSettingsForm.jsx

Wiederverwendbares Formular, das aus dem Settings-Schema generiert wird.

Unterstützte Feldtypen:

Typ UI-Element
string Text-Input
number Zahlen-Input mit Min/Max
boolean Toggle/Checkbox
select Dropdown
password Passwort-Input

PipelineStatusCard.jsx

Status-Anzeige-Widget für die Dashboard-Seite.

JobDetailDialog.jsx

Vollständiger Job-Detail-Dialog mit Logs-Viewer.


Hooks

useWebSocket.js

Zentraler Custom-Hook für die WebSocket-Verbindung.

useWebSocket({
-  onMessage: (msg) => {
-    if (msg.type === 'PIPELINE_STATE_CHANGED') {
-      setPipelineState(msg.payload);
-    }
-  }
-});
-

Features: - Automatische Verbindung zu /ws - Reconnect mit festem Intervall (1500ms) - Message-Parsing (JSON)


API-Client (client.js)

Zentraler HTTP-Client für alle Backend-Anfragen.

// Beispiel-Aufrufe
-const state = await api.getPipelineState();
-const results = await api.searchOmdb('Inception');
-await api.selectMetadata({ jobId, title, year, imdbId, selectedPlaylist });
-await api.confirmEncodeReview(jobId, {
-  selectedEncodeTitleId: 1,
-  selectedTrackSelection: { 1: { audioTrackIds: [1], subtitleTrackIds: [3] } }
-});
-

Features: - Zentralisierte Fehlerbehandlung - Automatische JSON-Serialisierung - Basis-URL aus Umgebungsvariable (VITE_API_BASE)


Build & Entwicklung

Entwicklungsserver

cd frontend
-npm run dev
-# → http://localhost:5173
-

Vite-Proxy-Konfiguration

In der Entwicklungsumgebung proxied Vite API-Anfragen zum Backend:

// vite.config.js
-proxy: {
-  '/api': 'http://localhost:3001',
-  '/ws': { target: 'ws://localhost:3001', ws: true }
-}
-

Production-Build

cd frontend
-npm run build
-# → frontend/dist/
+ Frontend-Komponenten - Ripster      

Frontend-Komponenten

Frontend: React + PrimeReact + Vite.


Hauptseiten

DashboardPage.jsx

Pipeline-Steuerung:

  • Status/Progress/ETA
  • Metadaten-Dialog
  • Playlist-Entscheidung
  • Review-Panel
  • Queue-Interaktion (reorder/add/remove)
  • Job-Aktionen (Start/Cancel/Retry/Re-Encode)
  • Hardware-Monitoring-Anzeige

SettingsPage.jsx

Konfiguration:

  • dynamisches Settings-Formular (DynamicSettingsForm)
  • Skripte/Ketten inkl. Reorder/Test
  • User-Presets
  • Cron-Jobs (CronJobsTab)

HistoryPage.jsx

Historie:

  • Job-Liste/Filter
  • Job-Details + Logs
  • OMDb-Nachzuweisung
  • Re-Encode/Restart-Workflows

Wichtige Komponenten

  • PipelineStatusCard.jsx
  • MetadataSelectionDialog.jsx
  • MediaInfoReviewPanel.jsx
  • JobDetailDialog.jsx
  • CronJobsTab.jsx

API-Client (api/client.js)

  • zentraler request() mit JSON-Handling
  • Fehlerobjekt aus API wird auf Error(message) gemappt
  • VITE_API_BASE default /api

WebSocket (hooks/useWebSocket.js)

  • URL: VITE_WS_URL oder automatisch ws(s)://<host>/ws
  • Auto-Reconnect mit 1500ms Intervall

In App.jsx werden u. a. verarbeitet:

  • PIPELINE_STATE_CHANGED
  • PIPELINE_PROGRESS
  • PIPELINE_QUEUE_CHANGED
  • DISC_DETECTED / DISC_REMOVED
  • HARDWARE_MONITOR_UPDATE

Build/Run

# dev
+npm run dev --prefix frontend
+
+# prod build
+npm run build --prefix frontend
 
\ No newline at end of file diff --git a/site/architecture/index.html b/site/architecture/index.html index fec2e84..1a97434 100644 --- a/site/architecture/index.html +++ b/site/architecture/index.html @@ -1,72 +1,29 @@ - Architektur - Ripster

Architektur

Ripster ist als klassische Client-Server-Anwendung mit Echtzeit-Kommunikation über WebSockets aufgebaut.


Systemüberblick

graph TB
+ Architektur - Ripster      

Architektur

Ripster ist eine Client-Server-Anwendung mit REST + WebSocket.


Systemüberblick

graph TB
     subgraph Browser["Browser (React)"]
-        Dashboard["Dashboard"]
-        Settings["Einstellungen"]
-        History["History"]
+        Dashboard[Dashboard]
+        Settings[Einstellungen]
+        History[Historie]
     end
 
     subgraph Backend["Node.js Backend"]
-        API["REST API\n(Express)"]
-        WS["WebSocket\nServer"]
-        Pipeline["Pipeline\nService"]
-        DB["SQLite\nDatenbank"]
+        API[REST API\nExpress]
+        WS[WebSocket\n/ws]
+        Pipeline[pipelineService]
+        Cron[cronService]
+        DB[(SQLite)]
     end
 
-    subgraph ExternalTools["Externe Tools"]
-        MakeMKV["makemkvcon"]
-        HandBrake["HandBrakeCLI"]
-        MediaInfo["mediainfo"]
+    subgraph Tools["Externe Tools"]
+        MakeMKV[makemkvcon]
+        HandBrake[HandBrakeCLI]
+        MediaInfo[mediainfo]
     end
 
-    subgraph ExternalAPIs["Externe APIs"]
-        OMDb["OMDb API"]
-        PushOver["PushOver"]
-    end
-
-    Browser <-->|HTTP REST| API
+    Browser <-->|HTTP| API
     Browser <-->|WebSocket| WS
     Pipeline --> MakeMKV
     Pipeline --> HandBrake
     Pipeline --> MediaInfo
-    Pipeline <-->|Metadaten| OMDb
-    Pipeline -->|Benachrichtigungen| PushOver
     API --> DB
-    Pipeline --> DB

Schichten-Architektur

Backend

index.js (Express Server)
-├── Routes (API-Endpunkte)
-│   ├── pipelineRoutes.js
-│   ├── settingsRoutes.js
-│   └── historyRoutes.js
-├── Services (Business Logic)
-│   ├── pipelineService.js    ← Kern-Orchestrierung
-│   ├── diskDetectionService.js
-│   ├── processRunner.js
-│   ├── websocketService.js
-│   ├── omdbService.js
-│   ├── settingsService.js
-│   ├── notificationService.js
-│   ├── historyService.js
-│   └── logger.js
-├── Database
-│   ├── database.js
-│   └── defaultSettings.js
-└── Utils
-    ├── encodePlan.js
-    ├── playlistAnalysis.js
-    ├── progressParsers.js
-    └── files.js
-

Frontend

App.jsx (React Router)
-├── Pages
-│   ├── DashboardPage.jsx     ← Haupt-Interface
-│   ├── SettingsPage.jsx
-│   └── DatabasePage.jsx      ← Historie/DB-Ansicht
-├── Components
-│   ├── PipelineStatusCard.jsx
-│   ├── MetadataSelectionDialog.jsx
-│   ├── MediaInfoReviewPanel.jsx
-│   ├── DynamicSettingsForm.jsx
-│   └── JobDetailDialog.jsx
-├── Hooks
-│   └── useWebSocket.js
-└── API
-    └── client.js
-

Weiterführende Dokumentation

\ No newline at end of file + Pipeline --> DB + Cron --> DB

Schichten

Backend

  • src/index.js (Bootstrapping, Routes, WS, Services)
  • src/routes/* (Pipeline, Settings, History, Crons)
  • src/services/* (Business-Logik)
  • src/db/database.js (Init/Migration)
  • src/utils/* (Parser, Dateifunktionen, Validierung)

Frontend

  • App.jsx + pages/* (Dashboard, Settings, History)
  • components/* (Status-/Review-/Dialog-Komponenten)
  • api/client.js (REST-Client)
  • hooks/useWebSocket.js (WS-Reconnect)

Weiterführend

\ No newline at end of file diff --git a/site/architecture/overview/index.html b/site/architecture/overview/index.html index d35f007..9673d13 100644 --- a/site/architecture/overview/index.html +++ b/site/architecture/overview/index.html @@ -1,21 +1,11 @@ - Übersicht - Ripster

Architektur-Übersicht


Kern-Designprinzipien

Event-Driven Pipeline

Der gesamte Ripping-Workflow ist als State Machine implementiert. Der pipelineService verwaltet den aktuellen Zustand und emittiert Ereignisse bei jedem Zustandswechsel. Der WebSocket-Service überträgt diese Ereignisse sofort an alle verbundenen Clients.

Zustandswechsel → Event → WebSocket → Frontend-Update
-

Service-Layer-Muster

HTTP-Route → Service → Datenbank
-

Routes delegieren die gesamte Business-Logik an Services. Services sind voneinander unabhängig und können einzeln getestet werden.

Schema-getriebene Einstellungen

Die Settings-Konfiguration definiert sowohl die Validierungsregeln als auch die UI-Struktur in einer einzigen Quelle (settings_schema-Tabelle). Die DynamicSettingsForm-Komponente rendert das Formular dynamisch aus dem Schema.


Echtzeit-Kommunikation

WebSocket-Protokoll

Der WebSocket-Server läuft unter dem Pfad /ws. Nachrichten werden als JSON übertragen:

{
-  "type": "PIPELINE_STATE_CHANGED",
-  "payload": {
-    "state": "ENCODING",
-    "activeJobId": 42,
-    "progress": 73.5,
-    "eta": "00:12:34"
-  }
-}
-

Nachrichtentypen:

Typ Beschreibung
PIPELINE_STATE_CHANGED Pipeline-Zustand hat gewechselt
PIPELINE_PROGRESS Fortschritt (% und ETA)
PIPELINE_QUEUE_CHANGED Queue-Status geändert
DISC_DETECTED Disc wurde erkannt
DISC_REMOVED Disc wurde entfernt
PIPELINE_ERROR Pipeline-Fehler aufgetreten
DISK_DETECTION_ERROR Laufwerkserkennung-Fehler

Reconnect-Logik

Der Frontend-Hook useWebSocket.js implementiert automatisches Reconnect mit festem Intervall von 1500ms bei Verbindungsabbrüchen.


Prozess-Management

processRunner.js

Externe Tools (MakeMKV, HandBrake, MediaInfo) werden als Child Processes gestartet:

spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'] })
-
  • stdout/stderr werden zeilenweise gelesen und in Echtzeit verarbeitet
  • Progress-Parsing erfolgt über reguläre Ausdrücke in progressParsers.js
  • Graceful Shutdown: SIGINT → Timeout → SIGKILL
  • Prozess-Tracking: Aktive Prozesse werden registriert für sauberes Beenden

Datenpersistenz

SQLite-Datenbank

Ripster verwendet eine einzige SQLite-Datei für alle persistenten Daten:

backend/data/ripster.db
-

Tabellen:

Tabelle Inhalt
jobs Alle Rip-Jobs mit Status, Logs, Metadaten
pipeline_state Aktueller Pipeline-Zustand (Singleton)
settings_schema Schema aller verfügbaren Einstellungen
settings_values Benutzer-konfigurierte Werte

Migrations-Strategie

Beim Start prüft database.js automatisch, ob das Schema aktuell ist, und führt fehlende Migrationen aus. Korrupte Datenbankdateien werden in ein Quarantäne-Verzeichnis verschoben und eine neue Datenbank erstellt.


Fehlerbehandlung

Strukturierte Fehler

Alle Fehler werden mit Kontext-Metadaten protokolliert:

logger.error('Encoding fehlgeschlagen', {
-  jobId: job.id,
-  command: cmd,
-  exitCode: code,
-  stderr: lastLines
-});
-

Job-Fehler-Recovery

  • Fehlgeschlagene Jobs bleiben in der Datenbank (Status ERROR)
  • Vollständige Fehler-Logs werden im Job-Datensatz gespeichert
  • Retry-Funktion ermöglicht Neustart von einem Fehler-Zustand
  • Re-Encode erlaubt erneutes Encodieren ohne neu zu rippen

Sicherheit

Eingabe-Validierung

  • Alle Benutzer-Eingaben werden in validators.js validiert
  • CLI-Argumente werden sicher über commandLine.js konstruiert (kein Shell-Injection-Risiko)
  • Pfade werden sanitisiert bevor sie an externe Prozesse übergeben werden

CORS-Konfiguration

CORS_ORIGIN=http://localhost:5173
-

In Produktion sollte dieser Wert auf die tatsächliche Frontend-URL gesetzt werden.

\ No newline at end of file + Übersicht - Ripster

Architektur-Übersicht


Kernprinzipien

Event-getriebene Pipeline

pipelineService hält einen Snapshot der State-Machine und broadcastet Änderungen sofort via WebSocket.

State-Änderung -> PIPELINE_STATE_CHANGED/PIPELINE_PROGRESS -> Frontend-Update
+

Service-Layer

Route -> Service -> DB/Tool-Execution
+

Routes enthalten kaum Business-Logik.

Schema-getriebene Settings

Settings sind DB-schema-getrieben (settings_schema + settings_values), UI rendert dynamisch aus diesen Daten.


Echtzeit-Kommunikation

WebSocket läuft auf /ws.

Wichtige Events:

  • PIPELINE_STATE_CHANGED, PIPELINE_PROGRESS, PIPELINE_QUEUE_CHANGED
  • DISC_DETECTED, DISC_REMOVED
  • HARDWARE_MONITOR_UPDATE
  • SETTINGS_UPDATED, SETTINGS_BULK_UPDATED
  • SETTINGS_SCRIPTS_UPDATED, SETTINGS_SCRIPT_CHAINS_UPDATED, USER_PRESETS_UPDATED
  • CRON_JOBS_UPDATED, CRON_JOB_UPDATED
  • PIPELINE_ERROR, DISK_DETECTION_ERROR

Prozessausführung

Externe Tools werden als Child-Processes gestartet (processRunner):

  • Streaming von stdout/stderr
  • Progress-Parsing (progressParsers.js)
  • kontrollierter Abbruch (SIGINT/SIGKILL-Fallback)

Persistenz

SQLite-Datei: backend/data/ripster.db

Kern-Tabellen:

  • jobs, pipeline_state
  • settings_schema, settings_values
  • scripts, script_chains, script_chain_steps
  • user_presets
  • cron_jobs, cron_run_logs

Beim Start werden Schema und Settings-Migrationen automatisch ausgeführt.


Fehlerbehandlung

Zentrales Error-Handling liefert:

{
+  "error": {
+    "message": "...",
+    "statusCode": 400,
+    "reqId": "...",
+    "details": []
+  }
+}
+

Fehlgeschlagene Jobs bleiben in der Historie (ERROR oder CANCELLED) und können erneut gestartet werden.


CORS & Runtime-Konfig

  • CORS_ORIGIN default: *
  • LOG_LEVEL default: info
  • DB-/Log-Pfade über DB_PATH/LOG_DIR konfigurierbar
\ No newline at end of file diff --git a/site/configuration/environment/index.html b/site/configuration/environment/index.html index f459863..ba611dd 100644 --- a/site/configuration/environment/index.html +++ b/site/configuration/environment/index.html @@ -1,19 +1,16 @@ - Umgebungsvariablen - Ripster

Umgebungsvariablen

Umgebungsvariablen überschreiben die Standardwerte und eignen sich für Server-Deployments.


Backend-Umgebungsvariablen

Konfigurationsdatei: backend/.env

Variable Standard Beschreibung
PORT 3001 Port des Express-Servers
DB_PATH ./data/ripster.db Pfad zur SQLite-Datenbankdatei
CORS_ORIGIN http://localhost:5173 Erlaubter CORS-Origin
LOG_DIR ./logs Verzeichnis für Log-Dateien
LOG_LEVEL info Log-Level (debug, info, warn, error)

Beispiel: backend/.env

PORT=3001
+ Umgebungsvariablen - Ripster      

Umgebungsvariablen

Umgebungsvariablen steuern Backend/Vite außerhalb der DB-basierten UI-Settings.


Backend (backend/.env)

Variable Default (Code) Beschreibung
PORT 3001 Express-Port
DB_PATH backend/data/ripster.db SQLite-Datei (relativ zu backend/)
LOG_DIR backend/logs Fallback-Logverzeichnis (wenn log_dir-Setting nicht gesetzt/lesbar)
CORS_ORIGIN * CORS-Origin für API
LOG_LEVEL info debug, info, warn, error

Beispiel:

PORT=3001
 DB_PATH=/var/lib/ripster/ripster.db
-CORS_ORIGIN=http://192.168.1.100:5173
-LOG_DIR=/var/log/ripster
+LOG_DIR=/var/log/ripster
+CORS_ORIGIN=http://192.168.1.50:5173
 LOG_LEVEL=info
-

Frontend-Umgebungsvariablen

Konfigurationsdatei: frontend/.env

Variable Standard Beschreibung
VITE_API_BASE http://localhost:3001 Backend-API-URL
VITE_WS_URL ws://localhost:3001 WebSocket-URL
VITE_PUBLIC_ORIGIN Öffentliche Origin-URL (für CORS)
VITE_HMR_HOST Vite HMR-Host (für Remote-Entwicklung)
VITE_HMR_PORT Vite HMR-Port

Beispiel: frontend/.env (Entwicklung)

VITE_API_BASE=http://localhost:3001
-VITE_WS_URL=ws://localhost:3001
-

Beispiel: frontend/.env (Netzwerk-Zugriff)

VITE_API_BASE=http://192.168.1.100:3001
-VITE_WS_URL=ws://192.168.1.100:3001
-VITE_PUBLIC_ORIGIN=http://192.168.1.100:5173
-

.env.example Dateien

Das Repository enthält Vorlagen für beide Konfigurationsdateien:

# Backend
-cp backend/.env.example backend/.env
-
-# Frontend
-cp frontend/.env.example frontend/.env
-

Priorität der Konfiguration

Einstellungen werden in folgender Reihenfolge geladen (höhere Priorität überschreibt niedrigere):

1. Systemumgebungsvariablen (export VAR=value)
-2. .env-Datei
-3. Hardcodierte Standardwerte in config.js
-

LOG_LEVEL

Level Ausgabe
debug Alle Meldungen inkl. Debugging
info Normale Betriebsinformationen
warn Warnungen + Fehler
error Nur Fehler

Produktionsempfehlung

Für Produktionsumgebungen LOG_LEVEL=info oder LOG_LEVEL=warn verwenden. debug erzeugt sehr viele Log-Einträge.

\ No newline at end of file +

Hinweis: backend/.env.example enthält bewusst dev-freundliche Werte (z. B. lokaler CORS_ORIGIN).


Frontend (frontend/.env)

Variable Default Beschreibung
VITE_API_BASE /api API-Basis für Fetch-Client
VITE_WS_URL automatisch aus window.location + /ws Optional explizite WebSocket-URL
VITE_PUBLIC_ORIGIN leer Öffentliche Vite-Origin (Remote-Dev)
VITE_ALLOWED_HOSTS true Komma-separierte Hostliste für Vite allowedHosts
VITE_HMR_PROTOCOL abgeleitet aus VITE_PUBLIC_ORIGIN HMR-Protokoll (ws/wss)
VITE_HMR_HOST abgeleitet aus VITE_PUBLIC_ORIGIN HMR-Host
VITE_HMR_CLIENT_PORT abgeleitet aus VITE_PUBLIC_ORIGIN HMR-Client-Port

Beispiele:

# lokal (mit Vite-Proxy)
+VITE_API_BASE=/api
+
# remote dev
+VITE_API_BASE=http://192.168.1.50:3001/api
+VITE_WS_URL=ws://192.168.1.50:3001/ws
+VITE_PUBLIC_ORIGIN=http://192.168.1.50:5173
+VITE_ALLOWED_HOSTS=192.168.1.50,ripster.local
+VITE_HMR_PROTOCOL=ws
+VITE_HMR_HOST=192.168.1.50
+VITE_HMR_CLIENT_PORT=5173
+

Priorität

  1. Prozess-Umgebungsvariablen
  2. .env
  3. Code-Defaults
\ No newline at end of file diff --git a/site/configuration/index.html b/site/configuration/index.html index d592a03..a4c7ec5 100644 --- a/site/configuration/index.html +++ b/site/configuration/index.html @@ -1 +1 @@ - Konfiguration - Ripster

Konfiguration

  • Einstellungsreferenz


    Alle verfügbaren Einstellungen mit Typen, Standardwerten und Beschreibungen.

    Einstellungsreferenz

  • Umgebungsvariablen


    Umgebungsvariablen für Backend und Frontend.

    Umgebungsvariablen

\ No newline at end of file + Konfiguration - Ripster

Konfiguration

  • Einstellungsreferenz


    Alle verfügbaren Einstellungen mit Typen, Standardwerten und Beschreibungen.

    Einstellungsreferenz

  • Umgebungsvariablen


    Umgebungsvariablen für Backend und Frontend.

    Umgebungsvariablen

\ No newline at end of file diff --git a/site/configuration/settings-reference/index.html b/site/configuration/settings-reference/index.html index fb0a88e..f5e9733 100644 --- a/site/configuration/settings-reference/index.html +++ b/site/configuration/settings-reference/index.html @@ -1,10 +1 @@ - Alle Einstellungen - Ripster

Einstellungsreferenz

Vollständige Übersicht aller Ripster-Einstellungen. Alle Einstellungen werden über die Web-Oberfläche unter Einstellungen verwaltet.


Kategorie: Pfade (paths)

Schlüssel Typ Standard Pflicht Beschreibung
raw_dir string Verzeichnis für rohe MKV-Dateien nach dem Ripping
movie_dir string Ausgabeverzeichnis für encodierte Filme
log_dir string ./logs Verzeichnis für Log-Dateien

Beispielkonfiguration

raw_dir   = /mnt/nas/raw
-movie_dir = /mnt/nas/movies
-log_dir   = /var/log/ripster
-

Kategorie: Tools (tools)

Schlüssel Typ Standard Beschreibung
makemkv_command string makemkvcon Befehl oder absoluter Pfad zu MakeMKV
handbrake_command string HandBrakeCLI Befehl oder absoluter Pfad zu HandBrake
mediainfo_command string mediainfo Befehl oder absoluter Pfad zu MediaInfo

Absolute Pfade verwenden

Falls die Tools nicht im PATH des Systems sind:

makemkv_command   = /usr/local/bin/makemkvcon
-handbrake_command = /usr/local/bin/HandBrakeCLI
-mediainfo_command = /usr/bin/mediainfo
-


Kategorie: Encoding (encoding)

Schlüssel Typ Standard Beschreibung
handbrake_preset string H.265 MKV 1080p30 Name des HandBrake-Presets
handbrake_extra_args string (leer) Zusätzliche HandBrake CLI-Argumente
output_extension string mkv Dateiendung der Ausgabedatei
filename_template string {title} ({year}) Template für den Dateinamen

Verfügbare HandBrake-Presets

Eine vollständige Liste der verfügbaren Presets:

HandBrakeCLI --preset-list
-

Häufig verwendete Presets:

Preset Beschreibung
H.265 MKV 1080p30 H.265/HEVC, Full-HD, 30fps
H.265 MKV 720p30 H.265/HEVC, HD, 30fps
H.264 MKV 1080p30 H.264/AVC, Full-HD, 30fps
HQ 1080p30 Surround Hohe Qualität, Full-HD mit Surround

Dateiname-Template-Platzhalter

Platzhalter Beispiel
{title} Inception
{year} 2010
{imdb_id} tt1375666
{type} movie

Kategorie: Laufwerk (drive)

Schlüssel Typ Standard Optionen Beschreibung
drive_mode select auto auto, explicit Laufwerk-Erkennungsmodus
drive_device string /dev/sr0 Geräte-Pfad (nur bei explicit)
disc_poll_interval_ms number 4000 1000–60000 Polling-Intervall in Millisekunden

drive_mode Optionen:

Modus Beschreibung
auto Ripster erkennt das Laufwerk automatisch
explicit Verwendet das in drive_device konfigurierte Gerät

Kategorie: MakeMKV (makemkv)

Schlüssel Typ Standard Min Max Beschreibung
makemkv_min_length_minutes number 15 0 999 Mindest-Titellänge in Minuten
makemkv_backup_mode boolean false Backup-Modus statt MKV-Modus

makemkv_min_length_minutes: Titel kürzer als dieser Wert werden von MakeMKV ignoriert. Verhindert das Rippen von Menü-Schleifen und kurzen Extra-Clips.

makemkv_backup_mode: Im Backup-Modus erstellt MakeMKV eine vollständige Disc-Kopie mit Menüs. Im Standard-Modus werden direkt MKV-Dateien erstellt.


Kategorie: OMDb (omdb)

Schlüssel Typ Standard Pflicht Beschreibung
omdb_api_key string API-Key von omdbapi.com
omdb_default_type select movie Standard-Suchtyp: movie oder series

Kategorie: Benachrichtigungen (notifications)

Schlüssel Typ Standard Beschreibung
pushover_user_key string PushOver User-Key
pushover_api_token string PushOver API-Token

Beide Felder müssen konfiguriert sein, um PushOver-Benachrichtigungen zu aktivieren. Die Verbindung kann mit dem Test-Button in den Einstellungen geprüft werden.


Standard-Einstellungen zurücksetzen

Über die Datenbank können Einstellungen auf Standardwerte zurückgesetzt werden:

sqlite3 backend/data/ripster.db \
-  "DELETE FROM settings_values WHERE key = 'handbrake_preset';"
-

Beim nächsten Laden der Einstellungen wird der Standardwert verwendet.

\ No newline at end of file + Alle Einstellungen - Ripster

Einstellungsreferenz

Alle Settings liegen in settings_schema/settings_values und werden über die UI verwaltet.


Profil-System

Ripster arbeitet mit Media-Profilen:

  • bluray
  • dvd
  • other

Viele Tool-/Pfad-Settings existieren als Profil-Varianten (*_bluray, *_dvd, *_other).

Wichtig:

  • Für raw_dir, movie_dir und die zugehörigen *_owner-Keys gibt es kein Cross-Profil-Fallback.
  • Für viele Tool-Keys werden profilspezifische Varianten bevorzugt.

Template-Platzhalter

Datei-/Ordner-Templates unterstützen:

  • ${title}
  • ${year}
  • ${imdbId}

Nicht gesetzte Werte werden zu unknown.


Kategorie: Pfade

Key Typ Default
raw_dir path data/output/raw
raw_dir_bluray path null
raw_dir_dvd path null
raw_dir_other path null
raw_dir_bluray_owner string null
raw_dir_dvd_owner string null
raw_dir_other_owner string null
movie_dir path data/output/movies
movie_dir_bluray path null
movie_dir_dvd path null
movie_dir_other path null
movie_dir_bluray_owner string null
movie_dir_dvd_owner string null
movie_dir_other_owner string null
log_dir path data/logs

Kategorie: Laufwerk

Key Typ Default Hinweis
drive_mode select auto auto oder explicit
drive_device path /dev/sr0 bei explicit relevant
makemkv_source_index number 0 MakeMKV Source-Index
disc_poll_interval_ms number 4000 1000..60000

Kategorie: Monitoring

Key Typ Default
hardware_monitoring_enabled boolean true
hardware_monitoring_interval_ms number 5000

Kategorie: Tools (global)

Key Typ Default
makemkv_command string makemkvcon
makemkv_registration_key string null
mediainfo_command string mediainfo
makemkv_min_length_minutes number 60
handbrake_command string HandBrakeCLI
handbrake_restart_delete_incomplete_output boolean true
pipeline_max_parallel_jobs number 1

Blu-ray-spezifisch

Key Typ Default
mediainfo_extra_args_bluray string null
makemkv_rip_mode_bluray select backup
makemkv_analyze_extra_args_bluray string null
makemkv_rip_extra_args_bluray string null
handbrake_preset_bluray string H.264 MKV 1080p30
handbrake_extra_args_bluray string null
output_extension_bluray select mkv
filename_template_bluray string ${title} (${year})
output_folder_template_bluray string null

DVD-spezifisch

Key Typ Default
mediainfo_extra_args_dvd string null
makemkv_rip_mode_dvd select mkv
makemkv_analyze_extra_args_dvd string null
makemkv_rip_extra_args_dvd string null
handbrake_preset_dvd string H.264 MKV 480p30
handbrake_extra_args_dvd string null
output_extension_dvd select mkv
filename_template_dvd string ${title} (${year})
output_folder_template_dvd string null

Kategorie: Metadaten

Key Typ Default
omdb_api_key string null
omdb_default_type select movie

Kategorie: Benachrichtigungen (PushOver)

Key Typ Default
pushover_enabled boolean false
pushover_token string null
pushover_user string null
pushover_device string null
pushover_title_prefix string Ripster
pushover_priority number 0
pushover_timeout_ms number 7000
pushover_notify_metadata_ready boolean true
pushover_notify_rip_started boolean true
pushover_notify_encoding_started boolean true
pushover_notify_job_finished boolean true
pushover_notify_job_error boolean true
pushover_notify_job_cancelled boolean true
pushover_notify_reencode_started boolean true
pushover_notify_reencode_finished boolean true

Entfernte Legacy-Keys

Diese Legacy-Keys werden bei Migration entfernt und sollten nicht mehr genutzt werden:

  • makemkv_backup_mode
  • mediainfo_extra_args
  • makemkv_rip_mode
  • makemkv_analyze_extra_args
  • makemkv_rip_extra_args
  • handbrake_preset
  • handbrake_extra_args
  • output_extension
  • filename_template
  • output_folder_template
  • pushover_notify_disc_detected
\ No newline at end of file diff --git a/site/deployment/development/index.html b/site/deployment/development/index.html index 6182314..5dcec5f 100644 --- a/site/deployment/development/index.html +++ b/site/deployment/development/index.html @@ -1,39 +1,24 @@ - Entwicklungsumgebung - Ripster

Entwicklungsumgebung


Voraussetzungen


Schnellstart

./start.sh
-

Das Skript startet automatisch: - Backend auf Port 3001 (mit Nodemon für Hot-Reload) - Frontend auf Port 5173 (mit Vite HMR)


Manuelle Entwicklungsumgebung

Terminal 1 – Backend

cd backend
+ Entwicklungsumgebung - Ripster      

Entwicklungsumgebung


Voraussetzungen

  • Node.js >= 20.19.0
  • externe Tools installiert (makemkvcon, HandBrakeCLI, mediainfo)

Schnellstart

./start.sh
+

Startet:

  • Backend (http://localhost:3001, mit nodemon)
  • Frontend (http://localhost:5173, mit Vite HMR)

Stoppen: Ctrl+C.


Manuell

Backend

cd backend
 npm install
 npm run dev
-

Backend läuft auf http://localhost:3001 mit Nodemon – Neustart bei Dateiänderungen.

Terminal 2 – Frontend

cd frontend
+

Frontend

cd frontend
 npm install
 npm run dev
-

Frontend läuft auf http://localhost:5173 mit Vite HMR – sofortige Browser-Updates.


Vite-Proxy

Im Entwicklungsmodus proxied Vite alle API- und WebSocket-Anfragen zum Backend:

// frontend/vite.config.js
-server: {
-  proxy: {
-    '/api': {
-      target: 'http://localhost:3001',
-      changeOrigin: true
-    },
-    '/ws': {
-      target: 'ws://localhost:3001',
-      ws: true
-    }
-  }
-}
-

Das bedeutet: Im Browser macht das Frontend Anfragen an localhost:5173/api/... – Vite leitet diese an localhost:3001/api/... weiter.


Remote-Entwicklung

Falls Ripster auf einem entfernten Server entwickelt wird (z.B. Homeserver), muss die Vite-Konfiguration angepasst werden:

# frontend/.env.local
-VITE_API_BASE=http://192.168.1.100:3001
-VITE_WS_URL=ws://192.168.1.100:3001
-VITE_HMR_HOST=192.168.1.100
-VITE_HMR_PORT=5173
-

Log-Level für Entwicklung

# backend/.env
-LOG_LEVEL=debug
-

Im Debug-Modus werden alle Ausgaben der externen Tools (MakeMKV, HandBrake) vollständig geloggt.


Stoppen

./kill.sh
-

Linting & Type-Checking

# Frontend (ESLint)
-cd frontend && npm run lint
-
-# Backend hat keine separaten Lint-Scripts,
-# nutze direkt eslint falls konfiguriert
-

Deployment-Script

Das deploy-ripster.sh-Script überträgt Code auf einen Remote-Server per SSH:

./deploy-ripster.sh
-

Was das Script tut: 1. rsync synchronisiert den Code (Backend-Quellcode ohne data/) 2. Die Datenbank (backend/data/) wird nicht überschrieben 3. Verbindung via SSH (konfigurierbar im Script)

Anpassung des Scripts:

# deploy-ripster.sh
-REMOTE_HOST="192.168.1.100"
-REMOTE_USER="michael"
-REMOTE_PATH="/home/michael/ripster"
-
\ No newline at end of file +

Vite-Proxy (Dev)

frontend/vite.config.js proxied standardmäßig:

  • /api -> http://127.0.0.1:3001
  • /ws -> ws://127.0.0.1:3001

Remote-Dev (optional)

Beispiel frontend/.env.local:

VITE_API_BASE=http://192.168.1.100:3001/api
+VITE_WS_URL=ws://192.168.1.100:3001/ws
+VITE_PUBLIC_ORIGIN=http://192.168.1.100:5173
+VITE_ALLOWED_HOSTS=192.168.1.100,ripster.local
+VITE_HMR_PROTOCOL=ws
+VITE_HMR_HOST=192.168.1.100
+VITE_HMR_CLIENT_PORT=5173
+

Nützliche Kommandos

# Root dev (backend + frontend)
+npm run dev
+
+# einzeln
+npm run dev:backend
+npm run dev:frontend
+
+# Frontend Build
+npm run build:frontend
+

Deploy-Script (optional)

deploy-ripster.sh synchronisiert den lokalen Stand auf einen Remote-Host per rsync/SSH und schützt backend/data.

\ No newline at end of file diff --git a/site/deployment/index.html b/site/deployment/index.html index 779d9c4..d5d4f34 100644 --- a/site/deployment/index.html +++ b/site/deployment/index.html @@ -1 +1 @@ - Deployment - Ripster

Deployment

  • Entwicklungsumgebung


    Lokale Entwicklungsumgebung einrichten.

    Entwicklung

  • Produktion


    Ripster auf einem Server dauerhaft betreiben.

    Produktion

\ No newline at end of file + Deployment - Ripster

Deployment

  • Entwicklungsumgebung


    Lokale Entwicklungsumgebung einrichten.

    Entwicklung

  • Produktion


    Ripster auf einem Server dauerhaft betreiben.

    Produktion

\ No newline at end of file diff --git a/site/deployment/production/index.html b/site/deployment/production/index.html index 080403b..8ba1805 100644 --- a/site/deployment/production/index.html +++ b/site/deployment/production/index.html @@ -1,99 +1,57 @@ - Produktion - Ripster

Produktions-Deployment


Empfohlene Architektur

Internet / Heimnetz
-        ↓
-   nginx (Reverse Proxy)
-        ↓
-   ┌────┴────┐
-   │         │
-Backend   Frontend
- :3001     (statische Dateien)
-

systemd-Service

Für ein dauerhaftes Betreiben als systemd-Service:

sudo nano /etc/systemd/system/ripster.service
-
[Unit]
-Description=Ripster - Disc Ripping Service
+ Produktion - Ripster      

Produktions-Deployment


Empfohlene Architektur

Client
+  -> nginx (Reverse Proxy + statisches Frontend)
+    -> Backend API/WebSocket (Node.js, Port 3001)
+

Wichtig: Das Backend serviert im aktuellen Stand keine frontend/dist-Dateien automatisch.


1) Frontend builden

cd frontend
+npm install
+npm run build
+

Artefakte liegen in frontend/dist/.


2) Backend als systemd-Service

Beispiel /etc/systemd/system/ripster-backend.service:

[Unit]
+Description=Ripster Backend
 After=network.target
 
 [Service]
 Type=simple
-User=michael
-WorkingDirectory=/home/michael/ripster
-ExecStart=/bin/bash /home/michael/ripster/start.sh
-ExecStop=/bin/bash /home/michael/ripster/kill.sh
-Restart=on-failure
-RestartSec=10s
-
-# Umgebungsvariablen
-Environment=NODE_ENV=production
-Environment=PORT=3001
-Environment=LOG_LEVEL=info
-
-[Install]
-WantedBy=multi-user.target
-
# Service aktivieren und starten
-sudo systemctl daemon-reload
-sudo systemctl enable ripster
-sudo systemctl start ripster
-
-# Status prüfen
-sudo systemctl status ripster
-
-# Logs anzeigen
-journalctl -u ripster -f
-

Frontend-Build

Für Produktion das Frontend bauen:

cd frontend
-npm run build
-

Die statischen Dateien landen in frontend/dist/.


nginx-Konfiguration

# /etc/nginx/sites-available/ripster
-server {
-    listen 80;
-    server_name ripster.local;
-
-    # Statisches Frontend
-    root /home/michael/ripster/frontend/dist;
-    index index.html;
-
-    # SPA Fallback (React Router)
-    location / {
-        try_files $uri $uri/ /index.html;
-    }
-
-    # API-Proxy zum Backend
-    location /api/ {
-        proxy_pass http://localhost:3001;
-        proxy_set_header Host $host;
-        proxy_set_header X-Real-IP $remote_addr;
-    }
-
-    # WebSocket-Proxy
-    location /ws {
-        proxy_pass http://localhost:3001;
-        proxy_http_version 1.1;
-        proxy_set_header Upgrade $http_upgrade;
-        proxy_set_header Connection "upgrade";
-        proxy_set_header Host $host;
-    }
-}
-
sudo ln -s /etc/nginx/sites-available/ripster /etc/nginx/sites-enabled/
-sudo nginx -t && sudo systemctl reload nginx
-

Nur-Backend-Produktion (ohne nginx)

Falls kein Reverse Proxy gewünscht ist, kann das Backend die Frontend-Dateien direkt ausliefern:

# Frontend bauen
-cd frontend && npm run build
-
-# Backend startet und serviert frontend/dist/
-cd backend && NODE_ENV=production npm start
-

Das Backend ist so konfiguriert, dass es im Produktionsmodus die frontend/dist/-Dateien als statische Assets ausliefert.


Datenbank-Backup

# Datenbank sichern
-cp backend/data/ripster.db backend/data/ripster.db.backup.$(date +%Y%m%d)
-
-# Oder mit SQLite-eigenem Backup-Befehl
-sqlite3 backend/data/ripster.db ".backup '/mnt/backup/ripster.db'"
-

Automatisches Backup

Cron-Job für tägliches Backup:

0 3 * * * sqlite3 /home/michael/ripster/backend/data/ripster.db ".backup '/mnt/backup/ripster-$(date +\%Y\%m\%d).db'"
-


Log-Rotation

Ripster rotiert Logs automatisch täglich. Falls zusätzlich systemd-Journal-Rotation gewünscht ist:

# /etc/logrotate.d/ripster
-/home/michael/ripster/backend/logs/*.log {
-    daily
-    rotate 14
-    compress
-    missingok
-    notifempty
-}
-

Sicherheitshinweise

Heimnetz-Einsatz

Ripster ist für den Einsatz im lokalen Heimnetz konzipiert und enthält keine Authentifizierung. Stelle sicher, dass der Dienst nicht öffentlich erreichbar ist.

Falls öffentlicher Zugang benötigt wird:

  1. Basic Auth via nginx:

    sudo htpasswd -c /etc/nginx/.htpasswd michael
    -
    location / {
    -    auth_basic "Ripster";
    -    auth_basic_user_file /etc/nginx/.htpasswd;
    -    # ...
    -}
    -

  2. VPN-Zugang (empfohlen): Zugriff nur über WireGuard/OpenVPN

  3. SSL/TLS: Let's Encrypt mit certbot für HTTPS

\ No newline at end of file +
User=ripster +WorkingDirectory=/opt/ripster/backend +ExecStart=/usr/bin/env node src/index.js +Restart=on-failure +RestartSec=5 +Environment=NODE_ENV=production +Environment=PORT=3001 +Environment=LOG_LEVEL=info + +[Install] +WantedBy=multi-user.target +

Aktivieren:

sudo systemctl daemon-reload
+sudo systemctl enable --now ripster-backend
+sudo systemctl status ripster-backend
+

3) nginx konfigurieren

Beispiel /etc/nginx/sites-available/ripster:

server {
+    listen 80;
+    server_name ripster.local;
+
+    root /opt/ripster/frontend/dist;
+    index index.html;
+
+    location / {
+        try_files $uri $uri/ /index.html;
+    }
+
+    location /api/ {
+        proxy_pass http://127.0.0.1:3001;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+    }
+
+    location /ws {
+        proxy_pass http://127.0.0.1:3001;
+        proxy_http_version 1.1;
+        proxy_set_header Upgrade $http_upgrade;
+        proxy_set_header Connection "upgrade";
+        proxy_set_header Host $host;
+    }
+}
+

Aktivieren:

sudo ln -s /etc/nginx/sites-available/ripster /etc/nginx/sites-enabled/
+sudo nginx -t
+sudo systemctl reload nginx
+

Datenbank-Backup

sqlite3 /opt/ripster/backend/data/ripster.db \
+  ".backup '/var/backups/ripster-$(date +%Y%m%d).db'"
+

Sicherheit

  • Ripster hat keine eingebaute Authentifizierung.
  • Für externen Zugriff mindestens Basic Auth + TLS + Netzwerksegmentierung/VPN einsetzen.
  • Secrets nicht ins Repo committen (.env, Settings-Felder).
\ No newline at end of file diff --git a/site/getting-started/configuration/index.html b/site/getting-started/configuration/index.html index 153b93f..95146a4 100644 --- a/site/getting-started/configuration/index.html +++ b/site/getting-started/configuration/index.html @@ -1,7 +1,3 @@ - Konfiguration - Ripster

Konfiguration

Alle Einstellungen werden über die Web-Oberfläche unter Einstellungen verwaltet und in der SQLite-Datenbank gespeichert.


Pflichteinstellungen

Diese Einstellungen müssen vor dem ersten Rip konfiguriert werden:

Pfade

Einstellung Beschreibung Beispiel
raw_dir Verzeichnis für rohe MKV-Dateien /mnt/nas/raw
movie_dir Ausgabeverzeichnis für kodierte Filme /mnt/nas/movies
log_dir Verzeichnis für Log-Dateien /var/log/ripster

Berechtigungen

Der Ripster-Prozess benötigt Schreibrechte auf alle konfigurierten Verzeichnisse.

# Verzeichnisse erstellen und Berechtigungen setzen
-sudo mkdir -p /mnt/nas/{raw,movies}
-sudo chown $USER:$USER /mnt/nas/{raw,movies}
-

OMDb API

Einstellung Beschreibung
omdb_api_key API-Key von omdbapi.com
omdb_default_type Standard-Suchtyp: movie oder series

Tool-Konfiguration

Einstellung Standard Beschreibung
makemkv_command makemkvcon Pfad oder Befehl für MakeMKV
handbrake_command HandBrakeCLI Pfad oder Befehl für HandBrake
mediainfo_command mediainfo Pfad oder Befehl für MediaInfo

Absolute Pfade

Falls die Tools nicht im PATH sind, verwende absolute Pfade:

/usr/local/bin/HandBrakeCLI
-


Encoding-Konfiguration

Einstellung Standard Beschreibung
handbrake_preset H.265 MKV 1080p30 HandBrake-Preset-Name
handbrake_extra_args (leer) Zusätzliche HandBrake-Argumente
output_extension mkv Dateiendung der Ausgabedatei
filename_template {title} ({year}) Template für Dateinamen

Dateiname-Template

Das Template unterstützt folgende Platzhalter:

Platzhalter Beschreibung Beispiel
{title} Filmtitel Inception
{year} Erscheinungsjahr 2010
{imdb_id} IMDb-ID tt1375666
{type} movie oder series movie

Beispiel-Template:

{title} ({year})
-→ Inception (2010).mkv
-


Laufwerk-Konfiguration

Einstellung Standard Beschreibung
drive_mode auto auto (automatisch erkennen) oder explicit (festes Gerät)
drive_device /dev/sr0 Geräte-Pfad (nur bei explicit)
disc_poll_interval_ms 4000 Polling-Intervall in Millisekunden

MakeMKV-Konfiguration

Einstellung Standard Beschreibung
makemkv_min_length_minutes 15 Mindestlänge für Titel in Minuten
makemkv_backup_mode false Backup-Modus statt MKV-Modus

Backup-Modus

Im Backup-Modus erstellt MakeMKV eine vollständige Kopie der Disc (inkl. Menüs). Der Standardmodus erstellt direkt MKV-Dateien.


Benachrichtigungen (PushOver)

Einstellung Beschreibung
pushover_user_key Dein PushOver User-Key
pushover_api_token API-Token deiner PushOver-App

Nach der Eingabe kann die Verbindung mit dem Test-Button geprüft werden.


Vollständige Einstellungsreferenz

Eine vollständige Liste aller Einstellungen mit Typen, Validierung und Standardwerten findest du unter:

Einstellungsreferenz

\ No newline at end of file + Konfiguration - Ripster

Konfiguration

Die Hauptkonfiguration erfolgt über die UI (Settings) und wird in SQLite gespeichert.


Pflichteinstellungen vor dem ersten Rip

1) Pfade

Einstellung Beschreibung Beispiel
raw_dir Basisverzeichnis für RAW-Rips /mnt/ripster/raw
movie_dir Basisverzeichnis für finale Encodes /mnt/ripster/movies
log_dir Verzeichnis für Prozess-/Backend-Logs /mnt/ripster/logs

Optional profilspezifisch:

  • raw_dir_bluray, raw_dir_dvd, raw_dir_other
  • movie_dir_bluray, movie_dir_dvd, movie_dir_other

2) Tools

Einstellung Standard
makemkv_command makemkvcon
handbrake_command HandBrakeCLI
mediainfo_command mediainfo

3) OMDb

Einstellung Beschreibung
omdb_api_key API-Key von omdbapi.com
omdb_default_type movie, series, episode

Encode-Konfiguration (wichtig)

Ripster arbeitet profilspezifisch, typischerweise über:

  • Blu-ray: handbrake_preset_bluray, handbrake_extra_args_bluray, output_extension_bluray, filename_template_bluray
  • DVD: handbrake_preset_dvd, handbrake_extra_args_dvd, output_extension_dvd, filename_template_dvd

Template-Platzhalter

Verfügbar in filename_template_* und output_folder_template_*:

  • ${title}
  • ${year}
  • ${imdbId}

Beispiel:

${title} (${year})
+-> Inception (2010).mkv
+

MakeMKV-spezifisch

Einstellung Standard Hinweis
makemkv_min_length_minutes 60 Kandidaten-Filter
makemkv_rip_mode_bluray backup mkv oder backup
makemkv_rip_mode_dvd mkv mkv oder backup
makemkv_registration_key leer optional, wird via makemkvcon reg gesetzt

Monitoring & Queue

Einstellung Standard
hardware_monitoring_enabled true
hardware_monitoring_interval_ms 5000
pipeline_max_parallel_jobs 1

PushOver (optional)

Basis:

  • pushover_enabled
  • pushover_token
  • pushover_user

Zusätzlich pro Event ein/aus (z. B. pushover_notify_job_finished).


Verwandte Doku

\ No newline at end of file diff --git a/site/getting-started/index.html b/site/getting-started/index.html index 4fc6e1b..a87b48a 100644 --- a/site/getting-started/index.html +++ b/site/getting-started/index.html @@ -1 +1 @@ - Erste Schritte - Ripster

Erste Schritte

Dieser Abschnitt führt dich durch die Installation und Einrichtung von Ripster.

Überblick

  • :material-list-check: Voraussetzungen


    Systemanforderungen und externe Tools, die vor der Installation benötigt werden.

    Voraussetzungen prüfen

  • Installation


    Schritt-für-Schritt-Anleitung zur Installation von Ripster.

    Installation starten

  • Konfiguration


    Einrichten von Pfaden, API-Keys und Encoding-Presets.

    Konfigurieren

  • Schnellstart


    Rippe deinen ersten Film in wenigen Minuten.

    Loslegen

\ No newline at end of file + Erste Schritte - Ripster

Erste Schritte

Dieser Abschnitt führt dich durch die Installation und Einrichtung von Ripster.

Überblick

  • :material-list-check: Voraussetzungen


    Systemanforderungen und externe Tools, die vor der Installation benötigt werden.

    Voraussetzungen prüfen

  • Installation


    Schritt-für-Schritt-Anleitung zur Installation von Ripster.

    Installation starten

  • Konfiguration


    Einrichten von Pfaden, API-Keys und Encoding-Presets.

    Konfigurieren

  • Schnellstart


    Rippe deinen ersten Film in wenigen Minuten.

    Loslegen

\ No newline at end of file diff --git a/site/getting-started/installation/index.html b/site/getting-started/installation/index.html index a4471e7..aacc36e 100644 --- a/site/getting-started/installation/index.html +++ b/site/getting-started/installation/index.html @@ -1,41 +1,21 @@ - Installation - Ripster

Installation


Repository klonen

git clone https://github.com/YOUR_GITHUB_USERNAME/ripster.git
+ Installation - Ripster      

Installation


Repository klonen

git clone https://github.com/YOUR_GITHUB_USERNAME/ripster.git
 cd ripster
-

Automatischer Start

Ripster enthält ein start.sh-Skript, das alle Abhängigkeiten installiert und Backend + Frontend gleichzeitig startet:

./start.sh
-

Das Skript führt automatisch folgende Schritte durch:

  1. Node.js-Versionscheck – prüft ob >= 20.19.0 verfügbar ist (mit nvm/npx-Fallback)
  2. Abhängigkeiten installierennpm install für Root, Backend und Frontend
  3. Dienste starten – Backend und Frontend werden parallel gestartet

Erfolgreich gestartet

  • Backend läuft auf http://localhost:3001
  • Frontend läuft auf http://localhost:5173

Manuelle Installation

Falls du mehr Kontrolle benötigst:

# Root-Abhängigkeiten
-npm install
-
-# Backend-Abhängigkeiten
-cd backend && npm install && cd ..
-
-# Frontend-Abhängigkeiten
-cd frontend && npm install && cd ..
-
-# Backend starten (Terminal 1)
-cd backend && npm run dev
-
-# Frontend starten (Terminal 2)
-cd frontend && npm run dev
-

Umgebungsvariablen konfigurieren

Backend

cp backend/.env.example backend/.env
-

Bearbeite backend/.env:

PORT=3001
-DB_PATH=./data/ripster.db
-CORS_ORIGIN=http://localhost:5173
-LOG_DIR=./logs
-LOG_LEVEL=info
-

Frontend

cp frontend/.env.example frontend/.env
-

Bearbeite frontend/.env:

VITE_API_BASE=http://localhost:3001
-VITE_WS_URL=ws://localhost:3001
-

Alle Umgebungsvariablen

Eine vollständige Übersicht aller Umgebungsvariablen findest du unter Umgebungsvariablen.


Datenbank initialisieren

Die SQLite-Datenbank wird automatisch beim ersten Start erstellt und mit dem Schema aus db/schema.sql initialisiert. Es sind keine manuellen Datenbankschritte erforderlich.

backend/data/
-└── ripster.db    ← Wird automatisch angelegt
-

Stoppen

./kill.sh
-

Das Skript beendet Backend- und Frontend-Prozesse graceful.


Verzeichnisstruktur nach Installation

ripster/
-├── backend/
-│   ├── data/           ← SQLite-Datenbank (nach erstem Start)
-│   ├── logs/           ← Log-Dateien
-│   ├── node_modules/   ← Backend-Abhängigkeiten
-│   └── .env            ← Backend-Konfiguration
-├── frontend/
-│   ├── node_modules/   ← Frontend-Abhängigkeiten
-│   ├── dist/           ← Production-Build (nach npm run build)
-│   └── .env            ← Frontend-Konfiguration
-└── node_modules/       ← Root-Abhängigkeiten (concurrently etc.)
-

Nächste Schritte

Nach erfolgreicher Installation:

  1. Öffne http://localhost:5173
  2. Navigiere zu Einstellungen
  3. Konfiguriere Pfade, API-Keys und Encoding-Presets

Zur Konfiguration

\ No newline at end of file +

Dev-Start (empfohlen)

./start.sh
+

start.sh:

  1. prüft Node-Version (>= 20.19.0)
  2. installiert Dependencies (Root/Backend/Frontend)
  3. startet Backend + Frontend parallel

Danach:

  • Backend: http://localhost:3001
  • Frontend: http://localhost:5173

Stoppen: mit Ctrl+C im laufenden Terminal.


Manuell starten

npm install
+npm --prefix backend install
+npm --prefix frontend install
+npm run dev
+

Oder getrennt:

npm run dev:backend
+npm run dev:frontend
+

Optional: .env-Dateien anlegen

Backend

cp backend/.env.example backend/.env
+

Beispiel:

PORT=3001
+DB_PATH=./data/ripster.db
+LOG_DIR=./logs
+CORS_ORIGIN=http://localhost:5173
+LOG_LEVEL=info
+

Frontend

cp frontend/.env.example frontend/.env
+

Beispiel:

VITE_API_BASE=/api
+# optional:
+# VITE_WS_URL=ws://localhost:3001/ws
+

Datenbank

SQLite wird automatisch beim Backend-Start initialisiert:

backend/data/ripster.db
+

Schema-Quelle: db/schema.sql


Nächste Schritte

  1. Browser öffnen: http://localhost:5173
  2. In Settings Pfade/Tools/API-Keys prüfen
  3. Erste Disc einlegen und Workflow starten
\ No newline at end of file diff --git a/site/getting-started/prerequisites/index.html b/site/getting-started/prerequisites/index.html index 08dee0d..30e8930 100644 --- a/site/getting-started/prerequisites/index.html +++ b/site/getting-started/prerequisites/index.html @@ -1,4 +1,4 @@ - Voraussetzungen - Ripster

Voraussetzungen

Bevor du Ripster installierst, stelle sicher, dass folgende Software auf deinem System verfügbar ist.


System-Anforderungen

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ür Roh-MKVs)

Node.js

Ripster benötigt Node.js >= 20.19.0.

# nvm installieren
+ Voraussetzungen - Ripster      

Voraussetzungen

Bevor du Ripster installierst, stelle sicher, dass folgende Software auf deinem System verfügbar ist.


System-Anforderungen

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ür Roh-MKVs)

Node.js

Ripster benötigt Node.js >= 20.19.0.

# nvm installieren
 curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
 
 # Node.js 20 installieren
@@ -38,10 +38,13 @@
 
 # Version prüfen
 mediainfo --Version
-

Disc-Laufwerk

Ripster benötigt ein physisches DVD- oder Blu-ray-Laufwerk.

Blu-ray unter Linux

Für Blu-ray-Ripping unter Linux wird zusätzlich libaacs benötigt. MakeMKV bringt jedoch eine eigene Entschlüsselung mit, daher ist dies in den meisten Fällen nicht erforderlich.

# Laufwerk prüfen
+

Disc-Laufwerk

Ripster benötigt ein physisches DVD- oder Blu-ray-Laufwerk.

LibDriveIO-Modus erforderlich

Das Laufwerk muss im LibDriveIO-Modus betrieben werden – MakeMKV greift direkt auf Rohdaten des Laufwerks zu. Ohne diesen Modus können verschlüsselte Blu-rays (insbesondere UHD) nicht gelesen werden.

Nicht alle Laufwerke unterstützen den direkten Zugriff. Eine Anleitung zur Einrichtung und Liste kompatibler Laufwerke findet sich im MakeMKV-Forum.

# Laufwerk prüfen
 ls /dev/sr*
 # oder
 lsblk | grep rom
-

OMDb API-Key

Ripster verwendet die OMDb API für Filmmetadaten.

  1. Registriere dich kostenlos auf omdbapi.com
  2. Bestätige deine E-Mail-Adresse
  3. Notiere deinen API-Key – du gibst ihn später in den Einstellungen ein

Optionale Voraussetzungen

PushOver (Benachrichtigungen)

Für mobile Push-Benachrichtigungen bei Fertigstellung oder Fehlern:

  • App kaufen auf pushover.net (~5 USD einmalig)
  • User Key und API Token notieren

SSH-Zugang (Deployment)

Für Remote-Deployment via deploy-ripster.sh:

# sshpass installieren
+
+# Laufwerk-Berechtigungen setzen (erforderlich für LibDriveIO)
+sudo chmod a+rw /dev/sr0
+

Blu-ray unter Linux

MakeMKV bringt mit LibDriveIO eine eigene Entschlüsselung mit – externe Bibliotheken wie libaacs sind in der Regel nicht erforderlich.


OMDb API-Key

Ripster verwendet die OMDb API für Filmmetadaten.

  1. Registriere dich kostenlos auf omdbapi.com
  2. Bestätige deine E-Mail-Adresse
  3. Notiere deinen API-Key – du gibst ihn später in den Einstellungen ein

Optionale Voraussetzungen

PushOver (Benachrichtigungen)

Für mobile Push-Benachrichtigungen bei Fertigstellung oder Fehlern:

  • App kaufen auf pushover.net (~5 USD einmalig)
  • User Key und API Token notieren

SSH-Zugang (Deployment)

Für Remote-Deployment via deploy-ripster.sh:

# sshpass installieren
 sudo apt-get install sshpass
 

Checkliste

  • [ ] Node.js >= 20.19.0 installiert (node --version)
  • [ ] makemkvcon installiert (makemkvcon --version)
  • [ ] HandBrakeCLI installiert (HandBrakeCLI --version)
  • [ ] mediainfo installiert (mediainfo --Version)
  • [ ] DVD/Blu-ray Laufwerk vorhanden (ls /dev/sr*)
  • [ ] OMDb API-Key beschafft
\ No newline at end of file diff --git a/site/getting-started/quickstart/index.html b/site/getting-started/quickstart/index.html index 368ecd0..637ba2b 100644 --- a/site/getting-started/quickstart/index.html +++ b/site/getting-started/quickstart/index.html @@ -1,95 +1,12 @@ - Schnellstart - Ripster

Schnellstart – Vollständiger Workflow

Nach der Installation und Konfiguration führt diese Seite Schritt für Schritt durch den ersten Rip – mit allen Details aus dem Code.


Übersicht: Pipeline-Ablauf

IDLE
Warten
1
DISC_DETECTED
Disc erkannt
2
METADATA_SELECTION
OMDb & Dialog
WAITING_FOR_USER_DECISION
Playlist wählen
(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)
FINISHED
Fertig

Legende: ● Warten  |  ■ Läuft automatisch  |  ■ Benutzeraktion  |  ⚠ Optional  |  ■ Encodierung  |  ✓ Fertig

Vollständiges Zustandsdiagramm (inkl. Fehler- & Alternativpfade)
flowchart LR
-    START(( )) --> IDLE
-
-    IDLE -->|Disc erkannt| DD[DISC_DETECTED]
-    DD -->|Analyse starten| META[METADATA\nSELECTION]
-
-    META -->|Metadaten übernommen| RTS[READY_TO\nSTART]
-    META -->|vorhandenes RAW +\nPlaylist offen| WUD[WAITING_FOR\nUSER_DECISION]
-    RTS -->|Auto-Start| RIP[RIPPING]
-    RTS -->|Auto-Start mit RAW| MIC[MEDIAINFO\nCHECK]
-
-    RIP -->|MKV fertig| MIC
-    RIP -->|Fehler| ERR
-
-    MIC -->|Playlist offen (Backup)| WUD
-    WUD -->|Playlist bestätigt| MIC
-    WUD -->|Playlist bestätigt,\nnoch kein RAW| RTS
-
-    MIC --> RTE[READY_TO\nENCODE]
-    RTE -->|Encoding starten| ENC[ENCODING]
-
-    ENC -->|inkl. Post-Skripte| FIN([FINISHED])
-    ENC -->|Fehler| ERR
-
-    ERR([ERROR]) -->|Retry / Cancel| IDLE
-
-    style FIN fill:#e8f5e9,stroke:#66bb6a,color:#2e7d32
-    style ERR fill:#ffebee,stroke:#ef5350,color:#c62828
-    style WUD fill:#fff8e1,stroke:#ffa726,color:#e65100
-    style ENC fill:#f3e5f5,stroke:#ab47bc,color:#6a1b9a

Schritt 1 – Ripster starten

cd ripster
+ Schnellstart - Ripster      

Schnellstart – Erster kompletter Job

Diese Seite führt durch den typischen ersten Lauf.


1) Starten

cd ripster
 ./start.sh
-

Öffne http://localhost:5173 im Browser. Das Dashboard zeigt IDLE.


Schritt 2 – Disc einlegen → DISC_DETECTED

Lege eine DVD oder Blu-ray ein. Der diskDetectionService pollt das Laufwerk alle disc_poll_interval_ms Millisekunden (Standard: 4 Sekunden).

Was passiert im Code:

  • diskDetectionService emittiert discInserted mit Geräteinformationen
  • pipelineService.onDiscInserted() wird aufgerufen
  • Dashboard-Status-Badge zeigt "Medium erkannt"
  • Status-Text zeigt "Neue Disk erkannt"
  • Der "Analyse starten"-Button wird aktiv

Manuelle Auslösung

Falls die automatische Erkennung nicht greift:

curl -X POST http://localhost:3001/api/pipeline/analyze
-


Schritt 3 – Analyse starten → METADATA_SELECTION

Klicke auf "Analyse starten".

Was passiert im Code:

  1. Ein neuer Job-Datensatz wird in der Datenbank angelegt (status: METADATA_SELECTION)
  2. Ripster versucht, den Titel automatisch aus dem Disc-Label/Modell zu ermitteln
  3. Mit diesem erkannten Titel wird sofort eine OMDb-Suche ausgelöst
  4. Der MetadataSelectionDialog öffnet sich im Frontend mit den vorgeladenen Suchergebnissen

Erkannter Titel: Der Disc-Label (z. B. INCEPTION) wird als Suchbegriff verwendet. Falls kein Label vorhanden, bleibt das Suchfeld leer.


Schritt 4 – Metadaten auswählen (MetadataSelectionDialog)

Der Dialog zeigt vorgeladene OMDb-Suchergebnisse. Du kannst:

4a) OMDb-Suchergebnis wählen

┌─────────────────────────────────────────────────┐
-│ Suche: [Inception                          ] 🔍 │
-├─────────────────────────────────────────────────┤
-│ ▶ Inception (2010)  ·  Movie  ·  tt1375666      │
-│   Inception: ...    ·  Series ·  ...             │
-├─────────────────────────────────────────────────┤
-│                           [Auswahl übernehmen]  │
-└─────────────────────────────────────────────────┘
-
  • Suche durch Titel anpassen und Enter drücken
  • Typ-Filter: movie / series umschalten möglich
  • Einen Eintrag anklicken, dann "Auswahl übernehmen"

4b) Manuelle Eingabe (ohne OMDb)

Falls kein passendes Ergebnis gefunden wird: - Titel, Jahr und IMDb-ID manuell eingeben - OMDb-Poster wird übersprungen

Was passiert nach Bestätigung:

Ripster ruft pipelineService.selectMetadata() auf und startet den nächsten Schritt automatisch:

  • Job wird auf READY_TO_START gesetzt (kurzer Übergangszustand)
  • Falls bereits RAW vorhanden: direkter Sprung zu MEDIAINFO_CHECK
  • Falls kein RAW vorhanden: automatischer Start von RIPPING
  • Wenn bereits andere Jobs laufen, landet der Start stattdessen in der Queue

Schritt 5 – Optional: Playlist-Auswahl → WAITING_FOR_USER_DECISION

Dieser Zustand erscheint nur bei mehrdeutigen Blu-ray-Playlists (typisch nach RAW-Analyse im Backup-Modus).

Der Playlist-Auswahl-Dialog erscheint zusätzlich (nach dem Metadaten-Dialog):

┌───────────────────────────────────────────────────────────────┐
-│ Playlist-Auswahl                                              │
-│ Es wurden mehrere Titel mit ähnlicher Laufzeit gefunden.      │
-│ Bitte wähle die korrekte Playlist:                            │
-├───────────┬──────────┬────────┬──────────────────────────────┤
-│ Playlist  │ Laufzeit │ Score  │ Bewertung                     │
-├───────────┼──────────┼────────┼──────────────────────────────┤
-│ ● 00800   │ 2:28:05  │  +18   │ wahrscheinlich korrekt        │
-│           │          │        │ (lineare Segmentfolge)        │
-├───────────┼──────────┼────────┼──────────────────────────────┤
-│ ○ 00801   │ 2:28:12  │   −4   │ Auffällige Segmentreihenfolge │
-├───────────┼──────────┼────────┼──────────────────────────────┤
-│ ○ 00900   │ 2:28:05  │  −32   │ Fake-Struktur                 │
-│           │          │        │ (alternierendes Sprungmuster) │
-└───────────┴──────────┴────────┴──────────────────────────────┘
-  847 Playlists insgesamt · 3 relevante Kandidaten (≥ 15 min)
-  Empfehlung: 00800 (vorausgewählt)
-                                           [Playlist übernehmen]
-
  • Die empfohlene Playlist ist vorausgewählt (Checkbox)
  • Score und Bewertungslabel helfen bei der Entscheidung
  • Nach "Playlist übernehmen" setzt Ripster automatisch fort:
  • mit vorhandenem RAW in MEDIAINFO_CHECK
  • ohne RAW über READY_TO_START weiter Richtung RIPPING

Scoring-Details

Wie die Scores berechnet werden, erklärt die Playlist-Analyse-Seite.


Schritt 6 – Ripping → RIPPING

Vorher prüft Ripster: Existiert bereits eine Raw-Datei für diesen Job?

  • Ja, Raw-Datei vorhanden → Direkt zu Schritt 7 (Track-Review), kein erneutes Ripping
  • Nein → MakeMKV-Ripping startet

Im Standardfall startet Ripster diesen Schritt automatisch nach der Metadaten-Auswahl. Der Button "Job starten" ist hauptsächlich für Sonderfälle sichtbar (z. B. Fallback/Queue).

Was MakeMKV ausführt (MKV-Modus):

makemkvcon mkv disc:0 all /mnt/raw/Inception-2010/ \
-  --minlength=900 -r
-

Was MakeMKV ausführt (Backup-Modus):

makemkvcon backup disc:0 /mnt/raw/Inception-2010-backup/ \
-  --decrypt -r
-

Live-Fortschritt wird aus der MakeMKV-Ausgabe geparst:

PRGV:2048,0,65536  → Fortschritt wird berechnet und per WebSocket gesendet
-PRGT:5011,0,"Sichern..."  → Aktueller Task-Name
-

Typische Dauer: - DVD: 20–45 Minuten - Blu-ray: 45–120 Minuten


Schritt 7 – Track-Review → READY_TO_ENCODE

Nach dem Ripping, nach Playlist-Übernahme oder direkt bei vorhandenem RAW startet der HandBrake-Scan:

HandBrakeCLI --scan -i <quelle> -t 0
-

Dieser Scan liest alle Tracks aus ohne zu encodieren. Ripster baut daraus den Encode-Plan mit automatischer Vorauswahl:

Status: MEDIAINFO_CHECK – läuft automatisch, kein Benutzereingriff

Danach öffnet sich das Encode-Review-Panel (READY_TO_ENCODE):

┌─────────────────────────────────────────────────────────────────┐
-│ Encode-Review                                                   │
-│ Titel: Disc Title 1  ·  Laufzeit: 2:28:05  ·  28 Kapitel       │
-├─────────────────────────────────────────────────────────────────┤
-│ Audio-Spuren                                                    │
-├──────┬─────────────────────────────┬───────────────────────────┤
-│  ☑  │ Track 1: English (AC3, 5.1)  │ Copy (ac3)                │
-│  ☑  │ Track 2: Deutsch (DTS, 5.1)  │ Fallback Transcode (av_aac)│
-│  ☐  │ Track 3: Français (AC3, 2.0) │ Nicht übernommen          │
-├──────┴─────────────────────────────┴───────────────────────────┤
-│ Untertitel-Spuren                                               │
-├──────┬─────────────────────────────┬────────┬──────┬──────────┤
-│  ☑  │ Track 1: Deutsch             │ Einbr.☐ │Forc.☐│Default☑ │
-│  ☐  │ Track 2: English             │ Einbr.☐ │Forc.☐│Default☐ │
-├──────┴─────────────────────────────┴────────┴──────┴──────────┤
-│                                  [Encoding starten]            │
-└─────────────────────────────────────────────────────────────────┘
-

Audio-Track-Aktionen verstehen

Symbol/Text Bedeutung
Copy (ac3) Track wird verlustfrei direkt übernommen
Copy (truehd) TrueHD-Track wird direkt übernommen
Transcode (av_aac) Track wird zu AAC umgewandelt
Fallback Transcode (av_aac) Copy nicht möglich → automatisch zu AAC
Preset-Default (HandBrake) HandBrake-Preset entscheidet
Nicht übernommen Track ist nicht ausgewählt

Untertitel-Flags

Flag Bedeutung
Einbrennen Untertitel werden fest ins Video gebrannt (nur ein Track möglich)
Forced Nur erzwungene Untertitel-Einblendungen übernehmen
Default Diese Spur wird beim Abspielen automatisch aktiviert

Vorauswahl-Regeln

Die Tracks mit wurden nach der Regel aus den Einstellungen automatisch vorausgewählt (selectedByRule: true). Die Auswahl kann frei geändert werden.

Klicke "Encoding starten" (bzw. im Pre-Rip-Modus "Backup + Encoding starten"), um fortzufahren. Falls die Auswahl noch nicht bestätigt wurde, übernimmt das Frontend die Bestätigung automatisch beim Start.


Schritt 8 – Encoding → ENCODING

HandBrake startet mit dem finalisierten Plan:

HandBrakeCLI \
-  -i /dev/sr0 \
-  -o "/mnt/movies/Inception (2010).mkv" \
-  -t 1 \
-  --preset "H.265 MKV 1080p30" \
-  -a 1,2 \
-  -E copy:ac3,av_aac \
-  -s 1 \
-  --subtitle-default 1
-

Live-Fortschritt wird aus HandBrake-stderr geparst:

Encoding: task 1 of 1, 73.50 % (45.23 fps, avg 44.12 fps, ETA 00h12m34s)
-

Das Dashboard zeigt: - Fortschrittsbalken (0–100 %) - Aktuelle Encoding-Geschwindigkeit (FPS) - Geschätzte Restzeit (ETA)

Typische Dauer (abhängig von CPU/GPU und Preset): - Schnelles Preset (fast): 0.5× Echtzeit - Standard-Preset: 1–3× Echtzeit - Langsames Preset (slow): 5–10× Echtzeit


Schritt 9 – Fertig! → FINISHED

/mnt/nas/movies/
-└── Inception (2010).mkv   ✓ Encodierung abgeschlossen
-
  • Job-Status in der Datenbank: FINISHED
  • PushOver-Benachrichtigung (falls konfiguriert)
  • Eintrag in der History mit vollständigen Logs

Fehlerbehandlung

Job im Status ERROR

  1. Dashboard: Details-Button → Log-Ausgabe prüfen
  2. Retry: Job vom Fehlerzustand neu starten (behält Metadaten)
  3. History: Vollständige Logs und Fehlerdetails

Häufige Fehlerursachen

Fehler Ursache Lösung
MakeMKV: Lizenzfehler Abgelaufene Beta-Lizenz Neue Lizenz im MakeMKV-Forum
HandBrake: Preset nicht gefunden Preset-Name falsch HandBrakeCLI --preset-list prüfen
Keine Disc erkannt Laufwerk-Berechtigungen sudo chmod a+rw /dev/sr0
Falsches Video (zerstückelt) Falsche Playlist Job re-encodieren mit anderer Playlist
OMDb: Keine Ergebnisse API-Key fehlt oder Titel nicht gefunden Einstellungen prüfen; manuell eingeben

Kurzübersicht aller Schritte

# Status Benutzeraktion Was Ripster tut
1 IDLE Disc einlegen Disc-Polling erkennt Disc
2 DISC_DETECTED "Analyse starten" klicken Job anlegen, OMDb vorsuchen
3 METADATA_SELECTION Film im Dialog auswählen Start automatisch einplanen/auslösen
4 READY_TO_START meist keine Übergangszustand vor Auto-Start
5 RIPPING Warten MakeMKV rippt, Fortschritt streamen
6 MEDIAINFO_CHECK Warten HandBrake-Scan, Encode-Plan bauen
7 WAITING_FOR_USER_DECISION (optional) Playlist manuell wählen Auf Bestätigung warten
8 READY_TO_ENCODE Tracks prüfen + "Encoding starten" Auswahl übernehmen, Start auslösen
9 ENCODING Warten HandBrake encodiert, inkl. Post-Skripte
10 FINISHED Datei fertig, Benachrichtigung senden
\ No newline at end of file +

Öffne http://localhost:5173.


2) Disc einlegen

Pipeline wechselt auf DISC_DETECTED.

Falls nötig manuell neu scannen:

curl -X POST http://localhost:3001/api/pipeline/rescan-disc
+

3) Analyse starten

Klicke im Dashboard auf Analyse starten.

Intern:

  • Job wird angelegt
  • MakeMKV-Analyse läuft (ANALYZING)
  • UI wechselt in Metadatenauswahl (METADATA_SELECTION)

4) Metadaten bestätigen

Im Dialog:

  • OMDb-Ergebnis wählen oder manuell eintragen
  • bei Playlist-Abfrage ggf. selectedPlaylist wählen

Nach Bestätigung startet Ripster automatisch weiter.


5) Pipeline-Pfade

Abhängig von Job/RAW-Situation:

  • kein RAW vorhanden -> RIPPING
  • RAW vorhanden -> MEDIAINFO_CHECK
  • mehrdeutige Playlist -> WAITING_FOR_USER_DECISION

Wenn Parallel-Limit erreicht ist, wird der Job in die Queue eingereiht.


6) Review (READY_TO_ENCODE)

Im Review-Panel:

  • Titel auswählen (falls mehrere)
  • Audio-/Subtitle-Tracks auswählen
  • optional User-Preset anwenden
  • optional Pre-/Post-Skripte und Ketten hinzufügen

Mit Encoding starten wird confirm-encode + Start ausgelöst.


7) Encoding (ENCODING)

Während Encoding:

  • Live-Fortschritt/ETA über WebSocket
  • Pre-Encode-Ausführungen laufen vor HandBrake
  • Post-Encode-Ausführungen laufen nach HandBrake

Wichtig:

  • Pre-Encode-Fehler -> Job endet in ERROR
  • Post-Encode-Fehler -> Job kann FINISHED bleiben, aber mit Fehlerhinweis im Status/Log

8) Abschluss (FINISHED)

Ergebnis:

  • Ausgabe in movie_dir (ggf. profilspezifisch)
  • Job in Historie sichtbar
  • Logs im konfigurierten log_dir

Nützliche API-Shortcuts

# Pipeline-Snapshot
+curl http://localhost:3001/api/pipeline/state
+
+# Queue-Snapshot
+curl http://localhost:3001/api/pipeline/queue
+
+# Jobs
+curl http://localhost:3001/api/history
+
\ No newline at end of file diff --git a/site/index.html b/site/index.html index d28f871..e439e9c 100644 --- a/site/index.html +++ b/site/index.html @@ -1,4 +1,4 @@ - Ripster

Ripster

Halbautomatische Disc-Ripping-Plattform für DVDs und Blu-rays


  • Automatisiertes Ripping


    Disc einlegen – Ripster erkennt sie automatisch und startet den Analyse-Workflow mit MakeMKV.

    Workflow verstehen

  • Metadata-Integration


    Automatische Suche in der OMDb-Datenbank für Filmtitel, Poster und IMDb-IDs.

    Konfiguration

  • Flexibles Encoding


    HandBrake-Encoding mit individueller Track-Auswahl für Audio- und Untertitelspuren.

    Encode-Planung

  • Job-Historie


    Vollständiges Audit-Trail aller Ripping-Jobs mit Logs und Re-Encode-Funktion.

    History API


Was ist Ripster?

Ripster ist eine webbasierte Anwendung zur halbautomatischen Digitalisierung von DVDs und Blu-rays. Die Anwendung kombiniert bewährte Open-Source-Tools zu einem durchgängigen, komfortablen Workflow:

Disc einlegen → Erkennung → Analyse → Metadaten wählen → Rippen → Encodieren → Fertig
+ Ripster      

Ripster

Halbautomatische Disc-Ripping-Plattform für DVDs und Blu-rays


  • Automatisiertes Ripping


    Disc einlegen – Ripster erkennt sie automatisch und startet den Analyse-Workflow mit MakeMKV.

    Workflow verstehen

  • Metadata-Integration


    Automatische Suche in der OMDb-Datenbank für Filmtitel, Poster und IMDb-IDs.

    Konfiguration

  • Flexibles Encoding


    HandBrake-Encoding mit individueller Track-Auswahl für Audio- und Untertitelspuren.

    Encode-Planung

  • Job-Historie


    Vollständiges Audit-Trail aller Ripping-Jobs mit Logs und Re-Encode-Funktion.

    History API


Was ist Ripster?

Ripster ist eine webbasierte Anwendung zur halbautomatischen Digitalisierung von DVDs und Blu-rays. Die Anwendung kombiniert bewährte Open-Source-Tools zu einem durchgängigen, komfortablen Workflow:

Disc einlegen → Erkennung → Analyse → Metadaten wählen → Rippen → Encodieren → Fertig
 

Kernfunktionen

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äne bei korrupten SQLite-Dateien
Re-Encoding Erneutes Encodieren ohne neu rippen

Technologie-Stack

  • Node.js >= 20.19.0 mit Express.js
  • SQLite3 mit automatischen Schema-Migrationen
  • WebSocket (ws) für Echtzeit-Kommunikation
  • Externe CLI-Tools: makemkvcon, HandBrakeCLI, mediainfo
  • React 18.3.1 mit React Router
  • Vite 5.4.12 als Build-Tool
  • PrimeReact 10.9.2 als UI-Bibliothek
  • WebSocket-Client für Live-Updates
Tool Zweck
makemkvcon Disc-Analyse & MKV/Backup-Ripping
HandBrakeCLI Video-Encoding
mediainfo Track-Informationen aus gerippten Dateien
OMDb API Filmmetadaten (Titel, Poster, IMDb-ID)

Schnellstart

# 1. Repository klonen
 git clone https://github.com/YOUR_GITHUB_USERNAME/ripster.git
 cd ripster
diff --git a/site/pipeline/encoding/index.html b/site/pipeline/encoding/index.html
index db51523..f5489d5 100644
--- a/site/pipeline/encoding/index.html
+++ b/site/pipeline/encoding/index.html
@@ -1,139 +1,29 @@
- Encode-Planung & Track-Auswahl - Ripster      

Encode-Planung & Track-Auswahl

encodePlan.js analysiert die HandBrake-Scan-Ausgabe, wählt Audio- und Untertitelspuren anhand von Regeln vor und erstellt einen vollständigen Encode-Plan für die Benutzer-Review.


Ablauf im Pipeline-Kontext

RIPPING abgeschlossen (oder Pre-Rip-Scan)
-          ↓
-HandBrake --scan (alle Titel & Tracks einlesen)
-          ↓
-buildTrackSelectors()     ← Regeln aus Einstellungen ableiten
-          ↓
-selectTrackIds()          ← Tracks anhand Regeln vorauswählen
-          ↓
-resolveAudioEncoderAction() ← Encoder-Aktion pro Track bestimmen
-          ↓
-buildDiscScanReview()     ← Vollständigen Encode-Plan erstellen
-          ↓
-READY_TO_ENCODE           ← Benutzer-Review im Frontend
-          ↓
-applyManualTrackSelectionToPlan() ← Benutzer-Auswahl anwenden
-          ↓
-ENCODING                  ← HandBrake-CLI mit finalem Plan starten
-

Phase 1: Pre-Rip Track-Scan

Ripster führt einen HandBrake-Scan bereits vor dem eigentlichen Ripping durch:

HandBrakeCLI --scan -i /dev/sr0 -t 0
-

Dieser Scan liest alle Titel und deren Tracks aus der Disc (ohne zu encodieren). So kann der Benutzer die Track-Auswahl bereits vor dem zeitintensiven Rip-Prozess bestätigen.

Pre-Rip vs. Post-Rip

Ob der Scan vor oder nach dem Ripping passiert, hängt vom konfigurierten Modus ab. Bei direktem Disc-Zugriff ist Pre-Rip möglich; nach einem MakeMKV-Backup wird die entstandene .mkv-Datei gescannt.


Phase 2: Track-Selektor-Regeln (buildTrackSelectors)

Die Regeln werden aus den HandBrake-Einstellungen abgeleitet. Es gibt fünf Selektionsmodi:

Modus Beschreibung
none Keine Tracks dieser Art übernehmen
first Nur den ersten Track übernehmen
all Alle Tracks übernehmen
language Nur Tracks in bestimmten Sprachen
explicit Bestimmte Track-IDs explizit angeben

Der aktive Modus wird aus den handbrake_*-Einstellungen und handbrake_extra_args abgeleitet. Explizite CLI-Argumente (--audio, --audio-lang-list) überschreiben die Basis-Konfiguration.


Phase 3: Automatische Vorauswahl (selectTrackIds)

Audio-Tracks

Modus 'none'      → Keine Audio-Tracks
-Modus 'all'       → Alle Tracks (oder nur erster, wenn firstOnly)
-Modus 'language'  → Alle Tracks in den konfigurierten Sprachen
-Modus 'explicit'  → Nur die angegebenen Track-IDs
-Modus 'first'     → Nur Track 1
-

Jeder Audio-Track erhält das Feld selectedByRule: true/false – dieses zeigt dem Benutzer, welche Tracks automatisch vorausgewählt wurden.

Sprach-Normalisierung (normalizeLanguage):

Alle Sprachcodes werden auf ISO 639-2 (3-Buchstaben) normalisiert:

Eingabe Normalisiert
de, ger deu
German deu
en, eng eng
English eng
fr, fre fra
ja, jpn jpn
Unbekannt und

Untertitel-Tracks

Gleiche Modus-Logik wie Audio, aber mit zusätzlichen Flags pro Track:

Flag Bedeutung
burnIn Untertitel in Video einbrennen (--subtitle-burned)
forced Nur erzwungene Untertitel übernehmen (--subtitle-forced)
defaultTrack Als Standard-Untertitelspur markieren (--subtitle-default)

Diese Flags werden im Encode-Review als Checkboxen angezeigt.


Phase 4: Encoder-Aktion bestimmen (resolveAudioEncoderAction)

Für jeden vorausgewählten Audio-Track bestimmt Ripster die Encoder-Aktion:

Encoder-Einstellung      Codec-Support in Copy-Mask?    Aktion
-─────────────────────────────────────────────────────────────────────
-Kein Encoder / 'preset-default'   →  preset-default     HandBrake-Preset entscheidet
-encoder.startsWith('copy')
-  UND Codec in audioCopyMask      →  copy               Direktkopie (verlustfrei)
-  UND Codec NICHT in audioCopyMask→  fallback            Transcode mit Fallback-Encoder
-sonstiger Encoder                 →  transcode           Transcode mit explizitem Encoder
-

Encoder-Aktionstypen:

Typ Label (UI) Qualität
preset-default Preset-Default (HandBrake) HandBrake entscheidet
copy Copy (ac3) Verlustfrei
fallback Fallback Transcode (av_aac) Mit Qualitätsverlust
transcode Transcode (av_aac) Mit Qualitätsverlust

Copy-kompatible Codecs (Standard Copy-Mask):

Codec Encoder-String
AC-3 copy:ac3
E-AC-3 copy:eac3
AAC copy:aac
MP3 copy:mp3
TrueHD copy:truehd
DTS copy:dts (nur mit spez. HandBrake-Build)
DTS-HD copy:dtshd (nur mit spez. HandBrake-Build)

DTS im Standard-HandBrake

Standard-HandBrake-Builds unterstützen kein DTS-Passthrough. DTS-Tracks werden dann automatisch auf den Fallback-Encoder umgestellt (Standard: av_aac).


Phase 5: Encode-Plan-Struktur

Der vollständige Plan wird im Job-Datensatz als encode_plan_json gespeichert:

{
-  "mode": "pre_rip",
-  "preRip": true,
-  "encodeInputTitleId": 1,
-  "encodeInputPath": "disc-track-scan://title-1",
-  "selectors": {
-    "audio": { "mode": "language", "languages": ["deu", "eng"], "copyMask": ["copy:ac3", "copy:eac3"] },
-    "subtitle": { "mode": "none" }
-  },
-  "titles": [
-    {
-      "id": 1,
-      "fileName": "Disc Title 1",
-      "durationSeconds": 8885,
-      "selectedByMinLength": true,
-      "isEncodeInput": true,
-      "audioTracks": [
-        {
-          "id": 1,
-          "sourceTrackId": 1,
-          "language": "eng",
-          "languageLabel": "English",
-          "title": "5.1 Surround",
-          "format": "AC3",
-          "codecToken": "ac3",
-          "channels": "6",
-          "selectedByRule": true,
-          "selectedForEncode": true,
-          "encodePreviewActions": [
-            { "type": "copy", "encoder": "copy:ac3", "label": "Copy (ac3)" }
-          ],
-          "encodePreviewSummary": "Copy (ac3)"
-        },
-        {
-          "id": 2,
-          "sourceTrackId": 2,
-          "language": "deu",
-          "languageLabel": "Deutsch",
-          "format": "DTS",
-          "codecToken": "dts",
-          "channels": "6",
-          "selectedByRule": true,
-          "selectedForEncode": true,
-          "encodePreviewActions": [
-            { "type": "fallback", "encoder": "av_aac", "label": "Fallback Transcode (av_aac)" }
-          ],
-          "encodePreviewSummary": "Fallback Transcode (av_aac)"
-        },
-        {
-          "id": 3,
-          "language": "fra",
-          "languageLabel": "Français",
-          "selectedByRule": false,
-          "selectedForEncode": false,
-          "encodePreviewSummary": "Nicht übernommen"
-        }
-      ],
-      "subtitleTracks": [
-        {
-          "id": 1,
-          "language": "deu",
-          "selectedByRule": true,
-          "selectedForEncode": true,
-          "burnIn": false,
-          "forced": false,
-          "defaultTrack": true,
-          "subtitlePreviewSummary": "Übernehmen",
-          "subtitlePreviewFlags": ["default"]
-        }
-      ]
-    }
-  ]
-}
-

Phase 6: Benutzer-Review im Frontend (MediaInfoReviewPanel)

Das Review-Panel zeigt:

┌─────────────────────────────────────────────────────────────────┐
-│ Encode-Review                            Titel: Disc Title 1    │
-│                                          Laufzeit: 2:28:05      │
-├─────────────────────────────────────────────────────────────────┤
-│ Audio-Spuren                                                    │
-├──────┬──────────────────────────┬──────────────────────────────┤
-│ [✓]  │ Track 1: English (AC3)   │ Copy (ac3)                   │
-│ [✓]  │ Track 2: Deutsch (DTS)   │ Fallback Transcode (av_aac)  │
-│ [ ]  │ Track 3: Français (DTS)  │ Nicht übernommen             │
-├──────┴──────────────────────────┴──────────────────────────────┤
-│ Untertitel-Spuren                                               │
-├──────┬──────────────────────────┬────────┬────────┬────────────┤
-│ [✓]  │ Track 1: Deutsch         │Einbr.[ ]│Forced[ ]│Default[✓]│
-│ [ ]  │ Track 2: English         │Einbr.[ ]│Forced[ ]│Default[ ]│
-├──────┴──────────────────────────┴────────┴────────┴────────────┤
-│                               [Encoding starten]               │
-└─────────────────────────────────────────────────────────────────┘
-

Der Benutzer kann: - Audio-Tracks per Checkbox aktivieren/deaktivieren - Untertitel-Flags (Einbrennen, Forced, Default) setzen - Mehrere Titel bei der Titleauswahl wechseln (für Discs mit mehreren Haupttiteln)


Phase 7: Benutzer-Auswahl anwenden (applyManualTrackSelectionToPlan)

Im Frontend wird die Benutzer-Auswahl beim Klick auf "Encoding starten" (ggf. automatisch) bestätigt und dann auf den Plan angewendet:

Payload: {
-  "selectedEncodeTitleId": 1,
-  "selectedTrackSelection": {
-    "1": {
-      "audioTrackIds": [1, 2],
-      "subtitleTrackIds": [1]
-    }
-  }
-}
-

Jeder Track erhält selectedForEncode: true/false entsprechend der Auswahl. Die Encoder-Aktionen (encodeActions) der nicht gewählten Tracks werden geleert.


Phase 8: HandBrake-CLI-Befehl

Aus dem finalisierten Plan baut Ripster den HandBrake-Aufruf:

HandBrakeCLI \
-  -i /dev/sr0 \
-  -o "/mnt/movies/Inception (2010).mkv" \
-  -t 1 \
-  --preset "H.265 MKV 1080p30" \
-  -a 1,2 \
-  -E copy:ac3,av_aac \
-  -s 1 \
-  --subtitle-default 1
-
Argument Quelle
-i encode_input_path aus Job
-o Ausgabepfad aus filename_template + movie_dir
-t Gewählter Titel-Index
-a Kommagetrennte Audio-Track-IDs der ausgewählten Tracks
-E Kommagetrennte Encoder-Aktionen (eine pro Track, gleiche Reihenfolge wie -a)
-s Kommagetrennte Untertitel-Track-IDs
--subtitle-default Track-ID der als Default markierten Untertitelspur
--preset handbrake_preset-Einstellung
Extras handbrake_extra_args-Einstellung

Dateiname-Template

Platzhalter Wert Beispiel
{title} Filmtitel von OMDb Inception
{year} Erscheinungsjahr 2010
{imdb_id} IMDb-ID tt1375666
{type} movie oder series movie

Sonderzeichen (:, /, ?, * etc.) werden automatisch aus dem Dateinamen entfernt.


Re-Encoding

Ein abgeschlossener Job kann ohne erneutes Ripping neu encodiert werden:

  1. Job in der History öffnen
  2. "Re-Encode" klicken
  3. Track-Auswahl anpassen (oder bestehende übernehmen)
  4. Encoding startet mit den aktuellen handbrake_*-Einstellungen

Nützlich bei geänderten Presets, anderen Sprach-Präferenzen oder nach einem Einstellungs-Update.

\ No newline at end of file + Encode-Planung & Track-Auswahl - Ripster

Encode-Planung & Track-Auswahl

Ripster erzeugt vor dem Encode einen encodePlan und lässt ihn im Review-Panel bestätigen.


Ablauf

Quelle bestimmen (Disc/RAW)
+  -> HandBrake-Scan (--scan --json)
+  -> Plan erstellen (Titel, Audio, Untertitel)
+  -> READY_TO_ENCODE
+  -> Benutzer bestätigt Auswahl
+  -> finaler HandBrake-Aufruf
+

Review-Inhalt (READY_TO_ENCODE)

  • auswählbarer Encode-Titel
  • Audio-Track-Selektion
  • Untertitel-Track-Selektion inkl. Flags
  • burnIn
  • forced
  • defaultTrack
  • optionale User-Presets (HandBrake-Preset + Extra-Args)
  • optionale Pre-/Post-Skripte und Ketten

Bestätigung (confirm-encode)

Typischer Payload:

{
+  "selectedEncodeTitleId": 1,
+  "selectedTrackSelection": {
+    "1": {
+      "audioTrackIds": [1, 2],
+      "subtitleTrackIds": [3]
+    }
+  },
+  "selectedPreEncodeScriptIds": [1],
+  "selectedPostEncodeScriptIds": [2],
+  "selectedPreEncodeChainIds": [3],
+  "selectedPostEncodeChainIds": [4],
+  "selectedUserPresetId": 5
+}
+

Ripster speichert die bestätigte Auswahl in jobs.encode_plan_json und markiert encode_review_confirmed = 1.


HandBrake-Aufruf

Grundstruktur:

HandBrakeCLI \
+  -i <input> \
+  -o <output> \
+  -t <titleId> \
+  -Z "<preset>" \
+  <extra-args> \
+  -a <audioTrackIds|none> \
+  -s <subtitleTrackIds|none>
+

Untertitel-Flags werden bei Bedarf ergänzt:

  • --subtitle-burned=<id>
  • --subtitle-default=<id>
  • --subtitle-forced=<id> oder --subtitle-forced

Pre-/Post-Encode-Ausführungen

  • Pre-Encode läuft vor HandBrake
  • Post-Encode läuft nach HandBrake

Verhalten bei Fehlern:

  • Pre-Encode-Fehler: Job wird als ERROR beendet (Encode startet nicht)
  • Post-Encode-Fehler: Job kann FINISHED bleiben, enthält aber Fehlerhinweis/Script-Summary

Dateinamen/Ordner

Der finale Outputpfad wird aus Settings-Templates aufgebaut.

Platzhalter:

  • ${title}
  • ${year}
  • ${imdbId}

Ungültige Dateizeichen werden sanitisiert.

\ No newline at end of file diff --git a/site/pipeline/index.html b/site/pipeline/index.html index fc9a185..304e95f 100644 --- a/site/pipeline/index.html +++ b/site/pipeline/index.html @@ -1 +1 @@ - Pipeline - Ripster

Pipeline

Der Pipeline-Abschnitt beschreibt den Kern-Workflow von Ripster.

  • Workflow & Zustände


    Der vollständige Ripping-Workflow mit allen Zustandsübergängen.

    Workflow

  • Encode-Planung


    Wie Ripster Audio- und Untertitel-Tracks analysiert und Encode-Pläne erstellt.

    Encoding

  • Playlist-Analyse


    Erkennung von Blu-ray Playlist-Obfuskierung und Auswahl der korrekten Playlist.

    Playlist-Analyse

  • Post-Encode-Skripte


    Automatische Ausführung von Shell-Skripten nach erfolgreichem Encoding – z. B. zum Verschieben oder Benachrichtigen.

    Post-Encode-Skripte

\ No newline at end of file + Pipeline - Ripster

Pipeline

Der Pipeline-Bereich beschreibt den Kern-Workflow von Ripster.

  • Workflow & Zustände


    Zustände, Übergänge und Queue-Verhalten.

    Workflow

  • Encode-Planung


    Wie Titel/Tracks für HandBrake vorbereitet und bestätigt werden.

    Encoding

  • Playlist-Analyse


    Bewertung mehrdeutiger Blu-ray-Playlists und manuelle Entscheidung.

    Playlist-Analyse

  • Encode-Skripte (Pre & Post)


    Skripte/Ketten vor und nach dem Encode ausführen.

    Encode-Skripte

\ No newline at end of file diff --git a/site/pipeline/playlist-analysis/index.html b/site/pipeline/playlist-analysis/index.html index 005e850..447218a 100644 --- a/site/pipeline/playlist-analysis/index.html +++ b/site/pipeline/playlist-analysis/index.html @@ -1,74 +1 @@ - Playlist-Analyse - Ripster

Playlist-Analyse

Einige Blu-rays verwenden Playlist-Obfuskierung als Kopierschutz. Ripster analysiert automatisch alle MakeMKV-Titel und empfiehlt die korrekte Playlist – auf Basis eines Segment-Scoring-Algorithmus aus playlistAnalysis.js.


Das Problem: Playlist-Obfuskierung

Moderne Blu-rays können Dutzende bis Hunderte von Titeln/Playlists enthalten. Der eigentliche Film steckt in genau einer davon – alle anderen sind:

  • Kurze Dummy-Titel (wenige Sekunden bis Minuten)
  • Titel mit verschachtelten Segmenten (absichtlich versetzte Reihenfolge, sodass der Film falsch gerippt wird)
  • Titel gleicher Länge (mehrere Playlists mit identischer Laufzeit, aber unterschiedlicher Segment-Reihenfolge)

Das Ziel der Obfuskierung: Ein einfacher Ripper wählt den erstbesten langen Titel – und bekommt ein zerstückeltes, unbrauchbares Video.


Wann wird die Analyse ausgelöst?

Die Playlist-Analyse wird automatisch gestartet sobald der Benutzer Metadaten bestätigt (nach dem Metadaten-Dialog). Ripster ruft makemkvcon im Info-Modus auf und parst die TINFO-Ausgabe.

TINFO:<titleId>,26,"<segment-list>"
-

Feld 26 enthält die kommagetrennte Liste der Segment-Nummern in der Abspielreihenfolge des Titels.


Algorithmus im Detail (playlistAnalysis.js)

Schritt 1 – Segment-Nummern parsen

TINFO:1,26,"00000,00001,00002,00003"  → [0, 1, 2, 3]       linearer Film
-TINFO:2,26,"00100,00050,00100,00051"  → [100, 50, 100, 51]  Fake-Playlist
-

Schritt 2 – Metriken berechnen (computeSegmentMetrics)

Für jedes aufeinanderfolgende Segment-Paar [a, b] wird diff = b − a berechnet:

Metrik Bedingung Bedeutung
directSequenceSteps diff == 1 Aufeinanderfolgende Segmente → linearer Film
backwardJumps b < a Rückwärtssprünge → verdächtig
largeJumps \|diff\| > 20 Große Sprünge → verdächtig
alternatingPairs Große Sprünge mit wechselndem Vorzeichen Hin-und-her-Muster → starker Fake-Indikator

Score-Formel:

score = (directSequenceSteps × 2) − (backwardJumps × 3) − (largeJumps × 2)
-

Konkrete Beispiele:

Segmentfolge directSeq backward large score Ergebnis
0,1,2,3,4,5 5 0 0 +10 Echter Film
0,1,100,2,101,3 2 0 4 -4 Verdächtig
50,10,60,11,70,12 0 3 3 -15 Fake

Schritt 3 – Bewertungslabel vergeben (buildEvaluationLabel)

alternatingRatio = alternatingPairs / largeJumps
-
-if alternatingRatio >= 0.55 AND alternatingPairs >= 3:
-  → "Fake-Struktur (alternierendes Sprungmuster)"
-
-else if backwardJumps > 0 OR largeJumps > 0:
-  → "Auffällige Segmentreihenfolge"
-
-else:
-  → "wahrscheinlich korrekt (lineare Segmentfolge)"
-

Schritt 4 – Duplikat-Gruppen bilden (buildSimilarityGroups)

Alle Titel werden nach ähnlicher Laufzeit gruppiert (±90 Sekunden Toleranz). Gibt es mehrere Kandidaten mit ähnlicher Laufzeit, ist das ein klares Zeichen für Obfuskierung:

8 Titel mit ~148 Minuten Laufzeit → Duplikat-Gruppe
-→ obfuscationDetected = true
-

Schritt 5 – Besten Kandidaten empfehlen (scoreCandidates)

Innerhalb der größten Duplikat-Gruppe werden alle Kandidaten sortiert nach:

  1. score (höher = besser)
  2. sequenceCoherence (Anteil linearer Segmentschritte)
  3. Laufzeit (länger = besser)
  4. Dateigröße (größer = besser als Tiebreaker)

Der erste Kandidat der sortierten Liste ist die Empfehlung.

Schritt 6 – Entscheidung erzwingen bei mehreren Kandidaten

Sobald nach MIN_LENGTH_MINUTES mehr als eine Playlist übrig bleibt, wird immer eine manuelle Auswahl verlangt:

candidateCount > 1  → manualDecisionRequired = true
-candidateCount <= 1 → manualDecisionRequired = false
-

Wann greift der Benutzer ein?

obfuscationDetected    = duplicateDurationGroups.length > 0
-manualDecisionRequired = candidates.length > 1
-
Ergebnis Nächster Pipeline-Zustand Aktion
Nur ein Kandidat nach Mindestlänge READY_TO_START Automatische Übernahme möglich
Mehrere Kandidaten nach Mindestlänge WAITING_FOR_USER_DECISION Benutzer muss Playlist auswählen

Benutzeroberfläche: Playlist-Auswahl-Dialog

Wenn manualDecisionRequired = true, öffnet sich der Playlist-Dialog nach dem Metadaten-Dialog:

┌───────────────────────────────────────────────────────────────────┐
-│ Playlist-Auswahl                                                  │
-├──────────┬──────────┬──────────┬────────────────────────────────┤
-│ Playlist │ Laufzeit │  Score   │ Bewertung                       │
-├──────────┼──────────┼──────────┼────────────────────────────────┤
-│ ★ 00800  │ 2:28:05  │   +18    │ wahrscheinlich korrekt          │
-│          │          │          │ (lineare Segmentfolge)          │
-├──────────┼──────────┼──────────┼────────────────────────────────┤
-│   00801  │ 2:28:12  │    −4    │ Auffällige Segmentreihenfolge   │
-├──────────┼──────────┼──────────┼────────────────────────────────┤
-│   00900  │ 2:28:05  │   −32    │ Fake-Struktur                   │
-│          │          │          │ (alternierendes Sprungmuster)   │
-└──────────┴──────────┴──────────┴────────────────────────────────┘
-  Hinweis: 847 Playlists insgesamt. 3 relevante Kandidaten (≥ 15 min).
-  Empfehlung: 00800 (★)
-
  • markiert die empfohlene Playlist (vorausgewählt)
  • Nur Titel ≥ makemkv_min_length_minutes erscheinen in der Liste
  • Der Benutzer wählt per Radio-Button und klickt "Bestätigen"
  • Erst nach dieser Bestätigung wechselt die Pipeline zu READY_TO_START

Vollständige Datenstruktur (analyzeContext.playlistAnalysis)

{
-  "titles": [
-    { "titleId": 1, "playlistId": "00800", "durationSeconds": 8885, "durationLabel": "2:28:05", "chapters": 28 }
-  ],
-  "candidates": [
-    { "titleId": 1, "playlistId": "00800", "durationSeconds": 8885 },
-    { "titleId": 2, "playlistId": "00801", "durationSeconds": 8892 }
-  ],
-  "evaluatedCandidates": [
-    {
-      "titleId": 1,
-      "playlistId": "00800",
-      "score": 18,
-      "sequenceCoherence": 0.95,
-      "evaluationLabel": "wahrscheinlich korrekt (lineare Segmentfolge)",
-      "metrics": {
-        "directSequenceSteps": 12,
-        "backwardJumps": 0,
-        "largeJumps": 1,
-        "alternatingPairs": 0
-      }
-    }
-  ],
-  "duplicateDurationGroups": [
-    [
-      { "titleId": 1, "playlistId": "00800" },
-      { "titleId": 2, "playlistId": "00801" }
-    ]
-  ],
-  "recommendation": {
-    "titleId": 1,
-    "playlistId": "00800",
-    "score": 18,
-    "reason": "Höchster Segment-Score in der größten Laufzeit-Gruppe"
-  },
-  "obfuscationDetected": true,
-  "manualDecisionRequired": true
-}
-

Konfiguration

Einstellung Standard Wirkung
makemkv_min_length_minutes 15 Titel kürzer als dieser Wert werden als Kandidaten ignoriert

Tipps bei Fehlempfehlung

Falsche Playlist gewählt?

Wenn das resultierende Video zerstückelt ist:

  1. Job in der History öffnen
  2. Re-Encode starten – diesmal eine andere Playlist wählen
  3. Alternativ: Korrekte Playlist im MakeMKV-Forum recherchieren

Keine Segment-Daten verfügbar

Bei DVDs oder älteren Blu-rays liefert MakeMKV manchmal keine Segmentinfos (TINFO-Feld 26 fehlt). In diesem Fall entfällt die Analyse und der erste Titel über der Mindestlänge wird automatisch verwendet.

\ No newline at end of file + Playlist-Analyse - Ripster

Playlist-Analyse

Ripster analysiert bei Blu-ray-ähnlichen Quellen Playlists und fordert bei Mehrdeutigkeit eine manuelle Auswahl an.


Ziel

Erkennen, welche Playlist wahrscheinlich der Hauptfilm ist, statt versehentlich eine Fake-/Dummy-Playlist zu verwenden.


Eingabedaten

Die Analyse basiert auf MakeMKV-Infos (u. a. Playlist-/Segment-Struktur, Laufzeiten, Titelzuordnung).


Auswertung (vereinfacht)

Für Kandidaten werden u. a. berücksichtigt:

  • Laufzeit
  • Segment-Reihenfolge
  • Rückwärtssprünge/große Sprünge
  • Kohärenz linearer Segmentfolgen
  • Duplikatgruppen mit ähnlicher Laufzeit

Daraus entstehen:

  • candidates
  • evaluatedCandidates (inkl. Score/Label)
  • recommendation
  • manualDecisionRequired

Wann muss der Benutzer entscheiden?

Wenn nach Filterung mehr als ein relevanter Kandidat übrig bleibt, setzt Ripster manualDecisionRequired = true und wechselt auf:

  • WAITING_FOR_USER_DECISION

Dann muss eine Playlist bestätigt werden, bevor der Workflow weiterläuft.


Konfigurationseinfluss

Key Wirkung
makemkv_min_length_minutes Mindestlaufzeit für Kandidaten

Default ist aktuell 60 Minuten.


UI-Verhalten

Bei manueller Entscheidung zeigt das Dashboard Kandidaten inkl. Score/Bewertung und markiert eine Empfehlung.

Nach Bestätigung:

  • mit vorhandenem RAW -> zurück zu MEDIAINFO_CHECK
  • ohne RAW -> Startpfad über READY_TO_START/RIPPING
\ No newline at end of file diff --git a/site/pipeline/post-encode-scripts/index.html b/site/pipeline/post-encode-scripts/index.html index 4864cc2..6c227d5 100644 --- a/site/pipeline/post-encode-scripts/index.html +++ b/site/pipeline/post-encode-scripts/index.html @@ -1,55 +1,6 @@ - Post-Encode-Skripte - Ripster

Post-Encode-Skripte

Post-Encode-Skripte ermöglichen es, nach erfolgreichem Encoding automatisch beliebige Shell-Befehle oder Programme auszuführen – z. B. zum Verschieben von Dateien, Benachrichtigen externer Dienste oder Auslösen weiterer Verarbeitungsschritte.


Funktionsweise

Nach einem erfolgreich abgeschlossenen Encoding-Schritt führt Ripster die konfigurierten Skripte sequenziell in der festgelegten Reihenfolge aus:

ENCODING abgeschlossen
-        ↓
-Skript 1 ausführen  ← Fehler? → Abbruch
-        ↓
-Skript 2 ausführen  ← Fehler? → Abbruch
-        ↓
-        ...
-        ↓
-FINISHED
-

Abbruch bei Fehler

Schlägt ein Skript fehl (Exit-Code ≠ 0), werden alle nachfolgenden Skripte nicht mehr ausgeführt. Der Job bleibt im Abschlusszustand FINISHED; der Fehler wird in Log/Status-Text und im postEncodeScripts-Summary festgehalten.


Skript-Verwaltung

Skripte werden über die Einstellungen-Seite angelegt und verwaltet. Sie stehen danach in jedem Encode-Review zur Auswahl.

Skript anlegen

Navigiere zu Einstellungen → Skripte und klicke "Neues Skript":

Feld Beschreibung
Name Anzeigename des Skripts (z. B. Zu Plex verschieben)
Befehl Shell-Befehl oder Skriptpfad (z. B. /home/michael/scripts/move-to-plex.sh)
Beschreibung Optionale Erklärung

Verfügbare Umgebungsvariablen

Jedes Skript wird mit folgenden Umgebungsvariablen aufgerufen:

Variable Inhalt Beispiel
RIPSTER_OUTPUT_PATH Absoluter Pfad der encodierten Datei /mnt/movies/Inception (2010).mkv
RIPSTER_JOB_ID Job-ID in der Datenbank 42
RIPSTER_TITLE Filmtitel Inception
RIPSTER_YEAR Erscheinungsjahr 2010
RIPSTER_IMDB_ID IMDb-ID tt1375666
RIPSTER_RAW_PATH Pfad zur Raw-MKV-Datei /mnt/raw/Inception-2010/t00.mkv

Beispiel-Skript: Datei nach Jellyfin verschieben

#!/bin/bash
-# /home/michael/scripts/move-to-jellyfin.sh
-
-TARGET_DIR="/mnt/media/movies"
-mkdir -p "$TARGET_DIR"
-mv "$RIPSTER_OUTPUT_PATH" "$TARGET_DIR/"
-echo "Verschoben: $RIPSTER_TITLE nach $TARGET_DIR"
-

Beispiel-Skript: Webhook auslösen

#!/bin/bash
-# /home/michael/scripts/notify-webhook.sh
-
-curl -s -X POST https://mein-webhook.example.com/ripster \
-  -H "Content-Type: application/json" \
-  -d "{\"title\": \"$RIPSTER_TITLE\", \"year\": \"$RIPSTER_YEAR\", \"path\": \"$RIPSTER_OUTPUT_PATH\"}"
-

Skript im Encode-Review auswählen

Im READY_TO_ENCODE-Zustand zeigt das MediaInfoReviewPanel einen Skript-Abschnitt:

┌──────────────────────────────────────────────────────────┐
-│ Post-Encode-Skripte                                      │
-├──────────────────────────────────────────────────────────┤
-│ Ausgewählte Skripte (Reihenfolge per Drag & Drop):       │
-│  ≡  1. Zu Plex verschieben                    [Entfernen]│
-│  ≡  2. Webhook auslösen                       [Entfernen]│
-├──────────────────────────────────────────────────────────┤
-│ Skript hinzufügen: [Zu Jellyfin verschieben ▾] [+ Hinzuf.]│
-└──────────────────────────────────────────────────────────┘
-
  • Reihenfolge per Drag & Drop ändern
  • Hinzufügen aus der Dropdown-Liste aller konfigurierten Skripte
  • Entfernen einzelner Skripte aus der aktuellen Auswahl
  • Skripte können pro Job unterschiedlich gewählt werden

Skript testen

Über die Einstellungen kann jedes Skript mit einem Test-Job ausgeführt werden:

POST /api/settings/scripts/:scriptId/test
-

Der Test-Aufruf befüllt die Umgebungsvariablen mit Platzhalter-Werten.


Ausführungs-Ergebnis

Das Ergebnis der Skript-Ausführung wird im Job-Datensatz gespeichert und in der History angezeigt:

{
-  "postEncodeScripts": {
-    "configured": 2,
-    "attempted": 2,
-    "succeeded": 2,
-    "failed": 0,
-    "skipped": 0,
-    "aborted": false,
-    "results": [
-      {
-        "scriptId": 1,
-        "scriptName": "Zu Plex verschieben",
-        "status": "SUCCESS"
-      },
-      {
-        "scriptId": 2,
-        "scriptName": "Webhook auslösen",
-        "status": "SUCCESS"
-      }
-    ]
-  }
-}
-
Feld Beschreibung
configured Anzahl ausgewählter Skripte
attempted Anzahl tatsächlich gestarteter Skripte
succeeded Erfolgreich ausgeführt (Exit-Code 0)
failed Fehlgeschlagen
skipped Nicht ausgeführt (wegen vorherigem Fehler)
aborted true, wenn die Kette abgebrochen wurde

API-Referenz

Eine vollständige API-Dokumentation der Skript-Endpunkte findest du unter:

Settings API – Skripte

\ No newline at end of file + Encode-Skripte (Pre & Post) - Ripster

Encode-Skripte (Pre & Post)

Ripster kann Skripte und Skript-Ketten vor und nach dem Encode ausführen.


Ablauf

READY_TO_ENCODE
+  -> Pre-Encode Skripte/Ketten
+  -> HandBrake Encoding
+  -> Post-Encode Skripte/Ketten
+  -> FINISHED oder ERROR
+

Auswahl im Review

Im Review-Panel kannst du getrennt wählen:

  • selectedPreEncodeScriptIds
  • selectedPostEncodeScriptIds
  • selectedPreEncodeChainIds
  • selectedPostEncodeChainIds

Fehlerverhalten

  • Pre-Encode-Fehler stoppen die Kette und führen zu ERROR.
  • Post-Encode-Fehler stoppen die restlichen Post-Schritte; Job kann dennoch FINISHED sein (mit Fehlerzusatz im Status/Log).

Verfügbare Umgebungsvariablen

Beim Script-Run werden gesetzt:

  • RIPSTER_SCRIPT_RUN_AT
  • RIPSTER_JOB_ID
  • RIPSTER_JOB_TITLE
  • RIPSTER_MODE
  • RIPSTER_INPUT_PATH
  • RIPSTER_OUTPUT_PATH
  • RIPSTER_RAW_PATH
  • RIPSTER_SCRIPT_ID
  • RIPSTER_SCRIPT_NAME
  • RIPSTER_SCRIPT_SOURCE

Skript-Ketten

Ketten unterstützen zwei Step-Typen:

  • script (führt ein hinterlegtes Skript aus)
  • wait (wartet waitSeconds)

Bei Fehler in einem Script-Step wird die Kette abgebrochen.


Testläufe

  • Skript testen: POST /api/settings/scripts/:id/test
  • Kette testen: POST /api/settings/script-chains/:id/test

Ergebnisse enthalten Erfolg/Exit-Code, Laufzeit und stdout/stderr.

\ No newline at end of file diff --git a/site/pipeline/workflow/index.html b/site/pipeline/workflow/index.html index 21899bd..83b0443 100644 --- a/site/pipeline/workflow/index.html +++ b/site/pipeline/workflow/index.html @@ -1,61 +1,15 @@ - Workflow & Zustände - Ripster

Workflow & Zustände

Der Ripping-Workflow von Ripster ist als State Machine implementiert. Jeder Zustand hat klar definierte Übergangsbedingungen und Aktionen.


Zustandsdiagramm

flowchart LR
-    START(( )) --> IDLE
-
-    IDLE -->|Disc erkannt| DD[DISC_DETECTED]
-    DD -->|Analyse starten| META[METADATA\nSELECTION]
-
-    META -->|Metadaten übernommen| RTS[READY_TO\nSTART]
-    META -->|vorhandenes RAW +\nPlaylist offen| WUD[WAITING_FOR\nUSER_DECISION]
-
-    RTS -->|Auto-Start| RIP[RIPPING]
-    RTS -->|Auto-Start mit RAW| MIC[MEDIAINFO\nCHECK]
-    RIP -->|MKV fertig| MIC
-    RIP -->|Fehler| ERR
-    RIP -->|Abbruch| CAN([CANCELLED])
-
-    MIC -->|Playlist offen (Backup)| WUD
-    WUD -->|Playlist bestätigt| MIC
-    WUD -->|Playlist bestätigt,\nnoch kein RAW| RTS
-    MIC --> RTE[READY_TO\nENCODE]
-    RTE -->|Encoding starten\n(bestätigt bei Bedarf automatisch)| ENC[ENCODING]
-
-    ENC -->|inkl. Post-Skripte| FIN([FINISHED])
-    ENC -->|Fehler| ERR
-    ENC -->|Abbruch| CAN
-
-    ERR([ERROR]) -->|Retry / Cancel| IDLE
-    CAN -->|Retry / Neu-Analyse| IDLE
-    FIN -->|Neue Disc| IDLE
-
-    style FIN fill:#e8f5e9,stroke:#66bb6a,color:#2e7d32
-    style ERR fill:#ffebee,stroke:#ef5350,color:#c62828
-    style CAN fill:#fff3e0,stroke:#fb8c00,color:#e65100
-    style WUD fill:#fff8e1,stroke:#ffa726,color:#e65100
-    style ENC fill:#f3e5f5,stroke:#ab47bc,color:#6a1b9a
-    style RIP fill:#e3f2fd,stroke:#42a5f5,color:#1565c0
-    style MIC fill:#e3f2fd,stroke:#42a5f5,color:#1565c0

UI-Badge-Bezeichnungen

Die Status-Badges im Dashboard verwenden diese Labels:

State Badge-Label
IDLE Bereit
DISC_DETECTED Medium erkannt
METADATA_SELECTION Metadatenauswahl
WAITING_FOR_USER_DECISION Warte auf Auswahl
READY_TO_START Startbereit
RIPPING Rippen
MEDIAINFO_CHECK Mediainfo-Pruefung
READY_TO_ENCODE Bereit zum Encodieren
ENCODING Encodieren
FINISHED Fertig
CANCELLED Abgebrochen
ERROR Fehler
Queue (kein eigener State) In der Queue

Zustandsbeschreibungen

IDLE

Ausgangszustand. Ripster wartet auf eine Disc.

  • diskDetectionService pollt das Laufwerk im konfigurierten Intervall
  • Bei Disc-Erkennung: automatischer Übergang zu DISC_DETECTED
  • WebSocket-Event: DISC_DETECTED

DISC_DETECTED

Disc erkannt, wartet auf Benutzeraktion.

  • Dashboard-Badge: "Medium erkannt"
  • Status-Text: "Neue Disk erkannt"
  • "Analyse starten"-Button wird aktiv
  • Kein Prozess läuft noch

Übergang: Benutzer klickt "Analyse starten" → METADATA_SELECTION


METADATA_SELECTION

Metadaten-Auswahl läuft.

  1. Job wird erstellt (status = METADATA_SELECTION)
  2. OMDb-Vorsuche mit erkanntem Disc-Label
  3. MetadataSelectionDialog öffnet sich mit vorgeladenen Ergebnissen
  4. Benutzer wählt Filmtitel (oder gibt manuell ein)
  5. Nach Bestätigung wird der Job automatisch für Start/Queue vorbereitet (selectMetadata + startPreparedJob)

Übergang (automatisch nach Metadaten-Bestätigung):

Ergebnis Nächster Zustand
Kein verwertbares RAW vorhanden READY_TO_START → automatisch RIPPING (oder Queue)
Verwertbares RAW vorhanden READY_TO_START → automatisch MEDIAINFO_CHECK (oder Queue)
Vorhandenes RAW + offene Playlist-Entscheidung WAITING_FOR_USER_DECISION

WAITING_FOR_USER_DECISION

Playlist-Obfuskierung erkannt – manuelle Auswahl erforderlich.

Neu seit „Skript Integration + UI Anpassungen"

Dieser Zustand wurde eingeführt, um Blu-rays mit mehreren Playlists ähnlicher Länge korrekt zu behandeln.

  • Playlist-Auswahl-Dialog wird im Dashboard angezeigt
  • Alle Kandidaten mit Score, Laufzeit und Bewertungslabel
  • Empfohlene Playlist ist vorausgewählt
  • Benutzer bestätigt mit "Playlist übernehmen"
  • Tritt häufig nach MEDIAINFO_CHECK auf (Backup-Analyse), seltener direkt nach METADATA_SELECTION bei vorhandenem RAW

Darstellung im Dashboard:

┌──────────────────────────────────────────────────────────┐
-│ Playlist-Auswahl erforderlich                            │
-│ Es wurden mehrere Titel mit ähnlicher Laufzeit gefunden. │
-├──────────┬──────────┬────────┬──────────────────────────┤
-│ Playlist │ Laufzeit │ Score  │ Bewertung                 │
-├──────────┼──────────┼────────┼──────────────────────────┤
-│ ● 00800  │ 2:28:05  │  +18   │ wahrscheinlich korrekt    │
-│ ○ 00801  │ 2:28:12  │   −4   │ Auffällige Segmentfolge   │
-│ ○ 00900  │ 2:28:05  │  −32   │ Fake-Struktur             │
-└──────────┴──────────┴────────┴──────────────────────────┘
-                              [Playlist übernehmen]
-

Übergang: selectMetadata(jobId, { selectedPlaylist }) setzt die Pipeline automatisch fort:

  • mit vorhandenem RAW nach MEDIAINFO_CHECK
  • ohne RAW über READY_TO_START weiter Richtung RIPPING

Mehr Details: Playlist-Analyse


READY_TO_START

Übergangs-/Fallback-Zustand vor dem eigentlichen Start.

  • Wird nach Metadaten-Bestätigung kurz gesetzt
  • startPreparedJob() wird danach automatisch ausgeführt
  • Wenn Parallel-Limit erreicht ist, wird der Start stattdessen in die Queue eingereiht
  • "Job starten" ist primär für Sonderfälle/Fallback sichtbar

Sonderfall – RAW-Datei bereits vorhanden: Wenn für diesen Job bereits ein verwertbares RAW unter raw_dir existiert, wird Ripping übersprungen und direkt MEDIAINFO_CHECK gestartet.

Übergang: startPreparedJob(jobId)RIPPING oder direkt MEDIAINFO_CHECK


RIPPING

MakeMKV rippt die Disc.

makemkvcon mkv disc:0 all /path/to/raw/ --minlength=900 -r
-

Erstellt MKV-Datei(en) direkt aus den gewählten Titeln.

makemkvcon backup disc:0 /path/to/raw/backup/ --decrypt -r
-

Erstellt vollständiges Disc-Backup inkl. Menüs.

Live-Updates aus MakeMKV-Ausgabe:

PRGV:2048,0,65536  → Fortschritt-Berechnung
-PRGT:5011,0,"..."  → Aktueller Task-Name
-

Typische Dauer: DVD 20–45 min · Blu-ray 45–120 min


MEDIAINFO_CHECK

HandBrake-Scan und Encode-Plan-Erstellung.

Dieser Zustand umfasst je nach Quelle mehrere Phasen:

  1. Optional: Playlist-Auflösung bei Blu-ray-Backup (inkl. MakeMKV/HandBrake-Zuordnung)
  2. HandBrake-Scan (HandBrakeCLI --scan) auf RAW-Input
  3. Encode-Plan-Erstellung mit automatischer Track-Vorauswahl

Kein Benutzereingriff – läuft automatisch durch.

Übergänge:

  • Eindeutige Quelle/Titelwahl möglich → READY_TO_ENCODE
  • Mehrdeutige Playlist erkannt → WAITING_FOR_USER_DECISION

READY_TO_ENCODE

Encode-Plan bereit.

Das MediaInfoReviewPanel zeigt:

  • Titel-Auswahl (bei Discs mit mehreren langen Titeln)
  • Audio-Tracks mit Encoder-Vorschau (Copy/Transcode/Fallback)
  • Untertitel-Tracks mit Flags (Einbrennen, Forced, Default)
  • Post-Encode-Skripte – Auswahl und Reihenfolge der auszuführenden Skripte

Im Frontend startet "Encoding starten" (bzw. "Backup + Encoding starten" im Pre-Rip-Modus) den nächsten Schritt. Falls die Review noch nicht bestätigt wurde, wird confirmEncodeReview(...) automatisch vor dem Start aufgerufen.

Übergang: startPreparedJob(jobId)ENCODING (oder im Pre-Rip-Fall zuerst RIPPING)


ENCODING

HandBrake encodiert die Datei.

HandBrakeCLI \
-  -i <quelle> -o <ziel> \
-  -t <titelId> \
-  --preset "H.265 MKV 1080p30" \
-  -a 1,2 -E copy:ac3,av_aac \
-  -s 1 --subtitle-default 1
-

Live-Updates aus HandBrake-stderr:

Encoding: task 1 of 1, 73.50 % (45.23 fps, avg 44.12 fps, ETA 00h12m34s)
-

Post-Encode-Skripte werden innerhalb dieses Zustands sequenziell ausgeführt (kein separater Pipeline-State).

Skriptfehler

Skriptfehler führen zum Abbruch der Skriptkette, der Job bleibt jedoch im Abschlusszustand FINISHED mit entsprechendem Hinweis im Status-Text/Log.


FINISHED

Job erfolgreich abgeschlossen.

  • Ausgabedatei liegt im konfigurierten movie_dir
  • Job-Status in Datenbank: FINISHED
  • PushOver-Benachrichtigung (falls konfiguriert)
  • WebSocket-Event: PIPELINE_STATE_CHANGED (State FINISHED)

CANCELLED

Job wurde vom Benutzer abgebrochen.

  • Entsteht bei aktivem Abbruch (/api/pipeline/cancel) während laufender Phase
  • Job-Status in Datenbank: CANCELLED
  • Im Dashboard stehen danach u. a. Retry Rippen, Review neu starten oder Encode neu starten (kontextabhängig) zur Verfügung

ERROR

Fehler aufgetreten.

  • Fehlerdetails im Job-Datensatz gespeichert
  • Fehler-Logs in History abrufbar
  • Retry: Neustart vom Fehlerzustand
  • Neu analysieren: Disc erneut als neuer Job starten

Abbrechen & Retry

Pipeline abbrechen

POST /api/pipeline/cancel
-
  • SIGINT → graceful exit (Timeout: 10 s) → SIGKILL
  • Laufender Job landet in CANCELLED (oder Queue-Eintrag wird entfernt, falls noch nicht gestartet)

Job wiederholen

POST /api/pipeline/retry/:jobId
-
  • Startet den Job neu in RIPPING (oder reiht den Retry in die Queue ein)
  • Metadaten bleiben erhalten; Encode-/Scan-Daten werden neu erzeugt

Re-Encode

POST /api/pipeline/reencode/:jobId
-
  • Encodiert bestehende Raw-MKV neu
  • Ermöglicht neue Track-Auswahl und andere Skripte
  • Kein Ripping erforderlich
\ No newline at end of file + Workflow & Zustände - Ripster

Workflow & Zustände

Ripster steuert den Ablauf als State-Machine im pipelineService.


Zustandsdiagramm (vereinfacht)

flowchart LR
+    IDLE --> DISC_DETECTED
+    DISC_DETECTED --> ANALYZING
+    ANALYZING --> METADATA_SELECTION
+    METADATA_SELECTION --> READY_TO_START
+    READY_TO_START --> RIPPING
+    READY_TO_START --> MEDIAINFO_CHECK
+    MEDIAINFO_CHECK --> WAITING_FOR_USER_DECISION
+    WAITING_FOR_USER_DECISION --> MEDIAINFO_CHECK
+    MEDIAINFO_CHECK --> READY_TO_ENCODE
+    READY_TO_ENCODE --> ENCODING
+    ENCODING --> FINISHED
+    ENCODING --> ERROR
+    RIPPING --> ERROR
+    RIPPING --> CANCELLED

State-Liste

State Bedeutung
IDLE Wartet auf Disc
DISC_DETECTED Disc erkannt
ANALYZING MakeMKV-Analyse läuft
METADATA_SELECTION Benutzer wählt Metadaten
WAITING_FOR_USER_DECISION Playlist-Auswahl nötig
READY_TO_START Übergangszustand vor Start
RIPPING MakeMKV-Rip läuft
MEDIAINFO_CHECK Quelle/Tracks werden ausgewertet
READY_TO_ENCODE Review ist bereit
ENCODING HandBrake läuft
FINISHED erfolgreich abgeschlossen
CANCELLED abgebrochen
ERROR fehlgeschlagen

Typische Pfade

Standardfall (kein vorhandenes RAW)

  1. Disc erkannt
  2. Analyse + Metadaten
  3. RIPPING
  4. MEDIAINFO_CHECK
  5. READY_TO_ENCODE
  6. ENCODING
  7. FINISHED

Vorhandenes RAW

READY_TO_START springt direkt zu MEDIAINFO_CHECK (kein neuer Rip).

Mehrdeutige Blu-ray-Playlist

MEDIAINFO_CHECK -> WAITING_FOR_USER_DECISION bis Benutzer Playlist bestätigt.


Queue-Verhalten

Wenn pipeline_max_parallel_jobs erreicht ist:

  • Job-Aktionen werden als Queue-Einträge abgelegt
  • Queue kann zusätzlich Nicht-Job-Einträge enthalten (script, chain, wait)
  • Reihenfolge ist per API/UI änderbar

Abbruch, Retry, Restart

  • cancel: laufenden Job abbrechen oder Queue-Eintrag entfernen
  • retry: Fehler-/Abbruch-Job neu starten
  • reencode: aus vorhandenem RAW neu encodieren
  • restart-review: Review aus RAW neu aufbauen
  • restart-encode: Encoding mit letzter bestätigter Auswahl neu starten
\ No newline at end of file diff --git a/site/search/search_index.json b/site/search/search_index.json index 59e1914..44dcf88 100644 --- a/site/search/search_index.json +++ b/site/search/search_index.json @@ -1 +1 @@ -{"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":"

Halbautomatische Disc-Ripping-Plattform f\u00fcr DVDs und Blu-rays

  • Automatisiertes Ripping

    Disc einlegen \u2013 Ripster erkennt sie automatisch und startet den Analyse-Workflow mit MakeMKV.

    Workflow verstehen

  • Metadata-Integration

    Automatische Suche in der OMDb-Datenbank f\u00fcr Filmtitel, Poster und IMDb-IDs.

    Konfiguration

  • Flexibles Encoding

    HandBrake-Encoding mit individueller Track-Auswahl f\u00fcr Audio- und Untertitelspuren.

    Encode-Planung

  • Job-Historie

    Vollst\u00e4ndiges Audit-Trail aller Ripping-Jobs mit Logs und Re-Encode-Funktion.

    History API

"},{"location":"#was-ist-ripster","title":"Was ist Ripster?","text":"

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:

Disc einlegen \u2192 Erkennung \u2192 Analyse \u2192 Metadaten w\u00e4hlen \u2192 Rippen \u2192 Encodieren \u2192 Fertig\n
"},{"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
  • Node.js >= 20.19.0 mit Express.js
  • SQLite3 mit automatischen Schema-Migrationen
  • WebSocket (ws) f\u00fcr Echtzeit-Kommunikation
  • Externe CLI-Tools: makemkvcon, HandBrakeCLI, mediainfo
  • React 18.3.1 mit React Router
  • Vite 5.4.12 als Build-Tool
  • PrimeReact 10.9.2 als UI-Bibliothek
  • WebSocket-Client f\u00fcr Live-Updates
Tool Zweck makemkvcon Disc-Analyse & MKV/Backup-Ripping HandBrakeCLI Video-Encoding mediainfo Track-Informationen aus gerippten Dateien OMDb API Filmmetadaten (Titel, Poster, IMDb-ID)"},{"location":"#schnellstart","title":"Schnellstart","text":"
# 1. Repository klonen\ngit clone https://github.com/YOUR_GITHUB_USERNAME/ripster.git\ncd ripster\n\n# 2. Starten (Node.js >= 20 erforderlich)\n./start.sh\n\n# 3. Browser \u00f6ffnen\nopen http://localhost:5173\n

Erste Schritte

Die vollst\u00e4ndige Installationsanleitung mit allen Voraussetzungen findest du unter Erste Schritte.

"},{"location":"#pipeline-uberblick","title":"Pipeline-\u00dcberblick","text":"
flowchart LR\n    IDLE --> DD[DISC_DETECTED]\n    DD --> META[METADATA\\nSELECTION]\n    META --> RTS[READY_TO\\nSTART]\n    RTS -->|Auto-Start| RIP[RIPPING]\n    RTS -->|Auto-Start mit RAW| MIC\n    RIP --> MIC[MEDIAINFO\\nCHECK]\n    MIC -->|Playlist offen (Backup)| WUD[WAITING_FOR\\nUSER_DECISION]\n    WUD --> MIC\n    MIC --> RTE[READY_TO\\nENCODE]\n    RTE --> ENC[ENCODING]\n    ENC -->|inkl. Post-Skripte| FIN([FINISHED])\n    ENC --> ERR([ERROR])\n    RIP --> 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

READY_TO_START ist in der Praxis meist ein kurzer \u00dcbergangszustand: der Job wird nach Metadaten-Auswahl automatisch gestartet oder in die Queue eingeplant.

"},{"location":"api/","title":"API-Referenz","text":"

Ripster bietet eine REST-API f\u00fcr alle Operationen sowie einen WebSocket-Endpunkt f\u00fcr Echtzeit-Updates.

"},{"location":"api/#basis-url","title":"Basis-URL","text":"
http://localhost:3001\n

Konfigurierbar \u00fcber die Umgebungsvariable PORT.

"},{"location":"api/#api-gruppen","title":"API-Gruppen","text":"
  • Pipeline API

    Pipeline-Steuerung: Analyse starten, Metadaten setzen, Ripping und Encoding steuern.

    Pipeline API

  • Settings API

    Einstellungen lesen und schreiben.

    Settings API

  • History API

    Job-Geschichte abfragen, Jobs l\u00f6schen, Orphan-Ordner importieren.

    History API

  • WebSocket Events

    Echtzeit-Events f\u00fcr Pipeline-Status, Fortschritt und Disc-Erkennung.

    WebSocket

"},{"location":"api/#authentifizierung","title":"Authentifizierung","text":"

Die API hat keine Authentifizierung. Sie ist f\u00fcr den Einsatz im lokalen Netzwerk konzipiert.

Produktionsbetrieb

Falls Ripster \u00f6ffentlich erreichbar sein soll, sch\u00fctze die API mit einem Reverse-Proxy (z. B. nginx mit Basic Auth oder OAuth).

"},{"location":"api/#fehlerformat","title":"Fehlerformat","text":"

Alle API-Fehler werden im folgenden Format zur\u00fcckgegeben:

{\n  \"error\": \"Job nicht gefunden\",\n  \"details\": \"Kein Job mit ID 999 vorhanden\"\n}\n

HTTP-Statuscodes:

Code Bedeutung 200 Erfolg 400 Ung\u00fcltige Anfrage 404 Ressource nicht gefunden 409 Konflikt (z.B. Pipeline bereits aktiv) 500 Interner Serverfehler"},{"location":"api/history/","title":"History API","text":"

Endpunkte f\u00fcr die Job-Histoire, Dateimanagement und Orphan-Import.

"},{"location":"api/history/#get-apihistory","title":"GET /api/history","text":"

Gibt eine Liste aller Jobs zur\u00fcck, optional gefiltert.

Query-Parameter:

Parameter Typ Beschreibung status string Filtert nach Status (z.B. FINISHED, ERROR) search string Sucht in Filmtiteln

Beispiel:

GET /api/history?status=FINISHED&search=Inception\n

Response:

{\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
"},{"location":"api/history/#get-apihistoryid","title":"GET /api/history/:id","text":"

Gibt Detail-Informationen f\u00fcr einen einzelnen Job zur\u00fcck.

URL-Parameter: id \u2013 Job-ID

Query-Parameter:

Parameter Typ Standard Beschreibung includeLogs boolean false Log-Inhalte einschlie\u00dfen includeLiveLog boolean false Aktuellen Live-Log einschlie\u00dfen

Response:

{\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
"},{"location":"api/history/#get-apihistorydatabase","title":"GET /api/history/database","text":"

Gibt alle rohen Datenbankzeilen zur\u00fcck (Debug-Ansicht).

Response:

{\n  \"jobs\": [ { \"id\": 1, \"status\": \"FINISHED\", ... } ],\n  \"total\": 15\n}\n
"},{"location":"api/history/#get-apihistoryorphan-raw","title":"GET /api/history/orphan-raw","text":"

Findet Raw-Ordner, die nicht als Jobs in der Datenbank registriert sind.

Response:

{\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
"},{"location":"api/history/#post-apihistoryorphan-rawimport","title":"POST /api/history/orphan-raw/import","text":"

Importiert einen Orphan-Raw-Ordner als Job in die Datenbank.

Request:

{\n  \"path\": \"/mnt/nas/raw/UnknownMovie_2023-12-01\"\n}\n

Response:

{\n  \"ok\": true,\n  \"jobId\": 99,\n  \"message\": \"Orphan-Ordner als Job importiert\"\n}\n

Nach dem Import kann dem Job \u00fcber /api/history/:id/omdb/assign Metadaten zugewiesen werden.

"},{"location":"api/history/#post-apihistoryidomdbassign","title":"POST /api/history/:id/omdb/assign","text":"

Weist einem bestehenden Job OMDb-Metadaten nachtr\u00e4glich zu.

URL-Parameter: id \u2013 Job-ID

Request:

{\n  \"imdbId\": \"tt1375666\",\n  \"title\": \"Inception\",\n  \"year\": \"2010\",\n  \"type\": \"movie\",\n  \"poster\": \"https://...\"\n}\n

Response:

{ \"ok\": true }\n
"},{"location":"api/history/#post-apihistoryiddelete-files","title":"POST /api/history/:id/delete-files","text":"

L\u00f6scht die Dateien eines Jobs (Raw und/oder Output), beh\u00e4lt den Job-Eintrag.

URL-Parameter: id \u2013 Job-ID

Request:

{\n  \"deleteRaw\": true,\n  \"deleteOutput\": false\n}\n

Response:

{\n  \"ok\": true,\n  \"deleted\": {\n    \"raw\": \"/mnt/nas/raw/Inception_t00.mkv\",\n    \"output\": null\n  }\n}\n
"},{"location":"api/history/#post-apihistoryiddelete","title":"POST /api/history/:id/delete","text":"

L\u00f6scht den Job-Eintrag aus der Datenbank, optional auch die Dateien.

URL-Parameter: id \u2013 Job-ID

Request:

{\n  \"deleteFiles\": true\n}\n

Response:

{ \"ok\": true, \"message\": \"Job gel\u00f6scht\" }\n

Unwiderruflich

Das L\u00f6schen von Jobs und Dateien ist nicht r\u00fcckg\u00e4ngig zu machen.

"},{"location":"api/pipeline/","title":"Pipeline API","text":"

Alle Endpunkte zur Steuerung des Ripster-Workflows.

"},{"location":"api/pipeline/#get-apipipelinestate","title":"GET /api/pipeline/state","text":"

Liefert den aktuellen Pipeline-Snapshot.

Response:

{\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

Pipeline-Zust\u00e4nde:

Wert Beschreibung IDLE Wartet auf Medium DISC_DETECTED Medium erkannt, wartet auf Analyse-Start METADATA_SELECTION Metadaten-Dialog aktiv WAITING_FOR_USER_DECISION Manuelle Playlist-Auswahl erforderlich READY_TO_START \u00dcbergang/Fallback vor Start RIPPING MakeMKV l\u00e4uft MEDIAINFO_CHECK HandBrake-Scan + Plan-Erstellung READY_TO_ENCODE Review bereit ENCODING HandBrake-Encoding l\u00e4uft (inkl. Post-Skripte) FINISHED Abgeschlossen CANCELLED Vom Benutzer abgebrochen ERROR Fehler"},{"location":"api/pipeline/#post-apipipelineanalyze","title":"POST /api/pipeline/analyze","text":"

Startet die Analyse f\u00fcr die aktuell erkannte Disc.

Request: kein Body

Response:

{\n  \"result\": {\n    \"jobId\": 42,\n    \"detectedTitle\": \"INCEPTION\",\n    \"omdbCandidates\": []\n  }\n}\n
"},{"location":"api/pipeline/#post-apipipelinerescan-disc","title":"POST /api/pipeline/rescan-disc","text":"

Erzwingt eine erneute Laufwerkspr\u00fcfung.

Response (Beispiel):

{\n  \"result\": {\n    \"emitted\": \"discInserted\"\n  }\n}\n
"},{"location":"api/pipeline/#get-apipipelineomdbsearchq","title":"GET /api/pipeline/omdb/search?q=

Sucht OMDb-Titel.

Response:

{\n  \"results\": [\n    {\n      \"imdbId\": \"tt1375666\",\n      \"title\": \"Inception\",\n      \"year\": \"2010\",\n      \"type\": \"movie\",\n      \"poster\": \"https://...\"\n    }\n  ]\n}\n
","text":""},{"location":"api/pipeline/#post-apipipelineselect-metadata","title":"POST /api/pipeline/select-metadata

Setzt Metadaten (und optional Playlist-Entscheidung).

Request:

{\n  \"jobId\": 42,\n  \"title\": \"Inception\",\n  \"year\": 2010,\n  \"imdbId\": \"tt1375666\",\n  \"poster\": \"https://...\",\n  \"fromOmdb\": true,\n  \"selectedPlaylist\": \"00800\"\n}\n

Response: { \"job\": { ... } }

Startlogik

Nach Metadaten-Best\u00e4tigung wird der n\u00e4chste Schritt automatisch ausgel\u00f6st (startPreparedJob). Der Job startet direkt oder wird in die Queue eingereiht.

","text":""},{"location":"api/pipeline/#post-apipipelinestartjobid","title":"POST /api/pipeline/start/:jobId

Startet einen vorbereiteten Job manuell (z. B. Fallback/Queue-Szenario).

Response (Beispiel):

{\n  \"result\": {\n    \"started\": true,\n    \"stage\": \"RIPPING\"\n  }\n}\n

M\u00f6gliche stage-Werte sind u. a. RIPPING, MEDIAINFO_CHECK, ENCODING.

","text":""},{"location":"api/pipeline/#post-apipipelineconfirm-encodejobid","title":"POST /api/pipeline/confirm-encode/:jobId

Best\u00e4tigt Review-Auswahl (Titel/Tracks/Post-Skripte).

Request:

{\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

Response: { \"job\": { ... } }

","text":""},{"location":"api/pipeline/#post-apipipelinecancel","title":"POST /api/pipeline/cancel

Bricht laufenden Job ab oder entfernt einen Queue-Eintrag.

Request (optional):

{\n  \"jobId\": 42\n}\n

Response (Beispiel):

{\n  \"result\": {\n    \"cancelled\": true,\n    \"queuedOnly\": false,\n    \"jobId\": 42\n  }\n}\n
","text":""},{"location":"api/pipeline/#post-apipipelineretryjobid","title":"POST /api/pipeline/retry/:jobId

Startet einen Job aus ERROR/CANCELLED erneut (oder reiht ihn in die Queue ein).

Response: { \"result\": { ... } }

","text":""},{"location":"api/pipeline/#post-apipipelineresume-readyjobid","title":"POST /api/pipeline/resume-ready/:jobId

L\u00e4dt einen READY_TO_ENCODE-Job nach Neustart wieder in die aktive Session.

Response: { \"job\": { ... } }

","text":""},{"location":"api/pipeline/#post-apipipelinereencodejobid","title":"POST /api/pipeline/reencode/:jobId

Startet Re-Encode aus bestehendem RAW.

Response: { \"result\": { ... } }

","text":""},{"location":"api/pipeline/#post-apipipelinerestart-reviewjobid","title":"POST /api/pipeline/restart-review/:jobId

Berechnet die Review aus vorhandenem RAW neu.

Response: { \"result\": { ... } }

","text":""},{"location":"api/pipeline/#post-apipipelinerestart-encodejobid","title":"POST /api/pipeline/restart-encode/:jobId

Startet Encoding mit der zuletzt best\u00e4tigten Auswahl neu.

Response: { \"result\": { ... } }

","text":""},{"location":"api/pipeline/#queue-endpunkte","title":"Queue-Endpunkte","text":""},{"location":"api/pipeline/#get-apipipelinequeue","title":"GET /api/pipeline/queue","text":"

Liefert den aktuellen Queue-Status.

Response: { \"queue\": { ... } }

"},{"location":"api/pipeline/#post-apipipelinequeuereorder","title":"POST /api/pipeline/queue/reorder","text":"

Sortiert Queue-Eintr\u00e4ge neu.

Request:

{\n  \"orderedJobIds\": [42, 43, 41]\n}\n

Response: { \"queue\": { ... } }

"},{"location":"api/settings/","title":"Settings API","text":"

Endpunkte zum Lesen und Schreiben der Anwendungseinstellungen.

"},{"location":"api/settings/#get-apisettings","title":"GET /api/settings","text":"

Gibt alle Einstellungen kategorisiert zur\u00fcck.

Response:

{\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
"},{"location":"api/settings/#put-apisettingskey","title":"PUT /api/settings/:key","text":"

Aktualisiert eine einzelne Einstellung.

URL-Parameter: key \u2013 Einstellungs-Schl\u00fcssel

Request:

{\n  \"value\": \"/mnt/storage/raw\"\n}\n

Response:

{ \"ok\": true, \"key\": \"raw_dir\", \"value\": \"/mnt/storage/raw\" }\n

Fehlerf\u00e4lle: - 400 \u2013 Ung\u00fcltiger Wert (Validierungsfehler) - 404 \u2013 Einstellung nicht gefunden

Encode-Review-Refresh

Wenn eine encoding-relevante Einstellung ge\u00e4ndert wird (z.B. handbrake_preset), wird der Encode-Plan f\u00fcr den aktuell wartenden Job automatisch neu berechnet.

"},{"location":"api/settings/#put-apisettings","title":"PUT /api/settings","text":"

Aktualisiert mehrere Einstellungen auf einmal.

Request:

{\n  \"raw_dir\": \"/mnt/storage/raw\",\n  \"movie_dir\": \"/mnt/storage/movies\",\n  \"handbrake_preset\": \"H.265 MKV 720p30\"\n}\n

Response:

{\n  \"ok\": true,\n  \"updated\": [\"raw_dir\", \"movie_dir\", \"handbrake_preset\"],\n  \"errors\": []\n}\n
"},{"location":"api/settings/#post-apisettingspushovertest","title":"POST /api/settings/pushover/test","text":"

Sendet eine Test-Benachrichtigung \u00fcber PushOver.

Request: Kein Body erforderlich (verwendet gespeicherte Zugangsdaten)

Response (Erfolg):

{ \"ok\": true, \"message\": \"Test-Benachrichtigung gesendet\" }\n

Response (Fehler):

{ \"ok\": false, \"error\": \"Ung\u00fcltiger API-Token\" }\n
"},{"location":"api/settings/#skript-verwaltung","title":"Skript-Verwaltung","text":"

Post-Encode-Skripte werden \u00fcber eigene Endpunkte unter /api/settings/scripts verwaltet.

"},{"location":"api/settings/#get-apisettingsscripts","title":"GET /api/settings/scripts","text":"

Gibt alle konfigurierten Skripte zur\u00fcck.

Response:

{\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
"},{"location":"api/settings/#post-apisettingsscripts","title":"POST /api/settings/scripts","text":"

Legt ein neues Post-Encode-Skript an.

Request:

{\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
Feld Typ Pflicht Beschreibung name string \u2705 Anzeigename command string \u2705 Shell-Befehl oder absoluter Skriptpfad description string \u2014 Optionale Beschreibung

Response:

{\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
"},{"location":"api/settings/#put-apisettingsscriptsscriptid","title":"PUT /api/settings/scripts/:scriptId","text":"

Aktualisiert ein vorhandenes Skript.

URL-Parameter: scriptId

Request: Gleiche Felder wie beim Anlegen (alle optional).

{ \"name\": \"Zu Jellyfin verschieben\", \"command\": \"/home/michael/scripts/move-to-jellyfin.sh\" }\n

Response: { \"ok\": true }

"},{"location":"api/settings/#delete-apisettingsscriptsscriptid","title":"DELETE /api/settings/scripts/:scriptId","text":"

L\u00f6scht ein Skript.

URL-Parameter: scriptId

Response: { \"ok\": true }

Referenzen in Jobs

Wenn das Skript in laufenden oder abgeschlossenen Jobs referenziert wird, wird es trotzdem gel\u00f6scht. In zuk\u00fcnftigen Encode-Reviews erscheint es nicht mehr.

"},{"location":"api/settings/#post-apisettingsscriptsscriptidtest","title":"POST /api/settings/scripts/:scriptId/test","text":"

F\u00fchrt ein Skript mit Platzhalter-Umgebungsvariablen aus (Testlauf).

URL-Parameter: scriptId

Response (Erfolg):

{\n  \"ok\": true,\n  \"exitCode\": 0,\n  \"stdout\": \"Testausgabe des Skripts\",\n  \"stderr\": \"\",\n  \"durationMs\": 245\n}\n

Response (Fehler):

{\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

Platzhalter-Werte beim Testlauf:

Variable Testwert RIPSTER_OUTPUT_PATH /tmp/ripster-test-output.mkv RIPSTER_JOB_ID 0 RIPSTER_TITLE Test Film RIPSTER_YEAR 2024 RIPSTER_IMDB_ID tt0000000 RIPSTER_RAW_PATH /tmp/ripster-test-raw.mkv"},{"location":"api/settings/#einstellungs-schlussel-referenz","title":"Einstellungs-Schl\u00fcssel Referenz","text":"

Eine vollst\u00e4ndige Liste aller Einstellungs-Schl\u00fcssel:

Schl\u00fcssel Kategorie Typ Beschreibung raw_dir paths string Raw-MKV Verzeichnis movie_dir paths string Ausgabe-Verzeichnis log_dir paths string Log-Verzeichnis makemkv_command tools string MakeMKV-Befehl handbrake_command tools string HandBrake-Befehl mediainfo_command tools string MediaInfo-Befehl handbrake_preset encoding string HandBrake-Preset-Name handbrake_extra_args encoding string Zusatz-Argumente output_extension encoding string Dateiendung (z.B. mkv) filename_template encoding string Dateiname-Template drive_mode drive select auto oder explicit drive_device drive string Ger\u00e4te-Pfad disc_poll_interval_ms drive number Polling-Intervall (ms) makemkv_min_length_minutes makemkv number Min. Titell\u00e4nge (Minuten) makemkv_backup_mode makemkv boolean Backup-Modus aktivieren omdb_api_key omdb string OMDb API-Key omdb_default_type omdb select Standard-Suchtyp pushover_user_key notifications string PushOver User-Key pushover_api_token notifications string PushOver API-Token"},{"location":"api/websocket/","title":"WebSocket Events","text":"

Ripster sendet Echtzeit-Updates \u00fcber WebSocket unter /ws.

"},{"location":"api/websocket/#verbindung","title":"Verbindung","text":"
const ws = new WebSocket('ws://localhost:3001/ws');\n\nws.onmessage = (event) => {\n  const message = JSON.parse(event.data);\n  console.log(message.type, message.payload);\n};\n
"},{"location":"api/websocket/#nachrichtenformat","title":"Nachrichtenformat","text":"

Alle Broadcasts haben dieses Schema:

{\n  \"type\": \"EVENT_TYPE\",\n  \"payload\": { },\n  \"timestamp\": \"2026-03-05T10:00:00.000Z\"\n}\n
"},{"location":"api/websocket/#event-typen","title":"Event-Typen","text":""},{"location":"api/websocket/#ws_connected","title":"WS_CONNECTED","text":"

Wird direkt nach Verbindungsaufbau gesendet.

{\n  \"type\": \"WS_CONNECTED\",\n  \"payload\": {\n    \"connectedAt\": \"2026-03-05T10:00:00.000Z\"\n  }\n}\n
"},{"location":"api/websocket/#pipeline_state_changed","title":"PIPELINE_STATE_CHANGED","text":"

Snapshot bei Zustandswechsel.

{\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
"},{"location":"api/websocket/#pipeline_progress","title":"PIPELINE_PROGRESS","text":"

Laufende Fortschrittsupdates w\u00e4hrend aktiver Phasen.

{\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
"},{"location":"api/websocket/#pipeline_queue_changed","title":"PIPELINE_QUEUE_CHANGED","text":"

Aktualisierung der Job-Queue.

{\n  \"type\": \"PIPELINE_QUEUE_CHANGED\",\n  \"payload\": {\n    \"maxParallelJobs\": 1,\n    \"runningCount\": 1,\n    \"queuedCount\": 2,\n    \"runningJobs\": [],\n    \"queuedJobs\": []\n  }\n}\n
"},{"location":"api/websocket/#disc_detected","title":"DISC_DETECTED","text":"

Disc erkannt.

{\n  \"type\": \"DISC_DETECTED\",\n  \"payload\": {\n    \"device\": {\n      \"path\": \"/dev/sr0\",\n      \"discLabel\": \"INCEPTION\"\n    }\n  }\n}\n
"},{"location":"api/websocket/#disc_removed","title":"DISC_REMOVED","text":"

Disc entfernt.

{\n  \"type\": \"DISC_REMOVED\",\n  \"payload\": {\n    \"device\": {\n      \"path\": \"/dev/sr0\"\n    }\n  }\n}\n
"},{"location":"api/websocket/#pipeline_error","title":"PIPELINE_ERROR","text":"

Fehler bei Pipeline-Disc-Events im Backend.

{\n  \"type\": \"PIPELINE_ERROR\",\n  \"payload\": {\n    \"message\": \"...\"\n  }\n}\n
"},{"location":"api/websocket/#disk_detection_error","title":"DISK_DETECTION_ERROR","text":"

Fehler im Laufwerkserkennungsdienst.

{\n  \"type\": \"DISK_DETECTION_ERROR\",\n  \"payload\": {\n    \"message\": \"...\"\n  }\n}\n
"},{"location":"api/websocket/#reconnect-verhalten","title":"Reconnect-Verhalten","text":"

useWebSocket.js versucht bei Verbindungsabbruch automatisch erneut zu verbinden.

  • fester Retry-Intervall: 1500ms
  • erneuter Versuch bis zum Unmount der Komponente
"},{"location":"api/websocket/#react-beispiel","title":"React-Beispiel","text":"
import { useWebSocket } from './hooks/useWebSocket';\n\nuseWebSocket({\n  onMessage: (msg) => {\n    if (msg.type === 'PIPELINE_STATE_CHANGED') {\n      setPipeline(msg.payload);\n    }\n  }\n});\n
"},{"location":"architecture/","title":"Architektur","text":"

Ripster ist als klassische Client-Server-Anwendung mit Echtzeit-Kommunikation \u00fcber WebSockets aufgebaut.

"},{"location":"architecture/#systemuberblick","title":"System\u00fcberblick","text":"
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 <-->|HTTP REST| API\n    Browser <-->|WebSocket| WS\n    Pipeline --> MakeMKV\n    Pipeline --> HandBrake\n    Pipeline --> MediaInfo\n    Pipeline <-->|Metadaten| OMDb\n    Pipeline -->|Benachrichtigungen| PushOver\n    API --> DB\n    Pipeline --> DB
"},{"location":"architecture/#schichten-architektur","title":"Schichten-Architektur","text":""},{"location":"architecture/#backend","title":"Backend","text":"
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
"},{"location":"architecture/#frontend","title":"Frontend","text":"
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
"},{"location":"architecture/#weiterfuhrende-dokumentation","title":"Weiterf\u00fchrende Dokumentation","text":"
  • \u00dcbersicht

  • Backend-Services

  • Frontend-Komponenten

  • Datenbank

"},{"location":"architecture/backend/","title":"Backend-Services","text":"

Das Backend ist in Node.js/Express geschrieben und in Services aufgeteilt, die jeweils eine klar abgegrenzte Verantwortlichkeit haben.

"},{"location":"architecture/backend/#pipelineservicejs","title":"pipelineService.js","text":"

Der Kern von Ripster \u2013 orchestriert den gesamten Ripping-Workflow.

"},{"location":"architecture/backend/#zustandigkeiten","title":"Zust\u00e4ndigkeiten","text":"
  • Verwaltung des Pipeline-Zustands als State Machine
  • Koordination zwischen allen externen Tools
  • Generierung von Encode-Pl\u00e4nen
  • Fehlerbehandlung und Recovery
"},{"location":"architecture/backend/#haupt-methoden","title":"Haupt-Methoden","text":"Methode Beschreibung analyzeDisc() Legt Job an und \u00f6ffnet Metadaten-Auswahl selectMetadata({...}) Setzt Metadaten/Playlist und triggert Auto-Start startPreparedJob(jobId) Startet vorbereiteten Job (oder Queue) confirmEncodeReview(jobId, options) Best\u00e4tigt Review inkl. Track/Skript-Auswahl cancel(jobId) Bricht laufenden Job ab oder entfernt Queue-Eintrag retry(jobId) Startet fehlgeschlagenen/abgebrochenen Job neu reencodeFromRaw(jobId) Encodiert aus vorhandenem RAW neu restartReviewFromRaw(jobId) Berechnet Review aus RAW neu restartEncodeWithLastSettings(jobId) Neustart mit letzter best\u00e4tigter Auswahl resumeReadyToEncodeJob(jobId) L\u00e4dt READY_TO_ENCODE nach Neustart in die Session"},{"location":"architecture/backend/#zustandsubergange","title":"Zustands\u00fcberg\u00e4nge","text":"
flowchart LR\n    START(( )) --> IDLE\n    IDLE -->|analyzeDisc()| META[METADATA\\nSELECTION]\n    META -->|selectMetadata()| RTS[READY_TO\\nSTART]\n    RTS -->|Auto-Start/Queue| RIP[RIPPING]\n    RTS -->|Auto-Start mit RAW| MIC[MEDIAINFO\\nCHECK]\n    RIP -->|MKV erstellt| MIC[MEDIAINFO\\nCHECK]\n    MIC -->|Playlist offen| WUD[WAITING_FOR\\nUSER_DECISION]\n    WUD -->|selectMetadata(selectedPlaylist)| MIC\n    MIC -->|Tracks analysiert| RTE[READY_TO\\nENCODE]\n    RTE -->|confirmEncodeReview() + startPreparedJob()| ENC[ENCODING]\n    ENC -->|HandBrake + Post-Skripte fertig| FIN([FINISHED])\n    ENC -->|Abbruch| CAN([CANCELLED])\n    ENC -->|Fehler| ERR([ERROR])\n    RIP -->|Fehler| ERR\n    RIP -->|Abbruch| CAN\n    ERR -->|retry() / cancel()| IDLE\n    CAN -->|retry() / analyzeDisc()| IDLE\n    FIN -->|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
"},{"location":"architecture/backend/#diskdetectionservicejs","title":"diskDetectionService.js","text":"

\u00dcberwacht das Disc-Laufwerk auf Disc-Einleger- und Auswurf-Ereignisse.

"},{"location":"architecture/backend/#modi","title":"Modi","text":"Modus Beschreibung auto Erkennt verf\u00fcgbare Laufwerke automatisch explicit \u00dcberwacht ein bestimmtes Ger\u00e4t (z.B. /dev/sr0)"},{"location":"architecture/backend/#polling","title":"Polling","text":"

Der Service pollt das Laufwerk im konfigurierten Intervall (disc_poll_interval_ms, Standard: 4000ms) und emittiert Events:

// Ereignisse\nemit('discInserted', { path: '/dev/sr0' })\nemit('discRemoved', { path: '/dev/sr0' })\n
"},{"location":"architecture/backend/#processrunnerjs","title":"processRunner.js","text":"

Verwaltet externe CLI-Prozesse.

"},{"location":"architecture/backend/#features","title":"Features","text":"
  • Streaming: stdout/stderr werden zeilenweise gelesen
  • Progress-Callbacks: Erm\u00f6glicht Echtzeit-Fortschrittsanzeige
  • Graceful Shutdown: SIGINT \u2192 Warte-Timeout \u2192 SIGKILL
  • Prozess-Registry: Verfolgt aktive Prozesse f\u00fcr sauberes Beenden
"},{"location":"architecture/backend/#nutzung","title":"Nutzung","text":"
const result = await runProcess(\n  'HandBrakeCLI',\n  ['--input', rawFile, '--output', outputFile, '--preset', preset],\n  {\n    onStderr: (line) => parseHandBrakeProgress(line),\n    onStdout: (line) => logger.debug(line)\n  }\n);\n
"},{"location":"architecture/backend/#websocketservicejs","title":"websocketService.js","text":"

WebSocket-Server f\u00fcr Echtzeit-Client-Kommunikation.

"},{"location":"architecture/backend/#betrieb","title":"Betrieb","text":"
  • L\u00e4uft auf Pfad /ws des Express-Servers
  • H\u00e4lt eine Registry aller verbundenen Clients
  • Erm\u00f6glicht Broadcast an alle Clients oder gezieltes Senden
"},{"location":"architecture/backend/#api","title":"API","text":"
broadcast('PIPELINE_STATE_CHANGED', { state, activeJobId });\nbroadcast('PIPELINE_PROGRESS', { state, progress, eta, statusText });\nbroadcast('PIPELINE_QUEUE_CHANGED', queueSnapshot);\n
"},{"location":"architecture/backend/#omdbservicejs","title":"omdbService.js","text":"

Integration mit der OMDb API.

"},{"location":"architecture/backend/#methoden","title":"Methoden","text":"Methode Beschreibung searchByTitle(title, type) Suche nach Titel (movie/series) fetchById(imdbId) Vollst\u00e4ndige Metadaten per IMDb-ID"},{"location":"architecture/backend/#zuruckgegebene-daten","title":"Zur\u00fcckgegebene Daten","text":"
{\n  \"imdbId\": \"tt1375666\",\n  \"title\": \"Inception\",\n  \"year\": \"2010\",\n  \"type\": \"movie\",\n  \"poster\": \"https://...\",\n  \"plot\": \"...\",\n  \"director\": \"Christopher Nolan\"\n}\n
"},{"location":"architecture/backend/#settingsservicejs","title":"settingsService.js","text":"

Verwaltet alle Anwendungseinstellungen.

"},{"location":"architecture/backend/#features_1","title":"Features","text":"
  • Schema-getriebene Validierung: Jede Einstellung hat Typ, Grenzen und Pflichtfeld-Flag
  • Kategorisierung: Einstellungen sind in Kategorien gruppiert (Paths, Tools, Encoding, ...)
  • Persistenz: Werte in SQLite, Schema ebenfalls in SQLite
  • Defaults: defaultSettings.js definiert Standardwerte
"},{"location":"architecture/backend/#einstellungs-kategorien","title":"Einstellungs-Kategorien","text":"Kategorie Einstellungen Pfade raw_dir, movie_dir, log_dir Laufwerk drive_mode, drive_device, disc_poll_interval_ms, makemkv_source_index Monitoring hardware_monitoring_enabled, hardware_monitoring_interval_ms Tools makemkv_command, handbrake_command, mediainfo_command, pipeline_max_parallel_jobs Metadaten omdb_api_key, omdb_default_type Benachrichtigungen pushover_user_key, pushover_api_token"},{"location":"architecture/backend/#historyservicejs","title":"historyService.js","text":"

Datenbankoperationen f\u00fcr Job-Historie.

"},{"location":"architecture/backend/#hauptoperationen","title":"Hauptoperationen","text":"Operation Beschreibung listJobs(filters) Jobs nach Status/Titel filtern getJob(id) Job-Details mit Logs abrufen findOrphanRawFolders() Nicht-getrackte Raw-Ordner finden importOrphanRaw(path) Orphan-Ordner als Job importieren assignOmdb(id, omdbData) OMDb-Metadaten nachtr\u00e4glich zuweisen deleteJob(id, deleteFiles) Job und optional Dateien l\u00f6schen"},{"location":"architecture/backend/#notificationservicejs","title":"notificationService.js","text":"

PushOver-Push-Benachrichtigungen.

await notify({\n  title: 'Ripster: Job abgeschlossen',\n  message: 'Inception (2010) wurde erfolgreich encodiert'\n});\n
"},{"location":"architecture/backend/#loggerjs","title":"logger.js","text":"

Strukturiertes Logging mit t\u00e4glicher Log-Rotation.

"},{"location":"architecture/backend/#log-level","title":"Log-Level","text":"Level Verwendung debug Detaillierte Entwicklungs-Informationen info Normale Betriebsereignisse warn Warnungen, die Aufmerksamkeit ben\u00f6tigen error Fehler, die den Betrieb beeintr\u00e4chtigen"},{"location":"architecture/backend/#log-dateien","title":"Log-Dateien","text":"
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
"},{"location":"architecture/database/","title":"Datenbank","text":"

Ripster verwendet SQLite3 als Datenbank. Die Datenbankdatei liegt unter backend/data/ripster.db.

"},{"location":"architecture/database/#schema-ubersicht","title":"Schema-\u00dcbersicht","text":"
-- Vier Haupt-Tabellen\nsettings_schema    -- Einstellungs-Definitionen\nsettings_values    -- Benutzer-Werte\njobs               -- Rip-Job-Datens\u00e4tze\npipeline_state     -- Aktueller Pipeline-Zustand (Singleton)\n
"},{"location":"architecture/database/#tabelle-jobs","title":"Tabelle: jobs","text":"

Die wichtigste Tabelle \u2013 speichert alle Ripping-Jobs.

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
"},{"location":"architecture/database/#job-status-werte","title":"Job-Status-Werte","text":"Status Beschreibung ANALYZING MakeMKV analysiert die Disc METADATA_SELECTION Wartet auf Benutzer-Metadaten-Auswahl READY_TO_START Bereit zum Starten RIPPING MakeMKV rippt die Disc MEDIAINFO_CHECK MediaInfo analysiert die Raw-Datei READY_TO_ENCODE Wartet auf Encode-Best\u00e4tigung ENCODING HandBrake encodiert FINISHED Erfolgreich abgeschlossen ERROR Fehler aufgetreten"},{"location":"architecture/database/#tabelle-pipeline_state","title":"Tabelle: pipeline_state","text":"

Singleton-Tabelle f\u00fcr den aktuellen Pipeline-Zustand (immer genau 1 Zeile).

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
"},{"location":"architecture/database/#tabelle-settings_schema","title":"Tabelle: settings_schema","text":"

Definiert alle verf\u00fcgbaren Einstellungen mit Metadaten.

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
"},{"location":"architecture/database/#tabelle-settings_values","title":"Tabelle: settings_values","text":"

Speichert benutzer-konfigurierte Werte.

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
"},{"location":"architecture/database/#schema-migrationen","title":"Schema-Migrationen","text":"

database.js implementiert automatische Migrationen:

  1. Beim Start wird das aktuelle Schema gepr\u00fcft
  2. Fehlende Tabellen werden erstellt
  3. Fehlende Spalten werden hinzugef\u00fcgt
  4. Neue Default-Einstellungen werden eingef\u00fcgt
"},{"location":"architecture/database/#korruptions-recovery","title":"Korruptions-Recovery","text":"

Falls die Datenbankdatei korrupt ist:

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
"},{"location":"architecture/database/#datenbankpfad-konfigurieren","title":"Datenbankpfad konfigurieren","text":"

Standard: ./data/ripster.db (relativ zum Backend-Verzeichnis)

\u00dcber Umgebungsvariable anpassen:

DB_PATH=/var/lib/ripster/ripster.db\n
"},{"location":"architecture/database/#direkte-datenbankinspektion","title":"Direkte Datenbankinspektion","text":"
# 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
"},{"location":"architecture/frontend/","title":"Frontend-Komponenten","text":"

Das Frontend ist mit React 18 und PrimeReact gebaut und kommuniziert \u00fcber REST-API und WebSocket mit dem Backend.

"},{"location":"architecture/frontend/#seiten-pages","title":"Seiten (Pages)","text":""},{"location":"architecture/frontend/#dashboardpagejsx","title":"DashboardPage.jsx","text":"

Die Hauptseite von Ripster \u2013 zeigt den aktuellen Pipeline-Status und erm\u00f6glicht alle Workflow-Aktionen.

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)

Zugeh\u00f6rige Komponenten: - PipelineStatusCard \u2013 Status-Widget - MetadataSelectionDialog \u2013 OMDb-Suche und Playlist-Auswahl - MediaInfoReviewPanel \u2013 Track-Auswahl vor dem Encoding - Queue- und Job-Karten-UI direkt in DashboardPage

"},{"location":"architecture/frontend/#settingspagejsx","title":"SettingsPage.jsx","text":"

Konfigurationsoberfl\u00e4che f\u00fcr alle Ripster-Einstellungen.

Funktionen: - Dynamisch generiertes Formular aus dem Settings-Schema - Echtzeit-Validierungsfeedback - PushOver-Verbindungstest - Automatische Aktualisierung des Encode-Reviews bei relevanten \u00c4nderungen

"},{"location":"architecture/frontend/#databasepagejsx-history","title":"DatabasePage.jsx (/history)","text":"

Job-Historie und Datenbankansicht mit vollst\u00e4ndigem Audit-Trail.

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

"},{"location":"architecture/frontend/#komponenten-components","title":"Komponenten (Components)","text":""},{"location":"architecture/frontend/#metadataselectiondialogjsx","title":"MetadataSelectionDialog.jsx","text":"

Dialog f\u00fcr die Metadaten-Auswahl nach der Disc-Analyse.

\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
"},{"location":"architecture/frontend/#mediainforeviewpaneljsx","title":"MediaInfoReviewPanel.jsx","text":"

Track-Auswahl-Panel vor dem Encoding.

\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
"},{"location":"architecture/frontend/#dynamicsettingsformjsx","title":"DynamicSettingsForm.jsx","text":"

Wiederverwendbares Formular, das aus dem Settings-Schema generiert wird.

Unterst\u00fctzte Feldtypen:

Typ UI-Element string Text-Input number Zahlen-Input mit Min/Max boolean Toggle/Checkbox select Dropdown password Passwort-Input"},{"location":"architecture/frontend/#pipelinestatuscardjsx","title":"PipelineStatusCard.jsx","text":"

Status-Anzeige-Widget f\u00fcr die Dashboard-Seite.

"},{"location":"architecture/frontend/#jobdetaildialogjsx","title":"JobDetailDialog.jsx","text":"

Vollst\u00e4ndiger Job-Detail-Dialog mit Logs-Viewer.

"},{"location":"architecture/frontend/#hooks","title":"Hooks","text":""},{"location":"architecture/frontend/#usewebsocketjs","title":"useWebSocket.js","text":"

Zentraler Custom-Hook f\u00fcr die WebSocket-Verbindung.

useWebSocket({\n  onMessage: (msg) => {\n    if (msg.type === 'PIPELINE_STATE_CHANGED') {\n      setPipelineState(msg.payload);\n    }\n  }\n});\n

Features: - Automatische Verbindung zu /ws - Reconnect mit festem Intervall (1500ms) - Message-Parsing (JSON)

"},{"location":"architecture/frontend/#api-client-clientjs","title":"API-Client (client.js)","text":"

Zentraler HTTP-Client f\u00fcr alle Backend-Anfragen.

// 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

Features: - Zentralisierte Fehlerbehandlung - Automatische JSON-Serialisierung - Basis-URL aus Umgebungsvariable (VITE_API_BASE)

"},{"location":"architecture/frontend/#build-entwicklung","title":"Build & Entwicklung","text":""},{"location":"architecture/frontend/#entwicklungsserver","title":"Entwicklungsserver","text":"
cd frontend\nnpm run dev\n# \u2192 http://localhost:5173\n
"},{"location":"architecture/frontend/#vite-proxy-konfiguration","title":"Vite-Proxy-Konfiguration","text":"

In der Entwicklungsumgebung proxied Vite API-Anfragen zum Backend:

// vite.config.js\nproxy: {\n  '/api': 'http://localhost:3001',\n  '/ws': { target: 'ws://localhost:3001', ws: true }\n}\n
"},{"location":"architecture/frontend/#production-build","title":"Production-Build","text":"
cd frontend\nnpm run build\n# \u2192 frontend/dist/\n
"},{"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":"

Der gesamte Ripping-Workflow ist als State Machine implementiert. Der pipelineService verwaltet den aktuellen Zustand und emittiert Ereignisse bei jedem Zustandswechsel. Der WebSocket-Service \u00fcbertr\u00e4gt diese Ereignisse sofort an alle verbundenen Clients.

Zustandswechsel \u2192 Event \u2192 WebSocket \u2192 Frontend-Update\n
"},{"location":"architecture/overview/#service-layer-muster","title":"Service-Layer-Muster","text":"
HTTP-Route \u2192 Service \u2192 Datenbank\n

Routes delegieren die gesamte Business-Logik an Services. Services sind voneinander unabh\u00e4ngig und k\u00f6nnen einzeln getestet werden.

"},{"location":"architecture/overview/#schema-getriebene-einstellungen","title":"Schema-getriebene Einstellungen","text":"

Die Settings-Konfiguration definiert sowohl die Validierungsregeln als auch die UI-Struktur in einer einzigen Quelle (settings_schema-Tabelle). Die DynamicSettingsForm-Komponente rendert das Formular dynamisch aus dem Schema.

"},{"location":"architecture/overview/#echtzeit-kommunikation","title":"Echtzeit-Kommunikation","text":""},{"location":"architecture/overview/#websocket-protokoll","title":"WebSocket-Protokoll","text":"

Der WebSocket-Server l\u00e4uft unter dem Pfad /ws. Nachrichten werden als JSON \u00fcbertragen:

{\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

Nachrichtentypen:

Typ Beschreibung PIPELINE_STATE_CHANGED Pipeline-Zustand hat gewechselt PIPELINE_PROGRESS Fortschritt (% und ETA) PIPELINE_QUEUE_CHANGED Queue-Status ge\u00e4ndert DISC_DETECTED Disc wurde erkannt DISC_REMOVED Disc wurde entfernt PIPELINE_ERROR Pipeline-Fehler aufgetreten DISK_DETECTION_ERROR Laufwerkserkennung-Fehler"},{"location":"architecture/overview/#reconnect-logik","title":"Reconnect-Logik","text":"

Der Frontend-Hook useWebSocket.js implementiert automatisches Reconnect mit festem Intervall von 1500ms bei Verbindungsabbr\u00fcchen.

"},{"location":"architecture/overview/#prozess-management","title":"Prozess-Management","text":""},{"location":"architecture/overview/#processrunnerjs","title":"processRunner.js","text":"

Externe Tools (MakeMKV, HandBrake, MediaInfo) werden als Child Processes gestartet:

spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'] })\n
  • stdout/stderr werden zeilenweise gelesen und in Echtzeit verarbeitet
  • Progress-Parsing erfolgt \u00fcber regul\u00e4re Ausdr\u00fccke in progressParsers.js
  • Graceful Shutdown: SIGINT \u2192 Timeout \u2192 SIGKILL
  • Prozess-Tracking: Aktive Prozesse werden registriert f\u00fcr sauberes Beenden
"},{"location":"architecture/overview/#datenpersistenz","title":"Datenpersistenz","text":""},{"location":"architecture/overview/#sqlite-datenbank","title":"SQLite-Datenbank","text":"

Ripster verwendet eine einzige SQLite-Datei f\u00fcr alle persistenten Daten:

backend/data/ripster.db\n

Tabellen:

Tabelle Inhalt jobs Alle Rip-Jobs mit Status, Logs, Metadaten pipeline_state Aktueller Pipeline-Zustand (Singleton) settings_schema Schema aller verf\u00fcgbaren Einstellungen settings_values Benutzer-konfigurierte Werte"},{"location":"architecture/overview/#migrations-strategie","title":"Migrations-Strategie","text":"

Beim Start pr\u00fcft database.js 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.

"},{"location":"architecture/overview/#fehlerbehandlung","title":"Fehlerbehandlung","text":""},{"location":"architecture/overview/#strukturierte-fehler","title":"Strukturierte Fehler","text":"

Alle Fehler werden mit Kontext-Metadaten protokolliert:

logger.error('Encoding fehlgeschlagen', {\n  jobId: job.id,\n  command: cmd,\n  exitCode: code,\n  stderr: lastLines\n});\n
"},{"location":"architecture/overview/#job-fehler-recovery","title":"Job-Fehler-Recovery","text":"
  • Fehlgeschlagene Jobs bleiben in der Datenbank (Status ERROR)
  • Vollst\u00e4ndige Fehler-Logs werden im Job-Datensatz gespeichert
  • Retry-Funktion erm\u00f6glicht Neustart von einem Fehler-Zustand
  • Re-Encode erlaubt erneutes Encodieren ohne neu zu rippen
"},{"location":"architecture/overview/#sicherheit","title":"Sicherheit","text":""},{"location":"architecture/overview/#eingabe-validierung","title":"Eingabe-Validierung","text":"
  • Alle Benutzer-Eingaben werden in validators.js validiert
  • CLI-Argumente werden sicher \u00fcber commandLine.js konstruiert (kein Shell-Injection-Risiko)
  • Pfade werden sanitisiert bevor sie an externe Prozesse \u00fcbergeben werden
"},{"location":"architecture/overview/#cors-konfiguration","title":"CORS-Konfiguration","text":"
CORS_ORIGIN=http://localhost:5173\n

In Produktion sollte dieser Wert auf die tats\u00e4chliche Frontend-URL gesetzt werden.

"},{"location":"configuration/","title":"Konfiguration","text":"
  • Einstellungsreferenz

    Alle verf\u00fcgbaren Einstellungen mit Typen, Standardwerten und Beschreibungen.

    Einstellungsreferenz

  • Umgebungsvariablen

    Umgebungsvariablen f\u00fcr Backend und Frontend.

    Umgebungsvariablen

"},{"location":"configuration/environment/","title":"Umgebungsvariablen","text":"

Umgebungsvariablen \u00fcberschreiben die Standardwerte und eignen sich f\u00fcr Server-Deployments.

"},{"location":"configuration/environment/#backend-umgebungsvariablen","title":"Backend-Umgebungsvariablen","text":"

Konfigurationsdatei: backend/.env

Variable Standard Beschreibung PORT 3001 Port des Express-Servers DB_PATH ./data/ripster.db Pfad zur SQLite-Datenbankdatei CORS_ORIGIN http://localhost:5173 Erlaubter CORS-Origin LOG_DIR ./logs Verzeichnis f\u00fcr Log-Dateien LOG_LEVEL info Log-Level (debug, info, warn, error)"},{"location":"configuration/environment/#beispiel-backendenv","title":"Beispiel: backend/.env","text":"
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
"},{"location":"configuration/environment/#frontend-umgebungsvariablen","title":"Frontend-Umgebungsvariablen","text":"

Konfigurationsdatei: frontend/.env

Variable Standard Beschreibung VITE_API_BASE http://localhost:3001 Backend-API-URL VITE_WS_URL ws://localhost:3001 WebSocket-URL VITE_PUBLIC_ORIGIN \u2014 \u00d6ffentliche Origin-URL (f\u00fcr CORS) VITE_HMR_HOST \u2014 Vite HMR-Host (f\u00fcr Remote-Entwicklung) VITE_HMR_PORT \u2014 Vite HMR-Port"},{"location":"configuration/environment/#beispiel-frontendenv-entwicklung","title":"Beispiel: frontend/.env (Entwicklung)","text":"
VITE_API_BASE=http://localhost:3001\nVITE_WS_URL=ws://localhost:3001\n
"},{"location":"configuration/environment/#beispiel-frontendenv-netzwerk-zugriff","title":"Beispiel: frontend/.env (Netzwerk-Zugriff)","text":"
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
"},{"location":"configuration/environment/#envexample-dateien","title":".env.example Dateien","text":"

Das Repository enth\u00e4lt Vorlagen f\u00fcr beide Konfigurationsdateien:

# Backend\ncp backend/.env.example backend/.env\n\n# Frontend\ncp frontend/.env.example frontend/.env\n
"},{"location":"configuration/environment/#prioritat-der-konfiguration","title":"Priorit\u00e4t der Konfiguration","text":"

Einstellungen werden in folgender Reihenfolge geladen (h\u00f6here Priorit\u00e4t \u00fcberschreibt niedrigere):

1. Systemumgebungsvariablen (export VAR=value)\n2. .env-Datei\n3. Hardcodierte Standardwerte in config.js\n
"},{"location":"configuration/environment/#log_level","title":"LOG_LEVEL","text":"Level Ausgabe debug Alle Meldungen inkl. Debugging info Normale Betriebsinformationen warn Warnungen + Fehler error Nur Fehler

Produktionsempfehlung

F\u00fcr Produktionsumgebungen LOG_LEVEL=info oder LOG_LEVEL=warn verwenden. debug erzeugt sehr viele Log-Eintr\u00e4ge.

"},{"location":"configuration/settings-reference/","title":"Einstellungsreferenz","text":"

Vollst\u00e4ndige \u00dcbersicht aller Ripster-Einstellungen. Alle Einstellungen werden \u00fcber die Web-Oberfl\u00e4che unter Einstellungen verwaltet.

"},{"location":"configuration/settings-reference/#kategorie-pfade-paths","title":"Kategorie: Pfade (paths)","text":"Schl\u00fcssel Typ Standard Pflicht Beschreibung raw_dir string \u2014 \u2705 Verzeichnis f\u00fcr rohe MKV-Dateien nach dem Ripping movie_dir string \u2014 \u2705 Ausgabeverzeichnis f\u00fcr encodierte Filme log_dir string ./logs \u2014 Verzeichnis f\u00fcr Log-Dateien

Beispielkonfiguration

raw_dir   = /mnt/nas/raw\nmovie_dir = /mnt/nas/movies\nlog_dir   = /var/log/ripster\n
"},{"location":"configuration/settings-reference/#kategorie-tools-tools","title":"Kategorie: Tools (tools)","text":"Schl\u00fcssel Typ Standard Beschreibung makemkv_command string makemkvcon Befehl oder absoluter Pfad zu MakeMKV handbrake_command string HandBrakeCLI Befehl oder absoluter Pfad zu HandBrake mediainfo_command string mediainfo Befehl oder absoluter Pfad zu MediaInfo

Absolute Pfade verwenden

Falls die Tools nicht im PATH des Systems sind:

makemkv_command   = /usr/local/bin/makemkvcon\nhandbrake_command = /usr/local/bin/HandBrakeCLI\nmediainfo_command = /usr/bin/mediainfo\n

"},{"location":"configuration/settings-reference/#kategorie-encoding-encoding","title":"Kategorie: Encoding (encoding)","text":"Schl\u00fcssel Typ Standard Beschreibung handbrake_preset string H.265 MKV 1080p30 Name des HandBrake-Presets handbrake_extra_args string (leer) Zus\u00e4tzliche HandBrake CLI-Argumente output_extension string mkv Dateiendung der Ausgabedatei filename_template string {title} ({year}) Template f\u00fcr den Dateinamen"},{"location":"configuration/settings-reference/#verfugbare-handbrake-presets","title":"Verf\u00fcgbare HandBrake-Presets","text":"

Eine vollst\u00e4ndige Liste der verf\u00fcgbaren Presets:

HandBrakeCLI --preset-list\n

H\u00e4ufig verwendete Presets:

Preset Beschreibung H.265 MKV 1080p30 H.265/HEVC, Full-HD, 30fps H.265 MKV 720p30 H.265/HEVC, HD, 30fps H.264 MKV 1080p30 H.264/AVC, Full-HD, 30fps HQ 1080p30 Surround Hohe Qualit\u00e4t, Full-HD mit Surround"},{"location":"configuration/settings-reference/#dateiname-template-platzhalter","title":"Dateiname-Template-Platzhalter","text":"Platzhalter Beispiel {title} Inception {year} 2010 {imdb_id} tt1375666 {type} movie"},{"location":"configuration/settings-reference/#kategorie-laufwerk-drive","title":"Kategorie: Laufwerk (drive)","text":"Schl\u00fcssel Typ Standard Optionen Beschreibung drive_mode select auto auto, explicit Laufwerk-Erkennungsmodus drive_device string /dev/sr0 \u2014 Ger\u00e4te-Pfad (nur bei explicit) disc_poll_interval_ms number 4000 1000\u201360000 Polling-Intervall in Millisekunden

drive_mode Optionen:

Modus Beschreibung auto Ripster erkennt das Laufwerk automatisch explicit Verwendet das in drive_device konfigurierte Ger\u00e4t"},{"location":"configuration/settings-reference/#kategorie-makemkv-makemkv","title":"Kategorie: MakeMKV (makemkv)","text":"Schl\u00fcssel Typ Standard Min Max Beschreibung makemkv_min_length_minutes number 15 0 999 Mindest-Titell\u00e4nge in Minuten makemkv_backup_mode boolean false \u2014 \u2014 Backup-Modus statt MKV-Modus

makemkv_min_length_minutes: Titel k\u00fcrzer als dieser Wert werden von MakeMKV ignoriert. Verhindert das Rippen von Men\u00fc-Schleifen und kurzen Extra-Clips.

makemkv_backup_mode: Im Backup-Modus erstellt MakeMKV eine vollst\u00e4ndige Disc-Kopie mit Men\u00fcs. Im Standard-Modus werden direkt MKV-Dateien erstellt.

"},{"location":"configuration/settings-reference/#kategorie-omdb-omdb","title":"Kategorie: OMDb (omdb)","text":"Schl\u00fcssel Typ Standard Pflicht Beschreibung omdb_api_key string \u2014 \u2705 API-Key von omdbapi.com omdb_default_type select movie \u2014 Standard-Suchtyp: movie oder series"},{"location":"configuration/settings-reference/#kategorie-benachrichtigungen-notifications","title":"Kategorie: Benachrichtigungen (notifications)","text":"Schl\u00fcssel Typ Standard Beschreibung pushover_user_key string \u2014 PushOver User-Key pushover_api_token string \u2014 PushOver API-Token

Beide Felder m\u00fcssen konfiguriert sein, um PushOver-Benachrichtigungen zu aktivieren. Die Verbindung kann mit dem Test-Button in den Einstellungen gepr\u00fcft werden.

"},{"location":"configuration/settings-reference/#standard-einstellungen-zurucksetzen","title":"Standard-Einstellungen zur\u00fccksetzen","text":"

\u00dcber die Datenbank k\u00f6nnen Einstellungen auf Standardwerte zur\u00fcckgesetzt werden:

sqlite3 backend/data/ripster.db \\\n  \"DELETE FROM settings_values WHERE key = 'handbrake_preset';\"\n

Beim n\u00e4chsten Laden der Einstellungen wird der Standardwert verwendet.

"},{"location":"deployment/","title":"Deployment","text":"
  • Entwicklungsumgebung

    Lokale Entwicklungsumgebung einrichten.

    Entwicklung

  • Produktion

    Ripster auf einem Server dauerhaft betreiben.

    Produktion

"},{"location":"deployment/development/","title":"Entwicklungsumgebung","text":""},{"location":"deployment/development/#voraussetzungen","title":"Voraussetzungen","text":"
  • Node.js >= 20.19.0
  • Alle externen Tools installiert
"},{"location":"deployment/development/#schnellstart","title":"Schnellstart","text":"
./start.sh\n

Das Skript startet automatisch: - Backend auf Port 3001 (mit Nodemon f\u00fcr Hot-Reload) - Frontend auf Port 5173 (mit Vite HMR)

"},{"location":"deployment/development/#manuelle-entwicklungsumgebung","title":"Manuelle Entwicklungsumgebung","text":""},{"location":"deployment/development/#terminal-1-backend","title":"Terminal 1 \u2013 Backend","text":"
cd backend\nnpm install\nnpm run dev\n

Backend l\u00e4uft auf http://localhost:3001 mit Nodemon \u2013 Neustart bei Datei\u00e4nderungen.

"},{"location":"deployment/development/#terminal-2-frontend","title":"Terminal 2 \u2013 Frontend","text":"
cd frontend\nnpm install\nnpm run dev\n

Frontend l\u00e4uft auf http://localhost:5173 mit Vite HMR \u2013 sofortige Browser-Updates.

"},{"location":"deployment/development/#vite-proxy","title":"Vite-Proxy","text":"

Im Entwicklungsmodus proxied Vite alle API- und WebSocket-Anfragen zum Backend:

// 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

Das bedeutet: Im Browser macht das Frontend Anfragen an localhost:5173/api/... \u2013 Vite leitet diese an localhost:3001/api/... weiter.

"},{"location":"deployment/development/#remote-entwicklung","title":"Remote-Entwicklung","text":"

Falls Ripster auf einem entfernten Server entwickelt wird (z.B. Homeserver), muss die Vite-Konfiguration angepasst werden:

# 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
"},{"location":"deployment/development/#log-level-fur-entwicklung","title":"Log-Level f\u00fcr Entwicklung","text":"
# backend/.env\nLOG_LEVEL=debug\n

Im Debug-Modus werden alle Ausgaben der externen Tools (MakeMKV, HandBrake) vollst\u00e4ndig geloggt.

"},{"location":"deployment/development/#stoppen","title":"Stoppen","text":"
./kill.sh\n
"},{"location":"deployment/development/#linting-type-checking","title":"Linting & Type-Checking","text":"
# Frontend (ESLint)\ncd frontend && npm run lint\n\n# Backend hat keine separaten Lint-Scripts,\n# nutze direkt eslint falls konfiguriert\n
"},{"location":"deployment/development/#deployment-script","title":"Deployment-Script","text":"

Das deploy-ripster.sh-Script \u00fcbertr\u00e4gt Code auf einen Remote-Server per SSH:

./deploy-ripster.sh\n

Was das Script tut: 1. rsync synchronisiert den Code (Backend-Quellcode ohne data/) 2. Die Datenbank (backend/data/) wird nicht \u00fcberschrieben 3. Verbindung via SSH (konfigurierbar im Script)

Anpassung des Scripts:

# deploy-ripster.sh\nREMOTE_HOST=\"192.168.1.100\"\nREMOTE_USER=\"michael\"\nREMOTE_PATH=\"/home/michael/ripster\"\n
"},{"location":"deployment/production/","title":"Produktions-Deployment","text":""},{"location":"deployment/production/#empfohlene-architektur","title":"Empfohlene Architektur","text":"
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
"},{"location":"deployment/production/#systemd-service","title":"systemd-Service","text":"

F\u00fcr ein dauerhaftes Betreiben als systemd-Service:

sudo nano /etc/systemd/system/ripster.service\n
[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
# 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
"},{"location":"deployment/production/#frontend-build","title":"Frontend-Build","text":"

F\u00fcr Produktion das Frontend bauen:

cd frontend\nnpm run build\n

Die statischen Dateien landen in frontend/dist/.

"},{"location":"deployment/production/#nginx-konfiguration","title":"nginx-Konfiguration","text":"
# /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
sudo ln -s /etc/nginx/sites-available/ripster /etc/nginx/sites-enabled/\nsudo nginx -t && sudo systemctl reload nginx\n
"},{"location":"deployment/production/#nur-backend-produktion-ohne-nginx","title":"Nur-Backend-Produktion (ohne nginx)","text":"

Falls kein Reverse Proxy gew\u00fcnscht ist, kann das Backend die Frontend-Dateien direkt ausliefern:

# Frontend bauen\ncd frontend && npm run build\n\n# Backend startet und serviert frontend/dist/\ncd backend && NODE_ENV=production npm start\n

Das Backend ist so konfiguriert, dass es im Produktionsmodus die frontend/dist/-Dateien als statische Assets ausliefert.

"},{"location":"deployment/production/#datenbank-backup","title":"Datenbank-Backup","text":"
# 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

Automatisches Backup

Cron-Job f\u00fcr t\u00e4gliches Backup:

0 3 * * * sqlite3 /home/michael/ripster/backend/data/ripster.db \".backup '/mnt/backup/ripster-$(date +\\%Y\\%m\\%d).db'\"\n

"},{"location":"deployment/production/#log-rotation","title":"Log-Rotation","text":"

Ripster rotiert Logs automatisch t\u00e4glich. Falls zus\u00e4tzlich systemd-Journal-Rotation gew\u00fcnscht ist:

# /etc/logrotate.d/ripster\n/home/michael/ripster/backend/logs/*.log {\n    daily\n    rotate 14\n    compress\n    missingok\n    notifempty\n}\n
"},{"location":"deployment/production/#sicherheitshinweise","title":"Sicherheitshinweise","text":"

Heimnetz-Einsatz

Ripster ist f\u00fcr den Einsatz im lokalen Heimnetz konzipiert und enth\u00e4lt keine Authentifizierung. Stelle sicher, dass der Dienst nicht \u00f6ffentlich erreichbar ist.

Falls \u00f6ffentlicher Zugang ben\u00f6tigt wird:

  1. Basic Auth via nginx:

    sudo htpasswd -c /etc/nginx/.htpasswd michael\n
    location / {\n    auth_basic \"Ripster\";\n    auth_basic_user_file /etc/nginx/.htpasswd;\n    # ...\n}\n

  2. VPN-Zugang (empfohlen): Zugriff nur \u00fcber WireGuard/OpenVPN

  3. SSL/TLS: Let's Encrypt mit certbot f\u00fcr HTTPS

"},{"location":"getting-started/","title":"Erste Schritte","text":"

Dieser Abschnitt f\u00fchrt dich durch die Installation und Einrichtung von Ripster.

"},{"location":"getting-started/#uberblick","title":"\u00dcberblick","text":"
  • :material-list-check: Voraussetzungen

    Systemanforderungen und externe Tools, die vor der Installation ben\u00f6tigt werden.

    Voraussetzungen pr\u00fcfen

  • Installation

    Schritt-f\u00fcr-Schritt-Anleitung zur Installation von Ripster.

    Installation starten

  • Konfiguration

    Einrichten von Pfaden, API-Keys und Encoding-Presets.

    Konfigurieren

  • Schnellstart

    Rippe deinen ersten Film in wenigen Minuten.

    Loslegen

"},{"location":"getting-started/configuration/","title":"Konfiguration","text":"

Alle Einstellungen werden \u00fcber die Web-Oberfl\u00e4che unter Einstellungen verwaltet und in der SQLite-Datenbank gespeichert.

"},{"location":"getting-started/configuration/#pflichteinstellungen","title":"Pflichteinstellungen","text":"

Diese Einstellungen m\u00fcssen vor dem ersten Rip konfiguriert werden:

"},{"location":"getting-started/configuration/#pfade","title":"Pfade","text":"Einstellung Beschreibung Beispiel raw_dir Verzeichnis f\u00fcr rohe MKV-Dateien /mnt/nas/raw movie_dir Ausgabeverzeichnis f\u00fcr kodierte Filme /mnt/nas/movies log_dir Verzeichnis f\u00fcr Log-Dateien /var/log/ripster

Berechtigungen

Der Ripster-Prozess ben\u00f6tigt Schreibrechte auf alle konfigurierten Verzeichnisse.

# Verzeichnisse erstellen und Berechtigungen setzen\nsudo mkdir -p /mnt/nas/{raw,movies}\nsudo chown $USER:$USER /mnt/nas/{raw,movies}\n
"},{"location":"getting-started/configuration/#omdb-api","title":"OMDb API","text":"Einstellung Beschreibung omdb_api_key API-Key von omdbapi.com omdb_default_type Standard-Suchtyp: movie oder series"},{"location":"getting-started/configuration/#tool-konfiguration","title":"Tool-Konfiguration","text":"Einstellung Standard Beschreibung makemkv_command makemkvcon Pfad oder Befehl f\u00fcr MakeMKV handbrake_command HandBrakeCLI Pfad oder Befehl f\u00fcr HandBrake mediainfo_command mediainfo Pfad oder Befehl f\u00fcr MediaInfo

Absolute Pfade

Falls die Tools nicht im PATH sind, verwende absolute Pfade:

/usr/local/bin/HandBrakeCLI\n

"},{"location":"getting-started/configuration/#encoding-konfiguration","title":"Encoding-Konfiguration","text":"Einstellung Standard Beschreibung handbrake_preset H.265 MKV 1080p30 HandBrake-Preset-Name handbrake_extra_args (leer) Zus\u00e4tzliche HandBrake-Argumente output_extension mkv Dateiendung der Ausgabedatei filename_template {title} ({year}) Template f\u00fcr Dateinamen"},{"location":"getting-started/configuration/#dateiname-template","title":"Dateiname-Template","text":"

Das Template unterst\u00fctzt folgende Platzhalter:

Platzhalter Beschreibung Beispiel {title} Filmtitel Inception {year} Erscheinungsjahr 2010 {imdb_id} IMDb-ID tt1375666 {type} movie oder series movie

Beispiel-Template:

{title} ({year})\n\u2192 Inception (2010).mkv\n

"},{"location":"getting-started/configuration/#laufwerk-konfiguration","title":"Laufwerk-Konfiguration","text":"Einstellung Standard Beschreibung drive_mode auto auto (automatisch erkennen) oder explicit (festes Ger\u00e4t) drive_device /dev/sr0 Ger\u00e4te-Pfad (nur bei explicit) disc_poll_interval_ms 4000 Polling-Intervall in Millisekunden"},{"location":"getting-started/configuration/#makemkv-konfiguration","title":"MakeMKV-Konfiguration","text":"Einstellung Standard Beschreibung makemkv_min_length_minutes 15 Mindestl\u00e4nge f\u00fcr Titel in Minuten makemkv_backup_mode false Backup-Modus statt MKV-Modus

Backup-Modus

Im Backup-Modus erstellt MakeMKV eine vollst\u00e4ndige Kopie der Disc (inkl. Men\u00fcs). Der Standardmodus erstellt direkt MKV-Dateien.

"},{"location":"getting-started/configuration/#benachrichtigungen-pushover","title":"Benachrichtigungen (PushOver)","text":"Einstellung Beschreibung pushover_user_key Dein PushOver User-Key pushover_api_token API-Token deiner PushOver-App

Nach der Eingabe kann die Verbindung mit dem Test-Button gepr\u00fcft werden.

"},{"location":"getting-started/configuration/#vollstandige-einstellungsreferenz","title":"Vollst\u00e4ndige Einstellungsreferenz","text":"

Eine vollst\u00e4ndige Liste aller Einstellungen mit Typen, Validierung und Standardwerten findest du unter:

Einstellungsreferenz

"},{"location":"getting-started/installation/","title":"Installation","text":""},{"location":"getting-started/installation/#repository-klonen","title":"Repository klonen","text":"
git clone https://github.com/YOUR_GITHUB_USERNAME/ripster.git\ncd ripster\n
"},{"location":"getting-started/installation/#automatischer-start","title":"Automatischer Start","text":"

Ripster enth\u00e4lt ein start.sh-Skript, das alle Abh\u00e4ngigkeiten installiert und Backend + Frontend gleichzeitig startet:

./start.sh\n

Das Skript f\u00fchrt automatisch folgende Schritte durch:

  1. Node.js-Versionscheck \u2013 pr\u00fcft ob >= 20.19.0 verf\u00fcgbar ist (mit nvm/npx-Fallback)
  2. Abh\u00e4ngigkeiten installieren \u2013 npm install f\u00fcr Root, Backend und Frontend
  3. Dienste starten \u2013 Backend und Frontend werden parallel gestartet

Erfolgreich gestartet

  • Backend l\u00e4uft auf http://localhost:3001
  • Frontend l\u00e4uft auf http://localhost:5173
"},{"location":"getting-started/installation/#manuelle-installation","title":"Manuelle Installation","text":"

Falls du mehr Kontrolle ben\u00f6tigst:

# Root-Abh\u00e4ngigkeiten\nnpm install\n\n# Backend-Abh\u00e4ngigkeiten\ncd backend && npm install && cd ..\n\n# Frontend-Abh\u00e4ngigkeiten\ncd frontend && npm install && cd ..\n\n# Backend starten (Terminal 1)\ncd backend && npm run dev\n\n# Frontend starten (Terminal 2)\ncd frontend && npm run dev\n
"},{"location":"getting-started/installation/#umgebungsvariablen-konfigurieren","title":"Umgebungsvariablen konfigurieren","text":""},{"location":"getting-started/installation/#backend","title":"Backend","text":"
cp backend/.env.example backend/.env\n

Bearbeite backend/.env:

PORT=3001\nDB_PATH=./data/ripster.db\nCORS_ORIGIN=http://localhost:5173\nLOG_DIR=./logs\nLOG_LEVEL=info\n
"},{"location":"getting-started/installation/#frontend","title":"Frontend","text":"
cp frontend/.env.example frontend/.env\n

Bearbeite frontend/.env:

VITE_API_BASE=http://localhost:3001\nVITE_WS_URL=ws://localhost:3001\n

Alle Umgebungsvariablen

Eine vollst\u00e4ndige \u00dcbersicht aller Umgebungsvariablen findest du unter Umgebungsvariablen.

"},{"location":"getting-started/installation/#datenbank-initialisieren","title":"Datenbank initialisieren","text":"

Die SQLite-Datenbank wird automatisch beim ersten Start erstellt und mit dem Schema aus db/schema.sql initialisiert. Es sind keine manuellen Datenbankschritte erforderlich.

backend/data/\n\u2514\u2500\u2500 ripster.db    \u2190 Wird automatisch angelegt\n
"},{"location":"getting-started/installation/#stoppen","title":"Stoppen","text":"
./kill.sh\n

Das Skript beendet Backend- und Frontend-Prozesse graceful.

"},{"location":"getting-started/installation/#verzeichnisstruktur-nach-installation","title":"Verzeichnisstruktur nach Installation","text":"
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
"},{"location":"getting-started/installation/#nachste-schritte","title":"N\u00e4chste Schritte","text":"

Nach erfolgreicher Installation:

  1. \u00d6ffne http://localhost:5173
  2. Navigiere zu Einstellungen
  3. Konfiguriere Pfade, API-Keys und Encoding-Presets

Zur Konfiguration

"},{"location":"getting-started/prerequisites/","title":"Voraussetzungen","text":"

Bevor du Ripster installierst, stelle sicher, dass folgende Software auf deinem System verf\u00fcgbar ist.

"},{"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":"

Ripster ben\u00f6tigt Node.js >= 20.19.0.

nvm (empfohlen)Ubuntu/DebianmacOS (Homebrew)
# 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
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
brew install node@20\nnode --version  # v20.x.x\n
"},{"location":"getting-started/prerequisites/#externe-tools","title":"Externe Tools","text":""},{"location":"getting-started/prerequisites/#makemkv","title":"MakeMKV","text":"

Lizenz erforderlich

MakeMKV ist f\u00fcr den pers\u00f6nlichen Gebrauch kostenlos (Beta-Lizenz), ben\u00f6tigt aber eine g\u00fcltige Lizenz.

# 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

MakeMKV Download

"},{"location":"getting-started/prerequisites/#handbrake-cli","title":"HandBrake CLI","text":"
# 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

HandBrake Download

"},{"location":"getting-started/prerequisites/#mediainfo","title":"MediaInfo","text":"
# Ubuntu/Debian\nsudo apt-get install mediainfo\n\n# macOS\nbrew install mediainfo\n\n# Version pr\u00fcfen\nmediainfo --Version\n
"},{"location":"getting-started/prerequisites/#disc-laufwerk","title":"Disc-Laufwerk","text":"

Ripster ben\u00f6tigt ein physisches DVD- oder Blu-ray-Laufwerk.

Blu-ray unter Linux

F\u00fcr Blu-ray-Ripping unter Linux wird zus\u00e4tzlich libaacs ben\u00f6tigt. MakeMKV bringt jedoch eine eigene Entschl\u00fcsselung mit, daher ist dies in den meisten F\u00e4llen nicht erforderlich.

# Laufwerk pr\u00fcfen\nls /dev/sr*\n# oder\nlsblk | grep rom\n
"},{"location":"getting-started/prerequisites/#omdb-api-key","title":"OMDb API-Key","text":"

Ripster verwendet die OMDb API f\u00fcr Filmmetadaten.

  1. Registriere dich kostenlos auf omdbapi.com
  2. Best\u00e4tige deine E-Mail-Adresse
  3. Notiere deinen API-Key \u2013 du gibst ihn sp\u00e4ter in den Einstellungen ein
"},{"location":"getting-started/prerequisites/#optionale-voraussetzungen","title":"Optionale Voraussetzungen","text":""},{"location":"getting-started/prerequisites/#pushover-benachrichtigungen","title":"PushOver (Benachrichtigungen)","text":"

F\u00fcr mobile Push-Benachrichtigungen bei Fertigstellung oder Fehlern:

  • App kaufen auf pushover.net (~5 USD einmalig)
  • User Key und API Token notieren
"},{"location":"getting-started/prerequisites/#ssh-zugang-deployment","title":"SSH-Zugang (Deployment)","text":"

F\u00fcr Remote-Deployment via deploy-ripster.sh:

# sshpass installieren\nsudo apt-get install sshpass\n
"},{"location":"getting-started/prerequisites/#checkliste","title":"Checkliste","text":"
  • [ ] Node.js >= 20.19.0 installiert (node --version)
  • [ ] makemkvcon installiert (makemkvcon --version)
  • [ ] HandBrakeCLI installiert (HandBrakeCLI --version)
  • [ ] mediainfo installiert (mediainfo --Version)
  • [ ] DVD/Blu-ray Laufwerk vorhanden (ls /dev/sr*)
  • [ ] OMDb API-Key beschafft
"},{"location":"getting-started/quickstart/","title":"Schnellstart \u2013 Vollst\u00e4ndiger Workflow","text":"

Nach der Installation und Konfiguration f\u00fchrt diese Seite Schritt f\u00fcr Schritt durch den ersten Rip \u2013 mit allen Details aus dem Code.

"},{"location":"getting-started/quickstart/#ubersicht-pipeline-ablauf","title":"\u00dcbersicht: Pipeline-Ablauf","text":"\u25cf IDLE Warten 1 DISC_DETECTED Disc erkannt 2 METADATA_SELECTION OMDb & 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

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

Vollst\u00e4ndiges Zustandsdiagramm (inkl. Fehler- & Alternativpfade)
flowchart LR\n    START(( )) --> IDLE\n\n    IDLE -->|Disc erkannt| DD[DISC_DETECTED]\n    DD -->|Analyse starten| META[METADATA\\nSELECTION]\n\n    META -->|Metadaten \u00fcbernommen| RTS[READY_TO\\nSTART]\n    META -->|vorhandenes RAW +\\nPlaylist offen| WUD[WAITING_FOR\\nUSER_DECISION]\n    RTS -->|Auto-Start| RIP[RIPPING]\n    RTS -->|Auto-Start mit RAW| MIC[MEDIAINFO\\nCHECK]\n\n    RIP -->|MKV fertig| MIC\n    RIP -->|Fehler| ERR\n\n    MIC -->|Playlist offen (Backup)| WUD\n    WUD -->|Playlist best\u00e4tigt| MIC\n    WUD -->|Playlist best\u00e4tigt,\\nnoch kein RAW| RTS\n\n    MIC --> RTE[READY_TO\\nENCODE]\n    RTE -->|Encoding starten| ENC[ENCODING]\n\n    ENC -->|inkl. Post-Skripte| FIN([FINISHED])\n    ENC -->|Fehler| ERR\n\n    ERR([ERROR]) -->|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
"},{"location":"getting-started/quickstart/#schritt-1-ripster-starten","title":"Schritt 1 \u2013 Ripster starten","text":"
cd ripster\n./start.sh\n

\u00d6ffne http://localhost:5173 im Browser. Das Dashboard zeigt IDLE.

"},{"location":"getting-started/quickstart/#schritt-2-disc-einlegen-disc_detected","title":"Schritt 2 \u2013 Disc einlegen \u2192 DISC_DETECTED","text":"

Lege eine DVD oder Blu-ray ein. Der diskDetectionService pollt das Laufwerk alle disc_poll_interval_ms Millisekunden (Standard: 4 Sekunden).

Was passiert im Code:

  • diskDetectionService emittiert discInserted mit Ger\u00e4teinformationen
  • pipelineService.onDiscInserted() wird aufgerufen
  • Dashboard-Status-Badge zeigt \"Medium erkannt\"
  • Status-Text zeigt \"Neue Disk erkannt\"
  • Der \"Analyse starten\"-Button wird aktiv

Manuelle Ausl\u00f6sung

Falls die automatische Erkennung nicht greift:

curl -X POST http://localhost:3001/api/pipeline/analyze\n

"},{"location":"getting-started/quickstart/#schritt-3-analyse-starten-metadata_selection","title":"Schritt 3 \u2013 Analyse starten \u2192 METADATA_SELECTION","text":"

Klicke auf \"Analyse starten\".

Was passiert im Code:

  1. Ein neuer Job-Datensatz wird in der Datenbank angelegt (status: METADATA_SELECTION)
  2. Ripster versucht, den Titel automatisch aus dem Disc-Label/Modell zu ermitteln
  3. Mit diesem erkannten Titel wird sofort eine OMDb-Suche ausgel\u00f6st
  4. Der MetadataSelectionDialog \u00f6ffnet sich im Frontend mit den vorgeladenen Suchergebnissen

Erkannter Titel: Der Disc-Label (z. B. INCEPTION) wird als Suchbegriff verwendet. Falls kein Label vorhanden, bleibt das Suchfeld leer.

"},{"location":"getting-started/quickstart/#schritt-4-metadaten-auswahlen-metadataselectiondialog","title":"Schritt 4 \u2013 Metadaten ausw\u00e4hlen (MetadataSelectionDialog)","text":"

Der Dialog zeigt vorgeladene OMDb-Suchergebnisse. Du kannst:

"},{"location":"getting-started/quickstart/#4a-omdb-suchergebnis-wahlen","title":"4a) OMDb-Suchergebnis w\u00e4hlen","text":"
\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
  • Suche durch Titel anpassen und Enter dr\u00fccken
  • Typ-Filter: movie / series umschalten m\u00f6glich
  • Einen Eintrag anklicken, dann \"Auswahl \u00fcbernehmen\"
"},{"location":"getting-started/quickstart/#4b-manuelle-eingabe-ohne-omdb","title":"4b) Manuelle Eingabe (ohne OMDb)","text":"

Falls kein passendes Ergebnis gefunden wird: - Titel, Jahr und IMDb-ID manuell eingeben - OMDb-Poster wird \u00fcbersprungen

Was passiert nach Best\u00e4tigung:

Ripster ruft pipelineService.selectMetadata() auf und startet den n\u00e4chsten Schritt automatisch:

  • Job wird auf READY_TO_START gesetzt (kurzer \u00dcbergangszustand)
  • Falls bereits RAW vorhanden: direkter Sprung zu MEDIAINFO_CHECK
  • Falls kein RAW vorhanden: automatischer Start von RIPPING
  • Wenn bereits andere Jobs laufen, landet der Start stattdessen in der Queue
"},{"location":"getting-started/quickstart/#schritt-5-optional-playlist-auswahl-waiting_for_user_decision","title":"Schritt 5 \u2013 Optional: Playlist-Auswahl \u2192 WAITING_FOR_USER_DECISION","text":"

Dieser Zustand erscheint nur bei mehrdeutigen Blu-ray-Playlists (typisch nach RAW-Analyse im Backup-Modus).

Der Playlist-Auswahl-Dialog erscheint zus\u00e4tzlich (nach dem Metadaten-Dialog):

\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
  • Die empfohlene Playlist ist vorausgew\u00e4hlt (Checkbox)
  • Score und Bewertungslabel helfen bei der Entscheidung
  • Nach \"Playlist \u00fcbernehmen\" setzt Ripster automatisch fort:
  • mit vorhandenem RAW in MEDIAINFO_CHECK
  • ohne RAW \u00fcber READY_TO_START weiter Richtung RIPPING

Scoring-Details

Wie die Scores berechnet werden, erkl\u00e4rt die Playlist-Analyse-Seite.

"},{"location":"getting-started/quickstart/#schritt-6-ripping-ripping","title":"Schritt 6 \u2013 Ripping \u2192 RIPPING","text":"

Vorher pr\u00fcft Ripster: Existiert bereits eine Raw-Datei f\u00fcr diesen Job?

  • Ja, Raw-Datei vorhanden \u2192 Direkt zu Schritt 7 (Track-Review), kein erneutes Ripping
  • Nein \u2192 MakeMKV-Ripping startet

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).

Was MakeMKV ausf\u00fchrt (MKV-Modus):

makemkvcon mkv disc:0 all /mnt/raw/Inception-2010/ \\\n  --minlength=900 -r\n

Was MakeMKV ausf\u00fchrt (Backup-Modus):

makemkvcon backup disc:0 /mnt/raw/Inception-2010-backup/ \\\n  --decrypt -r\n

Live-Fortschritt wird aus der MakeMKV-Ausgabe geparst:

PRGV:2048,0,65536  \u2192 Fortschritt wird berechnet und per WebSocket gesendet\nPRGT:5011,0,\"Sichern...\"  \u2192 Aktueller Task-Name\n

Typische Dauer: - DVD: 20\u201345 Minuten - Blu-ray: 45\u2013120 Minuten

"},{"location":"getting-started/quickstart/#schritt-7-track-review-ready_to_encode","title":"Schritt 7 \u2013 Track-Review \u2192 READY_TO_ENCODE","text":"

Nach dem Ripping, nach Playlist-\u00dcbernahme oder direkt bei vorhandenem RAW startet der HandBrake-Scan:

HandBrakeCLI --scan -i <quelle> -t 0\n

Dieser Scan liest alle Tracks aus ohne zu encodieren. Ripster baut daraus den Encode-Plan mit automatischer Vorauswahl:

Status: MEDIAINFO_CHECK \u2013 l\u00e4uft automatisch, kein Benutzereingriff

Danach \u00f6ffnet sich das Encode-Review-Panel (READY_TO_ENCODE):

\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
"},{"location":"getting-started/quickstart/#audio-track-aktionen-verstehen","title":"Audio-Track-Aktionen verstehen","text":"Symbol/Text Bedeutung Copy (ac3) Track wird verlustfrei direkt \u00fcbernommen Copy (truehd) TrueHD-Track wird direkt \u00fcbernommen Transcode (av_aac) Track wird zu AAC umgewandelt Fallback Transcode (av_aac) Copy nicht m\u00f6glich \u2192 automatisch zu AAC Preset-Default (HandBrake) HandBrake-Preset entscheidet Nicht \u00fcbernommen 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":"

Die Tracks mit \u2611 wurden nach der Regel aus den Einstellungen automatisch vorausgew\u00e4hlt (selectedByRule: true). Die Auswahl kann frei ge\u00e4ndert werden.

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.

"},{"location":"getting-started/quickstart/#schritt-8-encoding-encoding","title":"Schritt 8 \u2013 Encoding \u2192 ENCODING","text":"

HandBrake startet mit dem finalisierten Plan:

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

Live-Fortschritt wird aus HandBrake-stderr geparst:

Encoding: task 1 of 1, 73.50 % (45.23 fps, avg 44.12 fps, ETA 00h12m34s)\n

Das Dashboard zeigt: - Fortschrittsbalken (0\u2013100 %) - Aktuelle Encoding-Geschwindigkeit (FPS) - Gesch\u00e4tzte Restzeit (ETA)

Typische Dauer (abh\u00e4ngig von CPU/GPU und Preset): - Schnelles Preset (fast): 0.5\u00d7 Echtzeit - Standard-Preset: 1\u20133\u00d7 Echtzeit - Langsames Preset (slow): 5\u201310\u00d7 Echtzeit

"},{"location":"getting-started/quickstart/#schritt-9-fertig-finished","title":"Schritt 9 \u2013 Fertig! \u2192 FINISHED","text":"
/mnt/nas/movies/\n\u2514\u2500\u2500 Inception (2010).mkv   \u2713 Encodierung abgeschlossen\n
  • Job-Status in der Datenbank: FINISHED
  • PushOver-Benachrichtigung (falls konfiguriert)
  • Eintrag in der History mit vollst\u00e4ndigen Logs
"},{"location":"getting-started/quickstart/#fehlerbehandlung","title":"Fehlerbehandlung","text":""},{"location":"getting-started/quickstart/#job-im-status-error","title":"Job im Status ERROR","text":"
  1. Dashboard: Details-Button \u2192 Log-Ausgabe pr\u00fcfen
  2. Retry: Job vom Fehlerzustand neu starten (beh\u00e4lt Metadaten)
  3. History: Vollst\u00e4ndige Logs und Fehlerdetails
"},{"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 HandBrakeCLI --preset-list pr\u00fcfen Keine Disc erkannt Laufwerk-Berechtigungen sudo chmod a+rw /dev/sr0 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 IDLE Disc einlegen Disc-Polling erkennt Disc 2 DISC_DETECTED \"Analyse starten\" klicken Job anlegen, OMDb vorsuchen 3 METADATA_SELECTION Film im Dialog ausw\u00e4hlen Start automatisch einplanen/ausl\u00f6sen 4 READY_TO_START meist keine \u00dcbergangszustand vor Auto-Start 5 RIPPING Warten MakeMKV rippt, Fortschritt streamen 6 MEDIAINFO_CHECK Warten HandBrake-Scan, Encode-Plan bauen 7 WAITING_FOR_USER_DECISION (optional) Playlist manuell w\u00e4hlen Auf Best\u00e4tigung warten 8 READY_TO_ENCODE Tracks pr\u00fcfen + \"Encoding starten\" Auswahl \u00fcbernehmen, Start ausl\u00f6sen 9 ENCODING Warten HandBrake encodiert, inkl. Post-Skripte 10 FINISHED \u2014 Datei fertig, Benachrichtigung senden"},{"location":"pipeline/","title":"Pipeline","text":"

Der Pipeline-Abschnitt beschreibt den Kern-Workflow von Ripster.

  • Workflow & Zust\u00e4nde

    Der vollst\u00e4ndige Ripping-Workflow mit allen Zustands\u00fcberg\u00e4ngen.

    Workflow

  • Encode-Planung

    Wie Ripster Audio- und Untertitel-Tracks analysiert und Encode-Pl\u00e4ne erstellt.

    Encoding

  • Playlist-Analyse

    Erkennung von Blu-ray Playlist-Obfuskierung und Auswahl der korrekten Playlist.

    Playlist-Analyse

  • Post-Encode-Skripte

    Automatische Ausf\u00fchrung von Shell-Skripten nach erfolgreichem Encoding \u2013 z. B. zum Verschieben oder Benachrichtigen.

    Post-Encode-Skripte

"},{"location":"pipeline/encoding/","title":"Encode-Planung & Track-Auswahl","text":"

encodePlan.js 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.

"},{"location":"pipeline/encoding/#ablauf-im-pipeline-kontext","title":"Ablauf im Pipeline-Kontext","text":"
RIPPING abgeschlossen (oder Pre-Rip-Scan)\n          \u2193\nHandBrake --scan (alle Titel & 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
"},{"location":"pipeline/encoding/#phase-1-pre-rip-track-scan","title":"Phase 1: Pre-Rip Track-Scan","text":"

Ripster f\u00fchrt einen HandBrake-Scan bereits vor dem eigentlichen Ripping durch:

HandBrakeCLI --scan -i /dev/sr0 -t 0\n

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.

Pre-Rip vs. Post-Rip

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 .mkv-Datei gescannt.

"},{"location":"pipeline/encoding/#phase-2-track-selektor-regeln-buildtrackselectors","title":"Phase 2: Track-Selektor-Regeln (buildTrackSelectors)","text":"

Die Regeln werden aus den HandBrake-Einstellungen abgeleitet. Es gibt f\u00fcnf Selektionsmodi:

Modus Beschreibung none Keine Tracks dieser Art \u00fcbernehmen first Nur den ersten Track \u00fcbernehmen all Alle Tracks \u00fcbernehmen language Nur Tracks in bestimmten Sprachen explicit Bestimmte Track-IDs explizit angeben

Der aktive Modus wird aus den handbrake_*-Einstellungen und handbrake_extra_args abgeleitet. Explizite CLI-Argumente (--audio, --audio-lang-list) \u00fcberschreiben die Basis-Konfiguration.

"},{"location":"pipeline/encoding/#phase-3-automatische-vorauswahl-selecttrackids","title":"Phase 3: Automatische Vorauswahl (selectTrackIds)","text":""},{"location":"pipeline/encoding/#audio-tracks","title":"Audio-Tracks","text":"
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

Jeder Audio-Track erh\u00e4lt das Feld selectedByRule: true/false \u2013 dieses zeigt dem Benutzer, welche Tracks automatisch vorausgew\u00e4hlt wurden.

Sprach-Normalisierung (normalizeLanguage):

Alle Sprachcodes werden auf ISO 639-2 (3-Buchstaben) normalisiert:

Eingabe Normalisiert de, ger deu German deu en, eng eng English eng fr, fre fra ja, jpn jpn Unbekannt und"},{"location":"pipeline/encoding/#untertitel-tracks","title":"Untertitel-Tracks","text":"

Gleiche Modus-Logik wie Audio, aber mit zus\u00e4tzlichen Flags pro Track:

Flag Bedeutung burnIn Untertitel in Video einbrennen (--subtitle-burned) forced Nur erzwungene Untertitel \u00fcbernehmen (--subtitle-forced) defaultTrack Als Standard-Untertitelspur markieren (--subtitle-default)

Diese Flags werden im Encode-Review als Checkboxen angezeigt.

"},{"location":"pipeline/encoding/#phase-4-encoder-aktion-bestimmen-resolveaudioencoderaction","title":"Phase 4: Encoder-Aktion bestimmen (resolveAudioEncoderAction)","text":"

F\u00fcr jeden vorausgew\u00e4hlten Audio-Track bestimmt Ripster die Encoder-Aktion:

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

Encoder-Aktionstypen:

Typ Label (UI) Qualit\u00e4t preset-default Preset-Default (HandBrake) HandBrake entscheidet copy Copy (ac3) Verlustfrei fallback Fallback Transcode (av_aac) Mit Qualit\u00e4tsverlust transcode Transcode (av_aac) Mit Qualit\u00e4tsverlust

Copy-kompatible Codecs (Standard Copy-Mask):

Codec Encoder-String AC-3 copy:ac3 E-AC-3 copy:eac3 AAC copy:aac MP3 copy:mp3 TrueHD copy:truehd DTS copy:dts (nur mit spez. HandBrake-Build) DTS-HD copy:dtshd (nur mit spez. HandBrake-Build)

DTS im Standard-HandBrake

Standard-HandBrake-Builds unterst\u00fctzen kein DTS-Passthrough. DTS-Tracks werden dann automatisch auf den Fallback-Encoder umgestellt (Standard: av_aac).

"},{"location":"pipeline/encoding/#phase-5-encode-plan-struktur","title":"Phase 5: Encode-Plan-Struktur","text":"

Der vollst\u00e4ndige Plan wird im Job-Datensatz als encode_plan_json gespeichert:

{\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
"},{"location":"pipeline/encoding/#phase-6-benutzer-review-im-frontend-mediainforeviewpanel","title":"Phase 6: Benutzer-Review im Frontend (MediaInfoReviewPanel)","text":"

Das Review-Panel zeigt:

\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

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)

"},{"location":"pipeline/encoding/#phase-7-benutzer-auswahl-anwenden-applymanualtrackselectiontoplan","title":"Phase 7: Benutzer-Auswahl anwenden (applyManualTrackSelectionToPlan)","text":"

Im Frontend wird die Benutzer-Auswahl beim Klick auf \"Encoding starten\" (ggf. automatisch) best\u00e4tigt und dann auf den Plan angewendet:

Payload: {\n  \"selectedEncodeTitleId\": 1,\n  \"selectedTrackSelection\": {\n    \"1\": {\n      \"audioTrackIds\": [1, 2],\n      \"subtitleTrackIds\": [1]\n    }\n  }\n}\n

Jeder Track erh\u00e4lt selectedForEncode: true/false entsprechend der Auswahl. Die Encoder-Aktionen (encodeActions) der nicht gew\u00e4hlten Tracks werden geleert.

"},{"location":"pipeline/encoding/#phase-8-handbrake-cli-befehl","title":"Phase 8: HandBrake-CLI-Befehl","text":"

Aus dem finalisierten Plan baut Ripster den HandBrake-Aufruf:

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
Argument Quelle -i encode_input_path aus Job -o Ausgabepfad aus filename_template + movie_dir -t Gew\u00e4hlter Titel-Index -a Kommagetrennte Audio-Track-IDs der ausgew\u00e4hlten Tracks -E Kommagetrennte Encoder-Aktionen (eine pro Track, gleiche Reihenfolge wie -a) -s Kommagetrennte Untertitel-Track-IDs --subtitle-default Track-ID der als Default markierten Untertitelspur --preset handbrake_preset-Einstellung Extras handbrake_extra_args-Einstellung"},{"location":"pipeline/encoding/#dateiname-template","title":"Dateiname-Template","text":"Platzhalter Wert Beispiel {title} Filmtitel von OMDb Inception {year} Erscheinungsjahr 2010 {imdb_id} IMDb-ID tt1375666 {type} movie oder series movie

Sonderzeichen (:, /, ?, * etc.) werden automatisch aus dem Dateinamen entfernt.

"},{"location":"pipeline/encoding/#re-encoding","title":"Re-Encoding","text":"

Ein abgeschlossener Job kann ohne erneutes Ripping neu encodiert werden:

  1. Job in der History \u00f6ffnen
  2. \"Re-Encode\" klicken
  3. Track-Auswahl anpassen (oder bestehende \u00fcbernehmen)
  4. Encoding startet mit den aktuellen handbrake_*-Einstellungen

N\u00fctzlich bei ge\u00e4nderten Presets, anderen Sprach-Pr\u00e4ferenzen oder nach einem Einstellungs-Update.

"},{"location":"pipeline/playlist-analysis/","title":"Playlist-Analyse","text":"

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 playlistAnalysis.js.

"},{"location":"pipeline/playlist-analysis/#das-problem-playlist-obfuskierung","title":"Das Problem: Playlist-Obfuskierung","text":"

Moderne Blu-rays k\u00f6nnen Dutzende bis Hunderte von Titeln/Playlists enthalten. Der eigentliche Film steckt in genau einer davon \u2013 alle anderen sind:

  • Kurze Dummy-Titel (wenige Sekunden bis Minuten)
  • Titel mit verschachtelten Segmenten (absichtlich versetzte Reihenfolge, sodass der Film falsch gerippt wird)
  • Titel gleicher L\u00e4nge (mehrere Playlists mit identischer Laufzeit, aber unterschiedlicher Segment-Reihenfolge)

Das Ziel der Obfuskierung: Ein einfacher Ripper w\u00e4hlt den erstbesten langen Titel \u2013 und bekommt ein zerst\u00fcckeltes, unbrauchbares Video.

"},{"location":"pipeline/playlist-analysis/#wann-wird-die-analyse-ausgelost","title":"Wann wird die Analyse ausgel\u00f6st?","text":"

Die Playlist-Analyse wird automatisch gestartet sobald der Benutzer Metadaten best\u00e4tigt (nach dem Metadaten-Dialog). Ripster ruft makemkvcon im Info-Modus auf und parst die TINFO-Ausgabe.

TINFO:<titleId>,26,\"<segment-list>\"\n

Feld 26 enth\u00e4lt die kommagetrennte Liste der Segment-Nummern in der Abspielreihenfolge des Titels.

"},{"location":"pipeline/playlist-analysis/#algorithmus-im-detail-playlistanalysisjs","title":"Algorithmus im Detail (playlistAnalysis.js)","text":""},{"location":"pipeline/playlist-analysis/#schritt-1-segment-nummern-parsen","title":"Schritt 1 \u2013 Segment-Nummern parsen","text":"
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
"},{"location":"pipeline/playlist-analysis/#schritt-2-metriken-berechnen-computesegmentmetrics","title":"Schritt 2 \u2013 Metriken berechnen (computeSegmentMetrics)","text":"

F\u00fcr jedes aufeinanderfolgende Segment-Paar [a, b] wird diff = b \u2212 a berechnet:

Metrik Bedingung Bedeutung directSequenceSteps diff == 1 Aufeinanderfolgende Segmente \u2192 linearer Film backwardJumps b < a R\u00fcckw\u00e4rtsspr\u00fcnge \u2192 verd\u00e4chtig largeJumps \\|diff\\| > 20 Gro\u00dfe Spr\u00fcnge \u2192 verd\u00e4chtig alternatingPairs Gro\u00dfe Spr\u00fcnge mit wechselndem Vorzeichen Hin-und-her-Muster \u2192 starker Fake-Indikator

Score-Formel:

score = (directSequenceSteps \u00d7 2) \u2212 (backwardJumps \u00d7 3) \u2212 (largeJumps \u00d7 2)\n

Konkrete Beispiele:

Segmentfolge directSeq backward large score Ergebnis 0,1,2,3,4,5 5 0 0 +10 Echter Film 0,1,100,2,101,3 2 0 4 -4 Verd\u00e4chtig 50,10,60,11,70,12 0 3 3 -15 Fake"},{"location":"pipeline/playlist-analysis/#schritt-3-bewertungslabel-vergeben-buildevaluationlabel","title":"Schritt 3 \u2013 Bewertungslabel vergeben (buildEvaluationLabel)","text":"
alternatingRatio = alternatingPairs / largeJumps\n\nif alternatingRatio >= 0.55 AND alternatingPairs >= 3:\n  \u2192 \"Fake-Struktur (alternierendes Sprungmuster)\"\n\nelse if backwardJumps > 0 OR largeJumps > 0:\n  \u2192 \"Auff\u00e4llige Segmentreihenfolge\"\n\nelse:\n  \u2192 \"wahrscheinlich korrekt (lineare Segmentfolge)\"\n
"},{"location":"pipeline/playlist-analysis/#schritt-4-duplikat-gruppen-bilden-buildsimilaritygroups","title":"Schritt 4 \u2013 Duplikat-Gruppen bilden (buildSimilarityGroups)","text":"

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:

8 Titel mit ~148 Minuten Laufzeit \u2192 Duplikat-Gruppe\n\u2192 obfuscationDetected = true\n
"},{"location":"pipeline/playlist-analysis/#schritt-5-besten-kandidaten-empfehlen-scorecandidates","title":"Schritt 5 \u2013 Besten Kandidaten empfehlen (scoreCandidates)","text":"

Innerhalb der gr\u00f6\u00dften Duplikat-Gruppe werden alle Kandidaten sortiert nach:

  1. score (h\u00f6her = besser)
  2. sequenceCoherence (Anteil linearer Segmentschritte)
  3. Laufzeit (l\u00e4nger = besser)
  4. Dateigr\u00f6\u00dfe (gr\u00f6\u00dfer = besser als Tiebreaker)

Der erste Kandidat der sortierten Liste ist die Empfehlung.

"},{"location":"pipeline/playlist-analysis/#schritt-6-entscheidung-erzwingen-bei-mehreren-kandidaten","title":"Schritt 6 \u2013 Entscheidung erzwingen bei mehreren Kandidaten","text":"

Sobald nach MIN_LENGTH_MINUTES mehr als eine Playlist \u00fcbrig bleibt, wird immer eine manuelle Auswahl verlangt:

candidateCount > 1  \u2192 manualDecisionRequired = true\ncandidateCount <= 1 \u2192 manualDecisionRequired = false\n
"},{"location":"pipeline/playlist-analysis/#wann-greift-der-benutzer-ein","title":"Wann greift der Benutzer ein?","text":"
obfuscationDetected    = duplicateDurationGroups.length > 0\nmanualDecisionRequired = candidates.length > 1\n
Ergebnis N\u00e4chster Pipeline-Zustand Aktion Nur ein Kandidat nach Mindestl\u00e4nge READY_TO_START Automatische \u00dcbernahme m\u00f6glich Mehrere Kandidaten nach Mindestl\u00e4nge WAITING_FOR_USER_DECISION Benutzer muss Playlist ausw\u00e4hlen"},{"location":"pipeline/playlist-analysis/#benutzeroberflache-playlist-auswahl-dialog","title":"Benutzeroberfl\u00e4che: Playlist-Auswahl-Dialog","text":"

Wenn manualDecisionRequired = true, \u00f6ffnet sich der Playlist-Dialog nach dem Metadaten-Dialog:

\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
  • \u2605 markiert die empfohlene Playlist (vorausgew\u00e4hlt)
  • Nur Titel \u2265 makemkv_min_length_minutes erscheinen in der Liste
  • Der Benutzer w\u00e4hlt per Radio-Button und klickt \"Best\u00e4tigen\"
  • Erst nach dieser Best\u00e4tigung wechselt die Pipeline zu READY_TO_START
"},{"location":"pipeline/playlist-analysis/#vollstandige-datenstruktur-analyzecontextplaylistanalysis","title":"Vollst\u00e4ndige Datenstruktur (analyzeContext.playlistAnalysis)","text":"
{\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
"},{"location":"pipeline/playlist-analysis/#konfiguration","title":"Konfiguration","text":"Einstellung Standard Wirkung makemkv_min_length_minutes 15 Titel k\u00fcrzer als dieser Wert werden als Kandidaten ignoriert"},{"location":"pipeline/playlist-analysis/#tipps-bei-fehlempfehlung","title":"Tipps bei Fehlempfehlung","text":"

Falsche Playlist gew\u00e4hlt?

Wenn das resultierende Video zerst\u00fcckelt ist:

  1. Job in der History \u00f6ffnen
  2. Re-Encode starten \u2013 diesmal eine andere Playlist w\u00e4hlen
  3. Alternativ: Korrekte Playlist im MakeMKV-Forum recherchieren

Keine Segment-Daten verf\u00fcgbar

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.

"},{"location":"pipeline/post-encode-scripts/","title":"Post-Encode-Skripte","text":"

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.

"},{"location":"pipeline/post-encode-scripts/#funktionsweise","title":"Funktionsweise","text":"

Nach einem erfolgreich abgeschlossenen Encoding-Schritt f\u00fchrt Ripster die konfigurierten Skripte sequenziell in der festgelegten Reihenfolge aus:

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

Abbruch bei Fehler

Schl\u00e4gt ein Skript fehl (Exit-Code \u2260 0), werden alle nachfolgenden Skripte nicht mehr ausgef\u00fchrt. Der Job bleibt im Abschlusszustand FINISHED; der Fehler wird in Log/Status-Text und im postEncodeScripts-Summary festgehalten.

"},{"location":"pipeline/post-encode-scripts/#skript-verwaltung","title":"Skript-Verwaltung","text":"

Skripte werden \u00fcber die Einstellungen-Seite angelegt und verwaltet. Sie stehen danach in jedem Encode-Review zur Auswahl.

"},{"location":"pipeline/post-encode-scripts/#skript-anlegen","title":"Skript anlegen","text":"

Navigiere zu Einstellungen \u2192 Skripte und klicke \"Neues Skript\":

Feld Beschreibung Name Anzeigename des Skripts (z. B. Zu Plex verschieben) Befehl Shell-Befehl oder Skriptpfad (z. B. /home/michael/scripts/move-to-plex.sh) Beschreibung Optionale Erkl\u00e4rung"},{"location":"pipeline/post-encode-scripts/#verfugbare-umgebungsvariablen","title":"Verf\u00fcgbare Umgebungsvariablen","text":"

Jedes Skript wird mit folgenden Umgebungsvariablen aufgerufen:

Variable Inhalt Beispiel RIPSTER_OUTPUT_PATH Absoluter Pfad der encodierten Datei /mnt/movies/Inception (2010).mkv RIPSTER_JOB_ID Job-ID in der Datenbank 42 RIPSTER_TITLE Filmtitel Inception RIPSTER_YEAR Erscheinungsjahr 2010 RIPSTER_IMDB_ID IMDb-ID tt1375666 RIPSTER_RAW_PATH Pfad zur Raw-MKV-Datei /mnt/raw/Inception-2010/t00.mkv"},{"location":"pipeline/post-encode-scripts/#beispiel-skript-datei-nach-jellyfin-verschieben","title":"Beispiel-Skript: Datei nach Jellyfin verschieben","text":"
#!/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
"},{"location":"pipeline/post-encode-scripts/#beispiel-skript-webhook-auslosen","title":"Beispiel-Skript: Webhook ausl\u00f6sen","text":"
#!/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
"},{"location":"pipeline/post-encode-scripts/#skript-im-encode-review-auswahlen","title":"Skript im Encode-Review ausw\u00e4hlen","text":"

Im READY_TO_ENCODE-Zustand zeigt das MediaInfoReviewPanel einen Skript-Abschnitt:

\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 & 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
  • Reihenfolge per Drag & Drop \u00e4ndern
  • Hinzuf\u00fcgen aus der Dropdown-Liste aller konfigurierten Skripte
  • Entfernen einzelner Skripte aus der aktuellen Auswahl
  • Skripte k\u00f6nnen pro Job unterschiedlich gew\u00e4hlt werden
"},{"location":"pipeline/post-encode-scripts/#skript-testen","title":"Skript testen","text":"

\u00dcber die Einstellungen kann jedes Skript mit einem Test-Job ausgef\u00fchrt werden:

POST /api/settings/scripts/:scriptId/test\n

Der Test-Aufruf bef\u00fcllt die Umgebungsvariablen mit Platzhalter-Werten.

"},{"location":"pipeline/post-encode-scripts/#ausfuhrungs-ergebnis","title":"Ausf\u00fchrungs-Ergebnis","text":"

Das Ergebnis der Skript-Ausf\u00fchrung wird im Job-Datensatz gespeichert und in der History angezeigt:

{\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
Feld Beschreibung configured Anzahl ausgew\u00e4hlter Skripte attempted Anzahl tats\u00e4chlich gestarteter Skripte succeeded Erfolgreich ausgef\u00fchrt (Exit-Code 0) failed Fehlgeschlagen skipped Nicht ausgef\u00fchrt (wegen vorherigem Fehler) aborted true, wenn die Kette abgebrochen wurde"},{"location":"pipeline/post-encode-scripts/#api-referenz","title":"API-Referenz","text":"

Eine vollst\u00e4ndige API-Dokumentation der Skript-Endpunkte findest du unter:

Settings API \u2013 Skripte

"},{"location":"pipeline/workflow/","title":"Workflow & Zust\u00e4nde","text":"

Der Ripping-Workflow von Ripster ist als State Machine implementiert. Jeder Zustand hat klar definierte \u00dcbergangsbedingungen und Aktionen.

"},{"location":"pipeline/workflow/#zustandsdiagramm","title":"Zustandsdiagramm","text":"
flowchart LR\n    START(( )) --> IDLE\n\n    IDLE -->|Disc erkannt| DD[DISC_DETECTED]\n    DD -->|Analyse starten| META[METADATA\\nSELECTION]\n\n    META -->|Metadaten \u00fcbernommen| RTS[READY_TO\\nSTART]\n    META -->|vorhandenes RAW +\\nPlaylist offen| WUD[WAITING_FOR\\nUSER_DECISION]\n\n    RTS -->|Auto-Start| RIP[RIPPING]\n    RTS -->|Auto-Start mit RAW| MIC[MEDIAINFO\\nCHECK]\n    RIP -->|MKV fertig| MIC\n    RIP -->|Fehler| ERR\n    RIP -->|Abbruch| CAN([CANCELLED])\n\n    MIC -->|Playlist offen (Backup)| WUD\n    WUD -->|Playlist best\u00e4tigt| MIC\n    WUD -->|Playlist best\u00e4tigt,\\nnoch kein RAW| RTS\n    MIC --> RTE[READY_TO\\nENCODE]\n    RTE -->|Encoding starten\\n(best\u00e4tigt bei Bedarf automatisch)| ENC[ENCODING]\n\n    ENC -->|inkl. Post-Skripte| FIN([FINISHED])\n    ENC -->|Fehler| ERR\n    ENC -->|Abbruch| CAN\n\n    ERR([ERROR]) -->|Retry / Cancel| IDLE\n    CAN -->|Retry / Neu-Analyse| IDLE\n    FIN -->|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
"},{"location":"pipeline/workflow/#ui-badge-bezeichnungen","title":"UI-Badge-Bezeichnungen","text":"

Die Status-Badges im Dashboard verwenden diese Labels:

State Badge-Label IDLE Bereit DISC_DETECTED Medium erkannt METADATA_SELECTION Metadatenauswahl WAITING_FOR_USER_DECISION Warte auf Auswahl READY_TO_START Startbereit RIPPING Rippen MEDIAINFO_CHECK Mediainfo-Pruefung READY_TO_ENCODE Bereit zum Encodieren ENCODING Encodieren FINISHED Fertig CANCELLED Abgebrochen ERROR Fehler Queue (kein eigener State) In der Queue"},{"location":"pipeline/workflow/#zustandsbeschreibungen","title":"Zustandsbeschreibungen","text":""},{"location":"pipeline/workflow/#idle","title":"IDLE","text":"

Ausgangszustand. Ripster wartet auf eine Disc.

  • diskDetectionService pollt das Laufwerk im konfigurierten Intervall
  • Bei Disc-Erkennung: automatischer \u00dcbergang zu DISC_DETECTED
  • WebSocket-Event: DISC_DETECTED
"},{"location":"pipeline/workflow/#disc_detected","title":"DISC_DETECTED","text":"

Disc erkannt, wartet auf Benutzeraktion.

  • Dashboard-Badge: \"Medium erkannt\"
  • Status-Text: \"Neue Disk erkannt\"
  • \"Analyse starten\"-Button wird aktiv
  • Kein Prozess l\u00e4uft noch

\u00dcbergang: Benutzer klickt \"Analyse starten\" \u2192 METADATA_SELECTION

"},{"location":"pipeline/workflow/#metadata_selection","title":"METADATA_SELECTION","text":"

Metadaten-Auswahl l\u00e4uft.

  1. Job wird erstellt (status = METADATA_SELECTION)
  2. OMDb-Vorsuche mit erkanntem Disc-Label
  3. MetadataSelectionDialog \u00f6ffnet sich mit vorgeladenen Ergebnissen
  4. Benutzer w\u00e4hlt Filmtitel (oder gibt manuell ein)
  5. Nach Best\u00e4tigung wird der Job automatisch f\u00fcr Start/Queue vorbereitet (selectMetadata + startPreparedJob)

\u00dcbergang (automatisch nach Metadaten-Best\u00e4tigung):

Ergebnis N\u00e4chster Zustand Kein verwertbares RAW vorhanden READY_TO_START \u2192 automatisch RIPPING (oder Queue) Verwertbares RAW vorhanden READY_TO_START \u2192 automatisch MEDIAINFO_CHECK (oder Queue) Vorhandenes RAW + offene Playlist-Entscheidung WAITING_FOR_USER_DECISION"},{"location":"pipeline/workflow/#waiting_for_user_decision","title":"WAITING_FOR_USER_DECISION","text":"

Playlist-Obfuskierung erkannt \u2013 manuelle Auswahl erforderlich.

Neu seit \u201eSkript Integration + UI Anpassungen\"

Dieser Zustand wurde eingef\u00fchrt, um Blu-rays mit mehreren Playlists \u00e4hnlicher L\u00e4nge korrekt zu behandeln.

  • Playlist-Auswahl-Dialog wird im Dashboard angezeigt
  • Alle Kandidaten mit Score, Laufzeit und Bewertungslabel
  • Empfohlene Playlist ist vorausgew\u00e4hlt
  • Benutzer best\u00e4tigt mit \"Playlist \u00fcbernehmen\"
  • Tritt h\u00e4ufig nach MEDIAINFO_CHECK auf (Backup-Analyse), seltener direkt nach METADATA_SELECTION bei vorhandenem RAW

Darstellung im Dashboard:

\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

\u00dcbergang: selectMetadata(jobId, { selectedPlaylist }) setzt die Pipeline automatisch fort:

  • mit vorhandenem RAW nach MEDIAINFO_CHECK
  • ohne RAW \u00fcber READY_TO_START weiter Richtung RIPPING

Mehr Details: Playlist-Analyse

"},{"location":"pipeline/workflow/#ready_to_start","title":"READY_TO_START","text":"

\u00dcbergangs-/Fallback-Zustand vor dem eigentlichen Start.

  • Wird nach Metadaten-Best\u00e4tigung kurz gesetzt
  • startPreparedJob() wird danach automatisch ausgef\u00fchrt
  • Wenn Parallel-Limit erreicht ist, wird der Start stattdessen in die Queue eingereiht
  • \"Job starten\" ist prim\u00e4r f\u00fcr Sonderf\u00e4lle/Fallback sichtbar

Sonderfall \u2013 RAW-Datei bereits vorhanden: Wenn f\u00fcr diesen Job bereits ein verwertbares RAW unter raw_dir existiert, wird Ripping \u00fcbersprungen und direkt MEDIAINFO_CHECK gestartet.

\u00dcbergang: startPreparedJob(jobId) \u2192 RIPPING oder direkt MEDIAINFO_CHECK

"},{"location":"pipeline/workflow/#ripping","title":"RIPPING","text":"

MakeMKV rippt die Disc.

MKV-Modus (Standard)Backup-Modus
makemkvcon mkv disc:0 all /path/to/raw/ --minlength=900 -r\n

Erstellt MKV-Datei(en) direkt aus den gew\u00e4hlten Titeln.

makemkvcon backup disc:0 /path/to/raw/backup/ --decrypt -r\n

Erstellt vollst\u00e4ndiges Disc-Backup inkl. Men\u00fcs.

Live-Updates aus MakeMKV-Ausgabe:

PRGV:2048,0,65536  \u2192 Fortschritt-Berechnung\nPRGT:5011,0,\"...\"  \u2192 Aktueller Task-Name\n

Typische Dauer: DVD 20\u201345 min \u00b7 Blu-ray 45\u2013120 min

"},{"location":"pipeline/workflow/#mediainfo_check","title":"MEDIAINFO_CHECK","text":"

HandBrake-Scan und Encode-Plan-Erstellung.

Dieser Zustand umfasst je nach Quelle mehrere Phasen:

  1. Optional: Playlist-Aufl\u00f6sung bei Blu-ray-Backup (inkl. MakeMKV/HandBrake-Zuordnung)
  2. HandBrake-Scan (HandBrakeCLI --scan) auf RAW-Input
  3. Encode-Plan-Erstellung mit automatischer Track-Vorauswahl

Kein Benutzereingriff \u2013 l\u00e4uft automatisch durch.

\u00dcberg\u00e4nge:

  • Eindeutige Quelle/Titelwahl m\u00f6glich \u2192 READY_TO_ENCODE
  • Mehrdeutige Playlist erkannt \u2192 WAITING_FOR_USER_DECISION
"},{"location":"pipeline/workflow/#ready_to_encode","title":"READY_TO_ENCODE","text":"

Encode-Plan bereit.

Das MediaInfoReviewPanel zeigt:

  • Titel-Auswahl (bei Discs mit mehreren langen Titeln)
  • Audio-Tracks mit Encoder-Vorschau (Copy/Transcode/Fallback)
  • Untertitel-Tracks mit Flags (Einbrennen, Forced, Default)
  • Post-Encode-Skripte \u2013 Auswahl und Reihenfolge der auszuf\u00fchrenden Skripte

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 confirmEncodeReview(...) automatisch vor dem Start aufgerufen.

\u00dcbergang: startPreparedJob(jobId) \u2192 ENCODING (oder im Pre-Rip-Fall zuerst RIPPING)

"},{"location":"pipeline/workflow/#encoding","title":"ENCODING","text":"

HandBrake encodiert die Datei.

HandBrakeCLI \\\n  -i <quelle> -o <ziel> \\\n  -t <titelId> \\\n  --preset \"H.265 MKV 1080p30\" \\\n  -a 1,2 -E copy:ac3,av_aac \\\n  -s 1 --subtitle-default 1\n

Live-Updates aus HandBrake-stderr:

Encoding: task 1 of 1, 73.50 % (45.23 fps, avg 44.12 fps, ETA 00h12m34s)\n

Post-Encode-Skripte werden innerhalb dieses Zustands sequenziell ausgef\u00fchrt (kein separater Pipeline-State).

Skriptfehler

Skriptfehler f\u00fchren zum Abbruch der Skriptkette, der Job bleibt jedoch im Abschlusszustand FINISHED mit entsprechendem Hinweis im Status-Text/Log.

"},{"location":"pipeline/workflow/#finished","title":"FINISHED","text":"

Job erfolgreich abgeschlossen.

  • Ausgabedatei liegt im konfigurierten movie_dir
  • Job-Status in Datenbank: FINISHED
  • PushOver-Benachrichtigung (falls konfiguriert)
  • WebSocket-Event: PIPELINE_STATE_CHANGED (State FINISHED)
"},{"location":"pipeline/workflow/#cancelled","title":"CANCELLED","text":"

Job wurde vom Benutzer abgebrochen.

  • Entsteht bei aktivem Abbruch (/api/pipeline/cancel) w\u00e4hrend laufender Phase
  • Job-Status in Datenbank: CANCELLED
  • Im Dashboard stehen danach u. a. Retry Rippen, Review neu starten oder Encode neu starten (kontextabh\u00e4ngig) zur Verf\u00fcgung
"},{"location":"pipeline/workflow/#error","title":"ERROR","text":"

Fehler aufgetreten.

  • Fehlerdetails im Job-Datensatz gespeichert
  • Fehler-Logs in History abrufbar
  • Retry: Neustart vom Fehlerzustand
  • Neu analysieren: Disc erneut als neuer Job starten
"},{"location":"pipeline/workflow/#abbrechen-retry","title":"Abbrechen & Retry","text":""},{"location":"pipeline/workflow/#pipeline-abbrechen","title":"Pipeline abbrechen","text":"
POST /api/pipeline/cancel\n
  • SIGINT \u2192 graceful exit (Timeout: 10 s) \u2192 SIGKILL
  • Laufender Job landet in CANCELLED (oder Queue-Eintrag wird entfernt, falls noch nicht gestartet)
"},{"location":"pipeline/workflow/#job-wiederholen","title":"Job wiederholen","text":"
POST /api/pipeline/retry/:jobId\n
  • Startet den Job neu in RIPPING (oder reiht den Retry in die Queue ein)
  • Metadaten bleiben erhalten; Encode-/Scan-Daten werden neu erzeugt
"},{"location":"pipeline/workflow/#re-encode","title":"Re-Encode","text":"
POST /api/pipeline/reencode/:jobId\n
  • Encodiert bestehende Raw-MKV neu
  • Erm\u00f6glicht neue Track-Auswahl und andere Skripte
  • Kein Ripping erforderlich
"},{"location":"tools/","title":"Externe Tools","text":"

Ripster ist ein Orchestrator \u2013 die eigentliche Arbeit erledigen diese bew\u00e4hrten Open-Source-Tools:

  • MakeMKV

    Disc-Analyse und Ripping. Erstellt MKV-Dateien oder vollst\u00e4ndige Backups.

    MakeMKV

  • HandBrake

    Video-Encoding mit umfangreichen Preset-Optionen.

    HandBrake

  • MediaInfo

    Analyse von Track-Informationen in Mediendateien.

    MediaInfo

"},{"location":"tools/handbrake/","title":"HandBrake","text":"

HandBrake encodiert die rohen MKV-Dateien in das gew\u00fcnschte Format. Ripster nutzt HandBrakeCLI.

"},{"location":"tools/handbrake/#verwendeter-befehl","title":"Verwendeter Befehl","text":"
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
"},{"location":"tools/handbrake/#presets","title":"Presets","text":"

HandBrake verwendet Presets f\u00fcr vorkonfigurierte Encoding-Einstellungen.

"},{"location":"tools/handbrake/#empfohlene-presets","title":"Empfohlene Presets","text":"Preset Codec Aufl\u00f6sung F\u00fcr H.265 MKV 1080p30 HEVC/H.265 1080p Beste Qualit\u00e4t/Gr\u00f6\u00dfe H.265 MKV 720p30 HEVC/H.265 720p Kleinere Dateien H.264 MKV 1080p30 AVC/H.264 1080p Breiteste Kompatibilit\u00e4t HQ 1080p30 Surround HEVC/H.265 1080p Hohe Qualit\u00e4t mit Surround"},{"location":"tools/handbrake/#alle-presets-anzeigen","title":"Alle Presets anzeigen","text":"
HandBrakeCLI --preset-list\n
"},{"location":"tools/handbrake/#audio-encoding","title":"Audio-Encoding","text":""},{"location":"tools/handbrake/#copy-kompatible-codecs","title":"Copy-kompatible Codecs","text":"

HandBrake kann folgende Codecs direkt kopieren (kein Qualit\u00e4tsverlust):

Codec --aencoder Wert AC-3 copy:ac3 AAC copy:aac MP3 copy:mp3 TrueHD copy:truehd E-AC-3 copy:eac3"},{"location":"tools/handbrake/#transcoding","title":"Transcoding","text":"

Codecs die nicht kopiert werden k\u00f6nnen, werden zu AAC transcodiert:

Original Transcodiert zu DTS AAC (ffaac) DTS-HD AAC (ffaac)"},{"location":"tools/handbrake/#extra-argumente","title":"Extra-Argumente","text":"

\u00dcber die Einstellung handbrake_extra_args k\u00f6nnen beliebige HandBrake-Argumente hinzugef\u00fcgt werden:

# 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
"},{"location":"tools/handbrake/#fortschritts-parsing","title":"Fortschritts-Parsing","text":"

Ripster parst die HandBrake-Ausgabe auf stderr f\u00fcr die Fortschrittsanzeige:

Encoding: task 1 of 1, 73.50 % (45.23 fps, avg 44.12 fps, ETA 00h12m34s)\n

progressParsers.js extrahiert: - Prozentzahl - Aktuelle FPS - ETA

"},{"location":"tools/handbrake/#konfiguration-in-ripster","title":"Konfiguration in Ripster","text":"Einstellung Beschreibung handbrake_command Pfad/Befehl f\u00fcr HandBrakeCLI handbrake_preset Preset-Name handbrake_extra_args Zus\u00e4tzliche CLI-Argumente output_extension Dateiendung der Ausgabe"},{"location":"tools/handbrake/#troubleshooting","title":"Troubleshooting","text":""},{"location":"tools/handbrake/#handbrake-findet-preset-nicht","title":"HandBrake findet Preset nicht","text":"
# Preset-Liste anzeigen\nHandBrakeCLI --preset-list 2>&1 | grep -i \"h.265\"\n

Preset-Namen sind case-sensitive!

"},{"location":"tools/handbrake/#encoding-sehr-langsam","title":"Encoding sehr langsam","text":"
# CPU-Encoding-Preset anpassen (schneller = schlechtere Qualit\u00e4t)\nhandbrake_extra_args = --encoder-preset fast\n

Verf\u00fcgbare Presets: ultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow

"},{"location":"tools/handbrake/#gpu-encoding-nutzen-nvidia","title":"GPU-Encoding nutzen (NVIDIA)","text":"
handbrake_preset = H.265 NVENC 1080p\n

Erfordert HandBrake-Build mit NVENC-Unterst\u00fctzung und NVIDIA-GPU.

"},{"location":"tools/makemkv/","title":"MakeMKV","text":"

MakeMKV analysiert und rippt DVDs und Blu-rays. Ripster nutzt makemkvcon (die CLI-Version).

"},{"location":"tools/makemkv/#verwendete-befehle","title":"Verwendete Befehle","text":""},{"location":"tools/makemkv/#disc-analyse","title":"Disc-Analyse","text":"
makemkvcon -r --cache=1 info disc:0\n

Gibt alle Titel und Playlists der eingelegten Disc aus. Ripster parst diese Ausgabe um die verf\u00fcgbaren Tracks und Playlists zu bestimmen.

Parameter: - -r \u2013 Maschinen-lesbares Ausgabeformat - --cache=1 \u2013 Minimaler Disc-Cache - info disc:0 \u2013 Informationsabfrage f\u00fcr erstes Laufwerk

"},{"location":"tools/makemkv/#mkv-modus-standard","title":"MKV-Modus (Standard)","text":"
makemkvcon mkv disc:0 all /path/to/raw/ \\\n  --minlength=900 \\\n  -r\n

Erstellt MKV-Dateien aus allen Titeln, die l\u00e4nger als 15 Minuten sind.

Parameter: - mkv \u2013 MKV-Ausgabemodus - disc:0 \u2013 Erstes Disc-Laufwerk - all \u2013 Alle passenden Titel (nicht nur einen bestimmten) - --minlength=900 \u2013 Mindestl\u00e4nge in Sekunden (entspricht 15 Minuten)

"},{"location":"tools/makemkv/#backup-modus","title":"Backup-Modus","text":"
makemkvcon backup disc:0 /path/to/raw/backup/ \\\n  --decrypt \\\n  -r\n

Erstellt ein vollst\u00e4ndiges Disc-Backup mit Men\u00fcs.

Parameter: - backup \u2013 Backup-Modus - --decrypt \u2013 Verschl\u00fcsselung entfernen

"},{"location":"tools/makemkv/#ausgabeformat","title":"Ausgabeformat","text":"

MakeMKV gibt Fortschritt und Status in einem strukturierten Format aus:

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

Ripster's progressParsers.js parst diese Ausgabe f\u00fcr die Live-Fortschrittsanzeige.

"},{"location":"tools/makemkv/#makemkv-lizenz","title":"MakeMKV-Lizenz","text":"

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.

Ohne g\u00fcltige Lizenz k\u00f6nnen Blu-rays nicht entschl\u00fcsselt werden.

"},{"location":"tools/makemkv/#lizenz-eintragen","title":"Lizenz eintragen","text":"

Die Lizenz wird in den MakeMKV-Einstellungen eingetragen (GUI) oder direkt in:

~/.MakeMKV/settings.conf\n
app_Key = \"XXXX-XXXX-XXXX-XXXX-XXXX\"\n
"},{"location":"tools/makemkv/#konfiguration-in-ripster","title":"Konfiguration in Ripster","text":"Einstellung Beschreibung makemkv_command Pfad/Befehl f\u00fcr makemkvcon makemkv_min_length_minutes Mindest-Titell\u00e4nge (Standard: 15 Min) makemkv_backup_mode Backup-Modus statt MKV"},{"location":"tools/makemkv/#troubleshooting","title":"Troubleshooting","text":""},{"location":"tools/makemkv/#makemkv-erkennt-disc-nicht","title":"MakeMKV erkennt Disc nicht","text":"
# 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
"},{"location":"tools/makemkv/#langer-analyseprozess","title":"Langer Analyseprozess","text":"

Blu-ray-Analyse kann bei Discs mit vielen Playlists 5+ Minuten dauern. Dies ist normal.

"},{"location":"tools/makemkv/#fehlermeldung-libmmbd","title":"Fehlermeldung: \"LibMMBD\"","text":"

LibMMBD ist MakeMKVs interne Verschl\u00fcsselungsbibliothek. Bei Fehlern die MakeMKV-Version aktualisieren.

"},{"location":"tools/mediainfo/","title":"MediaInfo","text":"

MediaInfo analysiert die Track-Struktur von Mediendateien. Ripster nutzt es nach dem Ripping um Audio- und Untertitelspuren zu identifizieren.

"},{"location":"tools/mediainfo/#verwendeter-befehl","title":"Verwendeter Befehl","text":"
mediainfo --Output=JSON /path/to/raw/film.mkv\n

Gibt vollst\u00e4ndige Track-Informationen als JSON zur\u00fcck.

"},{"location":"tools/mediainfo/#ausgabe-struktur","title":"Ausgabe-Struktur","text":"
{\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
"},{"location":"tools/mediainfo/#verarbeitung-in-ripster","title":"Verarbeitung in Ripster","text":"

encodePlan.js verarbeitet die MediaInfo-Ausgabe:

  1. Track-Extraktion: Alle Audio- und Untertitel-Tracks werden extrahiert
  2. Sprach-Normalisierung: Sprachcodes werden auf ISO 639-3 normalisiert
  3. Codec-Klassifizierung: Bestimmt ob Codec kopiert oder transcodiert werden kann
  4. Track-Labels: Benutzerfreundliche Bezeichnungen (z.B. \"Deutsch (AC-3, 5.1)\")
"},{"location":"tools/mediainfo/#track-label-format","title":"Track-Label-Format","text":"
{Sprache} ({Format}, {Kan\u00e4le})\n

Beispiele: - Deutsch (AC-3, 5.1) - English (TrueHD, 7.1) - Fran\u00e7ais (AC-3, 2.0)

"},{"location":"tools/mediainfo/#konfiguration-in-ripster","title":"Konfiguration in Ripster","text":"Einstellung Beschreibung mediainfo_command Pfad/Befehl f\u00fcr mediainfo"},{"location":"tools/mediainfo/#troubleshooting","title":"Troubleshooting","text":""},{"location":"tools/mediainfo/#mediainfo-gibt-kein-json-aus","title":"MediaInfo gibt kein JSON aus","text":"
# Version pr\u00fcfen\nmediainfo --Version\n\n# JSON-Ausgabe testen\nmediainfo --Output=JSON /path/to/test.mkv\n

MediaInfo >= 17.10 wird empfohlen.

"},{"location":"tools/mediainfo/#sprache-als-und-angezeigt","title":"Sprache als \"und\" angezeigt","text":"

und 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.

"}]} \ No newline at end of file +{"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":"

Halbautomatische Disc-Ripping-Plattform f\u00fcr DVDs und Blu-rays

  • Automatisiertes Ripping

    Disc einlegen \u2013 Ripster erkennt sie automatisch und startet den Analyse-Workflow mit MakeMKV.

    Workflow verstehen

  • Metadata-Integration

    Automatische Suche in der OMDb-Datenbank f\u00fcr Filmtitel, Poster und IMDb-IDs.

    Konfiguration

  • Flexibles Encoding

    HandBrake-Encoding mit individueller Track-Auswahl f\u00fcr Audio- und Untertitelspuren.

    Encode-Planung

  • Job-Historie

    Vollst\u00e4ndiges Audit-Trail aller Ripping-Jobs mit Logs und Re-Encode-Funktion.

    History API

"},{"location":"#was-ist-ripster","title":"Was ist Ripster?","text":"

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:

Disc einlegen \u2192 Erkennung \u2192 Analyse \u2192 Metadaten w\u00e4hlen \u2192 Rippen \u2192 Encodieren \u2192 Fertig\n
"},{"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
  • Node.js >= 20.19.0 mit Express.js
  • SQLite3 mit automatischen Schema-Migrationen
  • WebSocket (ws) f\u00fcr Echtzeit-Kommunikation
  • Externe CLI-Tools: makemkvcon, HandBrakeCLI, mediainfo
  • React 18.3.1 mit React Router
  • Vite 5.4.12 als Build-Tool
  • PrimeReact 10.9.2 als UI-Bibliothek
  • WebSocket-Client f\u00fcr Live-Updates
Tool Zweck makemkvcon Disc-Analyse & MKV/Backup-Ripping HandBrakeCLI Video-Encoding mediainfo Track-Informationen aus gerippten Dateien OMDb API Filmmetadaten (Titel, Poster, IMDb-ID)"},{"location":"#schnellstart","title":"Schnellstart","text":"
# 1. Repository klonen\ngit clone https://github.com/YOUR_GITHUB_USERNAME/ripster.git\ncd ripster\n\n# 2. Starten (Node.js >= 20 erforderlich)\n./start.sh\n\n# 3. Browser \u00f6ffnen\nopen http://localhost:5173\n

Erste Schritte

Die vollst\u00e4ndige Installationsanleitung mit allen Voraussetzungen findest du unter Erste Schritte.

"},{"location":"#pipeline-uberblick","title":"Pipeline-\u00dcberblick","text":"
flowchart LR\n    IDLE --> DD[DISC_DETECTED]\n    DD --> META[METADATA\\nSELECTION]\n    META --> RTS[READY_TO\\nSTART]\n    RTS -->|Auto-Start| RIP[RIPPING]\n    RTS -->|Auto-Start mit RAW| MIC\n    RIP --> MIC[MEDIAINFO\\nCHECK]\n    MIC -->|Playlist offen (Backup)| WUD[WAITING_FOR\\nUSER_DECISION]\n    WUD --> MIC\n    MIC --> RTE[READY_TO\\nENCODE]\n    RTE --> ENC[ENCODING]\n    ENC -->|inkl. Post-Skripte| FIN([FINISHED])\n    ENC --> ERR([ERROR])\n    RIP --> 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

READY_TO_START ist in der Praxis meist ein kurzer \u00dcbergangszustand: der Job wird nach Metadaten-Auswahl automatisch gestartet oder in die Queue eingeplant.

"},{"location":"api/","title":"API-Referenz","text":"

Ripster bietet eine REST-API f\u00fcr Steuerung/Verwaltung sowie einen WebSocket-Endpunkt f\u00fcr Echtzeit-Updates.

"},{"location":"api/#basis-url","title":"Basis-URL","text":"
http://localhost:3001\n

API-Prefix: /api

Beispiele:

  • GET /api/health
  • GET /api/pipeline/state
"},{"location":"api/#api-gruppen","title":"API-Gruppen","text":"
  • Health

    Service-Liveness.

    GET /api/health

  • Pipeline API

    Analyse, Start/Retry/Cancel, Queue, Re-Encode.

    Pipeline API

  • Settings API

    Einstellungen, Skripte/Ketten, User-Presets.

    Settings API

  • History API

    Job-Historie, Orphan-Import, L\u00f6schoperationen.

    History API

  • Cron API

    Zeitgesteuerte Skript-/Kettenausf\u00fchrung.

    Cron API

  • WebSocket Events

    Pipeline-, Queue-, Disk-, Settings-, Cron- und Monitoring-Events.

    WebSocket

"},{"location":"api/#authentifizierung","title":"Authentifizierung","text":"

Es gibt keine eingebaute Authentifizierung. Ripster ist f\u00fcr lokalen Betrieb gedacht.

"},{"location":"api/#fehlerformat","title":"Fehlerformat","text":"

Fehler werden zentral als JSON geliefert:

{\n  \"error\": {\n    \"message\": \"Job nicht gefunden.\",\n    \"statusCode\": 404,\n    \"reqId\": \"req_...\",\n    \"details\": [\n      {\n        \"field\": \"name\",\n        \"message\": \"Name darf nicht leer sein.\"\n      }\n    ]\n  }\n}\n

details ist optional (z. B. bei Validierungsfehlern).

"},{"location":"api/#haufige-statuscodes","title":"H\u00e4ufige Statuscodes","text":"Code Bedeutung 200 Erfolg 201 Ressource erstellt 400 Ung\u00fcltige Anfrage / Validierungsfehler 404 Ressource nicht gefunden 409 Konflikt (z. B. falscher Pipeline-Zustand, Job l\u00e4uft bereits) 500 Interner Fehler"},{"location":"api/crons/","title":"Cron API","text":"

Ripster enth\u00e4lt ein eingebautes Cron-System f\u00fcr Skripte und Skript-Ketten (sourceType: script|chain).

"},{"location":"api/crons/#get-apicrons","title":"GET /api/crons","text":"

Listet alle Cron-Jobs.

{\n  \"jobs\": [\n    {\n      \"id\": 1,\n      \"name\": \"Nachtlauf Backup\",\n      \"cronExpression\": \"0 2 * * *\",\n      \"sourceType\": \"script\",\n      \"sourceId\": 3,\n      \"sourceName\": \"Backup-Skript\",\n      \"enabled\": true,\n      \"pushoverEnabled\": true,\n      \"lastRunAt\": \"2026-03-10T02:00:00.000Z\",\n      \"lastRunStatus\": \"success\",\n      \"nextRunAt\": \"2026-03-11T02:00:00.000Z\",\n      \"createdAt\": \"2026-03-01T10:00:00.000Z\",\n      \"updatedAt\": \"2026-03-10T02:00:05.000Z\"\n    }\n  ]\n}\n
"},{"location":"api/crons/#post-apicrons","title":"POST /api/crons","text":"

Erstellt Cron-Job.

{\n  \"name\": \"Nachtlauf Backup\",\n  \"cronExpression\": \"0 2 * * *\",\n  \"sourceType\": \"script\",\n  \"sourceId\": 3,\n  \"enabled\": true,\n  \"pushoverEnabled\": true\n}\n

Response: 201 mit { \"job\": { ... } }

"},{"location":"api/crons/#get-apicronsid","title":"GET /api/crons/:id","text":"

Response:

{ \"job\": { \"id\": 1, \"name\": \"...\" } }\n
"},{"location":"api/crons/#put-apicronsid","title":"PUT /api/crons/:id","text":"

Aktualisiert Cron-Job. Felder wie bei POST.

Response:

{ \"job\": { ... } }\n
"},{"location":"api/crons/#delete-apicronsid","title":"DELETE /api/crons/:id","text":"

Response:

{ \"removed\": { \"id\": 1, \"name\": \"Nachtlauf Backup\" } }\n
"},{"location":"api/crons/#get-apicronsidlogs","title":"GET /api/crons/:id/logs","text":"

Liefert Ausf\u00fchrungs-Logs.

Query-Parameter:

Parameter Typ Default Beschreibung limit number 20 Anzahl Eintr\u00e4ge, max. 100

Response:

{\n  \"logs\": [\n    {\n      \"id\": 42,\n      \"cronJobId\": 1,\n      \"startedAt\": \"2026-03-10T02:00:01.000Z\",\n      \"finishedAt\": \"2026-03-10T02:00:05.000Z\",\n      \"status\": \"success\",\n      \"output\": \"Backup abgeschlossen.\",\n      \"errorMessage\": null\n    }\n  ]\n}\n

status: running | success | error

"},{"location":"api/crons/#post-apicronsidrun","title":"POST /api/crons/:id/run","text":"

Triggert Job manuell (asynchron).

Response:

{ \"triggered\": true, \"cronJobId\": 1 }\n

Wenn Job bereits l\u00e4uft: 409.

"},{"location":"api/crons/#post-apicronsvalidate-expression","title":"POST /api/crons/validate-expression","text":"

Validiert 5-Felder-Cron-Ausdruck und berechnet n\u00e4chsten Lauf.

Request:

{ \"cronExpression\": \"*/15 * * * *\" }\n

G\u00fcltige Response:

{\n  \"valid\": true,\n  \"nextRunAt\": \"2026-03-10T14:15:00.000Z\"\n}\n

Ung\u00fcltige Response:

{\n  \"valid\": false,\n  \"error\": \"Cron-Ausdruck muss genau 5 Felder haben (Minute Stunde Tag Monat Wochentag).\",\n  \"nextRunAt\": null\n}\n
"},{"location":"api/crons/#cron-format","title":"Cron-Format","text":"

Ripster unterst\u00fctzt 5 Felder:

Minute Stunde Tag Monat Wochentag\n

Beispiele:

  • 0 2 * * * t\u00e4glich 02:00
  • */15 * * * * alle 15 Minuten
  • 0 6 * * 1-5 Mo-Fr 06:00
"},{"location":"api/crons/#websocket-events-zu-cron","title":"WebSocket-Events zu Cron","text":"
  • CRON_JOBS_UPDATED bei Create/Update/Delete
  • CRON_JOB_UPDATED bei Laufzeitstatus (running -> success|error)
"},{"location":"api/history/","title":"History API","text":"

Endpunkte f\u00fcr Job-Historie, Orphan-Import und L\u00f6schoperationen.

"},{"location":"api/history/#get-apihistory","title":"GET /api/history","text":"

Liefert Jobs (optionale Filter).

Query-Parameter:

Parameter Typ Beschreibung status string Filter nach Job-Status search string Suche in Titel-Feldern

Beispiel:

GET /api/history?status=FINISHED&search=Inception\n

Response:

{\n  \"jobs\": [\n    {\n      \"id\": 42,\n      \"status\": \"FINISHED\",\n      \"title\": \"Inception\",\n      \"raw_path\": \"/mnt/raw/Inception - RAW - job-42\",\n      \"output_path\": \"/mnt/movies/Inception (2010)/Inception (2010).mkv\",\n      \"mediaType\": \"bluray\",\n      \"ripSuccessful\": true,\n      \"encodeSuccess\": true,\n      \"created_at\": \"2026-03-10T08:00:00.000Z\",\n      \"updated_at\": \"2026-03-10T10:00:00.000Z\"\n    }\n  ]\n}\n
"},{"location":"api/history/#get-apihistoryid","title":"GET /api/history/:id","text":"

Liefert Job-Detail.

Query-Parameter:

Parameter Typ Standard Beschreibung includeLogs bool false Prozesslog laden includeLiveLog bool false alias-artig ebenfalls Prozesslog laden includeAllLogs bool false vollst\u00e4ndiges Log statt Tail logTailLines number 800 Tail-L\u00e4nge falls nicht includeAllLogs

Response:

{\n  \"job\": {\n    \"id\": 42,\n    \"status\": \"FINISHED\",\n    \"makemkvInfo\": {},\n    \"mediainfoInfo\": {},\n    \"handbrakeInfo\": {},\n    \"encodePlan\": {},\n    \"log\": \"...\",\n    \"log_count\": 1,\n    \"logMeta\": {\n      \"loaded\": true,\n      \"total\": 800,\n      \"returned\": 800,\n      \"truncated\": true\n    }\n  }\n}\n
"},{"location":"api/history/#get-apihistorydatabase","title":"GET /api/history/database","text":"

Debug-Ansicht der DB-Zeilen (angereichert).

Response:

{\n  \"rows\": [\n    {\n      \"id\": 42,\n      \"status\": \"FINISHED\",\n      \"rawFolderName\": \"Inception - RAW - job-42\"\n    }\n  ]\n}\n
"},{"location":"api/history/#get-apihistoryorphan-raw","title":"GET /api/history/orphan-raw","text":"

Sucht RAW-Ordner ohne zugeh\u00f6rigen Job.

Response:

{\n  \"rawDir\": \"/mnt/raw\",\n  \"rawDirs\": [\"/mnt/raw\", \"/mnt/raw-bluray\"],\n  \"rows\": [\n    {\n      \"rawPath\": \"/mnt/raw/Inception (2010) [tt1375666] - RAW - job-99\",\n      \"folderName\": \"Inception (2010) [tt1375666] - RAW - job-99\",\n      \"title\": \"Inception\",\n      \"year\": 2010,\n      \"imdbId\": \"tt1375666\",\n      \"folderJobId\": 99,\n      \"entryCount\": 4,\n      \"hasBlurayStructure\": true,\n      \"lastModifiedAt\": \"2026-03-10T09:00:00.000Z\"\n    }\n  ]\n}\n
"},{"location":"api/history/#post-apihistoryorphan-rawimport","title":"POST /api/history/orphan-raw/import","text":"

Importiert RAW-Ordner als FINISHED-Job.

Request:

{ \"rawPath\": \"/mnt/raw/Inception (2010) [tt1375666] - RAW - job-99\" }\n

Response:

{\n  \"job\": { \"id\": 77, \"status\": \"FINISHED\" },\n  \"uiReset\": { \"reset\": true, \"state\": \"IDLE\" }\n}\n
"},{"location":"api/history/#post-apihistoryidomdbassign","title":"POST /api/history/:id/omdb/assign","text":"

Weist OMDb-/Metadaten nachtr\u00e4glich zu.

Request:

{\n  \"imdbId\": \"tt1375666\",\n  \"title\": \"Inception\",\n  \"year\": 2010,\n  \"poster\": \"https://...\",\n  \"fromOmdb\": true\n}\n

Response:

{ \"job\": { \"id\": 42, \"imdb_id\": \"tt1375666\" } }\n
"},{"location":"api/history/#post-apihistoryiddelete-files","title":"POST /api/history/:id/delete-files","text":"

L\u00f6scht Dateien eines Jobs, beh\u00e4lt DB-Eintrag.

Request:

{ \"target\": \"both\" }\n

target: raw | movie | both

Response:

{\n  \"summary\": {\n    \"target\": \"both\",\n    \"raw\": { \"attempted\": true, \"deleted\": true, \"filesDeleted\": 12, \"dirsRemoved\": 3, \"reason\": null },\n    \"movie\": { \"attempted\": true, \"deleted\": false, \"filesDeleted\": 0, \"dirsRemoved\": 0, \"reason\": \"Movie-Datei/Pfad existiert nicht.\" }\n  },\n  \"job\": { \"id\": 42 }\n}\n
"},{"location":"api/history/#post-apihistoryiddelete","title":"POST /api/history/:id/delete","text":"

L\u00f6scht Job aus DB; optional auch Dateien.

Request:

{ \"target\": \"none\" }\n

target: none | raw | movie | both

Response:

{\n  \"deleted\": true,\n  \"jobId\": 42,\n  \"fileTarget\": \"both\",\n  \"fileSummary\": {\n    \"target\": \"both\",\n    \"raw\": { \"filesDeleted\": 10 },\n    \"movie\": { \"filesDeleted\": 1 }\n  },\n  \"uiReset\": {\n    \"reset\": true,\n    \"state\": \"IDLE\"\n  }\n}\n
"},{"location":"api/history/#hinweise","title":"Hinweise","text":"
  • Ein aktiver Pipeline-Job kann nicht gel\u00f6scht werden (409).
  • Alle L\u00f6schoperationen sind irreversibel.
"},{"location":"api/pipeline/","title":"Pipeline API","text":"

Endpunkte zur Steuerung des Pipeline-Workflows.

"},{"location":"api/pipeline/#get-apipipelinestate","title":"GET /api/pipeline/state","text":"

Liefert aktuellen Pipeline- und Hardware-Monitoring-Snapshot.

Response (Beispiel):

{\n  \"pipeline\": {\n    \"state\": \"READY_TO_ENCODE\",\n    \"activeJobId\": 42,\n    \"progress\": 0,\n    \"eta\": null,\n    \"statusText\": \"Mediainfo best\u00e4tigt - Encode manuell starten\",\n    \"context\": {\n      \"jobId\": 42\n    },\n    \"jobProgress\": {\n      \"42\": {\n        \"state\": \"MEDIAINFO_CHECK\",\n        \"progress\": 68.5,\n        \"eta\": null,\n        \"statusText\": \"MEDIAINFO_CHECK 68.50%\"\n      }\n    },\n    \"queue\": {\n      \"maxParallelJobs\": 1,\n      \"runningCount\": 1,\n      \"queuedCount\": 2,\n      \"runningJobs\": [],\n      \"queuedJobs\": []\n    }\n  },\n  \"hardwareMonitoring\": {\n    \"enabled\": true,\n    \"intervalMs\": 5000,\n    \"updatedAt\": \"2026-03-10T09:00:00.000Z\",\n    \"sample\": {\n      \"cpu\": {},\n      \"memory\": {},\n      \"gpu\": {},\n      \"storage\": {}\n    },\n    \"error\": null\n  }\n}\n
"},{"location":"api/pipeline/#post-apipipelineanalyze","title":"POST /api/pipeline/analyze","text":"

Startet Disc-Analyse und legt Job an.

Response:

{\n  \"result\": {\n    \"jobId\": 42,\n    \"detectedTitle\": \"INCEPTION\",\n    \"omdbCandidates\": []\n  }\n}\n
"},{"location":"api/pipeline/#post-apipipelinerescan-disc","title":"POST /api/pipeline/rescan-disc","text":"

Erzwingt erneute Laufwerkspr\u00fcfung.

Response (Beispiel):

{\n  \"result\": {\n    \"present\": true,\n    \"changed\": true,\n    \"emitted\": \"discInserted\",\n    \"device\": {\n      \"path\": \"/dev/sr0\",\n      \"discLabel\": \"INCEPTION\",\n      \"mediaProfile\": \"bluray\"\n    }\n  }\n}\n
"},{"location":"api/pipeline/#get-apipipelineomdbsearchq","title":"GET /api/pipeline/omdb/search?q=

OMDb-Titelsuche.

Response:

{\n  \"results\": [\n    {\n      \"imdbId\": \"tt1375666\",\n      \"title\": \"Inception\",\n      \"year\": \"2010\",\n      \"type\": \"movie\",\n      \"poster\": \"https://...\"\n    }\n  ]\n}\n
","text":""},{"location":"api/pipeline/#post-apipipelineselect-metadata","title":"POST /api/pipeline/select-metadata

Setzt Metadaten (und optional Playlist) f\u00fcr einen Job.

Request:

{\n  \"jobId\": 42,\n  \"title\": \"Inception\",\n  \"year\": 2010,\n  \"imdbId\": \"tt1375666\",\n  \"poster\": \"https://...\",\n  \"fromOmdb\": true,\n  \"selectedPlaylist\": \"00800\"\n}\n

Response:

{ \"job\": { \"id\": 42, \"status\": \"READY_TO_START\" } }\n
","text":""},{"location":"api/pipeline/#post-apipipelinestartjobid","title":"POST /api/pipeline/start/:jobId

Startet vorbereiteten Job oder queued ihn (je nach Parallel-Limit).

M\u00f6gliche Responses:

{ \"result\": { \"started\": true, \"stage\": \"RIPPING\" } }\n
{ \"result\": { \"queued\": true, \"started\": false, \"queuePosition\": 2, \"action\": \"START_PREPARED\" } }\n
","text":""},{"location":"api/pipeline/#post-apipipelineconfirm-encodejobid","title":"POST /api/pipeline/confirm-encode/:jobId

Best\u00e4tigt Review-Auswahl (Tracks, Pre/Post-Skripte/Ketten, User-Preset).

Request (typisch):

{\n  \"selectedEncodeTitleId\": 1,\n  \"selectedTrackSelection\": {\n    \"1\": {\n      \"audioTrackIds\": [1, 2],\n      \"subtitleTrackIds\": [3]\n    }\n  },\n  \"selectedPreEncodeScriptIds\": [1],\n  \"selectedPostEncodeScriptIds\": [2, 7],\n  \"selectedPreEncodeChainIds\": [3],\n  \"selectedPostEncodeChainIds\": [4],\n  \"selectedUserPresetId\": 5,\n  \"skipPipelineStateUpdate\": false\n}\n

Response:

{ \"job\": { \"id\": 42, \"encode_review_confirmed\": 1 } }\n
","text":""},{"location":"api/pipeline/#post-apipipelinecancel","title":"POST /api/pipeline/cancel

Bricht laufenden Job ab oder entfernt Queue-Eintrag.

Request (optional):

{ \"jobId\": 42 }\n

M\u00f6gliche Responses:

{ \"result\": { \"cancelled\": true, \"queuedOnly\": true, \"jobId\": 42 } }\n
{ \"result\": { \"cancelled\": true, \"queuedOnly\": false, \"jobId\": 42 } }\n
{ \"result\": { \"cancelled\": true, \"queuedOnly\": false, \"pending\": true, \"jobId\": 42 } }\n
","text":""},{"location":"api/pipeline/#post-apipipelineretryjobid","title":"POST /api/pipeline/retry/:jobId

Retry f\u00fcr ERROR/CANCELLED-Jobs (oder Queue-Einreihung).

","text":""},{"location":"api/pipeline/#post-apipipelinereencodejobid","title":"POST /api/pipeline/reencode/:jobId

Startet Re-Encode aus bestehendem RAW.

","text":""},{"location":"api/pipeline/#post-apipipelinerestart-reviewjobid","title":"POST /api/pipeline/restart-review/:jobId

Berechnet Review aus RAW neu.

","text":""},{"location":"api/pipeline/#post-apipipelinerestart-encodejobid","title":"POST /api/pipeline/restart-encode/:jobId

Startet Encoding mit letzter best\u00e4tigter Review neu.

","text":""},{"location":"api/pipeline/#post-apipipelineresume-readyjobid","title":"POST /api/pipeline/resume-ready/:jobId

L\u00e4dt READY_TO_ENCODE-Job nach Neustart wieder in aktive Session.

Alle Endpunkte liefern { result: ... } bzw. { job: ... }.

","text":""},{"location":"api/pipeline/#queue-endpunkte","title":"Queue-Endpunkte","text":""},{"location":"api/pipeline/#get-apipipelinequeue","title":"GET /api/pipeline/queue","text":"

Liefert Queue-Snapshot.

{\n  \"queue\": {\n    \"maxParallelJobs\": 1,\n    \"runningCount\": 1,\n    \"queuedCount\": 3,\n    \"runningJobs\": [\n      {\n        \"jobId\": 41,\n        \"title\": \"Inception\",\n        \"status\": \"ENCODING\",\n        \"lastState\": \"ENCODING\"\n      }\n    ],\n    \"queuedJobs\": [\n      {\n        \"entryId\": 11,\n        \"position\": 1,\n        \"type\": \"job\",\n        \"jobId\": 42,\n        \"action\": \"START_PREPARED\",\n        \"actionLabel\": \"Start\",\n        \"title\": \"Matrix\",\n        \"status\": \"READY_TO_ENCODE\",\n        \"lastState\": \"READY_TO_ENCODE\",\n        \"hasScripts\": true,\n        \"hasChains\": false,\n        \"enqueuedAt\": \"2026-03-10T09:00:00.000Z\"\n      },\n      {\n        \"entryId\": 12,\n        \"position\": 2,\n        \"type\": \"wait\",\n        \"waitSeconds\": 30,\n        \"title\": \"Warten 30s\",\n        \"status\": \"QUEUED\",\n        \"enqueuedAt\": \"2026-03-10T09:01:00.000Z\"\n      }\n    ],\n    \"updatedAt\": \"2026-03-10T09:01:02.000Z\"\n  }\n}\n
"},{"location":"api/pipeline/#post-apipipelinequeuereorder","title":"POST /api/pipeline/queue/reorder","text":"

Sortiert Queue-Eintr\u00e4ge neu.

Request:

{\n  \"orderedEntryIds\": [12, 11]\n}\n

Legacy fallback wird akzeptiert:

{\n  \"orderedJobIds\": [42, 43]\n}\n
"},{"location":"api/pipeline/#post-apipipelinequeueentry","title":"POST /api/pipeline/queue/entry","text":"

F\u00fcgt Nicht-Job-Queue-Eintrag hinzu (script, chain, wait).

Request-Beispiele:

{ \"type\": \"script\", \"scriptId\": 3 }\n
{ \"type\": \"chain\", \"chainId\": 2, \"insertAfterEntryId\": 11 }\n
{ \"type\": \"wait\", \"waitSeconds\": 45 }\n

Response:

{\n  \"result\": { \"entryId\": 12, \"type\": \"wait\", \"position\": 2 },\n  \"queue\": { \"...\": \"...\" }\n}\n
"},{"location":"api/pipeline/#delete-apipipelinequeueentryentryid","title":"DELETE /api/pipeline/queue/entry/:entryId","text":"

Entfernt Queue-Eintrag.

Response:

{ \"queue\": { \"...\": \"...\" } }\n
"},{"location":"api/pipeline/#pipeline-zustande","title":"Pipeline-Zust\u00e4nde State Bedeutung IDLE Wartet auf Medium DISC_DETECTED Medium erkannt ANALYZING MakeMKV-Analyse l\u00e4uft METADATA_SELECTION Metadaten-Auswahl WAITING_FOR_USER_DECISION Playlist-Entscheidung erforderlich READY_TO_START \u00dcbergang vor Start RIPPING MakeMKV-Rip l\u00e4uft MEDIAINFO_CHECK Titel-/Track-Auswertung READY_TO_ENCODE Review bereit ENCODING HandBrake-Encoding l\u00e4uft FINISHED Abgeschlossen CANCELLED Abgebrochen ERROR Fehler","text":""},{"location":"api/settings/","title":"Settings API","text":"

Endpunkte f\u00fcr Einstellungen, Skripte, Skript-Ketten und User-Presets.

"},{"location":"api/settings/#get-apisettings","title":"GET /api/settings","text":"

Liefert alle Einstellungen kategorisiert.

Response (Struktur):

{\n  \"categories\": [\n    {\n      \"category\": \"Pfade\",\n      \"settings\": [\n        {\n          \"key\": \"raw_dir\",\n          \"label\": \"Raw Ausgabeordner\",\n          \"type\": \"path\",\n          \"required\": true,\n          \"description\": \"...\",\n          \"defaultValue\": \"data/output/raw\",\n          \"options\": [],\n          \"validation\": { \"minLength\": 1 },\n          \"value\": \"data/output/raw\",\n          \"orderIndex\": 100\n        }\n      ]\n    }\n  ]\n}\n
"},{"location":"api/settings/#put-apisettingskey","title":"PUT /api/settings/:key","text":"

Aktualisiert eine einzelne Einstellung.

Request:

{ \"value\": \"/mnt/storage/raw\" }\n

Response:

{\n  \"setting\": {\n    \"key\": \"raw_dir\",\n    \"value\": \"/mnt/storage/raw\"\n  },\n  \"reviewRefresh\": {\n    \"triggered\": false,\n    \"reason\": \"not_ready\"\n  }\n}\n

reviewRefresh ist null oder ein Objekt mit Status der optionalen Review-Neuberechnung.

"},{"location":"api/settings/#put-apisettings","title":"PUT /api/settings","text":"

Aktualisiert mehrere Einstellungen atomar.

Request:

{\n  \"settings\": {\n    \"raw_dir\": \"/mnt/storage/raw\",\n    \"movie_dir\": \"/mnt/storage/movies\",\n    \"handbrake_preset_bluray\": \"H.264 MKV 1080p30\"\n  }\n}\n

Response:

{\n  \"changes\": [\n    { \"key\": \"raw_dir\", \"value\": \"/mnt/storage/raw\" },\n    { \"key\": \"movie_dir\", \"value\": \"/mnt/storage/movies\" }\n  ],\n  \"reviewRefresh\": {\n    \"triggered\": true,\n    \"jobId\": 42,\n    \"relevantKeys\": [\"handbrake_preset_bluray\"]\n  }\n}\n

Bei Validierungsfehlern kommt 400 mit error.details[].

"},{"location":"api/settings/#get-apisettingshandbrake-presets","title":"GET /api/settings/handbrake-presets","text":"

Liest Preset-Liste via HandBrakeCLI -z (mit Fallback auf konfigurierte Presets).

Response (Beispiel):

{\n  \"source\": \"handbrake-cli\",\n  \"message\": null,\n  \"options\": [\n    { \"label\": \"General/\", \"value\": \"__group__general\", \"disabled\": true, \"category\": \"General\" },\n    { \"label\": \"   Fast 1080p30\", \"value\": \"Fast 1080p30\", \"category\": \"General\" }\n  ]\n}\n
"},{"location":"api/settings/#post-apisettingspushovertest","title":"POST /api/settings/pushover/test","text":"

Sendet Testnachricht \u00fcber aktuelle PushOver-Settings.

Request (optional):

{\n  \"title\": \"Test\",\n  \"message\": \"Ripster Test\"\n}\n

Response:

{\n  \"result\": {\n    \"sent\": true,\n    \"eventKey\": \"test\",\n    \"requestId\": \"...\"\n  }\n}\n

Wenn PushOver deaktiviert ist oder Credentials fehlen, kommt i. d. R. ebenfalls 200 mit sent: false + reason.

"},{"location":"api/settings/#skripte","title":"Skripte","text":"

Basis: /api/settings/scripts

"},{"location":"api/settings/#get-apisettingsscripts","title":"GET /api/settings/scripts","text":"
{ \"scripts\": [ { \"id\": 1, \"name\": \"...\", \"scriptBody\": \"...\", \"orderIndex\": 1, \"createdAt\": \"...\", \"updatedAt\": \"...\" } ] }\n
"},{"location":"api/settings/#post-apisettingsscripts","title":"POST /api/settings/scripts","text":"
{ \"name\": \"Move\", \"scriptBody\": \"mv \\\"$RIPSTER_OUTPUT_PATH\\\" /mnt/movies/\" }\n

Response: 201 mit { \"script\": { ... } }

"},{"location":"api/settings/#put-apisettingsscriptsid","title":"PUT /api/settings/scripts/:id","text":"

Body wie POST, Response { \"script\": { ... } }.

"},{"location":"api/settings/#delete-apisettingsscriptsid","title":"DELETE /api/settings/scripts/:id","text":"

Response { \"removed\": { ... } }.

"},{"location":"api/settings/#post-apisettingsscriptsreorder","title":"POST /api/settings/scripts/reorder","text":"
{ \"orderedScriptIds\": [3, 1, 2] }\n

Response { \"scripts\": [ ... ] }.

"},{"location":"api/settings/#post-apisettingsscriptsidtest","title":"POST /api/settings/scripts/:id/test","text":"

F\u00fchrt Skript als Testlauf aus.

{\n  \"result\": {\n    \"scriptId\": 1,\n    \"scriptName\": \"Move\",\n    \"success\": true,\n    \"exitCode\": 0,\n    \"signal\": null,\n    \"timedOut\": false,\n    \"durationMs\": 120,\n    \"stdout\": \"...\",\n    \"stderr\": \"...\",\n    \"stdoutTruncated\": false,\n    \"stderrTruncated\": false\n  }\n}\n
"},{"location":"api/settings/#umgebungsvariablen-fur-skripte","title":"Umgebungsvariablen f\u00fcr Skripte","text":"

Diese Variablen werden beim Ausf\u00fchren gesetzt:

  • RIPSTER_SCRIPT_RUN_AT
  • RIPSTER_JOB_ID
  • RIPSTER_JOB_TITLE
  • RIPSTER_MODE
  • RIPSTER_INPUT_PATH
  • RIPSTER_OUTPUT_PATH
  • RIPSTER_RAW_PATH
  • RIPSTER_SCRIPT_ID
  • RIPSTER_SCRIPT_NAME
  • RIPSTER_SCRIPT_SOURCE
"},{"location":"api/settings/#skript-ketten","title":"Skript-Ketten","text":"

Basis: /api/settings/script-chains

Eine Kette hat Schritte vom Typ:

  • script (scriptId erforderlich)
  • wait (waitSeconds 1..3600)
"},{"location":"api/settings/#get-apisettingsscript-chains","title":"GET /api/settings/script-chains","text":"

Response { \"chains\": [ ... ] } (inkl. steps[]).

"},{"location":"api/settings/#get-apisettingsscript-chainsid","title":"GET /api/settings/script-chains/:id","text":"

Response { \"chain\": { ... } }.

"},{"location":"api/settings/#post-apisettingsscript-chains","title":"POST /api/settings/script-chains","text":"
{\n  \"name\": \"After Encode\",\n  \"steps\": [\n    { \"stepType\": \"script\", \"scriptId\": 1 },\n    { \"stepType\": \"wait\", \"waitSeconds\": 15 },\n    { \"stepType\": \"script\", \"scriptId\": 2 }\n  ]\n}\n

Response: 201 mit { \"chain\": { ... } }

"},{"location":"api/settings/#put-apisettingsscript-chainsid","title":"PUT /api/settings/script-chains/:id","text":"

Body wie POST, Response { \"chain\": { ... } }.

"},{"location":"api/settings/#delete-apisettingsscript-chainsid","title":"DELETE /api/settings/script-chains/:id","text":"

Response { \"removed\": { ... } }.

"},{"location":"api/settings/#post-apisettingsscript-chainsreorder","title":"POST /api/settings/script-chains/reorder","text":"
{ \"orderedChainIds\": [2, 1, 3] }\n

Response { \"chains\": [ ... ] }.

"},{"location":"api/settings/#post-apisettingsscript-chainsidtest","title":"POST /api/settings/script-chains/:id/test","text":"

Response:

{\n  \"result\": {\n    \"chainId\": 2,\n    \"chainName\": \"After Encode\",\n    \"steps\": 3,\n    \"succeeded\": 3,\n    \"failed\": 0,\n    \"aborted\": false,\n    \"results\": []\n  }\n}\n
"},{"location":"api/settings/#user-presets","title":"User-Presets","text":"

Basis: /api/settings/user-presets

"},{"location":"api/settings/#get-apisettingsuser-presets","title":"GET /api/settings/user-presets","text":"

Optionaler Query-Parameter: media_type=bluray|dvd|other|all

{\n  \"presets\": [\n    {\n      \"id\": 1,\n      \"name\": \"Blu-ray HQ\",\n      \"mediaType\": \"bluray\",\n      \"handbrakePreset\": \"H.264 MKV 1080p30\",\n      \"extraArgs\": \"--encoder-preset slow\",\n      \"description\": \"...\",\n      \"createdAt\": \"...\",\n      \"updatedAt\": \"...\"\n    }\n  ]\n}\n
"},{"location":"api/settings/#post-apisettingsuser-presets","title":"POST /api/settings/user-presets","text":"
{\n  \"name\": \"Blu-ray HQ\",\n  \"mediaType\": \"bluray\",\n  \"handbrakePreset\": \"H.264 MKV 1080p30\",\n  \"extraArgs\": \"--encoder-preset slow\",\n  \"description\": \"optional\"\n}\n

Response: 201 mit { \"preset\": { ... } }

"},{"location":"api/settings/#put-apisettingsuser-presetsid","title":"PUT /api/settings/user-presets/:id","text":"

Body mit beliebigen Feldern aus POST, Response { \"preset\": { ... } }.

"},{"location":"api/settings/#delete-apisettingsuser-presetsid","title":"DELETE /api/settings/user-presets/:id","text":"

Response { \"removed\": { ... } }.

"},{"location":"api/websocket/","title":"WebSocket Events","text":"

Ripster sendet Echtzeit-Updates \u00fcber /ws.

"},{"location":"api/websocket/#verbindung","title":"Verbindung","text":"
const ws = new WebSocket('ws://localhost:3001/ws');\n\nws.onmessage = (event) => {\n  const msg = JSON.parse(event.data);\n  console.log(msg.type, msg.payload);\n};\n
"},{"location":"api/websocket/#nachrichtenformat","title":"Nachrichtenformat","text":"

Die meisten Broadcasts haben dieses Schema:

{\n  \"type\": \"EVENT_TYPE\",\n  \"payload\": {},\n  \"timestamp\": \"2026-03-10T09:00:00.000Z\"\n}\n

Ausnahme: WS_CONNECTED beim Verbindungsaufbau enth\u00e4lt kein timestamp.

"},{"location":"api/websocket/#event-typen","title":"Event-Typen","text":""},{"location":"api/websocket/#ws_connected","title":"WS_CONNECTED","text":"

Sofort nach erfolgreicher Verbindung.

{\n  \"type\": \"WS_CONNECTED\",\n  \"payload\": {\n    \"connectedAt\": \"2026-03-10T09:00:00.000Z\"\n  }\n}\n
"},{"location":"api/websocket/#pipeline_state_changed","title":"PIPELINE_STATE_CHANGED","text":"

Neuer Pipeline-Snapshot.

{\n  \"type\": \"PIPELINE_STATE_CHANGED\",\n  \"payload\": {\n    \"state\": \"ENCODING\",\n    \"activeJobId\": 42,\n    \"progress\": 62.5,\n    \"eta\": \"00:12:34\",\n    \"statusText\": \"ENCODING 62.50%\",\n    \"context\": {},\n    \"jobProgress\": {\n      \"42\": {\n        \"state\": \"ENCODING\",\n        \"progress\": 62.5,\n        \"eta\": \"00:12:34\",\n        \"statusText\": \"ENCODING 62.50%\"\n      }\n    },\n    \"queue\": {\n      \"maxParallelJobs\": 1,\n      \"runningCount\": 1,\n      \"queuedCount\": 2,\n      \"runningJobs\": [],\n      \"queuedJobs\": []\n    }\n  }\n}\n
"},{"location":"api/websocket/#pipeline_progress","title":"PIPELINE_PROGRESS","text":"

Laufende Fortschrittsupdates.

{\n  \"type\": \"PIPELINE_PROGRESS\",\n  \"payload\": {\n    \"state\": \"ENCODING\",\n    \"activeJobId\": 42,\n    \"progress\": 62.5,\n    \"eta\": \"00:12:34\",\n    \"statusText\": \"ENCODING 62.50%\"\n  }\n}\n
"},{"location":"api/websocket/#pipeline_queue_changed","title":"PIPELINE_QUEUE_CHANGED","text":"

Queue-Snapshot aktualisiert.

"},{"location":"api/websocket/#disc_detected-disc_removed","title":"DISC_DETECTED / DISC_REMOVED","text":"

Disc-Insertion/-Removal.

{\n  \"type\": \"DISC_DETECTED\",\n  \"payload\": {\n    \"device\": {\n      \"path\": \"/dev/sr0\",\n      \"discLabel\": \"INCEPTION\",\n      \"model\": \"ASUS BW-16D1HT\",\n      \"fstype\": \"udf\",\n      \"mountpoint\": null,\n      \"mediaProfile\": \"bluray\"\n    }\n  }\n}\n

mediaProfile: bluray | dvd | other | null

"},{"location":"api/websocket/#hardware_monitor_update","title":"HARDWARE_MONITOR_UPDATE","text":"

Snapshot aus Hardware-Monitoring.

{\n  \"type\": \"HARDWARE_MONITOR_UPDATE\",\n  \"payload\": {\n    \"enabled\": true,\n    \"intervalMs\": 5000,\n    \"updatedAt\": \"2026-03-10T09:00:00.000Z\",\n    \"sample\": {\n      \"cpu\": {},\n      \"memory\": {},\n      \"gpu\": {},\n      \"storage\": {}\n    },\n    \"error\": null\n  }\n}\n
"},{"location":"api/websocket/#pipeline_error","title":"PIPELINE_ERROR","text":"

Fehler bei Disc-Event-Verarbeitung in Pipeline.

"},{"location":"api/websocket/#disk_detection_error","title":"DISK_DETECTION_ERROR","text":"

Fehler in Laufwerkserkennung.

"},{"location":"api/websocket/#settings_updated","title":"SETTINGS_UPDATED","text":"

Einzelnes Setting wurde gespeichert.

"},{"location":"api/websocket/#settings_bulk_updated","title":"SETTINGS_BULK_UPDATED","text":"

Bulk-Settings gespeichert.

{\n  \"type\": \"SETTINGS_BULK_UPDATED\",\n  \"payload\": {\n    \"count\": 3,\n    \"keys\": [\"raw_dir\", \"movie_dir\", \"handbrake_preset_bluray\"]\n  }\n}\n
"},{"location":"api/websocket/#settings_scripts_updated","title":"SETTINGS_SCRIPTS_UPDATED","text":"

Skript ge\u00e4ndert (created|updated|deleted|reordered).

"},{"location":"api/websocket/#settings_script_chains_updated","title":"SETTINGS_SCRIPT_CHAINS_UPDATED","text":"

Skript-Kette ge\u00e4ndert (created|updated|deleted|reordered).

"},{"location":"api/websocket/#user_presets_updated","title":"USER_PRESETS_UPDATED","text":"

User-Preset ge\u00e4ndert (created|updated|deleted).

"},{"location":"api/websocket/#cron_jobs_updated","title":"CRON_JOBS_UPDATED","text":"

Cron-Config ge\u00e4ndert (created|updated|deleted).

"},{"location":"api/websocket/#cron_job_updated","title":"CRON_JOB_UPDATED","text":"

Laufzeitstatus eines Cron-Jobs ge\u00e4ndert.

{\n  \"type\": \"CRON_JOB_UPDATED\",\n  \"payload\": {\n    \"id\": 1,\n    \"lastRunStatus\": \"running\",\n    \"lastRunAt\": \"2026-03-10T10:00:00.000Z\",\n    \"nextRunAt\": null\n  }\n}\n
"},{"location":"api/websocket/#reconnect-verhalten","title":"Reconnect-Verhalten","text":"

useWebSocket verbindet bei Abbruch automatisch neu:

  • Retry-Intervall: 1500ms
  • Wiederverbindung bis Komponente unmounted wird
"},{"location":"architecture/","title":"Architektur","text":"

Ripster ist eine Client-Server-Anwendung mit REST + WebSocket.

"},{"location":"architecture/#systemuberblick","title":"System\u00fcberblick","text":"
graph TB\n    subgraph Browser[\"Browser (React)\"]\n        Dashboard[Dashboard]\n        Settings[Einstellungen]\n        History[Historie]\n    end\n\n    subgraph Backend[\"Node.js Backend\"]\n        API[REST API\\nExpress]\n        WS[WebSocket\\n/ws]\n        Pipeline[pipelineService]\n        Cron[cronService]\n        DB[(SQLite)]\n    end\n\n    subgraph Tools[\"Externe Tools\"]\n        MakeMKV[makemkvcon]\n        HandBrake[HandBrakeCLI]\n        MediaInfo[mediainfo]\n    end\n\n    Browser <-->|HTTP| API\n    Browser <-->|WebSocket| WS\n    Pipeline --> MakeMKV\n    Pipeline --> HandBrake\n    Pipeline --> MediaInfo\n    API --> DB\n    Pipeline --> DB\n    Cron --> DB
"},{"location":"architecture/#schichten","title":"Schichten","text":""},{"location":"architecture/#backend","title":"Backend","text":"
  • src/index.js (Bootstrapping, Routes, WS, Services)
  • src/routes/* (Pipeline, Settings, History, Crons)
  • src/services/* (Business-Logik)
  • src/db/database.js (Init/Migration)
  • src/utils/* (Parser, Dateifunktionen, Validierung)
"},{"location":"architecture/#frontend","title":"Frontend","text":"
  • App.jsx + pages/* (Dashboard, Settings, History)
  • components/* (Status-/Review-/Dialog-Komponenten)
  • api/client.js (REST-Client)
  • hooks/useWebSocket.js (WS-Reconnect)
"},{"location":"architecture/#weiterfuhrend","title":"Weiterf\u00fchrend","text":"
  • \u00dcbersicht
  • Backend-Services
  • Frontend-Komponenten
  • Datenbank
"},{"location":"architecture/backend/","title":"Backend-Services","text":"

Das Backend ist in Services aufgeteilt, die von Express-Routen orchestriert werden.

"},{"location":"architecture/backend/#pipelineservicejs","title":"pipelineService.js","text":"

Zentrale Workflow-Orchestrierung.

Aufgaben:

  • Pipeline-State-Machine + Persistenz (pipeline_state)
  • Disc-Analyse/Rip/Review/Encode
  • Queue-Management (Jobs + script|chain|wait Eintr\u00e4ge)
  • Retry/Re-Encode/Restart-Flows
  • WebSocket-Broadcasts f\u00fcr State/Progress/Queue

Wichtige Methoden:

  • analyzeDisc()
  • selectMetadata()
  • startPreparedJob()
  • confirmEncodeReview()
  • cancel()
  • retry()
  • reencodeFromRaw()
  • restartReviewFromRaw()
  • restartEncodeWithLastSettings()
  • resumeReadyToEncodeJob()
  • enqueueNonJobEntry(), reorderQueue(), removeQueueEntry()
"},{"location":"architecture/backend/#diskdetectionservicejs","title":"diskDetectionService.js","text":"

Pollt Laufwerk(e) und emittiert:

  • discInserted
  • discRemoved
  • error

Zusatz:

  • Modus auto oder explicit
  • heuristische mediaProfile-Erkennung (bluray/dvd/other)
  • rescanAndEmit() f\u00fcr manuellen Trigger
"},{"location":"architecture/backend/#settingsservicejs","title":"settingsService.js","text":"

Settings-Layer mit Validation/Serialisierung.

Features:

  • getCategorizedSettings() f\u00fcr UI-Form
  • setSettingValue() / setSettingsBulk()
  • profilspezifische Aufl\u00f6sung (resolveEffectiveToolSettings)
  • CLI-Config-Building f\u00fcr MakeMKV/HandBrake/MediaInfo
  • HandBrake-Preset-Liste via HandBrakeCLI -z
  • MakeMKV-Registration-Command aus makemkv_registration_key
"},{"location":"architecture/backend/#historyservicejs","title":"historyService.js","text":"

Historie + Dateioperationen.

Features:

  • Job-Liste/Detail inkl. Log-Tail
  • Orphan-RAW-Erkennung und Import
  • OMDb-Nachzuweisung
  • Dateil\u00f6schung (raw|movie|both)
  • Job-L\u00f6schung (none|raw|movie|both)
"},{"location":"architecture/backend/#cronservicejs","title":"cronService.js","text":"

Integriertes Cron-System ohne externe Parser-Library.

Features:

  • 5-Feld-Cron-Parser + nextRun-Berechnung
  • Quellen: script oder chain
  • Laufzeitlogs (cron_run_logs)
  • manuelles Triggern
  • WebSocket-Events: CRON_JOBS_UPDATED, CRON_JOB_UPDATED
"},{"location":"architecture/backend/#weitere-services","title":"Weitere Services","text":"
  • scriptService.js (CRUD + Test + Wrapper-Ausf\u00fchrung)
  • scriptChainService.js (CRUD + Step-Execution)
  • userPresetService.js (HandBrake User-Presets)
  • hardwareMonitorService.js (CPU/RAM/GPU/Storage)
  • websocketService.js (Client-Registry + Broadcast)
  • notificationService.js (PushOver)
  • logger.js (rotierende Datei-Logs)
"},{"location":"architecture/backend/#bootstrapping-srcindexjs","title":"Bootstrapping (src/index.js)","text":"

Beim Start:

  1. DB init/migrate
  2. Pipeline-Init
  3. Cron-Init
  4. Express-Routes + Error-Handler
  5. WebSocket-Server auf /ws
  6. Hardware-Monitoring-Init
  7. Disk-Detection-Start
"},{"location":"architecture/database/","title":"Datenbank","text":"

Ripster verwendet SQLite (backend/data/ripster.db).

"},{"location":"architecture/database/#tabellen","title":"Tabellen","text":"
settings_schema\nsettings_values\njobs\npipeline_state\nscripts\nscript_chains\nscript_chain_steps\nuser_presets\ncron_jobs\ncron_run_logs\n
"},{"location":"architecture/database/#jobs","title":"jobs","text":"

Speichert Pipeline-Lifecycle und Artefakte pro Job.

Zentrale Felder:

  • Metadaten: title, year, imdb_id, poster_url, omdb_json, selected_from_omdb
  • Laufzeit: start_time, end_time, status, last_state
  • Pfade: raw_path, output_path, encode_input_path
  • Tool-Ausgaben: makemkv_info_json, handbrake_info_json, mediainfo_info_json, encode_plan_json
  • Kontrolle: encode_review_confirmed, rip_successful, error_message
  • Audit: created_at, updated_at
"},{"location":"architecture/database/#pipeline_state","title":"pipeline_state","text":"

Singleton-Tabelle (id = 1) f\u00fcr aktiven Snapshot:

  • state
  • active_job_id
  • progress
  • eta
  • status_text
  • context_json
  • updated_at
"},{"location":"architecture/database/#settings_schema-settings_values","title":"settings_schema + settings_values","text":"
  • settings_schema: Definition (Typ, Default, Validation, Reihenfolge)
  • settings_values: aktueller Wert pro Key
"},{"location":"architecture/database/#scripts-script_chains-script_chain_steps","title":"scripts, script_chains, script_chain_steps","text":"
  • scripts: Shell-Skripte (name, script_body, order_index)
  • script_chains: Ketten (name, order_index)
  • script_chain_steps: Schritte je Kette
  • step_type: script oder wait
  • script_id oder wait_seconds
"},{"location":"architecture/database/#user_presets","title":"user_presets","text":"

Benannte HandBrake-Preset-Sets:

  • name
  • media_type (bluray|dvd|other|all)
  • handbrake_preset
  • extra_args
  • description
"},{"location":"architecture/database/#cron_jobs-cron_run_logs","title":"cron_jobs + cron_run_logs","text":"
  • cron_jobs: Zeitplan + Status
  • cron_run_logs: einzelne L\u00e4ufe
  • status: running|success|error
  • output
  • error_message
"},{"location":"architecture/database/#migrationrecovery","title":"Migration/Recovery","text":"

Beim Start werden Schema und Settings-Metadaten automatisch abgeglichen.

Bei korruptem SQLite-File:

  1. Datei wird nach backend/data/corrupt-backups/ verschoben
  2. neue DB wird initialisiert
  3. Schema wird neu aufgebaut
"},{"location":"architecture/database/#direkte-inspektion","title":"Direkte Inspektion","text":"
sqlite3 backend/data/ripster.db\n\n.mode table\nSELECT id, status, title, created_at FROM jobs ORDER BY created_at DESC;\nSELECT key, value FROM settings_values ORDER BY key;\n
"},{"location":"architecture/frontend/","title":"Frontend-Komponenten","text":"

Frontend: React + PrimeReact + Vite.

"},{"location":"architecture/frontend/#hauptseiten","title":"Hauptseiten","text":""},{"location":"architecture/frontend/#dashboardpagejsx","title":"DashboardPage.jsx","text":"

Pipeline-Steuerung:

  • Status/Progress/ETA
  • Metadaten-Dialog
  • Playlist-Entscheidung
  • Review-Panel
  • Queue-Interaktion (reorder/add/remove)
  • Job-Aktionen (Start/Cancel/Retry/Re-Encode)
  • Hardware-Monitoring-Anzeige
"},{"location":"architecture/frontend/#settingspagejsx","title":"SettingsPage.jsx","text":"

Konfiguration:

  • dynamisches Settings-Formular (DynamicSettingsForm)
  • Skripte/Ketten inkl. Reorder/Test
  • User-Presets
  • Cron-Jobs (CronJobsTab)
"},{"location":"architecture/frontend/#historypagejsx","title":"HistoryPage.jsx","text":"

Historie:

  • Job-Liste/Filter
  • Job-Details + Logs
  • OMDb-Nachzuweisung
  • Re-Encode/Restart-Workflows
"},{"location":"architecture/frontend/#wichtige-komponenten","title":"Wichtige Komponenten","text":"
  • PipelineStatusCard.jsx
  • MetadataSelectionDialog.jsx
  • MediaInfoReviewPanel.jsx
  • JobDetailDialog.jsx
  • CronJobsTab.jsx
"},{"location":"architecture/frontend/#api-client-apiclientjs","title":"API-Client (api/client.js)","text":"
  • zentraler request() mit JSON-Handling
  • Fehlerobjekt aus API wird auf Error(message) gemappt
  • VITE_API_BASE default /api
"},{"location":"architecture/frontend/#websocket-hooksusewebsocketjs","title":"WebSocket (hooks/useWebSocket.js)","text":"
  • URL: VITE_WS_URL oder automatisch ws(s)://<host>/ws
  • Auto-Reconnect mit 1500ms Intervall

In App.jsx werden u. a. verarbeitet:

  • PIPELINE_STATE_CHANGED
  • PIPELINE_PROGRESS
  • PIPELINE_QUEUE_CHANGED
  • DISC_DETECTED / DISC_REMOVED
  • HARDWARE_MONITOR_UPDATE
"},{"location":"architecture/frontend/#buildrun","title":"Build/Run","text":"
# dev\nnpm run dev --prefix frontend\n\n# prod build\nnpm run build --prefix frontend\n
"},{"location":"architecture/overview/","title":"Architektur-\u00dcbersicht","text":""},{"location":"architecture/overview/#kernprinzipien","title":"Kernprinzipien","text":""},{"location":"architecture/overview/#event-getriebene-pipeline","title":"Event-getriebene Pipeline","text":"

pipelineService h\u00e4lt einen Snapshot der State-Machine und broadcastet \u00c4nderungen sofort via WebSocket.

State-\u00c4nderung -> PIPELINE_STATE_CHANGED/PIPELINE_PROGRESS -> Frontend-Update\n
"},{"location":"architecture/overview/#service-layer","title":"Service-Layer","text":"
Route -> Service -> DB/Tool-Execution\n

Routes enthalten kaum Business-Logik.

"},{"location":"architecture/overview/#schema-getriebene-settings","title":"Schema-getriebene Settings","text":"

Settings sind DB-schema-getrieben (settings_schema + settings_values), UI rendert dynamisch aus diesen Daten.

"},{"location":"architecture/overview/#echtzeit-kommunikation","title":"Echtzeit-Kommunikation","text":"

WebSocket l\u00e4uft auf /ws.

Wichtige Events:

  • PIPELINE_STATE_CHANGED, PIPELINE_PROGRESS, PIPELINE_QUEUE_CHANGED
  • DISC_DETECTED, DISC_REMOVED
  • HARDWARE_MONITOR_UPDATE
  • SETTINGS_UPDATED, SETTINGS_BULK_UPDATED
  • SETTINGS_SCRIPTS_UPDATED, SETTINGS_SCRIPT_CHAINS_UPDATED, USER_PRESETS_UPDATED
  • CRON_JOBS_UPDATED, CRON_JOB_UPDATED
  • PIPELINE_ERROR, DISK_DETECTION_ERROR
"},{"location":"architecture/overview/#prozessausfuhrung","title":"Prozessausf\u00fchrung","text":"

Externe Tools werden als Child-Processes gestartet (processRunner):

  • Streaming von stdout/stderr
  • Progress-Parsing (progressParsers.js)
  • kontrollierter Abbruch (SIGINT/SIGKILL-Fallback)
"},{"location":"architecture/overview/#persistenz","title":"Persistenz","text":"

SQLite-Datei: backend/data/ripster.db

Kern-Tabellen:

  • jobs, pipeline_state
  • settings_schema, settings_values
  • scripts, script_chains, script_chain_steps
  • user_presets
  • cron_jobs, cron_run_logs

Beim Start werden Schema und Settings-Migrationen automatisch ausgef\u00fchrt.

"},{"location":"architecture/overview/#fehlerbehandlung","title":"Fehlerbehandlung","text":"

Zentrales Error-Handling liefert:

{\n  \"error\": {\n    \"message\": \"...\",\n    \"statusCode\": 400,\n    \"reqId\": \"...\",\n    \"details\": []\n  }\n}\n

Fehlgeschlagene Jobs bleiben in der Historie (ERROR oder CANCELLED) und k\u00f6nnen erneut gestartet werden.

"},{"location":"architecture/overview/#cors-runtime-konfig","title":"CORS & Runtime-Konfig","text":"
  • CORS_ORIGIN default: *
  • LOG_LEVEL default: info
  • DB-/Log-Pfade \u00fcber DB_PATH/LOG_DIR konfigurierbar
"},{"location":"configuration/","title":"Konfiguration","text":"
  • Einstellungsreferenz

    Alle verf\u00fcgbaren Einstellungen mit Typen, Standardwerten und Beschreibungen.

    Einstellungsreferenz

  • Umgebungsvariablen

    Umgebungsvariablen f\u00fcr Backend und Frontend.

    Umgebungsvariablen

"},{"location":"configuration/environment/","title":"Umgebungsvariablen","text":"

Umgebungsvariablen steuern Backend/Vite au\u00dferhalb der DB-basierten UI-Settings.

"},{"location":"configuration/environment/#backend-backendenv","title":"Backend (backend/.env)","text":"Variable Default (Code) Beschreibung PORT 3001 Express-Port DB_PATH backend/data/ripster.db SQLite-Datei (relativ zu backend/) LOG_DIR backend/logs Fallback-Logverzeichnis (wenn log_dir-Setting nicht gesetzt/lesbar) CORS_ORIGIN * CORS-Origin f\u00fcr API LOG_LEVEL info debug, info, warn, error

Beispiel:

PORT=3001\nDB_PATH=/var/lib/ripster/ripster.db\nLOG_DIR=/var/log/ripster\nCORS_ORIGIN=http://192.168.1.50:5173\nLOG_LEVEL=info\n

Hinweis: backend/.env.example enth\u00e4lt bewusst dev-freundliche Werte (z. B. lokaler CORS_ORIGIN).

"},{"location":"configuration/environment/#frontend-frontendenv","title":"Frontend (frontend/.env)","text":"Variable Default Beschreibung VITE_API_BASE /api API-Basis f\u00fcr Fetch-Client VITE_WS_URL automatisch aus window.location + /ws Optional explizite WebSocket-URL VITE_PUBLIC_ORIGIN leer \u00d6ffentliche Vite-Origin (Remote-Dev) VITE_ALLOWED_HOSTS true Komma-separierte Hostliste f\u00fcr Vite allowedHosts VITE_HMR_PROTOCOL abgeleitet aus VITE_PUBLIC_ORIGIN HMR-Protokoll (ws/wss) VITE_HMR_HOST abgeleitet aus VITE_PUBLIC_ORIGIN HMR-Host VITE_HMR_CLIENT_PORT abgeleitet aus VITE_PUBLIC_ORIGIN HMR-Client-Port

Beispiele:

# lokal (mit Vite-Proxy)\nVITE_API_BASE=/api\n
# remote dev\nVITE_API_BASE=http://192.168.1.50:3001/api\nVITE_WS_URL=ws://192.168.1.50:3001/ws\nVITE_PUBLIC_ORIGIN=http://192.168.1.50:5173\nVITE_ALLOWED_HOSTS=192.168.1.50,ripster.local\nVITE_HMR_PROTOCOL=ws\nVITE_HMR_HOST=192.168.1.50\nVITE_HMR_CLIENT_PORT=5173\n
"},{"location":"configuration/environment/#prioritat","title":"Priorit\u00e4t","text":"
  1. Prozess-Umgebungsvariablen
  2. .env
  3. Code-Defaults
"},{"location":"configuration/settings-reference/","title":"Einstellungsreferenz","text":"

Alle Settings liegen in settings_schema/settings_values und werden \u00fcber die UI verwaltet.

"},{"location":"configuration/settings-reference/#profil-system","title":"Profil-System","text":"

Ripster arbeitet mit Media-Profilen:

  • bluray
  • dvd
  • other

Viele Tool-/Pfad-Settings existieren als Profil-Varianten (*_bluray, *_dvd, *_other).

Wichtig:

  • F\u00fcr raw_dir, movie_dir und die zugeh\u00f6rigen *_owner-Keys gibt es kein Cross-Profil-Fallback.
  • F\u00fcr viele Tool-Keys werden profilspezifische Varianten bevorzugt.
"},{"location":"configuration/settings-reference/#template-platzhalter","title":"Template-Platzhalter","text":"

Datei-/Ordner-Templates unterst\u00fctzen:

  • ${title}
  • ${year}
  • ${imdbId}

Nicht gesetzte Werte werden zu unknown.

"},{"location":"configuration/settings-reference/#kategorie-pfade","title":"Kategorie: Pfade","text":"Key Typ Default raw_dir path data/output/raw raw_dir_bluray path null raw_dir_dvd path null raw_dir_other path null raw_dir_bluray_owner string null raw_dir_dvd_owner string null raw_dir_other_owner string null movie_dir path data/output/movies movie_dir_bluray path null movie_dir_dvd path null movie_dir_other path null movie_dir_bluray_owner string null movie_dir_dvd_owner string null movie_dir_other_owner string null log_dir path data/logs"},{"location":"configuration/settings-reference/#kategorie-laufwerk","title":"Kategorie: Laufwerk","text":"Key Typ Default Hinweis drive_mode select auto auto oder explicit drive_device path /dev/sr0 bei explicit relevant makemkv_source_index number 0 MakeMKV Source-Index disc_poll_interval_ms number 4000 1000..60000"},{"location":"configuration/settings-reference/#kategorie-monitoring","title":"Kategorie: Monitoring","text":"Key Typ Default hardware_monitoring_enabled boolean true hardware_monitoring_interval_ms number 5000"},{"location":"configuration/settings-reference/#kategorie-tools-global","title":"Kategorie: Tools (global)","text":"Key Typ Default makemkv_command string makemkvcon makemkv_registration_key string null mediainfo_command string mediainfo makemkv_min_length_minutes number 60 handbrake_command string HandBrakeCLI handbrake_restart_delete_incomplete_output boolean true pipeline_max_parallel_jobs number 1"},{"location":"configuration/settings-reference/#blu-ray-spezifisch","title":"Blu-ray-spezifisch","text":"Key Typ Default mediainfo_extra_args_bluray string null makemkv_rip_mode_bluray select backup makemkv_analyze_extra_args_bluray string null makemkv_rip_extra_args_bluray string null handbrake_preset_bluray string H.264 MKV 1080p30 handbrake_extra_args_bluray string null output_extension_bluray select mkv filename_template_bluray string ${title} (${year}) output_folder_template_bluray string null"},{"location":"configuration/settings-reference/#dvd-spezifisch","title":"DVD-spezifisch","text":"Key Typ Default mediainfo_extra_args_dvd string null makemkv_rip_mode_dvd select mkv makemkv_analyze_extra_args_dvd string null makemkv_rip_extra_args_dvd string null handbrake_preset_dvd string H.264 MKV 480p30 handbrake_extra_args_dvd string null output_extension_dvd select mkv filename_template_dvd string ${title} (${year}) output_folder_template_dvd string null"},{"location":"configuration/settings-reference/#kategorie-metadaten","title":"Kategorie: Metadaten","text":"Key Typ Default omdb_api_key string null omdb_default_type select movie"},{"location":"configuration/settings-reference/#kategorie-benachrichtigungen-pushover","title":"Kategorie: Benachrichtigungen (PushOver)","text":"Key Typ Default pushover_enabled boolean false pushover_token string null pushover_user string null pushover_device string null pushover_title_prefix string Ripster pushover_priority number 0 pushover_timeout_ms number 7000 pushover_notify_metadata_ready boolean true pushover_notify_rip_started boolean true pushover_notify_encoding_started boolean true pushover_notify_job_finished boolean true pushover_notify_job_error boolean true pushover_notify_job_cancelled boolean true pushover_notify_reencode_started boolean true pushover_notify_reencode_finished boolean true"},{"location":"configuration/settings-reference/#entfernte-legacy-keys","title":"Entfernte Legacy-Keys","text":"

Diese Legacy-Keys werden bei Migration entfernt und sollten nicht mehr genutzt werden:

  • makemkv_backup_mode
  • mediainfo_extra_args
  • makemkv_rip_mode
  • makemkv_analyze_extra_args
  • makemkv_rip_extra_args
  • handbrake_preset
  • handbrake_extra_args
  • output_extension
  • filename_template
  • output_folder_template
  • pushover_notify_disc_detected
"},{"location":"deployment/","title":"Deployment","text":"
  • Entwicklungsumgebung

    Lokale Entwicklungsumgebung einrichten.

    Entwicklung

  • Produktion

    Ripster auf einem Server dauerhaft betreiben.

    Produktion

"},{"location":"deployment/development/","title":"Entwicklungsumgebung","text":""},{"location":"deployment/development/#voraussetzungen","title":"Voraussetzungen","text":"
  • Node.js >= 20.19.0
  • externe Tools installiert (makemkvcon, HandBrakeCLI, mediainfo)
"},{"location":"deployment/development/#schnellstart","title":"Schnellstart","text":"
./start.sh\n

Startet:

  • Backend (http://localhost:3001, mit nodemon)
  • Frontend (http://localhost:5173, mit Vite HMR)

Stoppen: Ctrl+C.

"},{"location":"deployment/development/#manuell","title":"Manuell","text":""},{"location":"deployment/development/#backend","title":"Backend","text":"
cd backend\nnpm install\nnpm run dev\n
"},{"location":"deployment/development/#frontend","title":"Frontend","text":"
cd frontend\nnpm install\nnpm run dev\n
"},{"location":"deployment/development/#vite-proxy-dev","title":"Vite-Proxy (Dev)","text":"

frontend/vite.config.js proxied standardm\u00e4\u00dfig:

  • /api -> http://127.0.0.1:3001
  • /ws -> ws://127.0.0.1:3001
"},{"location":"deployment/development/#remote-dev-optional","title":"Remote-Dev (optional)","text":"

Beispiel frontend/.env.local:

VITE_API_BASE=http://192.168.1.100:3001/api\nVITE_WS_URL=ws://192.168.1.100:3001/ws\nVITE_PUBLIC_ORIGIN=http://192.168.1.100:5173\nVITE_ALLOWED_HOSTS=192.168.1.100,ripster.local\nVITE_HMR_PROTOCOL=ws\nVITE_HMR_HOST=192.168.1.100\nVITE_HMR_CLIENT_PORT=5173\n
"},{"location":"deployment/development/#nutzliche-kommandos","title":"N\u00fctzliche Kommandos","text":"
# Root dev (backend + frontend)\nnpm run dev\n\n# einzeln\nnpm run dev:backend\nnpm run dev:frontend\n\n# Frontend Build\nnpm run build:frontend\n
"},{"location":"deployment/development/#deploy-script-optional","title":"Deploy-Script (optional)","text":"

deploy-ripster.sh synchronisiert den lokalen Stand auf einen Remote-Host per rsync/SSH und sch\u00fctzt backend/data.

"},{"location":"deployment/production/","title":"Produktions-Deployment","text":""},{"location":"deployment/production/#empfohlene-architektur","title":"Empfohlene Architektur","text":"
Client\n  -> nginx (Reverse Proxy + statisches Frontend)\n    -> Backend API/WebSocket (Node.js, Port 3001)\n

Wichtig: Das Backend serviert im aktuellen Stand keine frontend/dist-Dateien automatisch.

"},{"location":"deployment/production/#1-frontend-builden","title":"1) Frontend builden","text":"
cd frontend\nnpm install\nnpm run build\n

Artefakte liegen in frontend/dist/.

"},{"location":"deployment/production/#2-backend-als-systemd-service","title":"2) Backend als systemd-Service","text":"

Beispiel /etc/systemd/system/ripster-backend.service:

[Unit]\nDescription=Ripster Backend\nAfter=network.target\n\n[Service]\nType=simple\nUser=ripster\nWorkingDirectory=/opt/ripster/backend\nExecStart=/usr/bin/env node src/index.js\nRestart=on-failure\nRestartSec=5\nEnvironment=NODE_ENV=production\nEnvironment=PORT=3001\nEnvironment=LOG_LEVEL=info\n\n[Install]\nWantedBy=multi-user.target\n

Aktivieren:

sudo systemctl daemon-reload\nsudo systemctl enable --now ripster-backend\nsudo systemctl status ripster-backend\n
"},{"location":"deployment/production/#3-nginx-konfigurieren","title":"3) nginx konfigurieren","text":"

Beispiel /etc/nginx/sites-available/ripster:

server {\n    listen 80;\n    server_name ripster.local;\n\n    root /opt/ripster/frontend/dist;\n    index index.html;\n\n    location / {\n        try_files $uri $uri/ /index.html;\n    }\n\n    location /api/ {\n        proxy_pass http://127.0.0.1:3001;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n    }\n\n    location /ws {\n        proxy_pass http://127.0.0.1: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

Aktivieren:

sudo ln -s /etc/nginx/sites-available/ripster /etc/nginx/sites-enabled/\nsudo nginx -t\nsudo systemctl reload nginx\n
"},{"location":"deployment/production/#datenbank-backup","title":"Datenbank-Backup","text":"
sqlite3 /opt/ripster/backend/data/ripster.db \\\n  \".backup '/var/backups/ripster-$(date +%Y%m%d).db'\"\n
"},{"location":"deployment/production/#sicherheit","title":"Sicherheit","text":"
  • Ripster hat keine eingebaute Authentifizierung.
  • F\u00fcr externen Zugriff mindestens Basic Auth + TLS + Netzwerksegmentierung/VPN einsetzen.
  • Secrets nicht ins Repo committen (.env, Settings-Felder).
"},{"location":"getting-started/","title":"Erste Schritte","text":"

Dieser Abschnitt f\u00fchrt dich durch die Installation und Einrichtung von Ripster.

"},{"location":"getting-started/#uberblick","title":"\u00dcberblick","text":"
  • :material-list-check: Voraussetzungen

    Systemanforderungen und externe Tools, die vor der Installation ben\u00f6tigt werden.

    Voraussetzungen pr\u00fcfen

  • Installation

    Schritt-f\u00fcr-Schritt-Anleitung zur Installation von Ripster.

    Installation starten

  • Konfiguration

    Einrichten von Pfaden, API-Keys und Encoding-Presets.

    Konfigurieren

  • Schnellstart

    Rippe deinen ersten Film in wenigen Minuten.

    Loslegen

"},{"location":"getting-started/configuration/","title":"Konfiguration","text":"

Die Hauptkonfiguration erfolgt \u00fcber die UI (Settings) und wird in SQLite gespeichert.

"},{"location":"getting-started/configuration/#pflichteinstellungen-vor-dem-ersten-rip","title":"Pflichteinstellungen vor dem ersten Rip","text":""},{"location":"getting-started/configuration/#1-pfade","title":"1) Pfade","text":"Einstellung Beschreibung Beispiel raw_dir Basisverzeichnis f\u00fcr RAW-Rips /mnt/ripster/raw movie_dir Basisverzeichnis f\u00fcr finale Encodes /mnt/ripster/movies log_dir Verzeichnis f\u00fcr Prozess-/Backend-Logs /mnt/ripster/logs

Optional profilspezifisch:

  • raw_dir_bluray, raw_dir_dvd, raw_dir_other
  • movie_dir_bluray, movie_dir_dvd, movie_dir_other
"},{"location":"getting-started/configuration/#2-tools","title":"2) Tools","text":"Einstellung Standard makemkv_command makemkvcon handbrake_command HandBrakeCLI mediainfo_command mediainfo"},{"location":"getting-started/configuration/#3-omdb","title":"3) OMDb","text":"Einstellung Beschreibung omdb_api_key API-Key von omdbapi.com omdb_default_type movie, series, episode"},{"location":"getting-started/configuration/#encode-konfiguration-wichtig","title":"Encode-Konfiguration (wichtig)","text":"

Ripster arbeitet profilspezifisch, typischerweise \u00fcber:

  • Blu-ray: handbrake_preset_bluray, handbrake_extra_args_bluray, output_extension_bluray, filename_template_bluray
  • DVD: handbrake_preset_dvd, handbrake_extra_args_dvd, output_extension_dvd, filename_template_dvd
"},{"location":"getting-started/configuration/#template-platzhalter","title":"Template-Platzhalter","text":"

Verf\u00fcgbar in filename_template_* und output_folder_template_*:

  • ${title}
  • ${year}
  • ${imdbId}

Beispiel:

${title} (${year})\n-> Inception (2010).mkv\n
"},{"location":"getting-started/configuration/#makemkv-spezifisch","title":"MakeMKV-spezifisch","text":"Einstellung Standard Hinweis makemkv_min_length_minutes 60 Kandidaten-Filter makemkv_rip_mode_bluray backup mkv oder backup makemkv_rip_mode_dvd mkv mkv oder backup makemkv_registration_key leer optional, wird via makemkvcon reg gesetzt"},{"location":"getting-started/configuration/#monitoring-queue","title":"Monitoring & Queue","text":"Einstellung Standard hardware_monitoring_enabled true hardware_monitoring_interval_ms 5000 pipeline_max_parallel_jobs 1"},{"location":"getting-started/configuration/#pushover-optional","title":"PushOver (optional)","text":"

Basis:

  • pushover_enabled
  • pushover_token
  • pushover_user

Zus\u00e4tzlich pro Event ein/aus (z. B. pushover_notify_job_finished).

"},{"location":"getting-started/configuration/#verwandte-doku","title":"Verwandte Doku","text":"
  • Einstellungsreferenz
  • Umgebungsvariablen
"},{"location":"getting-started/installation/","title":"Installation","text":""},{"location":"getting-started/installation/#repository-klonen","title":"Repository klonen","text":"
git clone https://github.com/YOUR_GITHUB_USERNAME/ripster.git\ncd ripster\n
"},{"location":"getting-started/installation/#dev-start-empfohlen","title":"Dev-Start (empfohlen)","text":"
./start.sh\n

start.sh:

  1. pr\u00fcft Node-Version (>= 20.19.0)
  2. installiert Dependencies (Root/Backend/Frontend)
  3. startet Backend + Frontend parallel

Danach:

  • Backend: http://localhost:3001
  • Frontend: http://localhost:5173

Stoppen: mit Ctrl+C im laufenden Terminal.

"},{"location":"getting-started/installation/#manuell-starten","title":"Manuell starten","text":"
npm install\nnpm --prefix backend install\nnpm --prefix frontend install\nnpm run dev\n

Oder getrennt:

npm run dev:backend\nnpm run dev:frontend\n
"},{"location":"getting-started/installation/#optional-env-dateien-anlegen","title":"Optional: .env-Dateien anlegen","text":""},{"location":"getting-started/installation/#backend","title":"Backend","text":"
cp backend/.env.example backend/.env\n

Beispiel:

PORT=3001\nDB_PATH=./data/ripster.db\nLOG_DIR=./logs\nCORS_ORIGIN=http://localhost:5173\nLOG_LEVEL=info\n
"},{"location":"getting-started/installation/#frontend","title":"Frontend","text":"
cp frontend/.env.example frontend/.env\n

Beispiel:

VITE_API_BASE=/api\n# optional:\n# VITE_WS_URL=ws://localhost:3001/ws\n
"},{"location":"getting-started/installation/#datenbank","title":"Datenbank","text":"

SQLite wird automatisch beim Backend-Start initialisiert:

backend/data/ripster.db\n

Schema-Quelle: db/schema.sql

"},{"location":"getting-started/installation/#nachste-schritte","title":"N\u00e4chste Schritte","text":"
  1. Browser \u00f6ffnen: http://localhost:5173
  2. In Settings Pfade/Tools/API-Keys pr\u00fcfen
  3. Erste Disc einlegen und Workflow starten
"},{"location":"getting-started/prerequisites/","title":"Voraussetzungen","text":"

Bevor du Ripster installierst, stelle sicher, dass folgende Software auf deinem System verf\u00fcgbar ist.

"},{"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":"

Ripster ben\u00f6tigt Node.js >= 20.19.0.

nvm (empfohlen)Ubuntu/DebianmacOS (Homebrew)
# 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
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
brew install node@20\nnode --version  # v20.x.x\n
"},{"location":"getting-started/prerequisites/#externe-tools","title":"Externe Tools","text":""},{"location":"getting-started/prerequisites/#makemkv","title":"MakeMKV","text":"

Lizenz erforderlich

MakeMKV ist f\u00fcr den pers\u00f6nlichen Gebrauch kostenlos (Beta-Lizenz), ben\u00f6tigt aber eine g\u00fcltige Lizenz.

# 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

MakeMKV Download

"},{"location":"getting-started/prerequisites/#handbrake-cli","title":"HandBrake CLI","text":"
# 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

HandBrake Download

"},{"location":"getting-started/prerequisites/#mediainfo","title":"MediaInfo","text":"
# Ubuntu/Debian\nsudo apt-get install mediainfo\n\n# macOS\nbrew install mediainfo\n\n# Version pr\u00fcfen\nmediainfo --Version\n
"},{"location":"getting-started/prerequisites/#disc-laufwerk","title":"Disc-Laufwerk","text":"

Ripster ben\u00f6tigt ein physisches DVD- oder Blu-ray-Laufwerk.

LibDriveIO-Modus erforderlich

Das Laufwerk muss im LibDriveIO-Modus betrieben werden \u2013 MakeMKV greift direkt auf Rohdaten des Laufwerks zu. Ohne diesen Modus k\u00f6nnen verschl\u00fcsselte Blu-rays (insbesondere UHD) nicht gelesen werden.

Nicht alle Laufwerke unterst\u00fctzen den direkten Zugriff. Eine Anleitung zur Einrichtung und Liste kompatibler Laufwerke findet sich im MakeMKV-Forum.

# Laufwerk pr\u00fcfen\nls /dev/sr*\n# oder\nlsblk | grep rom\n\n# Laufwerk-Berechtigungen setzen (erforderlich f\u00fcr LibDriveIO)\nsudo chmod a+rw /dev/sr0\n

Blu-ray unter Linux

MakeMKV bringt mit LibDriveIO eine eigene Entschl\u00fcsselung mit \u2013 externe Bibliotheken wie libaacs sind in der Regel nicht erforderlich.

"},{"location":"getting-started/prerequisites/#omdb-api-key","title":"OMDb API-Key","text":"

Ripster verwendet die OMDb API f\u00fcr Filmmetadaten.

  1. Registriere dich kostenlos auf omdbapi.com
  2. Best\u00e4tige deine E-Mail-Adresse
  3. Notiere deinen API-Key \u2013 du gibst ihn sp\u00e4ter in den Einstellungen ein
"},{"location":"getting-started/prerequisites/#optionale-voraussetzungen","title":"Optionale Voraussetzungen","text":""},{"location":"getting-started/prerequisites/#pushover-benachrichtigungen","title":"PushOver (Benachrichtigungen)","text":"

F\u00fcr mobile Push-Benachrichtigungen bei Fertigstellung oder Fehlern:

  • App kaufen auf pushover.net (~5 USD einmalig)
  • User Key und API Token notieren
"},{"location":"getting-started/prerequisites/#ssh-zugang-deployment","title":"SSH-Zugang (Deployment)","text":"

F\u00fcr Remote-Deployment via deploy-ripster.sh:

# sshpass installieren\nsudo apt-get install sshpass\n
"},{"location":"getting-started/prerequisites/#checkliste","title":"Checkliste","text":"
  • [ ] Node.js >= 20.19.0 installiert (node --version)
  • [ ] makemkvcon installiert (makemkvcon --version)
  • [ ] HandBrakeCLI installiert (HandBrakeCLI --version)
  • [ ] mediainfo installiert (mediainfo --Version)
  • [ ] DVD/Blu-ray Laufwerk vorhanden (ls /dev/sr*)
  • [ ] OMDb API-Key beschafft
"},{"location":"getting-started/quickstart/","title":"Schnellstart \u2013 Erster kompletter Job","text":"

Diese Seite f\u00fchrt durch den typischen ersten Lauf.

"},{"location":"getting-started/quickstart/#1-starten","title":"1) Starten","text":"
cd ripster\n./start.sh\n

\u00d6ffne http://localhost:5173.

"},{"location":"getting-started/quickstart/#2-disc-einlegen","title":"2) Disc einlegen","text":"

Pipeline wechselt auf DISC_DETECTED.

Falls n\u00f6tig manuell neu scannen:

curl -X POST http://localhost:3001/api/pipeline/rescan-disc\n
"},{"location":"getting-started/quickstart/#3-analyse-starten","title":"3) Analyse starten","text":"

Klicke im Dashboard auf Analyse starten.

Intern:

  • Job wird angelegt
  • MakeMKV-Analyse l\u00e4uft (ANALYZING)
  • UI wechselt in Metadatenauswahl (METADATA_SELECTION)
"},{"location":"getting-started/quickstart/#4-metadaten-bestatigen","title":"4) Metadaten best\u00e4tigen","text":"

Im Dialog:

  • OMDb-Ergebnis w\u00e4hlen oder manuell eintragen
  • bei Playlist-Abfrage ggf. selectedPlaylist w\u00e4hlen

Nach Best\u00e4tigung startet Ripster automatisch weiter.

"},{"location":"getting-started/quickstart/#5-pipeline-pfade","title":"5) Pipeline-Pfade","text":"

Abh\u00e4ngig von Job/RAW-Situation:

  • kein RAW vorhanden -> RIPPING
  • RAW vorhanden -> MEDIAINFO_CHECK
  • mehrdeutige Playlist -> WAITING_FOR_USER_DECISION

Wenn Parallel-Limit erreicht ist, wird der Job in die Queue eingereiht.

"},{"location":"getting-started/quickstart/#6-review-ready_to_encode","title":"6) Review (READY_TO_ENCODE)","text":"

Im Review-Panel:

  • Titel ausw\u00e4hlen (falls mehrere)
  • Audio-/Subtitle-Tracks ausw\u00e4hlen
  • optional User-Preset anwenden
  • optional Pre-/Post-Skripte und Ketten hinzuf\u00fcgen

Mit Encoding starten wird confirm-encode + Start ausgel\u00f6st.

"},{"location":"getting-started/quickstart/#7-encoding-encoding","title":"7) Encoding (ENCODING)","text":"

W\u00e4hrend Encoding:

  • Live-Fortschritt/ETA \u00fcber WebSocket
  • Pre-Encode-Ausf\u00fchrungen laufen vor HandBrake
  • Post-Encode-Ausf\u00fchrungen laufen nach HandBrake

Wichtig:

  • Pre-Encode-Fehler -> Job endet in ERROR
  • Post-Encode-Fehler -> Job kann FINISHED bleiben, aber mit Fehlerhinweis im Status/Log
"},{"location":"getting-started/quickstart/#8-abschluss-finished","title":"8) Abschluss (FINISHED)","text":"

Ergebnis:

  • Ausgabe in movie_dir (ggf. profilspezifisch)
  • Job in Historie sichtbar
  • Logs im konfigurierten log_dir
"},{"location":"getting-started/quickstart/#nutzliche-api-shortcuts","title":"N\u00fctzliche API-Shortcuts","text":"
# Pipeline-Snapshot\ncurl http://localhost:3001/api/pipeline/state\n\n# Queue-Snapshot\ncurl http://localhost:3001/api/pipeline/queue\n\n# Jobs\ncurl http://localhost:3001/api/history\n
"},{"location":"pipeline/","title":"Pipeline","text":"

Der Pipeline-Bereich beschreibt den Kern-Workflow von Ripster.

  • Workflow & Zust\u00e4nde

    Zust\u00e4nde, \u00dcberg\u00e4nge und Queue-Verhalten.

    Workflow

  • Encode-Planung

    Wie Titel/Tracks f\u00fcr HandBrake vorbereitet und best\u00e4tigt werden.

    Encoding

  • Playlist-Analyse

    Bewertung mehrdeutiger Blu-ray-Playlists und manuelle Entscheidung.

    Playlist-Analyse

  • Encode-Skripte (Pre & Post)

    Skripte/Ketten vor und nach dem Encode ausf\u00fchren.

    Encode-Skripte

"},{"location":"pipeline/encoding/","title":"Encode-Planung & Track-Auswahl","text":"

Ripster erzeugt vor dem Encode einen encodePlan und l\u00e4sst ihn im Review-Panel best\u00e4tigen.

"},{"location":"pipeline/encoding/#ablauf","title":"Ablauf","text":"
Quelle bestimmen (Disc/RAW)\n  -> HandBrake-Scan (--scan --json)\n  -> Plan erstellen (Titel, Audio, Untertitel)\n  -> READY_TO_ENCODE\n  -> Benutzer best\u00e4tigt Auswahl\n  -> finaler HandBrake-Aufruf\n
"},{"location":"pipeline/encoding/#review-inhalt-ready_to_encode","title":"Review-Inhalt (READY_TO_ENCODE)","text":"
  • ausw\u00e4hlbarer Encode-Titel
  • Audio-Track-Selektion
  • Untertitel-Track-Selektion inkl. Flags
  • burnIn
  • forced
  • defaultTrack
  • optionale User-Presets (HandBrake-Preset + Extra-Args)
  • optionale Pre-/Post-Skripte und Ketten
"},{"location":"pipeline/encoding/#bestatigung-confirm-encode","title":"Best\u00e4tigung (confirm-encode)","text":"

Typischer Payload:

{\n  \"selectedEncodeTitleId\": 1,\n  \"selectedTrackSelection\": {\n    \"1\": {\n      \"audioTrackIds\": [1, 2],\n      \"subtitleTrackIds\": [3]\n    }\n  },\n  \"selectedPreEncodeScriptIds\": [1],\n  \"selectedPostEncodeScriptIds\": [2],\n  \"selectedPreEncodeChainIds\": [3],\n  \"selectedPostEncodeChainIds\": [4],\n  \"selectedUserPresetId\": 5\n}\n

Ripster speichert die best\u00e4tigte Auswahl in jobs.encode_plan_json und markiert encode_review_confirmed = 1.

"},{"location":"pipeline/encoding/#handbrake-aufruf","title":"HandBrake-Aufruf","text":"

Grundstruktur:

HandBrakeCLI \\\n  -i <input> \\\n  -o <output> \\\n  -t <titleId> \\\n  -Z \"<preset>\" \\\n  <extra-args> \\\n  -a <audioTrackIds|none> \\\n  -s <subtitleTrackIds|none>\n

Untertitel-Flags werden bei Bedarf erg\u00e4nzt:

  • --subtitle-burned=<id>
  • --subtitle-default=<id>
  • --subtitle-forced=<id> oder --subtitle-forced
"},{"location":"pipeline/encoding/#pre-post-encode-ausfuhrungen","title":"Pre-/Post-Encode-Ausf\u00fchrungen","text":"
  • Pre-Encode l\u00e4uft vor HandBrake
  • Post-Encode l\u00e4uft nach HandBrake

Verhalten bei Fehlern:

  • Pre-Encode-Fehler: Job wird als ERROR beendet (Encode startet nicht)
  • Post-Encode-Fehler: Job kann FINISHED bleiben, enth\u00e4lt aber Fehlerhinweis/Script-Summary
"},{"location":"pipeline/encoding/#dateinamenordner","title":"Dateinamen/Ordner","text":"

Der finale Outputpfad wird aus Settings-Templates aufgebaut.

Platzhalter:

  • ${title}
  • ${year}
  • ${imdbId}

Ung\u00fcltige Dateizeichen werden sanitisiert.

"},{"location":"pipeline/playlist-analysis/","title":"Playlist-Analyse","text":"

Ripster analysiert bei Blu-ray-\u00e4hnlichen Quellen Playlists und fordert bei Mehrdeutigkeit eine manuelle Auswahl an.

"},{"location":"pipeline/playlist-analysis/#ziel","title":"Ziel","text":"

Erkennen, welche Playlist wahrscheinlich der Hauptfilm ist, statt versehentlich eine Fake-/Dummy-Playlist zu verwenden.

"},{"location":"pipeline/playlist-analysis/#eingabedaten","title":"Eingabedaten","text":"

Die Analyse basiert auf MakeMKV-Infos (u. a. Playlist-/Segment-Struktur, Laufzeiten, Titelzuordnung).

"},{"location":"pipeline/playlist-analysis/#auswertung-vereinfacht","title":"Auswertung (vereinfacht)","text":"

F\u00fcr Kandidaten werden u. a. ber\u00fccksichtigt:

  • Laufzeit
  • Segment-Reihenfolge
  • R\u00fcckw\u00e4rtsspr\u00fcnge/gro\u00dfe Spr\u00fcnge
  • Koh\u00e4renz linearer Segmentfolgen
  • Duplikatgruppen mit \u00e4hnlicher Laufzeit

Daraus entstehen:

  • candidates
  • evaluatedCandidates (inkl. Score/Label)
  • recommendation
  • manualDecisionRequired
"},{"location":"pipeline/playlist-analysis/#wann-muss-der-benutzer-entscheiden","title":"Wann muss der Benutzer entscheiden?","text":"

Wenn nach Filterung mehr als ein relevanter Kandidat \u00fcbrig bleibt, setzt Ripster manualDecisionRequired = true und wechselt auf:

  • WAITING_FOR_USER_DECISION

Dann muss eine Playlist best\u00e4tigt werden, bevor der Workflow weiterl\u00e4uft.

"},{"location":"pipeline/playlist-analysis/#konfigurationseinfluss","title":"Konfigurationseinfluss","text":"Key Wirkung makemkv_min_length_minutes Mindestlaufzeit f\u00fcr Kandidaten

Default ist aktuell 60 Minuten.

"},{"location":"pipeline/playlist-analysis/#ui-verhalten","title":"UI-Verhalten","text":"

Bei manueller Entscheidung zeigt das Dashboard Kandidaten inkl. Score/Bewertung und markiert eine Empfehlung.

Nach Best\u00e4tigung:

  • mit vorhandenem RAW -> zur\u00fcck zu MEDIAINFO_CHECK
  • ohne RAW -> Startpfad \u00fcber READY_TO_START/RIPPING
"},{"location":"pipeline/post-encode-scripts/","title":"Encode-Skripte (Pre & Post)","text":"

Ripster kann Skripte und Skript-Ketten vor und nach dem Encode ausf\u00fchren.

"},{"location":"pipeline/post-encode-scripts/#ablauf","title":"Ablauf","text":"
READY_TO_ENCODE\n  -> Pre-Encode Skripte/Ketten\n  -> HandBrake Encoding\n  -> Post-Encode Skripte/Ketten\n  -> FINISHED oder ERROR\n
"},{"location":"pipeline/post-encode-scripts/#auswahl-im-review","title":"Auswahl im Review","text":"

Im Review-Panel kannst du getrennt w\u00e4hlen:

  • selectedPreEncodeScriptIds
  • selectedPostEncodeScriptIds
  • selectedPreEncodeChainIds
  • selectedPostEncodeChainIds
"},{"location":"pipeline/post-encode-scripts/#fehlerverhalten","title":"Fehlerverhalten","text":"
  • Pre-Encode-Fehler stoppen die Kette und f\u00fchren zu ERROR.
  • Post-Encode-Fehler stoppen die restlichen Post-Schritte; Job kann dennoch FINISHED sein (mit Fehlerzusatz im Status/Log).
"},{"location":"pipeline/post-encode-scripts/#verfugbare-umgebungsvariablen","title":"Verf\u00fcgbare Umgebungsvariablen","text":"

Beim Script-Run werden gesetzt:

  • RIPSTER_SCRIPT_RUN_AT
  • RIPSTER_JOB_ID
  • RIPSTER_JOB_TITLE
  • RIPSTER_MODE
  • RIPSTER_INPUT_PATH
  • RIPSTER_OUTPUT_PATH
  • RIPSTER_RAW_PATH
  • RIPSTER_SCRIPT_ID
  • RIPSTER_SCRIPT_NAME
  • RIPSTER_SCRIPT_SOURCE
"},{"location":"pipeline/post-encode-scripts/#skript-ketten","title":"Skript-Ketten","text":"

Ketten unterst\u00fctzen zwei Step-Typen:

  • script (f\u00fchrt ein hinterlegtes Skript aus)
  • wait (wartet waitSeconds)

Bei Fehler in einem Script-Step wird die Kette abgebrochen.

"},{"location":"pipeline/post-encode-scripts/#testlaufe","title":"Testl\u00e4ufe","text":"
  • Skript testen: POST /api/settings/scripts/:id/test
  • Kette testen: POST /api/settings/script-chains/:id/test

Ergebnisse enthalten Erfolg/Exit-Code, Laufzeit und stdout/stderr.

"},{"location":"pipeline/workflow/","title":"Workflow & Zust\u00e4nde","text":"

Ripster steuert den Ablauf als State-Machine im pipelineService.

"},{"location":"pipeline/workflow/#zustandsdiagramm-vereinfacht","title":"Zustandsdiagramm (vereinfacht)","text":"
flowchart LR\n    IDLE --> DISC_DETECTED\n    DISC_DETECTED --> ANALYZING\n    ANALYZING --> METADATA_SELECTION\n    METADATA_SELECTION --> READY_TO_START\n    READY_TO_START --> RIPPING\n    READY_TO_START --> MEDIAINFO_CHECK\n    MEDIAINFO_CHECK --> WAITING_FOR_USER_DECISION\n    WAITING_FOR_USER_DECISION --> MEDIAINFO_CHECK\n    MEDIAINFO_CHECK --> READY_TO_ENCODE\n    READY_TO_ENCODE --> ENCODING\n    ENCODING --> FINISHED\n    ENCODING --> ERROR\n    RIPPING --> ERROR\n    RIPPING --> CANCELLED
"},{"location":"pipeline/workflow/#state-liste","title":"State-Liste","text":"State Bedeutung IDLE Wartet auf Disc DISC_DETECTED Disc erkannt ANALYZING MakeMKV-Analyse l\u00e4uft METADATA_SELECTION Benutzer w\u00e4hlt Metadaten WAITING_FOR_USER_DECISION Playlist-Auswahl n\u00f6tig READY_TO_START \u00dcbergangszustand vor Start RIPPING MakeMKV-Rip l\u00e4uft MEDIAINFO_CHECK Quelle/Tracks werden ausgewertet READY_TO_ENCODE Review ist bereit ENCODING HandBrake l\u00e4uft FINISHED erfolgreich abgeschlossen CANCELLED abgebrochen ERROR fehlgeschlagen"},{"location":"pipeline/workflow/#typische-pfade","title":"Typische Pfade","text":""},{"location":"pipeline/workflow/#standardfall-kein-vorhandenes-raw","title":"Standardfall (kein vorhandenes RAW)","text":"
  1. Disc erkannt
  2. Analyse + Metadaten
  3. RIPPING
  4. MEDIAINFO_CHECK
  5. READY_TO_ENCODE
  6. ENCODING
  7. FINISHED
"},{"location":"pipeline/workflow/#vorhandenes-raw","title":"Vorhandenes RAW","text":"

READY_TO_START springt direkt zu MEDIAINFO_CHECK (kein neuer Rip).

"},{"location":"pipeline/workflow/#mehrdeutige-blu-ray-playlist","title":"Mehrdeutige Blu-ray-Playlist","text":"

MEDIAINFO_CHECK -> WAITING_FOR_USER_DECISION bis Benutzer Playlist best\u00e4tigt.

"},{"location":"pipeline/workflow/#queue-verhalten","title":"Queue-Verhalten","text":"

Wenn pipeline_max_parallel_jobs erreicht ist:

  • Job-Aktionen werden als Queue-Eintr\u00e4ge abgelegt
  • Queue kann zus\u00e4tzlich Nicht-Job-Eintr\u00e4ge enthalten (script, chain, wait)
  • Reihenfolge ist per API/UI \u00e4nderbar
"},{"location":"pipeline/workflow/#abbruch-retry-restart","title":"Abbruch, Retry, Restart","text":"
  • cancel: laufenden Job abbrechen oder Queue-Eintrag entfernen
  • retry: Fehler-/Abbruch-Job neu starten
  • reencode: aus vorhandenem RAW neu encodieren
  • restart-review: Review aus RAW neu aufbauen
  • restart-encode: Encoding mit letzter best\u00e4tigter Auswahl neu starten
"},{"location":"tools/","title":"Externe Tools","text":"

Ripster ist ein Orchestrator \u2013 die eigentliche Arbeit erledigen diese bew\u00e4hrten Open-Source-Tools:

  • MakeMKV

    Disc-Analyse und Ripping. Erstellt MKV-Dateien oder vollst\u00e4ndige Backups.

    MakeMKV

  • HandBrake

    Video-Encoding mit umfangreichen Preset-Optionen.

    HandBrake

  • MediaInfo

    Analyse von Track-Informationen in Mediendateien.

    MediaInfo

"},{"location":"tools/handbrake/","title":"HandBrake","text":"

Ripster verwendet HandBrakeCLI f\u00fcr Scan und Encode.

"},{"location":"tools/handbrake/#verwendete-aufrufe","title":"Verwendete Aufrufe","text":""},{"location":"tools/handbrake/#scan-review-aufbau","title":"Scan (Review-Aufbau)","text":"
HandBrakeCLI --scan --json -i <input> -t 0\n
"},{"location":"tools/handbrake/#encode-vereinfacht","title":"Encode (vereinfacht)","text":"
HandBrakeCLI \\\n  -i <input> \\\n  -o <output> \\\n  -t <titleId> \\\n  -Z \"<preset>\" \\\n  <extra-args> \\\n  -a <audioTrackIds|none> \\\n  -s <subtitleTrackIds|none>\n

Optional erg\u00e4nzt Ripster:

  • --subtitle-burned=<id>
  • --subtitle-default=<id>
  • --subtitle-forced=<id> oder --subtitle-forced
"},{"location":"tools/handbrake/#presets-auslesen","title":"Presets auslesen","text":"

Ripster liest Presets mit:

HandBrakeCLI -z\n
"},{"location":"tools/handbrake/#relevante-settings","title":"Relevante Settings","text":"Key Bedeutung handbrake_command CLI-Binary handbrake_preset_bluray / handbrake_preset_dvd profilspezifisches Preset handbrake_extra_args_bluray / handbrake_extra_args_dvd profilspezifische Zusatzargumente output_extension_bluray / output_extension_dvd Ausgabeformat handbrake_restart_delete_incomplete_output unvollst\u00e4ndige Ausgabe bei Neustart l\u00f6schen"},{"location":"tools/handbrake/#fortschritts-parsing","title":"Fortschritts-Parsing","text":"

Ripster parst HandBrake-Stderr (Prozent/ETA/Detail) und sendet WebSocket-Progress (PIPELINE_PROGRESS).

"},{"location":"tools/handbrake/#troubleshooting","title":"Troubleshooting","text":"
  • Preset nicht gefunden: Preset-Namen mit HandBrakeCLI -z pr\u00fcfen
  • sehr langsames Encoding: Preset/Extra-Args pr\u00fcfen (z. B. --encoder-preset)
"},{"location":"tools/makemkv/","title":"MakeMKV","text":"

Ripster nutzt makemkvcon f\u00fcr Disc-Analyse und Rip.

"},{"location":"tools/makemkv/#verwendete-aufrufe","title":"Verwendete Aufrufe","text":""},{"location":"tools/makemkv/#analyse","title":"Analyse","text":"
makemkvcon -r info <source>\n

<source> ist typischerweise:

  • disc:<index> (Auto-Modus)
  • dev:/dev/sr0 (explicit)
  • file:<path> (Datei/Ordner-Analyse)
"},{"location":"tools/makemkv/#rip-mkv-modus","title":"Rip (MKV-Modus)","text":"
makemkvcon mkv <source> <title-or-all> <rawDir> [--minlength=...] [...extraArgs]\n
"},{"location":"tools/makemkv/#rip-backup-modus","title":"Rip (Backup-Modus)","text":"
makemkvcon backup <source> <rawDir> --decrypt\n
"},{"location":"tools/makemkv/#registrierungsschlussel-optional","title":"Registrierungsschl\u00fcssel (optional)","text":"

Wenn makemkv_registration_key gesetzt ist, f\u00fchrt Ripster vor Analyse/Rip aus:

makemkvcon reg <key>\n
"},{"location":"tools/makemkv/#relevante-settings","title":"Relevante Settings","text":"Key Bedeutung makemkv_command CLI-Binary makemkv_source_index Source-Index im Auto-Modus makemkv_min_length_minutes Mindestlaufzeitfilter makemkv_rip_mode_bluray / makemkv_rip_mode_dvd mkv oder backup makemkv_analyze_extra_args_bluray / _dvd Zusatzargs Analyse makemkv_rip_extra_args_bluray / _dvd Zusatzargs Rip"},{"location":"tools/makemkv/#hinweise","title":"Hinweise","text":"
  • Blu-ray-Backups werden oft f\u00fcr robuste Playlist-Analyse genutzt.
  • MakeMKV-Ausgaben werden geparst und als makemkvInfo im Job gespeichert.
"},{"location":"tools/mediainfo/","title":"MediaInfo","text":"

Ripster nutzt mediainfo zur JSON-Analyse von Medien-Dateien.

"},{"location":"tools/mediainfo/#aufruf","title":"Aufruf","text":"
mediainfo --Output=JSON <input>\n

Der Input ist typischerweise eine RAW-Datei oder ein vom Workflow gew\u00e4hlter Inputpfad.

"},{"location":"tools/mediainfo/#verwendung-in-ripster","title":"Verwendung in Ripster","text":"
  • Track-/Codec-Metadaten f\u00fcr Review-Plan
  • Fallback-Informationen in bestimmten Analysepfaden
  • Persistenz als mediainfoInfo im Job
"},{"location":"tools/mediainfo/#relevante-settings","title":"Relevante Settings","text":"Key Bedeutung mediainfo_command CLI-Binary mediainfo_extra_args_bluray / _dvd profilspezifische Zusatzargumente"},{"location":"tools/mediainfo/#troubleshooting","title":"Troubleshooting","text":"
  • JSON-Test: mediainfo --Output=JSON <datei>
  • unbekannte Sprache erscheint oft als und (undetermined)
"}]} \ No newline at end of file diff --git a/site/sitemap.xml b/site/sitemap.xml index 3a08097..863a7d1 100644 --- a/site/sitemap.xml +++ b/site/sitemap.xml @@ -2,126 +2,130 @@ https://mboehmlaender.github.io/ripster/ - 2026-03-05 + 2026-03-10 https://mboehmlaender.github.io/ripster/api/ - 2026-03-05 + 2026-03-10 + + + https://mboehmlaender.github.io/ripster/api/crons/ + 2026-03-10 https://mboehmlaender.github.io/ripster/api/history/ - 2026-03-05 + 2026-03-10 https://mboehmlaender.github.io/ripster/api/pipeline/ - 2026-03-05 + 2026-03-10 https://mboehmlaender.github.io/ripster/api/settings/ - 2026-03-05 + 2026-03-10 https://mboehmlaender.github.io/ripster/api/websocket/ - 2026-03-05 + 2026-03-10 https://mboehmlaender.github.io/ripster/architecture/ - 2026-03-05 + 2026-03-10 https://mboehmlaender.github.io/ripster/architecture/backend/ - 2026-03-05 + 2026-03-10 https://mboehmlaender.github.io/ripster/architecture/database/ - 2026-03-05 + 2026-03-10 https://mboehmlaender.github.io/ripster/architecture/frontend/ - 2026-03-05 + 2026-03-10 https://mboehmlaender.github.io/ripster/architecture/overview/ - 2026-03-05 + 2026-03-10 https://mboehmlaender.github.io/ripster/configuration/ - 2026-03-05 + 2026-03-10 https://mboehmlaender.github.io/ripster/configuration/environment/ - 2026-03-05 + 2026-03-10 https://mboehmlaender.github.io/ripster/configuration/settings-reference/ - 2026-03-05 + 2026-03-10 https://mboehmlaender.github.io/ripster/deployment/ - 2026-03-05 + 2026-03-10 https://mboehmlaender.github.io/ripster/deployment/development/ - 2026-03-05 + 2026-03-10 https://mboehmlaender.github.io/ripster/deployment/production/ - 2026-03-05 + 2026-03-10 https://mboehmlaender.github.io/ripster/getting-started/ - 2026-03-05 + 2026-03-10 https://mboehmlaender.github.io/ripster/getting-started/configuration/ - 2026-03-05 + 2026-03-10 https://mboehmlaender.github.io/ripster/getting-started/installation/ - 2026-03-05 + 2026-03-10 https://mboehmlaender.github.io/ripster/getting-started/prerequisites/ - 2026-03-05 + 2026-03-10 https://mboehmlaender.github.io/ripster/getting-started/quickstart/ - 2026-03-05 + 2026-03-10 https://mboehmlaender.github.io/ripster/pipeline/ - 2026-03-05 + 2026-03-10 https://mboehmlaender.github.io/ripster/pipeline/encoding/ - 2026-03-05 + 2026-03-10 https://mboehmlaender.github.io/ripster/pipeline/playlist-analysis/ - 2026-03-05 + 2026-03-10 https://mboehmlaender.github.io/ripster/pipeline/post-encode-scripts/ - 2026-03-05 + 2026-03-10 https://mboehmlaender.github.io/ripster/pipeline/workflow/ - 2026-03-05 + 2026-03-10 https://mboehmlaender.github.io/ripster/tools/ - 2026-03-05 + 2026-03-10 https://mboehmlaender.github.io/ripster/tools/handbrake/ - 2026-03-05 + 2026-03-10 https://mboehmlaender.github.io/ripster/tools/makemkv/ - 2026-03-05 + 2026-03-10 https://mboehmlaender.github.io/ripster/tools/mediainfo/ - 2026-03-05 + 2026-03-10 \ No newline at end of file diff --git a/site/sitemap.xml.gz b/site/sitemap.xml.gz index b062ace..9a79c56 100644 Binary files a/site/sitemap.xml.gz and b/site/sitemap.xml.gz differ diff --git a/site/tools/handbrake/index.html b/site/tools/handbrake/index.html index 4c77aa4..ffa42bb 100644 --- a/site/tools/handbrake/index.html +++ b/site/tools/handbrake/index.html @@ -1,24 +1,11 @@ - HandBrake - Ripster

HandBrake

HandBrake encodiert die rohen MKV-Dateien in das gewünschte Format. Ripster nutzt HandBrakeCLI.


Verwendeter Befehl

HandBrakeCLI \
-  --input "/mnt/raw/Film_t00.mkv" \
-  --output "/mnt/movies/Film (2010).mkv" \
-  --preset "H.265 MKV 1080p30" \
-  --audio 1,2 \
-  --aencoder copy:ac3,ffaac \
-  --subtitle 1 \
-  --subtitle-default 1
-

Presets

HandBrake verwendet Presets für vorkonfigurierte Encoding-Einstellungen.

Empfohlene Presets

Preset Codec Auflösung Für
H.265 MKV 1080p30 HEVC/H.265 1080p Beste Qualität/Größe
H.265 MKV 720p30 HEVC/H.265 720p Kleinere Dateien
H.264 MKV 1080p30 AVC/H.264 1080p Breiteste Kompatibilität
HQ 1080p30 Surround HEVC/H.265 1080p Hohe Qualität mit Surround

Alle Presets anzeigen

HandBrakeCLI --preset-list
-

Audio-Encoding

Copy-kompatible Codecs

HandBrake kann folgende Codecs direkt kopieren (kein Qualitätsverlust):

Codec --aencoder Wert
AC-3 copy:ac3
AAC copy:aac
MP3 copy:mp3
TrueHD copy:truehd
E-AC-3 copy:eac3

Transcoding

Codecs die nicht kopiert werden können, werden zu AAC transcodiert:

Original Transcodiert zu
DTS AAC (ffaac)
DTS-HD AAC (ffaac)

Extra-Argumente

Über die Einstellung handbrake_extra_args können beliebige HandBrake-Argumente hinzugefügt werden:

# Cropping deaktivieren
---crop 0:0:0:0
-
-# Loose Anamorphic
---loose-anamorphic
-
-# Bestimmte Qualität setzen
---quality 20
-

Fortschritts-Parsing

Ripster parst die HandBrake-Ausgabe auf stderr für die Fortschrittsanzeige:

Encoding: task 1 of 1, 73.50 % (45.23 fps, avg 44.12 fps, ETA 00h12m34s)
-

progressParsers.js extrahiert: - Prozentzahl - Aktuelle FPS - ETA


Konfiguration in Ripster

Einstellung Beschreibung
handbrake_command Pfad/Befehl für HandBrakeCLI
handbrake_preset Preset-Name
handbrake_extra_args Zusätzliche CLI-Argumente
output_extension Dateiendung der Ausgabe

Troubleshooting

HandBrake findet Preset nicht

# Preset-Liste anzeigen
-HandBrakeCLI --preset-list 2>&1 | grep -i "h.265"
-

Preset-Namen sind case-sensitive!

Encoding sehr langsam

# CPU-Encoding-Preset anpassen (schneller = schlechtere Qualität)
-handbrake_extra_args = --encoder-preset fast
-

Verfügbare Presets: ultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow

GPU-Encoding nutzen (NVIDIA)

handbrake_preset = H.265 NVENC 1080p
-

Erfordert HandBrake-Build mit NVENC-Unterstützung und NVIDIA-GPU.

\ No newline at end of file + HandBrake - Ripster

HandBrake

Ripster verwendet HandBrakeCLI für Scan und Encode.


Verwendete Aufrufe

Scan (Review-Aufbau)

HandBrakeCLI --scan --json -i <input> -t 0
+

Encode (vereinfacht)

HandBrakeCLI \
+  -i <input> \
+  -o <output> \
+  -t <titleId> \
+  -Z "<preset>" \
+  <extra-args> \
+  -a <audioTrackIds|none> \
+  -s <subtitleTrackIds|none>
+

Optional ergänzt Ripster:

  • --subtitle-burned=<id>
  • --subtitle-default=<id>
  • --subtitle-forced=<id> oder --subtitle-forced

Presets auslesen

Ripster liest Presets mit:

HandBrakeCLI -z
+

Relevante Settings

Key Bedeutung
handbrake_command CLI-Binary
handbrake_preset_bluray / handbrake_preset_dvd profilspezifisches Preset
handbrake_extra_args_bluray / handbrake_extra_args_dvd profilspezifische Zusatzargumente
output_extension_bluray / output_extension_dvd Ausgabeformat
handbrake_restart_delete_incomplete_output unvollständige Ausgabe bei Neustart löschen

Fortschritts-Parsing

Ripster parst HandBrake-Stderr (Prozent/ETA/Detail) und sendet WebSocket-Progress (PIPELINE_PROGRESS).


Troubleshooting

  • Preset nicht gefunden: Preset-Namen mit HandBrakeCLI -z prüfen
  • sehr langsames Encoding: Preset/Extra-Args prüfen (z. B. --encoder-preset)
\ No newline at end of file diff --git a/site/tools/index.html b/site/tools/index.html index 5e583bb..a0fcb78 100644 --- a/site/tools/index.html +++ b/site/tools/index.html @@ -1 +1 @@ - Externe Tools - Ripster

Externe Tools

Ripster ist ein Orchestrator – die eigentliche Arbeit erledigen diese bewährten Open-Source-Tools:

  • MakeMKV


    Disc-Analyse und Ripping. Erstellt MKV-Dateien oder vollständige Backups.

    MakeMKV

  • HandBrake


    Video-Encoding mit umfangreichen Preset-Optionen.

    HandBrake

  • MediaInfo


    Analyse von Track-Informationen in Mediendateien.

    MediaInfo

\ No newline at end of file + Externe Tools - Ripster

Externe Tools

Ripster ist ein Orchestrator – die eigentliche Arbeit erledigen diese bewährten Open-Source-Tools:

  • MakeMKV


    Disc-Analyse und Ripping. Erstellt MKV-Dateien oder vollständige Backups.

    MakeMKV

  • HandBrake


    Video-Encoding mit umfangreichen Preset-Optionen.

    HandBrake

  • MediaInfo


    Analyse von Track-Informationen in Mediendateien.

    MediaInfo

\ No newline at end of file diff --git a/site/tools/makemkv/index.html b/site/tools/makemkv/index.html index 5995412..68bdd2b 100644 --- a/site/tools/makemkv/index.html +++ b/site/tools/makemkv/index.html @@ -1,20 +1,5 @@ - MakeMKV - Ripster

MakeMKV

MakeMKV analysiert und rippt DVDs und Blu-rays. Ripster nutzt makemkvcon (die CLI-Version).


Verwendete Befehle

Disc-Analyse

makemkvcon -r --cache=1 info disc:0
-

Gibt alle Titel und Playlists der eingelegten Disc aus. Ripster parst diese Ausgabe um die verfügbaren Tracks und Playlists zu bestimmen.

Parameter: - -r – Maschinen-lesbares Ausgabeformat - --cache=1 – Minimaler Disc-Cache - info disc:0 – Informationsabfrage für erstes Laufwerk

MKV-Modus (Standard)

makemkvcon mkv disc:0 all /path/to/raw/ \
-  --minlength=900 \
-  -r
-

Erstellt MKV-Dateien aus allen Titeln, die länger als 15 Minuten sind.

Parameter: - mkv – MKV-Ausgabemodus - disc:0 – Erstes Disc-Laufwerk - all – Alle passenden Titel (nicht nur einen bestimmten) - --minlength=900 – Mindestlänge in Sekunden (entspricht 15 Minuten)

Backup-Modus

makemkvcon backup disc:0 /path/to/raw/backup/ \
-  --decrypt \
-  -r
-

Erstellt ein vollständiges Disc-Backup mit Menüs.

Parameter: - backup – Backup-Modus - --decrypt – Verschlüsselung entfernen


Ausgabeformat

MakeMKV gibt Fortschritt und Status in einem strukturierten Format aus:

PRGV:current,total,max     → Fortschrittsbalken-Werte
-PRGT:code,id,"Beschreibung" → Aktueller Task
-PRGC:code,id,"Beschreibung" → Aktueller Sub-Task
-MSG:code,flags,count,"Text" → Nachricht
-

Ripster's progressParsers.js parst diese Ausgabe für die Live-Fortschrittsanzeige.


MakeMKV-Lizenz

MakeMKV ist Beta-Software und kostenlos für den persönlichen Gebrauch während der Beta-Phase. Eine Beta-Lizenz ist regelmäßig im MakeMKV-Forum verfügbar.

Ohne gültige Lizenz können Blu-rays nicht entschlüsselt werden.

Lizenz eintragen

Die Lizenz wird in den MakeMKV-Einstellungen eingetragen (GUI) oder direkt in:

~/.MakeMKV/settings.conf
-
app_Key = "XXXX-XXXX-XXXX-XXXX-XXXX"
-

Konfiguration in Ripster

Einstellung Beschreibung
makemkv_command Pfad/Befehl für makemkvcon
makemkv_min_length_minutes Mindest-Titellänge (Standard: 15 Min)
makemkv_backup_mode Backup-Modus statt MKV

Troubleshooting

MakeMKV erkennt Disc nicht

# Laufwerk-Berechtigungen prüfen
-ls -la /dev/sr0
-sudo chmod a+rw /dev/sr0
-
-# Oder Benutzer zur Gruppe cdrom hinzufügen
-sudo usermod -a -G cdrom $USER
-

Langer Analyseprozess

Blu-ray-Analyse kann bei Discs mit vielen Playlists 5+ Minuten dauern. Dies ist normal.

Fehlermeldung: "LibMMBD"

LibMMBD ist MakeMKVs interne Verschlüsselungsbibliothek. Bei Fehlern die MakeMKV-Version aktualisieren.

\ No newline at end of file + MakeMKV - Ripster

MakeMKV

Ripster nutzt makemkvcon für Disc-Analyse und Rip.


Verwendete Aufrufe

Analyse

makemkvcon -r info <source>
+

<source> ist typischerweise:

  • disc:<index> (Auto-Modus)
  • dev:/dev/sr0 (explicit)
  • file:<path> (Datei/Ordner-Analyse)

Rip (MKV-Modus)

makemkvcon mkv <source> <title-or-all> <rawDir> [--minlength=...] [...extraArgs]
+

Rip (Backup-Modus)

makemkvcon backup <source> <rawDir> --decrypt
+

Registrierungsschlüssel (optional)

Wenn makemkv_registration_key gesetzt ist, führt Ripster vor Analyse/Rip aus:

makemkvcon reg <key>
+

Relevante Settings

Key Bedeutung
makemkv_command CLI-Binary
makemkv_source_index Source-Index im Auto-Modus
makemkv_min_length_minutes Mindestlaufzeitfilter
makemkv_rip_mode_bluray / makemkv_rip_mode_dvd mkv oder backup
makemkv_analyze_extra_args_bluray / _dvd Zusatzargs Analyse
makemkv_rip_extra_args_bluray / _dvd Zusatzargs Rip

Hinweise

  • Blu-ray-Backups werden oft für robuste Playlist-Analyse genutzt.
  • MakeMKV-Ausgaben werden geparst und als makemkvInfo im Job gespeichert.
\ No newline at end of file diff --git a/site/tools/mediainfo/index.html b/site/tools/mediainfo/index.html index 5e09566..25871b9 100644 --- a/site/tools/mediainfo/index.html +++ b/site/tools/mediainfo/index.html @@ -1,46 +1,2 @@ - MediaInfo - Ripster

MediaInfo

MediaInfo analysiert die Track-Struktur von Mediendateien. Ripster nutzt es nach dem Ripping um Audio- und Untertitelspuren zu identifizieren.


Verwendeter Befehl

mediainfo --Output=JSON /path/to/raw/film.mkv
-

Gibt vollständige Track-Informationen als JSON zurück.


Ausgabe-Struktur

{
-  "media": {
-    "track": [
-      {
-        "@type": "General",
-        "Duration": "8885.042",
-        "Format": "Matroska"
-      },
-      {
-        "@type": "Video",
-        "Format": "HEVC",
-        "Width": "1920",
-        "Height": "1080",
-        "FrameRate": "23.976"
-      },
-      {
-        "@type": "Audio",
-        "StreamOrder": "1",
-        "Format": "TrueHD",
-        "Channels": "8",
-        "Language": "en"
-      },
-      {
-        "@type": "Audio",
-        "StreamOrder": "2",
-        "Format": "AC-3",
-        "Channels": "6",
-        "Language": "de"
-      },
-      {
-        "@type": "Text",
-        "StreamOrder": "1",
-        "Format": "UTF-8",
-        "Language": "de"
-      }
-    ]
-  }
-}
-

Verarbeitung in Ripster

encodePlan.js verarbeitet die MediaInfo-Ausgabe:

  1. Track-Extraktion: Alle Audio- und Untertitel-Tracks werden extrahiert
  2. Sprach-Normalisierung: Sprachcodes werden auf ISO 639-3 normalisiert
  3. Codec-Klassifizierung: Bestimmt ob Codec kopiert oder transcodiert werden kann
  4. Track-Labels: Benutzerfreundliche Bezeichnungen (z.B. "Deutsch (AC-3, 5.1)")

Track-Label-Format

{Sprache} ({Format}, {Kanäle})
-

Beispiele: - Deutsch (AC-3, 5.1) - English (TrueHD, 7.1) - Français (AC-3, 2.0)


Konfiguration in Ripster

Einstellung Beschreibung
mediainfo_command Pfad/Befehl für mediainfo

Troubleshooting

MediaInfo gibt kein JSON aus

# Version prüfen
-mediainfo --Version
-
-# JSON-Ausgabe testen
-mediainfo --Output=JSON /path/to/test.mkv
-

MediaInfo >= 17.10 wird empfohlen.

Sprache als "und" angezeigt

und steht für "undetermined" – die Sprache ist in der MKV-Datei nicht getaggt. Dies ist bei manchen Rips normal. Der Track wird trotzdem angezeigt und kann manuell ausgewählt werden.

\ No newline at end of file + MediaInfo - Ripster

MediaInfo

Ripster nutzt mediainfo zur JSON-Analyse von Medien-Dateien.


Aufruf

mediainfo --Output=JSON <input>
+

Der Input ist typischerweise eine RAW-Datei oder ein vom Workflow gewählter Inputpfad.


Verwendung in Ripster

  • Track-/Codec-Metadaten für Review-Plan
  • Fallback-Informationen in bestimmten Analysepfaden
  • Persistenz als mediainfoInfo im Job

Relevante Settings

Key Bedeutung
mediainfo_command CLI-Binary
mediainfo_extra_args_bluray / _dvd profilspezifische Zusatzargumente

Troubleshooting

  • JSON-Test: mediainfo --Output=JSON <datei>
  • unbekannte Sprache erscheint oft als und (undetermined)
\ No newline at end of file