From 05f29028f6cdbc9dd7ce252d730084a9855e9002 Mon Sep 17 00:00:00 2001 From: mboehmlaender Date: Mon, 9 Mar 2026 12:08:36 +0000 Subject: [PATCH] some pload --- backend/src/db/database.js | 57 +- backend/src/db/defaultSettings.js | 709 ------------------ backend/src/services/pipelineService.js | 19 + backend/src/services/settingsService.js | 14 +- db/schema.sql | 265 +++++++ .../src/components/DynamicSettingsForm.jsx | 186 +++-- frontend/src/styles/app.css | 23 + install.sh | 18 + 8 files changed, 463 insertions(+), 828 deletions(-) delete mode 100644 backend/src/db/defaultSettings.js diff --git a/backend/src/db/database.js b/backend/src/db/database.js index 07f6d35..691f258 100644 --- a/backend/src/db/database.js +++ b/backend/src/db/database.js @@ -3,7 +3,6 @@ const path = require('path'); const sqlite3 = require('sqlite3'); const { open } = require('sqlite'); const { dbPath } = require('../config'); -const { defaultSchema } = require('./defaultSettings'); const logger = require('../services/logger').child('DB'); const { errorToMeta } = require('../utils/errorMeta'); const { setLogRootDir, getJobLogDir } = require('../services/logPathService'); @@ -521,7 +520,7 @@ async function openAndPrepareDatabase() { const schemaModel = await loadSchemaModel(); await applySchemaModel(dbInstance, schemaModel); - await seedDefaultSettings(dbInstance); + await seedFromSchemaFile(dbInstance); await migrateLegacyProfiledToolSettings(dbInstance); await removeDeprecatedSettings(dbInstance); await ensurePipelineStateRow(dbInstance); @@ -564,52 +563,16 @@ async function initDatabase({ allowRecovery = true } = {}) { } -async function seedDefaultSettings(db) { - let seeded = 0; - for (const item of defaultSchema) { - await db.run( - ` - INSERT INTO settings_schema - (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) - VALUES - (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ON CONFLICT(key) DO UPDATE SET - category = excluded.category, - label = excluded.label, - type = excluded.type, - required = excluded.required, - description = excluded.description, - default_value = COALESCE(settings_schema.default_value, excluded.default_value), - options_json = excluded.options_json, - validation_json = excluded.validation_json, - order_index = excluded.order_index, - updated_at = CURRENT_TIMESTAMP - `, - [ - item.key, - item.category, - item.label, - item.type, - item.required, - item.description || null, - item.defaultValue || null, - JSON.stringify(item.options || []), - JSON.stringify(item.validation || {}), - item.orderIndex || 0 - ] - ); - - await db.run( - ` - INSERT INTO settings_values (key, value) - VALUES (?, ?) - ON CONFLICT(key) DO NOTHING - `, - [item.key, item.defaultValue || null] - ); - seeded += 1; +async function seedFromSchemaFile(db) { + const schemaSql = fs.readFileSync(schemaFilePath, 'utf-8'); + const statements = schemaSql + .split(/;\s*\n/) + .map((s) => s.trim()) + .filter((s) => /^INSERT\b/i.test(s)); + for (const stmt of statements) { + await db.run(stmt); } - logger.info('seed:settings', { count: seeded }); + logger.info('seed:settings', { count: statements.length }); } async function readCurrentOrDefaultSettingValue(db, key) { diff --git a/backend/src/db/defaultSettings.js b/backend/src/db/defaultSettings.js deleted file mode 100644 index 99c2eaa..0000000 --- a/backend/src/db/defaultSettings.js +++ /dev/null @@ -1,709 +0,0 @@ -const defaultSchema = [ - { - key: 'drive_mode', - category: 'Laufwerk', - label: 'Laufwerksmodus', - type: 'select', - required: 1, - description: 'Auto-Discovery oder explizites Device.', - defaultValue: 'auto', - options: [ - { label: 'Auto Discovery', value: 'auto' }, - { label: 'Explizites Device', value: 'explicit' } - ], - validation: {}, - orderIndex: 10 - }, - { - key: 'drive_device', - category: 'Laufwerk', - label: 'Device Pfad', - type: 'path', - required: 0, - description: 'Nur für expliziten Modus, z.B. /dev/sr0.', - defaultValue: '/dev/sr0', - options: [], - validation: {}, - orderIndex: 20 - }, - { - key: 'makemkv_source_index', - category: 'Laufwerk', - label: 'MakeMKV Source Index', - type: 'number', - required: 1, - description: 'Disc Index im Auto-Modus.', - defaultValue: '0', - options: [], - validation: { min: 0, max: 20 }, - orderIndex: 30 - }, - { - key: 'disc_poll_interval_ms', - category: 'Laufwerk', - label: 'Polling Intervall (ms)', - type: 'number', - required: 1, - description: 'Intervall für Disk-Erkennung.', - defaultValue: '4000', - options: [], - validation: { min: 1000, max: 60000 }, - orderIndex: 40 - }, - { - key: 'raw_dir', - category: 'Pfade', - label: 'Raw Ausgabeordner', - type: 'path', - required: 1, - description: 'Zwischenablage für MakeMKV Rip.', - defaultValue: '/mnt/arm-storage/media/raw', - options: [], - validation: { minLength: 1 }, - orderIndex: 100 - }, - { - key: 'raw_dir_bluray', - category: 'Pfade', - label: 'Raw Ausgabeordner (Blu-ray)', - type: 'path', - required: 0, - description: 'Optionaler RAW-Zielpfad nur für Blu-ray. Leer = Fallback auf "Raw Ausgabeordner".', - defaultValue: '', - options: [], - validation: {}, - orderIndex: 101 - }, - { - key: 'raw_dir_dvd', - category: 'Pfade', - label: 'Raw Ausgabeordner (DVD)', - type: 'path', - required: 0, - description: 'Optionaler RAW-Zielpfad nur für DVD. Leer = Fallback auf "Raw Ausgabeordner".', - defaultValue: '', - options: [], - validation: {}, - orderIndex: 102 - }, - { - key: 'raw_dir_other', - category: 'Pfade', - label: 'Raw Ausgabeordner (Sonstiges)', - type: 'path', - required: 0, - description: 'Optionaler RAW-Zielpfad nur für Sonstiges. Leer = Fallback auf "Raw Ausgabeordner".', - defaultValue: '', - options: [], - validation: {}, - orderIndex: 103 - }, - { - key: 'movie_dir', - category: 'Pfade', - label: 'Film Ausgabeordner', - type: 'path', - required: 1, - description: 'Finale HandBrake Ausgabe.', - defaultValue: '/mnt/arm-storage/media/movies', - options: [], - validation: { minLength: 1 }, - orderIndex: 110 - }, - { - key: 'movie_dir_bluray', - category: 'Pfade', - label: 'Film Ausgabeordner (Blu-ray)', - type: 'path', - required: 0, - description: 'Optionaler Encode-Zielpfad nur für Blu-ray. Leer = Fallback auf "Film Ausgabeordner".', - defaultValue: '', - options: [], - validation: {}, - orderIndex: 111 - }, - { - key: 'movie_dir_dvd', - category: 'Pfade', - label: 'Film Ausgabeordner (DVD)', - type: 'path', - required: 0, - description: 'Optionaler Encode-Zielpfad nur für DVD. Leer = Fallback auf "Film Ausgabeordner".', - defaultValue: '', - options: [], - validation: {}, - orderIndex: 112 - }, - { - key: 'movie_dir_other', - category: 'Pfade', - label: 'Film Ausgabeordner (Sonstiges)', - type: 'path', - required: 0, - description: 'Optionaler Encode-Zielpfad nur für Sonstiges. Leer = Fallback auf "Film Ausgabeordner".', - defaultValue: '', - options: [], - validation: {}, - orderIndex: 113 - }, - { - key: 'log_dir', - category: 'Pfade', - label: 'Log Ordner', - type: 'path', - required: 1, - description: 'Basisordner für Logs. Job-Logs liegen direkt hier, Backend-Logs in /backend.', - defaultValue: '/mnt/arm-storage/logs', - options: [], - validation: { minLength: 1 }, - orderIndex: 120 - }, - { - key: 'hardware_monitoring_enabled', - category: 'Monitoring', - label: 'Hardware Monitoring aktiviert', - type: 'boolean', - required: 1, - description: 'Master-Schalter: aktiviert/deaktiviert das komplette Hardware-Monitoring (Polling + Berechnung + WebSocket-Updates).', - defaultValue: 'true', - options: [], - validation: {}, - orderIndex: 130 - }, - { - key: 'hardware_monitoring_interval_ms', - category: 'Monitoring', - label: 'Hardware Monitoring Intervall (ms)', - type: 'number', - required: 1, - description: 'Polling-Intervall für CPU/RAM/GPU/Storage-Metriken.', - defaultValue: '5000', - options: [], - validation: { min: 1000, max: 60000 }, - orderIndex: 140 - }, - { - key: 'makemkv_command', - category: 'Tools', - label: 'MakeMKV Kommando', - type: 'string', - required: 1, - description: 'Pfad oder Befehl für makemkvcon.', - defaultValue: 'makemkvcon', - options: [], - validation: { minLength: 1 }, - orderIndex: 200 - }, - { - key: 'makemkv_registration_key', - category: 'Tools', - label: 'MakeMKV Key', - type: 'string', - required: 0, - description: 'Optionaler Registrierungsschlüssel. Wird vor Analyze/Rip automatisch per "makemkvcon reg" gesetzt.', - defaultValue: '', - options: [], - validation: {}, - orderIndex: 202 - }, - { - key: 'mediainfo_command', - category: 'Tools', - label: 'Mediainfo Kommando', - type: 'string', - required: 1, - description: 'Pfad oder Befehl für mediainfo.', - defaultValue: 'mediainfo', - options: [], - validation: { minLength: 1 }, - orderIndex: 205 - }, - { - key: 'makemkv_min_length_minutes', - category: 'Tools', - label: 'Minimale Titellänge (Minuten)', - type: 'number', - required: 1, - description: 'Filtert kurze Titel beim Rip.', - defaultValue: '60', - options: [], - validation: { min: 1, max: 1000 }, - orderIndex: 210 - }, - { - key: 'handbrake_command', - category: 'Tools', - label: 'HandBrake Kommando', - type: 'string', - required: 1, - description: 'Pfad oder Befehl für HandBrakeCLI.', - defaultValue: 'HandBrakeCLI', - options: [], - validation: { minLength: 1 }, - orderIndex: 215 - }, - { - key: 'handbrake_restart_delete_incomplete_output', - category: 'Tools', - label: 'Encode-Neustart: unvollständige Ausgabe löschen', - type: 'boolean', - required: 1, - description: 'Wenn aktiv, wird bei "Encode neu starten" der bisherige (nicht erfolgreiche) Output vor Start entfernt.', - defaultValue: 'true', - options: [], - validation: {}, - orderIndex: 220 - }, - { - key: 'pipeline_max_parallel_jobs', - category: 'Tools', - label: 'Parallele Jobs', - type: 'number', - required: 1, - description: 'Maximale Anzahl parallel laufender Jobs. Weitere Starts landen in der Queue.', - defaultValue: '1', - options: [], - validation: { min: 1, max: 12 }, - orderIndex: 225 - }, - { - key: 'mediainfo_extra_args_bluray', - category: 'Tools', - label: 'Mediainfo Extra Args', - type: 'string', - required: 0, - description: 'Zusätzliche CLI-Parameter für mediainfo (Blu-ray).', - defaultValue: '', - options: [], - validation: {}, - orderIndex: 300 - }, - { - key: 'makemkv_rip_mode_bluray', - category: 'Tools', - label: 'MakeMKV Rip Modus', - type: 'select', - required: 1, - description: 'mkv: direkte MKV-Dateien; backup: vollständige Blu-ray Struktur im RAW-Ordner.', - defaultValue: 'backup', - options: [ - { label: 'MKV', value: 'mkv' }, - { label: 'Backup', value: 'backup' } - ], - validation: {}, - orderIndex: 305 - }, - { - key: 'makemkv_analyze_extra_args_bluray', - category: 'Tools', - label: 'MakeMKV Analyze Extra Args', - type: 'string', - required: 0, - description: 'Zusätzliche CLI-Parameter für Analyze (Blu-ray).', - defaultValue: '', - options: [], - validation: {}, - orderIndex: 310 - }, - { - key: 'makemkv_rip_extra_args_bluray', - category: 'Tools', - label: 'MakeMKV Rip Extra Args', - type: 'string', - required: 0, - description: 'Zusätzliche CLI-Parameter für Rip (Blu-ray).', - defaultValue: '', - options: [], - validation: {}, - orderIndex: 315 - }, - { - key: 'handbrake_preset_bluray', - category: 'Tools', - label: 'HandBrake Preset', - type: 'string', - required: 1, - description: 'Preset Name für -Z (Blu-ray).', - defaultValue: 'H.264 MKV 1080p30', - options: [], - validation: { minLength: 1 }, - orderIndex: 320 - }, - { - key: 'handbrake_extra_args_bluray', - category: 'Tools', - label: 'HandBrake Extra Args', - type: 'string', - required: 0, - description: 'Zusätzliche CLI-Argumente (Blu-ray).', - defaultValue: '--audio-lang-list deu,eng --first-audio --subtitle-lang-list deu,eng --first-subtitle --aencoder copy --audio-copy-mask ac3,eac3,dts --audio-fallback ac3 --encoder-preset slow --quality 18 --encoder-tune film --encoder-profile high --encoder-level 4.1', - options: [], - validation: {}, - orderIndex: 325 - }, - { - key: 'output_extension_bluray', - category: 'Tools', - label: 'Ausgabeformat', - type: 'select', - required: 1, - description: 'Dateiendung für finale Datei (Blu-ray).', - defaultValue: 'mkv', - options: [ - { label: 'MKV', value: 'mkv' }, - { label: 'MP4', value: 'mp4' } - ], - validation: {}, - orderIndex: 330 - }, - { - key: 'filename_template_bluray', - category: 'Tools', - label: 'Dateiname Template', - type: 'string', - required: 1, - description: 'Verfügbare Tokens: ${title}, ${year}, ${imdbId} (Blu-ray).', - defaultValue: '${title} (${year})', - options: [], - validation: { minLength: 1 }, - orderIndex: 335 - }, - { - key: 'output_folder_template_bluray', - category: 'Tools', - label: 'Ordnername Template', - type: 'string', - required: 0, - description: 'Optional. Verfügbare Tokens: ${title}, ${year}, ${imdbId}. Leer = Dateiname-Template (Blu-ray).', - defaultValue: '', - options: [], - validation: {}, - orderIndex: 340 - }, - { - key: 'mediainfo_extra_args_dvd', - category: 'Tools', - label: 'Mediainfo Extra Args', - type: 'string', - required: 0, - description: 'Zusätzliche CLI-Parameter für mediainfo (DVD).', - defaultValue: '', - options: [], - validation: {}, - orderIndex: 500 - }, - { - key: 'makemkv_rip_mode_dvd', - category: 'Tools', - label: 'MakeMKV Rip Modus', - type: 'select', - required: 1, - description: 'mkv: direkte MKV-Dateien; backup: vollständige Disc-Struktur im RAW-Ordner.', - defaultValue: 'mkv', - options: [ - { label: 'MKV', value: 'mkv' }, - { label: 'Backup', value: 'backup' } - ], - validation: {}, - orderIndex: 505 - }, - { - key: 'makemkv_analyze_extra_args_dvd', - category: 'Tools', - label: 'MakeMKV Analyze Extra Args', - type: 'string', - required: 0, - description: 'Zusätzliche CLI-Parameter für Analyze (DVD).', - defaultValue: '', - options: [], - validation: {}, - orderIndex: 510 - }, - { - key: 'makemkv_rip_extra_args_dvd', - category: 'Tools', - label: 'MakeMKV Rip Extra Args', - type: 'string', - required: 0, - description: 'Zusätzliche CLI-Parameter für Rip (DVD).', - defaultValue: '', - options: [], - validation: {}, - orderIndex: 515 - }, - { - key: 'handbrake_preset_dvd', - category: 'Tools', - label: 'HandBrake Preset', - type: 'string', - required: 1, - description: 'Preset Name für -Z (DVD).', - defaultValue: 'H.264 MKV 480p30', - options: [], - validation: { minLength: 1 }, - orderIndex: 520 - }, - { - key: 'handbrake_extra_args_dvd', - category: 'Tools', - label: 'HandBrake Extra Args', - type: 'string', - required: 0, - description: 'Zusätzliche CLI-Argumente (DVD).', - defaultValue: '--audio-lang-list deu,eng --first-audio --subtitle-lang-list deu,eng --first-subtitle --aencoder copy --audio-copy-mask ac3,eac3,dts --audio-fallback ac3 --encoder-preset slow --quality 18 --encoder-tune film --encoder-profile high --encoder-level 4.1', - options: [], - validation: {}, - orderIndex: 525 - }, - { - key: 'output_extension_dvd', - category: 'Tools', - label: 'Ausgabeformat', - type: 'select', - required: 1, - description: 'Dateiendung für finale Datei (DVD).', - defaultValue: 'mkv', - options: [ - { label: 'MKV', value: 'mkv' }, - { label: 'MP4', value: 'mp4' } - ], - validation: {}, - orderIndex: 530 - }, - { - key: 'filename_template_dvd', - category: 'Tools', - label: 'Dateiname Template', - type: 'string', - required: 1, - description: 'Verfügbare Tokens: ${title}, ${year}, ${imdbId} (DVD).', - defaultValue: '${title} (${year})', - options: [], - validation: { minLength: 1 }, - orderIndex: 535 - }, - { - key: 'output_folder_template_dvd', - category: 'Tools', - label: 'Ordnername Template', - type: 'string', - required: 0, - description: 'Optional. Verfügbare Tokens: ${title}, ${year}, ${imdbId}. Leer = Dateiname-Template (DVD).', - defaultValue: '', - options: [], - validation: {}, - orderIndex: 540 - }, - { - key: 'omdb_api_key', - category: 'Metadaten', - label: 'OMDb API Key', - type: 'string', - required: 0, - description: 'API Key für Metadatensuche.', - defaultValue: '', - options: [], - validation: {}, - orderIndex: 400 - }, - { - key: 'omdb_default_type', - category: 'Metadaten', - label: 'OMDb Typ', - type: 'select', - required: 1, - description: 'Vorauswahl für Suche.', - defaultValue: 'movie', - options: [ - { label: 'Movie', value: 'movie' }, - { label: 'Series', value: 'series' }, - { label: 'Episode', value: 'episode' } - ], - validation: {}, - orderIndex: 410 - }, - { - key: 'pushover_enabled', - category: 'Benachrichtigungen', - label: 'PushOver aktiviert', - type: 'boolean', - required: 1, - description: 'Master-Schalter für PushOver Versand.', - defaultValue: 'false', - options: [], - validation: {}, - orderIndex: 500 - }, - { - key: 'pushover_token', - category: 'Benachrichtigungen', - label: 'PushOver Token', - type: 'string', - required: 0, - description: 'Application Token für PushOver.', - defaultValue: '', - options: [], - validation: {}, - orderIndex: 510 - }, - { - key: 'pushover_user', - category: 'Benachrichtigungen', - label: 'PushOver User', - type: 'string', - required: 0, - description: 'User-Key für PushOver.', - defaultValue: '', - options: [], - validation: {}, - orderIndex: 520 - }, - { - key: 'pushover_device', - category: 'Benachrichtigungen', - label: 'PushOver Device (optional)', - type: 'string', - required: 0, - description: 'Optionales Ziel-Device in PushOver.', - defaultValue: '', - options: [], - validation: {}, - orderIndex: 530 - }, - { - key: 'pushover_title_prefix', - category: 'Benachrichtigungen', - label: 'PushOver Titel-Präfix', - type: 'string', - required: 1, - description: 'Prefix im PushOver Titel.', - defaultValue: 'Ripster', - options: [], - validation: { minLength: 1 }, - orderIndex: 540 - }, - { - key: 'pushover_priority', - category: 'Benachrichtigungen', - label: 'PushOver Priority', - type: 'number', - required: 1, - description: 'Priorität -2 bis 2.', - defaultValue: '0', - options: [], - validation: { min: -2, max: 2 }, - orderIndex: 550 - }, - { - key: 'pushover_timeout_ms', - category: 'Benachrichtigungen', - label: 'PushOver Timeout (ms)', - type: 'number', - required: 1, - description: 'HTTP Timeout für PushOver Requests.', - defaultValue: '7000', - options: [], - validation: { min: 1000, max: 60000 }, - orderIndex: 560 - }, - { - key: 'pushover_notify_metadata_ready', - category: 'Benachrichtigungen', - label: 'Bei Metadaten-Auswahl senden', - type: 'boolean', - required: 1, - description: 'Sendet wenn Metadaten zur Auswahl bereitstehen.', - defaultValue: 'true', - options: [], - validation: {}, - orderIndex: 570 - }, - { - key: 'pushover_notify_rip_started', - category: 'Benachrichtigungen', - label: 'Bei Rip-Start senden', - type: 'boolean', - required: 1, - description: 'Sendet beim Start des MakeMKV-Rips.', - defaultValue: 'true', - options: [], - validation: {}, - orderIndex: 580 - }, - { - key: 'pushover_notify_encoding_started', - category: 'Benachrichtigungen', - label: 'Bei Encode-Start senden', - type: 'boolean', - required: 1, - description: 'Sendet beim Start von HandBrake.', - defaultValue: 'true', - options: [], - validation: {}, - orderIndex: 590 - }, - { - key: 'pushover_notify_job_finished', - category: 'Benachrichtigungen', - label: 'Bei Erfolg senden', - type: 'boolean', - required: 1, - description: 'Sendet bei erfolgreich abgeschlossenem Job.', - defaultValue: 'true', - options: [], - validation: {}, - orderIndex: 600 - }, - { - key: 'pushover_notify_job_error', - category: 'Benachrichtigungen', - label: 'Bei Fehler senden', - type: 'boolean', - required: 1, - description: 'Sendet bei Fehlern in der Pipeline.', - defaultValue: 'true', - options: [], - validation: {}, - orderIndex: 610 - }, - { - key: 'pushover_notify_job_cancelled', - category: 'Benachrichtigungen', - label: 'Bei Abbruch senden', - type: 'boolean', - required: 1, - description: 'Sendet wenn Job manuell abgebrochen wurde.', - defaultValue: 'true', - options: [], - validation: {}, - orderIndex: 620 - }, - { - key: 'pushover_notify_reencode_started', - category: 'Benachrichtigungen', - label: 'Bei Re-Encode Start senden', - type: 'boolean', - required: 1, - description: 'Sendet beim Start von RAW Re-Encode.', - defaultValue: 'true', - options: [], - validation: {}, - orderIndex: 630 - }, - { - key: 'pushover_notify_reencode_finished', - category: 'Benachrichtigungen', - label: 'Bei Re-Encode Erfolg senden', - type: 'boolean', - required: 1, - description: 'Sendet bei erfolgreichem RAW Re-Encode.', - defaultValue: 'true', - options: [], - validation: {}, - orderIndex: 640 - } -]; - -module.exports = { - defaultSchema -}; diff --git a/backend/src/services/pipelineService.js b/backend/src/services/pipelineService.js index 5c86c44..a60ee88 100644 --- a/backend/src/services/pipelineService.js +++ b/backend/src/services/pipelineService.js @@ -376,6 +376,22 @@ function ensureUniqueOutputPath(outputPath) { return attempt; } +function chownRecursive(targetPath, ownerSpec) { + const spec = String(ownerSpec || '').trim(); + if (!spec || !targetPath) { + return; + } + try { + const { spawnSync } = require('child_process'); + const result = spawnSync('chown', ['-R', spec, targetPath], { timeout: 15000 }); + if (result.status !== 0) { + logger.warn('chown:failed', { targetPath, spec, stderr: String(result.stderr || '') }); + } + } catch (error) { + logger.warn('chown:error', { targetPath, spec, error: error?.message }); + } +} + function moveFileWithFallback(sourcePath, targetPath) { try { fs.renameSync(sourcePath, targetPath); @@ -6670,6 +6686,7 @@ class PipelineService extends EventEmitter { preferredFinalOutputPath ); const finalizedOutputPath = outputFinalization.outputPath; + chownRecursive(path.dirname(finalizedOutputPath), settings.movie_dir_owner); if (outputFinalization.outputPathWithTimestamp) { await historyService.appendLog( jobId, @@ -6903,6 +6920,7 @@ class PipelineService extends EventEmitter { const rawDirName = buildRawDirName(metadataBase, jobId, { incomplete: true }); const rawJobDir = path.join(rawBaseDir, rawDirName); ensureDir(rawJobDir); + chownRecursive(rawJobDir, settings.raw_dir_owner); logger.info('rip:raw-dir-created', { jobId, rawJobDir }); const deviceCandidate = this.detectedDisc || this.snapshot.context?.device || { @@ -7061,6 +7079,7 @@ class PipelineService extends EventEmitter { try { fs.renameSync(rawJobDir, completedRawJobDir); activeRawJobDir = completedRawJobDir; + chownRecursive(activeRawJobDir, settings.raw_dir_owner); await historyService.updateRawPathByOldPath(rawJobDir, completedRawJobDir); await historyService.appendLog( jobId, diff --git a/backend/src/services/settingsService.js b/backend/src/services/settingsService.js index cd55456..525c695 100644 --- a/backend/src/services/settingsService.js +++ b/backend/src/services/settingsService.js @@ -35,11 +35,21 @@ const PROFILED_SETTINGS = { dvd: 'raw_dir_dvd', other: 'raw_dir_other' }, + raw_dir_owner: { + bluray: 'raw_dir_bluray_owner', + dvd: 'raw_dir_dvd_owner', + other: 'raw_dir_other_owner' + }, movie_dir: { bluray: 'movie_dir_bluray', dvd: 'movie_dir_dvd', other: 'movie_dir_other' }, + movie_dir_owner: { + bluray: 'movie_dir_bluray_owner', + dvd: 'movie_dir_dvd_owner', + other: 'movie_dir_other_owner' + }, mediainfo_extra_args: { bluray: 'mediainfo_extra_args_bluray', dvd: 'mediainfo_extra_args_dvd' @@ -79,7 +89,9 @@ const PROFILED_SETTINGS = { }; const STRICT_PROFILE_ONLY_SETTING_KEYS = new Set([ 'raw_dir', - 'movie_dir' + 'raw_dir_owner', + 'movie_dir', + 'movie_dir_owner' ]); function applyRuntimeLogDirSetting(rawValue) { diff --git a/db/schema.sql b/db/schema.sql index e4fd055..86dd53b 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -131,3 +131,268 @@ CREATE TABLE cron_run_logs ( ); CREATE INDEX idx_cron_run_logs_job ON cron_run_logs(cron_job_id, id DESC); + +-- ============================================================================= +-- Default Settings Seed +-- ============================================================================= + +-- Pfade – Eigentümer für alternative Verzeichnisse (inline in DynamicSettingsForm gerendert) +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('raw_dir_bluray_owner', 'Pfade', 'Eigentümer Raw-Ordner (Blu-ray)', 'string', 0, 'Eigentümer der Dateien im Format user:gruppe. Nur aktiv wenn ein alternativer Pfad gesetzt ist. Leer = Standardbenutzer des Dienstes.', NULL, '[]', '{}', 1015); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('raw_dir_bluray_owner', NULL); + +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('raw_dir_dvd_owner', 'Pfade', 'Eigentümer Raw-Ordner (DVD)', 'string', 0, 'Eigentümer der Dateien im Format user:gruppe. Nur aktiv wenn ein alternativer Pfad gesetzt ist. Leer = Standardbenutzer des Dienstes.', NULL, '[]', '{}', 1025); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('raw_dir_dvd_owner', NULL); + +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('raw_dir_other_owner', 'Pfade', 'Eigentümer Raw-Ordner (Sonstiges)', 'string', 0, 'Eigentümer der Dateien im Format user:gruppe. Nur aktiv wenn ein alternativer Pfad gesetzt ist. Leer = Standardbenutzer des Dienstes.', NULL, '[]', '{}', 1035); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('raw_dir_other_owner', NULL); + +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('movie_dir_bluray_owner', 'Pfade', 'Eigentümer Film-Ordner (Blu-ray)', 'string', 0, 'Eigentümer der Dateien im Format user:gruppe. Nur aktiv wenn ein alternativer Pfad gesetzt ist. Leer = Standardbenutzer des Dienstes.', NULL, '[]', '{}', 1115); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('movie_dir_bluray_owner', NULL); + +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('movie_dir_dvd_owner', 'Pfade', 'Eigentümer Film-Ordner (DVD)', 'string', 0, 'Eigentümer der Dateien im Format user:gruppe. Nur aktiv wenn ein alternativer Pfad gesetzt ist. Leer = Standardbenutzer des Dienstes.', NULL, '[]', '{}', 1125); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('movie_dir_dvd_owner', NULL); + +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('movie_dir_other_owner', 'Pfade', 'Eigentümer Film-Ordner (Sonstiges)', 'string', 0, 'Eigentümer der Dateien im Format user:gruppe. Nur aktiv wenn ein alternativer Pfad gesetzt ist. Leer = Standardbenutzer des Dienstes.', NULL, '[]', '{}', 1135); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('movie_dir_other_owner', NULL); + +-- Laufwerk +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('drive_mode', 'Laufwerk', 'Laufwerksmodus', 'select', 1, 'Auto-Discovery oder explizites Device.', 'auto', '[{"label":"Auto Discovery","value":"auto"},{"label":"Explizites Device","value":"explicit"}]', '{}', 10); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('drive_mode', 'auto'); + +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('drive_device', 'Laufwerk', 'Device Pfad', 'path', 0, 'Nur für expliziten Modus, z.B. /dev/sr0.', '/dev/sr0', '[]', '{}', 20); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('drive_device', '/dev/sr0'); + +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('makemkv_source_index', 'Laufwerk', 'MakeMKV Source Index', 'number', 1, 'Disc Index im Auto-Modus.', '0', '[]', '{"min":0,"max":20}', 30); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('makemkv_source_index', '0'); + +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('disc_poll_interval_ms', 'Laufwerk', 'Polling Intervall (ms)', 'number', 1, 'Intervall für Disk-Erkennung.', '4000', '[]', '{"min":1000,"max":60000}', 40); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('disc_poll_interval_ms', '4000'); + +-- Pfade +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('raw_dir', 'Pfade', 'Raw Ausgabeordner', 'path', 1, 'Zwischenablage für MakeMKV Rip.', 'data/output/raw', '[]', '{"minLength":1}', 100); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('raw_dir', 'data/output/raw'); + +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('raw_dir_bluray', 'Pfade', 'Raw Ausgabeordner (Blu-ray)', 'path', 0, 'Optionaler RAW-Zielpfad nur für Blu-ray. Leer = Fallback auf "Raw Ausgabeordner".', NULL, '[]', '{}', 101); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('raw_dir_bluray', NULL); + +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('raw_dir_dvd', 'Pfade', 'Raw Ausgabeordner (DVD)', 'path', 0, 'Optionaler RAW-Zielpfad nur für DVD. Leer = Fallback auf "Raw Ausgabeordner".', NULL, '[]', '{}', 102); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('raw_dir_dvd', NULL); + +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('raw_dir_other', 'Pfade', 'Raw Ausgabeordner (Sonstiges)', 'path', 0, 'Optionaler RAW-Zielpfad nur für Sonstiges. Leer = Fallback auf "Raw Ausgabeordner".', NULL, '[]', '{}', 103); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('raw_dir_other', NULL); + +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('movie_dir', 'Pfade', 'Film Ausgabeordner', 'path', 1, 'Finale HandBrake Ausgabe.', 'data/output/movies', '[]', '{"minLength":1}', 110); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('movie_dir', 'data/output/movies'); + +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('movie_dir_bluray', 'Pfade', 'Film Ausgabeordner (Blu-ray)', 'path', 0, 'Optionaler Encode-Zielpfad nur für Blu-ray. Leer = Fallback auf "Film Ausgabeordner".', NULL, '[]', '{}', 111); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('movie_dir_bluray', NULL); + +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('movie_dir_dvd', 'Pfade', 'Film Ausgabeordner (DVD)', 'path', 0, 'Optionaler Encode-Zielpfad nur für DVD. Leer = Fallback auf "Film Ausgabeordner".', NULL, '[]', '{}', 112); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('movie_dir_dvd', NULL); + +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('movie_dir_other', 'Pfade', 'Film Ausgabeordner (Sonstiges)', 'path', 0, 'Optionaler Encode-Zielpfad nur für Sonstiges. Leer = Fallback auf "Film Ausgabeordner".', NULL, '[]', '{}', 113); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('movie_dir_other', NULL); + +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('log_dir', 'Pfade', 'Log Ordner', 'path', 1, 'Basisordner für Logs. Job-Logs liegen direkt hier, Backend-Logs in /backend.', 'data/logs', '[]', '{"minLength":1}', 120); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('log_dir', 'data/logs'); + +-- Monitoring +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('hardware_monitoring_enabled', 'Monitoring', 'Hardware Monitoring aktiviert', 'boolean', 1, 'Master-Schalter: aktiviert/deaktiviert das komplette Hardware-Monitoring (Polling + Berechnung + WebSocket-Updates).', 'true', '[]', '{}', 130); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('hardware_monitoring_enabled', 'true'); + +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('hardware_monitoring_interval_ms', 'Monitoring', 'Hardware Monitoring Intervall (ms)', 'number', 1, 'Polling-Intervall für CPU/RAM/GPU/Storage-Metriken.', '5000', '[]', '{"min":1000,"max":60000}', 140); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('hardware_monitoring_interval_ms', '5000'); + +-- Tools +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('makemkv_command', 'Tools', 'MakeMKV Kommando', 'string', 1, 'Pfad oder Befehl für makemkvcon.', 'makemkvcon', '[]', '{"minLength":1}', 200); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('makemkv_command', 'makemkvcon'); + +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('makemkv_registration_key', 'Tools', 'MakeMKV Key', 'string', 0, 'Optionaler Registrierungsschlüssel. Wird vor Analyze/Rip automatisch per "makemkvcon reg" gesetzt.', NULL, '[]', '{}', 202); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('makemkv_registration_key', NULL); + +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('mediainfo_command', 'Tools', 'Mediainfo Kommando', 'string', 1, 'Pfad oder Befehl für mediainfo.', 'mediainfo', '[]', '{"minLength":1}', 205); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('mediainfo_command', 'mediainfo'); + +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('makemkv_min_length_minutes', 'Tools', 'Minimale Titellänge (Minuten)', 'number', 1, 'Filtert kurze Titel beim Rip.', '60', '[]', '{"min":1,"max":1000}', 210); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('makemkv_min_length_minutes', '60'); + +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('handbrake_command', 'Tools', 'HandBrake Kommando', 'string', 1, 'Pfad oder Befehl für HandBrakeCLI.', 'HandBrakeCLI', '[]', '{"minLength":1}', 215); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('handbrake_command', 'HandBrakeCLI'); + +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('handbrake_restart_delete_incomplete_output', 'Tools', 'Encode-Neustart: unvollständige Ausgabe löschen', 'boolean', 1, 'Wenn aktiv, wird bei "Encode neu starten" der bisherige (nicht erfolgreiche) Output vor Start entfernt.', 'true', '[]', '{}', 220); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('handbrake_restart_delete_incomplete_output', 'true'); + +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('pipeline_max_parallel_jobs', 'Tools', 'Parallele Jobs', 'number', 1, 'Maximale Anzahl parallel laufender Jobs. Weitere Starts landen in der Queue.', '1', '[]', '{"min":1,"max":12}', 225); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('pipeline_max_parallel_jobs', '1'); + +-- Tools – Blu-ray +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('mediainfo_extra_args_bluray', 'Tools', 'Mediainfo Extra Args', 'string', 0, 'Zusätzliche CLI-Parameter für mediainfo (Blu-ray).', NULL, '[]', '{}', 300); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('mediainfo_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 ('makemkv_rip_mode_bluray', 'Tools', 'MakeMKV Rip Modus', 'select', 1, 'mkv: direkte MKV-Dateien; backup: vollständige Blu-ray Struktur im RAW-Ordner.', 'backup', '[{"label":"MKV","value":"mkv"},{"label":"Backup","value":"backup"}]', '{}', 305); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('makemkv_rip_mode_bluray', 'backup'); + +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('makemkv_analyze_extra_args_bluray', 'Tools', 'MakeMKV Analyze Extra Args', 'string', 0, 'Zusätzliche CLI-Parameter für Analyze (Blu-ray).', NULL, '[]', '{}', 310); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('makemkv_analyze_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 ('makemkv_rip_extra_args_bluray', 'Tools', 'MakeMKV Rip Extra Args', 'string', 0, 'Zusätzliche CLI-Parameter für Rip (Blu-ray).', NULL, '[]', '{}', 315); +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); +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) +VALUES ('handbrake_extra_args_bluray', 'Tools', 'HandBrake Extra Args', 'string', 0, 'Zusätzliche CLI-Argumente (Blu-ray).', NULL, '[]', '{}', 325); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('handbrake_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 ('output_extension_bluray', 'Tools', 'Ausgabeformat', 'select', 1, 'Dateiendung für finale Datei (Blu-ray).', 'mkv', '[{"label":"MKV","value":"mkv"},{"label":"MP4","value":"mp4"}]', '{}', 330); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('output_extension_bluray', 'mkv'); + +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('filename_template_bluray', 'Tools', 'Dateiname Template', 'string', 1, 'Verfügbare Tokens: ${title}, ${year}, ${imdbId} (Blu-ray).', '${title} (${year})', '[]', '{"minLength":1}', 335); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('filename_template_bluray', '${title} (${year})'); + +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('output_folder_template_bluray', 'Tools', 'Ordnername Template', 'string', 0, 'Optional. Verfügbare Tokens: ${title}, ${year}, ${imdbId}. Leer = Dateiname-Template (Blu-ray).', NULL, '[]', '{}', 340); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('output_folder_template_bluray', NULL); + +-- Tools – DVD +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('mediainfo_extra_args_dvd', 'Tools', 'Mediainfo Extra Args', 'string', 0, 'Zusätzliche CLI-Parameter für mediainfo (DVD).', NULL, '[]', '{}', 500); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('mediainfo_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 ('makemkv_rip_mode_dvd', 'Tools', 'MakeMKV Rip Modus', 'select', 1, 'mkv: direkte MKV-Dateien; backup: vollständige Disc-Struktur im RAW-Ordner.', 'mkv', '[{"label":"MKV","value":"mkv"},{"label":"Backup","value":"backup"}]', '{}', 505); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('makemkv_rip_mode_dvd', 'mkv'); + +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('makemkv_analyze_extra_args_dvd', 'Tools', 'MakeMKV Analyze Extra Args', 'string', 0, 'Zusätzliche CLI-Parameter für Analyze (DVD).', NULL, '[]', '{}', 510); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('makemkv_analyze_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 ('makemkv_rip_extra_args_dvd', 'Tools', 'MakeMKV Rip Extra Args', 'string', 0, 'Zusätzliche CLI-Parameter für Rip (DVD).', NULL, '[]', '{}', 515); +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); +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) +VALUES ('handbrake_extra_args_dvd', 'Tools', 'HandBrake Extra Args', 'string', 0, 'Zusätzliche CLI-Argumente (DVD).', NULL, '[]', '{}', 525); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('handbrake_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 ('output_extension_dvd', 'Tools', 'Ausgabeformat', 'select', 1, 'Dateiendung für finale Datei (DVD).', 'mkv', '[{"label":"MKV","value":"mkv"},{"label":"MP4","value":"mp4"}]', '{}', 530); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('output_extension_dvd', 'mkv'); + +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('filename_template_dvd', 'Tools', 'Dateiname Template', 'string', 1, 'Verfügbare Tokens: ${title}, ${year}, ${imdbId} (DVD).', '${title} (${year})', '[]', '{"minLength":1}', 535); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('filename_template_dvd', '${title} (${year})'); + +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('output_folder_template_dvd', 'Tools', 'Ordnername Template', 'string', 0, 'Optional. Verfügbare Tokens: ${title}, ${year}, ${imdbId}. Leer = Dateiname-Template (DVD).', NULL, '[]', '{}', 540); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('output_folder_template_dvd', NULL); + +-- Metadaten +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('omdb_api_key', 'Metadaten', 'OMDb API Key', 'string', 0, 'API Key für Metadatensuche.', NULL, '[]', '{}', 400); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('omdb_api_key', NULL); + +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('omdb_default_type', 'Metadaten', 'OMDb Typ', 'select', 1, 'Vorauswahl für Suche.', 'movie', '[{"label":"Movie","value":"movie"},{"label":"Series","value":"series"},{"label":"Episode","value":"episode"}]', '{}', 410); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('omdb_default_type', 'movie'); + +-- Benachrichtigungen +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('pushover_enabled', 'Benachrichtigungen', 'PushOver aktiviert', 'boolean', 1, 'Master-Schalter für PushOver Versand.', 'false', '[]', '{}', 500); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('pushover_enabled', 'false'); + +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('pushover_token', 'Benachrichtigungen', 'PushOver Token', 'string', 0, 'Application Token für PushOver.', NULL, '[]', '{}', 510); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('pushover_token', NULL); + +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('pushover_user', 'Benachrichtigungen', 'PushOver User', 'string', 0, 'User-Key für PushOver.', NULL, '[]', '{}', 520); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('pushover_user', NULL); + +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('pushover_device', 'Benachrichtigungen', 'PushOver Device (optional)', 'string', 0, 'Optionales Ziel-Device in PushOver.', NULL, '[]', '{}', 530); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('pushover_device', NULL); + +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('pushover_title_prefix', 'Benachrichtigungen', 'PushOver Titel-Präfix', 'string', 1, 'Prefix im PushOver Titel.', 'Ripster', '[]', '{"minLength":1}', 540); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('pushover_title_prefix', 'Ripster'); + +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('pushover_priority', 'Benachrichtigungen', 'PushOver Priority', 'number', 1, 'Priorität -2 bis 2.', '0', '[]', '{"min":-2,"max":2}', 550); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('pushover_priority', '0'); + +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('pushover_timeout_ms', 'Benachrichtigungen', 'PushOver Timeout (ms)', 'number', 1, 'HTTP Timeout für PushOver Requests.', '7000', '[]', '{"min":1000,"max":60000}', 560); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('pushover_timeout_ms', '7000'); + +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('pushover_notify_metadata_ready', 'Benachrichtigungen', 'Bei Metadaten-Auswahl senden', 'boolean', 1, 'Sendet wenn Metadaten zur Auswahl bereitstehen.', 'true', '[]', '{}', 570); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('pushover_notify_metadata_ready', 'true'); + +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('pushover_notify_rip_started', 'Benachrichtigungen', 'Bei Rip-Start senden', 'boolean', 1, 'Sendet beim Start des MakeMKV-Rips.', 'true', '[]', '{}', 580); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('pushover_notify_rip_started', 'true'); + +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('pushover_notify_encoding_started', 'Benachrichtigungen', 'Bei Encode-Start senden', 'boolean', 1, 'Sendet beim Start von HandBrake.', 'true', '[]', '{}', 590); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('pushover_notify_encoding_started', 'true'); + +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('pushover_notify_job_finished', 'Benachrichtigungen', 'Bei Erfolg senden', 'boolean', 1, 'Sendet bei erfolgreich abgeschlossenem Job.', 'true', '[]', '{}', 600); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('pushover_notify_job_finished', 'true'); + +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('pushover_notify_job_error', 'Benachrichtigungen', 'Bei Fehler senden', 'boolean', 1, 'Sendet bei Fehlern in der Pipeline.', 'true', '[]', '{}', 610); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('pushover_notify_job_error', 'true'); + +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('pushover_notify_job_cancelled', 'Benachrichtigungen', 'Bei Abbruch senden', 'boolean', 1, 'Sendet wenn Job manuell abgebrochen wurde.', 'true', '[]', '{}', 620); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('pushover_notify_job_cancelled', 'true'); + +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('pushover_notify_reencode_started', 'Benachrichtigungen', 'Bei Re-Encode Start senden', 'boolean', 1, 'Sendet beim Start von RAW Re-Encode.', 'true', '[]', '{}', 630); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('pushover_notify_reencode_started', 'true'); + +INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) +VALUES ('pushover_notify_reencode_finished', 'Benachrichtigungen', 'Bei Re-Encode Erfolg senden', 'boolean', 1, 'Sendet bei erfolgreichem RAW Re-Encode.', 'true', '[]', '{}', 640); +INSERT OR IGNORE INTO settings_values (key, value) VALUES ('pushover_notify_reencode_finished', 'true'); diff --git a/frontend/src/components/DynamicSettingsForm.jsx b/frontend/src/components/DynamicSettingsForm.jsx index d06ef57..3f84237 100644 --- a/frontend/src/components/DynamicSettingsForm.jsx +++ b/frontend/src/components/DynamicSettingsForm.jsx @@ -161,83 +161,127 @@ export default function DynamicSettingsForm({ {section.description ? {section.description} : null} ) : null} -
- {(section.settings || []).map((setting) => { - const value = values?.[setting.key]; - const error = errors?.[setting.key] || null; - const dirty = Boolean(dirtyKeys?.has?.(setting.key)); + {(() => { + const ownerKeySet = new Set( + (section.settings || []) + .filter((s) => String(s.key || '').endsWith('_owner')) + .map((s) => s.key) + ); + const settingsByKey = new Map( + (section.settings || []).map((s) => [s.key, s]) + ); + const visibleSettings = (section.settings || []).filter( + (s) => !ownerKeySet.has(s.key) + ); - return ( -
- + return ( +
+ {visibleSettings.map((setting) => { + const value = values?.[setting.key]; + const error = errors?.[setting.key] || null; + const dirty = Boolean(dirtyKeys?.has?.(setting.key)); - {setting.type === 'string' || setting.type === 'path' ? ( - onChange?.(setting.key, event.target.value)} - /> - ) : null} + const ownerKey = `${setting.key}_owner`; + const ownerSetting = settingsByKey.get(ownerKey) || null; + const pathHasValue = Boolean(String(value ?? '').trim()); - {setting.type === 'number' ? ( - onChange?.(setting.key, event.value)} - mode="decimal" - useGrouping={false} - /> - ) : null} + return ( +
+ - {setting.type === 'boolean' ? ( - onChange?.(setting.key, event.value)} - /> - ) : null} + {setting.type === 'string' || setting.type === 'path' ? ( + onChange?.(setting.key, event.target.value)} + /> + ) : null} - {setting.type === 'select' ? ( - onChange?.(setting.key, event.value)} - /> - ) : null} + {setting.type === 'number' ? ( + onChange?.(setting.key, event.value)} + mode="decimal" + useGrouping={false} + /> + ) : null} - {setting.description || ''} - {isHandBrakePresetSetting(setting) ? ( - - Preset-Erklärung:{' '} - - HandBrake Official Presets - - - ) : null} - {error ? ( - {error} - ) : ( - - )} -
- ); - })} -
+ {setting.type === 'boolean' ? ( + onChange?.(setting.key, event.value)} + /> + ) : null} + + {setting.type === 'select' ? ( + onChange?.(setting.key, event.value)} + /> + ) : null} + + {setting.description || ''} + {isHandBrakePresetSetting(setting) ? ( + + Preset-Erklärung:{' '} + + HandBrake Official Presets + + + ) : null} + {error ? ( + {error} + ) : ( + + )} + + {ownerSetting ? ( +
+ + onChange?.(ownerKey, event.target.value)} + /> + {errors?.[ownerKey] ? ( + {errors[ownerKey]} + ) : ( + + )} +
+ ) : null} +
+ ); + })} +
+ ); + })()} ))} diff --git a/frontend/src/styles/app.css b/frontend/src/styles/app.css index e118c55..0d30659 100644 --- a/frontend/src/styles/app.css +++ b/frontend/src/styles/app.css @@ -1100,6 +1100,29 @@ body { width: fit-content; } +.setting-owner-row { + display: grid; + gap: 0.25rem; + margin-top: 0.35rem; + padding-top: 0.5rem; + border-top: 1px dashed var(--rip-border); +} + +.setting-owner-label { + font-size: 0.8rem; + color: var(--rip-muted); + font-weight: 500; +} + +.setting-owner-row .p-inputtext:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.setting-owner-row .saved-tag { + width: fit-content; +} + .script-manager-wrap { display: grid; gap: 0.8rem; diff --git a/install.sh b/install.sh index 2c76d87..3ee03be 100755 --- a/install.sh +++ b/install.sh @@ -413,6 +413,9 @@ fi # Daten- und Log-Verzeichnisse sicherstellen mkdir -p "$INSTALL_DIR/backend/data" mkdir -p "$INSTALL_DIR/backend/logs" +mkdir -p "$INSTALL_DIR/backend/data/output/raw" +mkdir -p "$INSTALL_DIR/backend/data/output/movies" +mkdir -p "$INSTALL_DIR/backend/data/logs" # Gesicherte Daten zurückspielen if [[ -n "${DATA_BACKUP:-}" && -d "$DATA_BACKUP" ]]; then @@ -477,6 +480,21 @@ chown -R "$SERVICE_USER:$SERVICE_USER" "$INSTALL_DIR" chmod -R 755 "$INSTALL_DIR" chmod 600 "$ENV_FILE" +# Ausgabe- und Log-Verzeichnisse dem installierenden User zuweisen +# (SUDO_USER = der echte User hinter sudo; leer wenn direkt als root ausgeführt) +ACTUAL_USER="${SUDO_USER:-}" +if [[ -n "$ACTUAL_USER" && "$ACTUAL_USER" != "root" ]]; then + chown -R "$ACTUAL_USER:$SERVICE_USER" \ + "$INSTALL_DIR/backend/data/output" \ + "$INSTALL_DIR/backend/data/logs" + chmod -R 775 \ + "$INSTALL_DIR/backend/data/output" \ + "$INSTALL_DIR/backend/data/logs" + ok "Verzeichnisse $ACTUAL_USER:$SERVICE_USER (775) zugewiesen" +else + ok "Verzeichnisse bereits $SERVICE_USER gehörig (kein SUDO_USER erkannt)" +fi + # --- Systemd-Dienst: Backend ------------------------------------------------- header "Systemd-Dienst (Backend) erstellen"