HandBrake
This commit is contained in:
10
README.md
10
README.md
@@ -6,11 +6,15 @@ Ripster ist eine lokale Web-Anwendung für halbautomatisches Disc-Ripping mit Ma
|
||||
|
||||
> **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
|
||||
> - **DVD-Erkennung verbessert** – robuste Media-Profil-Erkennung (Blu-ray / DVD / CD) aus UDF/ISO9660-Dateisystemtyp, Laufwerk-Modell und Disc-Label
|
||||
> - **Profil-spezifische Einstellungen** – Separate Konfiguration für Blu-ray und DVD: eigene Pfade, HandBrake-Presets, Rip-Modi, Extra-Args, Dateinamen-Templates; automatische Auflösung anhand des erkannten Medientyps
|
||||
> - **Cron-Job-System** – Skripte und Skript-Ketten zeitgesteuert ausführen; eigener Expression-Parser, Ausführungs-Logs, manuelle Auslösung, PushOver-Integration pro Job
|
||||
> - **User-Presets** – benannte HandBrake-Preset-Sammlungen (Preset + Extra-Args) pro Medientyp anlegen und im Review-Panel auswählen
|
||||
> - **DVD-/Blu-ray-Erkennung verbessert** – robuste Media-Profil-Erkennung aus UDF/ISO9660-Dateisystemtyp, Laufwerk-Modell und Disc-Label; Medientyp-Indikator in der UI
|
||||
> - **Pre-Encode-Ausführungen** – Skripte und Ketten können nun auch *vor* dem Encode-Schritt ausgeführt werden (zusätzlich zu Post-Encode)
|
||||
> - **Sortierbare Skripte & Ketten** – Reihenfolge über Drag & Drop festlegen; wird persistent 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)
|
||||
> - **`handbrake_restart_delete_incomplete_output`** – unvollständige Ausgabe wird beim Encode-Neustart automatisch gelöscht
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -100,12 +100,14 @@ router.post(
|
||||
const selectedTrackSelection = req.body?.selectedTrackSelection ?? null;
|
||||
const selectedPostEncodeScriptIds = req.body?.selectedPostEncodeScriptIds;
|
||||
const skipPipelineStateUpdate = Boolean(req.body?.skipPipelineStateUpdate);
|
||||
const selectedUserPresetId = req.body?.selectedUserPresetId ?? null;
|
||||
logger.info('post:confirm-encode', {
|
||||
reqId: req.reqId,
|
||||
jobId,
|
||||
selectedEncodeTitleId,
|
||||
selectedTrackSelectionProvided: Boolean(selectedTrackSelection),
|
||||
skipPipelineStateUpdate,
|
||||
selectedUserPresetId,
|
||||
selectedPostEncodeScriptIdsCount: Array.isArray(selectedPostEncodeScriptIds)
|
||||
? selectedPostEncodeScriptIds.length
|
||||
: 0
|
||||
@@ -114,7 +116,8 @@ router.post(
|
||||
selectedEncodeTitleId,
|
||||
selectedTrackSelection,
|
||||
selectedPostEncodeScriptIds,
|
||||
skipPipelineStateUpdate
|
||||
skipPipelineStateUpdate,
|
||||
selectedUserPresetId
|
||||
});
|
||||
res.json({ job });
|
||||
})
|
||||
|
||||
@@ -7,6 +7,7 @@ const notificationService = require('../services/notificationService');
|
||||
const pipelineService = require('../services/pipelineService');
|
||||
const wsService = require('../services/websocketService');
|
||||
const hardwareMonitorService = require('../services/hardwareMonitorService');
|
||||
const userPresetService = require('../services/userPresetService');
|
||||
const logger = require('../services/logger').child('SETTINGS_ROUTE');
|
||||
|
||||
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(
|
||||
'/pushover/test',
|
||||
asyncHandler(async (req, res) => {
|
||||
|
||||
@@ -17,6 +17,7 @@ const { ensureDir, sanitizeFileName, renderTemplate, findMediaFiles } = require(
|
||||
const { buildMediainfoReview } = require('../utils/encodePlan');
|
||||
const { analyzePlaylistObfuscation, normalizePlaylistId } = require('../utils/playlistAnalysis');
|
||||
const { errorToMeta } = require('../utils/errorMeta');
|
||||
const userPresetService = require('./userPresetService');
|
||||
|
||||
const RUNNING_STATES = new Set(['ANALYZING', 'RIPPING', 'ENCODING', 'MEDIAINFO_CHECK']);
|
||||
const REVIEW_REFRESH_SETTING_PREFIXES = [
|
||||
@@ -5583,6 +5584,21 @@ class PipelineService extends EventEmitter {
|
||||
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 = {
|
||||
...planForConfirm,
|
||||
postEncodeScriptIds: selectedPostEncodeScripts.map((item) => Number(item.id)),
|
||||
@@ -5598,7 +5614,15 @@ class PipelineService extends EventEmitter {
|
||||
postEncodeChainIds: selectedPostEncodeChainIds,
|
||||
preEncodeChainIds: selectedPreEncodeChainIds,
|
||||
reviewConfirmed: true,
|
||||
reviewConfirmedAt: nowIso()
|
||||
reviewConfirmedAt: nowIso(),
|
||||
userPreset: resolvedUserPreset
|
||||
? {
|
||||
id: resolvedUserPreset.id,
|
||||
name: resolvedUserPreset.name,
|
||||
handbrakePreset: resolvedUserPreset.handbrakePreset,
|
||||
extraArgs: resolvedUserPreset.extraArgs
|
||||
}
|
||||
: null
|
||||
};
|
||||
const inputPath = isPreRipMode
|
||||
? null
|
||||
@@ -5622,6 +5646,7 @@ class PipelineService extends EventEmitter {
|
||||
+ ` Pre-Encode-Ketten: ${selectedPreEncodeChainIds.length > 0 ? selectedPreEncodeChainIds.join(',') : 'none'}.`
|
||||
+ ` Post-Encode-Scripte: ${selectedPostEncodeScripts.length > 0 ? selectedPostEncodeScripts.map((item) => item.name).join(' -> ') : 'none'}.`
|
||||
+ ` Post-Encode-Ketten: ${selectedPostEncodeChainIds.length > 0 ? selectedPostEncodeChainIds.join(',') : 'none'}.`
|
||||
+ (resolvedUserPreset ? ` User-Preset: "${resolvedUserPreset.name}" (ID ${resolvedUserPreset.id}).` : '')
|
||||
);
|
||||
|
||||
if (!skipPipelineStateUpdate) {
|
||||
@@ -6683,7 +6708,8 @@ class PipelineService extends EventEmitter {
|
||||
trackSelection,
|
||||
titleId: handBrakeTitleId,
|
||||
mediaProfile,
|
||||
settingsMap: settings
|
||||
settingsMap: settings,
|
||||
userPreset: encodePlan?.userPreset || null
|
||||
});
|
||||
if (trackSelection) {
|
||||
await historyService.appendLog(
|
||||
|
||||
@@ -847,10 +847,20 @@ class SettingsService {
|
||||
if (selectedTitleId !== null) {
|
||||
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 hasSelection = rawSelection && typeof rawSelection === 'object';
|
||||
|
||||
@@ -860,7 +870,8 @@ class SettingsService {
|
||||
args: [...baseArgs, ...extra],
|
||||
inputFile,
|
||||
outputFile,
|
||||
selectedTitleId
|
||||
selectedTitleId,
|
||||
userPresetId: userPreset?.id || null
|
||||
});
|
||||
return { cmd, args: [...baseArgs, ...extra] };
|
||||
}
|
||||
|
||||
133
backend/src/services/userPresetService.js
Normal file
133
backend/src/services/userPresetService.js
Normal 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
BIN
bin/HandBrakeCLI
Executable file
Binary file not shown.
@@ -1,167 +1,194 @@
|
||||
#!/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
|
||||
|
||||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'; BOLD='\033[1m'; RESET='\033[0m'
|
||||
info() { echo -e "${BLUE}[INFO]${RESET} $*"; }
|
||||
ok() { echo -e "${GREEN}[OK]${RESET} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${RESET} $*"; }
|
||||
fatal() { echo -e "${RED}[FEHLER]${RESET} $*" >&2; exit 1; }
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd -P)"
|
||||
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)}"
|
||||
|
||||
HANDBRAKE_VERSION="1.9.0"
|
||||
export LANG="${LANG:-C.UTF-8}"
|
||||
export LC_ALL="${LC_ALL:-C.UTF-8}"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--version) HANDBRAKE_VERSION="$2"; shift 2 ;;
|
||||
-h|--help) echo "Verwendung: sudo bash $0 [--version X.Y.Z]"; exit 0 ;;
|
||||
*) fatal "Unbekannte Option: $1" ;;
|
||||
esac
|
||||
done
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
RESET='\033[0m'
|
||||
|
||||
[[ $EUID -eq 0 ]] || fatal "Bitte als root ausführen: sudo bash $0"
|
||||
info() { echo -e "${BLUE}[INFO]${RESET} $*"; }
|
||||
ok() { echo -e "${GREEN}[OK]${RESET} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${RESET} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${RESET} $*" >&2; }
|
||||
fatal() { error "$*"; exit 1; }
|
||||
|
||||
[[ -f /etc/os-release ]] && . /etc/os-release || fatal "OS nicht erkennbar"
|
||||
|
||||
echo -e "\n${BOLD}${BLUE}══════════════════════════════════════════${RESET}"
|
||||
echo -e "${BOLD} HandBrake ${HANDBRAKE_VERSION} mit NVDEC bauen${RESET}"
|
||||
echo -e "${BOLD}${BLUE}══════════════════════════════════════════${RESET}\n"
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 1. Build-Abhängigkeiten
|
||||
# --------------------------------------------------------------------------
|
||||
info "Installiere Build-Abhängigkeiten..."
|
||||
apt-get update -qq
|
||||
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 \
|
||||
libturbojpeg0-dev libvorbis-dev libvpx-dev libx264-dev libxml2-dev \
|
||||
m4 meson nasm ninja-build patch pkg-config python3 tar zlib1g-dev \
|
||||
>/dev/null
|
||||
ok "Build-Abhängigkeiten installiert"
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 2. CUDA-Header für NVDEC
|
||||
# --------------------------------------------------------------------------
|
||||
info "Prüfe CUDA-Header für NVDEC-Support..."
|
||||
if dpkg -l 2>/dev/null | grep -q '^ii.*nvidia-cuda-toolkit'; then
|
||||
ok "nvidia-cuda-toolkit bereits installiert"
|
||||
else
|
||||
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"
|
||||
run_as_root() {
|
||||
if [[ "${EUID}" -eq 0 ]]; then
|
||||
"$@"
|
||||
elif command -v sudo >/dev/null 2>&1; then
|
||||
sudo "$@"
|
||||
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."
|
||||
fatal "Root-Rechte erforderlich. Bitte als root ausführen oder sudo installieren."
|
||||
fi
|
||||
}
|
||||
|
||||
apt_get() {
|
||||
run_as_root env DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a \
|
||||
apt-get -o Dpkg::Use-Pty=0 "$@"
|
||||
}
|
||||
|
||||
require_cmd() {
|
||||
local cmd="$1"
|
||||
command -v "$cmd" >/dev/null 2>&1 || fatal "Benötigter Befehl fehlt: $cmd"
|
||||
}
|
||||
|
||||
cleanup_stale_tmp_build_dirs() {
|
||||
local stale_dirs=()
|
||||
shopt -s nullglob
|
||||
stale_dirs=(/tmp/handbrake-nvdec-build-*)
|
||||
shopt -u nullglob
|
||||
|
||||
if [[ ${#stale_dirs[@]} -gt 0 ]]; then
|
||||
warn "Bereinige alte temporäre Build-Ordner in /tmp..."
|
||||
run_as_root rm -rf "${stale_dirs[@]}"
|
||||
fi
|
||||
}
|
||||
|
||||
repair_package_state() {
|
||||
local audit_output=""
|
||||
audit_output="$(run_as_root dpkg --audit || true)"
|
||||
|
||||
if [[ -n "${audit_output//[[:space:]]/}" ]]; then
|
||||
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 \
|
||||
libass-dev libbz2-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 libx264-dev libxml2-dev libvpx-dev \
|
||||
m4 make meson nasm ninja-build patch pkg-config tar zlib1g-dev \
|
||||
curl libssl-dev clang bzip2 ca-certificates wget libffmpeg-nvenc-dev
|
||||
|
||||
if [[ ! -f /usr/include/ffnvcodec/dynlink_nvcuvid.h ]]; then
|
||||
warn "NVDEC-Header (dynlink_nvcuvid.h) nicht gefunden. Versuche nvidia-cuda-toolkit als Fallback..."
|
||||
if ! apt_get install -y nvidia-cuda-toolkit; then
|
||||
fatal "NVDEC-Header fehlen und nvidia-cuda-toolkit konnte nicht installiert werden."
|
||||
fi
|
||||
if [[ ! -f /usr/include/ffnvcodec/dynlink_nvcuvid.h && ! -f /usr/include/nvcuvid.h ]]; then
|
||||
fatal "NVDEC-Header weiterhin nicht vorhanden. Prüfe Repository-Konfiguration (universe/multiverse)."
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 3. Alle vorhandenen HandBrake-Installationen entfernen
|
||||
# --------------------------------------------------------------------------
|
||||
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"
|
||||
download_source() {
|
||||
local tarball="$1"
|
||||
local url="https://github.com/HandBrake/HandBrake/releases/download/${HANDBRAKE_VERSION}/HandBrake-${HANDBRAKE_VERSION}-source.tar.bz2"
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 4. Quellcode herunterladen
|
||||
# --------------------------------------------------------------------------
|
||||
TMP_DIR=$(mktemp -d)
|
||||
trap 'cd /; rm -rf "$TMP_DIR"' EXIT
|
||||
|
||||
SRC_URL="https://github.com/HandBrake/HandBrake/releases/download/${HANDBRAKE_VERSION}/HandBrake-${HANDBRAKE_VERSION}-source.tar.bz2"
|
||||
TARBALL="${TMP_DIR}/handbrake-src.tar.bz2"
|
||||
|
||||
info "Lade HandBrake ${HANDBRAKE_VERSION} herunter..."
|
||||
info "URL: ${SRC_URL}"
|
||||
curl -fL --progress-bar "$SRC_URL" -o "$TARBALL" || \
|
||||
wget --progress=bar:force "$SRC_URL" -O "$TARBALL" || \
|
||||
fatal "Download fehlgeschlagen. Bitte Version prüfen: https://github.com/HandBrake/HandBrake/releases"
|
||||
|
||||
info "Entpacke..."
|
||||
tar xjf "$TARBALL" -C "$TMP_DIR"
|
||||
|
||||
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 "Quellverzeichnis nicht gefunden"
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 5. Konfigurieren & Bauen
|
||||
# --------------------------------------------------------------------------
|
||||
cd "$SRC_DIR"
|
||||
|
||||
info "Konfiguriere HandBrake mit NVDEC (--enable-nvdec)..."
|
||||
./configure \
|
||||
--launch-jobs="$(nproc)" \
|
||||
--enable-nvdec \
|
||||
--prefix=/usr/local \
|
||||
2>&1 | tail -15
|
||||
|
||||
info "Baue HandBrake mit $(nproc) Threads – das dauert 10–30 Minuten..."
|
||||
make --directory=build -j"$(nproc)"
|
||||
|
||||
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 ✓"
|
||||
info "Lade HandBrake ${HANDBRAKE_VERSION} Quellcode..."
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fL "$url" -o "$tarball"
|
||||
else
|
||||
warn "NVDEC: nicht in --help gefunden (evtl. kein --enable-nvdec oder kein CUDA-Header)"
|
||||
wget -O "$tarball" "$url"
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
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
|
||||
|
||||
require_cmd nproc
|
||||
require_cmd tar
|
||||
require_cmd dpkg
|
||||
|
||||
cleanup_stale_tmp_build_dirs
|
||||
install_build_dependencies
|
||||
require_cmd make
|
||||
|
||||
local tmp_dir tarball src_dir
|
||||
tmp_dir="$(mktemp -d -p /tmp handbrake-nvdec-build-XXXXXX)"
|
||||
tarball="${tmp_dir}/HandBrake-${HANDBRAKE_VERSION}-source.tar.bz2"
|
||||
src_dir="${tmp_dir}/HandBrake-${HANDBRAKE_VERSION}"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$tmp_dir"
|
||||
rm -f "$OUTPUT_TMP"
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
download_source "$tarball"
|
||||
|
||||
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 \
|
||||
--disable-gtk \
|
||||
--prefix=/usr/local >"$configure_log" 2>&1
|
||||
)
|
||||
|
||||
if ! rg -q 'Enable NVDEC:[[:space:]]+True' "$configure_log"; then
|
||||
tail -n 80 "$configure_log" >&2 || true
|
||||
fatal "Configure hat NVDEC nicht aktiviert (Enable NVDEC != True)."
|
||||
fi
|
||||
|
||||
# Laufzeit-Bibliothek prüfen
|
||||
if ldconfig -p 2>/dev/null | grep -q libnvcuvid; then
|
||||
ok "libnvcuvid: gefunden – NVDEC zur Laufzeit verfügbar ✓"
|
||||
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."
|
||||
if ! rg -q 'Enable NVENC:[[:space:]]+True' "$configure_log"; then
|
||||
tail -n 80 "$configure_log" >&2 || true
|
||||
fatal "Configure hat NVENC nicht aktiviert (Enable NVENC != True)."
|
||||
fi
|
||||
else
|
||||
fatal "HandBrakeCLI nach dem Build nicht gefunden – Build fehlgeschlagen."
|
||||
fi
|
||||
|
||||
rg 'Enable NVENC|Enable NVDEC' "$configure_log" || true
|
||||
|
||||
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 "$@"
|
||||
|
||||
@@ -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 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
|
||||
-- =============================================================================
|
||||
|
||||
@@ -115,11 +115,11 @@ Sendet eine Test-Benachrichtigung über PushOver.
|
||||
|
||||
## 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
|
||||
|
||||
Gibt alle konfigurierten Skripte zurück.
|
||||
Gibt alle Skripte zurück, sortiert nach `orderIndex`.
|
||||
|
||||
**Response:**
|
||||
|
||||
@@ -127,11 +127,12 @@ Gibt alle konfigurierten Skripte zurück.
|
||||
{
|
||||
"scripts": [
|
||||
{
|
||||
"id": "script-abc123",
|
||||
"id": 1,
|
||||
"name": "Zu Plex verschieben",
|
||||
"command": "/home/michael/scripts/move-to-plex.sh",
|
||||
"description": "Verschiebt die fertige Datei ins Plex-Verzeichnis",
|
||||
"createdAt": "2024-01-15T10:00:00.000Z"
|
||||
"scriptBody": "mv \"$RIPSTER_OUTPUT_PATH\" /mnt/plex/movies/",
|
||||
"orderIndex": 1,
|
||||
"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
|
||||
|
||||
Legt ein neues Post-Encode-Skript an.
|
||||
Legt ein neues Skript an.
|
||||
|
||||
**Request:**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Zu Plex verschieben",
|
||||
"command": "/home/michael/scripts/move-to-plex.sh",
|
||||
"description": "Verschiebt die fertige Datei ins Plex-Verzeichnis"
|
||||
"scriptBody": "mv \"$RIPSTER_OUTPUT_PATH\" /mnt/plex/movies/"
|
||||
}
|
||||
```
|
||||
|
||||
| Feld | Typ | Pflicht | Beschreibung |
|
||||
|------|-----|---------|-------------|
|
||||
| `name` | string | ✅ | Anzeigename |
|
||||
| `command` | string | ✅ | Shell-Befehl oder absoluter Skriptpfad |
|
||||
| `description` | string | — | Optionale Beschreibung |
|
||||
| `name` | string | ✅ | Anzeigename (eindeutig) |
|
||||
| `scriptBody` | string | ✅ | Shell-Befehl oder mehrzeiliges Skript |
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"script": {
|
||||
"id": "script-abc123",
|
||||
"name": "Zu Plex verschieben",
|
||||
"command": "/home/michael/scripts/move-to-plex.sh"
|
||||
}
|
||||
}
|
||||
```
|
||||
**Response:** `201 Created` – `{ "script": { ... } }`
|
||||
|
||||
---
|
||||
|
||||
### PUT /api/settings/scripts/:scriptId
|
||||
### PUT /api/settings/scripts/:id
|
||||
|
||||
Aktualisiert ein vorhandenes Skript.
|
||||
|
||||
**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 }`
|
||||
Aktualisiert ein vorhandenes Skript. Alle Felder optional.
|
||||
|
||||
---
|
||||
|
||||
### DELETE /api/settings/scripts/:scriptId
|
||||
### DELETE /api/settings/scripts/:id
|
||||
|
||||
Löscht ein Skript.
|
||||
|
||||
**URL-Parameter:** `scriptId`
|
||||
|
||||
**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.
|
||||
!!! warning "Referenzen"
|
||||
Das Skript wird gelöscht, auch wenn es in Job-Historien referenziert ist. In zukünftigen Reviews erscheint es nicht mehr.
|
||||
|
||||
---
|
||||
|
||||
### POST /api/settings/scripts/:scriptId/test
|
||||
### POST /api/settings/scripts/:id/test
|
||||
|
||||
Führt ein Skript mit Platzhalter-Umgebungsvariablen aus (Testlauf).
|
||||
|
||||
**URL-Parameter:** `scriptId`
|
||||
|
||||
**Response (Erfolg):**
|
||||
**Response:**
|
||||
|
||||
```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:**
|
||||
|
||||
| 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
|
||||
|
||||
Eine vollständige Liste aller Einstellungs-Schlüssel:
|
||||
|
||||
| Schlüssel | Kategorie | Typ | Beschreibung |
|
||||
|---------|----------|-----|-------------|
|
||||
| `raw_dir` | paths | string | Raw-MKV Verzeichnis |
|
||||
| `movie_dir` | paths | string | Ausgabe-Verzeichnis |
|
||||
| `log_dir` | paths | string | Log-Verzeichnis |
|
||||
| `makemkv_command` | tools | string | MakeMKV-Befehl |
|
||||
| `handbrake_command` | tools | string | HandBrake-Befehl |
|
||||
| `mediainfo_command` | tools | string | MediaInfo-Befehl |
|
||||
| `handbrake_preset` | encoding | string | HandBrake-Preset-Name |
|
||||
| `handbrake_extra_args` | encoding | string | Zusatz-Argumente |
|
||||
| `output_extension` | encoding | string | Dateiendung (z.B. `mkv`) |
|
||||
| `filename_template` | encoding | string | Dateiname-Template |
|
||||
| `drive_mode` | drive | select | `auto` oder `explicit` |
|
||||
| `drive_device` | drive | string | Geräte-Pfad |
|
||||
| `disc_poll_interval_ms` | drive | number | Polling-Intervall (ms) |
|
||||
| `makemkv_min_length_minutes` | makemkv | number | Min. Titellänge (Minuten) |
|
||||
| `makemkv_backup_mode` | makemkv | boolean | Backup-Modus aktivieren |
|
||||
| `omdb_api_key` | omdb | string | OMDb API-Key |
|
||||
| `omdb_default_type` | omdb | select | Standard-Suchtyp |
|
||||
| `pushover_user_key` | notifications | string | PushOver User-Key |
|
||||
| `pushover_api_token` | notifications | string | PushOver API-Token |
|
||||
Eine vollständige Übersicht aller Schlüssel:
|
||||
[:octicons-arrow-right-24: Einstellungsreferenz](../configuration/settings-reference.md)
|
||||
|
||||
@@ -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
|
||||
|
||||
Wird gesendet, wenn ein Cron-Job angelegt, aktualisiert oder gelöscht wurde.
|
||||
|
||||
@@ -46,7 +46,7 @@ flowchart LR
|
||||
WUD -->|selectMetadata(selectedPlaylist)| MIC
|
||||
MIC -->|Tracks analysiert| RTE[READY_TO\nENCODE]
|
||||
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 -->|Fehler| ERR([ERROR])
|
||||
RIP -->|Fehler| ERR
|
||||
@@ -84,10 +84,18 @@ Der Service pollt das Laufwerk im konfigurierten Intervall (`disc_poll_interval_
|
||||
|
||||
```js
|
||||
// Ereignisse
|
||||
emit('discInserted', { path: '/dev/sr0' })
|
||||
emit('discInserted', { path: '/dev/sr0', mediaProfile: 'bluray', ... })
|
||||
emit('discRemoved', { path: '/dev/sr0' })
|
||||
```
|
||||
|
||||
### Media-Profil-Erkennung
|
||||
|
||||
Das erkannte Gerät enthält ein `mediaProfile`-Feld (`"bluray"`, `"dvd"`, `"other"` oder `null`). Die Erkennung nutzt eine Heuristik aus drei Quellen (absteigend nach Priorität):
|
||||
|
||||
1. Explizit gesetztes `media_profile` aus den Settings
|
||||
2. Disc-Label und Laufwerks-Modell (Regex gegen bekannte Begriffe)
|
||||
3. Dateisystemtyp: `udf` → bevorzugt DVD, kombiniert mit Modell; `iso9660/cdfs` → DVD oder CD
|
||||
|
||||
---
|
||||
|
||||
## processRunner.js
|
||||
@@ -170,20 +178,50 @@ Verwaltet alle Anwendungseinstellungen.
|
||||
### Features
|
||||
|
||||
- **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
|
||||
- **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
|
||||
|
||||
| Kategorie | Einstellungen |
|
||||
|-----------|--------------|
|
||||
| `Pfade` | `raw_dir`, `movie_dir`, `log_dir` |
|
||||
| Kategorie | Ausgewählte Schlüssel |
|
||||
|-----------|----------------------|
|
||||
| `Pfade` | `raw_dir[_bluray/_dvd/_other]`, `movie_dir[_bluray/_dvd/_other]`, `log_dir` |
|
||||
| `Laufwerk` | `drive_mode`, `drive_device`, `disc_poll_interval_ms`, `makemkv_source_index` |
|
||||
| `Monitoring` | `hardware_monitoring_enabled`, `hardware_monitoring_interval_ms` |
|
||||
| `Tools` | `makemkv_command`, `handbrake_command`, `mediainfo_command`, `pipeline_max_parallel_jobs` |
|
||||
| `Tools – Blu-ray` | `handbrake_preset_bluray`, `makemkv_rip_mode_bluray`, … |
|
||||
| `Tools – DVD` | `handbrake_preset_dvd`, `makemkv_rip_mode_dvd`, … |
|
||||
| `Metadaten` | `omdb_api_key`, `omdb_default_type` |
|
||||
| `Benachrichtigungen` | `pushover_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
|
||||
|
||||
PushOver-Push-Benachrichtigungen.
|
||||
|
||||
@@ -14,6 +14,7 @@ pipeline_state -- Aktueller Pipeline-Zustand (Singleton)
|
||||
scripts -- Shell-Skripte für Pre-/Post-Encode-Ausführung
|
||||
script_chains -- Geordnete Ketten aus mehreren Skripten
|
||||
script_chain_steps -- Einzelschritte einer Skript-Kette
|
||||
user_presets -- Benannte HandBrake-Preset-Sammlungen pro Medientyp
|
||||
cron_jobs -- Zeitgesteuerte Aufgaben (eigener Cron-Parser)
|
||||
cron_run_logs -- Ausführungs-Protokolle für Cron-Jobs
|
||||
```
|
||||
@@ -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
|
||||
|
||||
Speichern den Zeitplan und die Ausführungs-Historie des eingebauten Cron-Systems.
|
||||
|
||||
@@ -1,138 +1,180 @@
|
||||
# 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 |
|
||||
|---------|-----|---------|---------|-------------|
|
||||
| `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 |
|
||||
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:
|
||||
|
||||
!!! example "Beispielkonfiguration"
|
||||
```
|
||||
raw_dir = /mnt/nas/raw
|
||||
movie_dir = /mnt/nas/movies
|
||||
log_dir = /var/log/ripster
|
||||
```
|
||||
| Profil | Erkennungsmerkmale |
|
||||
|--------|--------------------|
|
||||
| `bluray` | UDF-Dateisystem, Laufwerk-Modell enthält „Blu-ray", Disc-Label wie BDMV |
|
||||
| `dvd` | ISO9660/UDF, Laufwerk-Modell enthält „DVD", VIDEO_TS-Struktur |
|
||||
| `other` | Alles andere (CD, unbekannt) |
|
||||
|
||||
**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 |
|
||||
|---------|-----|---------|-------------|
|
||||
|-----------|-----|---------|-------------|
|
||||
| `drive_mode` | select | `auto` | `auto` = automatisch erkennen, `explicit` = festes Gerät |
|
||||
| `drive_device` | string | `/dev/sr0` | Geräte-Pfad (nur bei `explicit`) |
|
||||
| `disc_poll_interval_ms` | number | `4000` | Polling-Intervall in Millisekunden (1000–60000) |
|
||||
| `makemkv_source_index` | number | `0` | Laufwerk-Index für MakeMKV (bei mehreren Laufwerken) |
|
||||
|
||||
---
|
||||
|
||||
## Kategorie: Tools (global)
|
||||
|
||||
| Schlüssel | Typ | Standard | Beschreibung |
|
||||
|-----------|-----|---------|-------------|
|
||||
| `makemkv_command` | string | `makemkvcon` | Befehl oder absoluter Pfad zu MakeMKV |
|
||||
| `handbrake_command` | string | `HandBrakeCLI` | Befehl oder absoluter Pfad zu HandBrake |
|
||||
| `mediainfo_command` | string | `mediainfo` | Befehl oder absoluter Pfad zu MediaInfo |
|
||||
| `makemkv_min_length_minutes` | number | `15` | Mindest-Titellänge in Minuten (0–999) |
|
||||
| `pipeline_max_parallel_jobs` | number | `1` | Maximale Anzahl parallel laufender Jobs (1–12) |
|
||||
| `handbrake_restart_delete_incomplete_output` | boolean | `true` | Unvollständige Ausgabedatei beim Encode-Neustart löschen |
|
||||
|
||||
!!! tip "Absolute Pfade verwenden"
|
||||
Falls die Tools nicht im `PATH` des Systems sind:
|
||||
```
|
||||
makemkv_command = /usr/local/bin/makemkvcon
|
||||
handbrake_command = /usr/local/bin/HandBrakeCLI
|
||||
mediainfo_command = /usr/bin/mediainfo
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Kategorie: Encoding (encoding)
|
||||
### Kategorie: Tools – Blu-ray
|
||||
|
||||
| Schlüssel | Typ | Standard | Beschreibung |
|
||||
|---------|-----|---------|-------------|
|
||||
| `handbrake_preset` | string | `H.265 MKV 1080p30` | Name des HandBrake-Presets |
|
||||
| `handbrake_extra_args` | string | _(leer)_ | Zusätzliche HandBrake CLI-Argumente |
|
||||
| `output_extension` | string | `mkv` | Dateiendung der Ausgabedatei |
|
||||
| `filename_template` | string | `{title} ({year})` | Template für den Dateinamen |
|
||||
|-----------|-----|---------|-------------|
|
||||
| `makemkv_rip_mode_bluray` | select | `backup` | Rip-Modus: `mkv` oder `backup` |
|
||||
| `makemkv_analyze_extra_args_bluray` | string | — | Zusatz-CLI-Parameter für Analyse (Blu-ray) |
|
||||
| `makemkv_rip_extra_args_bluray` | string | — | Zusatz-CLI-Parameter für Rip (Blu-ray) |
|
||||
| `mediainfo_extra_args_bluray` | string | — | Zusatz-CLI-Parameter für mediainfo (Blu-ray) |
|
||||
| `handbrake_preset_bluray` | string | `H.264 MKV 1080p30` | HandBrake-Preset für Blu-rays |
|
||||
| `handbrake_extra_args_bluray` | string | — | Zusatz-CLI-Argumente für HandBrake (Blu-ray) |
|
||||
| `output_extension_bluray` | select | `mkv` | Ausgabeformat: `mkv` oder `mp4` |
|
||||
| `filename_template_bluray` | string | `${title} (${year})` | Dateiname-Template (Blu-ray) |
|
||||
| `output_folder_template_bluray` | string | — | Ordnername-Template (Blu-ray, leer = Dateiname-Template) |
|
||||
|
||||
### 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
|
||||
HandBrakeCLI --preset-list
|
||||
```
|
||||
### Globale Fallback-Einstellungen für Encode
|
||||
|
||||
Häufig verwendete Presets:
|
||||
Diese Werte werden verwendet, wenn kein profil-spezifischer Wert konfiguriert ist:
|
||||
|
||||
| Preset | Beschreibung |
|
||||
|--------|-------------|
|
||||
| `H.265 MKV 1080p30` | H.265/HEVC, Full-HD, 30fps |
|
||||
| `H.265 MKV 720p30` | H.265/HEVC, HD, 30fps |
|
||||
| `H.264 MKV 1080p30` | H.264/AVC, Full-HD, 30fps |
|
||||
| `HQ 1080p30 Surround` | Hohe Qualität, Full-HD mit Surround |
|
||||
| Schlüssel | Typ | Standard | Beschreibung |
|
||||
|-----------|-----|---------|-------------|
|
||||
| `handbrake_preset` | string | `H.265 MKV 1080p30` | Fallback HandBrake-Preset |
|
||||
| `handbrake_extra_args` | string | — | Fallback Extra-Args |
|
||||
| `makemkv_rip_mode` | select | `mkv` | Fallback Rip-Modus |
|
||||
| `makemkv_analyze_extra_args` | string | — | Fallback Analyse-Args |
|
||||
| `makemkv_rip_extra_args` | string | — | Fallback Rip-Args |
|
||||
| `mediainfo_extra_args` | string | — | Fallback MediaInfo-Args |
|
||||
| `output_extension` | select | `mkv` | Fallback Ausgabeformat |
|
||||
| `filename_template` | string | `${title} (${year})` | Fallback Dateiname-Template |
|
||||
| `output_folder_template` | string | — | Fallback Ordnername-Template |
|
||||
|
||||
### Dateiname-Template-Platzhalter
|
||||
### Template-Platzhalter
|
||||
|
||||
| Platzhalter | Beispiel |
|
||||
|------------|---------|
|
||||
| `{title}` | `Inception` |
|
||||
| `{year}` | `2010` |
|
||||
| `{imdb_id}` | `tt1375666` |
|
||||
| `{type}` | `movie` |
|
||||
| `${title}` | `Inception` |
|
||||
| `${year}` | `2010` |
|
||||
| `${imdbId}` | `tt1375666` |
|
||||
|
||||
---
|
||||
|
||||
## Kategorie: Laufwerk (drive)
|
||||
|
||||
| Schlüssel | Typ | Standard | Optionen | Beschreibung |
|
||||
|---------|-----|---------|---------|-------------|
|
||||
| `drive_mode` | select | `auto` | `auto`, `explicit` | Laufwerk-Erkennungsmodus |
|
||||
| `drive_device` | string | `/dev/sr0` | — | Geräte-Pfad (nur bei `explicit`) |
|
||||
| `disc_poll_interval_ms` | number | `4000` | 1000–60000 | Polling-Intervall in Millisekunden |
|
||||
|
||||
**`drive_mode` Optionen:**
|
||||
|
||||
| Modus | Beschreibung |
|
||||
|------|-------------|
|
||||
| `auto` | Ripster erkennt das Laufwerk automatisch |
|
||||
| `explicit` | Verwendet das in `drive_device` konfigurierte Gerät |
|
||||
|
||||
---
|
||||
|
||||
## Kategorie: MakeMKV (makemkv)
|
||||
|
||||
| Schlüssel | Typ | Standard | Min | Max | Beschreibung |
|
||||
|---------|-----|---------|-----|-----|-------------|
|
||||
| `makemkv_min_length_minutes` | number | `15` | `0` | `999` | Mindest-Titellänge in Minuten |
|
||||
| `makemkv_backup_mode` | boolean | `false` | — | — | Backup-Modus statt MKV-Modus |
|
||||
|
||||
**`makemkv_min_length_minutes`:** Titel kürzer als dieser Wert werden von MakeMKV ignoriert. Verhindert das Rippen von Menü-Schleifen und kurzen Extra-Clips.
|
||||
|
||||
**`makemkv_backup_mode`:** Im Backup-Modus erstellt MakeMKV eine vollständige Disc-Kopie mit Menüs. Im Standard-Modus werden direkt MKV-Dateien erstellt.
|
||||
|
||||
---
|
||||
|
||||
## Kategorie: OMDb (omdb)
|
||||
|
||||
| Schlüssel | Typ | Standard | Pflicht | Beschreibung |
|
||||
|---------|-----|---------|---------|-------------|
|
||||
| `omdb_api_key` | string | — | ✅ | API-Key von [omdbapi.com](https://www.omdbapi.com/) |
|
||||
| `omdb_default_type` | select | `movie` | — | Standard-Suchtyp: `movie` oder `series` |
|
||||
|
||||
---
|
||||
|
||||
## Kategorie: Benachrichtigungen (notifications)
|
||||
## Kategorie: Metadaten
|
||||
|
||||
| Schlüssel | Typ | Standard | Beschreibung |
|
||||
|---------|-----|---------|-------------|
|
||||
| `pushover_user_key` | string | — | PushOver User-Key |
|
||||
| `pushover_api_token` | string | — | PushOver API-Token |
|
||||
|-----------|-----|---------|-------------|
|
||||
| `omdb_api_key` | string | — | API-Key von [omdbapi.com](https://www.omdbapi.com/) |
|
||||
| `omdb_default_type` | select | `movie` | Vorauswahl für OMDb-Suche: `movie`, `series`, `episode` |
|
||||
|
||||
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
|
||||
|
||||
Über die Datenbank können Einstellungen auf Standardwerte zurückgesetzt werden:
|
||||
Einen einzelnen Wert über die Datenbank zurücksetzen:
|
||||
|
||||
```bash
|
||||
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.
|
||||
|
||||
@@ -266,6 +266,29 @@ export const api = {
|
||||
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 ──────────────────────────────────────────────────────────────
|
||||
getCronJobs() {
|
||||
return request('/crons');
|
||||
|
||||
@@ -174,7 +174,8 @@ function buildHandBrakeCommandPreview({
|
||||
title,
|
||||
selectedAudioTrackIds,
|
||||
selectedSubtitleTrackIds,
|
||||
commandOutputPath = null
|
||||
commandOutputPath = null,
|
||||
presetOverride = null
|
||||
}) {
|
||||
const inputPath = String(title?.filePath || review?.encodeInputPath || '').trim();
|
||||
const handBrakeCmd = String(
|
||||
@@ -182,8 +183,12 @@ function buildHandBrakeCommandPreview({
|
||||
|| review?.selectors?.handBrakeCommand
|
||||
|| 'HandBrakeCLI'
|
||||
).trim() || 'HandBrakeCLI';
|
||||
const preset = String(review?.selectors?.preset || '').trim();
|
||||
const extraArgs = String(review?.selectors?.extraArgs || '').trim();
|
||||
const preset = presetOverride !== null
|
||||
? 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 mappedTitleId = Number.isFinite(rawMappedTitleId) && rawMappedTitleId > 0
|
||||
? Math.trunc(rawMappedTitleId)
|
||||
@@ -697,7 +702,10 @@ export default function MediaInfoReviewPanel({
|
||||
onAddPostEncodeItem = null,
|
||||
onChangePostEncodeItem = null,
|
||||
onRemovePostEncodeItem = null,
|
||||
onReorderPostEncodeItem = null
|
||||
onReorderPostEncodeItem = null,
|
||||
userPresets = [],
|
||||
selectedUserPresetId = null,
|
||||
onUserPresetChange = null
|
||||
}) {
|
||||
if (!review) {
|
||||
return <p>Keine Mediainfo-Daten vorhanden.</p>;
|
||||
@@ -711,6 +719,18 @@ export default function MediaInfoReviewPanel({
|
||||
const playlistRecommendation = review.playlistRecommendation || null;
|
||||
const rawPreset = String(review.selectors?.preset || '').trim();
|
||||
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 : [])
|
||||
.map((item) => ({
|
||||
id: normalizeScriptId(item?.id),
|
||||
@@ -740,10 +760,56 @@ export default function MediaInfoReviewPanel({
|
||||
|
||||
return (
|
||||
<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><strong>Preset:</strong> {presetLabel}</div>
|
||||
<div><strong>Extra Args:</strong> {review.selectors?.extraArgs || '(keine)'}</div>
|
||||
<div><strong>Preset-Profil:</strong> {review.selectors?.presetProfileSource || '-'}</div>
|
||||
<div>
|
||||
<strong>Preset:</strong>{' '}
|
||||
{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>Encode Input:</strong> {encodeInputTitle?.fileName || '-'}</div>
|
||||
<div><strong>Audio Auswahl:</strong> {review.selectors?.audio?.mode || '-'}</div>
|
||||
@@ -1090,7 +1156,8 @@ export default function MediaInfoReviewPanel({
|
||||
title,
|
||||
selectedAudioTrackIds,
|
||||
selectedSubtitleTrackIds,
|
||||
commandOutputPath
|
||||
commandOutputPath,
|
||||
presetOverride: effectivePresetOverride
|
||||
});
|
||||
return (
|
||||
<div className="handbrake-command-preview">
|
||||
|
||||
@@ -226,6 +226,7 @@ export default function PipelineStatusCard({
|
||||
const reviewConfirmed = Boolean(pipeline?.context?.reviewConfirmed || mediaInfoReview?.reviewConfirmed);
|
||||
const reviewMode = String(mediaInfoReview?.mode || '').trim().toLowerCase();
|
||||
const isPreRipReview = reviewMode === 'pre_rip' || Boolean(mediaInfoReview?.preRip);
|
||||
const jobMediaProfile = String(pipeline?.context?.mediaProfile || '').trim().toLowerCase() || null;
|
||||
const [selectedEncodeTitleId, setSelectedEncodeTitleId] = useState(null);
|
||||
const [selectedPlaylistId, setSelectedPlaylistId] = useState(null);
|
||||
const [trackSelectionByTitle, setTrackSelectionByTitle] = useState({});
|
||||
@@ -236,16 +237,19 @@ export default function PipelineStatusCard({
|
||||
// Unified ordered lists: [{type: 'script'|'chain', id: number}]
|
||||
const [preEncodeItems, setPreEncodeItems] = useState([]);
|
||||
const [postEncodeItems, setPostEncodeItems] = useState([]);
|
||||
const [userPresets, setUserPresets] = useState([]);
|
||||
const [selectedUserPresetId, setSelectedUserPresetId] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const loadSettings = async () => {
|
||||
try {
|
||||
const [settingsResponse, presetsResponse, scriptsResponse, chainsResponse] = await Promise.allSettled([
|
||||
const [settingsResponse, presetsResponse, scriptsResponse, chainsResponse, userPresetsResponse] = await Promise.allSettled([
|
||||
api.getSettings(),
|
||||
api.getHandBrakePresets(),
|
||||
api.getScripts(),
|
||||
api.getScriptChains()
|
||||
api.getScriptChains(),
|
||||
api.getUserPresets()
|
||||
]);
|
||||
if (!cancelled) {
|
||||
const categories = settingsResponse.status === 'fulfilled'
|
||||
@@ -269,6 +273,10 @@ export default function PipelineStatusCard({
|
||||
? (Array.isArray(chainsResponse.value?.chains) ? chainsResponse.value.chains : [])
|
||||
: [];
|
||||
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) {
|
||||
if (!cancelled) {
|
||||
@@ -276,6 +284,7 @@ export default function PipelineStatusCard({
|
||||
setPresetDisplayMap({});
|
||||
setScriptCatalog([]);
|
||||
setChainCatalog([]);
|
||||
setUserPresets([]);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -298,6 +307,7 @@ export default function PipelineStatusCard({
|
||||
...normalizeScriptIdList(mediaInfoReview?.postEncodeScriptIds || []).map((id) => ({ type: 'script', id })),
|
||||
...normChain(mediaInfoReview?.postEncodeChainIds).map((id) => ({ type: 'chain', id }))
|
||||
]);
|
||||
setSelectedUserPresetId(null);
|
||||
}, [mediaInfoReview?.encodeInputTitleId, mediaInfoReview?.generatedAt, retryJobId]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -323,6 +333,14 @@ export default function PipelineStatusCard({
|
||||
const reviewPlaylistDecisionRequired = Boolean(mediaInfoReview?.playlistDecisionRequired);
|
||||
const hasSelectedEncodeTitle = Boolean(normalizeTitleId(selectedEncodeTitleId));
|
||||
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
|
||||
? Boolean(retryJobId)
|
||||
: Boolean(retryJobId && encodeInputPath);
|
||||
@@ -575,7 +593,8 @@ export default function PipelineStatusCard({
|
||||
selectedPostEncodeScriptIds: selectedPostScriptIds,
|
||||
selectedPreEncodeScriptIds: selectedPreScriptIds,
|
||||
selectedPostEncodeChainIds: selectedPostChainIds,
|
||||
selectedPreEncodeChainIds: selectedPreChainIds
|
||||
selectedPreEncodeChainIds: selectedPreChainIds,
|
||||
selectedUserPresetId: selectedUserPresetId || null
|
||||
});
|
||||
}}
|
||||
loading={busy}
|
||||
@@ -786,6 +805,9 @@ export default function PipelineStatusCard({
|
||||
availableChains={chainCatalog}
|
||||
preEncodeItems={preEncodeItems}
|
||||
postEncodeItems={postEncodeItems}
|
||||
userPresets={filteredUserPresets}
|
||||
selectedUserPresetId={selectedUserPresetId}
|
||||
onUserPresetChange={(presetId) => setSelectedUserPresetId(presetId)}
|
||||
allowEncodeItemSelection={state === 'READY_TO_ENCODE' && !reviewConfirmed && !queueLocked}
|
||||
onAddPreEncodeItem={(itemType) => {
|
||||
setPreEncodeItems((prev) => {
|
||||
|
||||
@@ -792,6 +792,7 @@ export default function DashboardPage({
|
||||
selectedPreEncodeScriptIds: startOptions.selectedPreEncodeScriptIds ?? [],
|
||||
selectedPostEncodeChainIds: startOptions.selectedPostEncodeChainIds ?? [],
|
||||
selectedPreEncodeChainIds: startOptions.selectedPreEncodeChainIds ?? [],
|
||||
selectedUserPresetId: startOptions.selectedUserPresetId ?? null,
|
||||
skipPipelineStateUpdate: true
|
||||
});
|
||||
}
|
||||
|
||||
@@ -156,6 +156,21 @@ export default function SettingsPage() {
|
||||
const [chainEditorErrors, setChainEditorErrors] = useState({});
|
||||
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 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 () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -250,6 +350,7 @@ export default function SettingsPage() {
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
loadUserPresets();
|
||||
}, []);
|
||||
|
||||
const dirtyKeys = useMemo(() => {
|
||||
@@ -1379,6 +1480,173 @@ export default function SettingsPage() {
|
||||
</Dialog>
|
||||
</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">
|
||||
<CronJobsTab />
|
||||
</TabPanel>
|
||||
|
||||
362
install-dev.sh
362
install-dev.sh
@@ -17,6 +17,9 @@
|
||||
# --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)
|
||||
# --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)
|
||||
# --reinstall Vorhandene Installation ersetzen (Daten bleiben erhalten)
|
||||
# -h, --help Diese Hilfe anzeigen
|
||||
@@ -44,10 +47,14 @@ FRONTEND_HOST="" # wird automatisch ermittelt, wenn leer
|
||||
SKIP_MAKEMKV=false
|
||||
SKIP_HANDBRAKE=false
|
||||
BUILD_HANDBRAKE_NVDEC=false
|
||||
HANDBRAKE_MODE_SELECTED=false
|
||||
HANDBRAKE_VERSION="1.9.0"
|
||||
HANDBRAKE_UPDATE_POLICY="keep"
|
||||
HANDBRAKE_UPDATE_POLICY_SELECTED=false
|
||||
SKIP_NGINX=false
|
||||
REINSTALL=false
|
||||
SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
HANDBRAKE_SELFBUILD_MARKER="/usr/local/share/ripster/handbrake-selfbuild.env"
|
||||
|
||||
# --- Argumente parsen ---------------------------------------------------------
|
||||
while [[ $# -gt 0 ]]; do
|
||||
@@ -58,8 +65,14 @@ while [[ $# -gt 0 ]]; do
|
||||
--host) FRONTEND_HOST="$2"; shift 2 ;;
|
||||
--no-makemkv) SKIP_MAKEMKV=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-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 ;;
|
||||
--reinstall) REINSTALL=true; shift ;;
|
||||
-h|--help)
|
||||
@@ -183,46 +196,99 @@ install_makemkv() {
|
||||
warn "Beta-Key: https://www.makemkv.com/forum/viewtopic.php?t=1053"
|
||||
}
|
||||
|
||||
remove_all_handbrake() {
|
||||
info "Entferne alle vorhandenen HandBrake-Installationen..."
|
||||
handbrake_has_nvdec() {
|
||||
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
|
||||
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
|
||||
local found
|
||||
found=$(command -v HandBrakeCLI 2>/dev/null || true)
|
||||
[[ -z "$found" ]] && break
|
||||
warn "Entferne: $found"
|
||||
rm -f "$found"
|
||||
done
|
||||
|
||||
if [[ -e /usr/local/bin/HandBrakeCLI ]] && ! handbrake_is_self_built "/usr/local/bin/HandBrakeCLI"; then
|
||||
warn "Entferne fremdes /usr/local/bin/HandBrakeCLI"
|
||||
rm -f /usr/local/bin/HandBrakeCLI
|
||||
fi
|
||||
|
||||
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
|
||||
ok "Alte HandBrake-Installation(en) entfernt"
|
||||
}
|
||||
|
||||
build_handbrake_nvdec() {
|
||||
header "HandBrake ${HANDBRAKE_VERSION} mit NVDEC aus Quellcode bauen"
|
||||
|
||||
local tmp_dir
|
||||
tmp_dir=$(mktemp -d)
|
||||
local cache_dir="/var/cache/ripster/handbrake"
|
||||
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
|
||||
remove_all_handbrake
|
||||
if [[ -t 0 && -d "$cache_dir" ]]; then
|
||||
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
|
||||
info "Installiere Build-Abhängigkeiten..."
|
||||
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 \
|
||||
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 \
|
||||
libopus-dev libsamplerate0-dev libspeex-dev libtheora-dev libtool libtool-bin libva-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
|
||||
|
||||
# CUDA Toolkit für NVDEC-Header
|
||||
@@ -242,35 +308,109 @@ build_handbrake_nvdec() {
|
||||
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})"
|
||||
if [[ ! -d "$src_dir" ]]; then
|
||||
if [[ ! -f "$tarball" ]]; then
|
||||
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})"
|
||||
else
|
||||
info "Nutze vorhandenes HandBrake-Source-Archiv: $tarball"
|
||||
fi
|
||||
|
||||
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"
|
||||
info "Entpacke Quellcode..."
|
||||
tar xjf "$tarball" -C "$cache_dir"
|
||||
[[ -d "$src_dir" ]] || src_dir=$(find "$cache_dir" -maxdepth 1 -type d -name "HandBrake*" | head -1)
|
||||
[[ -d "$src_dir" ]] || fatal "HandBrake-Quellverzeichnis nicht gefunden in $cache_dir"
|
||||
else
|
||||
info "Nutze vorhandenen HandBrake-Source-Baum: $src_dir"
|
||||
fi
|
||||
|
||||
cd "$src_dir"
|
||||
|
||||
info "Konfiguriere HandBrake mit NVDEC..."
|
||||
./configure --launch-jobs="$(nproc)" --enable-nvdec --prefix=/usr/local 2>&1 | tail -10
|
||||
local configure_log="${src_dir}/build/configure-ripster.log"
|
||||
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
|
||||
)
|
||||
|
||||
info "Baue HandBrake ($(nproc) Threads – bitte warten)..."
|
||||
make --directory=build -j"$(nproc)"
|
||||
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
|
||||
|
||||
info "Installiere HandBrake nach /usr/local/bin..."
|
||||
make --directory=build install
|
||||
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)..."
|
||||
make --directory=build -j"$(nproc)"
|
||||
info "Installiere HandBrake nach /usr/local/bin..."
|
||||
make --directory=build install
|
||||
fi
|
||||
|
||||
cd /
|
||||
rm -rf "$tmp_dir"
|
||||
hash -r 2>/dev/null || true
|
||||
|
||||
if command_exists HandBrakeCLI; then
|
||||
local ver
|
||||
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}"
|
||||
if ldconfig -p 2>/dev/null | grep -q libnvcuvid; then
|
||||
ok "libnvcuvid gefunden – NVDEC ist zur Laufzeit verfügbar."
|
||||
@@ -292,80 +432,108 @@ has_nvidia_gpu() {
|
||||
install_handbrake() {
|
||||
header "HandBrake CLI installieren"
|
||||
|
||||
# NVIDIA-GPU vorhanden? → immer NVDEC-Build erzwingen
|
||||
if has_nvidia_gpu; then
|
||||
info "NVIDIA-GPU erkannt – HandBrake wird mit NVDEC aus Quellcode gebaut."
|
||||
BUILD_HANDBRAKE_NVDEC=true
|
||||
local hb_path
|
||||
local current_mode="none"
|
||||
hb_path=$(command -v HandBrakeCLI 2>/dev/null || true)
|
||||
if [[ -n "$hb_path" ]]; then
|
||||
if handbrake_has_nvdec; then
|
||||
current_mode="nvdec"
|
||||
else
|
||||
current_mode="standard"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --build-handbrake oder NVIDIA erkannt: aus Quellcode mit NVDEC bauen
|
||||
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
|
||||
return
|
||||
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
|
||||
local ver
|
||||
ver=$(HandBrakeCLI --version 2>&1 | head -1)
|
||||
ok "HandBrakeCLI bereits installiert: ${ver}"
|
||||
ok "HandBrakeCLI installiert: $(HandBrakeCLI --version 2>&1 | head -1)"
|
||||
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
|
||||
ok "HandBrakeCLI installiert (Standard-Repos)"
|
||||
if command_exists handbrake-cli; then
|
||||
ln -sf "$(command -v handbrake-cli)" /usr/local/bin/HandBrakeCLI
|
||||
hash -r 2>/dev/null || true
|
||||
ok "HandBrakeCLI Alias angelegt auf: $(command -v handbrake-cli)"
|
||||
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
|
||||
|
||||
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"
|
||||
fatal "HandBrake wurde installiert, aber kein CLI-Befehl wurde gefunden."
|
||||
}
|
||||
|
||||
# --- apt-Hilfsfunktionen ------------------------------------------------------
|
||||
|
||||
252
install.sh
252
install.sh
@@ -20,8 +20,6 @@
|
||||
# --host <hostname> Hostname/IP für die Weboberfläche (Standard: Maschinen-IP)
|
||||
# --no-makemkv MakeMKV-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
|
||||
# --reinstall Vorhandene Installation aktualisieren (Daten bleiben erhalten)
|
||||
# -h, --help Diese Hilfe anzeigen
|
||||
@@ -29,6 +27,8 @@
|
||||
set -euo pipefail
|
||||
|
||||
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 -------------------------------------------------------------------
|
||||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
|
||||
@@ -51,8 +51,7 @@ BACKEND_PORT="3001"
|
||||
FRONTEND_HOST=""
|
||||
SKIP_MAKEMKV=false
|
||||
SKIP_HANDBRAKE=false
|
||||
BUILD_HANDBRAKE_NVDEC=false
|
||||
HANDBRAKE_VERSION="1.9.0"
|
||||
HANDBRAKE_INSTALL_MODE=""
|
||||
SKIP_NGINX=false
|
||||
REINSTALL=false
|
||||
|
||||
@@ -66,8 +65,6 @@ while [[ $# -gt 0 ]]; do
|
||||
--host) FRONTEND_HOST="$2"; shift 2 ;;
|
||||
--no-makemkv) SKIP_MAKEMKV=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 ;;
|
||||
--reinstall) REINSTALL=true; shift ;;
|
||||
-h|--help)
|
||||
@@ -188,201 +185,81 @@ install_makemkv() {
|
||||
warn "Beta-Key: https://www.makemkv.com/forum/viewtopic.php?t=1053"
|
||||
}
|
||||
|
||||
remove_all_handbrake() {
|
||||
info "Entferne alle vorhandenen HandBrake-Installationen..."
|
||||
# apt
|
||||
apt-get remove -y handbrake-cli handbrake 2>/dev/null || true
|
||||
# snap
|
||||
snap remove handbrake-cli 2>/dev/null || true
|
||||
# bekannte Binär-Pfade
|
||||
rm -f /usr/bin/HandBrakeCLI \
|
||||
/usr/local/bin/HandBrakeCLI \
|
||||
/snap/bin/handbrake-cli \
|
||||
/snap/bin/HandBrakeCLI
|
||||
# alle weiteren Fundstellen über PATH
|
||||
while true; do
|
||||
local found
|
||||
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"
|
||||
select_handbrake_mode() {
|
||||
[[ "$SKIP_HANDBRAKE" == true ]] && return
|
||||
|
||||
local mode_answer=""
|
||||
echo ""
|
||||
echo "Install HandBrake:"
|
||||
echo ""
|
||||
echo "1. Standard version (apt install handbrake-cli)"
|
||||
echo "2. GPU version with NVDEC (use bundled binary)"
|
||||
|
||||
if [[ -t 0 ]]; then
|
||||
read -r -p "Select option [1/2]: " mode_answer
|
||||
elif [[ -r /dev/tty ]]; then
|
||||
read -r -p "Select option [1/2]: " mode_answer </dev/tty
|
||||
else
|
||||
HANDBRAKE_INSTALL_MODE="standard"
|
||||
warn "Kein interaktives Terminal erkannt – verwende Standardversion (apt)."
|
||||
return
|
||||
fi
|
||||
|
||||
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() {
|
||||
header "HandBrake ${HANDBRAKE_VERSION} mit NVDEC aus Quellcode bauen"
|
||||
|
||||
local tmp_dir
|
||||
tmp_dir=$(mktemp -d)
|
||||
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"
|
||||
|
||||
# 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 10–30 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"
|
||||
install_handbrake_standard() {
|
||||
info "Installiere HandBrakeCLI aus den Standard-Repositories..."
|
||||
info "Aktualisiere Paketlisten..."
|
||||
apt_update
|
||||
apt-get install -y handbrake-cli
|
||||
hash -r 2>/dev/null || true
|
||||
|
||||
if command_exists HandBrakeCLI; then
|
||||
local ver
|
||||
ver=$(HandBrakeCLI --version 2>&1 | head -1)
|
||||
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
|
||||
else
|
||||
fatal "HandBrakeCLI nach dem Build nicht gefunden – Build fehlgeschlagen."
|
||||
ok "HandBrakeCLI installiert: $(HandBrakeCLI --version 2>&1 | head -1)"
|
||||
return
|
||||
fi
|
||||
|
||||
if command_exists handbrake-cli; then
|
||||
ok "handbrake-cli installiert: $(handbrake-cli --version 2>&1 | head -1)"
|
||||
return
|
||||
fi
|
||||
|
||||
fatal "HandBrake wurde installiert, aber kein CLI-Befehl wurde gefunden."
|
||||
}
|
||||
|
||||
handbrake_has_nvdec() {
|
||||
command_exists HandBrakeCLI || return 1
|
||||
HandBrakeCLI --help 2>&1 | grep -qi "nvdec"
|
||||
install_handbrake_gpu_bundled() {
|
||||
info "Installiere gebündeltes HandBrakeCLI mit 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() {
|
||||
header "HandBrake CLI installieren"
|
||||
|
||||
# Bereits installiert MIT NVDEC → nichts tun
|
||||
if handbrake_has_nvdec; then
|
||||
ok "HandBrakeCLI mit NVDEC bereits installiert: $(HandBrakeCLI --version 2>&1 | head -1)"
|
||||
return
|
||||
if [[ -z "$HANDBRAKE_INSTALL_MODE" ]]; then
|
||||
HANDBRAKE_INSTALL_MODE="standard"
|
||||
fi
|
||||
|
||||
# Installiert OHNE NVDEC → entfernen und NVDEC-Build erzwingen
|
||||
if command_exists HandBrakeCLI; then
|
||||
warn "HandBrakeCLI ohne NVDEC gefunden – wird ersetzt durch NVDEC-Build."
|
||||
BUILD_HANDBRAKE_NVDEC=true
|
||||
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; }
|
||||
;;
|
||||
case "$HANDBRAKE_INSTALL_MODE" in
|
||||
standard) install_handbrake_standard ;;
|
||||
gpu) install_handbrake_gpu_bundled ;;
|
||||
*) fatal "Unbekannter HandBrake-Modus: $HANDBRAKE_INSTALL_MODE" ;;
|
||||
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 ------------------------------------------------------
|
||||
@@ -463,6 +340,9 @@ EOF
|
||||
fi
|
||||
}
|
||||
|
||||
# --- HandBrake-Installmodus auswählen ----------------------------------------
|
||||
select_handbrake_mode
|
||||
|
||||
# --- Systemabhängigkeiten -----------------------------------------------------
|
||||
header "Systemabhängigkeiten installieren"
|
||||
|
||||
|
||||
133
kill.sh
133
kill.sh
@@ -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 "$@"
|
||||
Reference in New Issue
Block a user