From 8e3c67565dd38dd36482528a28b015caadb0eeeb Mon Sep 17 00:00:00 2001 From: mboehmlaender Date: Mon, 9 Mar 2026 13:28:21 +0000 Subject: [PATCH] Test --- backend/src/db/database.js | 36 ++++ backend/src/services/pipelineService.js | 123 +++++++++----- backend/src/services/settingsService.js | 4 +- build-handbrake-nvdec.sh | 159 ++++++++++++++++++ db/schema.sql | 4 +- .../src/components/MediaInfoReviewPanel.jsx | 7 +- .../src/components/PipelineStatusCard.jsx | 6 +- frontend/src/pages/SettingsPage.jsx | 6 +- install-dev.sh | 130 ++++++++++++-- install.sh | 139 +++++++++++++-- 10 files changed, 536 insertions(+), 78 deletions(-) create mode 100755 build-handbrake-nvdec.sh diff --git a/backend/src/db/database.js b/backend/src/db/database.js index c610b72..aa6293b 100644 --- a/backend/src/db/database.js +++ b/backend/src/db/database.js @@ -523,6 +523,7 @@ async function openAndPrepareDatabase() { await seedFromSchemaFile(dbInstance); await migrateLegacyProfiledToolSettings(dbInstance); await removeDeprecatedSettings(dbInstance); + await migrateSettingsSchemaMetadata(dbInstance); await ensurePipelineStateRow(dbInstance); const syncedLogRoot = await configureRuntimeLogRootFromSettings(dbInstance, { ensure: true }); logger.info('log-root:synced', { @@ -677,6 +678,41 @@ async function removeDeprecatedSettings(db) { } } +// Aktualisiert settings_schema-Metadaten (required, description, validation_json) +// für bestehende Einträge, ohne user-konfigurierte Werte in settings_values anzutasten. +const SETTINGS_SCHEMA_METADATA_UPDATES = [ + { + key: 'handbrake_preset_bluray', + required: 0, + description: 'Preset Name für -Z (Blu-ray). Leer = kein Preset, nur CLI-Parameter werden verwendet.', + validation_json: '{}' + }, + { + key: 'handbrake_preset_dvd', + required: 0, + description: 'Preset Name für -Z (DVD). Leer = kein Preset, nur CLI-Parameter werden verwendet.', + validation_json: '{}' + } +]; + +async function migrateSettingsSchemaMetadata(db) { + for (const update of SETTINGS_SCHEMA_METADATA_UPDATES) { + const result = await db.run( + `UPDATE settings_schema + SET required = ?, description = ?, validation_json = ?, updated_at = CURRENT_TIMESTAMP + WHERE key = ? AND (required != ? OR description != ? OR validation_json != ?)`, + [ + update.required, update.description, update.validation_json, + update.key, + update.required, update.description, update.validation_json + ] + ); + if (result?.changes > 0) { + logger.info('migrate:settings-schema-metadata', { key: update.key }); + } + } +} + async function getDb() { return initDatabase(); } diff --git a/backend/src/services/pipelineService.js b/backend/src/services/pipelineService.js index bd592da..77b8f2e 100644 --- a/backend/src/services/pipelineService.js +++ b/backend/src/services/pipelineService.js @@ -2633,14 +2633,16 @@ class PipelineService extends EventEmitter { return String(mkInfo?.status || '').trim().toUpperCase() === 'SUCCESS'; } - resolveCurrentRawPath(rawBaseDir, storedRawPath) { + resolveCurrentRawPath(rawBaseDir, storedRawPath, extraBaseDirs = []) { const stored = String(storedRawPath || '').trim(); if (!stored) { return null; } + const folderName = path.basename(stored); const candidates = [stored]; - if (rawBaseDir) { - const byFolder = path.join(rawBaseDir, path.basename(stored)); + const allBaseDirs = [rawBaseDir, ...extraBaseDirs].filter(Boolean); + for (const baseDir of allBaseDirs) { + const byFolder = path.join(baseDir, folderName); if (!candidates.includes(byFolder)) { candidates.push(byFolder); } @@ -2660,7 +2662,13 @@ class PipelineService extends EventEmitter { async migrateRawFolderNamingOnStartup(db) { const settings = await settingsService.getSettingsMap(); const rawBaseDir = String(settings?.raw_dir || '').trim(); - if (!rawBaseDir || !fs.existsSync(rawBaseDir)) { + const rawExtraDirs = [ + settings?.raw_dir_bluray, + settings?.raw_dir_dvd, + settings?.raw_dir_other + ].map((d) => String(d || '').trim()).filter(Boolean); + const allRawDirs = [rawBaseDir, ...rawExtraDirs].filter((d) => d && fs.existsSync(d)); + if (allRawDirs.length === 0) { return; } @@ -2680,40 +2688,42 @@ class PipelineService extends EventEmitter { let missingCount = 0; const discoveredByJobId = new Map(); - try { - const dirEntries = fs.readdirSync(rawBaseDir, { withFileTypes: true }); - for (const entry of dirEntries) { - if (!entry.isDirectory()) { - continue; - } - const match = String(entry.name || '').match(/-\s*RAW\s*-\s*job-(\d+)\s*$/i); - if (!match) { - continue; - } - const mappedJobId = Number(match[1]); - if (!Number.isFinite(mappedJobId) || mappedJobId <= 0) { - continue; - } - const candidatePath = path.join(rawBaseDir, entry.name); - let mtimeMs = 0; - try { - mtimeMs = Number(fs.statSync(candidatePath).mtimeMs || 0); - } catch (_error) { - // ignore fs errors and keep zero mtime - } - const current = discoveredByJobId.get(mappedJobId); - if (!current || mtimeMs > current.mtimeMs) { - discoveredByJobId.set(mappedJobId, { - path: candidatePath, - mtimeMs - }); + for (const scanDir of allRawDirs) { + try { + const dirEntries = fs.readdirSync(scanDir, { withFileTypes: true }); + for (const entry of dirEntries) { + if (!entry.isDirectory()) { + continue; + } + const match = String(entry.name || '').match(/-\s*RAW\s*-\s*job-(\d+)\s*$/i); + if (!match) { + continue; + } + const mappedJobId = Number(match[1]); + if (!Number.isFinite(mappedJobId) || mappedJobId <= 0) { + continue; + } + const candidatePath = path.join(scanDir, entry.name); + let mtimeMs = 0; + try { + mtimeMs = Number(fs.statSync(candidatePath).mtimeMs || 0); + } catch (_error) { + // ignore fs errors and keep zero mtime + } + const current = discoveredByJobId.get(mappedJobId); + if (!current || mtimeMs > current.mtimeMs) { + discoveredByJobId.set(mappedJobId, { + path: candidatePath, + mtimeMs + }); + } } + } catch (scanError) { + logger.warn('startup:raw-dir-migrate:scan-failed', { + scanDir, + error: errorToMeta(scanError) + }); } - } catch (scanError) { - logger.warn('startup:raw-dir-migrate:scan-failed', { - rawBaseDir, - error: errorToMeta(scanError) - }); } for (const row of rows) { @@ -2728,7 +2738,7 @@ class PipelineService extends EventEmitter { ripFlagUpdateCount += 1; } - const currentRawPath = this.resolveCurrentRawPath(rawBaseDir, row.raw_path) + const currentRawPath = this.resolveCurrentRawPath(rawBaseDir, row.raw_path, rawExtraDirs) || discoveredByJobId.get(jobId)?.path || null; if (!currentRawPath) { @@ -2736,6 +2746,8 @@ class PipelineService extends EventEmitter { continue; } + // Keep renamed folder in the same base dir as the current path + const currentBaseDir = path.dirname(currentRawPath); const currentFolderName = path.basename(currentRawPath).replace(/^Incomplete_/i, '').trim(); const folderYearMatch = currentFolderName.match(/\((19|20)\d{2}\)/); const fallbackYear = folderYearMatch @@ -2748,7 +2760,7 @@ class PipelineService extends EventEmitter { }, jobId); const shouldBeIncomplete = !isJobFinished(row); const desiredRawPath = path.join( - rawBaseDir, + currentBaseDir, buildRawDirName(metadataBase, jobId, { incomplete: shouldBeIncomplete }) ); @@ -2791,7 +2803,7 @@ class PipelineService extends EventEmitter { ripFlagUpdateCount, conflictCount, missingCount, - rawBaseDir + scannedDirs: allRawDirs }); } } @@ -3814,7 +3826,18 @@ class PipelineService extends EventEmitter { }; } - if (!job.raw_path || !fs.existsSync(job.raw_path)) { + const refreshSettings = await settingsService.getSettingsMap(); + const refreshRawBaseDir = String(refreshSettings?.raw_dir || '').trim(); + const refreshRawExtraDirs = [ + refreshSettings?.raw_dir_bluray, + refreshSettings?.raw_dir_dvd, + refreshSettings?.raw_dir_other + ].map((d) => String(d || '').trim()).filter(Boolean); + const resolvedRefreshRawPath = job.raw_path + ? this.resolveCurrentRawPath(refreshRawBaseDir, job.raw_path, refreshRawExtraDirs) + : null; + + if (!resolvedRefreshRawPath) { return { triggered: false, reason: 'raw_path_missing', @@ -3824,6 +3847,10 @@ class PipelineService extends EventEmitter { }; } + if (resolvedRefreshRawPath !== job.raw_path) { + await historyService.updateJob(activeJobId, { raw_path: resolvedRefreshRawPath }); + } + const existingPlan = this.safeParseJson(job.encode_plan_json); const mode = existingPlan?.mode || this.snapshot.context?.mode || 'rip'; const sourceJobId = existingPlan?.sourceJobId || this.snapshot.context?.sourceJobId || null; @@ -3834,7 +3861,7 @@ class PipelineService extends EventEmitter { `Settings gespeichert (${relevantKeys.join(', ')}). Titel-/Spurprüfung wird mit aktueller Konfiguration neu gestartet.` ); - this.runReviewForRawJob(activeJobId, job.raw_path, { mode, sourceJobId }).catch((error) => { + this.runReviewForRawJob(activeJobId, resolvedRefreshRawPath, { mode, sourceJobId }).catch((error) => { logger.error('settings:refresh-review:failed', { jobId: activeJobId, relevantKeys, @@ -5659,7 +5686,12 @@ class PipelineService extends EventEmitter { const reencodeSettings = await settingsService.getSettingsMap(); const reencodeRawBaseDir = String(reencodeSettings?.raw_dir || '').trim(); - const resolvedReencodeRawPath = this.resolveCurrentRawPath(reencodeRawBaseDir, sourceJob.raw_path); + const reencodeRawExtraDirs = [ + reencodeSettings?.raw_dir_bluray, + reencodeSettings?.raw_dir_dvd, + reencodeSettings?.raw_dir_other + ].map((d) => String(d || '').trim()).filter(Boolean); + const resolvedReencodeRawPath = this.resolveCurrentRawPath(reencodeRawBaseDir, sourceJob.raw_path, reencodeRawExtraDirs); if (!resolvedReencodeRawPath) { const error = new Error(`Re-Encode nicht möglich: RAW-Pfad existiert nicht (${sourceJob.raw_path}).`); error.statusCode = 400; @@ -7420,7 +7452,12 @@ class PipelineService extends EventEmitter { const reviewSettings = await settingsService.getSettingsMap(); const reviewRawBaseDir = String(reviewSettings?.raw_dir || '').trim(); - const resolvedReviewRawPath = this.resolveCurrentRawPath(reviewRawBaseDir, sourceJob.raw_path); + const reviewRawExtraDirs = [ + reviewSettings?.raw_dir_bluray, + reviewSettings?.raw_dir_dvd, + reviewSettings?.raw_dir_other + ].map((d) => String(d || '').trim()).filter(Boolean); + const resolvedReviewRawPath = this.resolveCurrentRawPath(reviewRawBaseDir, sourceJob.raw_path, reviewRawExtraDirs); if (!resolvedReviewRawPath) { const error = new Error(`Review-Neustart nicht möglich: RAW-Pfad existiert nicht (${sourceJob.raw_path}).`); error.statusCode = 400; diff --git a/backend/src/services/settingsService.js b/backend/src/services/settingsService.js index 525c695..5555208 100644 --- a/backend/src/services/settingsService.js +++ b/backend/src/services/settingsService.js @@ -847,7 +847,9 @@ class SettingsService { if (selectedTitleId !== null) { baseArgs.push('-t', String(selectedTitleId)); } - baseArgs.push('-Z', map.handbrake_preset); + if (map.handbrake_preset) { + baseArgs.push('-Z', map.handbrake_preset); + } const extra = splitArgs(map.handbrake_extra_args); const rawSelection = options?.trackSelection || null; const hasSelection = rawSelection && typeof rawSelection === 'object'; diff --git a/build-handbrake-nvdec.sh b/build-handbrake-nvdec.sh new file mode 100755 index 0000000..7aa54d0 --- /dev/null +++ b/build-handbrake-nvdec.sh @@ -0,0 +1,159 @@ +#!/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; } + +HANDBRAKE_VERSION="1.9.0" + +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 + +[[ $EUID -eq 0 ]] || fatal "Bitte als root ausführen: sudo bash $0" + +[[ -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" + else + warn "nvidia-cuda-toolkit nicht in Standard-Repos – versuche NVIDIA CUDA Repo..." + local_ver="${VERSION_ID//./}" + cuda_deb="/tmp/cuda-keyring.deb" + if curl -fsSL \ + "https://developer.download.nvidia.com/compute/cuda/repos/ubuntu${local_ver}/x86_64/cuda-keyring_1.1-1_all.deb" \ + -o "$cuda_deb" 2>/dev/null; then + dpkg -i "$cuda_deb" + apt-get update -qq + # Minimale Header statt vollem Toolkit + apt-get install -y cuda-cudart-dev-12-8 >/dev/null 2>&1 && \ + ok "CUDA-Header installiert (cuda-cudart-dev-12-8)" || \ + warn "CUDA-Header-Installation fehlgeschlagen – NVDEC könnte im Build fehlen." + else + warn "NVIDIA CUDA Repo nicht erreichbar – NVDEC könnte im Build fehlen." + fi + fi +fi + +# -------------------------------------------------------------------------- +# 3. Alte Installation entfernen +# -------------------------------------------------------------------------- +if command -v HandBrakeCLI &>/dev/null; then + EXISTING=$(HandBrakeCLI --version 2>&1 | head -1) + warn "Entferne vorhandenes HandBrakeCLI: ${EXISTING}" + apt-get remove -y handbrake-cli 2>/dev/null || true + snap remove handbrake-cli 2>/dev/null || true + rm -f /usr/bin/HandBrakeCLI /usr/local/bin/HandBrakeCLI +fi + +# -------------------------------------------------------------------------- +# 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 ✓" + else + warn "NVDEC: nicht in --help gefunden (evtl. kein --enable-nvdec oder kein CUDA-Header)" + 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." + fi +else + fatal "HandBrakeCLI nach dem Build nicht gefunden – Build fehlgeschlagen." +fi diff --git a/db/schema.sql b/db/schema.sql index 86dd53b..61b036a 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -271,7 +271,7 @@ VALUES ('makemkv_rip_extra_args_bluray', 'Tools', 'MakeMKV Rip Extra Args', 'str INSERT OR IGNORE INTO settings_values (key, value) VALUES ('makemkv_rip_extra_args_bluray', NULL); INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) -VALUES ('handbrake_preset_bluray', 'Tools', 'HandBrake Preset', 'string', 1, 'Preset Name für -Z (Blu-ray).', 'H.264 MKV 1080p30', '[]', '{"minLength":1}', 320); +VALUES ('handbrake_preset_bluray', 'Tools', 'HandBrake Preset', 'string', 0, 'Preset Name für -Z (Blu-ray). Leer = kein Preset, nur CLI-Parameter werden verwendet.', 'H.264 MKV 1080p30', '[]', '{}', 320); INSERT OR IGNORE INTO settings_values (key, value) VALUES ('handbrake_preset_bluray', 'H.264 MKV 1080p30'); INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) @@ -308,7 +308,7 @@ VALUES ('makemkv_rip_extra_args_dvd', 'Tools', 'MakeMKV Rip Extra Args', 'string INSERT OR IGNORE INTO settings_values (key, value) VALUES ('makemkv_rip_extra_args_dvd', NULL); INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) -VALUES ('handbrake_preset_dvd', 'Tools', 'HandBrake Preset', 'string', 1, 'Preset Name für -Z (DVD).', 'H.264 MKV 480p30', '[]', '{"minLength":1}', 520); +VALUES ('handbrake_preset_dvd', 'Tools', 'HandBrake Preset', 'string', 0, 'Preset Name für -Z (DVD). Leer = kein Preset, nur CLI-Parameter werden verwendet.', 'H.264 MKV 480p30', '[]', '{}', 520); INSERT OR IGNORE INTO settings_values (key, value) VALUES ('handbrake_preset_dvd', 'H.264 MKV 480p30'); INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) diff --git a/frontend/src/components/MediaInfoReviewPanel.jsx b/frontend/src/components/MediaInfoReviewPanel.jsx index c734dcc..2042c70 100644 --- a/frontend/src/components/MediaInfoReviewPanel.jsx +++ b/frontend/src/components/MediaInfoReviewPanel.jsx @@ -709,7 +709,8 @@ export default function MediaInfoReviewPanel({ const processedFiles = Number(review.processedFiles || titles.length || 0); const totalFiles = Number(review.totalFiles || titles.length || 0); const playlistRecommendation = review.playlistRecommendation || null; - const presetLabel = String(presetDisplayValue || review.selectors?.preset || '').trim() || '-'; + const rawPreset = String(review.selectors?.preset || '').trim(); + const presetLabel = String(presetDisplayValue || rawPreset).trim() || '(kein Preset)'; const scriptCatalog = (Array.isArray(availableScripts) ? availableScripts : []) .map((item) => ({ id: normalizeScriptId(item?.id), @@ -974,7 +975,9 @@ export default function MediaInfoReviewPanel({ ) : titles.map((title) => { const titleEligible = title?.eligibleForEncode !== false; const titleChecked = allowTitleSelection - ? currentSelectedId === normalizeTitleId(title.id) + ? (currentSelectedId !== null + ? currentSelectedId === normalizeTitleId(title.id) + : Boolean(title.selectedForEncode)) : Boolean(title.selectedForEncode); const titleSelectionEntry = trackSelectionByTitle?.[title.id] || trackSelectionByTitle?.[String(title.id)] || {}; const subtitleTracks = Array.isArray(title.subtitleTracks) ? title.subtitleTracks : []; diff --git a/frontend/src/components/PipelineStatusCard.jsx b/frontend/src/components/PipelineStatusCard.jsx index c81a5cc..f3d655c 100644 --- a/frontend/src/components/PipelineStatusCard.jsx +++ b/frontend/src/components/PipelineStatusCard.jsx @@ -427,7 +427,11 @@ export default function PipelineStatusCard({ return presetDisplayMap[preset] || preset; }, [mediaInfoReview?.selectors?.preset, presetDisplayMap]); const buildSelectedTrackSelectionForCurrentTitle = () => { - const encodeTitleId = normalizeTitleId(selectedEncodeTitleId); + const encodeTitleId = normalizeTitleId(selectedEncodeTitleId) + || normalizeTitleId(mediaInfoReview?.encodeInputTitleId) + || normalizeTitleId( + (Array.isArray(mediaInfoReview?.titles) ? mediaInfoReview.titles : []).find((t) => t?.selectedForEncode)?.id + ); const selectionEntry = encodeTitleId ? (trackSelectionByTitle?.[encodeTitleId] || trackSelectionByTitle?.[String(encodeTitleId)] || null) : null; diff --git a/frontend/src/pages/SettingsPage.jsx b/frontend/src/pages/SettingsPage.jsx index 51fa9a8..98c9329 100644 --- a/frontend/src/pages/SettingsPage.jsx +++ b/frontend/src/pages/SettingsPage.jsx @@ -93,6 +93,10 @@ function injectHandBrakePresetOptions(categories, presetPayload) { }); }; + // "(kein Preset)" immer als erste Option — ermöglicht reinen CLI-Betrieb + normalizedOptions.push({ label: '(kein Preset – nur CLI-Parameter)', value: '', disabled: false }); + seenValues.add(''); + for (const option of sourceOptions) { if (option?.disabled) { addGroupOption(option); @@ -103,7 +107,7 @@ function injectHandBrakePresetOptions(categories, presetPayload) { addSelectableOption(setting?.value); addSelectableOption(setting?.defaultValue); - if (normalizedOptions.length === 0) { + if (normalizedOptions.length <= 1) { return setting; } diff --git a/install-dev.sh b/install-dev.sh index 4a9ed1b..8181ef5 100755 --- a/install-dev.sh +++ b/install-dev.sh @@ -15,6 +15,8 @@ # --host 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 (Frontend läuft dann auf Port 5173) # --reinstall Vorhandene Installation ersetzen (Daten bleiben erhalten) # -h, --help Diese Hilfe anzeigen @@ -41,6 +43,8 @@ BACKEND_PORT="3001" FRONTEND_HOST="" # wird automatisch ermittelt, wenn leer SKIP_MAKEMKV=false SKIP_HANDBRAKE=false +BUILD_HANDBRAKE_NVDEC=false +HANDBRAKE_VERSION="1.9.0" SKIP_NGINX=false REINSTALL=false SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -48,14 +52,16 @@ SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # --- Argumente parsen --------------------------------------------------------- while [[ $# -gt 0 ]]; do case "$1" in - --dir) INSTALL_DIR="$2"; shift 2 ;; - --user) SERVICE_USER="$2"; shift 2 ;; - --port) BACKEND_PORT="$2"; shift 2 ;; - --host) FRONTEND_HOST="$2"; shift 2 ;; - --no-makemkv) SKIP_MAKEMKV=true; shift ;; - --no-handbrake) SKIP_HANDBRAKE=true; shift ;; - --no-nginx) SKIP_NGINX=true; shift ;; - --reinstall) REINSTALL=true; shift ;; + --dir) INSTALL_DIR="$2"; shift 2 ;; + --user) SERVICE_USER="$2"; shift 2 ;; + --port) BACKEND_PORT="$2"; shift 2 ;; + --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) sed -n '/^# Verwendung/,/^# ====/p' "$0" | head -n -1 | sed 's/^# \?//' exit 0 ;; @@ -177,11 +183,107 @@ install_makemkv() { warn "Beta-Key: https://www.makemkv.com/forum/viewtopic.php?t=1053" } +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" + + # 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 \ + 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" + + # Alte Installation entfernen + if command_exists HandBrakeCLI; then + warn "Entferne vorhandenes HandBrakeCLI..." + apt-get remove -y handbrake-cli 2>/dev/null || true + snap remove handbrake-cli 2>/dev/null || true + rm -f /usr/bin/HandBrakeCLI /usr/local/bin/HandBrakeCLI + fi + + # 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" + + info "Konfiguriere HandBrake mit NVDEC..." + ./configure --launch-jobs="$(nproc)" --enable-nvdec --prefix=/usr/local 2>&1 | tail -10 + + info "Baue HandBrake ($(nproc) Threads – bitte warten)..." + make --directory=build -j"$(nproc)" + + info "Installiere HandBrake nach /usr/local/bin..." + make --directory=build install + + cd / + rm -rf "$tmp_dir" + + if command_exists HandBrakeCLI; then + local ver + ver=$(HandBrakeCLI --version 2>&1 | head -1) + 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." + else + warn "libnvcuvid NICHT gefunden. NVDEC benötigt den installierten NVIDIA-Treiber." + fi + else + fatal "HandBrakeCLI nach dem Build nicht gefunden – Build fehlgeschlagen." + fi +} + install_handbrake() { header "HandBrake CLI installieren" + if [[ "$BUILD_HANDBRAKE_NVDEC" == true ]]; then + build_handbrake_nvdec + return + fi + if command_exists HandBrakeCLI; then - ok "HandBrakeCLI bereits installiert" + local ver + ver=$(HandBrakeCLI --version 2>&1 | head -1) + ok "HandBrakeCLI bereits installiert: ${ver}" + if ! HandBrakeCLI --help 2>&1 | grep -qi "nvdec"; then + warn "Das installierte HandBrakeCLI unterstützt kein NVDEC." + warn "Für NVDEC neu bauen: sudo bash install-dev.sh --no-makemkv --no-nginx --build-handbrake" + fi return fi @@ -189,13 +291,14 @@ install_handbrake() { 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 ! HandBrakeCLI --help 2>&1 | grep -qi "nvdec"; then + warn "Dieses HandBrakeCLI hat kein NVDEC. Für NVDEC: sudo bash install-dev.sh --no-makemkv --no-nginx --build-handbrake" + fi return fi case "$ID" in ubuntu) - # Strategie 2 (Ubuntu < 24.04): PPA manuell per Key + Sources-Datei eintragen, - # ohne add-apt-repository (schlägt auf Noble mit 401 fehl). local codename="${VERSION_CODENAME:-jammy}" local ppa_sources="/etc/apt/sources.list.d/handbrake.list" local ppa_key="/etc/apt/keyrings/handbrake.gpg" @@ -217,7 +320,6 @@ EOF 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 @@ -228,7 +330,6 @@ EOF ;; debian) - # Strategie 2 (Debian): Backports 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" \ @@ -241,7 +342,8 @@ EOF esac warn "HandBrake CLI konnte nicht automatisch installiert werden." - warn "Bitte manuell installieren: https://handbrake.fr/downloads2.php" + warn "Für einen NVDEC-Build: sudo bash install-dev.sh --no-makemkv --no-nginx --build-handbrake" + warn "Oder manuell: https://handbrake.fr/downloads2.php" } # --- apt-Hilfsfunktionen ------------------------------------------------------ diff --git a/install.sh b/install.sh index ec4342b..e7b971f 100755 --- a/install.sh +++ b/install.sh @@ -20,6 +20,8 @@ # --host 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 @@ -49,21 +51,25 @@ BACKEND_PORT="3001" FRONTEND_HOST="" SKIP_MAKEMKV=false SKIP_HANDBRAKE=false +BUILD_HANDBRAKE_NVDEC=false +HANDBRAKE_VERSION="1.9.0" SKIP_NGINX=false REINSTALL=false # --- Argumente parsen --------------------------------------------------------- while [[ $# -gt 0 ]]; do case "$1" in - --branch) GIT_BRANCH="$2"; shift 2 ;; - --dir) INSTALL_DIR="$2"; shift 2 ;; - --user) SERVICE_USER="$2"; shift 2 ;; - --port) BACKEND_PORT="$2"; shift 2 ;; - --host) FRONTEND_HOST="$2"; shift 2 ;; - --no-makemkv) SKIP_MAKEMKV=true; shift ;; - --no-handbrake) SKIP_HANDBRAKE=true; shift ;; - --no-nginx) SKIP_NGINX=true; shift ;; - --reinstall) REINSTALL=true; shift ;; + --branch) GIT_BRANCH="$2"; shift 2 ;; + --dir) INSTALL_DIR="$2"; shift 2 ;; + --user) SERVICE_USER="$2"; shift 2 ;; + --port) BACKEND_PORT="$2"; shift 2 ;; + --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) sed -n '/^# Verwendung/,/^# ====/p' "$0" | head -n -1 | sed 's/^# \?//' exit 0 ;; @@ -182,11 +188,115 @@ install_makemkv() { warn "Beta-Key: https://www.makemkv.com/forum/viewtopic.php?t=1053" } +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" + + # 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 \ + 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..." + # Fallback: nur die minimalen Header aus dem NVIDIA-CUDA-Repo + 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" + + # Alte Installation entfernen + if command_exists HandBrakeCLI; then + warn "Entferne vorhandenes HandBrakeCLI..." + apt-get remove -y handbrake-cli 2>/dev/null || true + snap remove handbrake-cli 2>/dev/null || true + rm -f /usr/bin/HandBrakeCLI /usr/local/bin/HandBrakeCLI + fi + + # 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" + + 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." + fi +} + install_handbrake() { header "HandBrake CLI installieren" + # --build-handbrake: immer aus Quellcode mit NVDEC bauen + if [[ "$BUILD_HANDBRAKE_NVDEC" == true ]]; then + build_handbrake_nvdec + return + fi + + # Bereits installiert? if command_exists HandBrakeCLI; then - ok "HandBrakeCLI bereits installiert" + local ver + ver=$(HandBrakeCLI --version 2>&1 | head -1) + ok "HandBrakeCLI bereits installiert: ${ver}" + if ! HandBrakeCLI --help 2>&1 | grep -qi "nvdec"; then + warn "Das installierte HandBrakeCLI unterstützt kein NVDEC." + warn "Für NVDEC neu bauen: sudo bash install.sh --no-makemkv --no-nginx --build-handbrake" + fi return fi @@ -194,13 +304,14 @@ install_handbrake() { 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 ! HandBrakeCLI --help 2>&1 | grep -qi "nvdec"; then + warn "Dieses HandBrakeCLI hat kein NVDEC. Für NVDEC: sudo bash install.sh --no-makemkv --no-nginx --build-handbrake" + fi return fi case "$ID" in ubuntu) - # Strategie 2 (Ubuntu < 24.04): PPA manuell per Key + Sources-Datei eintragen, - # ohne add-apt-repository (schlägt auf Noble mit 401 fehl). local codename="${VERSION_CODENAME:-jammy}" local ppa_sources="/etc/apt/sources.list.d/handbrake.list" local ppa_key="/etc/apt/keyrings/handbrake.gpg" @@ -233,7 +344,6 @@ EOF ;; debian) - # Strategie 2 (Debian): Backports 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" \ @@ -246,7 +356,8 @@ EOF esac warn "HandBrake CLI konnte nicht automatisch installiert werden." - warn "Bitte manuell installieren: https://handbrake.fr/downloads2.php" + 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 ------------------------------------------------------