Bugfix and Docs
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -74,3 +74,9 @@ frontend/.env.*
|
|||||||
Thumbs.db
|
Thumbs.db
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Scripts
|
||||||
|
# ----------------------------
|
||||||
|
deploy-ripster.sh
|
||||||
|
build-handbrake-nvdec.sh
|
||||||
100
README.md
100
README.md
@@ -1,35 +1,21 @@
|
|||||||
# Ripster
|
# Ripster
|
||||||
|
|
||||||
Ripster ist eine lokale Web-Anwendung für halbautomatisches Disc-Ripping mit MakeMKV + HandBrake inklusive Metadaten-Auswahl, Titel-/Spurprüfung und Job-Historie.
|
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.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
> **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
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Was Ripster kann
|
## Was Ripster kann
|
||||||
|
|
||||||
- Disc-Erkennung mit Pipeline-Status in Echtzeit (WebSocket)
|
- 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
|
- Metadaten-Suche und Zuordnung über OMDb
|
||||||
- MakeMKV-Analyse und Rip (MKV oder Backup-Modus)
|
- MakeMKV-Analyse und Rip (`mkv` oder `backup`) mit profilspezifischen Settings
|
||||||
- HandBrake-Encode mit Preset + Extra-Args + Track-Override
|
- HandBrake-Review und Encoding mit Track-Auswahl, User-Presets, Extra-Args
|
||||||
- Manuelle Playlist-/Titel-Auswahl bei komplexen Blu-rays
|
- Pre- und Post-Encode-Ausführungen (Skripte und/oder Skript-Ketten)
|
||||||
- Pre- und Post-Encode-Skripte & Skript-Ketten (inkl. Drag-and-Drop-Sortierung)
|
- Pipeline-Queue mit Job- und Nicht-Job-Einträgen (`script`, `chain`, `wait`)
|
||||||
- Cron-Jobs: Skripte und Ketten zeitgesteuert ausführen (eigener Expression-Parser, Logs, PushOver)
|
- Cron-Jobs für Skripte/Ketten (inkl. Logs und manueller Auslösung)
|
||||||
- Historie mit Re-Encode, Löschfunktionen und Detailansicht
|
- Historie mit Re-Encode, Review-Neustart, File-/Job-Löschung und Orphan-Import
|
||||||
- Dateibasierte Logs (Backend + Job-Prozesslogs)
|
- Hardware-Monitoring (CPU/RAM/GPU/Storage) im Dashboard
|
||||||
|
|
||||||
## Tech-Stack
|
## Tech-Stack
|
||||||
|
|
||||||
@@ -39,7 +25,7 @@ Ripster ist eine lokale Web-Anwendung für halbautomatisches Disc-Ripping mit Ma
|
|||||||
|
|
||||||
## Voraussetzungen
|
## 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))
|
- Node.js `>= 20.19.0` (siehe [.nvmrc](.nvmrc))
|
||||||
- Installierte CLI-Tools im `PATH`:
|
- Installierte CLI-Tools im `PATH`:
|
||||||
- `makemkvcon`
|
- `makemkvcon`
|
||||||
@@ -54,7 +40,7 @@ Ripster ist eine lokale Web-Anwendung für halbautomatisches Disc-Ripping mit Ma
|
|||||||
|
|
||||||
`start.sh` erledigt:
|
`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)
|
2. Dependencies installieren (Root, Backend, Frontend)
|
||||||
3. Dev-Umgebung starten (`backend` + `frontend`)
|
3. Dev-Umgebung starten (`backend` + `frontend`)
|
||||||
|
|
||||||
@@ -63,11 +49,7 @@ Danach:
|
|||||||
- Frontend: `http://localhost:5173`
|
- Frontend: `http://localhost:5173`
|
||||||
- Backend API: `http://localhost:3001/api`
|
- Backend API: `http://localhost:3001/api`
|
||||||
|
|
||||||
Stoppen:
|
Stoppen: laufenden Prozess mit `Ctrl+C` im Terminal beenden.
|
||||||
|
|
||||||
```bash
|
|
||||||
./kill.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## Manueller Start
|
## Manueller Start
|
||||||
|
|
||||||
@@ -99,23 +81,23 @@ npm run start
|
|||||||
|
|
||||||
## Konfiguration
|
## Konfiguration
|
||||||
|
|
||||||
### 1) UI-Settings (empfohlen)
|
### UI-Settings (empfohlen)
|
||||||
|
|
||||||
Die meisten Einstellungen werden in der App unter `Settings` gepflegt und in SQLite gespeichert:
|
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`
|
- Tools: `makemkv_command`, `handbrake_command`, `mediainfo_command`
|
||||||
- Encode: `handbrake_preset`, `handbrake_extra_args`, `output_extension`, `filename_template`
|
- Profile: `*_bluray` / `*_dvd` Varianten für Rip-/Encode-Verhalten
|
||||||
- Laufwerk/Scan: `drive_mode`, `drive_device`, Polling
|
- Queue/Monitoring: `pipeline_max_parallel_jobs`, `hardware_monitoring_*`
|
||||||
- Benachrichtigungen: PushOver
|
- Benachrichtigungen: PushOver
|
||||||
|
|
||||||
### 2) Umgebungsvariablen
|
### Umgebungsvariablen
|
||||||
|
|
||||||
Backend (`backend/src/config.js`):
|
Backend (`backend/src/config.js`):
|
||||||
|
|
||||||
- `PORT` (Default: `3001`)
|
- `PORT` (Default: `3001`)
|
||||||
- `DB_PATH` (Default: `backend/data/ripster.db`)
|
- `DB_PATH` (Default: `backend/data/ripster.db`)
|
||||||
- `LOG_DIR` (Fallback-Logpfad, Default: `backend/logs`)
|
- `LOG_DIR` (Default: `backend/logs`)
|
||||||
- `CORS_ORIGIN` (Default: `*`)
|
- `CORS_ORIGIN` (Default: `*`)
|
||||||
- `LOG_LEVEL` (`debug|info|warn|error`, Default: `info`)
|
- `LOG_LEVEL` (`debug|info|warn|error`, Default: `info`)
|
||||||
|
|
||||||
@@ -123,7 +105,7 @@ Frontend (Vite):
|
|||||||
|
|
||||||
- `VITE_API_BASE` (Default: `/api`)
|
- `VITE_API_BASE` (Default: `/api`)
|
||||||
- `VITE_WS_URL` (optional, überschreibt automatische WS-URL)
|
- `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
|
## Logs und Daten
|
||||||
|
|
||||||
@@ -131,9 +113,9 @@ Log-Ziel ist primär der in den Settings gepflegte `log_dir`.
|
|||||||
|
|
||||||
- Backend-Logs: `<log_dir>/backend/backend-latest.log` und Tagesdateien
|
- Backend-Logs: `<log_dir>/backend/backend-latest.log` und Tagesdateien
|
||||||
- Job-Logs: `<log_dir>/job-<id>.process.log`
|
- Job-Logs: `<log_dir>/job-<id>.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
|
## Projektstruktur
|
||||||
|
|
||||||
@@ -150,28 +132,55 @@ ripster/
|
|||||||
pages/
|
pages/
|
||||||
components/
|
components/
|
||||||
api/
|
api/
|
||||||
|
db/schema.sql
|
||||||
start.sh
|
start.sh
|
||||||
kill.sh
|
install.sh
|
||||||
deploy-ripster.sh
|
install-dev.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
## API-Überblick
|
## API-Überblick
|
||||||
|
|
||||||
|
**Health**
|
||||||
|
- `GET /api/health`
|
||||||
|
|
||||||
**Pipeline**
|
**Pipeline**
|
||||||
- `GET /api/pipeline/state`
|
- `GET /api/pipeline/state`
|
||||||
- `POST /api/pipeline/analyze`
|
- `POST /api/pipeline/analyze`
|
||||||
|
- `POST /api/pipeline/rescan-disc`
|
||||||
|
- `POST /api/pipeline/select-metadata`
|
||||||
- `POST /api/pipeline/start/:jobId`
|
- `POST /api/pipeline/start/:jobId`
|
||||||
- `POST /api/pipeline/confirm-encode/: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**
|
**History**
|
||||||
- `GET /api/history`
|
- `GET /api/history`
|
||||||
- `GET /api/history/:id`
|
- `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**
|
**Settings**
|
||||||
- `GET /api/settings`
|
- `GET /api/settings`
|
||||||
|
- `PUT /api/settings/:key`
|
||||||
- `PUT /api/settings`
|
- `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`
|
- `GET /api/crons`
|
||||||
- `POST /api/crons`
|
- `POST /api/crons`
|
||||||
- `GET /api/crons/:id`
|
- `GET /api/crons/:id`
|
||||||
@@ -185,17 +194,16 @@ ripster/
|
|||||||
|
|
||||||
- WebSocket verbindet nicht:
|
- WebSocket verbindet nicht:
|
||||||
- prüfen, ob Frontend über Vite-Proxy läuft (`/ws` -> Backend)
|
- 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:
|
- Keine Disc erkannt:
|
||||||
- `drive_mode=explicit` testen und `drive_device` setzen (z. B. `/dev/sr0`)
|
- `drive_mode=explicit` testen und `drive_device` setzen (z. B. `/dev/sr0`)
|
||||||
- HandBrake/MakeMKV Fehler:
|
- HandBrake/MakeMKV Fehler:
|
||||||
- CLI-Binaries im `PATH` prüfen
|
- CLI-Binaries im `PATH` prüfen
|
||||||
- Preset-Name exakt wie in `HandBrakeCLI -z` hinterlegen
|
- Preset-Name mit `HandBrakeCLI -z` prüfen
|
||||||
- Startfehler wegen Schema:
|
- Startfehler wegen Schema:
|
||||||
- sicherstellen, dass die erwartete Schema-Datei vorhanden ist (`db/schema.sql`)
|
- `db/schema.sql` vorhanden halten
|
||||||
|
|
||||||
## Sicherheit
|
## Sicherheit
|
||||||
|
|
||||||
- Keine echten Tokens/Passwörter ins Repository committen.
|
- Keine echten Tokens/Passwörter ins Repository committen.
|
||||||
- Lokale Secrets in `.env` oder in Settings pflegen, aber nicht versionieren.
|
- Lokale Secrets in `.env` oder in Settings pflegen, aber nicht versionieren.
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ function parseJsonSafe(raw, fallback = null) {
|
|||||||
const PROCESS_LOG_TAIL_MAX_BYTES = 1024 * 1024;
|
const PROCESS_LOG_TAIL_MAX_BYTES = 1024 * 1024;
|
||||||
const processLogStreams = new Map();
|
const processLogStreams = new Map();
|
||||||
const PROFILE_PATH_SUFFIXES = ['bluray', 'dvd', 'other'];
|
const PROFILE_PATH_SUFFIXES = ['bluray', 'dvd', 'other'];
|
||||||
|
const RAW_INCOMPLETE_PREFIX = 'Incomplete_';
|
||||||
|
const RAW_RIP_COMPLETE_PREFIX = 'Rip_Complete_';
|
||||||
|
|
||||||
function inspectDirectory(dirPath) {
|
function inspectDirectory(dirPath) {
|
||||||
if (!dirPath) {
|
if (!dirPath) {
|
||||||
@@ -430,9 +432,29 @@ function normalizeComparablePath(inputPath) {
|
|||||||
return resolveSafe(String(inputPath || '')).replace(/[\\/]+$/, '');
|
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) {
|
function parseRawFolderMetadata(folderName) {
|
||||||
const rawName = String(folderName || '').trim();
|
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 folderJobIdMatch = normalizedRawName.match(/-\s*RAW\s*-\s*job-(\d+)\s*$/i);
|
||||||
const folderJobId = folderJobIdMatch ? Number(folderJobIdMatch[1]) : null;
|
const folderJobId = folderJobIdMatch ? Number(folderJobIdMatch[1]) : null;
|
||||||
let working = normalizedRawName.replace(/\s*-\s*RAW\s*-\s*job-\d+\s*$/i, '').trim();
|
let working = normalizedRawName.replace(/\s*-\s*RAW\s*-\s*job-\d+\s*$/i, '').trim();
|
||||||
@@ -1053,6 +1075,7 @@ class HistoryService {
|
|||||||
detectedTitle: effectiveTitle
|
detectedTitle: effectiveTitle
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const renameSteps = [];
|
||||||
let finalRawPath = absRawPath;
|
let finalRawPath = absRawPath;
|
||||||
const renamedRawPath = buildRawPathForJobId(absRawPath, created.id);
|
const renamedRawPath = buildRawPathForJobId(absRawPath, created.id);
|
||||||
const shouldRenameRawFolder = normalizeComparablePath(renamedRawPath) !== absRawPath;
|
const shouldRenameRawFolder = normalizeComparablePath(renamedRawPath) !== absRawPath;
|
||||||
@@ -1067,6 +1090,7 @@ class HistoryService {
|
|||||||
try {
|
try {
|
||||||
fs.renameSync(absRawPath, renamedRawPath);
|
fs.renameSync(absRawPath, renamedRawPath);
|
||||||
finalRawPath = normalizeComparablePath(renamedRawPath);
|
finalRawPath = normalizeComparablePath(renamedRawPath);
|
||||||
|
renameSteps.push({ from: absRawPath, to: finalRawPath });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await db.run('DELETE FROM jobs WHERE id = ?', [created.id]);
|
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}`);
|
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, {
|
await this.updateJob(created.id, {
|
||||||
status: 'FINISHED',
|
status: 'FINISHED',
|
||||||
last_state: 'FINISHED',
|
last_state: 'FINISHED',
|
||||||
@@ -1105,8 +1153,8 @@ class HistoryService {
|
|||||||
await this.appendLog(
|
await this.appendLog(
|
||||||
created.id,
|
created.id,
|
||||||
'SYSTEM',
|
'SYSTEM',
|
||||||
shouldRenameRawFolder
|
renameSteps.length > 0
|
||||||
? `Historieneintrag aus RAW erstellt. Ordner umbenannt: ${absRawPath} -> ${finalRawPath}`
|
? `Historieneintrag aus RAW erstellt. Ordner umbenannt: ${renameSteps.map((step) => `${step.from} -> ${step.to}`).join(' | ')}`
|
||||||
: `Historieneintrag aus bestehendem RAW-Ordner erstellt: ${finalRawPath}`
|
: `Historieneintrag aus bestehendem RAW-Ordner erstellt: ${finalRawPath}`
|
||||||
);
|
);
|
||||||
if (metadata.imdbId) {
|
if (metadata.imdbId) {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -716,7 +716,16 @@ class SettingsService {
|
|||||||
options?.mediaProfile || deviceInfo?.mediaProfile || null
|
options?.mediaProfile || deviceInfo?.mediaProfile || null
|
||||||
);
|
);
|
||||||
const cmd = map.makemkv_command;
|
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 });
|
logger.debug('cli:makemkv:analyze', { cmd, args, deviceInfo });
|
||||||
return { cmd, args };
|
return { cmd, args };
|
||||||
}
|
}
|
||||||
@@ -726,7 +735,16 @@ class SettingsService {
|
|||||||
const map = this.resolveEffectiveToolSettings(rawMap, options?.mediaProfile || null);
|
const map = this.resolveEffectiveToolSettings(rawMap, options?.mediaProfile || null);
|
||||||
const cmd = map.makemkv_command;
|
const cmd = map.makemkv_command;
|
||||||
const sourceArg = `file:${sourcePath}`;
|
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);
|
const titleIdRaw = Number(options?.titleId);
|
||||||
// "makemkvcon info" supports only <source>; title filtering is done in app parser.
|
// "makemkvcon info" supports only <source>; title filtering is done in app parser.
|
||||||
logger.debug('cli:makemkv:analyze:path', {
|
logger.debug('cli:makemkv:analyze:path', {
|
||||||
|
|||||||
@@ -444,7 +444,9 @@ function buildBaseTrackSelectors(settings, presetProfile = null) {
|
|||||||
explicitIds: [],
|
explicitIds: [],
|
||||||
firstOnly: baseSubtitleMode === 'first',
|
firstOnly: baseSubtitleMode === 'first',
|
||||||
selectionSource: profile.source === 'preset-export' ? 'preset' : 'default',
|
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,
|
burnedTrackId: null,
|
||||||
defaultTrackId: null,
|
defaultTrackId: null,
|
||||||
forcedTrackId: null,
|
forcedTrackId: null,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
const LARGE_JUMP_THRESHOLD = 20;
|
const LARGE_JUMP_THRESHOLD = 20;
|
||||||
const DEFAULT_DURATION_SIMILARITY_SECONDS = 90;
|
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) {
|
function parseDurationSeconds(raw) {
|
||||||
const text = String(raw || '').trim();
|
const text = String(raw || '').trim();
|
||||||
@@ -151,6 +153,7 @@ function parseAnalyzeTitles(lines) {
|
|||||||
chapters: 0,
|
chapters: 0,
|
||||||
segmentNumbers: [],
|
segmentNumbers: [],
|
||||||
segmentFiles: [],
|
segmentFiles: [],
|
||||||
|
streams: {},
|
||||||
fields: {}
|
fields: {}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -164,6 +167,57 @@ function parseAnalyzeTitles(lines) {
|
|||||||
title.playlistIdFromMap = normalizePlaylistId(mapping.playlistId);
|
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);
|
const tinfo = String(line || '').match(/^TINFO:(\d+),(\d+),\d+,"([^"]*)"/i);
|
||||||
if (!tinfo) {
|
if (!tinfo) {
|
||||||
continue;
|
continue;
|
||||||
@@ -242,20 +296,64 @@ function parseAnalyzeTitles(lines) {
|
|||||||
const playlistId = normalizePlaylistId(item.playlistId);
|
const playlistId = normalizePlaylistId(item.playlistId);
|
||||||
const playlistIdFromMap = normalizePlaylistId(item.playlistIdFromMap);
|
const playlistIdFromMap = normalizePlaylistId(item.playlistIdFromMap);
|
||||||
const playlistIdFromField16 = normalizePlaylistId(item.playlistIdFromField16);
|
const playlistIdFromField16 = normalizePlaylistId(item.playlistIdFromField16);
|
||||||
// Prefer explicit title<->playlist map lines from MakeMKV (MSG:3016).
|
const field16Raw = String(item?.fields?.[16] || '').trim();
|
||||||
const resolvedPlaylistId = playlistIdFromMap || playlistIdFromField16 || playlistId;
|
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 segmentNumbers = Array.isArray(item.segmentNumbers) ? item.segmentNumbers : [];
|
||||||
const segmentFiles = segmentNumbers
|
const segmentFiles = segmentNumbers
|
||||||
.map((number) => toSegmentFile(number))
|
.map((number) => toSegmentFile(number))
|
||||||
.filter(Boolean);
|
.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 {
|
return {
|
||||||
...item,
|
...restItem,
|
||||||
playlistId: resolvedPlaylistId,
|
playlistId: resolvedPlaylistId,
|
||||||
playlistIdFromMap,
|
playlistIdFromMap,
|
||||||
playlistIdFromField16,
|
playlistIdFromField16,
|
||||||
playlistFile: resolvedPlaylistId ? `${resolvedPlaylistId}.mpls` : null,
|
playlistFile: resolvedPlaylistId ? `${resolvedPlaylistId}.mpls` : null,
|
||||||
durationLabel: item.durationLabel || formatDuration(item.durationSeconds),
|
durationLabel: item.durationLabel || formatDuration(item.durationSeconds),
|
||||||
|
audioTracks,
|
||||||
|
subtitleTracks,
|
||||||
|
audioTrackCount: audioTracks.length,
|
||||||
|
subtitleTrackCount: subtitleTracks.length,
|
||||||
segmentNumbers,
|
segmentNumbers,
|
||||||
segmentFiles
|
segmentFiles
|
||||||
};
|
};
|
||||||
@@ -277,6 +375,58 @@ function uniqueOrdered(values) {
|
|||||||
return output;
|
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) {
|
function buildSimilarityGroups(candidates, durationSimilaritySeconds) {
|
||||||
const list = Array.isArray(candidates) ? [...candidates] : [];
|
const list = Array.isArray(candidates) ? [...candidates] : [];
|
||||||
const tolerance = Math.max(0, Math.round(Number(durationSimilaritySeconds || 0)));
|
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))
|
.filter((title) => String(title.playlistIdFromMap) !== String(title.playlistIdFromField16))
|
||||||
.slice(0, 25)
|
.slice(0, 25)
|
||||||
.map((title) =>
|
.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 = {}) {
|
function analyzePlaylistObfuscation(lines, minLengthMinutes = 60, options = {}) {
|
||||||
const parsedTitles = parseAnalyzeTitles(lines);
|
const parsedTitles = parseAnalyzeTitles(lines);
|
||||||
|
const reportedTitleCount = parseReportedTitleCount(lines);
|
||||||
const minSeconds = Math.max(0, Math.round(Number(minLengthMinutes || 0) * 60));
|
const minSeconds = Math.max(0, Math.round(Number(minLengthMinutes || 0) * 60));
|
||||||
const durationSimilaritySeconds = Math.max(
|
const durationSimilaritySeconds = Math.max(
|
||||||
0,
|
0,
|
||||||
Math.round(Number(options.durationSimilaritySeconds || DEFAULT_DURATION_SIMILARITY_SECONDS))
|
Math.round(Number(options.durationSimilaritySeconds || DEFAULT_DURATION_SIMILARITY_SECONDS))
|
||||||
);
|
);
|
||||||
|
|
||||||
const candidates = parsedTitles
|
const candidatesRaw = parsedTitles
|
||||||
.filter((item) => Number(item.durationSeconds || 0) >= minSeconds)
|
.filter((item) => Number(item.durationSeconds || 0) >= minSeconds)
|
||||||
.sort((a, b) => b.durationSeconds - a.durationSeconds || b.sizeBytes - a.sizeBytes || a.titleId - b.titleId);
|
.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 obfuscationDetected = similarityGroups.length > 0;
|
||||||
const multipleCandidatesDetected = candidates.length > 1;
|
const multipleCandidatesDetected = candidatePlaylistsAll.length > 1;
|
||||||
const manualDecisionRequired = multipleCandidatesDetected;
|
const manualDecisionRequired = multipleCandidatesDetected;
|
||||||
const decisionPool = manualDecisionRequired ? candidates : [];
|
const decisionPool = manualDecisionRequired ? playlistBackedCandidates : [];
|
||||||
const evaluatedCandidates = decisionPool.length > 0 ? scoreCandidates(decisionPool) : [];
|
const evaluatedCandidates = decisionPool.length > 0 ? scoreCandidates(decisionPool) : [];
|
||||||
const recommendation = evaluatedCandidates[0] || null;
|
const recommendation = evaluatedCandidates[0] || null;
|
||||||
const candidatePlaylists = manualDecisionRequired
|
const candidatePlaylists = manualDecisionRequired ? candidatePlaylistsAll : [];
|
||||||
? uniqueOrdered(decisionPool.map((item) => item.playlistId).filter(Boolean))
|
|
||||||
: [];
|
|
||||||
const playlistSegments = buildPlaylistSegmentMap(decisionPool);
|
const playlistSegments = buildPlaylistSegmentMap(decisionPool);
|
||||||
const playlistToTitleId = buildPlaylistToTitleIdMap(parsedTitles);
|
const playlistToTitleId = buildPlaylistToTitleIdMap(parsedTitles);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
generatedAt: new Date().toISOString(),
|
generatedAt: new Date().toISOString(),
|
||||||
|
reportedTitleCount,
|
||||||
minLengthMinutes: Number(minLengthMinutes || 0),
|
minLengthMinutes: Number(minLengthMinutes || 0),
|
||||||
minLengthSeconds: minSeconds,
|
minLengthSeconds: minSeconds,
|
||||||
durationSimilaritySeconds,
|
durationSimilaritySeconds,
|
||||||
@@ -570,6 +728,9 @@ function analyzePlaylistObfuscation(lines, minLengthMinutes = 60, options = {})
|
|||||||
},
|
},
|
||||||
warningLines: [
|
warningLines: [
|
||||||
...extractWarningLines(lines),
|
...extractWarningLines(lines),
|
||||||
|
...(reportedTitleCount !== null && reportedTitleCount !== parsedTitles.length
|
||||||
|
? [`Titel-Anzahl abweichend: TCOUNT=${reportedTitleCount}, geparst=${parsedTitles.length}`]
|
||||||
|
: []),
|
||||||
...extractPlaylistMismatchWarnings(parsedTitles)
|
...extractPlaylistMismatchWarnings(parsedTitles)
|
||||||
].slice(0, 60)
|
].slice(0, 60)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -33,12 +33,12 @@ function parseMakeMkvProgress(line) {
|
|||||||
const prgv = line.match(/PRGV:(\d+),(\d+),(\d+)/);
|
const prgv = line.match(/PRGV:(\d+),(\d+),(\d+)/);
|
||||||
if (prgv) {
|
if (prgv) {
|
||||||
// Format: PRGV:current,total,max (official makemkv docs)
|
// Format: PRGV:current,total,max (official makemkv docs)
|
||||||
// progress = current / max
|
// current = per-file progress, total = overall progress across all files
|
||||||
const current = Number(prgv[1]);
|
const total = Number(prgv[2]);
|
||||||
const max = Number(prgv[3]);
|
const max = Number(prgv[3]);
|
||||||
|
|
||||||
if (max > 0) {
|
if (max > 0) {
|
||||||
return { percent: clampPercent((current / max) * 100), eta: null };
|
return { percent: clampPercent((total / max) * 100), eta: null };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 "Uebertrage lokalen Ordner ${LOCAL_PATH} nach ${REMOTE_TARGET} ..."
|
||||||
echo "backend/data wird weder uebertragen noch auf dem Ziel geloescht: ${DATA_RELATIVE_DIR}"
|
echo "backend/data wird weder uebertragen noch auf dem Ziel geloescht: ${DATA_RELATIVE_DIR}"
|
||||||
sshpass -p "$SSH_PASSWORD" rsync -az --progress --delete \
|
sshpass -p "$SSH_PASSWORD" rsync -az --progress --delete \
|
||||||
|
--exclude "${DATA_RELATIVE_DIR}" \
|
||||||
|
--filter "protect ${DATA_RELATIVE_DIR}" \
|
||||||
--filter "protect debug" \
|
--filter "protect debug" \
|
||||||
-e "ssh $SSH_OPTS" \
|
-e "ssh $SSH_OPTS" \
|
||||||
"${LOCAL_PATH}/" "${REMOTE_TARGET}/"
|
"${LOCAL_PATH}/" "${REMOTE_TARGET}/"
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
# Cron API
|
# 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`
|
Listet alle Cron-Jobs.
|
||||||
|
|
||||||
Alle konfigurierten Cron-Jobs auflisten.
|
|
||||||
|
|
||||||
**Antwort:**
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -21,13 +17,14 @@ Alle konfigurierten Cron-Jobs auflisten.
|
|||||||
"cronExpression": "0 2 * * *",
|
"cronExpression": "0 2 * * *",
|
||||||
"sourceType": "script",
|
"sourceType": "script",
|
||||||
"sourceId": 3,
|
"sourceId": 3,
|
||||||
|
"sourceName": "Backup-Skript",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"pushoverEnabled": true,
|
"pushoverEnabled": true,
|
||||||
"lastRunAt": "2026-03-09T02:00:00.000Z",
|
"lastRunAt": "2026-03-10T02:00:00.000Z",
|
||||||
"lastRunStatus": "success",
|
"lastRunStatus": "success",
|
||||||
"nextRunAt": "2026-03-10T02:00:00.000Z",
|
"nextRunAt": "2026-03-11T02:00:00.000Z",
|
||||||
"createdAt": "2026-03-01T10: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.
|
Erstellt Cron-Job.
|
||||||
|
|
||||||
**Body:**
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -52,16 +47,25 @@ Neuen Cron-Job anlegen.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
| Feld | Typ | Pflicht | Beschreibung |
|
Response: `201` mit `{ "job": { ... } }`
|
||||||
|------|-----|---------|-------------|
|
|
||||||
| `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`) |
|
|
||||||
|
|
||||||
**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
|
```json
|
||||||
{ "job": { ... } }
|
{ "job": { ... } }
|
||||||
@@ -69,53 +73,27 @@ Neuen Cron-Job anlegen.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### `GET /api/crons/:id`
|
## DELETE /api/crons/:id
|
||||||
|
|
||||||
Einzelnen Cron-Job abrufen.
|
Response:
|
||||||
|
|
||||||
**Antwort:**
|
|
||||||
|
|
||||||
```json
|
```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`.
|
Liefert Ausführungs-Logs.
|
||||||
|
|
||||||
**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.
|
|
||||||
|
|
||||||
**Query-Parameter:**
|
**Query-Parameter:**
|
||||||
|
|
||||||
| Parameter | Typ | Default | Beschreibung |
|
| Parameter | Typ | Default | Beschreibung |
|
||||||
|-----------|-----|---------|-------------|
|
|-----------|-----|---------|-------------|
|
||||||
| `limit` | number | 20 | Anzahl Einträge (max. 100) |
|
| `limit` | number | `20` | Anzahl Einträge, max. `100` |
|
||||||
|
|
||||||
**Antwort:**
|
**Response:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -123,62 +101,54 @@ Ausführungs-Logs eines Cron-Jobs abrufen.
|
|||||||
{
|
{
|
||||||
"id": 42,
|
"id": 42,
|
||||||
"cronJobId": 1,
|
"cronJobId": 1,
|
||||||
"startedAt": "2026-03-09T02:00:01.000Z",
|
"startedAt": "2026-03-10T02:00:01.000Z",
|
||||||
"finishedAt": "2026-03-09T02:00:05.000Z",
|
"finishedAt": "2026-03-10T02:00:05.000Z",
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"exitCode": 0,
|
"output": "Backup abgeschlossen.",
|
||||||
"stdout": "Backup abgeschlossen.",
|
"errorMessage": null
|
||||||
"stderr": "",
|
|
||||||
"triggeredBy": "cron"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
| Feld | Beschreibung |
|
`status`: `running` | `success` | `error`
|
||||||
|------|-------------|
|
|
||||||
| `status` | `"success"`, `"error"` oder `"running"` |
|
|
||||||
| `triggeredBy` | `"cron"` (zeitgesteuert) oder `"manual"` (manuell ausgelöst) |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### `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
|
```json
|
||||||
{
|
{ "triggered": true, "cronJobId": 1 }
|
||||||
"status": "success",
|
|
||||||
"exitCode": 0,
|
|
||||||
"stdout": "...",
|
|
||||||
"stderr": ""
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
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
|
```json
|
||||||
{ "cronExpression": "*/15 * * * *" }
|
{ "cronExpression": "*/15 * * * *" }
|
||||||
```
|
```
|
||||||
|
|
||||||
**Antwort (gültig):**
|
**Gültige Response:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"valid": true,
|
"valid": true,
|
||||||
"nextRunAt": "2026-03-09T14:15:00.000Z"
|
"nextRunAt": "2026-03-10T14:15:00.000Z"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Antwort (ungültig):**
|
**Ungültige Response:**
|
||||||
|
|
||||||
```json
|
```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:
|
||||||
|
|
||||||
```
|
```text
|
||||||
┌───────────── Minute (0-59)
|
Minute Stunde Tag Monat Wochentag
|
||||||
│ ┌────────── Stunde (0-23)
|
|
||||||
│ │ ┌─────── Tag (1-31)
|
|
||||||
│ │ │ ┌──── Monat (1-12)
|
|
||||||
│ │ │ │ ┌─ Wochentag (0-7, 0 und 7 = Sonntag)
|
|
||||||
│ │ │ │ │
|
|
||||||
* * * * *
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Beispiele
|
Beispiele:
|
||||||
|
|
||||||
| Ausdruck | Beschreibung |
|
- `0 2 * * *` täglich 02:00
|
||||||
|----------|-------------|
|
- `*/15 * * * *` alle 15 Minuten
|
||||||
| `0 2 * * *` | Täglich um 02:00 Uhr |
|
- `0 6 * * 1-5` Mo-Fr 06:00
|
||||||
| `*/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` |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## WebSocket-Event
|
## WebSocket-Events zu Cron
|
||||||
|
|
||||||
Bei Änderungen an Cron-Jobs (Anlegen, Aktualisieren, Löschen) wird ein `CRON_JOBS_UPDATED`-Event gesendet:
|
- `CRON_JOBS_UPDATED` bei Create/Update/Delete
|
||||||
|
- `CRON_JOB_UPDATED` bei Laufzeitstatus (`running` -> `success|error`)
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "CRON_JOBS_UPDATED",
|
|
||||||
"payload": {
|
|
||||||
"action": "created",
|
|
||||||
"id": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`action` ist `"created"`, `"updated"` oder `"deleted"`.
|
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
# History API
|
# 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
|
## GET /api/history
|
||||||
|
|
||||||
Gibt eine Liste aller Jobs zurück, optional gefiltert.
|
Liefert Jobs (optionale Filter).
|
||||||
|
|
||||||
**Query-Parameter:**
|
**Query-Parameter:**
|
||||||
|
|
||||||
| Parameter | Typ | Beschreibung |
|
| Parameter | Typ | Beschreibung |
|
||||||
|----------|-----|-------------|
|
|----------|-----|-------------|
|
||||||
| `status` | string | Filtert nach Status (z.B. `FINISHED`, `ERROR`) |
|
| `status` | string | Filter nach Job-Status |
|
||||||
| `search` | string | Sucht in Filmtiteln |
|
| `search` | string | Suche in Titel-Feldern |
|
||||||
|
|
||||||
**Beispiel:**
|
**Beispiel:**
|
||||||
|
|
||||||
```
|
```text
|
||||||
GET /api/history?status=FINISHED&search=Inception
|
GET /api/history?status=FINISHED&search=Inception
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -30,17 +30,15 @@ GET /api/history?status=FINISHED&search=Inception
|
|||||||
"id": 42,
|
"id": 42,
|
||||||
"status": "FINISHED",
|
"status": "FINISHED",
|
||||||
"title": "Inception",
|
"title": "Inception",
|
||||||
"imdb_id": "tt1375666",
|
"raw_path": "/mnt/raw/Inception - RAW - job-42",
|
||||||
"omdb_year": "2010",
|
"output_path": "/mnt/movies/Inception (2010)/Inception (2010).mkv",
|
||||||
"omdb_type": "movie",
|
"mediaType": "bluray",
|
||||||
"omdb_poster": "https://...",
|
"ripSuccessful": true,
|
||||||
"raw_path": "/mnt/nas/raw/Inception_t00.mkv",
|
"encodeSuccess": true,
|
||||||
"output_path": "/mnt/nas/movies/Inception (2010).mkv",
|
"created_at": "2026-03-10T08:00:00.000Z",
|
||||||
"created_at": "2024-01-15T10:00:00.000Z",
|
"updated_at": "2026-03-10T10:00:00.000Z"
|
||||||
"updated_at": "2024-01-15T12:30:00.000Z"
|
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"total": 1
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -48,34 +46,37 @@ GET /api/history?status=FINISHED&search=Inception
|
|||||||
|
|
||||||
## GET /api/history/:id
|
## GET /api/history/:id
|
||||||
|
|
||||||
Gibt Detail-Informationen für einen einzelnen Job zurück.
|
Liefert Job-Detail.
|
||||||
|
|
||||||
**URL-Parameter:** `id` – Job-ID
|
|
||||||
|
|
||||||
**Query-Parameter:**
|
**Query-Parameter:**
|
||||||
|
|
||||||
| Parameter | Typ | Standard | Beschreibung |
|
| Parameter | Typ | Standard | Beschreibung |
|
||||||
|----------|-----|---------|-------------|
|
|----------|-----|---------|-------------|
|
||||||
| `includeLogs` | boolean | `false` | Log-Inhalte einschließen |
|
| `includeLogs` | bool | `false` | Prozesslog laden |
|
||||||
| `includeLiveLog` | boolean | `false` | Aktuellen Live-Log einschließen |
|
| `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:**
|
**Response:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"id": 42,
|
"job": {
|
||||||
"status": "FINISHED",
|
"id": 42,
|
||||||
"title": "Inception",
|
"status": "FINISHED",
|
||||||
"imdb_id": "tt1375666",
|
"makemkvInfo": {},
|
||||||
"encode_plan": { ... },
|
"mediainfoInfo": {},
|
||||||
"makemkv_output": { ... },
|
"handbrakeInfo": {},
|
||||||
"mediainfo_output": { ... },
|
"encodePlan": {},
|
||||||
"handbrake_log": "/path/to/log",
|
"log": "...",
|
||||||
"logs": {
|
"log_count": 1,
|
||||||
"handbrake": "Encoding: task 1 of 1, 100.0%\n..."
|
"logMeta": {
|
||||||
},
|
"loaded": true,
|
||||||
"created_at": "2024-01-15T10:00:00.000Z",
|
"total": 800,
|
||||||
"updated_at": "2024-01-15T12:30:00.000Z"
|
"returned": 800,
|
||||||
|
"truncated": true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -83,14 +84,19 @@ Gibt Detail-Informationen für einen einzelnen Job zurück.
|
|||||||
|
|
||||||
## GET /api/history/database
|
## GET /api/history/database
|
||||||
|
|
||||||
Gibt alle rohen Datenbankzeilen zurück (Debug-Ansicht).
|
Debug-Ansicht der DB-Zeilen (angereichert).
|
||||||
|
|
||||||
**Response:**
|
**Response:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"jobs": [ { "id": 1, "status": "FINISHED", ... } ],
|
"rows": [
|
||||||
"total": 15
|
{
|
||||||
|
"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
|
## 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:**
|
**Response:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"orphans": [
|
"rawDir": "/mnt/raw",
|
||||||
|
"rawDirs": ["/mnt/raw", "/mnt/raw-bluray"],
|
||||||
|
"rows": [
|
||||||
{
|
{
|
||||||
"path": "/mnt/nas/raw/UnknownMovie_2023-12-01",
|
"rawPath": "/mnt/raw/Inception (2010) [tt1375666] - RAW - job-99",
|
||||||
"size": "45.2 GB",
|
"folderName": "Inception (2010) [tt1375666] - RAW - job-99",
|
||||||
"modifiedAt": "2023-12-01T15:00:00.000Z",
|
"title": "Inception",
|
||||||
"files": ["t00.mkv", "t01.mkv"]
|
"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
|
## POST /api/history/orphan-raw/import
|
||||||
|
|
||||||
Importiert einen Orphan-Raw-Ordner als Job in die Datenbank.
|
Importiert RAW-Ordner als FINISHED-Job.
|
||||||
|
|
||||||
**Request:**
|
**Request:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{ "rawPath": "/mnt/raw/Inception (2010) [tt1375666] - RAW - job-99" }
|
||||||
"path": "/mnt/nas/raw/UnknownMovie_2023-12-01"
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response:**
|
**Response:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"ok": true,
|
"job": { "id": 77, "status": "FINISHED" },
|
||||||
"jobId": 99,
|
"uiReset": { "reset": true, "state": "IDLE" }
|
||||||
"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
|
## POST /api/history/:id/omdb/assign
|
||||||
|
|
||||||
Weist einem bestehenden Job OMDb-Metadaten nachträglich zu.
|
Weist OMDb-/Metadaten nachträglich zu.
|
||||||
|
|
||||||
**URL-Parameter:** `id` – Job-ID
|
|
||||||
|
|
||||||
**Request:**
|
**Request:**
|
||||||
|
|
||||||
@@ -155,44 +161,42 @@ Weist einem bestehenden Job OMDb-Metadaten nachträglich zu.
|
|||||||
{
|
{
|
||||||
"imdbId": "tt1375666",
|
"imdbId": "tt1375666",
|
||||||
"title": "Inception",
|
"title": "Inception",
|
||||||
"year": "2010",
|
"year": 2010,
|
||||||
"type": "movie",
|
"poster": "https://...",
|
||||||
"poster": "https://..."
|
"fromOmdb": true
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response:**
|
**Response:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "ok": true }
|
{ "job": { "id": 42, "imdb_id": "tt1375666" } }
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## POST /api/history/:id/delete-files
|
## POST /api/history/:id/delete-files
|
||||||
|
|
||||||
Löscht die Dateien eines Jobs (Raw und/oder Output), behält den Job-Eintrag.
|
Löscht Dateien eines Jobs, behält DB-Eintrag.
|
||||||
|
|
||||||
**URL-Parameter:** `id` – Job-ID
|
|
||||||
|
|
||||||
**Request:**
|
**Request:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{ "target": "both" }
|
||||||
"deleteRaw": true,
|
|
||||||
"deleteOutput": false
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`target`: `raw` | `movie` | `both`
|
||||||
|
|
||||||
**Response:**
|
**Response:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"ok": true,
|
"summary": {
|
||||||
"deleted": {
|
"target": "both",
|
||||||
"raw": "/mnt/nas/raw/Inception_t00.mkv",
|
"raw": { "attempted": true, "deleted": true, "filesDeleted": 12, "dirsRemoved": 3, "reason": null },
|
||||||
"output": 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
|
## POST /api/history/:id/delete
|
||||||
|
|
||||||
Löscht den Job-Eintrag aus der Datenbank, optional auch die Dateien.
|
Löscht Job aus DB; optional auch Dateien.
|
||||||
|
|
||||||
**URL-Parameter:** `id` – Job-ID
|
|
||||||
|
|
||||||
**Request:**
|
**Request:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{ "target": "none" }
|
||||||
"deleteFiles": true
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`target`: `none` | `raw` | `movie` | `both`
|
||||||
|
|
||||||
**Response:**
|
**Response:**
|
||||||
|
|
||||||
```json
|
```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.
|
||||||
|
|||||||
@@ -1,16 +1,21 @@
|
|||||||
# API-Referenz
|
# 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
|
## Basis-URL
|
||||||
|
|
||||||
```
|
```text
|
||||||
http://localhost:3001
|
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`.
|
|||||||
|
|
||||||
<div class="grid cards" markdown>
|
<div class="grid cards" markdown>
|
||||||
|
|
||||||
|
- :material-heart-pulse: **Health**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Service-Liveness.
|
||||||
|
|
||||||
|
`GET /api/health`
|
||||||
|
|
||||||
- :material-pipe: **Pipeline API**
|
- :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)
|
[: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)
|
[: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)
|
[: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)
|
[: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)
|
[:octicons-arrow-right-24: WebSocket](websocket.md)
|
||||||
|
|
||||||
@@ -64,30 +77,41 @@ Konfigurierbar über die Umgebungsvariable `PORT`.
|
|||||||
|
|
||||||
## Authentifizierung
|
## Authentifizierung
|
||||||
|
|
||||||
Die API hat **keine Authentifizierung**. Sie ist für den Einsatz im lokalen Netzwerk konzipiert.
|
Es gibt keine eingebaute Authentifizierung. Ripster ist für lokalen Betrieb gedacht.
|
||||||
|
|
||||||
!!! warning "Produktionsbetrieb"
|
|
||||||
Falls Ripster öffentlich erreichbar sein soll, schütze die API mit einem Reverse-Proxy (z. B. nginx mit Basic Auth oder OAuth).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Fehlerformat
|
## Fehlerformat
|
||||||
|
|
||||||
Alle API-Fehler werden im folgenden Format zurückgegeben:
|
Fehler werden zentral als JSON geliefert:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"error": "Job nicht gefunden",
|
"error": {
|
||||||
"details": "Kein Job mit ID 999 vorhanden"
|
"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 |
|
| Code | Bedeutung |
|
||||||
|-----|-----------|
|
|------|-----------|
|
||||||
| `200` | Erfolg |
|
| `200` | Erfolg |
|
||||||
| `400` | Ungültige Anfrage |
|
| `201` | Ressource erstellt |
|
||||||
|
| `400` | Ungültige Anfrage / Validierungsfehler |
|
||||||
| `404` | Ressource nicht gefunden |
|
| `404` | Ressource nicht gefunden |
|
||||||
| `409` | Konflikt (z.B. Pipeline bereits aktiv) |
|
| `409` | Konflikt (z. B. falscher Pipeline-Zustand, Job läuft bereits) |
|
||||||
| `500` | Interner Serverfehler |
|
| `500` | Interner Fehler |
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
# Pipeline API
|
# Pipeline API
|
||||||
|
|
||||||
Alle Endpunkte zur Steuerung des Ripster-Workflows.
|
Endpunkte zur Steuerung des Pipeline-Workflows.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## GET /api/pipeline/state
|
## GET /api/pipeline/state
|
||||||
|
|
||||||
Liefert den aktuellen Pipeline-Snapshot.
|
Liefert aktuellen Pipeline- und Hardware-Monitoring-Snapshot.
|
||||||
|
|
||||||
**Response:**
|
**Response (Beispiel):**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -17,45 +17,46 @@ Liefert den aktuellen Pipeline-Snapshot.
|
|||||||
"activeJobId": 42,
|
"activeJobId": 42,
|
||||||
"progress": 0,
|
"progress": 0,
|
||||||
"eta": null,
|
"eta": null,
|
||||||
"statusText": "Mediainfo geladen - bitte bestätigen",
|
"statusText": "Mediainfo bestätigt - Encode manuell starten",
|
||||||
"context": {
|
"context": {
|
||||||
"jobId": 42
|
"jobId": 42
|
||||||
},
|
},
|
||||||
|
"jobProgress": {
|
||||||
|
"42": {
|
||||||
|
"state": "MEDIAINFO_CHECK",
|
||||||
|
"progress": 68.5,
|
||||||
|
"eta": null,
|
||||||
|
"statusText": "MEDIAINFO_CHECK 68.50%"
|
||||||
|
}
|
||||||
|
},
|
||||||
"queue": {
|
"queue": {
|
||||||
"maxParallelJobs": 1,
|
"maxParallelJobs": 1,
|
||||||
"runningCount": 0,
|
"runningCount": 1,
|
||||||
"queuedCount": 0,
|
"queuedCount": 2,
|
||||||
"runningJobs": [],
|
"runningJobs": [],
|
||||||
"queuedJobs": []
|
"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
|
## POST /api/pipeline/analyze
|
||||||
|
|
||||||
Startet die Analyse für die aktuell erkannte Disc.
|
Startet Disc-Analyse und legt Job an.
|
||||||
|
|
||||||
**Request:** kein Body
|
|
||||||
|
|
||||||
**Response:**
|
**Response:**
|
||||||
|
|
||||||
@@ -73,14 +74,21 @@ Startet die Analyse für die aktuell erkannte Disc.
|
|||||||
|
|
||||||
## POST /api/pipeline/rescan-disc
|
## POST /api/pipeline/rescan-disc
|
||||||
|
|
||||||
Erzwingt eine erneute Laufwerksprüfung.
|
Erzwingt erneute Laufwerksprüfung.
|
||||||
|
|
||||||
**Response (Beispiel):**
|
**Response (Beispiel):**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"result": {
|
"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=<query>
|
## GET /api/pipeline/omdb/search?q=<query>
|
||||||
|
|
||||||
Sucht OMDb-Titel.
|
OMDb-Titelsuche.
|
||||||
|
|
||||||
**Response:**
|
**Response:**
|
||||||
|
|
||||||
@@ -111,7 +119,7 @@ Sucht OMDb-Titel.
|
|||||||
|
|
||||||
## POST /api/pipeline/select-metadata
|
## POST /api/pipeline/select-metadata
|
||||||
|
|
||||||
Setzt Metadaten (und optional Playlist-Entscheidung).
|
Setzt Metadaten (und optional Playlist) für einen Job.
|
||||||
|
|
||||||
**Request:**
|
**Request:**
|
||||||
|
|
||||||
@@ -127,38 +135,35 @@ Setzt Metadaten (und optional Playlist-Entscheidung).
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response:** `{ "job": { ... } }`
|
**Response:**
|
||||||
|
|
||||||
!!! note "Startlogik"
|
```json
|
||||||
Nach Metadaten-Bestätigung wird der nächste Schritt automatisch ausgelöst (`startPreparedJob`).
|
{ "job": { "id": 42, "status": "READY_TO_START" } }
|
||||||
Der Job startet direkt oder wird in die Queue eingereiht.
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## POST /api/pipeline/start/:jobId
|
## 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
|
```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
|
## 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
|
```json
|
||||||
{
|
{
|
||||||
@@ -169,78 +174,70 @@ Bestätigt Review-Auswahl (Titel/Tracks/Post-Skripte).
|
|||||||
"subtitleTrackIds": [3]
|
"subtitleTrackIds": [3]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"selectedPreEncodeScriptIds": [1],
|
||||||
"selectedPostEncodeScriptIds": [2, 7],
|
"selectedPostEncodeScriptIds": [2, 7],
|
||||||
|
"selectedPreEncodeChainIds": [3],
|
||||||
|
"selectedPostEncodeChainIds": [4],
|
||||||
|
"selectedUserPresetId": 5,
|
||||||
"skipPipelineStateUpdate": false
|
"skipPipelineStateUpdate": false
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response:** `{ "job": { ... } }`
|
**Response:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "job": { "id": 42, "encode_review_confirmed": 1 } }
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## POST /api/pipeline/cancel
|
## POST /api/pipeline/cancel
|
||||||
|
|
||||||
Bricht laufenden Job ab oder entfernt einen Queue-Eintrag.
|
Bricht laufenden Job ab oder entfernt Queue-Eintrag.
|
||||||
|
|
||||||
**Request (optional):**
|
**Request (optional):**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{ "jobId": 42 }
|
||||||
"jobId": 42
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response (Beispiel):**
|
**Mögliche Responses:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{ "result": { "cancelled": true, "queuedOnly": true, "jobId": 42 } }
|
||||||
"result": {
|
```
|
||||||
"cancelled": true,
|
|
||||||
"queuedOnly": false,
|
```json
|
||||||
"jobId": 42
|
{ "result": { "cancelled": true, "queuedOnly": false, "jobId": 42 } }
|
||||||
}
|
```
|
||||||
}
|
|
||||||
|
```json
|
||||||
|
{ "result": { "cancelled": true, "queuedOnly": false, "pending": true, "jobId": 42 } }
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## POST /api/pipeline/retry/:jobId
|
## POST /api/pipeline/retry/:jobId
|
||||||
|
|
||||||
Startet einen Job aus `ERROR`/`CANCELLED` erneut (oder reiht ihn in die Queue ein).
|
Retry für `ERROR`/`CANCELLED`-Jobs (oder Queue-Einreihung).
|
||||||
|
|
||||||
**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
|
## POST /api/pipeline/reencode/:jobId
|
||||||
|
|
||||||
Startet Re-Encode aus bestehendem RAW.
|
Startet Re-Encode aus bestehendem RAW.
|
||||||
|
|
||||||
**Response:** `{ "result": { ... } }`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## POST /api/pipeline/restart-review/:jobId
|
## POST /api/pipeline/restart-review/:jobId
|
||||||
|
|
||||||
Berechnet die Review aus vorhandenem RAW neu.
|
Berechnet Review aus RAW neu.
|
||||||
|
|
||||||
**Response:** `{ "result": { ... } }`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## POST /api/pipeline/restart-encode/:jobId
|
## 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
|
### 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
|
### POST /api/pipeline/queue/reorder
|
||||||
|
|
||||||
@@ -260,8 +299,71 @@ Sortiert Queue-Einträge neu.
|
|||||||
|
|
||||||
```json
|
```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 |
|
||||||
|
|||||||
@@ -1,38 +1,36 @@
|
|||||||
# Settings API
|
# Settings API
|
||||||
|
|
||||||
Endpunkte zum Lesen und Schreiben der Anwendungseinstellungen.
|
Endpunkte für Einstellungen, Skripte, Skript-Ketten und User-Presets.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## GET /api/settings
|
## GET /api/settings
|
||||||
|
|
||||||
Gibt alle Einstellungen kategorisiert zurück.
|
Liefert alle Einstellungen kategorisiert.
|
||||||
|
|
||||||
**Response:**
|
**Response (Struktur):**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"paths": {
|
"categories": [
|
||||||
"raw_dir": {
|
{
|
||||||
"value": "/mnt/nas/raw",
|
"category": "Pfade",
|
||||||
"schema": {
|
"settings": [
|
||||||
"type": "string",
|
{
|
||||||
"label": "Raw-Verzeichnis",
|
"key": "raw_dir",
|
||||||
"description": "Speicherort für rohe MKV-Dateien",
|
"label": "Raw Ausgabeordner",
|
||||||
"required": true
|
"type": "path",
|
||||||
}
|
"required": true,
|
||||||
},
|
"description": "...",
|
||||||
"movie_dir": {
|
"defaultValue": "data/output/raw",
|
||||||
"value": "/mnt/nas/movies",
|
"options": [],
|
||||||
"schema": { ... }
|
"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.
|
Aktualisiert eine einzelne Einstellung.
|
||||||
|
|
||||||
**URL-Parameter:** `key` – Einstellungs-Schlüssel
|
|
||||||
|
|
||||||
**Request:**
|
**Request:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{ "value": "/mnt/storage/raw" }
|
||||||
"value": "/mnt/storage/raw"
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response:**
|
**Response:**
|
||||||
|
|
||||||
```json
|
```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:**
|
`reviewRefresh` ist `null` oder ein Objekt mit Status der optionalen Review-Neuberechnung.
|
||||||
- `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.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## PUT /api/settings
|
## PUT /api/settings
|
||||||
|
|
||||||
Aktualisiert mehrere Einstellungen auf einmal.
|
Aktualisiert mehrere Einstellungen atomar.
|
||||||
|
|
||||||
**Request:**
|
**Request:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"raw_dir": "/mnt/storage/raw",
|
"settings": {
|
||||||
"movie_dir": "/mnt/storage/movies",
|
"raw_dir": "/mnt/storage/raw",
|
||||||
"handbrake_preset": "H.265 MKV 720p30"
|
"movie_dir": "/mnt/storage/movies",
|
||||||
|
"handbrake_preset_bluray": "H.264 MKV 1080p30"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -85,9 +85,36 @@ Aktualisiert mehrere Einstellungen auf einmal.
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"ok": true,
|
"changes": [
|
||||||
"updated": ["raw_dir", "movie_dir", "handbrake_preset"],
|
{ "key": "raw_dir", "value": "/mnt/storage/raw" },
|
||||||
"errors": []
|
{ "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
|
## POST /api/settings/pushover/test
|
||||||
|
|
||||||
Sendet eine Test-Benachrichtigung über PushOver.
|
Sendet Testnachricht über aktuelle PushOver-Settings.
|
||||||
|
|
||||||
**Request:** Kein Body erforderlich (verwendet gespeicherte Zugangsdaten)
|
**Request (optional):**
|
||||||
|
|
||||||
**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:**
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"scripts": [
|
"title": "Test",
|
||||||
{
|
"message": "Ripster Test"
|
||||||
"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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 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:**
|
**Response:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"result": {
|
"result": {
|
||||||
"success": true,
|
"sent": true,
|
||||||
"steps": [
|
"eventKey": "test",
|
||||||
{ "scriptId": 1, "scriptName": "Zu Plex verschieben", "success": true, "exitCode": 0 }
|
"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
|
### POST /api/settings/script-chains/reorder
|
||||||
|
|
||||||
Ändert die Reihenfolge der Ketten (persistiert in `order_index`).
|
|
||||||
|
|
||||||
**Request:**
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "orderedChainIds": [2, 1, 3] }
|
{ "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
|
## 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
|
### GET /api/settings/user-presets
|
||||||
|
|
||||||
Gibt alle User-Presets zurück. Optional gefiltert per Query-Parameter `mediaType`.
|
Optionaler Query-Parameter: `media_type=bluray|dvd|other|all`
|
||||||
|
|
||||||
**Query-Parameter:**
|
|
||||||
|
|
||||||
| Parameter | Werte | Beschreibung |
|
|
||||||
|-----------|-------|-------------|
|
|
||||||
| `mediaType` | `bluray`, `dvd`, `other`, `all` | Filtert Presets nach Medientyp |
|
|
||||||
|
|
||||||
**Response:**
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"presets": [
|
"presets": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": "Blu-ray High Quality",
|
"name": "Blu-ray HQ",
|
||||||
"mediaType": "bluray",
|
"mediaType": "bluray",
|
||||||
"handbrakePreset": "H.265 MKV 1080p30",
|
"handbrakePreset": "H.264 MKV 1080p30",
|
||||||
"extraArgs": "--encoder-preset slow",
|
"extraArgs": "--encoder-preset slow",
|
||||||
"description": "Langsam, aber beste Qualität",
|
"description": "...",
|
||||||
"createdAt": "2026-01-15T10:00:00.000Z",
|
"createdAt": "...",
|
||||||
"updatedAt": "2026-01-15T10:00:00.000Z"
|
"updatedAt": "..."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### POST /api/settings/user-presets
|
### POST /api/settings/user-presets
|
||||||
|
|
||||||
Legt ein neues User-Preset an.
|
|
||||||
|
|
||||||
**Request:**
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"name": "Blu-ray High Quality",
|
"name": "Blu-ray HQ",
|
||||||
"mediaType": "bluray",
|
"mediaType": "bluray",
|
||||||
"handbrakePreset": "H.265 MKV 1080p30",
|
"handbrakePreset": "H.264 MKV 1080p30",
|
||||||
"extraArgs": "--encoder-preset slow",
|
"extraArgs": "--encoder-preset slow",
|
||||||
"description": "Langsam, aber beste Qualität"
|
"description": "optional"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
| Feld | Typ | Pflicht | Beschreibung |
|
Response: `201` mit `{ "preset": { ... } }`
|
||||||
|------|-----|---------|-------------|
|
|
||||||
| `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": { ... } }`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### PUT /api/settings/user-presets/:id
|
### 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
|
### DELETE /api/settings/user-presets/:id
|
||||||
|
|
||||||
Löscht ein User-Preset.
|
Response `{ "removed": { ... } }`.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Einstellungs-Schlüssel Referenz
|
|
||||||
|
|
||||||
Eine vollständige Übersicht aller Schlüssel:
|
|
||||||
[:octicons-arrow-right-24: Einstellungsreferenz](../configuration/settings-reference.md)
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# WebSocket Events
|
# 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');
|
const ws = new WebSocket('ws://localhost:3001/ws');
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
const message = JSON.parse(event.data);
|
const msg = JSON.parse(event.data);
|
||||||
console.log(message.type, message.payload);
|
console.log(msg.type, msg.payload);
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -19,36 +19,38 @@ ws.onmessage = (event) => {
|
|||||||
|
|
||||||
## Nachrichtenformat
|
## Nachrichtenformat
|
||||||
|
|
||||||
Alle Broadcasts haben dieses Schema:
|
Die meisten Broadcasts haben dieses Schema:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"type": "EVENT_TYPE",
|
"type": "EVENT_TYPE",
|
||||||
"payload": { },
|
"payload": {},
|
||||||
"timestamp": "2026-03-05T10:00:00.000Z"
|
"timestamp": "2026-03-10T09:00:00.000Z"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Ausnahme: `WS_CONNECTED` beim Verbindungsaufbau enthält kein `timestamp`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Event-Typen
|
## Event-Typen
|
||||||
|
|
||||||
### WS_CONNECTED
|
### WS_CONNECTED
|
||||||
|
|
||||||
Wird direkt nach Verbindungsaufbau gesendet.
|
Sofort nach erfolgreicher Verbindung.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"type": "WS_CONNECTED",
|
"type": "WS_CONNECTED",
|
||||||
"payload": {
|
"payload": {
|
||||||
"connectedAt": "2026-03-05T10:00:00.000Z"
|
"connectedAt": "2026-03-10T09:00:00.000Z"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### PIPELINE_STATE_CHANGED
|
### PIPELINE_STATE_CHANGED
|
||||||
|
|
||||||
Snapshot bei Zustandswechsel.
|
Neuer Pipeline-Snapshot.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -56,14 +58,24 @@ Snapshot bei Zustandswechsel.
|
|||||||
"payload": {
|
"payload": {
|
||||||
"state": "ENCODING",
|
"state": "ENCODING",
|
||||||
"activeJobId": 42,
|
"activeJobId": 42,
|
||||||
"progress": 73.5,
|
"progress": 62.5,
|
||||||
"eta": "00:12:34",
|
"eta": "00:12:34",
|
||||||
"statusText": "Encoding mit HandBrake",
|
"statusText": "ENCODING 62.50%",
|
||||||
"context": {},
|
"context": {},
|
||||||
|
"jobProgress": {
|
||||||
|
"42": {
|
||||||
|
"state": "ENCODING",
|
||||||
|
"progress": 62.5,
|
||||||
|
"eta": "00:12:34",
|
||||||
|
"statusText": "ENCODING 62.50%"
|
||||||
|
}
|
||||||
|
},
|
||||||
"queue": {
|
"queue": {
|
||||||
"maxParallelJobs": 1,
|
"maxParallelJobs": 1,
|
||||||
"runningCount": 1,
|
"runningCount": 1,
|
||||||
"queuedCount": 0
|
"queuedCount": 2,
|
||||||
|
"runningJobs": [],
|
||||||
|
"queuedJobs": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,7 +83,7 @@ Snapshot bei Zustandswechsel.
|
|||||||
|
|
||||||
### PIPELINE_PROGRESS
|
### PIPELINE_PROGRESS
|
||||||
|
|
||||||
Laufende Fortschrittsupdates während aktiver Phasen.
|
Laufende Fortschrittsupdates.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -79,33 +91,20 @@ Laufende Fortschrittsupdates während aktiver Phasen.
|
|||||||
"payload": {
|
"payload": {
|
||||||
"state": "ENCODING",
|
"state": "ENCODING",
|
||||||
"activeJobId": 42,
|
"activeJobId": 42,
|
||||||
"progress": 73.5,
|
"progress": 62.5,
|
||||||
"eta": "00:12:34",
|
"eta": "00:12:34",
|
||||||
"statusText": "ENCODING 73.50% - task 1 of 1"
|
"statusText": "ENCODING 62.50%"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### PIPELINE_QUEUE_CHANGED
|
### PIPELINE_QUEUE_CHANGED
|
||||||
|
|
||||||
Aktualisierung der Job-Queue.
|
Queue-Snapshot aktualisiert.
|
||||||
|
|
||||||
```json
|
### DISC_DETECTED / DISC_REMOVED
|
||||||
{
|
|
||||||
"type": "PIPELINE_QUEUE_CHANGED",
|
|
||||||
"payload": {
|
|
||||||
"maxParallelJobs": 1,
|
|
||||||
"runningCount": 1,
|
|
||||||
"queuedCount": 2,
|
|
||||||
"runningJobs": [],
|
|
||||||
"queuedJobs": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### DISC_DETECTED
|
Disc-Insertion/-Removal.
|
||||||
|
|
||||||
Disc erkannt.
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -114,7 +113,6 @@ Disc erkannt.
|
|||||||
"device": {
|
"device": {
|
||||||
"path": "/dev/sr0",
|
"path": "/dev/sr0",
|
||||||
"discLabel": "INCEPTION",
|
"discLabel": "INCEPTION",
|
||||||
"label": "INCEPTION",
|
|
||||||
"model": "ASUS BW-16D1HT",
|
"model": "ASUS BW-16D1HT",
|
||||||
"fstype": "udf",
|
"fstype": "udf",
|
||||||
"mountpoint": null,
|
"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
|
```json
|
||||||
{
|
{
|
||||||
"type": "DISC_REMOVED",
|
"type": "HARDWARE_MONITOR_UPDATE",
|
||||||
"payload": {
|
"payload": {
|
||||||
"device": {
|
"enabled": true,
|
||||||
"path": "/dev/sr0"
|
"intervalMs": 5000,
|
||||||
}
|
"updatedAt": "2026-03-10T09:00:00.000Z",
|
||||||
|
"sample": {
|
||||||
|
"cpu": {},
|
||||||
|
"memory": {},
|
||||||
|
"gpu": {},
|
||||||
|
"storage": {}
|
||||||
|
},
|
||||||
|
"error": null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### PIPELINE_ERROR
|
### PIPELINE_ERROR
|
||||||
|
|
||||||
Fehler bei Pipeline-Disc-Events im Backend.
|
Fehler bei Disc-Event-Verarbeitung in Pipeline.
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "PIPELINE_ERROR",
|
|
||||||
"payload": {
|
|
||||||
"message": "..."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### DISK_DETECTION_ERROR
|
### DISK_DETECTION_ERROR
|
||||||
|
|
||||||
Fehler im Laufwerkserkennungsdienst.
|
Fehler in Laufwerkserkennung.
|
||||||
|
|
||||||
|
### SETTINGS_UPDATED
|
||||||
|
|
||||||
|
Einzelnes Setting wurde gespeichert.
|
||||||
|
|
||||||
|
### SETTINGS_BULK_UPDATED
|
||||||
|
|
||||||
|
Bulk-Settings gespeichert.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"type": "DISK_DETECTION_ERROR",
|
"type": "SETTINGS_BULK_UPDATED",
|
||||||
"payload": {
|
"payload": {
|
||||||
"message": "..."
|
"count": 3,
|
||||||
|
"keys": ["raw_dir", "movie_dir", "handbrake_preset_bluray"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### SETTINGS_SCRIPTS_UPDATED
|
### SETTINGS_SCRIPTS_UPDATED
|
||||||
|
|
||||||
Wird gesendet, wenn ein Skript angelegt, aktualisiert, gelöscht oder umsortiert wurde.
|
Skript geändert (`created|updated|deleted|reordered`).
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "SETTINGS_SCRIPTS_UPDATED",
|
|
||||||
"payload": {
|
|
||||||
"action": "reordered",
|
|
||||||
"count": 3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`action` ist `"created"`, `"updated"`, `"deleted"` oder `"reordered"`.
|
|
||||||
|
|
||||||
### SETTINGS_SCRIPT_CHAINS_UPDATED
|
### SETTINGS_SCRIPT_CHAINS_UPDATED
|
||||||
|
|
||||||
Wird gesendet bei Änderungen an Skript-Ketten.
|
Skript-Kette geändert (`created|updated|deleted|reordered`).
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "SETTINGS_SCRIPT_CHAINS_UPDATED",
|
|
||||||
"payload": {
|
|
||||||
"action": "created",
|
|
||||||
"id": 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### USER_PRESETS_UPDATED
|
### USER_PRESETS_UPDATED
|
||||||
|
|
||||||
Wird gesendet, wenn ein User-Preset angelegt, aktualisiert oder gelöscht wurde.
|
User-Preset geändert (`created|updated|deleted`).
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "USER_PRESETS_UPDATED",
|
|
||||||
"payload": {
|
|
||||||
"action": "created",
|
|
||||||
"id": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`action` ist `"created"`, `"updated"` oder `"deleted"`.
|
|
||||||
|
|
||||||
### CRON_JOBS_UPDATED
|
### 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
|
```json
|
||||||
{
|
{
|
||||||
"type": "CRON_JOBS_UPDATED",
|
"type": "CRON_JOB_UPDATED",
|
||||||
"payload": {
|
"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
|
## Reconnect-Verhalten
|
||||||
|
|
||||||
`useWebSocket.js` versucht bei Verbindungsabbruch automatisch erneut zu verbinden.
|
`useWebSocket` verbindet bei Abbruch automatisch neu:
|
||||||
|
|
||||||
- fester Retry-Intervall: `1500ms`
|
- Retry-Intervall: `1500ms`
|
||||||
- erneuter Versuch bis zum Unmount der Komponente
|
- Wiederverbindung bis Komponente unmounted wird
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## React-Beispiel
|
|
||||||
|
|
||||||
```js
|
|
||||||
import { useWebSocket } from './hooks/useWebSocket';
|
|
||||||
|
|
||||||
useWebSocket({
|
|
||||||
onMessage: (msg) => {
|
|
||||||
if (msg.type === 'PIPELINE_STATE_CHANGED') {
|
|
||||||
setPipeline(msg.payload);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -1,305 +1,116 @@
|
|||||||
# Backend-Services
|
# 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
|
- Pipeline-State-Machine + Persistenz (`pipeline_state`)
|
||||||
- Koordination zwischen allen externen Tools
|
- Disc-Analyse/Rip/Review/Encode
|
||||||
- Generierung von Encode-Plänen
|
- Queue-Management (Jobs + `script|chain|wait` Einträge)
|
||||||
- Fehlerbehandlung und Recovery
|
- Retry/Re-Encode/Restart-Flows
|
||||||
|
- WebSocket-Broadcasts für State/Progress/Queue
|
||||||
|
|
||||||
### Haupt-Methoden
|
Wichtige Methoden:
|
||||||
|
|
||||||
| Methode | Beschreibung |
|
- `analyzeDisc()`
|
||||||
|---------|-------------|
|
- `selectMetadata()`
|
||||||
| `analyzeDisc()` | Legt Job an und öffnet Metadaten-Auswahl |
|
- `startPreparedJob()`
|
||||||
| `selectMetadata({...})` | Setzt Metadaten/Playlist und triggert Auto-Start |
|
- `confirmEncodeReview()`
|
||||||
| `startPreparedJob(jobId)` | Startet vorbereiteten Job (oder Queue) |
|
- `cancel()`
|
||||||
| `confirmEncodeReview(jobId, options)` | Bestätigt Review inkl. Track/Skript-Auswahl |
|
- `retry()`
|
||||||
| `cancel(jobId)` | Bricht laufenden Job ab oder entfernt Queue-Eintrag |
|
- `reencodeFromRaw()`
|
||||||
| `retry(jobId)` | Startet fehlgeschlagenen/abgebrochenen Job neu |
|
- `restartReviewFromRaw()`
|
||||||
| `reencodeFromRaw(jobId)` | Encodiert aus vorhandenem RAW neu |
|
- `restartEncodeWithLastSettings()`
|
||||||
| `restartReviewFromRaw(jobId)` | Berechnet Review aus RAW neu |
|
- `resumeReadyToEncodeJob()`
|
||||||
| `restartEncodeWithLastSettings(jobId)` | Neustart mit letzter bestätigter Auswahl |
|
- `enqueueNonJobEntry()`, `reorderQueue()`, `removeQueueEntry()`
|
||||||
| `resumeReadyToEncodeJob(jobId)` | Lädt READY_TO_ENCODE nach Neustart in die Session |
|
|
||||||
|
|
||||||
### Zustandsübergänge
|
|
||||||
|
|
||||||
<div class="pipeline-diagram">
|
|
||||||
|
|
||||||
```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
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 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 |
|
Zusatz:
|
||||||
|------|-------------|
|
|
||||||
| `auto` | Erkennt verfügbare Laufwerke automatisch |
|
|
||||||
| `explicit` | Überwacht ein bestimmtes Gerät (z.B. `/dev/sr0`) |
|
|
||||||
|
|
||||||
### Polling
|
- Modus `auto` oder `explicit`
|
||||||
|
- heuristische `mediaProfile`-Erkennung (`bluray`/`dvd`/`other`)
|
||||||
Der Service pollt das Laufwerk im konfigurierten Intervall (`disc_poll_interval_ms`, Standard: 4000ms) und emittiert Events:
|
- `rescanAndEmit()` für manuellen Trigger
|
||||||
|
|
||||||
```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
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## processRunner.js
|
## `settingsService.js`
|
||||||
|
|
||||||
Verwaltet externe CLI-Prozesse.
|
Settings-Layer mit Validation/Serialisierung.
|
||||||
|
|
||||||
### Features
|
Features:
|
||||||
|
|
||||||
- **Streaming**: stdout/stderr werden zeilenweise gelesen
|
- `getCategorizedSettings()` für UI-Form
|
||||||
- **Progress-Callbacks**: Ermöglicht Echtzeit-Fortschrittsanzeige
|
- `setSettingValue()` / `setSettingsBulk()`
|
||||||
- **Graceful Shutdown**: SIGINT → Warte-Timeout → SIGKILL
|
- profilspezifische Auflösung (`resolveEffectiveToolSettings`)
|
||||||
- **Prozess-Registry**: Verfolgt aktive Prozesse für sauberes Beenden
|
- CLI-Config-Building für MakeMKV/HandBrake/MediaInfo
|
||||||
|
- HandBrake-Preset-Liste via `HandBrakeCLI -z`
|
||||||
### Nutzung
|
- MakeMKV-Registration-Command aus `makemkv_registration_key`
|
||||||
|
|
||||||
```js
|
|
||||||
const result = await runProcess(
|
|
||||||
'HandBrakeCLI',
|
|
||||||
['--input', rawFile, '--output', outputFile, '--preset', preset],
|
|
||||||
{
|
|
||||||
onStderr: (line) => parseHandBrakeProgress(line),
|
|
||||||
onStdout: (line) => logger.debug(line)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## websocketService.js
|
## `historyService.js`
|
||||||
|
|
||||||
WebSocket-Server für Echtzeit-Client-Kommunikation.
|
Historie + Dateioperationen.
|
||||||
|
|
||||||
### Betrieb
|
Features:
|
||||||
|
|
||||||
- Läuft auf Pfad `/ws` des Express-Servers
|
- Job-Liste/Detail inkl. Log-Tail
|
||||||
- Hält eine Registry aller verbundenen Clients
|
- Orphan-RAW-Erkennung und Import
|
||||||
- Ermöglicht Broadcast an alle Clients oder gezieltes Senden
|
- OMDb-Nachzuweisung
|
||||||
|
- Dateilöschung (`raw|movie|both`)
|
||||||
### API
|
- Job-Löschung (`none|raw|movie|both`)
|
||||||
|
|
||||||
```js
|
|
||||||
broadcast('PIPELINE_STATE_CHANGED', { state, activeJobId });
|
|
||||||
broadcast('PIPELINE_PROGRESS', { state, progress, eta, statusText });
|
|
||||||
broadcast('PIPELINE_QUEUE_CHANGED', queueSnapshot);
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## omdbService.js
|
## `cronService.js`
|
||||||
|
|
||||||
Integration mit der [OMDb API](https://www.omdbapi.com/).
|
Integriertes Cron-System ohne externe Parser-Library.
|
||||||
|
|
||||||
### Methoden
|
Features:
|
||||||
|
|
||||||
| Methode | Beschreibung |
|
- 5-Feld-Cron-Parser + `nextRun`-Berechnung
|
||||||
|---------|-------------|
|
- Quellen: `script` oder `chain`
|
||||||
| `searchByTitle(title, type)` | Suche nach Titel (movie/series) |
|
- Laufzeitlogs (`cron_run_logs`)
|
||||||
| `fetchById(imdbId)` | Vollständige Metadaten per IMDb-ID |
|
- manuelles Triggern
|
||||||
|
- WebSocket-Events: `CRON_JOBS_UPDATED`, `CRON_JOB_UPDATED`
|
||||||
### Zurückgegebene Daten
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"imdbId": "tt1375666",
|
|
||||||
"title": "Inception",
|
|
||||||
"year": "2010",
|
|
||||||
"type": "movie",
|
|
||||||
"poster": "https://...",
|
|
||||||
"plot": "...",
|
|
||||||
"director": "Christopher Nolan"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## settingsService.js
|
## Weitere Services
|
||||||
|
|
||||||
Verwaltet alle Anwendungseinstellungen.
|
- `scriptService.js` (CRUD + Test + Wrapper-Ausführung)
|
||||||
|
- `scriptChainService.js` (CRUD + Step-Execution)
|
||||||
### Features
|
- `userPresetService.js` (HandBrake User-Presets)
|
||||||
|
- `hardwareMonitorService.js` (CPU/RAM/GPU/Storage)
|
||||||
- **Schema-getriebene Validierung**: Jede Einstellung hat Typ, Grenzen und Pflichtfeld-Flag
|
- `websocketService.js` (Client-Registry + Broadcast)
|
||||||
- **Kategorisierung**: Einstellungen sind in Kategorien gruppiert (Pfade, Tools, Metadaten, …)
|
- `notificationService.js` (PushOver)
|
||||||
- **Persistenz**: Werte in SQLite, Schema ebenfalls in SQLite
|
- `logger.js` (rotierende Datei-Logs)
|
||||||
- **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_*` |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## userPresetService.js
|
## Bootstrapping (`src/index.js`)
|
||||||
|
|
||||||
Verwaltet benannte HandBrake-Preset-Sammlungen pro Medientyp.
|
Beim Start:
|
||||||
|
|
||||||
### Methoden
|
1. DB init/migrate
|
||||||
|
2. Pipeline-Init
|
||||||
| Methode | Beschreibung |
|
3. Cron-Init
|
||||||
|---------|-------------|
|
4. Express-Routes + Error-Handler
|
||||||
| `listPresets(mediaType?)` | Alle Presets; optional nach Medientyp filtern (`bluray`/`dvd`/`other`/`all`) |
|
5. WebSocket-Server auf `/ws`
|
||||||
| `getPresetById(id)` | Einzelnes Preset |
|
6. Hardware-Monitoring-Init
|
||||||
| `createPreset(payload)` | Neues Preset anlegen |
|
7. Disk-Detection-Start
|
||||||
| `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,273 +1,112 @@
|
|||||||
# Datenbank
|
# 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
|
```text
|
||||||
settings_schema -- Einstellungs-Definitionen
|
settings_schema
|
||||||
settings_values -- Benutzer-Werte
|
settings_values
|
||||||
jobs -- Rip-Job-Datensätze
|
jobs
|
||||||
pipeline_state -- Aktueller Pipeline-Zustand (Singleton)
|
pipeline_state
|
||||||
scripts -- Shell-Skripte für Pre-/Post-Encode-Ausführung
|
scripts
|
||||||
script_chains -- Geordnete Ketten aus mehreren Skripten
|
script_chains
|
||||||
script_chain_steps -- Einzelschritte einer Skript-Kette
|
script_chain_steps
|
||||||
user_presets -- Benannte HandBrake-Preset-Sammlungen pro Medientyp
|
user_presets
|
||||||
cron_jobs -- Zeitgesteuerte Aufgaben (eigener Cron-Parser)
|
cron_jobs
|
||||||
cron_run_logs -- Ausführungs-Protokolle für Cron-Jobs
|
cron_run_logs
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Tabelle: jobs
|
## `jobs`
|
||||||
|
|
||||||
Die wichtigste Tabelle – speichert alle Ripping-Jobs.
|
Speichert Pipeline-Lifecycle und Artefakte pro Job.
|
||||||
|
|
||||||
```sql
|
Zentrale Felder:
|
||||||
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
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! info "rip_successful"
|
- Metadaten: `title`, `year`, `imdb_id`, `poster_url`, `omdb_json`, `selected_from_omdb`
|
||||||
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.
|
- Laufzeit: `start_time`, `end_time`, `status`, `last_state`
|
||||||
|
- Pfade: `raw_path`, `output_path`, `encode_input_path`
|
||||||
### Job-Status-Werte
|
- Tool-Ausgaben: `makemkv_info_json`, `handbrake_info_json`, `mediainfo_info_json`, `encode_plan_json`
|
||||||
|
- Kontrolle: `encode_review_confirmed`, `rip_successful`, `error_message`
|
||||||
| Status | Beschreibung |
|
- Audit: `created_at`, `updated_at`
|
||||||
|--------|-------------|
|
|
||||||
| `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
|
## `pipeline_state`
|
||||||
|
|
||||||
Singleton-Tabelle für den aktuellen Pipeline-Zustand (immer genau 1 Zeile).
|
Singleton-Tabelle (`id = 1`) für aktiven Snapshot:
|
||||||
|
|
||||||
```sql
|
- `state`
|
||||||
CREATE TABLE pipeline_state (
|
- `active_job_id`
|
||||||
id INTEGER PRIMARY KEY CHECK(id = 1),
|
- `progress`
|
||||||
state TEXT NOT NULL DEFAULT 'IDLE',
|
- `eta`
|
||||||
job_id INTEGER, -- Aktiver Job (NULL wenn IDLE)
|
- `status_text`
|
||||||
progress REAL, -- Fortschritt 0-100
|
- `context_json`
|
||||||
eta TEXT, -- Geschätzte Restzeit
|
- `updated_at`
|
||||||
updated_at TEXT NOT NULL
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Tabelle: settings_schema
|
## `settings_schema` + `settings_values`
|
||||||
|
|
||||||
Definiert alle verfügbaren Einstellungen mit Metadaten.
|
- `settings_schema`: Definition (Typ, Default, Validation, Reihenfolge)
|
||||||
|
- `settings_values`: aktueller Wert pro Key
|
||||||
```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
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Tabelle: settings_values
|
## `scripts`, `script_chains`, `script_chain_steps`
|
||||||
|
|
||||||
Speichert benutzer-konfigurierte Werte.
|
- `scripts`: Shell-Skripte (`name`, `script_body`, `order_index`)
|
||||||
|
- `script_chains`: Ketten (`name`, `order_index`)
|
||||||
```sql
|
- `script_chain_steps`: Schritte je Kette
|
||||||
CREATE TABLE settings_values (
|
- `step_type`: `script` oder `wait`
|
||||||
key TEXT PRIMARY KEY REFERENCES settings_schema(key),
|
- `script_id` oder `wait_seconds`
|
||||||
value TEXT NOT NULL,
|
|
||||||
updated_at TEXT NOT NULL
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Tabelle: scripts
|
## `user_presets`
|
||||||
|
|
||||||
Verwaltet Shell-Skripte, die vor oder nach dem Encode-Schritt ausgeführt werden können.
|
Benannte HandBrake-Preset-Sets:
|
||||||
|
|
||||||
```sql
|
- `name`
|
||||||
CREATE TABLE scripts (
|
- `media_type` (`bluray|dvd|other|all`)
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
- `handbrake_preset`
|
||||||
name TEXT NOT NULL UNIQUE,
|
- `extra_args`
|
||||||
script_body TEXT NOT NULL,
|
- `description`
|
||||||
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.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Tabelle: user_presets
|
## `cron_jobs` + `cron_run_logs`
|
||||||
|
|
||||||
Speichert benannte HandBrake-Preset-Sammlungen pro Medientyp.
|
- `cron_jobs`: Zeitplan + Status
|
||||||
|
- `cron_run_logs`: einzelne Läufe
|
||||||
```sql
|
- `status`: `running|success|error`
|
||||||
CREATE TABLE user_presets (
|
- `output`
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
- `error_message`
|
||||||
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.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 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
|
Bei korruptem SQLite-File:
|
||||||
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
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE cron_run_logs (
|
1. Datei wird nach `backend/data/corrupt-backups/` verschoben
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
2. neue DB wird initialisiert
|
||||||
cron_job_id INTEGER NOT NULL REFERENCES cron_jobs(id) ON DELETE CASCADE,
|
3. Schema wird neu aufgebaut
|
||||||
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.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Schema-Migrationen
|
## Direkte Inspektion
|
||||||
|
|
||||||
`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
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# SQLite3-CLI
|
|
||||||
sqlite3 backend/data/ripster.db
|
sqlite3 backend/data/ripster.db
|
||||||
|
|
||||||
# Alle Jobs anzeigen
|
|
||||||
.mode table
|
.mode table
|
||||||
SELECT id, status, title, created_at FROM jobs ORDER BY created_at DESC;
|
SELECT id, status, title, created_at FROM jobs ORDER BY created_at DESC;
|
||||||
|
SELECT key, value FROM settings_values ORDER BY key;
|
||||||
# Einstellungen anzeigen
|
|
||||||
SELECT key, value FROM settings_values;
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,192 +1,82 @@
|
|||||||
# Frontend-Komponenten
|
# 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:**
|
- Status/Progress/ETA
|
||||||
- Anzeige des aktuellen Pipeline-Zustands (IDLE, DISC_DETECTED, METADATA_SELECTION, RIPPING, MEDIAINFO_CHECK, READY_TO_ENCODE, ENCODING, ...)
|
- Metadaten-Dialog
|
||||||
- Live-Fortschrittsbalken mit ETA
|
- Playlist-Entscheidung
|
||||||
- Trigger für Metadaten-Dialog
|
- Review-Panel
|
||||||
- Playlist-Entscheidungs-UI (bei Blu-ray Obfuskierung)
|
- Queue-Interaktion (reorder/add/remove)
|
||||||
- Encode-Review mit Track-Auswahl
|
- Job-Aktionen (Start/Cancel/Retry/Re-Encode)
|
||||||
- Job-Steuerung (Start, Abbruch, Retry, Queue-Interaktion)
|
- Hardware-Monitoring-Anzeige
|
||||||
|
|
||||||
**Zugehörige Komponenten:**
|
### `SettingsPage.jsx`
|
||||||
- `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
|
Konfiguration:
|
||||||
|
|
||||||
Konfigurationsoberfläche für alle Ripster-Einstellungen.
|
- dynamisches Settings-Formular (`DynamicSettingsForm`)
|
||||||
|
- Skripte/Ketten inkl. Reorder/Test
|
||||||
|
- User-Presets
|
||||||
|
- Cron-Jobs (`CronJobsTab`)
|
||||||
|
|
||||||
**Funktionen:**
|
### `HistoryPage.jsx`
|
||||||
- Dynamisch generiertes Formular aus dem Settings-Schema
|
|
||||||
- Echtzeit-Validierungsfeedback
|
|
||||||
- PushOver-Verbindungstest
|
|
||||||
- Automatische Aktualisierung des Encode-Reviews bei relevanten Änderungen
|
|
||||||
|
|
||||||
### DatabasePage.jsx (`/history`)
|
Historie:
|
||||||
|
|
||||||
Job-Historie und Datenbankansicht mit vollständigem Audit-Trail.
|
- Job-Liste/Filter
|
||||||
|
- Job-Details + Logs
|
||||||
**Funktionen:**
|
- OMDb-Nachzuweisung
|
||||||
- Sortier- und filterbares Job-Verzeichnis
|
- Re-Encode/Restart-Workflows
|
||||||
- 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)
|
## Wichtige Komponenten
|
||||||
|
|
||||||
### MetadataSelectionDialog.jsx
|
- `PipelineStatusCard.jsx`
|
||||||
|
- `MetadataSelectionDialog.jsx`
|
||||||
Dialog für die Metadaten-Auswahl nach der Disc-Analyse.
|
- `MediaInfoReviewPanel.jsx`
|
||||||
|
- `JobDetailDialog.jsx`
|
||||||
```
|
- `CronJobsTab.jsx`
|
||||||
┌─────────────────────────────────────┐
|
|
||||||
│ 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
|
## API-Client (`api/client.js`)
|
||||||
|
|
||||||
### useWebSocket.js
|
- zentraler `request()` mit JSON-Handling
|
||||||
|
- Fehlerobjekt aus API wird auf `Error(message)` gemappt
|
||||||
Zentraler Custom-Hook für die WebSocket-Verbindung.
|
- `VITE_API_BASE` default `/api`
|
||||||
|
|
||||||
```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)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## API-Client (client.js)
|
## WebSocket (`hooks/useWebSocket.js`)
|
||||||
|
|
||||||
Zentraler HTTP-Client für alle Backend-Anfragen.
|
- URL: `VITE_WS_URL` oder automatisch `ws(s)://<host>/ws`
|
||||||
|
- Auto-Reconnect mit 1500ms Intervall
|
||||||
|
|
||||||
```js
|
In `App.jsx` werden u. a. verarbeitet:
|
||||||
// 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:**
|
- `PIPELINE_STATE_CHANGED`
|
||||||
- Zentralisierte Fehlerbehandlung
|
- `PIPELINE_PROGRESS`
|
||||||
- Automatische JSON-Serialisierung
|
- `PIPELINE_QUEUE_CHANGED`
|
||||||
- Basis-URL aus Umgebungsvariable (`VITE_API_BASE`)
|
- `DISC_DETECTED` / `DISC_REMOVED`
|
||||||
|
- `HARDWARE_MONITOR_UPDATE`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Build & Entwicklung
|
## Build/Run
|
||||||
|
|
||||||
### Entwicklungsserver
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd frontend
|
# dev
|
||||||
npm run dev
|
npm run dev --prefix frontend
|
||||||
# → http://localhost:5173
|
|
||||||
```
|
# prod build
|
||||||
|
npm run build --prefix frontend
|
||||||
### 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/
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Architektur
|
# 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
|
```mermaid
|
||||||
graph TB
|
graph TB
|
||||||
subgraph Browser["Browser (React)"]
|
subgraph Browser["Browser (React)"]
|
||||||
Dashboard["Dashboard"]
|
Dashboard[Dashboard]
|
||||||
Settings["Einstellungen"]
|
Settings[Einstellungen]
|
||||||
History["History"]
|
History[Historie]
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph Backend["Node.js Backend"]
|
subgraph Backend["Node.js Backend"]
|
||||||
API["REST API\n(Express)"]
|
API[REST API\nExpress]
|
||||||
WS["WebSocket\nServer"]
|
WS[WebSocket\n/ws]
|
||||||
Pipeline["Pipeline\nService"]
|
Pipeline[pipelineService]
|
||||||
DB["SQLite\nDatenbank"]
|
Cron[cronService]
|
||||||
|
DB[(SQLite)]
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph ExternalTools["Externe Tools"]
|
subgraph Tools["Externe Tools"]
|
||||||
MakeMKV["makemkvcon"]
|
MakeMKV[makemkvcon]
|
||||||
HandBrake["HandBrakeCLI"]
|
HandBrake[HandBrakeCLI]
|
||||||
MediaInfo["mediainfo"]
|
MediaInfo[mediainfo]
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph ExternalAPIs["Externe APIs"]
|
Browser <-->|HTTP| API
|
||||||
OMDb["OMDb API"]
|
|
||||||
PushOver["PushOver"]
|
|
||||||
end
|
|
||||||
|
|
||||||
Browser <-->|HTTP REST| API
|
|
||||||
Browser <-->|WebSocket| WS
|
Browser <-->|WebSocket| WS
|
||||||
Pipeline --> MakeMKV
|
Pipeline --> MakeMKV
|
||||||
Pipeline --> HandBrake
|
Pipeline --> HandBrake
|
||||||
Pipeline --> MediaInfo
|
Pipeline --> MediaInfo
|
||||||
Pipeline <-->|Metadaten| OMDb
|
|
||||||
Pipeline -->|Benachrichtigungen| PushOver
|
|
||||||
API --> DB
|
API --> DB
|
||||||
Pipeline --> DB
|
Pipeline --> DB
|
||||||
|
Cron --> DB
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Schichten-Architektur
|
## Schichten
|
||||||
|
|
||||||
### Backend
|
### Backend
|
||||||
|
|
||||||
```
|
- `src/index.js` (Bootstrapping, Routes, WS, Services)
|
||||||
index.js (Express Server)
|
- `src/routes/*` (Pipeline, Settings, History, Crons)
|
||||||
├── Routes (API-Endpunkte)
|
- `src/services/*` (Business-Logik)
|
||||||
│ ├── pipelineRoutes.js
|
- `src/db/database.js` (Init/Migration)
|
||||||
│ ├── settingsRoutes.js
|
- `src/utils/*` (Parser, Dateifunktionen, Validierung)
|
||||||
│ └── 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
|
### Frontend
|
||||||
|
|
||||||
```
|
- `App.jsx` + `pages/*` (Dashboard, Settings, History)
|
||||||
App.jsx (React Router)
|
- `components/*` (Status-/Review-/Dialog-Komponenten)
|
||||||
├── Pages
|
- `api/client.js` (REST-Client)
|
||||||
│ ├── DashboardPage.jsx ← Haupt-Interface
|
- `hooks/useWebSocket.js` (WS-Reconnect)
|
||||||
│ ├── 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
|
## Weiterführend
|
||||||
|
|
||||||
<div class="grid cards" markdown>
|
<div class="grid cards" markdown>
|
||||||
|
|
||||||
- [:octicons-arrow-right-24: Übersicht](overview.md)
|
- [:octicons-arrow-right-24: Übersicht](overview.md)
|
||||||
|
- [:octicons-arrow-right-24: Backend-Services](backend.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: Frontend-Komponenten](frontend.md)
|
|
||||||
|
|
||||||
- [:octicons-arrow-right-24: Datenbank](database.md)
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
```
|
```text
|
||||||
Zustandswechsel → Event → WebSocket → Frontend-Update
|
State-Änderung -> PIPELINE_STATE_CHANGED/PIPELINE_PROGRESS -> Frontend-Update
|
||||||
```
|
```
|
||||||
|
|
||||||
### Service-Layer-Muster
|
### Service-Layer
|
||||||
|
|
||||||
```
|
```text
|
||||||
HTTP-Route → Service → Datenbank
|
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
|
## 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
|
- `PIPELINE_STATE_CHANGED`, `PIPELINE_PROGRESS`, `PIPELINE_QUEUE_CHANGED`
|
||||||
{
|
- `DISC_DETECTED`, `DISC_REMOVED`
|
||||||
"type": "PIPELINE_STATE_CHANGED",
|
- `HARDWARE_MONITOR_UPDATE`
|
||||||
"payload": {
|
- `SETTINGS_UPDATED`, `SETTINGS_BULK_UPDATED`
|
||||||
"state": "ENCODING",
|
- `SETTINGS_SCRIPTS_UPDATED`, `SETTINGS_SCRIPT_CHAINS_UPDATED`, `USER_PRESETS_UPDATED`
|
||||||
"activeJobId": 42,
|
- `CRON_JOBS_UPDATED`, `CRON_JOB_UPDATED`
|
||||||
"progress": 73.5,
|
- `PIPELINE_ERROR`, `DISK_DETECTION_ERROR`
|
||||||
"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
|
## Prozessausführung
|
||||||
|
|
||||||
### processRunner.js
|
Externe Tools werden als Child-Processes gestartet (`processRunner`):
|
||||||
|
|
||||||
Externe Tools (MakeMKV, HandBrake, MediaInfo) werden als **Child Processes** gestartet:
|
- Streaming von stdout/stderr
|
||||||
|
- Progress-Parsing (`progressParsers.js`)
|
||||||
```js
|
- kontrollierter Abbruch (SIGINT/SIGKILL-Fallback)
|
||||||
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
|
## Persistenz
|
||||||
|
|
||||||
### SQLite-Datenbank
|
SQLite-Datei: `backend/data/ripster.db`
|
||||||
|
|
||||||
Ripster verwendet eine **einzige SQLite-Datei** für alle persistenten Daten:
|
Kern-Tabellen:
|
||||||
|
|
||||||
```
|
- `jobs`, `pipeline_state`
|
||||||
backend/data/ripster.db
|
- `settings_schema`, `settings_values`
|
||||||
```
|
- `scripts`, `script_chains`, `script_chain_steps`
|
||||||
|
- `user_presets`
|
||||||
|
- `cron_jobs`, `cron_run_logs`
|
||||||
|
|
||||||
**Tabellen:**
|
Beim Start werden Schema und Settings-Migrationen automatisch ausgeführt.
|
||||||
|
|
||||||
| 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
|
## Fehlerbehandlung
|
||||||
|
|
||||||
### Strukturierte Fehler
|
Zentrales Error-Handling liefert:
|
||||||
|
|
||||||
Alle Fehler werden mit Kontext-Metadaten protokolliert:
|
```json
|
||||||
|
{
|
||||||
```js
|
"error": {
|
||||||
logger.error('Encoding fehlgeschlagen', {
|
"message": "...",
|
||||||
jobId: job.id,
|
"statusCode": 400,
|
||||||
command: cmd,
|
"reqId": "...",
|
||||||
exitCode: code,
|
"details": []
|
||||||
stderr: lastLines
|
}
|
||||||
});
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Job-Fehler-Recovery
|
Fehlgeschlagene Jobs bleiben in der Historie (`ERROR` oder `CANCELLED`) und können erneut gestartet werden.
|
||||||
|
|
||||||
- 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
|
## CORS & Runtime-Konfig
|
||||||
|
|
||||||
### Eingabe-Validierung
|
- `CORS_ORIGIN` default: `*`
|
||||||
|
- `LOG_LEVEL` default: `info`
|
||||||
- Alle Benutzer-Eingaben werden in `validators.js` validiert
|
- DB-/Log-Pfade über `DB_PATH`/`LOG_DIR` konfigurierbar
|
||||||
- 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.
|
|
||||||
|
|||||||
@@ -1,96 +1,67 @@
|
|||||||
# Umgebungsvariablen
|
# 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 |
|
Beispiel:
|
||||||
|---------|---------|-------------|
|
|
||||||
| `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
|
|
||||||
|
|
||||||
```env
|
```env
|
||||||
PORT=3001
|
PORT=3001
|
||||||
DB_PATH=/var/lib/ripster/ripster.db
|
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
|
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 | Default | Beschreibung |
|
||||||
|
|
||||||
| Variable | Standard | Beschreibung |
|
|
||||||
|---------|---------|-------------|
|
|---------|---------|-------------|
|
||||||
| `VITE_API_BASE` | `http://localhost:3001` | Backend-API-URL |
|
| `VITE_API_BASE` | `/api` | API-Basis für Fetch-Client |
|
||||||
| `VITE_WS_URL` | `ws://localhost:3001` | WebSocket-URL |
|
| `VITE_WS_URL` | automatisch aus `window.location` + `/ws` | Optional explizite WebSocket-URL |
|
||||||
| `VITE_PUBLIC_ORIGIN` | — | Öffentliche Origin-URL (für CORS) |
|
| `VITE_PUBLIC_ORIGIN` | leer | Öffentliche Vite-Origin (Remote-Dev) |
|
||||||
| `VITE_HMR_HOST` | — | Vite HMR-Host (für Remote-Entwicklung) |
|
| `VITE_ALLOWED_HOSTS` | `true` | Komma-separierte Hostliste für Vite `allowedHosts` |
|
||||||
| `VITE_HMR_PORT` | — | Vite HMR-Port |
|
| `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
|
```env
|
||||||
VITE_API_BASE=http://localhost:3001
|
# lokal (mit Vite-Proxy)
|
||||||
VITE_WS_URL=ws://localhost:3001
|
VITE_API_BASE=/api
|
||||||
```
|
```
|
||||||
|
|
||||||
### Beispiel: frontend/.env (Netzwerk-Zugriff)
|
|
||||||
|
|
||||||
```env
|
```env
|
||||||
VITE_API_BASE=http://192.168.1.100:3001
|
# remote dev
|
||||||
VITE_WS_URL=ws://192.168.1.100:3001
|
VITE_API_BASE=http://192.168.1.50:3001/api
|
||||||
VITE_PUBLIC_ORIGIN=http://192.168.1.100:5173
|
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:
|
1. Prozess-Umgebungsvariablen
|
||||||
|
2. `.env`
|
||||||
```bash
|
3. Code-Defaults
|
||||||
# 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,180 +1,165 @@
|
|||||||
# Einstellungsreferenz
|
# 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
|
## 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`
|
||||||
|--------|--------------------|
|
- `dvd`
|
||||||
| `bluray` | UDF-Dateisystem, Laufwerk-Modell enthält „Blu-ray", Disc-Label wie BDMV |
|
- `other`
|
||||||
| `dvd` | ISO9660/UDF, Laufwerk-Modell enthält „DVD", VIDEO_TS-Struktur |
|
|
||||||
| `other` | Alles andere (CD, unbekannt) |
|
|
||||||
|
|
||||||
**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
|
Wichtig:
|
||||||
2. Alternativ-Profil als Fallback (Blu-ray → DVD-Wert als Fallback und umgekehrt)
|
|
||||||
|
|
||||||
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
|
## Kategorie: Pfade
|
||||||
|
|
||||||
| Schlüssel | Typ | Pflicht | Beschreibung |
|
| Key | Typ | Default |
|
||||||
|-----------|-----|---------|-------------|
|
|-----|-----|---------|
|
||||||
| `raw_dir` | string | ✅ | Verzeichnis für rohe MKV-Dateien (Fallback wenn kein Profil-Wert) |
|
| `raw_dir` | path | `data/output/raw` |
|
||||||
| `raw_dir_bluray` | string | — | Raw-Verzeichnis für Blu-rays |
|
| `raw_dir_bluray` | path | `null` |
|
||||||
| `raw_dir_dvd` | string | — | Raw-Verzeichnis für DVDs |
|
| `raw_dir_dvd` | path | `null` |
|
||||||
| `raw_dir_other` | string | — | Raw-Verzeichnis für sonstige Medien |
|
| `raw_dir_other` | path | `null` |
|
||||||
| `raw_dir_owner` | string | — | Besitzer für Raw-Verzeichnis (`user:group`, Fallback) |
|
| `raw_dir_bluray_owner` | string | `null` |
|
||||||
| `raw_dir_bluray_owner` | string | — | Besitzer für Raw-Verzeichnis (Blu-ray) |
|
| `raw_dir_dvd_owner` | string | `null` |
|
||||||
| `raw_dir_dvd_owner` | string | — | Besitzer für Raw-Verzeichnis (DVD) |
|
| `raw_dir_other_owner` | string | `null` |
|
||||||
| `raw_dir_other_owner` | string | — | Besitzer für Raw-Verzeichnis (Sonstiges) |
|
| `movie_dir` | path | `data/output/movies` |
|
||||||
| `movie_dir` | string | ✅ | Ausgabeverzeichnis für Filme (Fallback) |
|
| `movie_dir_bluray` | path | `null` |
|
||||||
| `movie_dir_bluray` | string | — | Ausgabeverzeichnis für Blu-rays |
|
| `movie_dir_dvd` | path | `null` |
|
||||||
| `movie_dir_dvd` | string | — | Ausgabeverzeichnis für DVDs |
|
| `movie_dir_other` | path | `null` |
|
||||||
| `movie_dir_other` | string | — | Ausgabeverzeichnis für sonstige Medien |
|
| `movie_dir_bluray_owner` | string | `null` |
|
||||||
| `movie_dir_owner` | string | — | Besitzer für Ausgabeverzeichnis (Fallback) |
|
| `movie_dir_dvd_owner` | string | `null` |
|
||||||
| `movie_dir_bluray_owner` | string | — | Besitzer für Ausgabeverzeichnis (Blu-ray) |
|
| `movie_dir_other_owner` | string | `null` |
|
||||||
| `movie_dir_dvd_owner` | string | — | Besitzer für Ausgabeverzeichnis (DVD) |
|
| `log_dir` | path | `data/logs` |
|
||||||
| `movie_dir_other_owner` | string | — | Besitzer für Ausgabeverzeichnis (Sonstiges) |
|
|
||||||
| `log_dir` | string | — | Verzeichnis für Log-Dateien (Standard: `./logs`) |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Kategorie: Laufwerk
|
## Kategorie: Laufwerk
|
||||||
|
|
||||||
| Schlüssel | Typ | Standard | Beschreibung |
|
| Key | Typ | Default | Hinweis |
|
||||||
|-----------|-----|---------|-------------|
|
|-----|-----|---------|--------|
|
||||||
| `drive_mode` | select | `auto` | `auto` = automatisch erkennen, `explicit` = festes Gerät |
|
| `drive_mode` | select | `auto` | `auto` oder `explicit` |
|
||||||
| `drive_device` | string | `/dev/sr0` | Geräte-Pfad (nur bei `explicit`) |
|
| `drive_device` | path | `/dev/sr0` | bei `explicit` relevant |
|
||||||
| `disc_poll_interval_ms` | number | `4000` | Polling-Intervall in Millisekunden (1000–60000) |
|
| `makemkv_source_index` | number | `0` | MakeMKV Source-Index |
|
||||||
| `makemkv_source_index` | number | `0` | Laufwerk-Index für MakeMKV (bei mehreren Laufwerken) |
|
| `disc_poll_interval_ms` | number | `4000` | 1000..60000 |
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 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 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Kategorie: Monitoring
|
## Kategorie: Monitoring
|
||||||
|
|
||||||
| Schlüssel | Typ | Standard | Beschreibung |
|
| Key | Typ | Default |
|
||||||
|-----------|-----|---------|-------------|
|
|-----|-----|---------|
|
||||||
| `hardware_monitoring_enabled` | boolean | `false` | Hardware-Monitoring aktivieren (CPU, RAM, Temp.) |
|
| `hardware_monitoring_enabled` | boolean | `true` |
|
||||||
| `hardware_monitoring_interval_ms` | number | `5000` | Monitoring-Polling-Intervall (ms) |
|
| `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
|
### Blu-ray-spezifisch
|
||||||
sqlite3 backend/data/ripster.db \
|
|
||||||
"DELETE FROM settings_values WHERE key = 'handbrake_preset_bluray';"
|
|
||||||
```
|
|
||||||
|
|
||||||
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`
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
## Voraussetzungen
|
## Voraussetzungen
|
||||||
|
|
||||||
- Node.js >= 20.19.0
|
- 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
|
./start.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Das Skript startet automatisch:
|
Startet:
|
||||||
- **Backend** auf Port 3001 (mit Nodemon für Hot-Reload)
|
|
||||||
- **Frontend** auf Port 5173 (mit Vite HMR)
|
- Backend (`http://localhost:3001`, mit nodemon)
|
||||||
|
- Frontend (`http://localhost:5173`, mit Vite HMR)
|
||||||
|
|
||||||
|
Stoppen: `Ctrl+C`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Manuelle Entwicklungsumgebung
|
## Manuell
|
||||||
|
|
||||||
### Terminal 1 – Backend
|
### Backend
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd backend
|
cd backend
|
||||||
@@ -31,9 +34,7 @@ npm install
|
|||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Backend läuft auf `http://localhost:3001` mit **Nodemon** – Neustart bei Dateiänderungen.
|
### Frontend
|
||||||
|
|
||||||
### Terminal 2 – Frontend
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd frontend
|
cd frontend
|
||||||
@@ -41,97 +42,44 @@ npm install
|
|||||||
npm run dev
|
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:
|
Beispiel `frontend/.env.local`:
|
||||||
|
|
||||||
```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:
|
|
||||||
|
|
||||||
```env
|
```env
|
||||||
# frontend/.env.local
|
VITE_API_BASE=http://192.168.1.100:3001/api
|
||||||
VITE_API_BASE=http://192.168.1.100:3001
|
VITE_WS_URL=ws://192.168.1.100:3001/ws
|
||||||
VITE_WS_URL=ws://192.168.1.100:3001
|
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_HOST=192.168.1.100
|
||||||
VITE_HMR_PORT=5173
|
VITE_HMR_CLIENT_PORT=5173
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Log-Level für Entwicklung
|
## Nützliche Kommandos
|
||||||
|
|
||||||
```env
|
|
||||||
# backend/.env
|
|
||||||
LOG_LEVEL=debug
|
|
||||||
```
|
|
||||||
|
|
||||||
Im Debug-Modus werden alle Ausgaben der externen Tools (MakeMKV, HandBrake) vollständig geloggt.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Stoppen
|
|
||||||
|
|
||||||
```bash
|
```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"
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -4,42 +4,44 @@
|
|||||||
|
|
||||||
## Empfohlene Architektur
|
## Empfohlene Architektur
|
||||||
|
|
||||||
|
```text
|
||||||
|
Client
|
||||||
|
-> nginx (Reverse Proxy + statisches Frontend)
|
||||||
|
-> Backend API/WebSocket (Node.js, Port 3001)
|
||||||
```
|
```
|
||||||
Internet / Heimnetz
|
|
||||||
↓
|
Wichtig: Das Backend serviert im aktuellen Stand keine `frontend/dist`-Dateien automatisch.
|
||||||
nginx (Reverse Proxy)
|
|
||||||
↓
|
|
||||||
┌────┴────┐
|
|
||||||
│ │
|
|
||||||
Backend Frontend
|
|
||||||
:3001 (statische Dateien)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## systemd-Service
|
## 1) Frontend builden
|
||||||
|
|
||||||
Für ein dauerhaftes Betreiben als systemd-Service:
|
|
||||||
|
|
||||||
```bash
|
```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
|
```ini
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Ripster - Disc Ripping Service
|
Description=Ripster Backend
|
||||||
After=network.target
|
After=network.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
User=michael
|
User=ripster
|
||||||
WorkingDirectory=/home/michael/ripster
|
WorkingDirectory=/opt/ripster/backend
|
||||||
ExecStart=/bin/bash /home/michael/ripster/start.sh
|
ExecStart=/usr/bin/env node src/index.js
|
||||||
ExecStop=/bin/bash /home/michael/ripster/kill.sh
|
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=10s
|
RestartSec=5
|
||||||
|
|
||||||
# Umgebungsvariablen
|
|
||||||
Environment=NODE_ENV=production
|
Environment=NODE_ENV=production
|
||||||
Environment=PORT=3001
|
Environment=PORT=3001
|
||||||
Environment=LOG_LEVEL=info
|
Environment=LOG_LEVEL=info
|
||||||
@@ -48,61 +50,40 @@ Environment=LOG_LEVEL=info
|
|||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Aktivieren:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Service aktivieren und starten
|
|
||||||
sudo systemctl daemon-reload
|
sudo systemctl daemon-reload
|
||||||
sudo systemctl enable ripster
|
sudo systemctl enable --now ripster-backend
|
||||||
sudo systemctl start ripster
|
sudo systemctl status ripster-backend
|
||||||
|
|
||||||
# Status prüfen
|
|
||||||
sudo systemctl status ripster
|
|
||||||
|
|
||||||
# Logs anzeigen
|
|
||||||
journalctl -u ripster -f
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Frontend-Build
|
## 3) nginx konfigurieren
|
||||||
|
|
||||||
Für Produktion das Frontend bauen:
|
Beispiel `/etc/nginx/sites-available/ripster`:
|
||||||
|
|
||||||
```bash
|
|
||||||
cd frontend
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
Die statischen Dateien landen in `frontend/dist/`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## nginx-Konfiguration
|
|
||||||
|
|
||||||
```nginx
|
```nginx
|
||||||
# /etc/nginx/sites-available/ripster
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name ripster.local;
|
server_name ripster.local;
|
||||||
|
|
||||||
# Statisches Frontend
|
root /opt/ripster/frontend/dist;
|
||||||
root /home/michael/ripster/frontend/dist;
|
|
||||||
index index.html;
|
index index.html;
|
||||||
|
|
||||||
# SPA Fallback (React Router)
|
|
||||||
location / {
|
location / {
|
||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri/ /index.html;
|
||||||
}
|
}
|
||||||
|
|
||||||
# API-Proxy zum Backend
|
|
||||||
location /api/ {
|
location /api/ {
|
||||||
proxy_pass http://localhost:3001;
|
proxy_pass http://127.0.0.1:3001;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
}
|
}
|
||||||
|
|
||||||
# WebSocket-Proxy
|
|
||||||
location /ws {
|
location /ws {
|
||||||
proxy_pass http://localhost:3001;
|
proxy_pass http://127.0.0.1:3001;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
@@ -111,83 +92,27 @@ server {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Aktivieren:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo ln -s /etc/nginx/sites-available/ripster /etc/nginx/sites-enabled/
|
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
|
## Datenbank-Backup
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Datenbank sichern
|
sqlite3 /opt/ripster/backend/data/ripster.db \
|
||||||
cp backend/data/ripster.db backend/data/ripster.db.backup.$(date +%Y%m%d)
|
".backup '/var/backups/ripster-$(date +%Y%m%d).db'"
|
||||||
|
|
||||||
# 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
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Sicherheitshinweise
|
## Sicherheit
|
||||||
|
|
||||||
!!! warning "Heimnetz-Einsatz"
|
- Ripster hat keine eingebaute Authentifizierung.
|
||||||
Ripster ist für den Einsatz im **lokalen Heimnetz** konzipiert und enthält **keine Authentifizierung**. Stelle sicher, dass der Dienst nicht öffentlich erreichbar ist.
|
- Für externen Zugriff mindestens Basic Auth + TLS + Netzwerksegmentierung/VPN einsetzen.
|
||||||
|
- Secrets nicht ins Repo committen (`.env`, Settings-Felder).
|
||||||
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
|
|
||||||
|
|||||||
@@ -1,118 +1,99 @@
|
|||||||
# Konfiguration
|
# 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:
|
### 1) Pfade
|
||||||
|
|
||||||
### Pfade
|
|
||||||
|
|
||||||
| Einstellung | Beschreibung | Beispiel |
|
| Einstellung | Beschreibung | Beispiel |
|
||||||
|------------|-------------|---------|
|
|------------|-------------|---------|
|
||||||
| `raw_dir` | Verzeichnis für rohe MKV-Dateien | `/mnt/nas/raw` |
|
| `raw_dir` | Basisverzeichnis für RAW-Rips | `/mnt/ripster/raw` |
|
||||||
| `movie_dir` | Ausgabeverzeichnis für kodierte Filme | `/mnt/nas/movies` |
|
| `movie_dir` | Basisverzeichnis für finale Encodes | `/mnt/ripster/movies` |
|
||||||
| `log_dir` | Verzeichnis für Log-Dateien | `/var/log/ripster` |
|
| `log_dir` | Verzeichnis für Prozess-/Backend-Logs | `/mnt/ripster/logs` |
|
||||||
|
|
||||||
!!! warning "Berechtigungen"
|
Optional profilspezifisch:
|
||||||
Der Ripster-Prozess benötigt **Schreibrechte** auf alle konfigurierten Verzeichnisse.
|
|
||||||
|
|
||||||
```bash
|
- `raw_dir_bluray`, `raw_dir_dvd`, `raw_dir_other`
|
||||||
# Verzeichnisse erstellen und Berechtigungen setzen
|
- `movie_dir_bluray`, `movie_dir_dvd`, `movie_dir_other`
|
||||||
sudo mkdir -p /mnt/nas/{raw,movies}
|
|
||||||
sudo chown $USER:$USER /mnt/nas/{raw,movies}
|
|
||||||
```
|
|
||||||
|
|
||||||
### OMDb API
|
### 2) Tools
|
||||||
|
|
||||||
|
| Einstellung | Standard |
|
||||||
|
|------------|---------|
|
||||||
|
| `makemkv_command` | `makemkvcon` |
|
||||||
|
| `handbrake_command` | `HandBrakeCLI` |
|
||||||
|
| `mediainfo_command` | `mediainfo` |
|
||||||
|
|
||||||
|
### 3) OMDb
|
||||||
|
|
||||||
| Einstellung | Beschreibung |
|
| Einstellung | Beschreibung |
|
||||||
|------------|-------------|
|
|------------|-------------|
|
||||||
| `omdb_api_key` | API-Key von omdbapi.com |
|
| `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 |
|
Ripster arbeitet profilspezifisch, typischerweise über:
|
||||||
|------------|---------|-------------|
|
|
||||||
| `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 |
|
|
||||||
|
|
||||||
!!! tip "Absolute Pfade"
|
- Blu-ray: `handbrake_preset_bluray`, `handbrake_extra_args_bluray`, `output_extension_bluray`, `filename_template_bluray`
|
||||||
Falls die Tools nicht im `PATH` sind, verwende absolute Pfade:
|
- DVD: `handbrake_preset_dvd`, `handbrake_extra_args_dvd`, `output_extension_dvd`, `filename_template_dvd`
|
||||||
```
|
|
||||||
/usr/local/bin/HandBrakeCLI
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
### Template-Platzhalter
|
||||||
|
|
||||||
## Encoding-Konfiguration
|
Verfügbar in `filename_template_*` und `output_folder_template_*`:
|
||||||
|
|
||||||
| Einstellung | Standard | Beschreibung |
|
- `${title}`
|
||||||
|------------|---------|-------------|
|
- `${year}`
|
||||||
| `handbrake_preset` | `H.265 MKV 1080p30` | HandBrake-Preset-Name |
|
- `${imdbId}`
|
||||||
| `handbrake_extra_args` | _(leer)_ | Zusätzliche HandBrake-Argumente |
|
|
||||||
| `output_extension` | `mkv` | Dateiendung der Ausgabedatei |
|
|
||||||
| `filename_template` | `{title} ({year})` | Template für Dateinamen |
|
|
||||||
|
|
||||||
### Dateiname-Template
|
Beispiel:
|
||||||
|
|
||||||
Das Template unterstützt folgende Platzhalter:
|
```text
|
||||||
|
${title} (${year})
|
||||||
| Platzhalter | Beschreibung | Beispiel |
|
-> Inception (2010).mkv
|
||||||
|------------|-------------|---------|
|
|
||||||
| `{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
|
## MakeMKV-spezifisch
|
||||||
|
|
||||||
| Einstellung | Standard | Beschreibung |
|
| Einstellung | Standard | Hinweis |
|
||||||
|------------|---------|-------------|
|
|------------|---------|--------|
|
||||||
| `drive_mode` | `auto` | `auto` (automatisch erkennen) oder `explicit` (festes Gerät) |
|
| `makemkv_min_length_minutes` | `60` | Kandidaten-Filter |
|
||||||
| `drive_device` | `/dev/sr0` | Geräte-Pfad (nur bei `explicit`) |
|
| `makemkv_rip_mode_bluray` | `backup` | `mkv` oder `backup` |
|
||||||
| `disc_poll_interval_ms` | `4000` | Polling-Intervall in Millisekunden |
|
| `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 |
|
| Einstellung | Standard |
|
||||||
|------------|---------|-------------|
|
|------------|---------|
|
||||||
| `makemkv_min_length_minutes` | `15` | Mindestlänge für Titel in Minuten |
|
| `hardware_monitoring_enabled` | `true` |
|
||||||
| `makemkv_backup_mode` | `false` | Backup-Modus statt MKV-Modus |
|
| `hardware_monitoring_interval_ms` | `5000` |
|
||||||
|
| `pipeline_max_parallel_jobs` | `1` |
|
||||||
!!! info "Backup-Modus"
|
|
||||||
Im Backup-Modus erstellt MakeMKV eine vollständige Kopie der Disc (inkl. Menüs). Der Standardmodus erstellt direkt MKV-Dateien.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Benachrichtigungen (PushOver)
|
## PushOver (optional)
|
||||||
|
|
||||||
| Einstellung | Beschreibung |
|
Basis:
|
||||||
|------------|-------------|
|
|
||||||
| `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.
|
- `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:
|
- [Einstellungsreferenz](../configuration/settings-reference.md)
|
||||||
|
- [Umgebungsvariablen](../configuration/environment.md)
|
||||||
[:octicons-arrow-right-24: Einstellungsreferenz](../configuration/settings-reference.md)
|
|
||||||
|
|||||||
@@ -11,50 +11,46 @@ cd ripster
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Automatischer Start
|
## Dev-Start (empfohlen)
|
||||||
|
|
||||||
Ripster enthält ein `start.sh`-Skript, das alle Abhängigkeiten installiert und Backend + Frontend gleichzeitig startet:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./start.sh
|
./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)
|
1. prüft Node-Version (`>= 20.19.0`)
|
||||||
2. **Abhängigkeiten installieren** – `npm install` für Root, Backend und Frontend
|
2. installiert Dependencies (Root/Backend/Frontend)
|
||||||
3. **Dienste starten** – Backend und Frontend werden parallel gestartet
|
3. startet Backend + Frontend parallel
|
||||||
|
|
||||||
!!! success "Erfolgreich gestartet"
|
Danach:
|
||||||
- Backend läuft auf `http://localhost:3001`
|
|
||||||
- Frontend läuft auf `http://localhost:5173`
|
- Backend: `http://localhost:3001`
|
||||||
|
- Frontend: `http://localhost:5173`
|
||||||
|
|
||||||
|
Stoppen: mit `Ctrl+C` im laufenden Terminal.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Manuelle Installation
|
## Manuell starten
|
||||||
|
|
||||||
Falls du mehr Kontrolle benötigst:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Root-Abhängigkeiten
|
|
||||||
npm install
|
npm install
|
||||||
|
npm --prefix backend install
|
||||||
|
npm --prefix frontend install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
# Backend-Abhängigkeiten
|
Oder getrennt:
|
||||||
cd backend && npm install && cd ..
|
|
||||||
|
|
||||||
# Frontend-Abhängigkeiten
|
```bash
|
||||||
cd frontend && npm install && cd ..
|
npm run dev:backend
|
||||||
|
npm run dev:frontend
|
||||||
# Backend starten (Terminal 1)
|
|
||||||
cd backend && npm run dev
|
|
||||||
|
|
||||||
# Frontend starten (Terminal 2)
|
|
||||||
cd frontend && npm run dev
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Umgebungsvariablen konfigurieren
|
## Optional: .env-Dateien anlegen
|
||||||
|
|
||||||
### Backend
|
### Backend
|
||||||
|
|
||||||
@@ -62,13 +58,13 @@ cd frontend && npm run dev
|
|||||||
cp backend/.env.example backend/.env
|
cp backend/.env.example backend/.env
|
||||||
```
|
```
|
||||||
|
|
||||||
Bearbeite `backend/.env`:
|
Beispiel:
|
||||||
|
|
||||||
```env
|
```env
|
||||||
PORT=3001
|
PORT=3001
|
||||||
DB_PATH=./data/ripster.db
|
DB_PATH=./data/ripster.db
|
||||||
CORS_ORIGIN=http://localhost:5173
|
|
||||||
LOG_DIR=./logs
|
LOG_DIR=./logs
|
||||||
|
CORS_ORIGIN=http://localhost:5173
|
||||||
LOG_LEVEL=info
|
LOG_LEVEL=info
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -78,63 +74,30 @@ LOG_LEVEL=info
|
|||||||
cp frontend/.env.example frontend/.env
|
cp frontend/.env.example frontend/.env
|
||||||
```
|
```
|
||||||
|
|
||||||
Bearbeite `frontend/.env`:
|
Beispiel:
|
||||||
|
|
||||||
```env
|
```env
|
||||||
VITE_API_BASE=http://localhost:3001
|
VITE_API_BASE=/api
|
||||||
VITE_WS_URL=ws://localhost:3001
|
# optional:
|
||||||
```
|
# VITE_WS_URL=ws://localhost:3001/ws
|
||||||
|
|
||||||
!!! 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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Stoppen
|
## Datenbank
|
||||||
|
|
||||||
```bash
|
SQLite wird automatisch beim Backend-Start initialisiert:
|
||||||
./kill.sh
|
|
||||||
|
```text
|
||||||
|
backend/data/ripster.db
|
||||||
```
|
```
|
||||||
|
|
||||||
Das Skript beendet Backend- und Frontend-Prozesse graceful.
|
Schema-Quelle: `db/schema.sql`
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 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
|
## Nächste Schritte
|
||||||
|
|
||||||
Nach erfolgreicher Installation:
|
1. Browser öffnen: `http://localhost:5173`
|
||||||
|
2. In `Settings` Pfade/Tools/API-Keys prüfen
|
||||||
1. Öffne [http://localhost:5173](http://localhost:5173)
|
3. Erste Disc einlegen und Workflow starten
|
||||||
2. Navigiere zu **Einstellungen**
|
|
||||||
3. Konfiguriere Pfade, API-Keys und Encoding-Presets
|
|
||||||
|
|
||||||
[:octicons-arrow-right-24: Zur Konfiguration](configuration.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)
|
- App kaufen auf [pushover.net](https://pushover.net) (~5 USD einmalig)
|
||||||
- **User Key** und **API Token** notieren
|
- **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
|
## Checkliste
|
||||||
|
|||||||
@@ -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
|
## 1) Starten
|
||||||
|
|
||||||
<div class="pipeline-steps">
|
|
||||||
<div class="pipeline-step">
|
|
||||||
<div class="pipeline-step-badge step-idle">●</div>
|
|
||||||
<div class="pipeline-step-label">IDLE</div>
|
|
||||||
<div class="pipeline-step-sub">Warten</div>
|
|
||||||
</div>
|
|
||||||
<div class="pipeline-step">
|
|
||||||
<div class="pipeline-step-badge step-idle">1</div>
|
|
||||||
<div class="pipeline-step-label">DISC_DETECTED</div>
|
|
||||||
<div class="pipeline-step-sub">Disc erkannt</div>
|
|
||||||
</div>
|
|
||||||
<div class="pipeline-step">
|
|
||||||
<div class="pipeline-step-badge step-running">2</div>
|
|
||||||
<div class="pipeline-step-label">METADATA_SELECTION</div>
|
|
||||||
<div class="pipeline-step-sub">OMDb & Dialog</div>
|
|
||||||
</div>
|
|
||||||
<div class="pipeline-step">
|
|
||||||
<div class="pipeline-step-badge step-wait">⚠</div>
|
|
||||||
<div class="pipeline-step-label">WAITING_FOR_USER_DECISION</div>
|
|
||||||
<div class="pipeline-step-sub">Playlist wählen<br><em>(nur bei Obfusk.)</em></div>
|
|
||||||
</div>
|
|
||||||
<div class="pipeline-step">
|
|
||||||
<div class="pipeline-step-badge step-user">3</div>
|
|
||||||
<div class="pipeline-step-label">READY_TO_START</div>
|
|
||||||
<div class="pipeline-step-sub">Bereit</div>
|
|
||||||
</div>
|
|
||||||
<div class="pipeline-step">
|
|
||||||
<div class="pipeline-step-badge step-running">4</div>
|
|
||||||
<div class="pipeline-step-label">RIPPING</div>
|
|
||||||
<div class="pipeline-step-sub">MakeMKV</div>
|
|
||||||
</div>
|
|
||||||
<div class="pipeline-step">
|
|
||||||
<div class="pipeline-step-badge step-running">5</div>
|
|
||||||
<div class="pipeline-step-label">MEDIAINFO_CHECK</div>
|
|
||||||
<div class="pipeline-step-sub">HandBrake-Scan</div>
|
|
||||||
</div>
|
|
||||||
<div class="pipeline-step">
|
|
||||||
<div class="pipeline-step-badge step-user">6</div>
|
|
||||||
<div class="pipeline-step-label">READY_TO_ENCODE</div>
|
|
||||||
<div class="pipeline-step-sub">Track-Review</div>
|
|
||||||
</div>
|
|
||||||
<div class="pipeline-step">
|
|
||||||
<div class="pipeline-step-badge step-encode">7</div>
|
|
||||||
<div class="pipeline-step-label">ENCODING</div>
|
|
||||||
<div class="pipeline-step-sub">HandBrake</div>
|
|
||||||
</div>
|
|
||||||
<div class="pipeline-step">
|
|
||||||
<div class="pipeline-step-badge step-encode">8*</div>
|
|
||||||
<div class="pipeline-step-label">POST-ENCODE</div>
|
|
||||||
<div class="pipeline-step-sub">Skripte<br><em>(innerhalb ENCODING)</em></div>
|
|
||||||
</div>
|
|
||||||
<div class="pipeline-step">
|
|
||||||
<div class="pipeline-step-badge step-done">✓</div>
|
|
||||||
<div class="pipeline-step-label">FINISHED</div>
|
|
||||||
<div class="pipeline-step-sub">Fertig</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
**Legende:** <span style="color:#546e7a">● Warten</span> | <span style="color:#1565c0">■ Läuft automatisch</span> | <span style="color:#3949ab">■ Benutzeraktion</span> | <span style="color:#e65100">⚠ Optional</span> | <span style="color:#6a1b9a">■ Encodierung</span> | <span style="color:#2e7d32">✓ Fertig</span>
|
|
||||||
|
|
||||||
??? note "Vollständiges Zustandsdiagramm (inkl. Fehler- & Alternativpfade)"
|
|
||||||
|
|
||||||
<div class="pipeline-diagram">
|
|
||||||
|
|
||||||
```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
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Schritt 1 – Ripster starten
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd ripster
|
cd ripster
|
||||||
./start.sh
|
./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:**
|
Falls nötig manuell neu scannen:
|
||||||
|
|
||||||
- `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):**
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
makemkvcon mkv disc:0 all /mnt/raw/Inception-2010/ \
|
curl -X POST http://localhost:3001/api/pipeline/rescan-disc
|
||||||
--minlength=900 -r
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**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
|
```bash
|
||||||
makemkvcon backup disc:0 /mnt/raw/Inception-2010-backup/ \
|
# Pipeline-Snapshot
|
||||||
--decrypt -r
|
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 <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:
|
|
||||||
|
|
||||||
```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 |
|
|
||||||
|
|||||||
@@ -1,329 +1,103 @@
|
|||||||
# Encode-Planung & Track-Auswahl
|
# 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
|
||||||
|
|
||||||
```
|
```text
|
||||||
RIPPING abgeschlossen (oder Pre-Rip-Scan)
|
Quelle bestimmen (Disc/RAW)
|
||||||
↓
|
-> HandBrake-Scan (--scan --json)
|
||||||
HandBrake --scan (alle Titel & Tracks einlesen)
|
-> Plan erstellen (Titel, Audio, Untertitel)
|
||||||
↓
|
-> READY_TO_ENCODE
|
||||||
buildTrackSelectors() ← Regeln aus Einstellungen ableiten
|
-> Benutzer bestätigt Auswahl
|
||||||
↓
|
-> finaler HandBrake-Aufruf
|
||||||
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
|
## Review-Inhalt (`READY_TO_ENCODE`)
|
||||||
|
|
||||||
Ripster führt einen **HandBrake-Scan** bereits **vor dem eigentlichen Ripping** durch:
|
- auswählbarer Encode-Titel
|
||||||
|
- Audio-Track-Selektion
|
||||||
```bash
|
- Untertitel-Track-Selektion inkl. Flags
|
||||||
HandBrakeCLI --scan -i /dev/sr0 -t 0
|
- `burnIn`
|
||||||
```
|
- `forced`
|
||||||
|
- `defaultTrack`
|
||||||
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.
|
- optionale User-Presets (HandBrake-Preset + Extra-Args)
|
||||||
|
- optionale Pre-/Post-Skripte und Ketten
|
||||||
!!! info "Pre-Rip vs. Post-Rip"
|
|
||||||
Ob der Scan vor oder nach dem Ripping passiert, hängt vom konfigurierten Modus ab. Bei direktem Disc-Zugriff ist Pre-Rip möglich; nach einem MakeMKV-Backup wird die entstandene `.mkv`-Datei gescannt.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Phase 2: Track-Selektor-Regeln (`buildTrackSelectors`)
|
## Bestätigung (`confirm-encode`)
|
||||||
|
|
||||||
Die Regeln werden aus den HandBrake-Einstellungen abgeleitet. Es gibt fünf **Selektionsmodi**:
|
Typischer Payload:
|
||||||
|
|
||||||
| Modus | Beschreibung |
|
|
||||||
|------|-------------|
|
|
||||||
| `none` | Keine Tracks dieser Art übernehmen |
|
|
||||||
| `first` | Nur den ersten Track übernehmen |
|
|
||||||
| `all` | Alle Tracks übernehmen |
|
|
||||||
| `language` | Nur Tracks in bestimmten Sprachen |
|
|
||||||
| `explicit` | Bestimmte Track-IDs explizit angeben |
|
|
||||||
|
|
||||||
Der aktive Modus wird aus den `handbrake_*`-Einstellungen und `handbrake_extra_args` abgeleitet. Explizite CLI-Argumente (`--audio`, `--audio-lang-list`) überschreiben die Basis-Konfiguration.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 3: Automatische Vorauswahl (`selectTrackIds`)
|
|
||||||
|
|
||||||
### Audio-Tracks
|
|
||||||
|
|
||||||
```
|
|
||||||
Modus 'none' → Keine Audio-Tracks
|
|
||||||
Modus 'all' → Alle Tracks (oder nur erster, wenn firstOnly)
|
|
||||||
Modus 'language' → Alle Tracks in den konfigurierten Sprachen
|
|
||||||
Modus 'explicit' → Nur die angegebenen Track-IDs
|
|
||||||
Modus 'first' → Nur Track 1
|
|
||||||
```
|
|
||||||
|
|
||||||
Jeder Audio-Track erhält das Feld `selectedByRule: true/false` – dieses zeigt dem Benutzer, welche Tracks automatisch vorausgewählt wurden.
|
|
||||||
|
|
||||||
**Sprach-Normalisierung (`normalizeLanguage`):**
|
|
||||||
|
|
||||||
Alle Sprachcodes werden auf **ISO 639-2** (3-Buchstaben) normalisiert:
|
|
||||||
|
|
||||||
| Eingabe | Normalisiert |
|
|
||||||
|--------|-------------|
|
|
||||||
| `de`, `ger` | `deu` |
|
|
||||||
| `German` | `deu` |
|
|
||||||
| `en`, `eng` | `eng` |
|
|
||||||
| `English` | `eng` |
|
|
||||||
| `fr`, `fre` | `fra` |
|
|
||||||
| `ja`, `jpn` | `jpn` |
|
|
||||||
| Unbekannt | `und` |
|
|
||||||
|
|
||||||
### Untertitel-Tracks
|
|
||||||
|
|
||||||
Gleiche Modus-Logik wie Audio, aber mit **zusätzlichen Flags** pro Track:
|
|
||||||
|
|
||||||
| Flag | Bedeutung |
|
|
||||||
|------|-----------|
|
|
||||||
| `burnIn` | Untertitel in Video einbrennen (`--subtitle-burned`) |
|
|
||||||
| `forced` | Nur erzwungene Untertitel übernehmen (`--subtitle-forced`) |
|
|
||||||
| `defaultTrack` | Als Standard-Untertitelspur markieren (`--subtitle-default`) |
|
|
||||||
|
|
||||||
Diese Flags werden im Encode-Review als Checkboxen angezeigt.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 4: Encoder-Aktion bestimmen (`resolveAudioEncoderAction`)
|
|
||||||
|
|
||||||
Für jeden vorausgewählten Audio-Track bestimmt Ripster die Encoder-Aktion:
|
|
||||||
|
|
||||||
```
|
|
||||||
Encoder-Einstellung Codec-Support in Copy-Mask? Aktion
|
|
||||||
─────────────────────────────────────────────────────────────────────
|
|
||||||
Kein Encoder / 'preset-default' → preset-default HandBrake-Preset entscheidet
|
|
||||||
encoder.startsWith('copy')
|
|
||||||
UND Codec in audioCopyMask → copy Direktkopie (verlustfrei)
|
|
||||||
UND Codec NICHT in audioCopyMask→ fallback Transcode mit Fallback-Encoder
|
|
||||||
sonstiger Encoder → transcode Transcode mit explizitem Encoder
|
|
||||||
```
|
|
||||||
|
|
||||||
**Encoder-Aktionstypen:**
|
|
||||||
|
|
||||||
| Typ | Label (UI) | Qualität |
|
|
||||||
|----|-----------|---------|
|
|
||||||
| `preset-default` | `Preset-Default (HandBrake)` | HandBrake entscheidet |
|
|
||||||
| `copy` | `Copy (ac3)` | Verlustfrei |
|
|
||||||
| `fallback` | `Fallback Transcode (av_aac)` | Mit Qualitätsverlust |
|
|
||||||
| `transcode` | `Transcode (av_aac)` | Mit Qualitätsverlust |
|
|
||||||
|
|
||||||
**Copy-kompatible Codecs (Standard Copy-Mask):**
|
|
||||||
|
|
||||||
| Codec | Encoder-String |
|
|
||||||
|-------|---------------|
|
|
||||||
| AC-3 | `copy:ac3` |
|
|
||||||
| E-AC-3 | `copy:eac3` |
|
|
||||||
| AAC | `copy:aac` |
|
|
||||||
| MP3 | `copy:mp3` |
|
|
||||||
| TrueHD | `copy:truehd` |
|
|
||||||
| DTS | `copy:dts` *(nur mit spez. HandBrake-Build)* |
|
|
||||||
| DTS-HD | `copy:dtshd` *(nur mit spez. HandBrake-Build)* |
|
|
||||||
|
|
||||||
!!! warning "DTS im Standard-HandBrake"
|
|
||||||
Standard-HandBrake-Builds unterstützen kein DTS-Passthrough. DTS-Tracks werden dann automatisch auf den Fallback-Encoder umgestellt (Standard: `av_aac`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 5: Encode-Plan-Struktur
|
|
||||||
|
|
||||||
Der vollständige Plan wird im Job-Datensatz als `encode_plan_json` gespeichert:
|
|
||||||
|
|
||||||
```json
|
```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,
|
"selectedEncodeTitleId": 1,
|
||||||
"selectedTrackSelection": {
|
"selectedTrackSelection": {
|
||||||
"1": {
|
"1": {
|
||||||
"audioTrackIds": [1, 2],
|
"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
|
```bash
|
||||||
HandBrakeCLI \
|
HandBrakeCLI \
|
||||||
-i /dev/sr0 \
|
-i <input> \
|
||||||
-o "/mnt/movies/Inception (2010).mkv" \
|
-o <output> \
|
||||||
-t 1 \
|
-t <titleId> \
|
||||||
--preset "H.265 MKV 1080p30" \
|
-Z "<preset>" \
|
||||||
-a 1,2 \
|
<extra-args> \
|
||||||
-E copy:ac3,av_aac \
|
-a <audioTrackIds|none> \
|
||||||
-s 1 \
|
-s <subtitleTrackIds|none>
|
||||||
--subtitle-default 1
|
|
||||||
```
|
```
|
||||||
|
|
||||||
| Argument | Quelle |
|
Untertitel-Flags werden bei Bedarf ergänzt:
|
||||||
|---------|--------|
|
|
||||||
| `-i` | `encode_input_path` aus Job |
|
- `--subtitle-burned=<id>`
|
||||||
| `-o` | Ausgabepfad aus `filename_template` + `movie_dir` |
|
- `--subtitle-default=<id>`
|
||||||
| `-t` | Gewählter Titel-Index |
|
- `--subtitle-forced=<id>` oder `--subtitle-forced`
|
||||||
| `-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
|
## Pre-/Post-Encode-Ausführungen
|
||||||
|
|
||||||
| Platzhalter | Wert | Beispiel |
|
- Pre-Encode läuft vor HandBrake
|
||||||
|------------|------|---------|
|
- Post-Encode läuft nach HandBrake
|
||||||
| `{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.
|
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
|
Platzhalter:
|
||||||
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.
|
- `${title}`
|
||||||
|
- `${year}`
|
||||||
|
- `${imdbId}`
|
||||||
|
|
||||||
|
Ungültige Dateizeichen werden sanitisiert.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Pipeline
|
# Pipeline
|
||||||
|
|
||||||
Der Pipeline-Abschnitt beschreibt den Kern-Workflow von Ripster.
|
Der Pipeline-Bereich beschreibt den Kern-Workflow von Ripster.
|
||||||
|
|
||||||
<div class="grid cards" markdown>
|
<div class="grid cards" markdown>
|
||||||
|
|
||||||
@@ -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)
|
[: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)
|
[: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)
|
[: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)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,217 +1,65 @@
|
|||||||
# Playlist-Analyse
|
# 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:
|
Erkennen, welche Playlist wahrscheinlich der Hauptfilm ist, statt versehentlich eine Fake-/Dummy-Playlist zu verwenden.
|
||||||
|
|
||||||
- **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?
|
## 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.
|
Die Analyse basiert auf MakeMKV-Infos (u. a. Playlist-/Segment-Struktur, Laufzeiten, Titelzuordnung).
|
||||||
|
|
||||||
```
|
|
||||||
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`)
|
## Auswertung (vereinfacht)
|
||||||
|
|
||||||
### Schritt 1 – Segment-Nummern parsen
|
Für Kandidaten werden u. a. berücksichtigt:
|
||||||
|
|
||||||
```
|
- Laufzeit
|
||||||
TINFO:1,26,"00000,00001,00002,00003" → [0, 1, 2, 3] linearer Film
|
- Segment-Reihenfolge
|
||||||
TINFO:2,26,"00100,00050,00100,00051" → [100, 50, 100, 51] Fake-Playlist
|
- 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:
|
- `candidates`
|
||||||
|
- `evaluatedCandidates` (inkl. Score/Label)
|
||||||
| Metrik | Bedingung | Bedeutung |
|
- `recommendation`
|
||||||
|--------|----------|-----------|
|
- `manualDecisionRequired`
|
||||||
| `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?
|
## Wann muss der Benutzer entscheiden?
|
||||||
|
|
||||||
```
|
Wenn nach Filterung mehr als ein relevanter Kandidat übrig bleibt, setzt Ripster `manualDecisionRequired = true` und wechselt auf:
|
||||||
obfuscationDetected = duplicateDurationGroups.length > 0
|
|
||||||
manualDecisionRequired = candidates.length > 1
|
|
||||||
```
|
|
||||||
|
|
||||||
| Ergebnis | Nächster Pipeline-Zustand | Aktion |
|
- `WAITING_FOR_USER_DECISION`
|
||||||
|---------|--------------------------|--------|
|
|
||||||
| Nur ein Kandidat nach Mindestlänge | `READY_TO_START` | Automatische Übernahme möglich |
|
Dann muss eine Playlist bestätigt werden, bevor der Workflow weiterläuft.
|
||||||
| Mehrere Kandidaten nach Mindestlänge | `WAITING_FOR_USER_DECISION` | Benutzer muss Playlist auswählen |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 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 |
|
||||||
|
|
||||||
```
|
Default ist aktuell `60` Minuten.
|
||||||
┌───────────────────────────────────────────────────────────────────┐
|
|
||||||
│ 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`)
|
## UI-Verhalten
|
||||||
|
|
||||||
```json
|
Bei manueller Entscheidung zeigt das Dashboard Kandidaten inkl. Score/Bewertung und markiert eine Empfehlung.
|
||||||
{
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
Nach Bestätigung:
|
||||||
|
|
||||||
## Konfiguration
|
- mit vorhandenem RAW -> zurück zu `MEDIAINFO_CHECK`
|
||||||
|
- ohne RAW -> Startpfad über `READY_TO_START`/`RIPPING`
|
||||||
| 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.
|
|
||||||
|
|||||||
@@ -1,173 +1,70 @@
|
|||||||
# Encode-Skripte (Pre & Post)
|
# 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
|
READY_TO_ENCODE
|
||||||
↓
|
-> Pre-Encode Skripte/Ketten
|
||||||
[Pre-Encode-Ausführungen] ← Fehler? → Abbruch
|
-> HandBrake Encoding
|
||||||
Skript/Kette 1, 2, …
|
-> Post-Encode Skripte/Ketten
|
||||||
↓
|
-> FINISHED oder ERROR
|
||||||
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\"}"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 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:
|
||||||
|
|
||||||
```
|
- `selectedPreEncodeScriptIds`
|
||||||
┌──────────────────────────────────────────────────────────┐
|
- `selectedPostEncodeScriptIds`
|
||||||
│ Pre-Encode Ausführungen (optional) │
|
- `selectedPreEncodeChainIds`
|
||||||
├──────────────────────────────────────────────────────────┤
|
- `selectedPostEncodeChainIds`
|
||||||
│ ≡ 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
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Skript testen
|
## Fehlerverhalten
|
||||||
|
|
||||||
Über die Einstellungen kann jedes Skript mit einem Test-Job ausgeführt werden:
|
- 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).
|
||||||
```http
|
|
||||||
POST /api/settings/scripts/:scriptId/test
|
|
||||||
```
|
|
||||||
|
|
||||||
Der Test-Aufruf befüllt die Umgebungsvariablen mit Platzhalter-Werten.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 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
|
- `RIPSTER_SCRIPT_RUN_AT`
|
||||||
{
|
- `RIPSTER_JOB_ID`
|
||||||
"postEncodeScripts": {
|
- `RIPSTER_JOB_TITLE`
|
||||||
"configured": 2,
|
- `RIPSTER_MODE`
|
||||||
"attempted": 2,
|
- `RIPSTER_INPUT_PATH`
|
||||||
"succeeded": 2,
|
- `RIPSTER_OUTPUT_PATH`
|
||||||
"failed": 0,
|
- `RIPSTER_RAW_PATH`
|
||||||
"skipped": 0,
|
- `RIPSTER_SCRIPT_ID`
|
||||||
"aborted": false,
|
- `RIPSTER_SCRIPT_NAME`
|
||||||
"results": [
|
- `RIPSTER_SCRIPT_SOURCE`
|
||||||
{
|
|
||||||
"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
|
## 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.
|
||||||
|
|||||||
@@ -1,329 +1,87 @@
|
|||||||
# Workflow & Zustände
|
# 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)
|
||||||
|
|
||||||
<div class="pipeline-diagram">
|
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart LR
|
flowchart LR
|
||||||
START(( )) --> IDLE
|
IDLE --> DISC_DETECTED
|
||||||
|
DISC_DETECTED --> ANALYZING
|
||||||
IDLE -->|Disc erkannt| DD[DISC_DETECTED]
|
ANALYZING --> METADATA_SELECTION
|
||||||
DD -->|Analyse starten| META[METADATA\nSELECTION]
|
METADATA_SELECTION --> READY_TO_START
|
||||||
|
READY_TO_START --> RIPPING
|
||||||
META -->|Metadaten übernommen| RTS[READY_TO\nSTART]
|
READY_TO_START --> MEDIAINFO_CHECK
|
||||||
META -->|vorhandenes RAW +\nPlaylist offen| WUD[WAITING_FOR\nUSER_DECISION]
|
MEDIAINFO_CHECK --> WAITING_FOR_USER_DECISION
|
||||||
|
WAITING_FOR_USER_DECISION --> MEDIAINFO_CHECK
|
||||||
RTS -->|Auto-Start| RIP[RIPPING]
|
MEDIAINFO_CHECK --> READY_TO_ENCODE
|
||||||
RTS -->|Auto-Start mit RAW| MIC[MEDIAINFO\nCHECK]
|
READY_TO_ENCODE --> ENCODING
|
||||||
RIP -->|MKV fertig| MIC
|
ENCODING --> FINISHED
|
||||||
RIP -->|Fehler| ERR
|
ENCODING --> ERROR
|
||||||
RIP -->|Abbruch| CAN([CANCELLED])
|
RIPPING --> ERROR
|
||||||
|
RIPPING --> 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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
</div>
|
---
|
||||||
|
|
||||||
|
## 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 |
|
1. Disc erkannt
|
||||||
|------|-------------|
|
2. Analyse + Metadaten
|
||||||
| `IDLE` | `Bereit` |
|
3. `RIPPING`
|
||||||
| `DISC_DETECTED` | `Medium erkannt` |
|
4. `MEDIAINFO_CHECK`
|
||||||
| `METADATA_SELECTION` | `Metadatenauswahl` |
|
5. `READY_TO_ENCODE`
|
||||||
| `WAITING_FOR_USER_DECISION` | `Warte auf Auswahl` |
|
6. `ENCODING`
|
||||||
| `READY_TO_START` | `Startbereit` |
|
7. `FINISHED`
|
||||||
| `RIPPING` | `Rippen` |
|
|
||||||
| `MEDIAINFO_CHECK` | `Mediainfo-Pruefung` |
|
### Vorhandenes RAW
|
||||||
| `READY_TO_ENCODE` | `Bereit zum Encodieren` |
|
|
||||||
| `ENCODING` | `Encodieren` |
|
`READY_TO_START` springt direkt zu `MEDIAINFO_CHECK` (kein neuer Rip).
|
||||||
| `FINISHED` | `Fertig` |
|
|
||||||
| `CANCELLED` | `Abgebrochen` |
|
### Mehrdeutige Blu-ray-Playlist
|
||||||
| `ERROR` | `Fehler` |
|
|
||||||
| Queue (kein eigener State) | `In der Queue` |
|
`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.
|
- Job-Aktionen werden als Queue-Einträge abgelegt
|
||||||
|
- Queue kann zusätzlich Nicht-Job-Einträge enthalten (`script`, `chain`, `wait`)
|
||||||
- `diskDetectionService` pollt das Laufwerk im konfigurierten Intervall
|
- Reihenfolge ist per API/UI änderbar
|
||||||
- Bei Disc-Erkennung: automatischer Übergang zu `DISC_DETECTED`
|
|
||||||
- WebSocket-Event: `DISC_DETECTED`
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### DISC_DETECTED
|
## Abbruch, Retry, Restart
|
||||||
|
|
||||||
**Disc erkannt, wartet auf Benutzeraktion.**
|
- `cancel`: laufenden Job abbrechen oder Queue-Eintrag entfernen
|
||||||
|
- `retry`: Fehler-/Abbruch-Job neu starten
|
||||||
- Dashboard-Badge: **"Medium erkannt"**
|
- `reencode`: aus vorhandenem RAW neu encodieren
|
||||||
- Status-Text: **"Neue Disk erkannt"**
|
- `restart-review`: Review aus RAW neu aufbauen
|
||||||
- **"Analyse starten"**-Button wird aktiv
|
- `restart-encode`: Encoding mit letzter bestätigter Auswahl neu starten
|
||||||
- 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 <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).
|
|
||||||
|
|
||||||
!!! 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
|
|
||||||
|
|||||||
@@ -1,137 +1,69 @@
|
|||||||
# HandBrake
|
# 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 <input> -t 0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Encode (vereinfacht)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
HandBrakeCLI \
|
HandBrakeCLI \
|
||||||
--input "/mnt/raw/Film_t00.mkv" \
|
-i <input> \
|
||||||
--output "/mnt/movies/Film (2010).mkv" \
|
-o <output> \
|
||||||
--preset "H.265 MKV 1080p30" \
|
-t <titleId> \
|
||||||
--audio 1,2 \
|
-Z "<preset>" \
|
||||||
--aencoder copy:ac3,ffaac \
|
<extra-args> \
|
||||||
--subtitle 1 \
|
-a <audioTrackIds|none> \
|
||||||
--subtitle-default 1
|
-s <subtitleTrackIds|none>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Optional ergänzt Ripster:
|
||||||
|
|
||||||
|
- `--subtitle-burned=<id>`
|
||||||
|
- `--subtitle-default=<id>`
|
||||||
|
- `--subtitle-forced=<id>` oder `--subtitle-forced`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Presets
|
## Presets auslesen
|
||||||
|
|
||||||
HandBrake verwendet **Presets** für vorkonfigurierte Encoding-Einstellungen.
|
Ripster liest Presets mit:
|
||||||
|
|
||||||
### 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
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
HandBrakeCLI --preset-list
|
HandBrakeCLI -z
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Audio-Encoding
|
## Relevante Settings
|
||||||
|
|
||||||
### Copy-kompatible Codecs
|
| Key | Bedeutung |
|
||||||
|
|-----|-----------|
|
||||||
HandBrake kann folgende Codecs direkt kopieren (kein Qualitätsverlust):
|
| `handbrake_command` | CLI-Binary |
|
||||||
|
| `handbrake_preset_bluray` / `handbrake_preset_dvd` | profilspezifisches Preset |
|
||||||
| Codec | `--aencoder` Wert |
|
| `handbrake_extra_args_bluray` / `handbrake_extra_args_dvd` | profilspezifische Zusatzargumente |
|
||||||
|-------|-----------------|
|
| `output_extension_bluray` / `output_extension_dvd` | Ausgabeformat |
|
||||||
| AC-3 | `copy:ac3` |
|
| `handbrake_restart_delete_incomplete_output` | unvollständige Ausgabe bei Neustart löschen |
|
||||||
| 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
|
## Fortschritts-Parsing
|
||||||
|
|
||||||
Ripster parst die HandBrake-Ausgabe auf stderr für die Fortschrittsanzeige:
|
Ripster parst HandBrake-Stderr (Prozent/ETA/Detail) und sendet WebSocket-Progress (`PIPELINE_PROGRESS`).
|
||||||
|
|
||||||
```
|
|
||||||
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
|
## 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
|
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.
|
||||||
# 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.
|
|
||||||
|
|||||||
@@ -1,160 +1,61 @@
|
|||||||
# MakeMKV
|
# 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
|
```bash
|
||||||
makemkvcon -r --cache=1 info disc:0
|
makemkvcon -r info <source>
|
||||||
```
|
```
|
||||||
|
|
||||||
Gibt alle Titel und Playlists der eingelegten Disc aus. Ripster parst diese Ausgabe um die verfügbaren Tracks und Playlists zu bestimmen.
|
`<source>` ist typischerweise:
|
||||||
|
|
||||||
**Parameter:**
|
- `disc:<index>` (Auto-Modus)
|
||||||
- `-r` – Maschinen-lesbares Ausgabeformat
|
- `dev:/dev/sr0` (explicit)
|
||||||
- `--cache=1` – Minimaler Disc-Cache
|
- `file:<path>` (Datei/Ordner-Analyse)
|
||||||
- `info disc:0` – Informationsabfrage für erstes Laufwerk
|
|
||||||
|
|
||||||
### MKV-Modus (Standard)
|
### Rip (MKV-Modus)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
makemkvcon mkv disc:0 all /path/to/raw/ \
|
makemkvcon mkv <source> <title-or-all> <rawDir> [--minlength=...] [...extraArgs]
|
||||||
--minlength=900 \
|
|
||||||
-r
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Erstellt MKV-Dateien aus allen Titeln, die länger als 15 Minuten sind.
|
### Rip (Backup-Modus)
|
||||||
|
|
||||||
**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
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
makemkvcon backup disc:0 /path/to/raw/backup/ \
|
makemkvcon backup <source> <rawDir> --decrypt
|
||||||
--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"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Konfiguration in Ripster
|
## Registrierungsschlüssel (optional)
|
||||||
|
|
||||||
| Einstellung | Beschreibung |
|
Wenn `makemkv_registration_key` gesetzt ist, führt Ripster vor Analyse/Rip aus:
|
||||||
|------------|-------------|
|
|
||||||
| `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
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Laufwerk-Berechtigungen prüfen
|
makemkvcon reg <key>
|
||||||
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.
|
## 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.
|
||||||
|
|||||||
@@ -1,108 +1,37 @@
|
|||||||
# MediaInfo
|
# 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
|
```bash
|
||||||
mediainfo --Output=JSON /path/to/raw/film.mkv
|
mediainfo --Output=JSON <input>
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
- Track-/Codec-Metadaten für Review-Plan
|
||||||
{
|
- Fallback-Informationen in bestimmten Analysepfaden
|
||||||
"media": {
|
- Persistenz als `mediainfoInfo` im Job
|
||||||
"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
|
## Relevante Settings
|
||||||
|
|
||||||
`encodePlan.js` verarbeitet die MediaInfo-Ausgabe:
|
| Key | Bedeutung |
|
||||||
|
|-----|-----------|
|
||||||
1. **Track-Extraktion**: Alle Audio- und Untertitel-Tracks werden extrahiert
|
| `mediainfo_command` | CLI-Binary |
|
||||||
2. **Sprach-Normalisierung**: Sprachcodes werden auf ISO 639-3 normalisiert
|
| `mediainfo_extra_args_bluray` / `_dvd` | profilspezifische Zusatzargumente |
|
||||||
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
|
## Troubleshooting
|
||||||
|
|
||||||
### MediaInfo gibt kein JSON aus
|
- JSON-Test: `mediainfo --Output=JSON <datei>`
|
||||||
|
- unbekannte Sprache erscheint oft als `und` (undetermined)
|
||||||
```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.
|
|
||||||
|
|||||||
@@ -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) {
|
function splitArgs(input) {
|
||||||
if (!input || typeof input !== 'string') {
|
if (!input || typeof input !== 'string') {
|
||||||
return [];
|
return [];
|
||||||
@@ -601,6 +618,8 @@ function TrackList({
|
|||||||
const displayAudioTitle = audioChannelLabel(track.channels);
|
const displayAudioTitle = audioChannelLabel(track.channels);
|
||||||
const audioVariant = type === 'audio' ? extractAudioVariant(displayHint) : '';
|
const audioVariant = type === 'audio' ? extractAudioVariant(displayHint) : '';
|
||||||
const disabled = !allowSelection || (type === 'subtitle' && burned);
|
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}`;
|
let displayText = `#${track.id} | ${displayLanguage} | ${displayCodec}`;
|
||||||
if (type === 'audio') {
|
if (type === 'audio') {
|
||||||
@@ -616,6 +635,10 @@ function TrackList({
|
|||||||
}
|
}
|
||||||
if (type === 'subtitle' && burned) {
|
if (type === 'subtitle' && burned) {
|
||||||
displayText += ' | burned';
|
displayText += ' | burned';
|
||||||
|
} else if (type === 'subtitle' && forcedOnlyTrack) {
|
||||||
|
displayText += ' | forced-only';
|
||||||
|
} else if (type === 'subtitle' && forcedAvailable) {
|
||||||
|
displayText += ' | forced verfügbar';
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -1074,7 +1097,6 @@ export default function MediaInfoReviewPanel({
|
|||||||
allowTrackSelection
|
allowTrackSelection
|
||||||
&& allowTitleSelection
|
&& allowTitleSelection
|
||||||
&& titleChecked
|
&& titleChecked
|
||||||
&& titleEligible
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -1090,7 +1112,7 @@ export default function MediaInfoReviewPanel({
|
|||||||
onSelectEncodeTitle(normalizeTitleId(title.id));
|
onSelectEncodeTitle(normalizeTitleId(title.id));
|
||||||
}}
|
}}
|
||||||
readOnly={!allowTitleSelection}
|
readOnly={!allowTitleSelection}
|
||||||
disabled={!allowTitleSelection || !titleEligible}
|
disabled={!allowTitleSelection}
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
#{title.id} | {title.fileName} | {formatDuration(title.durationMinutes)} | {formatBytes(title.sizeBytes)}
|
#{title.id} | {title.fileName} | {formatDuration(title.durationMinutes)} | {formatBytes(title.sizeBytes)}
|
||||||
|
|||||||
@@ -24,6 +24,18 @@ function normalizePlaylistId(value) {
|
|||||||
return match ? String(match[1]).padStart(5, '0') : null;
|
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) {
|
function normalizeTrackId(value) {
|
||||||
const parsed = Number(value);
|
const parsed = Number(value);
|
||||||
if (!Number.isFinite(parsed) || parsed <= 0) {
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
||||||
@@ -376,10 +388,18 @@ export default function PipelineStatusCard({
|
|||||||
item?.structuralMetrics?.sequenceCoherence ?? item?.sequenceCoherence
|
item?.structuralMetrics?.sequenceCoherence ?? item?.sequenceCoherence
|
||||||
);
|
);
|
||||||
const handBrakeTitleId = Number(item?.handBrakeTitleId);
|
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 {
|
return {
|
||||||
playlistId,
|
playlistId,
|
||||||
playlistFile,
|
playlistFile,
|
||||||
titleId: Number.isFinite(Number(item?.titleId)) ? Number(item.titleId) : null,
|
titleId: Number.isFinite(Number(item?.titleId)) ? Number(item.titleId) : null,
|
||||||
|
durationSeconds,
|
||||||
|
durationLabel: durationLabel || null,
|
||||||
score: Number.isFinite(score) ? score : null,
|
score: Number.isFinite(score) ? score : null,
|
||||||
evaluationLabel: item?.evaluationLabel || null,
|
evaluationLabel: item?.evaluationLabel || null,
|
||||||
segmentCommand: item?.segmentCommand
|
segmentCommand: item?.segmentCommand
|
||||||
@@ -688,6 +708,7 @@ export default function PipelineStatusCard({
|
|||||||
<span>
|
<span>
|
||||||
{row.playlistFile}
|
{row.playlistFile}
|
||||||
{row.titleId !== null ? ` | Titel #${row.titleId}` : ''}
|
{row.titleId !== null ? ` | Titel #${row.titleId}` : ''}
|
||||||
|
{row.durationLabel ? ` | Dauer ${row.durationLabel}` : ''}
|
||||||
{row.score !== null ? ` | Score ${row.score}` : ''}
|
{row.score !== null ? ` | Score ${row.score}` : ''}
|
||||||
{row.recommended ? ' | empfohlen' : ''}
|
{row.recommended ? ' | empfohlen' : ''}
|
||||||
</span>
|
</span>
|
||||||
@@ -757,7 +778,7 @@ export default function PipelineStatusCard({
|
|||||||
{(state === 'READY_TO_ENCODE' || state === 'MEDIAINFO_CHECK' || mediaInfoReview) ? (
|
{(state === 'READY_TO_ENCODE' || state === 'MEDIAINFO_CHECK' || mediaInfoReview) ? (
|
||||||
<div className="mediainfo-review-block">
|
<div className="mediainfo-review-block">
|
||||||
<h3>Titel-/Spurprüfung</h3>
|
<h3>Titel-/Spurprüfung</h3>
|
||||||
{state === 'READY_TO_ENCODE' && !reviewConfirmed && !queueLocked ? (
|
{state === 'READY_TO_ENCODE' && !queueLocked ? (
|
||||||
<small>
|
<small>
|
||||||
{isPreRipReview
|
{isPreRipReview
|
||||||
? 'Spurauswahl kann direkt übernommen werden. Beim Klick auf "Backup + Encoding starten" wird automatisch bestätigt und gestartet.'
|
? '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}
|
presetDisplayValue={presetDisplayValue}
|
||||||
commandOutputPath={commandOutputPath}
|
commandOutputPath={commandOutputPath}
|
||||||
selectedEncodeTitleId={normalizeTitleId(selectedEncodeTitleId)}
|
selectedEncodeTitleId={normalizeTitleId(selectedEncodeTitleId)}
|
||||||
allowTitleSelection={state === 'READY_TO_ENCODE' && !reviewConfirmed && !queueLocked}
|
allowTitleSelection={state === 'READY_TO_ENCODE' && !queueLocked}
|
||||||
onSelectEncodeTitle={(titleId) => setSelectedEncodeTitleId(normalizeTitleId(titleId))}
|
onSelectEncodeTitle={(titleId) => setSelectedEncodeTitleId(normalizeTitleId(titleId))}
|
||||||
allowTrackSelection={state === 'READY_TO_ENCODE' && !reviewConfirmed && !queueLocked}
|
allowTrackSelection={state === 'READY_TO_ENCODE' && !queueLocked}
|
||||||
trackSelectionByTitle={trackSelectionByTitle}
|
trackSelectionByTitle={trackSelectionByTitle}
|
||||||
onTrackSelectionChange={(titleId, trackType, trackId, checked) => {
|
onTrackSelectionChange={(titleId, trackType, trackId, checked) => {
|
||||||
const normalizedTitleId = normalizeTitleId(titleId);
|
const normalizedTitleId = normalizeTitleId(titleId);
|
||||||
@@ -808,7 +829,7 @@ export default function PipelineStatusCard({
|
|||||||
userPresets={filteredUserPresets}
|
userPresets={filteredUserPresets}
|
||||||
selectedUserPresetId={selectedUserPresetId}
|
selectedUserPresetId={selectedUserPresetId}
|
||||||
onUserPresetChange={(presetId) => setSelectedUserPresetId(presetId)}
|
onUserPresetChange={(presetId) => setSelectedUserPresetId(presetId)}
|
||||||
allowEncodeItemSelection={state === 'READY_TO_ENCODE' && !reviewConfirmed && !queueLocked}
|
allowEncodeItemSelection={state === 'READY_TO_ENCODE' && !queueLocked}
|
||||||
onAddPreEncodeItem={(itemType) => {
|
onAddPreEncodeItem={(itemType) => {
|
||||||
setPreEncodeItems((prev) => {
|
setPreEncodeItems((prev) => {
|
||||||
const current = Array.isArray(prev) ? prev : [];
|
const current = Array.isArray(prev) ? prev : [];
|
||||||
|
|||||||
12
install.sh
12
install.sh
@@ -392,7 +392,7 @@ else
|
|||||||
ok "Benutzer '$SERVICE_USER' angelegt"
|
ok "Benutzer '$SERVICE_USER' angelegt"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for grp in cdrom optical disk; do
|
for grp in cdrom optical disk video render; do
|
||||||
if getent group "$grp" &>/dev/null; then
|
if getent group "$grp" &>/dev/null; then
|
||||||
usermod -aG "$grp" "$SERVICE_USER" 2>/dev/null || true
|
usermod -aG "$grp" "$SERVICE_USER" 2>/dev/null || true
|
||||||
info "Benutzer '$SERVICE_USER' zur Gruppe '$grp' hinzugefügt"
|
info "Benutzer '$SERVICE_USER' zur Gruppe '$grp' hinzugefügt"
|
||||||
@@ -541,6 +541,16 @@ StandardOutput=journal
|
|||||||
StandardError=journal
|
StandardError=journal
|
||||||
SyslogIdentifier=ripster-backend
|
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
|
NoNewPrivileges=true
|
||||||
ProtectSystem=full
|
ProtectSystem=full
|
||||||
ProtectHome=read-only
|
ProtectHome=read-only
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
56
site/api/crons/index.html
Normal file
56
site/api/crons/index.html
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -2,126 +2,130 @@
|
|||||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
<url>
|
<url>
|
||||||
<loc>https://mboehmlaender.github.io/ripster/</loc>
|
<loc>https://mboehmlaender.github.io/ripster/</loc>
|
||||||
<lastmod>2026-03-05</lastmod>
|
<lastmod>2026-03-10</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://mboehmlaender.github.io/ripster/api/</loc>
|
<loc>https://mboehmlaender.github.io/ripster/api/</loc>
|
||||||
<lastmod>2026-03-05</lastmod>
|
<lastmod>2026-03-10</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://mboehmlaender.github.io/ripster/api/crons/</loc>
|
||||||
|
<lastmod>2026-03-10</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://mboehmlaender.github.io/ripster/api/history/</loc>
|
<loc>https://mboehmlaender.github.io/ripster/api/history/</loc>
|
||||||
<lastmod>2026-03-05</lastmod>
|
<lastmod>2026-03-10</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://mboehmlaender.github.io/ripster/api/pipeline/</loc>
|
<loc>https://mboehmlaender.github.io/ripster/api/pipeline/</loc>
|
||||||
<lastmod>2026-03-05</lastmod>
|
<lastmod>2026-03-10</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://mboehmlaender.github.io/ripster/api/settings/</loc>
|
<loc>https://mboehmlaender.github.io/ripster/api/settings/</loc>
|
||||||
<lastmod>2026-03-05</lastmod>
|
<lastmod>2026-03-10</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://mboehmlaender.github.io/ripster/api/websocket/</loc>
|
<loc>https://mboehmlaender.github.io/ripster/api/websocket/</loc>
|
||||||
<lastmod>2026-03-05</lastmod>
|
<lastmod>2026-03-10</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://mboehmlaender.github.io/ripster/architecture/</loc>
|
<loc>https://mboehmlaender.github.io/ripster/architecture/</loc>
|
||||||
<lastmod>2026-03-05</lastmod>
|
<lastmod>2026-03-10</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://mboehmlaender.github.io/ripster/architecture/backend/</loc>
|
<loc>https://mboehmlaender.github.io/ripster/architecture/backend/</loc>
|
||||||
<lastmod>2026-03-05</lastmod>
|
<lastmod>2026-03-10</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://mboehmlaender.github.io/ripster/architecture/database/</loc>
|
<loc>https://mboehmlaender.github.io/ripster/architecture/database/</loc>
|
||||||
<lastmod>2026-03-05</lastmod>
|
<lastmod>2026-03-10</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://mboehmlaender.github.io/ripster/architecture/frontend/</loc>
|
<loc>https://mboehmlaender.github.io/ripster/architecture/frontend/</loc>
|
||||||
<lastmod>2026-03-05</lastmod>
|
<lastmod>2026-03-10</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://mboehmlaender.github.io/ripster/architecture/overview/</loc>
|
<loc>https://mboehmlaender.github.io/ripster/architecture/overview/</loc>
|
||||||
<lastmod>2026-03-05</lastmod>
|
<lastmod>2026-03-10</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://mboehmlaender.github.io/ripster/configuration/</loc>
|
<loc>https://mboehmlaender.github.io/ripster/configuration/</loc>
|
||||||
<lastmod>2026-03-05</lastmod>
|
<lastmod>2026-03-10</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://mboehmlaender.github.io/ripster/configuration/environment/</loc>
|
<loc>https://mboehmlaender.github.io/ripster/configuration/environment/</loc>
|
||||||
<lastmod>2026-03-05</lastmod>
|
<lastmod>2026-03-10</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://mboehmlaender.github.io/ripster/configuration/settings-reference/</loc>
|
<loc>https://mboehmlaender.github.io/ripster/configuration/settings-reference/</loc>
|
||||||
<lastmod>2026-03-05</lastmod>
|
<lastmod>2026-03-10</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://mboehmlaender.github.io/ripster/deployment/</loc>
|
<loc>https://mboehmlaender.github.io/ripster/deployment/</loc>
|
||||||
<lastmod>2026-03-05</lastmod>
|
<lastmod>2026-03-10</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://mboehmlaender.github.io/ripster/deployment/development/</loc>
|
<loc>https://mboehmlaender.github.io/ripster/deployment/development/</loc>
|
||||||
<lastmod>2026-03-05</lastmod>
|
<lastmod>2026-03-10</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://mboehmlaender.github.io/ripster/deployment/production/</loc>
|
<loc>https://mboehmlaender.github.io/ripster/deployment/production/</loc>
|
||||||
<lastmod>2026-03-05</lastmod>
|
<lastmod>2026-03-10</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://mboehmlaender.github.io/ripster/getting-started/</loc>
|
<loc>https://mboehmlaender.github.io/ripster/getting-started/</loc>
|
||||||
<lastmod>2026-03-05</lastmod>
|
<lastmod>2026-03-10</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://mboehmlaender.github.io/ripster/getting-started/configuration/</loc>
|
<loc>https://mboehmlaender.github.io/ripster/getting-started/configuration/</loc>
|
||||||
<lastmod>2026-03-05</lastmod>
|
<lastmod>2026-03-10</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://mboehmlaender.github.io/ripster/getting-started/installation/</loc>
|
<loc>https://mboehmlaender.github.io/ripster/getting-started/installation/</loc>
|
||||||
<lastmod>2026-03-05</lastmod>
|
<lastmod>2026-03-10</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://mboehmlaender.github.io/ripster/getting-started/prerequisites/</loc>
|
<loc>https://mboehmlaender.github.io/ripster/getting-started/prerequisites/</loc>
|
||||||
<lastmod>2026-03-05</lastmod>
|
<lastmod>2026-03-10</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://mboehmlaender.github.io/ripster/getting-started/quickstart/</loc>
|
<loc>https://mboehmlaender.github.io/ripster/getting-started/quickstart/</loc>
|
||||||
<lastmod>2026-03-05</lastmod>
|
<lastmod>2026-03-10</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://mboehmlaender.github.io/ripster/pipeline/</loc>
|
<loc>https://mboehmlaender.github.io/ripster/pipeline/</loc>
|
||||||
<lastmod>2026-03-05</lastmod>
|
<lastmod>2026-03-10</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://mboehmlaender.github.io/ripster/pipeline/encoding/</loc>
|
<loc>https://mboehmlaender.github.io/ripster/pipeline/encoding/</loc>
|
||||||
<lastmod>2026-03-05</lastmod>
|
<lastmod>2026-03-10</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://mboehmlaender.github.io/ripster/pipeline/playlist-analysis/</loc>
|
<loc>https://mboehmlaender.github.io/ripster/pipeline/playlist-analysis/</loc>
|
||||||
<lastmod>2026-03-05</lastmod>
|
<lastmod>2026-03-10</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://mboehmlaender.github.io/ripster/pipeline/post-encode-scripts/</loc>
|
<loc>https://mboehmlaender.github.io/ripster/pipeline/post-encode-scripts/</loc>
|
||||||
<lastmod>2026-03-05</lastmod>
|
<lastmod>2026-03-10</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://mboehmlaender.github.io/ripster/pipeline/workflow/</loc>
|
<loc>https://mboehmlaender.github.io/ripster/pipeline/workflow/</loc>
|
||||||
<lastmod>2026-03-05</lastmod>
|
<lastmod>2026-03-10</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://mboehmlaender.github.io/ripster/tools/</loc>
|
<loc>https://mboehmlaender.github.io/ripster/tools/</loc>
|
||||||
<lastmod>2026-03-05</lastmod>
|
<lastmod>2026-03-10</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://mboehmlaender.github.io/ripster/tools/handbrake/</loc>
|
<loc>https://mboehmlaender.github.io/ripster/tools/handbrake/</loc>
|
||||||
<lastmod>2026-03-05</lastmod>
|
<lastmod>2026-03-10</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://mboehmlaender.github.io/ripster/tools/makemkv/</loc>
|
<loc>https://mboehmlaender.github.io/ripster/tools/makemkv/</loc>
|
||||||
<lastmod>2026-03-05</lastmod>
|
<lastmod>2026-03-10</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://mboehmlaender.github.io/ripster/tools/mediainfo/</loc>
|
<loc>https://mboehmlaender.github.io/ripster/tools/mediainfo/</loc>
|
||||||
<lastmod>2026-03-05</lastmod>
|
<lastmod>2026-03-10</lastmod>
|
||||||
</url>
|
</url>
|
||||||
</urlset>
|
</urlset>
|
||||||
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user