HandBrake

This commit is contained in:
2026-03-09 20:37:56 +00:00
parent 1b07fa4f14
commit 4c879d2485
22 changed files with 1590 additions and 773 deletions

View File

@@ -6,11 +6,15 @@ Ripster ist eine lokale Web-Anwendung für halbautomatisches Disc-Ripping mit Ma
> **Neu seit letztem Release** > **Neu seit letztem Release**
> >
> - **Cron-Job-System** Skripte und Skript-Ketten zeitgesteuert ausführen; eigener Expression-Parser, Ausführungs-Logs, manuelle Auslösung, PushOver-Integration > - **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
> - **DVD-Erkennung verbessert** robuste Media-Profil-Erkennung (Blu-ray / DVD / CD) aus UDF/ISO9660-Dateisystemtyp, Laufwerk-Modell und Disc-Label > - **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) > - **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 in der Datenbank gespeichert > - **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) > - **`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
--- ---

View File

@@ -100,12 +100,14 @@ router.post(
const selectedTrackSelection = req.body?.selectedTrackSelection ?? null; const selectedTrackSelection = req.body?.selectedTrackSelection ?? null;
const selectedPostEncodeScriptIds = req.body?.selectedPostEncodeScriptIds; const selectedPostEncodeScriptIds = req.body?.selectedPostEncodeScriptIds;
const skipPipelineStateUpdate = Boolean(req.body?.skipPipelineStateUpdate); const skipPipelineStateUpdate = Boolean(req.body?.skipPipelineStateUpdate);
const selectedUserPresetId = req.body?.selectedUserPresetId ?? null;
logger.info('post:confirm-encode', { logger.info('post:confirm-encode', {
reqId: req.reqId, reqId: req.reqId,
jobId, jobId,
selectedEncodeTitleId, selectedEncodeTitleId,
selectedTrackSelectionProvided: Boolean(selectedTrackSelection), selectedTrackSelectionProvided: Boolean(selectedTrackSelection),
skipPipelineStateUpdate, skipPipelineStateUpdate,
selectedUserPresetId,
selectedPostEncodeScriptIdsCount: Array.isArray(selectedPostEncodeScriptIds) selectedPostEncodeScriptIdsCount: Array.isArray(selectedPostEncodeScriptIds)
? selectedPostEncodeScriptIds.length ? selectedPostEncodeScriptIds.length
: 0 : 0
@@ -114,7 +116,8 @@ router.post(
selectedEncodeTitleId, selectedEncodeTitleId,
selectedTrackSelection, selectedTrackSelection,
selectedPostEncodeScriptIds, selectedPostEncodeScriptIds,
skipPipelineStateUpdate skipPipelineStateUpdate,
selectedUserPresetId
}); });
res.json({ job }); res.json({ job });
}) })

View File

@@ -7,6 +7,7 @@ const notificationService = require('../services/notificationService');
const pipelineService = require('../services/pipelineService'); const pipelineService = require('../services/pipelineService');
const wsService = require('../services/websocketService'); const wsService = require('../services/websocketService');
const hardwareMonitorService = require('../services/hardwareMonitorService'); const hardwareMonitorService = require('../services/hardwareMonitorService');
const userPresetService = require('../services/userPresetService');
const logger = require('../services/logger').child('SETTINGS_ROUTE'); const logger = require('../services/logger').child('SETTINGS_ROUTE');
const router = express.Router(); const router = express.Router();
@@ -304,6 +305,52 @@ router.put(
}) })
); );
// ── User Presets ──────────────────────────────────────────────────────────────
router.get(
'/user-presets',
asyncHandler(async (req, res) => {
const mediaType = req.query.media_type || null;
logger.debug('get:user-presets', { reqId: req.reqId, mediaType });
const presets = await userPresetService.listPresets(mediaType);
res.json({ presets });
})
);
router.post(
'/user-presets',
asyncHandler(async (req, res) => {
const payload = req.body || {};
logger.info('post:user-presets:create', { reqId: req.reqId, name: payload?.name });
const preset = await userPresetService.createPreset(payload);
wsService.broadcast('USER_PRESETS_UPDATED', { action: 'created', id: preset.id });
res.status(201).json({ preset });
})
);
router.put(
'/user-presets/:id',
asyncHandler(async (req, res) => {
const presetId = Number(req.params.id);
const payload = req.body || {};
logger.info('put:user-presets:update', { reqId: req.reqId, presetId });
const preset = await userPresetService.updatePreset(presetId, payload);
wsService.broadcast('USER_PRESETS_UPDATED', { action: 'updated', id: preset.id });
res.json({ preset });
})
);
router.delete(
'/user-presets/:id',
asyncHandler(async (req, res) => {
const presetId = Number(req.params.id);
logger.info('delete:user-presets', { reqId: req.reqId, presetId });
const removed = await userPresetService.deletePreset(presetId);
wsService.broadcast('USER_PRESETS_UPDATED', { action: 'deleted', id: removed.id });
res.json({ removed });
})
);
router.post( router.post(
'/pushover/test', '/pushover/test',
asyncHandler(async (req, res) => { asyncHandler(async (req, res) => {

View File

@@ -17,6 +17,7 @@ const { ensureDir, sanitizeFileName, renderTemplate, findMediaFiles } = require(
const { buildMediainfoReview } = require('../utils/encodePlan'); const { buildMediainfoReview } = require('../utils/encodePlan');
const { analyzePlaylistObfuscation, normalizePlaylistId } = require('../utils/playlistAnalysis'); const { analyzePlaylistObfuscation, normalizePlaylistId } = require('../utils/playlistAnalysis');
const { errorToMeta } = require('../utils/errorMeta'); const { errorToMeta } = require('../utils/errorMeta');
const userPresetService = require('./userPresetService');
const RUNNING_STATES = new Set(['ANALYZING', 'RIPPING', 'ENCODING', 'MEDIAINFO_CHECK']); const RUNNING_STATES = new Set(['ANALYZING', 'RIPPING', 'ENCODING', 'MEDIAINFO_CHECK']);
const REVIEW_REFRESH_SETTING_PREFIXES = [ const REVIEW_REFRESH_SETTING_PREFIXES = [
@@ -5583,6 +5584,21 @@ class PipelineService extends EventEmitter {
throw error; throw error;
} }
// Resolve user preset if provided
const rawUserPresetId = options?.selectedUserPresetId ?? null;
const userPresetId = rawUserPresetId !== null && rawUserPresetId !== undefined
? Number(rawUserPresetId)
: null;
let resolvedUserPreset = null;
if (Number.isFinite(userPresetId) && userPresetId > 0) {
resolvedUserPreset = await userPresetService.getPresetById(userPresetId);
if (!resolvedUserPreset) {
const error = new Error(`User-Preset ${userPresetId} nicht gefunden.`);
error.statusCode = 404;
throw error;
}
}
const confirmedPlan = { const confirmedPlan = {
...planForConfirm, ...planForConfirm,
postEncodeScriptIds: selectedPostEncodeScripts.map((item) => Number(item.id)), postEncodeScriptIds: selectedPostEncodeScripts.map((item) => Number(item.id)),
@@ -5598,7 +5614,15 @@ class PipelineService extends EventEmitter {
postEncodeChainIds: selectedPostEncodeChainIds, postEncodeChainIds: selectedPostEncodeChainIds,
preEncodeChainIds: selectedPreEncodeChainIds, preEncodeChainIds: selectedPreEncodeChainIds,
reviewConfirmed: true, reviewConfirmed: true,
reviewConfirmedAt: nowIso() reviewConfirmedAt: nowIso(),
userPreset: resolvedUserPreset
? {
id: resolvedUserPreset.id,
name: resolvedUserPreset.name,
handbrakePreset: resolvedUserPreset.handbrakePreset,
extraArgs: resolvedUserPreset.extraArgs
}
: null
}; };
const inputPath = isPreRipMode const inputPath = isPreRipMode
? null ? null
@@ -5622,6 +5646,7 @@ class PipelineService extends EventEmitter {
+ ` Pre-Encode-Ketten: ${selectedPreEncodeChainIds.length > 0 ? selectedPreEncodeChainIds.join(',') : 'none'}.` + ` Pre-Encode-Ketten: ${selectedPreEncodeChainIds.length > 0 ? selectedPreEncodeChainIds.join(',') : 'none'}.`
+ ` Post-Encode-Scripte: ${selectedPostEncodeScripts.length > 0 ? selectedPostEncodeScripts.map((item) => item.name).join(' -> ') : 'none'}.` + ` Post-Encode-Scripte: ${selectedPostEncodeScripts.length > 0 ? selectedPostEncodeScripts.map((item) => item.name).join(' -> ') : 'none'}.`
+ ` Post-Encode-Ketten: ${selectedPostEncodeChainIds.length > 0 ? selectedPostEncodeChainIds.join(',') : 'none'}.` + ` Post-Encode-Ketten: ${selectedPostEncodeChainIds.length > 0 ? selectedPostEncodeChainIds.join(',') : 'none'}.`
+ (resolvedUserPreset ? ` User-Preset: "${resolvedUserPreset.name}" (ID ${resolvedUserPreset.id}).` : '')
); );
if (!skipPipelineStateUpdate) { if (!skipPipelineStateUpdate) {
@@ -6683,7 +6708,8 @@ class PipelineService extends EventEmitter {
trackSelection, trackSelection,
titleId: handBrakeTitleId, titleId: handBrakeTitleId,
mediaProfile, mediaProfile,
settingsMap: settings settingsMap: settings,
userPreset: encodePlan?.userPreset || null
}); });
if (trackSelection) { if (trackSelection) {
await historyService.appendLog( await historyService.appendLog(

View File

@@ -847,10 +847,20 @@ class SettingsService {
if (selectedTitleId !== null) { if (selectedTitleId !== null) {
baseArgs.push('-t', String(selectedTitleId)); baseArgs.push('-t', String(selectedTitleId));
} }
if (map.handbrake_preset) {
baseArgs.push('-Z', map.handbrake_preset); // User preset overrides settings-derived preset and extra args
const userPreset = options?.userPreset || null;
const effectiveHandbrakePreset = userPreset !== null
? (userPreset.handbrakePreset || null)
: (map.handbrake_preset || null);
const effectiveExtraArgs = userPreset !== null
? (userPreset.extraArgs || '')
: (map.handbrake_extra_args || '');
if (effectiveHandbrakePreset) {
baseArgs.push('-Z', effectiveHandbrakePreset);
} }
const extra = splitArgs(map.handbrake_extra_args); const extra = splitArgs(effectiveExtraArgs);
const rawSelection = options?.trackSelection || null; const rawSelection = options?.trackSelection || null;
const hasSelection = rawSelection && typeof rawSelection === 'object'; const hasSelection = rawSelection && typeof rawSelection === 'object';
@@ -860,7 +870,8 @@ class SettingsService {
args: [...baseArgs, ...extra], args: [...baseArgs, ...extra],
inputFile, inputFile,
outputFile, outputFile,
selectedTitleId selectedTitleId,
userPresetId: userPreset?.id || null
}); });
return { cmd, args: [...baseArgs, ...extra] }; return { cmd, args: [...baseArgs, ...extra] };
} }

View File

@@ -0,0 +1,133 @@
const { getDb } = require('../db/database');
const logger = require('./logger').child('USER_PRESET');
const VALID_MEDIA_TYPES = new Set(['bluray', 'dvd', 'other', 'all']);
function normalizeMediaType(value) {
const v = String(value || '').trim().toLowerCase();
return VALID_MEDIA_TYPES.has(v) ? v : 'all';
}
function rowToPreset(row) {
if (!row) {
return null;
}
return {
id: row.id,
name: row.name,
mediaType: row.media_type,
handbrakePreset: row.handbrake_preset || null,
extraArgs: row.extra_args || null,
description: row.description || null,
createdAt: row.created_at,
updatedAt: row.updated_at
};
}
async function listPresets(mediaType = null) {
const db = await getDb();
let rows;
if (mediaType && VALID_MEDIA_TYPES.has(mediaType)) {
rows = await db.all(
`SELECT * FROM user_presets WHERE media_type = ? OR media_type = 'all' ORDER BY name ASC`,
[mediaType]
);
} else {
rows = await db.all(`SELECT * FROM user_presets ORDER BY media_type ASC, name ASC`);
}
return rows.map(rowToPreset);
}
async function getPresetById(id) {
const db = await getDb();
const row = await db.get(`SELECT * FROM user_presets WHERE id = ? LIMIT 1`, [id]);
return rowToPreset(row);
}
async function createPreset(payload) {
const name = String(payload?.name || '').trim();
if (!name) {
const error = new Error('Preset-Name darf nicht leer sein.');
error.statusCode = 400;
throw error;
}
const mediaType = normalizeMediaType(payload?.mediaType);
const handbrakePreset = String(payload?.handbrakePreset || '').trim() || null;
const extraArgs = String(payload?.extraArgs || '').trim() || null;
const description = String(payload?.description || '').trim() || null;
const db = await getDb();
const result = await db.run(
`INSERT INTO user_presets (name, media_type, handbrake_preset, extra_args, description)
VALUES (?, ?, ?, ?, ?)`,
[name, mediaType, handbrakePreset, extraArgs, description]
);
const preset = await getPresetById(result.lastID);
logger.info('create', { id: preset.id, name: preset.name, mediaType: preset.mediaType });
return preset;
}
async function updatePreset(id, payload) {
const db = await getDb();
const existing = await getPresetById(id);
if (!existing) {
const error = new Error(`Preset ${id} nicht gefunden.`);
error.statusCode = 404;
throw error;
}
const name = payload?.name !== undefined ? String(payload.name || '').trim() : existing.name;
if (!name) {
const error = new Error('Preset-Name darf nicht leer sein.');
error.statusCode = 400;
throw error;
}
const mediaType = payload?.mediaType !== undefined
? normalizeMediaType(payload.mediaType)
: existing.mediaType;
const handbrakePreset = payload?.handbrakePreset !== undefined
? (String(payload.handbrakePreset || '').trim() || null)
: existing.handbrakePreset;
const extraArgs = payload?.extraArgs !== undefined
? (String(payload.extraArgs || '').trim() || null)
: existing.extraArgs;
const description = payload?.description !== undefined
? (String(payload.description || '').trim() || null)
: existing.description;
await db.run(
`UPDATE user_presets
SET name = ?, media_type = ?, handbrake_preset = ?, extra_args = ?, description = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?`,
[name, mediaType, handbrakePreset, extraArgs, description, id]
);
const updated = await getPresetById(id);
logger.info('update', { id: updated.id, name: updated.name });
return updated;
}
async function deletePreset(id) {
const db = await getDb();
const existing = await getPresetById(id);
if (!existing) {
const error = new Error(`Preset ${id} nicht gefunden.`);
error.statusCode = 404;
throw error;
}
await db.run(`DELETE FROM user_presets WHERE id = ?`, [id]);
logger.info('delete', { id: existing.id, name: existing.name });
return existing;
}
module.exports = {
listPresets,
getPresetById,
createPreset,
updatePreset,
deletePreset
};

BIN
bin/HandBrakeCLI Executable file

Binary file not shown.

View File

@@ -1,167 +1,194 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# =============================================================================
# HandBrake mit NVDEC aus Quellcode bauen
# Ubuntu 22.04 / 24.04, Debian 11 / 12
#
# Verwendung:
# sudo bash build-handbrake-nvdec.sh [--version 1.9.0]
#
# NVDEC benötigt zur Laufzeit den NVIDIA-Treiber (libnvcuvid.so).
# =============================================================================
set -euo pipefail set -euo pipefail
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd -P)"
BLUE='\033[0;34m'; BOLD='\033[1m'; RESET='\033[0m' REPO_ROOT="$SCRIPT_DIR"
BIN_DIR="${REPO_ROOT}/bin"
OUTPUT_BIN="${BIN_DIR}/HandBrakeCLI"
OUTPUT_TMP="${BIN_DIR}/.HandBrakeCLI.build-tmp"
HANDBRAKE_VERSION="${1:-1.10.0}"
JOBS="${JOBS:-$(nproc)}"
export LANG="${LANG:-C.UTF-8}"
export LC_ALL="${LC_ALL:-C.UTF-8}"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
RESET='\033[0m'
info() { echo -e "${BLUE}[INFO]${RESET} $*"; } info() { echo -e "${BLUE}[INFO]${RESET} $*"; }
ok() { echo -e "${GREEN}[OK]${RESET} $*"; } ok() { echo -e "${GREEN}[OK]${RESET} $*"; }
warn() { echo -e "${YELLOW}[WARN]${RESET} $*"; } warn() { echo -e "${YELLOW}[WARN]${RESET} $*"; }
fatal() { echo -e "${RED}[FEHLER]${RESET} $*" >&2; exit 1; } error() { echo -e "${RED}[ERROR]${RESET} $*" >&2; }
fatal() { error "$*"; exit 1; }
HANDBRAKE_VERSION="1.9.0" run_as_root() {
if [[ "${EUID}" -eq 0 ]]; then
"$@"
elif command -v sudo >/dev/null 2>&1; then
sudo "$@"
else
fatal "Root-Rechte erforderlich. Bitte als root ausführen oder sudo installieren."
fi
}
while [[ $# -gt 0 ]]; do apt_get() {
case "$1" in run_as_root env DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a \
--version) HANDBRAKE_VERSION="$2"; shift 2 ;; apt-get -o Dpkg::Use-Pty=0 "$@"
-h|--help) echo "Verwendung: sudo bash $0 [--version X.Y.Z]"; exit 0 ;; }
*) fatal "Unbekannte Option: $1" ;;
esac
done
[[ $EUID -eq 0 ]] || fatal "Bitte als root ausführen: sudo bash $0" require_cmd() {
local cmd="$1"
command -v "$cmd" >/dev/null 2>&1 || fatal "Benötigter Befehl fehlt: $cmd"
}
[[ -f /etc/os-release ]] && . /etc/os-release || fatal "OS nicht erkennbar" cleanup_stale_tmp_build_dirs() {
local stale_dirs=()
shopt -s nullglob
stale_dirs=(/tmp/handbrake-nvdec-build-*)
shopt -u nullglob
echo -e "\n${BOLD}${BLUE}══════════════════════════════════════════${RESET}" if [[ ${#stale_dirs[@]} -gt 0 ]]; then
echo -e "${BOLD} HandBrake ${HANDBRAKE_VERSION} mit NVDEC bauen${RESET}" warn "Bereinige alte temporäre Build-Ordner in /tmp..."
echo -e "${BOLD}${BLUE}══════════════════════════════════════════${RESET}\n" run_as_root rm -rf "${stale_dirs[@]}"
fi
}
# -------------------------------------------------------------------------- repair_package_state() {
# 1. Build-Abhängigkeiten local audit_output=""
# -------------------------------------------------------------------------- audit_output="$(run_as_root dpkg --audit || true)"
info "Installiere Build-Abhängigkeiten..."
apt-get update -qq if [[ -n "${audit_output//[[:space:]]/}" ]]; then
apt-get install -y \ warn "Unvollständiger Paketstatus erkannt. Repariere dpkg/apt..."
run_as_root env DEBIAN_FRONTEND=noninteractive dpkg --configure -a
apt_get --fix-broken install -y
ok "Paketstatus repariert."
fi
}
install_build_dependencies() {
repair_package_state
info "Aktualisiere Paketlisten..."
apt_get update
info "Installiere Build-Abhängigkeiten..."
apt_get install -y \
autoconf automake build-essential cmake git \ autoconf automake build-essential cmake git \
libass-dev libbz2-dev libdvdnav-dev libdvdread-dev \ libass-dev libbz2-dev libfontconfig-dev libfreetype-dev libfribidi-dev libharfbuzz-dev \
libfontconfig-dev libfreetype-dev libfribidi-dev libharfbuzz-dev \
libjansson-dev liblzma-dev libmp3lame-dev libnuma-dev libogg-dev \ libjansson-dev liblzma-dev libmp3lame-dev libnuma-dev libogg-dev \
libopus-dev libsamplerate0-dev libspeex-dev libtheora-dev libtool \ libopus-dev libsamplerate0-dev libspeex-dev libtheora-dev libtool libtool-bin \
libturbojpeg0-dev libvorbis-dev libvpx-dev libx264-dev libxml2-dev \ libturbojpeg0-dev libvorbis-dev libx264-dev libxml2-dev libvpx-dev \
m4 meson nasm ninja-build patch pkg-config python3 tar zlib1g-dev \ m4 make meson nasm ninja-build patch pkg-config tar zlib1g-dev \
>/dev/null curl libssl-dev clang bzip2 ca-certificates wget libffmpeg-nvenc-dev
ok "Build-Abhängigkeiten installiert"
# -------------------------------------------------------------------------- if [[ ! -f /usr/include/ffnvcodec/dynlink_nvcuvid.h ]]; then
# 2. CUDA-Header für NVDEC warn "NVDEC-Header (dynlink_nvcuvid.h) nicht gefunden. Versuche nvidia-cuda-toolkit als Fallback..."
# -------------------------------------------------------------------------- if ! apt_get install -y nvidia-cuda-toolkit; then
info "Prüfe CUDA-Header für NVDEC-Support..." fatal "NVDEC-Header fehlen und nvidia-cuda-toolkit konnte nicht installiert werden."
if dpkg -l 2>/dev/null | grep -q '^ii.*nvidia-cuda-toolkit'; then fi
ok "nvidia-cuda-toolkit bereits installiert" if [[ ! -f /usr/include/ffnvcodec/dynlink_nvcuvid.h && ! -f /usr/include/nvcuvid.h ]]; then
else fatal "NVDEC-Header weiterhin nicht vorhanden. Prüfe Repository-Konfiguration (universe/multiverse)."
info "Installiere nvidia-cuda-toolkit (für NVDEC-Header)..."
if apt-get install -y nvidia-cuda-toolkit >/dev/null 2>&1; then
ok "nvidia-cuda-toolkit installiert"
else
warn "nvidia-cuda-toolkit nicht in Standard-Repos versuche NVIDIA CUDA Repo..."
local_ver="${VERSION_ID//./}"
cuda_deb="/tmp/cuda-keyring.deb"
if curl -fsSL \
"https://developer.download.nvidia.com/compute/cuda/repos/ubuntu${local_ver}/x86_64/cuda-keyring_1.1-1_all.deb" \
-o "$cuda_deb" 2>/dev/null; then
dpkg -i "$cuda_deb"
apt-get update -qq
# Minimale Header statt vollem Toolkit
apt-get install -y cuda-cudart-dev-12-8 >/dev/null 2>&1 && \
ok "CUDA-Header installiert (cuda-cudart-dev-12-8)" || \
warn "CUDA-Header-Installation fehlgeschlagen NVDEC könnte im Build fehlen."
else
warn "NVIDIA CUDA Repo nicht erreichbar NVDEC könnte im Build fehlen."
fi fi
fi fi
fi }
# -------------------------------------------------------------------------- download_source() {
# 3. Alle vorhandenen HandBrake-Installationen entfernen local tarball="$1"
# -------------------------------------------------------------------------- local url="https://github.com/HandBrake/HandBrake/releases/download/${HANDBRAKE_VERSION}/HandBrake-${HANDBRAKE_VERSION}-source.tar.bz2"
info "Entferne alle vorhandenen HandBrake-Installationen..."
apt-get remove -y handbrake-cli handbrake 2>/dev/null || true
snap remove handbrake-cli 2>/dev/null || true
rm -f /usr/bin/HandBrakeCLI \
/usr/local/bin/HandBrakeCLI \
/snap/bin/handbrake-cli \
/snap/bin/HandBrakeCLI
while true; do
FOUND=$(command -v HandBrakeCLI 2>/dev/null || true)
[[ -z "$FOUND" ]] && break
warn "Entferne: $FOUND"
rm -f "$FOUND"
done
hash -r 2>/dev/null || true
ok "Alte HandBrake-Installation(en) entfernt"
# -------------------------------------------------------------------------- info "Lade HandBrake ${HANDBRAKE_VERSION} Quellcode..."
# 4. Quellcode herunterladen if command -v curl >/dev/null 2>&1; then
# -------------------------------------------------------------------------- curl -fL "$url" -o "$tarball"
TMP_DIR=$(mktemp -d) else
trap 'cd /; rm -rf "$TMP_DIR"' EXIT wget -O "$tarball" "$url"
fi
}
SRC_URL="https://github.com/HandBrake/HandBrake/releases/download/${HANDBRAKE_VERSION}/HandBrake-${HANDBRAKE_VERSION}-source.tar.bz2" main() {
TARBALL="${TMP_DIR}/handbrake-src.tar.bz2" if [[ ! -f /etc/os-release ]]; then
fatal "/etc/os-release fehlt. Nur Debian/Ubuntu/Proxmox werden unterstützt."
fi
# shellcheck disable=SC1091
. /etc/os-release
case "${ID:-}" in
debian|ubuntu|linuxmint|pop) ;;
*)
warn "Ungetestetes Betriebssystem: ${PRETTY_NAME:-unknown}. Es wird trotzdem versucht fortzufahren."
;;
esac
info "Lade HandBrake ${HANDBRAKE_VERSION} herunter..." require_cmd nproc
info "URL: ${SRC_URL}" require_cmd tar
curl -fL --progress-bar "$SRC_URL" -o "$TARBALL" || \ require_cmd dpkg
wget --progress=bar:force "$SRC_URL" -O "$TARBALL" || \
fatal "Download fehlgeschlagen. Bitte Version prüfen: https://github.com/HandBrake/HandBrake/releases"
info "Entpacke..." cleanup_stale_tmp_build_dirs
tar xjf "$TARBALL" -C "$TMP_DIR" install_build_dependencies
require_cmd make
SRC_DIR="${TMP_DIR}/HandBrake-${HANDBRAKE_VERSION}" local tmp_dir tarball src_dir
[[ -d "$SRC_DIR" ]] || SRC_DIR=$(find "$TMP_DIR" -maxdepth 1 -type d -name "HandBrake*" | head -1) tmp_dir="$(mktemp -d -p /tmp handbrake-nvdec-build-XXXXXX)"
[[ -d "$SRC_DIR" ]] || fatal "Quellverzeichnis nicht gefunden" tarball="${tmp_dir}/HandBrake-${HANDBRAKE_VERSION}-source.tar.bz2"
src_dir="${tmp_dir}/HandBrake-${HANDBRAKE_VERSION}"
# -------------------------------------------------------------------------- cleanup() {
# 5. Konfigurieren & Bauen rm -rf "$tmp_dir"
# -------------------------------------------------------------------------- rm -f "$OUTPUT_TMP"
cd "$SRC_DIR" }
trap cleanup EXIT INT TERM
info "Konfiguriere HandBrake mit NVDEC (--enable-nvdec)..." download_source "$tarball"
./configure \
--launch-jobs="$(nproc)" \ info "Entpacke Quellcode..."
tar xjf "$tarball" -C "$tmp_dir"
[[ -d "$src_dir" ]] || fatal "Entpacktes Quellverzeichnis nicht gefunden: $src_dir"
local configure_log
configure_log="${tmp_dir}/configure.log"
info "Konfiguriere Build (NVDEC aktiviert)..."
(
cd "$src_dir"
./configure \
--launch-jobs="$JOBS" \
--enable-nvdec \ --enable-nvdec \
--prefix=/usr/local \ --disable-gtk \
2>&1 | tail -15 --prefix=/usr/local >"$configure_log" 2>&1
)
info "Baue HandBrake mit $(nproc) Threads das dauert 1030 Minuten..." if ! rg -q 'Enable NVDEC:[[:space:]]+True' "$configure_log"; then
make --directory=build -j"$(nproc)" tail -n 80 "$configure_log" >&2 || true
fatal "Configure hat NVDEC nicht aktiviert (Enable NVDEC != True)."
info "Installiere nach /usr/local/bin/..."
make --directory=build install
# --------------------------------------------------------------------------
# 6. Ergebnis prüfen
# --------------------------------------------------------------------------
if command -v HandBrakeCLI &>/dev/null; then
VER=$(HandBrakeCLI --version 2>&1 | head -1)
ok "Erfolgreich installiert: ${VER}"
echo ""
# NVDEC im Binary prüfen
if HandBrakeCLI --help 2>&1 | grep -qi "nvdec"; then
ok "NVDEC: im Binary vorhanden ✓"
else
warn "NVDEC: nicht in --help gefunden (evtl. kein --enable-nvdec oder kein CUDA-Header)"
fi fi
# Laufzeit-Bibliothek prüfen if ! rg -q 'Enable NVENC:[[:space:]]+True' "$configure_log"; then
if ldconfig -p 2>/dev/null | grep -q libnvcuvid; then tail -n 80 "$configure_log" >&2 || true
ok "libnvcuvid: gefunden NVDEC zur Laufzeit verfügbar ✓" fatal "Configure hat NVENC nicht aktiviert (Enable NVENC != True)."
else
warn "libnvcuvid: NICHT gefunden"
warn "→ Bitte NVIDIA-Treiber installieren: apt-get install nvidia-driver-XXX"
warn " NVDEC ist im Binary vorhanden, funktioniert aber erst mit dem Treiber."
fi fi
else
fatal "HandBrakeCLI nach dem Build nicht gefunden Build fehlgeschlagen." rg 'Enable NVENC|Enable NVDEC' "$configure_log" || true
fi
info "Baue HandBrakeCLI mit ${JOBS} Threads (das kann länger dauern)..."
make --directory="${src_dir}/build" -j"$JOBS"
[[ -x "${src_dir}/build/HandBrakeCLI" ]] || fatal "Build erfolgreich, aber HandBrakeCLI wurde nicht gefunden."
mkdir -p "$BIN_DIR"
install -m 0755 "${src_dir}/build/HandBrakeCLI" "$OUTPUT_TMP"
if "$OUTPUT_TMP" --help 2>&1 | rg -qi "nvdec|nvenc"; then
ok "Hinweis: NVENC/NVDEC-Begriffe in --help gefunden."
else
warn "--help zeigt NVENC/NVDEC nicht explizit. Maßgeblich ist die Configure-Zusammenfassung (Enable NVENC/NVDEC: True)."
fi
mv -f "$OUTPUT_TMP" "$OUTPUT_BIN"
ok "Fertig: ${OUTPUT_BIN}"
"$OUTPUT_BIN" --version | head -1
info "Aufgeräumt: Nur ${OUTPUT_BIN} bleibt im Repository als Build-Artefakt."
}
main "$@"

View File

@@ -132,6 +132,19 @@ CREATE TABLE cron_run_logs (
CREATE INDEX idx_cron_run_logs_job ON cron_run_logs(cron_job_id, id DESC); CREATE INDEX idx_cron_run_logs_job ON cron_run_logs(cron_job_id, id DESC);
CREATE TABLE user_presets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
media_type TEXT NOT NULL DEFAULT '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
);
CREATE INDEX idx_user_presets_media_type ON user_presets(media_type);
-- ============================================================================= -- =============================================================================
-- Default Settings Seed -- Default Settings Seed
-- ============================================================================= -- =============================================================================

View File

@@ -115,11 +115,11 @@ Sendet eine Test-Benachrichtigung über PushOver.
## Skript-Verwaltung ## Skript-Verwaltung
Post-Encode-Skripte werden über eigene Endpunkte unter `/api/settings/scripts` verwaltet. 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 ### GET /api/settings/scripts
Gibt alle konfigurierten Skripte zurück. Gibt alle Skripte zurück, sortiert nach `orderIndex`.
**Response:** **Response:**
@@ -127,11 +127,12 @@ Gibt alle konfigurierten Skripte zurück.
{ {
"scripts": [ "scripts": [
{ {
"id": "script-abc123", "id": 1,
"name": "Zu Plex verschieben", "name": "Zu Plex verschieben",
"command": "/home/michael/scripts/move-to-plex.sh", "scriptBody": "mv \"$RIPSTER_OUTPUT_PATH\" /mnt/plex/movies/",
"description": "Verschiebt die fertige Datei ins Plex-Verzeichnis", "orderIndex": 1,
"createdAt": "2024-01-15T10:00:00.000Z" "createdAt": "2026-01-15T10:00:00.000Z",
"updatedAt": "2026-01-15T10:00:00.000Z"
} }
] ]
} }
@@ -141,75 +142,46 @@ Gibt alle konfigurierten Skripte zurück.
### POST /api/settings/scripts ### POST /api/settings/scripts
Legt ein neues Post-Encode-Skript an. Legt ein neues Skript an.
**Request:** **Request:**
```json ```json
{ {
"name": "Zu Plex verschieben", "name": "Zu Plex verschieben",
"command": "/home/michael/scripts/move-to-plex.sh", "scriptBody": "mv \"$RIPSTER_OUTPUT_PATH\" /mnt/plex/movies/"
"description": "Verschiebt die fertige Datei ins Plex-Verzeichnis"
} }
``` ```
| Feld | Typ | Pflicht | Beschreibung | | Feld | Typ | Pflicht | Beschreibung |
|------|-----|---------|-------------| |------|-----|---------|-------------|
| `name` | string | ✅ | Anzeigename | | `name` | string | ✅ | Anzeigename (eindeutig) |
| `command` | string | ✅ | Shell-Befehl oder absoluter Skriptpfad | | `scriptBody` | string | ✅ | Shell-Befehl oder mehrzeiliges Skript |
| `description` | string | — | Optionale Beschreibung |
**Response:** **Response:** `201 Created` `{ "script": { ... } }`
```json
{
"ok": true,
"script": {
"id": "script-abc123",
"name": "Zu Plex verschieben",
"command": "/home/michael/scripts/move-to-plex.sh"
}
}
```
--- ---
### PUT /api/settings/scripts/:scriptId ### PUT /api/settings/scripts/:id
Aktualisiert ein vorhandenes Skript. Aktualisiert ein vorhandenes Skript. Alle Felder optional.
**URL-Parameter:** `scriptId`
**Request:** Gleiche Felder wie beim Anlegen (alle optional).
```json
{ "name": "Zu Jellyfin verschieben", "command": "/home/michael/scripts/move-to-jellyfin.sh" }
```
**Response:** `{ "ok": true }`
--- ---
### DELETE /api/settings/scripts/:scriptId ### DELETE /api/settings/scripts/:id
Löscht ein Skript. Löscht ein Skript.
**URL-Parameter:** `scriptId` !!! warning "Referenzen"
Das Skript wird gelöscht, auch wenn es in Job-Historien referenziert ist. In zukünftigen Reviews erscheint es nicht mehr.
**Response:** `{ "ok": true }`
!!! warning "Referenzen in Jobs"
Wenn das Skript in laufenden oder abgeschlossenen Jobs referenziert wird, wird es trotzdem gelöscht. In zukünftigen Encode-Reviews erscheint es nicht mehr.
--- ---
### POST /api/settings/scripts/:scriptId/test ### POST /api/settings/scripts/:id/test
Führt ein Skript mit Platzhalter-Umgebungsvariablen aus (Testlauf). Führt ein Skript mit Platzhalter-Umgebungsvariablen aus (Testlauf).
**URL-Parameter:** `scriptId` **Response:**
**Response (Erfolg):**
```json ```json
{ {
@@ -221,18 +193,6 @@ Führt ein Skript mit Platzhalter-Umgebungsvariablen aus (Testlauf).
} }
``` ```
**Response (Fehler):**
```json
{
"ok": false,
"exitCode": 1,
"stdout": "",
"stderr": "Datei nicht gefunden: /home/michael/scripts/move-to-plex.sh",
"durationMs": 12
}
```
**Platzhalter-Werte beim Testlauf:** **Platzhalter-Werte beim Testlauf:**
| Variable | Testwert | | Variable | Testwert |
@@ -246,28 +206,149 @@ Führt ein Skript mit Platzhalter-Umgebungsvariablen aus (Testlauf).
--- ---
### POST /api/settings/scripts/reorder
Ändert die Reihenfolge der Skripte (persistiert in `order_index`).
**Request:**
```json
{ "orderedScriptIds": [3, 1, 2] }
```
**Response:** `{ "scripts": [ ... ] }` alle Skripte in neuer Reihenfolge.
---
## Skript-Ketten-Verwaltung
Skript-Ketten werden unter `/api/settings/script-chains` verwaltet.
### GET /api/settings/script-chains
Gibt alle Ketten zurück (inkl. Schritte).
### POST /api/settings/script-chains
Legt eine neue Kette an.
```json
{ "name": "Nach Jellyfin deployen" }
```
### PUT /api/settings/script-chains/:id
Aktualisiert eine Kette (Name, Schritte).
### DELETE /api/settings/script-chains/:id
Löscht eine Kette und alle ihre Schritte.
### POST /api/settings/script-chains/:id/test
Führt eine Kette mit Platzhalter-Umgebungsvariablen aus (Testlauf).
**Response:**
```json
{
"result": {
"success": true,
"steps": [
{ "scriptId": 1, "scriptName": "Zu Plex verschieben", "success": true, "exitCode": 0 }
]
}
}
```
### POST /api/settings/script-chains/reorder
Ändert die Reihenfolge der Ketten (persistiert in `order_index`).
**Request:**
```json
{ "orderedChainIds": [2, 1, 3] }
```
---
## User-Presets
Benannte HandBrake-Preset-Sammlungen, die im Encode-Review schnell angewendet werden können. Unter `/api/settings/user-presets` verwaltet.
### GET /api/settings/user-presets
Gibt alle User-Presets zurück. Optional gefiltert per Query-Parameter `mediaType`.
**Query-Parameter:**
| Parameter | Werte | Beschreibung |
|-----------|-------|-------------|
| `mediaType` | `bluray`, `dvd`, `other`, `all` | Filtert Presets nach Medientyp |
**Response:**
```json
{
"presets": [
{
"id": 1,
"name": "Blu-ray High Quality",
"mediaType": "bluray",
"handbrakePreset": "H.265 MKV 1080p30",
"extraArgs": "--encoder-preset slow",
"description": "Langsam, aber beste Qualität",
"createdAt": "2026-01-15T10:00:00.000Z",
"updatedAt": "2026-01-15T10:00:00.000Z"
}
]
}
```
---
### POST /api/settings/user-presets
Legt ein neues User-Preset an.
**Request:**
```json
{
"name": "Blu-ray High Quality",
"mediaType": "bluray",
"handbrakePreset": "H.265 MKV 1080p30",
"extraArgs": "--encoder-preset slow",
"description": "Langsam, aber beste Qualität"
}
```
| Feld | Typ | Pflicht | Beschreibung |
|------|-----|---------|-------------|
| `name` | string | ✅ | Anzeigename |
| `mediaType` | string | — | `bluray`, `dvd`, `other`, `all` (Standard: `all`) |
| `handbrakePreset` | string | — | HandBrake-Preset-Name (`-Z`) |
| `extraArgs` | string | — | Zusatz-CLI-Argumente |
| `description` | string | — | Optionale Beschreibung |
**Response:** `201 Created` `{ "preset": { ... } }`
---
### PUT /api/settings/user-presets/:id
Aktualisiert ein User-Preset. Alle Felder optional.
---
### DELETE /api/settings/user-presets/:id
Löscht ein User-Preset.
---
## Einstellungs-Schlüssel Referenz ## Einstellungs-Schlüssel Referenz
Eine vollständige Liste aller Einstellungs-Schlüssel: Eine vollständige Übersicht aller Schlüssel:
[:octicons-arrow-right-24: Einstellungsreferenz](../configuration/settings-reference.md)
| Schlüssel | Kategorie | Typ | Beschreibung |
|---------|----------|-----|-------------|
| `raw_dir` | paths | string | Raw-MKV Verzeichnis |
| `movie_dir` | paths | string | Ausgabe-Verzeichnis |
| `log_dir` | paths | string | Log-Verzeichnis |
| `makemkv_command` | tools | string | MakeMKV-Befehl |
| `handbrake_command` | tools | string | HandBrake-Befehl |
| `mediainfo_command` | tools | string | MediaInfo-Befehl |
| `handbrake_preset` | encoding | string | HandBrake-Preset-Name |
| `handbrake_extra_args` | encoding | string | Zusatz-Argumente |
| `output_extension` | encoding | string | Dateiendung (z.B. `mkv`) |
| `filename_template` | encoding | string | Dateiname-Template |
| `drive_mode` | drive | select | `auto` oder `explicit` |
| `drive_device` | drive | string | Geräte-Pfad |
| `disc_poll_interval_ms` | drive | number | Polling-Intervall (ms) |
| `makemkv_min_length_minutes` | makemkv | number | Min. Titellänge (Minuten) |
| `makemkv_backup_mode` | makemkv | boolean | Backup-Modus aktivieren |
| `omdb_api_key` | omdb | string | OMDb API-Key |
| `omdb_default_type` | omdb | select | Standard-Suchtyp |
| `pushover_user_key` | notifications | string | PushOver User-Key |
| `pushover_api_token` | notifications | string | PushOver API-Token |

View File

@@ -167,6 +167,52 @@ Fehler im Laufwerkserkennungsdienst.
} }
``` ```
### SETTINGS_SCRIPTS_UPDATED
Wird gesendet, wenn ein Skript angelegt, aktualisiert, gelöscht oder umsortiert wurde.
```json
{
"type": "SETTINGS_SCRIPTS_UPDATED",
"payload": {
"action": "reordered",
"count": 3
}
}
```
`action` ist `"created"`, `"updated"`, `"deleted"` oder `"reordered"`.
### SETTINGS_SCRIPT_CHAINS_UPDATED
Wird gesendet bei Änderungen an Skript-Ketten.
```json
{
"type": "SETTINGS_SCRIPT_CHAINS_UPDATED",
"payload": {
"action": "created",
"id": 2
}
}
```
### USER_PRESETS_UPDATED
Wird gesendet, wenn ein User-Preset angelegt, aktualisiert oder gelöscht wurde.
```json
{
"type": "USER_PRESETS_UPDATED",
"payload": {
"action": "created",
"id": 1
}
}
```
`action` ist `"created"`, `"updated"` oder `"deleted"`.
### CRON_JOBS_UPDATED ### CRON_JOBS_UPDATED
Wird gesendet, wenn ein Cron-Job angelegt, aktualisiert oder gelöscht wurde. Wird gesendet, wenn ein Cron-Job angelegt, aktualisiert oder gelöscht wurde.

View File

@@ -46,7 +46,7 @@ flowchart LR
WUD -->|selectMetadata(selectedPlaylist)| MIC WUD -->|selectMetadata(selectedPlaylist)| MIC
MIC -->|Tracks analysiert| RTE[READY_TO\nENCODE] MIC -->|Tracks analysiert| RTE[READY_TO\nENCODE]
RTE -->|confirmEncodeReview() + startPreparedJob()| ENC[ENCODING] RTE -->|confirmEncodeReview() + startPreparedJob()| ENC[ENCODING]
ENC -->|HandBrake + Post-Skripte fertig| FIN([FINISHED]) ENC -->|Pre-Encode → HandBrake Post-Encode fertig| FIN([FINISHED])
ENC -->|Abbruch| CAN([CANCELLED]) ENC -->|Abbruch| CAN([CANCELLED])
ENC -->|Fehler| ERR([ERROR]) ENC -->|Fehler| ERR([ERROR])
RIP -->|Fehler| ERR RIP -->|Fehler| ERR
@@ -84,10 +84,18 @@ Der Service pollt das Laufwerk im konfigurierten Intervall (`disc_poll_interval_
```js ```js
// Ereignisse // Ereignisse
emit('discInserted', { path: '/dev/sr0' }) emit('discInserted', { path: '/dev/sr0', mediaProfile: 'bluray', ... })
emit('discRemoved', { path: '/dev/sr0' }) 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 ## processRunner.js
@@ -170,20 +178,50 @@ Verwaltet alle Anwendungseinstellungen.
### Features ### Features
- **Schema-getriebene Validierung**: Jede Einstellung hat Typ, Grenzen und Pflichtfeld-Flag - **Schema-getriebene Validierung**: Jede Einstellung hat Typ, Grenzen und Pflichtfeld-Flag
- **Kategorisierung**: Einstellungen sind in Kategorien gruppiert (Paths, Tools, Encoding, ...) - **Kategorisierung**: Einstellungen sind in Kategorien gruppiert (Pfade, Tools, Metadaten, …)
- **Persistenz**: Werte in SQLite, Schema ebenfalls in SQLite - **Persistenz**: Werte in SQLite, Schema ebenfalls in SQLite
- **Defaults**: `defaultSettings.js` definiert Standardwerte - **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 ### Einstellungs-Kategorien
| Kategorie | Einstellungen | | Kategorie | Ausgewählte Schlüssel |
|-----------|--------------| |-----------|----------------------|
| `Pfade` | `raw_dir`, `movie_dir`, `log_dir` | | `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` | | `Laufwerk` | `drive_mode`, `drive_device`, `disc_poll_interval_ms`, `makemkv_source_index` |
| `Monitoring` | `hardware_monitoring_enabled`, `hardware_monitoring_interval_ms` | | `Monitoring` | `hardware_monitoring_enabled`, `hardware_monitoring_interval_ms` |
| `Tools` | `makemkv_command`, `handbrake_command`, `mediainfo_command`, `pipeline_max_parallel_jobs` | | `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` | | `Metadaten` | `omdb_api_key`, `omdb_default_type` |
| `Benachrichtigungen` | `pushover_user_key`, `pushover_api_token` | | `Benachrichtigungen` | `pushover_enabled`, `pushover_token`, `pushover_notify_*` |
---
## userPresetService.js
Verwaltet benannte HandBrake-Preset-Sammlungen pro Medientyp.
### Methoden
| Methode | Beschreibung |
|---------|-------------|
| `listPresets(mediaType?)` | Alle Presets; optional nach Medientyp filtern (`bluray`/`dvd`/`other`/`all`) |
| `getPresetById(id)` | Einzelnes Preset |
| `createPreset(payload)` | Neues Preset anlegen |
| `updatePreset(id, payload)` | Preset aktualisieren |
| `deletePreset(id)` | Preset löschen |
!!! info "mediaType = 'all'"
Presets mit `mediaType = 'all'` erscheinen bei Filterung nach jedem Medientyp.
--- ---
@@ -204,6 +242,33 @@ Datenbankoperationen für Job-Historie.
--- ---
## 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 ## notificationService.js
PushOver-Push-Benachrichtigungen. PushOver-Push-Benachrichtigungen.

View File

@@ -14,6 +14,7 @@ pipeline_state -- Aktueller Pipeline-Zustand (Singleton)
scripts -- Shell-Skripte für Pre-/Post-Encode-Ausführung scripts -- Shell-Skripte für Pre-/Post-Encode-Ausführung
script_chains -- Geordnete Ketten aus mehreren Skripten script_chains -- Geordnete Ketten aus mehreren Skripten
script_chain_steps -- Einzelschritte einer Skript-Kette script_chain_steps -- Einzelschritte einer Skript-Kette
user_presets -- Benannte HandBrake-Preset-Sammlungen pro Medientyp
cron_jobs -- Zeitgesteuerte Aufgaben (eigener Cron-Parser) cron_jobs -- Zeitgesteuerte Aufgaben (eigener Cron-Parser)
cron_run_logs -- Ausführungs-Protokolle für Cron-Jobs cron_run_logs -- Ausführungs-Protokolle für Cron-Jobs
``` ```
@@ -161,6 +162,28 @@ CREATE TABLE script_chain_steps (
--- ---
## Tabelle: user_presets
Speichert benannte HandBrake-Preset-Sammlungen pro Medientyp.
```sql
CREATE TABLE user_presets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
media_type TEXT NOT NULL DEFAULT 'all', -- 'bluray', 'dvd', 'other', 'all'
handbrake_preset TEXT,
extra_args TEXT,
description TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
```
!!! info "Medientyp-Filter"
`GET /api/settings/user-presets?mediaType=bluray` gibt Presets mit `media_type = 'bluray'` **und** `media_type = 'all'` zurück.
---
## Tabellen: cron_jobs & cron_run_logs ## Tabellen: cron_jobs & cron_run_logs
Speichern den Zeitplan und die Ausführungs-Historie des eingebauten Cron-Systems. Speichern den Zeitplan und die Ausführungs-Historie des eingebauten Cron-Systems.

View File

@@ -1,138 +1,180 @@
# Einstellungsreferenz # Einstellungsreferenz
Vollständige Übersicht aller Ripster-Einstellungen. Alle Einstellungen werden über die Web-Oberfläche unter **Einstellungen** verwaltet. Vollständige Übersicht aller Ripster-Einstellungen. Alle Einstellungen werden über die Web-Oberfläche unter **Einstellungen** verwaltet und in SQLite gespeichert.
--- ---
## Kategorie: Pfade (paths) ## Profil-System
| Schlüssel | Typ | Standard | Pflicht | Beschreibung | 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:
|---------|-----|---------|---------|-------------|
| `raw_dir` | string | — | ✅ | Verzeichnis für rohe MKV-Dateien nach dem Ripping |
| `movie_dir` | string | — | ✅ | Ausgabeverzeichnis für encodierte Filme |
| `log_dir` | string | `./logs` | — | Verzeichnis für Log-Dateien |
!!! example "Beispielkonfiguration" | Profil | Erkennungsmerkmale |
``` |--------|--------------------|
raw_dir = /mnt/nas/raw | `bluray` | UDF-Dateisystem, Laufwerk-Modell enthält „Blu-ray", Disc-Label wie BDMV |
movie_dir = /mnt/nas/movies | `dvd` | ISO9660/UDF, Laufwerk-Modell enthält „DVD", VIDEO_TS-Struktur |
log_dir = /var/log/ripster | `other` | Alles andere (CD, unbekannt) |
```
**Auflösungsreihenfolge für profil-spezifische Einstellungen:**
1. Profil-spezifischer Wert (`_bluray` / `_dvd`) wenn gesetzt, hat dieser Vorrang
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.
--- ---
## Kategorie: Tools (tools) ## Kategorie: Pfade
| Schlüssel | Typ | Pflicht | Beschreibung |
|-----------|-----|---------|-------------|
| `raw_dir` | string | ✅ | Verzeichnis für rohe MKV-Dateien (Fallback wenn kein Profil-Wert) |
| `raw_dir_bluray` | string | — | Raw-Verzeichnis für Blu-rays |
| `raw_dir_dvd` | string | — | Raw-Verzeichnis für DVDs |
| `raw_dir_other` | string | — | Raw-Verzeichnis für sonstige Medien |
| `raw_dir_owner` | string | — | Besitzer für Raw-Verzeichnis (`user:group`, Fallback) |
| `raw_dir_bluray_owner` | string | — | Besitzer für Raw-Verzeichnis (Blu-ray) |
| `raw_dir_dvd_owner` | string | — | Besitzer für Raw-Verzeichnis (DVD) |
| `raw_dir_other_owner` | string | — | Besitzer für Raw-Verzeichnis (Sonstiges) |
| `movie_dir` | string | ✅ | Ausgabeverzeichnis für Filme (Fallback) |
| `movie_dir_bluray` | string | — | Ausgabeverzeichnis für Blu-rays |
| `movie_dir_dvd` | string | — | Ausgabeverzeichnis für DVDs |
| `movie_dir_other` | string | — | Ausgabeverzeichnis für sonstige Medien |
| `movie_dir_owner` | string | — | Besitzer für Ausgabeverzeichnis (Fallback) |
| `movie_dir_bluray_owner` | string | — | Besitzer für Ausgabeverzeichnis (Blu-ray) |
| `movie_dir_dvd_owner` | string | — | Besitzer für Ausgabeverzeichnis (DVD) |
| `movie_dir_other_owner` | string | — | Besitzer für Ausgabeverzeichnis (Sonstiges) |
| `log_dir` | string | — | Verzeichnis für Log-Dateien (Standard: `./logs`) |
---
## Kategorie: Laufwerk
| Schlüssel | Typ | Standard | Beschreibung | | Schlüssel | Typ | Standard | Beschreibung |
|---------|-----|---------|-------------| |-----------|-----|---------|-------------|
| `drive_mode` | select | `auto` | `auto` = automatisch erkennen, `explicit` = festes Gerät |
| `drive_device` | string | `/dev/sr0` | Geräte-Pfad (nur bei `explicit`) |
| `disc_poll_interval_ms` | number | `4000` | Polling-Intervall in Millisekunden (100060000) |
| `makemkv_source_index` | number | `0` | Laufwerk-Index für MakeMKV (bei mehreren Laufwerken) |
---
## Kategorie: Tools (global)
| Schlüssel | Typ | Standard | Beschreibung |
|-----------|-----|---------|-------------|
| `makemkv_command` | string | `makemkvcon` | Befehl oder absoluter Pfad zu MakeMKV | | `makemkv_command` | string | `makemkvcon` | Befehl oder absoluter Pfad zu MakeMKV |
| `handbrake_command` | string | `HandBrakeCLI` | Befehl oder absoluter Pfad zu HandBrake | | `handbrake_command` | string | `HandBrakeCLI` | Befehl oder absoluter Pfad zu HandBrake |
| `mediainfo_command` | string | `mediainfo` | Befehl oder absoluter Pfad zu MediaInfo | | `mediainfo_command` | string | `mediainfo` | Befehl oder absoluter Pfad zu MediaInfo |
| `makemkv_min_length_minutes` | number | `15` | Mindest-Titellänge in Minuten (0999) |
| `pipeline_max_parallel_jobs` | number | `1` | Maximale Anzahl parallel laufender Jobs (112) |
| `handbrake_restart_delete_incomplete_output` | boolean | `true` | Unvollständige Ausgabedatei beim Encode-Neustart löschen |
!!! tip "Absolute Pfade verwenden" ### Kategorie: Tools Blu-ray
Falls die Tools nicht im `PATH` des Systems sind:
```
makemkv_command = /usr/local/bin/makemkvcon
handbrake_command = /usr/local/bin/HandBrakeCLI
mediainfo_command = /usr/bin/mediainfo
```
---
## Kategorie: Encoding (encoding)
| Schlüssel | Typ | Standard | Beschreibung | | Schlüssel | Typ | Standard | Beschreibung |
|---------|-----|---------|-------------| |-----------|-----|---------|-------------|
| `handbrake_preset` | string | `H.265 MKV 1080p30` | Name des HandBrake-Presets | | `makemkv_rip_mode_bluray` | select | `backup` | Rip-Modus: `mkv` oder `backup` |
| `handbrake_extra_args` | string | _(leer)_ | Zusätzliche HandBrake CLI-Argumente | | `makemkv_analyze_extra_args_bluray` | string | | Zusatz-CLI-Parameter für Analyse (Blu-ray) |
| `output_extension` | string | `mkv` | Dateiendung der Ausgabedatei | | `makemkv_rip_extra_args_bluray` | string | — | Zusatz-CLI-Parameter für Rip (Blu-ray) |
| `filename_template` | string | `{title} ({year})` | Template für den Dateinamen | | `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) |
### Verfügbare HandBrake-Presets ### Kategorie: Tools DVD
Eine vollständige Liste der verfügbaren Presets: | 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) |
```bash ### Globale Fallback-Einstellungen für Encode
HandBrakeCLI --preset-list
```
Häufig verwendete Presets: Diese Werte werden verwendet, wenn kein profil-spezifischer Wert konfiguriert ist:
| Preset | Beschreibung | | Schlüssel | Typ | Standard | Beschreibung |
|--------|-------------| |-----------|-----|---------|-------------|
| `H.265 MKV 1080p30` | H.265/HEVC, Full-HD, 30fps | | `handbrake_preset` | string | `H.265 MKV 1080p30` | Fallback HandBrake-Preset |
| `H.265 MKV 720p30` | H.265/HEVC, HD, 30fps | | `handbrake_extra_args` | string | — | Fallback Extra-Args |
| `H.264 MKV 1080p30` | H.264/AVC, Full-HD, 30fps | | `makemkv_rip_mode` | select | `mkv` | Fallback Rip-Modus |
| `HQ 1080p30 Surround` | Hohe Qualität, Full-HD mit Surround | | `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 |
### Dateiname-Template-Platzhalter ### Template-Platzhalter
| Platzhalter | Beispiel | | Platzhalter | Beispiel |
|------------|---------| |------------|---------|
| `{title}` | `Inception` | | `${title}` | `Inception` |
| `{year}` | `2010` | | `${year}` | `2010` |
| `{imdb_id}` | `tt1375666` | | `${imdbId}` | `tt1375666` |
| `{type}` | `movie` |
--- ---
## Kategorie: Laufwerk (drive) ## Kategorie: Metadaten
| Schlüssel | Typ | Standard | Optionen | Beschreibung |
|---------|-----|---------|---------|-------------|
| `drive_mode` | select | `auto` | `auto`, `explicit` | Laufwerk-Erkennungsmodus |
| `drive_device` | string | `/dev/sr0` | — | Geräte-Pfad (nur bei `explicit`) |
| `disc_poll_interval_ms` | number | `4000` | 100060000 | Polling-Intervall in Millisekunden |
**`drive_mode` Optionen:**
| Modus | Beschreibung |
|------|-------------|
| `auto` | Ripster erkennt das Laufwerk automatisch |
| `explicit` | Verwendet das in `drive_device` konfigurierte Gerät |
---
## Kategorie: MakeMKV (makemkv)
| Schlüssel | Typ | Standard | Min | Max | Beschreibung |
|---------|-----|---------|-----|-----|-------------|
| `makemkv_min_length_minutes` | number | `15` | `0` | `999` | Mindest-Titellänge in Minuten |
| `makemkv_backup_mode` | boolean | `false` | — | — | Backup-Modus statt MKV-Modus |
**`makemkv_min_length_minutes`:** Titel kürzer als dieser Wert werden von MakeMKV ignoriert. Verhindert das Rippen von Menü-Schleifen und kurzen Extra-Clips.
**`makemkv_backup_mode`:** Im Backup-Modus erstellt MakeMKV eine vollständige Disc-Kopie mit Menüs. Im Standard-Modus werden direkt MKV-Dateien erstellt.
---
## Kategorie: OMDb (omdb)
| Schlüssel | Typ | Standard | Pflicht | Beschreibung |
|---------|-----|---------|---------|-------------|
| `omdb_api_key` | string | — | ✅ | API-Key von [omdbapi.com](https://www.omdbapi.com/) |
| `omdb_default_type` | select | `movie` | — | Standard-Suchtyp: `movie` oder `series` |
---
## Kategorie: Benachrichtigungen (notifications)
| Schlüssel | Typ | Standard | Beschreibung | | Schlüssel | Typ | Standard | Beschreibung |
|---------|-----|---------|-------------| |-----------|-----|---------|-------------|
| `pushover_user_key` | string | — | PushOver User-Key | | `omdb_api_key` | string | — | API-Key von [omdbapi.com](https://www.omdbapi.com/) |
| `pushover_api_token` | string | — | PushOver API-Token | | `omdb_default_type` | select | `movie` | Vorauswahl für OMDb-Suche: `movie`, `series`, `episode` |
Beide Felder müssen konfiguriert sein, um PushOver-Benachrichtigungen zu aktivieren. Die Verbindung kann mit dem **Test-Button** in den Einstellungen geprüft werden. ---
## 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
| Schlüssel | Typ | Standard | Beschreibung |
|-----------|-----|---------|-------------|
| `hardware_monitoring_enabled` | boolean | `false` | Hardware-Monitoring aktivieren (CPU, RAM, Temp.) |
| `hardware_monitoring_interval_ms` | number | `5000` | Monitoring-Polling-Intervall (ms) |
--- ---
## Standard-Einstellungen zurücksetzen ## Standard-Einstellungen zurücksetzen
Über die Datenbank können Einstellungen auf Standardwerte zurückgesetzt werden: Einen einzelnen Wert über die Datenbank zurücksetzen:
```bash ```bash
sqlite3 backend/data/ripster.db \ sqlite3 backend/data/ripster.db \
"DELETE FROM settings_values WHERE key = 'handbrake_preset';" "DELETE FROM settings_values WHERE key = 'handbrake_preset_bluray';"
``` ```
Beim nächsten Laden der Einstellungen wird der Standardwert verwendet. Beim nächsten Laden wird der Standardwert aus `settings_schema.default_value` verwendet.

View File

@@ -266,6 +266,29 @@ export const api = {
return request(`/history/${jobId}${suffix}`); return request(`/history/${jobId}${suffix}`);
}, },
// ── User Presets ───────────────────────────────────────────────────────────
getUserPresets(mediaType = null) {
const suffix = mediaType ? `?media_type=${encodeURIComponent(mediaType)}` : '';
return request(`/settings/user-presets${suffix}`);
},
createUserPreset(payload = {}) {
return request('/settings/user-presets', {
method: 'POST',
body: JSON.stringify(payload)
});
},
updateUserPreset(id, payload = {}) {
return request(`/settings/user-presets/${encodeURIComponent(id)}`, {
method: 'PUT',
body: JSON.stringify(payload)
});
},
deleteUserPreset(id) {
return request(`/settings/user-presets/${encodeURIComponent(id)}`, {
method: 'DELETE'
});
},
// ── Cron Jobs ────────────────────────────────────────────────────────────── // ── Cron Jobs ──────────────────────────────────────────────────────────────
getCronJobs() { getCronJobs() {
return request('/crons'); return request('/crons');

View File

@@ -174,7 +174,8 @@ function buildHandBrakeCommandPreview({
title, title,
selectedAudioTrackIds, selectedAudioTrackIds,
selectedSubtitleTrackIds, selectedSubtitleTrackIds,
commandOutputPath = null commandOutputPath = null,
presetOverride = null
}) { }) {
const inputPath = String(title?.filePath || review?.encodeInputPath || '').trim(); const inputPath = String(title?.filePath || review?.encodeInputPath || '').trim();
const handBrakeCmd = String( const handBrakeCmd = String(
@@ -182,8 +183,12 @@ function buildHandBrakeCommandPreview({
|| review?.selectors?.handBrakeCommand || review?.selectors?.handBrakeCommand
|| 'HandBrakeCLI' || 'HandBrakeCLI'
).trim() || 'HandBrakeCLI'; ).trim() || 'HandBrakeCLI';
const preset = String(review?.selectors?.preset || '').trim(); const preset = presetOverride !== null
const extraArgs = String(review?.selectors?.extraArgs || '').trim(); ? String(presetOverride.handbrakePreset || '').trim()
: String(review?.selectors?.preset || '').trim();
const extraArgs = presetOverride !== null
? String(presetOverride.extraArgs || '').trim()
: String(review?.selectors?.extraArgs || '').trim();
const rawMappedTitleId = Number(review?.handBrakeTitleId); const rawMappedTitleId = Number(review?.handBrakeTitleId);
const mappedTitleId = Number.isFinite(rawMappedTitleId) && rawMappedTitleId > 0 const mappedTitleId = Number.isFinite(rawMappedTitleId) && rawMappedTitleId > 0
? Math.trunc(rawMappedTitleId) ? Math.trunc(rawMappedTitleId)
@@ -697,7 +702,10 @@ export default function MediaInfoReviewPanel({
onAddPostEncodeItem = null, onAddPostEncodeItem = null,
onChangePostEncodeItem = null, onChangePostEncodeItem = null,
onRemovePostEncodeItem = null, onRemovePostEncodeItem = null,
onReorderPostEncodeItem = null onReorderPostEncodeItem = null,
userPresets = [],
selectedUserPresetId = null,
onUserPresetChange = null
}) { }) {
if (!review) { if (!review) {
return <p>Keine Mediainfo-Daten vorhanden.</p>; return <p>Keine Mediainfo-Daten vorhanden.</p>;
@@ -711,6 +719,18 @@ export default function MediaInfoReviewPanel({
const playlistRecommendation = review.playlistRecommendation || null; const playlistRecommendation = review.playlistRecommendation || null;
const rawPreset = String(review.selectors?.preset || '').trim(); const rawPreset = String(review.selectors?.preset || '').trim();
const presetLabel = String(presetDisplayValue || rawPreset).trim() || '(kein Preset)'; const presetLabel = String(presetDisplayValue || rawPreset).trim() || '(kein Preset)';
// User preset resolution
const normalizedUserPresets = Array.isArray(userPresets) ? userPresets : [];
const selectedUserPreset = selectedUserPresetId
? normalizedUserPresets.find((p) => Number(p.id) === Number(selectedUserPresetId)) || null
: null;
const effectivePresetOverride = selectedUserPreset
? { handbrakePreset: selectedUserPreset.handbrakePreset || '', extraArgs: selectedUserPreset.extraArgs || '' }
: null;
const hasUserPresets = normalizedUserPresets.length > 0;
const allowUserPresetSelection = hasUserPresets && typeof onUserPresetChange === 'function' && allowEncodeItemSelection;
const scriptCatalog = (Array.isArray(availableScripts) ? availableScripts : []) const scriptCatalog = (Array.isArray(availableScripts) ? availableScripts : [])
.map((item) => ({ .map((item) => ({
id: normalizeScriptId(item?.id), id: normalizeScriptId(item?.id),
@@ -740,10 +760,56 @@ export default function MediaInfoReviewPanel({
return ( return (
<div className="media-review-wrap"> <div className="media-review-wrap">
{allowUserPresetSelection && (
<div className="user-preset-selector" style={{ marginBottom: '0.75rem', padding: '0.75rem', border: '1px solid var(--surface-border, #e0e0e0)', borderRadius: '6px', background: 'var(--surface-ground, #f8f8f8)' }}>
<label style={{ display: 'block', marginBottom: '0.4rem', fontWeight: 600 }}>
Encode-Preset auswählen
</label>
<Dropdown
value={selectedUserPresetId ? Number(selectedUserPresetId) : null}
options={[
{ label: '(Einstellungen-Fallback)', value: null },
...normalizedUserPresets.map((p) => ({
label: `${p.name}${p.mediaType !== 'all' ? ` [${p.mediaType === 'bluray' ? 'Blu-ray' : p.mediaType === 'dvd' ? 'DVD' : 'Sonstiges'}]` : ''}`,
value: Number(p.id)
}))
]}
onChange={(e) => onUserPresetChange(e.value)}
placeholder="(Einstellungen-Fallback)"
style={{ width: '100%' }}
/>
{selectedUserPreset && (
<div style={{ marginTop: '0.4rem', fontSize: '0.8rem', opacity: 0.8 }}>
{selectedUserPreset.handbrakePreset
? <span><strong>-Z</strong> {selectedUserPreset.handbrakePreset}</span>
: <span style={{ opacity: 0.6 }}>(kein Preset-Name)</span>}
{selectedUserPreset.extraArgs && (
<span style={{ marginLeft: '1rem' }}><strong>Args:</strong> {selectedUserPreset.extraArgs}</span>
)}
{selectedUserPreset.description && (
<span style={{ marginLeft: '1rem', opacity: 0.7 }}>{selectedUserPreset.description}</span>
)}
</div>
)}
</div>
)}
<div className="media-review-meta"> <div className="media-review-meta">
<div><strong>Preset:</strong> {presetLabel}</div> <div>
<div><strong>Extra Args:</strong> {review.selectors?.extraArgs || '(keine)'}</div> <strong>Preset:</strong>{' '}
<div><strong>Preset-Profil:</strong> {review.selectors?.presetProfileSource || '-'}</div> {effectivePresetOverride
? (effectivePresetOverride.handbrakePreset || '(kein Preset)')
: presetLabel}
{effectivePresetOverride && <span style={{ marginLeft: '0.5rem', fontSize: '0.75rem', opacity: 0.7 }}>(User-Preset: {selectedUserPreset?.name})</span>}
</div>
<div>
<strong>Extra Args:</strong>{' '}
{effectivePresetOverride
? (effectivePresetOverride.extraArgs || '(keine)')
: (review.selectors?.extraArgs || '(keine)')}
{effectivePresetOverride && !selectedUserPreset?.extraArgs && <span style={{ marginLeft: '0.5rem', fontSize: '0.75rem', opacity: 0.7 }}>(aus User-Preset)</span>}
</div>
<div><strong>Preset-Profil:</strong> {effectivePresetOverride ? 'user-preset' : (review.selectors?.presetProfileSource || '-')}</div>
<div><strong>MIN_LENGTH_MINUTES:</strong> {review.minLengthMinutes}</div> <div><strong>MIN_LENGTH_MINUTES:</strong> {review.minLengthMinutes}</div>
<div><strong>Encode Input:</strong> {encodeInputTitle?.fileName || '-'}</div> <div><strong>Encode Input:</strong> {encodeInputTitle?.fileName || '-'}</div>
<div><strong>Audio Auswahl:</strong> {review.selectors?.audio?.mode || '-'}</div> <div><strong>Audio Auswahl:</strong> {review.selectors?.audio?.mode || '-'}</div>
@@ -1090,7 +1156,8 @@ export default function MediaInfoReviewPanel({
title, title,
selectedAudioTrackIds, selectedAudioTrackIds,
selectedSubtitleTrackIds, selectedSubtitleTrackIds,
commandOutputPath commandOutputPath,
presetOverride: effectivePresetOverride
}); });
return ( return (
<div className="handbrake-command-preview"> <div className="handbrake-command-preview">

View File

@@ -226,6 +226,7 @@ export default function PipelineStatusCard({
const reviewConfirmed = Boolean(pipeline?.context?.reviewConfirmed || mediaInfoReview?.reviewConfirmed); const reviewConfirmed = Boolean(pipeline?.context?.reviewConfirmed || mediaInfoReview?.reviewConfirmed);
const reviewMode = String(mediaInfoReview?.mode || '').trim().toLowerCase(); const reviewMode = String(mediaInfoReview?.mode || '').trim().toLowerCase();
const isPreRipReview = reviewMode === 'pre_rip' || Boolean(mediaInfoReview?.preRip); const isPreRipReview = reviewMode === 'pre_rip' || Boolean(mediaInfoReview?.preRip);
const jobMediaProfile = String(pipeline?.context?.mediaProfile || '').trim().toLowerCase() || null;
const [selectedEncodeTitleId, setSelectedEncodeTitleId] = useState(null); const [selectedEncodeTitleId, setSelectedEncodeTitleId] = useState(null);
const [selectedPlaylistId, setSelectedPlaylistId] = useState(null); const [selectedPlaylistId, setSelectedPlaylistId] = useState(null);
const [trackSelectionByTitle, setTrackSelectionByTitle] = useState({}); const [trackSelectionByTitle, setTrackSelectionByTitle] = useState({});
@@ -236,16 +237,19 @@ export default function PipelineStatusCard({
// Unified ordered lists: [{type: 'script'|'chain', id: number}] // Unified ordered lists: [{type: 'script'|'chain', id: number}]
const [preEncodeItems, setPreEncodeItems] = useState([]); const [preEncodeItems, setPreEncodeItems] = useState([]);
const [postEncodeItems, setPostEncodeItems] = useState([]); const [postEncodeItems, setPostEncodeItems] = useState([]);
const [userPresets, setUserPresets] = useState([]);
const [selectedUserPresetId, setSelectedUserPresetId] = useState(null);
useEffect(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
const loadSettings = async () => { const loadSettings = async () => {
try { try {
const [settingsResponse, presetsResponse, scriptsResponse, chainsResponse] = await Promise.allSettled([ const [settingsResponse, presetsResponse, scriptsResponse, chainsResponse, userPresetsResponse] = await Promise.allSettled([
api.getSettings(), api.getSettings(),
api.getHandBrakePresets(), api.getHandBrakePresets(),
api.getScripts(), api.getScripts(),
api.getScriptChains() api.getScriptChains(),
api.getUserPresets()
]); ]);
if (!cancelled) { if (!cancelled) {
const categories = settingsResponse.status === 'fulfilled' const categories = settingsResponse.status === 'fulfilled'
@@ -269,6 +273,10 @@ export default function PipelineStatusCard({
? (Array.isArray(chainsResponse.value?.chains) ? chainsResponse.value.chains : []) ? (Array.isArray(chainsResponse.value?.chains) ? chainsResponse.value.chains : [])
: []; : [];
setChainCatalog(chains.map((item) => ({ id: item?.id, name: item?.name }))); setChainCatalog(chains.map((item) => ({ id: item?.id, name: item?.name })));
const allUserPresets = userPresetsResponse.status === 'fulfilled'
? (Array.isArray(userPresetsResponse.value?.presets) ? userPresetsResponse.value.presets : [])
: [];
setUserPresets(allUserPresets);
} }
} catch (_error) { } catch (_error) {
if (!cancelled) { if (!cancelled) {
@@ -276,6 +284,7 @@ export default function PipelineStatusCard({
setPresetDisplayMap({}); setPresetDisplayMap({});
setScriptCatalog([]); setScriptCatalog([]);
setChainCatalog([]); setChainCatalog([]);
setUserPresets([]);
} }
} }
}; };
@@ -298,6 +307,7 @@ export default function PipelineStatusCard({
...normalizeScriptIdList(mediaInfoReview?.postEncodeScriptIds || []).map((id) => ({ type: 'script', id })), ...normalizeScriptIdList(mediaInfoReview?.postEncodeScriptIds || []).map((id) => ({ type: 'script', id })),
...normChain(mediaInfoReview?.postEncodeChainIds).map((id) => ({ type: 'chain', id })) ...normChain(mediaInfoReview?.postEncodeChainIds).map((id) => ({ type: 'chain', id }))
]); ]);
setSelectedUserPresetId(null);
}, [mediaInfoReview?.encodeInputTitleId, mediaInfoReview?.generatedAt, retryJobId]); }, [mediaInfoReview?.encodeInputTitleId, mediaInfoReview?.generatedAt, retryJobId]);
useEffect(() => { useEffect(() => {
@@ -323,6 +333,14 @@ export default function PipelineStatusCard({
const reviewPlaylistDecisionRequired = Boolean(mediaInfoReview?.playlistDecisionRequired); const reviewPlaylistDecisionRequired = Boolean(mediaInfoReview?.playlistDecisionRequired);
const hasSelectedEncodeTitle = Boolean(normalizeTitleId(selectedEncodeTitleId)); const hasSelectedEncodeTitle = Boolean(normalizeTitleId(selectedEncodeTitleId));
const canConfirmReview = !reviewPlaylistDecisionRequired || hasSelectedEncodeTitle; const canConfirmReview = !reviewPlaylistDecisionRequired || hasSelectedEncodeTitle;
// Filter user presets by job media profile ('all' presets always shown)
const filteredUserPresets = (Array.isArray(userPresets) ? userPresets : []).filter((p) => {
if (!jobMediaProfile) {
return true;
}
return p.mediaType === 'all' || p.mediaType === jobMediaProfile;
});
const canStartReadyJob = isPreRipReview const canStartReadyJob = isPreRipReview
? Boolean(retryJobId) ? Boolean(retryJobId)
: Boolean(retryJobId && encodeInputPath); : Boolean(retryJobId && encodeInputPath);
@@ -575,7 +593,8 @@ export default function PipelineStatusCard({
selectedPostEncodeScriptIds: selectedPostScriptIds, selectedPostEncodeScriptIds: selectedPostScriptIds,
selectedPreEncodeScriptIds: selectedPreScriptIds, selectedPreEncodeScriptIds: selectedPreScriptIds,
selectedPostEncodeChainIds: selectedPostChainIds, selectedPostEncodeChainIds: selectedPostChainIds,
selectedPreEncodeChainIds: selectedPreChainIds selectedPreEncodeChainIds: selectedPreChainIds,
selectedUserPresetId: selectedUserPresetId || null
}); });
}} }}
loading={busy} loading={busy}
@@ -786,6 +805,9 @@ export default function PipelineStatusCard({
availableChains={chainCatalog} availableChains={chainCatalog}
preEncodeItems={preEncodeItems} preEncodeItems={preEncodeItems}
postEncodeItems={postEncodeItems} postEncodeItems={postEncodeItems}
userPresets={filteredUserPresets}
selectedUserPresetId={selectedUserPresetId}
onUserPresetChange={(presetId) => setSelectedUserPresetId(presetId)}
allowEncodeItemSelection={state === 'READY_TO_ENCODE' && !reviewConfirmed && !queueLocked} allowEncodeItemSelection={state === 'READY_TO_ENCODE' && !reviewConfirmed && !queueLocked}
onAddPreEncodeItem={(itemType) => { onAddPreEncodeItem={(itemType) => {
setPreEncodeItems((prev) => { setPreEncodeItems((prev) => {

View File

@@ -792,6 +792,7 @@ export default function DashboardPage({
selectedPreEncodeScriptIds: startOptions.selectedPreEncodeScriptIds ?? [], selectedPreEncodeScriptIds: startOptions.selectedPreEncodeScriptIds ?? [],
selectedPostEncodeChainIds: startOptions.selectedPostEncodeChainIds ?? [], selectedPostEncodeChainIds: startOptions.selectedPostEncodeChainIds ?? [],
selectedPreEncodeChainIds: startOptions.selectedPreEncodeChainIds ?? [], selectedPreEncodeChainIds: startOptions.selectedPreEncodeChainIds ?? [],
selectedUserPresetId: startOptions.selectedUserPresetId ?? null,
skipPipelineStateUpdate: true skipPipelineStateUpdate: true
}); });
} }

View File

@@ -156,6 +156,21 @@ export default function SettingsPage() {
const [chainEditorErrors, setChainEditorErrors] = useState({}); const [chainEditorErrors, setChainEditorErrors] = useState({});
const [chainDragSource, setChainDragSource] = useState(null); const [chainDragSource, setChainDragSource] = useState(null);
// User presets state
const [userPresets, setUserPresets] = useState([]);
const [userPresetsLoading, setUserPresetsLoading] = useState(false);
const [userPresetSaving, setUserPresetSaving] = useState(false);
const [userPresetEditor, setUserPresetEditor] = useState({
open: false,
id: null,
name: '',
mediaType: 'all',
handbrakePreset: '',
extraArgs: '',
description: ''
});
const [userPresetErrors, setUserPresetErrors] = useState({});
const toastRef = useRef(null); const toastRef = useRef(null);
const loadScripts = async ({ silent = false } = {}) => { const loadScripts = async ({ silent = false } = {}) => {
@@ -195,6 +210,91 @@ export default function SettingsPage() {
} }
}; };
const loadUserPresets = async ({ silent = false } = {}) => {
if (!silent) {
setUserPresetsLoading(true);
}
try {
const response = await api.getUserPresets();
setUserPresets(Array.isArray(response?.presets) ? response.presets : []);
} catch (error) {
if (!silent) {
toastRef.current?.show({ severity: 'error', summary: 'User-Presets', detail: error.message });
}
} finally {
if (!silent) {
setUserPresetsLoading(false);
}
}
};
const openNewUserPreset = () => {
setUserPresetEditor({ open: true, id: null, name: '', mediaType: 'all', handbrakePreset: '', extraArgs: '', description: '' });
setUserPresetErrors({});
};
const openEditUserPreset = (preset) => {
setUserPresetEditor({
open: true,
id: preset.id,
name: preset.name || '',
mediaType: preset.mediaType || 'all',
handbrakePreset: preset.handbrakePreset || '',
extraArgs: preset.extraArgs || '',
description: preset.description || ''
});
setUserPresetErrors({});
};
const closeUserPresetEditor = () => {
setUserPresetEditor((prev) => ({ ...prev, open: false }));
setUserPresetErrors({});
};
const handleSaveUserPreset = async () => {
const errors = {};
if (!userPresetEditor.name.trim()) {
errors.name = 'Name ist erforderlich.';
}
if (Object.keys(errors).length > 0) {
setUserPresetErrors(errors);
return;
}
setUserPresetSaving(true);
try {
const payload = {
name: userPresetEditor.name.trim(),
mediaType: userPresetEditor.mediaType,
handbrakePreset: userPresetEditor.handbrakePreset.trim(),
extraArgs: userPresetEditor.extraArgs.trim(),
description: userPresetEditor.description.trim()
};
if (userPresetEditor.id) {
await api.updateUserPreset(userPresetEditor.id, payload);
toastRef.current?.show({ severity: 'success', summary: 'Preset', detail: 'Preset aktualisiert.' });
} else {
await api.createUserPreset(payload);
toastRef.current?.show({ severity: 'success', summary: 'Preset', detail: 'Preset erstellt.' });
}
closeUserPresetEditor();
await loadUserPresets({ silent: true });
} catch (error) {
toastRef.current?.show({ severity: 'error', summary: 'Preset speichern', detail: error.message });
} finally {
setUserPresetSaving(false);
}
};
const handleDeleteUserPreset = async (presetId) => {
try {
await api.deleteUserPreset(presetId);
toastRef.current?.show({ severity: 'success', summary: 'Preset', detail: 'Preset gelöscht.' });
await loadUserPresets({ silent: true });
} catch (error) {
toastRef.current?.show({ severity: 'error', summary: 'Preset löschen', detail: error.message });
}
};
const load = async () => { const load = async () => {
setLoading(true); setLoading(true);
try { try {
@@ -250,6 +350,7 @@ export default function SettingsPage() {
useEffect(() => { useEffect(() => {
load(); load();
loadUserPresets();
}, []); }, []);
const dirtyKeys = useMemo(() => { const dirtyKeys = useMemo(() => {
@@ -1379,6 +1480,173 @@ export default function SettingsPage() {
</Dialog> </Dialog>
</TabPanel> </TabPanel>
<TabPanel header="Encode-Presets">
<div className="actions-row">
<Button
label="Neues Preset"
icon="pi pi-plus"
onClick={openNewUserPreset}
severity="success"
outlined
disabled={userPresetSaving}
/>
<Button
label="Presets neu laden"
icon="pi pi-refresh"
severity="secondary"
onClick={() => loadUserPresets()}
loading={userPresetsLoading}
disabled={userPresetSaving}
/>
</div>
<small>
Encode-Presets fassen ein HandBrake-Preset und zusätzliche CLI-Argumente zusammen.
Sie sind medienbezogen (Blu-ray, DVD, Sonstiges oder Universell) und können vor dem Encode
in der Mediainfo-Prüfung ausgewählt werden. Kein Preset gewählt = Fallback aus Einstellungen.
</small>
{userPresetsLoading ? (
<p style={{ marginTop: '1rem' }}>Lade Presets ...</p>
) : userPresets.length === 0 ? (
<p style={{ marginTop: '1rem' }}>Keine Presets vorhanden. Lege ein neues Preset an.</p>
) : (
<div className="script-list" style={{ marginTop: '1rem' }}>
{userPresets.map((preset) => (
<div key={preset.id} className="script-list-item">
<div className="script-list-main">
<div className="script-title-line">
<strong>#{preset.id} {preset.name}</strong>
<span style={{ marginLeft: '0.5rem', fontSize: '0.8rem', opacity: 0.7 }}>
{preset.mediaType === 'bluray' ? 'Blu-ray'
: preset.mediaType === 'dvd' ? 'DVD'
: preset.mediaType === 'other' ? 'Sonstiges'
: 'Universell'}
</span>
</div>
{preset.description && <small style={{ display: 'block', marginTop: '0.2rem', opacity: 0.8 }}>{preset.description}</small>}
<div style={{ marginTop: '0.3rem', fontFamily: 'monospace', fontSize: '0.8rem' }}>
{preset.handbrakePreset
? <span><span style={{ opacity: 0.6 }}>Preset:</span> {preset.handbrakePreset}</span>
: <span style={{ opacity: 0.5 }}>(kein Preset-Name)</span>}
{preset.extraArgs && (
<span style={{ marginLeft: '1rem' }}><span style={{ opacity: 0.6 }}>Args:</span> {preset.extraArgs}</span>
)}
</div>
</div>
<div className="script-list-actions">
<Button
icon="pi pi-pencil"
severity="secondary"
outlined
rounded
title="Bearbeiten"
onClick={() => openEditUserPreset(preset)}
/>
<Button
icon="pi pi-trash"
severity="danger"
outlined
rounded
title="Löschen"
onClick={() => handleDeleteUserPreset(preset.id)}
/>
</div>
</div>
))}
</div>
)}
<Dialog
header={userPresetEditor.id ? 'Preset bearbeiten' : 'Neues Preset'}
visible={userPresetEditor.open}
style={{ width: '520px' }}
onHide={closeUserPresetEditor}
modal
>
<div className="script-editor-fields" style={{ display: 'flex', flexDirection: 'column', gap: '0.8rem' }}>
<div>
<label htmlFor="preset-name" style={{ display: 'block', marginBottom: '0.3rem' }}>Name *</label>
<InputText
id="preset-name"
value={userPresetEditor.name}
onChange={(e) => setUserPresetEditor((prev) => ({ ...prev, name: e.target.value }))}
placeholder="z.B. Blu-ray HQ"
style={{ width: '100%' }}
/>
{userPresetErrors.name && <small className="error-text">{userPresetErrors.name}</small>}
</div>
<div>
<label htmlFor="preset-media-type" style={{ display: 'block', marginBottom: '0.3rem' }}>Medientyp</label>
<select
id="preset-media-type"
value={userPresetEditor.mediaType}
onChange={(e) => setUserPresetEditor((prev) => ({ ...prev, mediaType: e.target.value }))}
style={{ width: '100%', padding: '0.5rem', borderRadius: '4px', border: '1px solid var(--surface-border, #ccc)', background: 'var(--surface-overlay, #fff)', color: 'var(--text-color, #000)' }}
>
<option value="all">Universell (alle Medien)</option>
<option value="bluray">Blu-ray</option>
<option value="dvd">DVD</option>
<option value="other">Sonstiges</option>
</select>
</div>
<div>
<label htmlFor="preset-hb-preset" style={{ display: 'block', marginBottom: '0.3rem' }}>HandBrake Preset (-Z)</label>
<InputText
id="preset-hb-preset"
value={userPresetEditor.handbrakePreset}
onChange={(e) => setUserPresetEditor((prev) => ({ ...prev, handbrakePreset: e.target.value }))}
placeholder="z.B. H.264 MKV 1080p30 (leer = kein Preset)"
style={{ width: '100%' }}
/>
</div>
<div>
<label htmlFor="preset-extra-args" style={{ display: 'block', marginBottom: '0.3rem' }}>Extra Args</label>
<InputText
id="preset-extra-args"
value={userPresetEditor.extraArgs}
onChange={(e) => setUserPresetEditor((prev) => ({ ...prev, extraArgs: e.target.value }))}
placeholder="z.B. -q 22 --encoder x264"
style={{ width: '100%' }}
/>
</div>
<div>
<label htmlFor="preset-description" style={{ display: 'block', marginBottom: '0.3rem' }}>Beschreibung (optional)</label>
<InputTextarea
id="preset-description"
value={userPresetEditor.description}
onChange={(e) => setUserPresetEditor((prev) => ({ ...prev, description: e.target.value }))}
rows={3}
autoResize
placeholder="Kurzbeschreibung für dieses Preset"
style={{ width: '100%' }}
/>
</div>
<div className="actions-row" style={{ marginTop: '0.5rem' }}>
<Button
label={userPresetEditor.id ? 'Aktualisieren' : 'Erstellen'}
icon="pi pi-save"
onClick={handleSaveUserPreset}
loading={userPresetSaving}
/>
<Button
label="Abbrechen"
icon="pi pi-times"
severity="secondary"
outlined
onClick={closeUserPresetEditor}
disabled={userPresetSaving}
/>
</div>
</div>
</Dialog>
</TabPanel>
<TabPanel header="Cronjobs"> <TabPanel header="Cronjobs">
<CronJobsTab /> <CronJobsTab />
</TabPanel> </TabPanel>

View File

@@ -17,6 +17,9 @@
# --no-handbrake HandBrake-Installation überspringen # --no-handbrake HandBrake-Installation überspringen
# --build-handbrake HandBrake aus Quellcode mit NVDEC-Unterstützung bauen # --build-handbrake HandBrake aus Quellcode mit NVDEC-Unterstützung bauen
# --handbrake-version HandBrake-Version für Source-Build (Standard: 1.9.0) # --handbrake-version HandBrake-Version für Source-Build (Standard: 1.9.0)
# --handbrake-update-policy <keep|prompt|build>
# Bei NVDEC-Self-Build: bei neuer Git-Release behalten,
# nachfragen oder direkt neu bauen (Standard: keep)
# --no-nginx Nginx-Einrichtung überspringen (Frontend läuft dann auf Port 5173) # --no-nginx Nginx-Einrichtung überspringen (Frontend läuft dann auf Port 5173)
# --reinstall Vorhandene Installation ersetzen (Daten bleiben erhalten) # --reinstall Vorhandene Installation ersetzen (Daten bleiben erhalten)
# -h, --help Diese Hilfe anzeigen # -h, --help Diese Hilfe anzeigen
@@ -44,10 +47,14 @@ FRONTEND_HOST="" # wird automatisch ermittelt, wenn leer
SKIP_MAKEMKV=false SKIP_MAKEMKV=false
SKIP_HANDBRAKE=false SKIP_HANDBRAKE=false
BUILD_HANDBRAKE_NVDEC=false BUILD_HANDBRAKE_NVDEC=false
HANDBRAKE_MODE_SELECTED=false
HANDBRAKE_VERSION="1.9.0" HANDBRAKE_VERSION="1.9.0"
HANDBRAKE_UPDATE_POLICY="keep"
HANDBRAKE_UPDATE_POLICY_SELECTED=false
SKIP_NGINX=false SKIP_NGINX=false
REINSTALL=false REINSTALL=false
SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
HANDBRAKE_SELFBUILD_MARKER="/usr/local/share/ripster/handbrake-selfbuild.env"
# --- Argumente parsen --------------------------------------------------------- # --- Argumente parsen ---------------------------------------------------------
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
@@ -58,8 +65,14 @@ while [[ $# -gt 0 ]]; do
--host) FRONTEND_HOST="$2"; shift 2 ;; --host) FRONTEND_HOST="$2"; shift 2 ;;
--no-makemkv) SKIP_MAKEMKV=true; shift ;; --no-makemkv) SKIP_MAKEMKV=true; shift ;;
--no-handbrake) SKIP_HANDBRAKE=true; shift ;; --no-handbrake) SKIP_HANDBRAKE=true; shift ;;
--build-handbrake) BUILD_HANDBRAKE_NVDEC=true; shift ;; --build-handbrake) BUILD_HANDBRAKE_NVDEC=true; HANDBRAKE_MODE_SELECTED=true; shift ;;
--handbrake-version) HANDBRAKE_VERSION="$2"; shift 2 ;; --handbrake-version) HANDBRAKE_VERSION="$2"; shift 2 ;;
--handbrake-update-policy)
case "$2" in
keep|prompt|build) HANDBRAKE_UPDATE_POLICY="$2"; HANDBRAKE_UPDATE_POLICY_SELECTED=true ;;
*) fatal "Ungültige --handbrake-update-policy: $2 (erlaubt: keep|prompt|build)" ;;
esac
shift 2 ;;
--no-nginx) SKIP_NGINX=true; shift ;; --no-nginx) SKIP_NGINX=true; shift ;;
--reinstall) REINSTALL=true; shift ;; --reinstall) REINSTALL=true; shift ;;
-h|--help) -h|--help)
@@ -183,46 +196,99 @@ install_makemkv() {
warn "Beta-Key: https://www.makemkv.com/forum/viewtopic.php?t=1053" warn "Beta-Key: https://www.makemkv.com/forum/viewtopic.php?t=1053"
} }
remove_all_handbrake() { handbrake_has_nvdec() {
info "Entferne alle vorhandenen HandBrake-Installationen..." command_exists HandBrakeCLI || return 1
HandBrakeCLI --help 2>&1 | grep -qi "nvdec"
}
handbrake_installed_version() {
command_exists HandBrakeCLI || return 1
HandBrakeCLI --version 2>/dev/null | grep -oE '[0-9]+(\.[0-9]+){1,3}' | head -1
}
handbrake_latest_git_version() {
local latest=""
latest=$(curl -fsSL --max-time 10 "https://api.github.com/repos/HandBrake/HandBrake/releases/latest" 2>/dev/null \
| grep -oE '"tag_name"[[:space:]]*:[[:space:]]*"[^"]+"' \
| head -1 \
| sed -E 's/.*"([^"]+)".*/\1/' \
| sed 's/^v//')
[[ -n "$latest" ]] || return 1
[[ "$latest" =~ ^[0-9]+(\.[0-9]+){1,3}$ ]] || return 1
printf '%s\n' "$latest"
}
handbrake_is_self_built() {
local hb_path="${1:-$(command -v HandBrakeCLI 2>/dev/null || true)}"
local resolved_path=""
[[ -n "$hb_path" ]] || return 1
[[ "$hb_path" == "/usr/local/bin/HandBrakeCLI" ]] || return 1
[[ -f "$HANDBRAKE_SELFBUILD_MARKER" ]] && return 0
resolved_path=$(readlink -f "$hb_path" 2>/dev/null || true)
dpkg -S "$hb_path" >/dev/null 2>&1 && return 1
[[ -n "$resolved_path" ]] && dpkg -S "$resolved_path" >/dev/null 2>&1 && return 1
return 0
}
remove_non_selfbuilt_handbrake() {
info "Entferne nicht-selbst-gebaute HandBrake-Installationen..."
apt-get remove -y handbrake-cli handbrake 2>/dev/null || true apt-get remove -y handbrake-cli handbrake 2>/dev/null || true
snap remove handbrake-cli 2>/dev/null || true snap remove handbrake-cli 2>/dev/null || true
rm -f /usr/bin/HandBrakeCLI \ rm -f /usr/bin/HandBrakeCLI \
/usr/local/bin/HandBrakeCLI \
/snap/bin/handbrake-cli \ /snap/bin/handbrake-cli \
/snap/bin/HandBrakeCLI /snap/bin/HandBrakeCLI
while true; do
local found if [[ -e /usr/local/bin/HandBrakeCLI ]] && ! handbrake_is_self_built "/usr/local/bin/HandBrakeCLI"; then
found=$(command -v HandBrakeCLI 2>/dev/null || true) warn "Entferne fremdes /usr/local/bin/HandBrakeCLI"
[[ -z "$found" ]] && break rm -f /usr/local/bin/HandBrakeCLI
warn "Entferne: $found" fi
rm -f "$found"
done hash -r 2>/dev/null || true
ok "Bereinigung abgeschlossen"
}
remove_selfbuilt_handbrake() {
if [[ -e /usr/local/bin/HandBrakeCLI ]] && handbrake_is_self_built "/usr/local/bin/HandBrakeCLI"; then
warn "Entferne selbst gebautes /usr/local/bin/HandBrakeCLI"
rm -f /usr/local/bin/HandBrakeCLI
fi
rm -f "$HANDBRAKE_SELFBUILD_MARKER"
hash -r 2>/dev/null || true hash -r 2>/dev/null || true
ok "Alte HandBrake-Installation(en) entfernt"
} }
build_handbrake_nvdec() { build_handbrake_nvdec() {
header "HandBrake ${HANDBRAKE_VERSION} mit NVDEC aus Quellcode bauen" header "HandBrake ${HANDBRAKE_VERSION} mit NVDEC aus Quellcode bauen"
local tmp_dir local cache_dir="/var/cache/ripster/handbrake"
tmp_dir=$(mktemp -d)
local src_url="https://github.com/HandBrake/HandBrake/releases/download/${HANDBRAKE_VERSION}/HandBrake-${HANDBRAKE_VERSION}-source.tar.bz2" local src_url="https://github.com/HandBrake/HandBrake/releases/download/${HANDBRAKE_VERSION}/HandBrake-${HANDBRAKE_VERSION}-source.tar.bz2"
local tarball="${tmp_dir}/handbrake-src.tar.bz2" local tarball="${cache_dir}/HandBrake-${HANDBRAKE_VERSION}-source.tar.bz2"
local src_dir="${cache_dir}/HandBrake-${HANDBRAKE_VERSION}"
# Alte Installationen vollständig entfernen if [[ -t 0 && -d "$cache_dir" ]]; then
remove_all_handbrake local clear_cache_answer=""
warn "Build-Cache gefunden: $cache_dir"
warn "Wenn du ihn löschst, startet der Source-Build wieder komplett von vorne."
read -r -p "Build-Cache jetzt löschen (sudo rm -rf $cache_dir)? [y/N] " clear_cache_answer
case "${clear_cache_answer,,}" in
y|yes|j|ja)
rm -rf "$cache_dir"
info "Build-Cache gelöscht."
;;
esac
fi
mkdir -p "$cache_dir"
# Build-Abhängigkeiten # Build-Abhängigkeiten
info "Installiere Build-Abhängigkeiten..." info "Installiere Build-Abhängigkeiten..."
apt-get install -y \ apt-get install -y \
autoconf automake build-essential cmake git \ autoconf automake build-essential clang cmake git \
libass-dev libbz2-dev libdvdnav-dev libdvdread-dev \ libass-dev libbz2-dev libdvdnav-dev libdvdread-dev \
libfontconfig-dev libfreetype-dev libfribidi-dev libharfbuzz-dev \ libfontconfig-dev libfreetype-dev libfribidi-dev libharfbuzz-dev \
libjansson-dev liblzma-dev libmp3lame-dev libnuma-dev libogg-dev \ libjansson-dev liblzma-dev libmp3lame-dev libnuma-dev libogg-dev \
libopus-dev libsamplerate0-dev libspeex-dev libtheora-dev libtool libtool-bin \ libopus-dev libsamplerate0-dev libspeex-dev libtheora-dev libtool libtool-bin libva-dev \
libturbojpeg0-dev libvorbis-dev libvpx-dev libx264-dev libxml2-dev \ libturbojpeg0-dev libvorbis-dev libvpx-dev libx264-dev libxml2-dev \
m4 meson nasm ninja-build patch pkg-config python3 tar zlib1g-dev \ m4 meson nasm ninja-build patch pkg-config python3 tar yasm zlib1g-dev \
>/dev/null >/dev/null
# CUDA Toolkit für NVDEC-Header # CUDA Toolkit für NVDEC-Header
@@ -242,35 +308,109 @@ build_handbrake_nvdec() {
fi fi
ok "Build-Abhängigkeiten installiert" ok "Build-Abhängigkeiten installiert"
# Quellcode herunterladen if [[ ! -d "$src_dir" ]]; then
if [[ ! -f "$tarball" ]]; then
info "Lade HandBrake ${HANDBRAKE_VERSION} herunter..." info "Lade HandBrake ${HANDBRAKE_VERSION} herunter..."
curl -fsSL "$src_url" -o "$tarball" 2>/dev/null || \ curl -fsSL "$src_url" -o "$tarball" 2>/dev/null || \
wget -q "$src_url" -O "$tarball" || \ wget -q "$src_url" -O "$tarball" || \
fatal "HandBrake-Quellcode konnte nicht heruntergeladen werden (${src_url})" fatal "HandBrake-Quellcode konnte nicht heruntergeladen werden (${src_url})"
else
info "Nutze vorhandenes HandBrake-Source-Archiv: $tarball"
fi
info "Entpacke Quellcode..." info "Entpacke Quellcode..."
tar xjf "$tarball" -C "$tmp_dir" tar xjf "$tarball" -C "$cache_dir"
local src_dir="${tmp_dir}/HandBrake-${HANDBRAKE_VERSION}" [[ -d "$src_dir" ]] || src_dir=$(find "$cache_dir" -maxdepth 1 -type d -name "HandBrake*" | head -1)
[[ -d "$src_dir" ]] || src_dir=$(find "$tmp_dir" -maxdepth 1 -type d -name "HandBrake*" | head -1) [[ -d "$src_dir" ]] || fatal "HandBrake-Quellverzeichnis nicht gefunden in $cache_dir"
[[ -d "$src_dir" ]] || fatal "HandBrake-Quellverzeichnis nicht gefunden in $tmp_dir" else
info "Nutze vorhandenen HandBrake-Source-Baum: $src_dir"
fi
cd "$src_dir" cd "$src_dir"
info "Konfiguriere HandBrake mit NVDEC..." local configure_log="${src_dir}/build/configure-ripster.log"
./configure --launch-jobs="$(nproc)" --enable-nvdec --prefix=/usr/local 2>&1 | tail -10 local configure_stamp="${src_dir}/build/.ripster-config"
local configure_args="--enable-nvdec --disable-gtk --prefix=/usr/local"
local need_configure="false"
local configure_cmd=(
./configure
--launch-jobs="$(nproc)"
--enable-nvdec
--disable-gtk
--prefix=/usr/local
)
if [[ ! -f "$src_dir/build/GNUmakefile" ]]; then
need_configure="true"
elif [[ ! -f "$configure_stamp" ]]; then
need_configure="true"
elif ! grep -qx "args=${configure_args}" "$configure_stamp"; then
need_configure="true"
fi
if [[ "$need_configure" == "true" ]]; then
if [[ -d "$src_dir/build" ]]; then
configure_cmd=(./configure --force "${configure_cmd[@]:1}")
fi
if [[ -f "$src_dir/build/GNUmakefile" ]]; then
info "Vorhandener Build gefunden aktualisiere Konfiguration (CLI-only)."
else
info "Konfiguriere HandBrake mit NVDEC (CLI-only)..."
fi
if ! "${configure_cmd[@]}" >"$configure_log" 2>&1; then
tail -n 50 "$configure_log" >&2 || true
fatal "HandBrake-Konfiguration fehlgeschlagen. Vollständiges Log: $configure_log"
fi
tail -n 10 "$configure_log"
mkdir -p "${src_dir}/build"
cat > "$configure_stamp" <<EOF
args=${configure_args}
version=${HANDBRAKE_VERSION}
configured_at_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)
EOF
else
info "Vorhandene CLI-only-Konfiguration erkannt überspringe configure."
fi
if [[ -x "$src_dir/build/HandBrakeCLI" ]]; then
info "Vorhandenes HandBrakeCLI-Build-Artefakt gefunden versuche direkte Installation."
if ! make --directory=build install; then
warn "Direkte Installation fehlgeschlagen setze Build fort."
make --directory=build -j"$(nproc)"
make --directory=build install
fi
hash -r 2>/dev/null || true
if ! command_exists HandBrakeCLI; then
warn "HandBrakeCLI nach direkter Installation nicht gefunden setze Build fort."
make --directory=build -j"$(nproc)"
make --directory=build install
fi
else
info "Baue HandBrake ($(nproc) Threads bitte warten)..." info "Baue HandBrake ($(nproc) Threads bitte warten)..."
make --directory=build -j"$(nproc)" make --directory=build -j"$(nproc)"
info "Installiere HandBrake nach /usr/local/bin..." info "Installiere HandBrake nach /usr/local/bin..."
make --directory=build install make --directory=build install
fi
cd / cd /
rm -rf "$tmp_dir" hash -r 2>/dev/null || true
if command_exists HandBrakeCLI; then if command_exists HandBrakeCLI; then
local ver local ver
ver=$(HandBrakeCLI --version 2>&1 | head -1) ver=$(HandBrakeCLI --version 2>&1 | head -1)
handbrake_has_nvdec || fatal "HandBrakeCLI ist installiert, aber ohne NVDEC-Unterstützung."
mkdir -p "$(dirname "$HANDBRAKE_SELFBUILD_MARKER")"
cat > "$HANDBRAKE_SELFBUILD_MARKER" <<EOF
version=${HANDBRAKE_VERSION}
source_dir=${src_dir}
binary_path=$(command -v HandBrakeCLI)
installed_at_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)
EOF
ok "HandBrakeCLI mit NVDEC installiert: ${ver}" ok "HandBrakeCLI mit NVDEC installiert: ${ver}"
if ldconfig -p 2>/dev/null | grep -q libnvcuvid; then if ldconfig -p 2>/dev/null | grep -q libnvcuvid; then
ok "libnvcuvid gefunden NVDEC ist zur Laufzeit verfügbar." ok "libnvcuvid gefunden NVDEC ist zur Laufzeit verfügbar."
@@ -292,80 +432,108 @@ has_nvidia_gpu() {
install_handbrake() { install_handbrake() {
header "HandBrake CLI installieren" header "HandBrake CLI installieren"
# NVIDIA-GPU vorhanden? → immer NVDEC-Build erzwingen local hb_path
if has_nvidia_gpu; then local current_mode="none"
info "NVIDIA-GPU erkannt HandBrake wird mit NVDEC aus Quellcode gebaut." hb_path=$(command -v HandBrakeCLI 2>/dev/null || true)
BUILD_HANDBRAKE_NVDEC=true if [[ -n "$hb_path" ]]; then
if handbrake_has_nvdec; then
current_mode="nvdec"
else
current_mode="standard"
fi
fi fi
# --build-handbrake oder NVIDIA erkannt: aus Quellcode mit NVDEC bauen
if [[ "$BUILD_HANDBRAKE_NVDEC" == true ]]; then if [[ "$BUILD_HANDBRAKE_NVDEC" == true ]]; then
info "Installmodus: Source-Build mit NVDEC-Support"
if has_nvidia_gpu; then
info "NVIDIA-GPU erkannt NVDEC-Build wird verwendet."
fi
if handbrake_has_nvdec; then
if handbrake_is_self_built "$hb_path"; then
local installed_ver latest_ver answer
installed_ver=$(handbrake_installed_version || true)
latest_ver=$(handbrake_latest_git_version || true)
if [[ -n "$installed_ver" && -n "$latest_ver" ]] && dpkg --compare-versions "$latest_ver" gt "$installed_ver"; then
case "$HANDBRAKE_UPDATE_POLICY" in
keep)
warn "Neuere HandBrake-Version verfügbar (${latest_ver}, installiert: ${installed_ver}) behalte aktuelle Installation."
return
;;
prompt)
if [[ -t 0 ]]; then
read -r -p "Neue HandBrake-Version ${latest_ver} verfügbar (installiert ${installed_ver}). Neu bauen? [y/N] " answer
case "${answer,,}" in
y|yes|j|ja)
info "Aktualisiere auf HandBrake ${latest_ver}."
HANDBRAKE_VERSION="$latest_ver"
;;
*)
info "Behalte bestehende Installation (${installed_ver})."
return
;;
esac
else
warn "Neuere HandBrake-Version verfügbar (${latest_ver}), aber kein TTY für Rückfrage behalte aktuelle Installation."
return
fi
;;
build)
info "Neuere HandBrake-Version verfügbar (${latest_ver}, installiert: ${installed_ver}) aktualisiere."
HANDBRAKE_VERSION="$latest_ver"
;;
esac
else
ok "Selbst gebautes HandBrakeCLI mit NVDEC bereits installiert: $(HandBrakeCLI --version 2>&1 | head -1)"
return
fi
fi
warn "HandBrakeCLI mit NVDEC gefunden (${hb_path}), aber nicht als Selbst-Build erkannt."
fi
if [[ -n "$hb_path" ]] && ! handbrake_has_nvdec; then
warn "HandBrakeCLI ohne NVDEC gefunden (${hb_path}) wird ersetzt durch Selbst-Build."
elif [[ -z "$hb_path" ]]; then
info "Kein HandBrakeCLI gefunden baue aus Quellcode."
fi
remove_non_selfbuilt_handbrake
build_handbrake_nvdec build_handbrake_nvdec
return return
fi fi
# Bereits installiert → nichts tun info "Installmodus: Standard (APT, ohne NVDEC-Zwang)"
if [[ "$current_mode" == "standard" ]] && ! handbrake_is_self_built "$hb_path"; then
ok "HandBrakeCLI bereits installiert: $(HandBrakeCLI --version 2>&1 | head -1)"
return
fi
if [[ "$current_mode" == "nvdec" ]]; then
warn "Umschalten von NVDEC-Build auf Standard-Installation."
fi
remove_selfbuilt_handbrake
remove_non_selfbuilt_handbrake
info "Installiere HandBrakeCLI aus den Standard-Repositories..."
apt_update
apt-get install -y handbrake-cli >/dev/null
hash -r 2>/dev/null || true
if command_exists HandBrakeCLI; then if command_exists HandBrakeCLI; then
local ver ok "HandBrakeCLI installiert: $(HandBrakeCLI --version 2>&1 | head -1)"
ver=$(HandBrakeCLI --version 2>&1 | head -1)
ok "HandBrakeCLI bereits installiert: ${ver}"
return return
fi fi
# Strategie 1: direkt aus den Distro-Repos (Ubuntu Universe / Debian) if command_exists handbrake-cli; then
info "Versuche HandBrake CLI aus den Standard-Repos..." ln -sf "$(command -v handbrake-cli)" /usr/local/bin/HandBrakeCLI
if apt-get install -y handbrake-cli 2>/dev/null; then hash -r 2>/dev/null || true
ok "HandBrakeCLI installiert (Standard-Repos)" ok "HandBrakeCLI Alias angelegt auf: $(command -v handbrake-cli)"
return return
fi fi
case "$ID" in fatal "HandBrake wurde installiert, aber kein CLI-Befehl wurde gefunden."
ubuntu)
local codename="${VERSION_CODENAME:-jammy}"
local ppa_sources="/etc/apt/sources.list.d/handbrake.list"
local ppa_key="/etc/apt/keyrings/handbrake.gpg"
info "Füge HandBrake PPA manuell hinzu (${codename})..."
mkdir -p /etc/apt/keyrings
if curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x8771ADB0816950D8" \
| gpg --dearmor -o "$ppa_key" 2>/dev/null; then
cat > "$ppa_sources" <<EOF
deb [signed-by=${ppa_key}] https://ppa.launchpadcontent.net/stebbins/handbrake-releases/ubuntu ${codename} main
EOF
apt_update
apt-get install -y handbrake-cli 2>/dev/null && \
{ ok "HandBrakeCLI installiert (PPA)"; return; } || \
{ warn "PPA-Installation fehlgeschlagen, räume auf...";
rm -f "$ppa_key" "$ppa_sources"; }
else
warn "PPA-Key konnte nicht geladen werden."
fi
if command_exists snap; then
info "Versuche HandBrake via snap..."
if snap install handbrake-cli 2>/dev/null; then
ok "HandBrakeCLI installiert (snap)"
return
fi
fi
;;
debian)
info "Versuche HandBrake CLI über Debian Backports..."
if ! find /etc/apt/sources.list.d/ -name "*.list" -exec grep -l "backports" {} \; 2>/dev/null | grep -q .; then
echo "deb http://deb.debian.org/debian ${VERSION_CODENAME}-backports main" \
> /etc/apt/sources.list.d/backports.list
apt_update
fi
apt-get install -y -t "${VERSION_CODENAME}-backports" handbrake-cli 2>/dev/null && \
{ ok "HandBrakeCLI installiert (Backports)"; return; }
;;
esac
warn "HandBrake CLI konnte nicht automatisch installiert werden."
warn "Für einen NVDEC-Build: sudo bash install-dev.sh --no-makemkv --no-nginx --build-handbrake"
warn "Oder manuell: https://handbrake.fr/downloads2.php"
} }
# --- apt-Hilfsfunktionen ------------------------------------------------------ # --- apt-Hilfsfunktionen ------------------------------------------------------

View File

@@ -20,8 +20,6 @@
# --host <hostname> Hostname/IP für die Weboberfläche (Standard: Maschinen-IP) # --host <hostname> Hostname/IP für die Weboberfläche (Standard: Maschinen-IP)
# --no-makemkv MakeMKV-Installation überspringen # --no-makemkv MakeMKV-Installation überspringen
# --no-handbrake HandBrake-Installation überspringen # --no-handbrake HandBrake-Installation überspringen
# --build-handbrake HandBrake aus Quellcode mit NVDEC-Unterstützung bauen
# --handbrake-version HandBrake-Version für Source-Build (Standard: 1.9.0)
# --no-nginx Nginx-Einrichtung überspringen # --no-nginx Nginx-Einrichtung überspringen
# --reinstall Vorhandene Installation aktualisieren (Daten bleiben erhalten) # --reinstall Vorhandene Installation aktualisieren (Daten bleiben erhalten)
# -h, --help Diese Hilfe anzeigen # -h, --help Diese Hilfe anzeigen
@@ -29,6 +27,8 @@
set -euo pipefail set -euo pipefail
REPO_URL="https://github.com/Mboehmlaender/ripster.git" REPO_URL="https://github.com/Mboehmlaender/ripster.git"
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd -P)"
BUNDLED_HANDBRAKE_CLI="${SCRIPT_DIR}/bin/HandBrakeCLI"
# --- Farben ------------------------------------------------------------------- # --- Farben -------------------------------------------------------------------
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
@@ -51,8 +51,7 @@ BACKEND_PORT="3001"
FRONTEND_HOST="" FRONTEND_HOST=""
SKIP_MAKEMKV=false SKIP_MAKEMKV=false
SKIP_HANDBRAKE=false SKIP_HANDBRAKE=false
BUILD_HANDBRAKE_NVDEC=false HANDBRAKE_INSTALL_MODE=""
HANDBRAKE_VERSION="1.9.0"
SKIP_NGINX=false SKIP_NGINX=false
REINSTALL=false REINSTALL=false
@@ -66,8 +65,6 @@ while [[ $# -gt 0 ]]; do
--host) FRONTEND_HOST="$2"; shift 2 ;; --host) FRONTEND_HOST="$2"; shift 2 ;;
--no-makemkv) SKIP_MAKEMKV=true; shift ;; --no-makemkv) SKIP_MAKEMKV=true; shift ;;
--no-handbrake) SKIP_HANDBRAKE=true; shift ;; --no-handbrake) SKIP_HANDBRAKE=true; shift ;;
--build-handbrake) BUILD_HANDBRAKE_NVDEC=true; shift ;;
--handbrake-version) HANDBRAKE_VERSION="$2"; shift 2 ;;
--no-nginx) SKIP_NGINX=true; shift ;; --no-nginx) SKIP_NGINX=true; shift ;;
--reinstall) REINSTALL=true; shift ;; --reinstall) REINSTALL=true; shift ;;
-h|--help) -h|--help)
@@ -188,201 +185,81 @@ install_makemkv() {
warn "Beta-Key: https://www.makemkv.com/forum/viewtopic.php?t=1053" warn "Beta-Key: https://www.makemkv.com/forum/viewtopic.php?t=1053"
} }
remove_all_handbrake() { select_handbrake_mode() {
info "Entferne alle vorhandenen HandBrake-Installationen..." [[ "$SKIP_HANDBRAKE" == true ]] && return
# apt
apt-get remove -y handbrake-cli handbrake 2>/dev/null || true local mode_answer=""
# snap echo ""
snap remove handbrake-cli 2>/dev/null || true echo "Install HandBrake:"
# bekannte Binär-Pfade echo ""
rm -f /usr/bin/HandBrakeCLI \ echo "1. Standard version (apt install handbrake-cli)"
/usr/local/bin/HandBrakeCLI \ echo "2. GPU version with NVDEC (use bundled binary)"
/snap/bin/handbrake-cli \
/snap/bin/HandBrakeCLI if [[ -t 0 ]]; then
# alle weiteren Fundstellen über PATH read -r -p "Select option [1/2]: " mode_answer
while true; do elif [[ -r /dev/tty ]]; then
local found read -r -p "Select option [1/2]: " mode_answer </dev/tty
found=$(command -v HandBrakeCLI 2>/dev/null || true) else
[[ -z "$found" ]] && break HANDBRAKE_INSTALL_MODE="standard"
warn "Entferne: $found" warn "Kein interaktives Terminal erkannt verwende Standardversion (apt)."
rm -f "$found" return
done fi
hash -r 2>/dev/null || true
ok "Alte HandBrake-Installation(en) entfernt" case "$mode_answer" in
2) HANDBRAKE_INSTALL_MODE="gpu" ;;
1|"") HANDBRAKE_INSTALL_MODE="standard" ;;
*) warn "Ungültige Auswahl '$mode_answer' verwende Standardversion."; HANDBRAKE_INSTALL_MODE="standard" ;;
esac
} }
build_handbrake_nvdec() { install_handbrake_standard() {
header "HandBrake ${HANDBRAKE_VERSION} mit NVDEC aus Quellcode bauen" info "Installiere HandBrakeCLI aus den Standard-Repositories..."
info "Aktualisiere Paketlisten..."
local tmp_dir apt_update
tmp_dir=$(mktemp -d) apt-get install -y handbrake-cli
local src_url="https://github.com/HandBrake/HandBrake/releases/download/${HANDBRAKE_VERSION}/HandBrake-${HANDBRAKE_VERSION}-source.tar.bz2" hash -r 2>/dev/null || true
local tarball="${tmp_dir}/handbrake-src.tar.bz2"
# Alte Installationen vollständig entfernen
remove_all_handbrake
# Build-Abhängigkeiten
info "Installiere Build-Abhängigkeiten..."
apt-get install -y \
autoconf automake build-essential cmake git \
libass-dev libbz2-dev libdvdnav-dev libdvdread-dev \
libfontconfig-dev libfreetype-dev libfribidi-dev libharfbuzz-dev \
libjansson-dev liblzma-dev libmp3lame-dev libnuma-dev libogg-dev \
libopus-dev libsamplerate0-dev libspeex-dev libtheora-dev libtool libtool-bin \
libturbojpeg0-dev libvorbis-dev libvpx-dev libx264-dev libxml2-dev \
m4 meson nasm ninja-build patch pkg-config python3 tar zlib1g-dev \
>/dev/null
# CUDA Toolkit für NVDEC-Header
info "Installiere CUDA Toolkit (für NVDEC-Header)..."
if ! dpkg -l 2>/dev/null | grep -q '^ii.*nvidia-cuda-toolkit'; then
apt-get install -y nvidia-cuda-toolkit >/dev/null 2>&1 || {
warn "nvidia-cuda-toolkit nicht verfügbar versuche Fallback-Header..."
local cuda_keyring="/tmp/cuda-keyring.deb"
local ubuntu_ver="${VERSION_ID//./}"
curl -fsSL "https://developer.download.nvidia.com/compute/cuda/repos/ubuntu${ubuntu_ver}/x86_64/cuda-keyring_1.1-1_all.deb" \
-o "$cuda_keyring" 2>/dev/null && \
dpkg -i "$cuda_keyring" 2>/dev/null && \
apt-get update -qq && \
apt-get install -y cuda-cudart-dev-12-8 >/dev/null 2>&1 || \
warn "CUDA-Header konnten nicht installiert werden NVDEC wird möglicherweise nicht verfügbar sein."
}
fi
ok "Build-Abhängigkeiten installiert"
# Quellcode herunterladen
info "Lade HandBrake ${HANDBRAKE_VERSION} herunter..."
curl -fsSL "$src_url" -o "$tarball" 2>/dev/null || \
wget -q "$src_url" -O "$tarball" || \
fatal "HandBrake-Quellcode konnte nicht heruntergeladen werden (${src_url})"
info "Entpacke Quellcode..."
tar xjf "$tarball" -C "$tmp_dir"
local src_dir="${tmp_dir}/HandBrake-${HANDBRAKE_VERSION}"
[[ -d "$src_dir" ]] || src_dir=$(find "$tmp_dir" -maxdepth 1 -type d -name "HandBrake*" | head -1)
[[ -d "$src_dir" ]] || fatal "HandBrake-Quellverzeichnis nicht gefunden in $tmp_dir"
cd "$src_dir"
# Konfigurieren mit NVDEC
info "Konfiguriere HandBrake mit NVDEC..."
./configure --launch-jobs="$(nproc)" --enable-nvdec --prefix=/usr/local 2>&1 | tail -10
# Bauen (dauert je nach Hardware 1030 Min)
info "Baue HandBrake ($(nproc) Threads bitte warten)..."
make --directory=build -j"$(nproc)"
info "Installiere HandBrake nach /usr/local/bin..."
make --directory=build install
cd /
rm -rf "$tmp_dir"
if command_exists HandBrakeCLI; then if command_exists HandBrakeCLI; then
local ver ok "HandBrakeCLI installiert: $(HandBrakeCLI --version 2>&1 | head -1)"
ver=$(HandBrakeCLI --version 2>&1 | head -1) return
ok "HandBrakeCLI mit NVDEC installiert: ${ver}"
# NVDEC-Verfügbarkeit zur Laufzeit hängt vom NVIDIA-Treiber ab.
# Prüfe ob libnvcuvid vorhanden:
if ldconfig -p 2>/dev/null | grep -q libnvcuvid; then
ok "libnvcuvid gefunden NVDEC ist zur Laufzeit verfügbar."
else
warn "libnvcuvid NICHT gefunden. NVDEC benötigt den installierten NVIDIA-Treiber (nvidia-driver-XXX)."
warn "Stelle sicher, dass der NVIDIA-Treiber auf dem System installiert ist."
fi fi
else
fatal "HandBrakeCLI nach dem Build nicht gefunden Build fehlgeschlagen." if command_exists handbrake-cli; then
ok "handbrake-cli installiert: $(handbrake-cli --version 2>&1 | head -1)"
return
fi fi
fatal "HandBrake wurde installiert, aber kein CLI-Befehl wurde gefunden."
} }
handbrake_has_nvdec() { install_handbrake_gpu_bundled() {
command_exists HandBrakeCLI || return 1 info "Installiere gebündeltes HandBrakeCLI mit NVDEC..."
HandBrakeCLI --help 2>&1 | grep -qi "nvdec"
if [[ ! -f "$BUNDLED_HANDBRAKE_CLI" ]]; then
fatal "Bundled Binary fehlt: ./bin/HandBrakeCLI (aufgelöst zu: $BUNDLED_HANDBRAKE_CLI)"
fi
install -m 0755 "$BUNDLED_HANDBRAKE_CLI" /usr/local/bin/HandBrakeCLI
hash -r 2>/dev/null || true
ok "Bundled HandBrakeCLI installiert nach /usr/local/bin/HandBrakeCLI"
if command_exists HandBrakeCLI; then
ok "HandBrakeCLI Version: $(HandBrakeCLI --version 2>&1 | head -1)"
fi
} }
install_handbrake() { install_handbrake() {
header "HandBrake CLI installieren" header "HandBrake CLI installieren"
# Bereits installiert MIT NVDEC → nichts tun if [[ -z "$HANDBRAKE_INSTALL_MODE" ]]; then
if handbrake_has_nvdec; then HANDBRAKE_INSTALL_MODE="standard"
ok "HandBrakeCLI mit NVDEC bereits installiert: $(HandBrakeCLI --version 2>&1 | head -1)"
return
fi fi
# Installiert OHNE NVDEC → entfernen und NVDEC-Build erzwingen case "$HANDBRAKE_INSTALL_MODE" in
if command_exists HandBrakeCLI; then standard) install_handbrake_standard ;;
warn "HandBrakeCLI ohne NVDEC gefunden wird ersetzt durch NVDEC-Build." gpu) install_handbrake_gpu_bundled ;;
BUILD_HANDBRAKE_NVDEC=true *) fatal "Unbekannter HandBrake-Modus: $HANDBRAKE_INSTALL_MODE" ;;
fi
# --build-handbrake-Flag oder kein NVDEC in vorhandener Installation
if [[ "$BUILD_HANDBRAKE_NVDEC" == true ]]; then
build_handbrake_nvdec
return
fi
# Strategie 1: direkt aus den Distro-Repos (Ubuntu Universe / Debian)
info "Versuche HandBrake CLI aus den Standard-Repos..."
if apt-get install -y handbrake-cli 2>/dev/null; then
# Nach apt-Install: NVDEC prüfen falls nicht, sofort NVDEC-Build
if handbrake_has_nvdec; then
ok "HandBrakeCLI installiert (Standard-Repos, mit NVDEC)"
else
warn "Installiertes HandBrakeCLI hat kein NVDEC ersetze durch NVDEC-Build."
build_handbrake_nvdec
fi
return
fi
case "$ID" in
ubuntu)
local codename="${VERSION_CODENAME:-jammy}"
local ppa_sources="/etc/apt/sources.list.d/handbrake.list"
local ppa_key="/etc/apt/keyrings/handbrake.gpg"
info "Füge HandBrake PPA manuell hinzu (${codename})..."
mkdir -p /etc/apt/keyrings
if curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x8771ADB0816950D8" \
| gpg --dearmor -o "$ppa_key" 2>/dev/null; then
cat > "$ppa_sources" <<EOF
deb [signed-by=${ppa_key}] https://ppa.launchpadcontent.net/stebbins/handbrake-releases/ubuntu ${codename} main
EOF
apt_update
apt-get install -y handbrake-cli 2>/dev/null && \
{ ok "HandBrakeCLI installiert (PPA)"; return; } || \
{ warn "PPA-Installation fehlgeschlagen, räume auf...";
rm -f "$ppa_key" "$ppa_sources"; }
else
warn "PPA-Key konnte nicht geladen werden."
fi
# Strategie 3 (Ubuntu): snap
if command_exists snap; then
info "Versuche HandBrake via snap..."
if snap install handbrake-cli 2>/dev/null; then
ok "HandBrakeCLI installiert (snap)"
return
fi
fi
;;
debian)
info "Versuche HandBrake CLI über Debian Backports..."
if ! find /etc/apt/sources.list.d/ -name "*.list" -exec grep -l "backports" {} \; 2>/dev/null | grep -q .; then
echo "deb http://deb.debian.org/debian ${VERSION_CODENAME}-backports main" \
> /etc/apt/sources.list.d/backports.list
apt_update
fi
apt-get install -y -t "${VERSION_CODENAME}-backports" handbrake-cli 2>/dev/null && \
{ ok "HandBrakeCLI installiert (Backports)"; return; }
;;
esac esac
warn "HandBrake CLI konnte nicht automatisch installiert werden."
warn "Für einen NVDEC-Build: sudo bash install.sh --no-makemkv --no-nginx --build-handbrake"
warn "Oder manuell: https://handbrake.fr/downloads2.php"
} }
# --- apt-Hilfsfunktionen ------------------------------------------------------ # --- apt-Hilfsfunktionen ------------------------------------------------------
@@ -463,6 +340,9 @@ EOF
fi fi
} }
# --- HandBrake-Installmodus auswählen ----------------------------------------
select_handbrake_mode
# --- Systemabhängigkeiten ----------------------------------------------------- # --- Systemabhängigkeiten -----------------------------------------------------
header "Systemabhängigkeiten installieren" header "Systemabhängigkeiten installieren"

133
kill.sh
View File

@@ -1,133 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PID_FILE="$ROOT_DIR/start.pid"
WAIT_SECONDS=8
declare -A PID_SET=()
is_running() {
local pid="$1"
kill -0 "$pid" 2>/dev/null
}
is_pid_under_root() {
local pid="$1"
local cwd
cwd="$(readlink -f "/proc/$pid/cwd" 2>/dev/null || true)"
[[ -n "$cwd" && "$cwd" == "$ROOT_DIR"* ]]
}
add_pid() {
local pid="$1"
[[ "$pid" =~ ^[0-9]+$ ]] || return 0
(( pid > 1 )) || return 0
(( pid == $$ )) && return 0
is_running "$pid" || return 0
PID_SET["$pid"]=1
}
collect_descendants() {
local parent="$1"
local child
while read -r child; do
[[ -n "$child" ]] || continue
add_pid "$child"
collect_descendants "$child"
done < <(pgrep -P "$parent" 2>/dev/null || true)
}
collect_from_pid_file() {
local pid
[[ -f "$PID_FILE" ]] || return 0
pid="$(tr -d '[:space:]' < "$PID_FILE" || true)"
[[ "$pid" =~ ^[0-9]+$ ]] || return 0
is_running "$pid" || return 0
add_pid "$pid"
collect_descendants "$pid"
}
collect_by_process_scan() {
local pid cmd
while read -r pid cmd; do
[[ -n "$pid" ]] || continue
is_pid_under_root "$pid" || continue
case "$cmd" in
*concurrently*|*npm\ run\ dev*|*nodemon*|*vite*|*backend/src/index.js*)
add_pid "$pid"
collect_descendants "$pid"
;;
esac
done < <(ps -eo pid=,cmd=)
}
collect_all_targets() {
collect_from_pid_file
collect_by_process_scan
}
terminate_pids() {
local signal="$1"
local pid
for pid in "${!PID_SET[@]}"; do
is_running "$pid" || continue
kill "-$signal" "$pid" 2>/dev/null || true
done
}
count_running_targets() {
local pid count=0
for pid in "${!PID_SET[@]}"; do
if is_running "$pid"; then
count=$((count + 1))
fi
done
echo "$count"
}
main() {
local running_count elapsed
collect_all_targets
if [[ ${#PID_SET[@]} -eq 0 ]]; then
echo "Keine laufenden Ripster-Prozesse gefunden."
rm -f "$PID_FILE"
exit 0
fi
echo "Beende ${#PID_SET[@]} Ripster-Prozess(e) ..."
terminate_pids TERM
elapsed=0
while (( elapsed < WAIT_SECONDS )); do
running_count="$(count_running_targets)"
if [[ "$running_count" -eq 0 ]]; then
break
fi
sleep 1
elapsed=$((elapsed + 1))
done
running_count="$(count_running_targets)"
if [[ "$running_count" -gt 0 ]]; then
echo "Noch $running_count Prozess(e) aktiv, sende SIGKILL ..."
terminate_pids KILL
fi
rm -f "$PID_FILE"
echo "Ripster-Prozesse wurden beendet."
}
main "$@"