diff --git a/backend/src/db/database.js b/backend/src/db/database.js index 6278e5d..91dbf9f 100644 --- a/backend/src/db/database.js +++ b/backend/src/db/database.js @@ -855,6 +855,21 @@ async function migrateSettingsSchemaMetadata(db) { logger.info('migrate:settings-schema-category-moved', { key: move.key, category: move.category }); } } + + const rawDirCdLabel = 'CD RAW-Ordner'; + const rawDirCdDescription = 'Basisordner für CD-Rips. Enthält die WAV-Rohdaten (RAW) sowie den encodierten Audio-Output. Leer = Standardpfad (data/output/cd).'; + const rawDirCdResult = await db.run( + `UPDATE settings_schema + SET label = ?, description = ?, updated_at = CURRENT_TIMESTAMP + WHERE key = 'raw_dir_cd' AND (label != ? OR description != ?)`, + [rawDirCdLabel, rawDirCdDescription, rawDirCdLabel, rawDirCdDescription] + ); + if (rawDirCdResult?.changes > 0) { + logger.info('migrate:settings-schema-cd-raw-updated', { + key: 'raw_dir_cd', + label: rawDirCdLabel + }); + } } async function getDb() { diff --git a/backend/src/services/cdRipService.js b/backend/src/services/cdRipService.js index 24a8358..45f0b8d 100644 --- a/backend/src/services/cdRipService.js +++ b/backend/src/services/cdRipService.js @@ -321,6 +321,20 @@ function formatCommandLine(cmd, args = []) { return [quoteShellArg(cmd), ...normalizedArgs.map((arg) => quoteShellArg(arg))].join(' '); } +function copyFilePreservingRaw(sourcePath, targetPath) { + const rawSource = String(sourcePath || '').trim(); + const rawTarget = String(targetPath || '').trim(); + if (!rawSource || !rawTarget) { + return; + } + const source = path.resolve(rawSource); + const target = path.resolve(rawTarget); + if (source === target) { + return; + } + fs.copyFileSync(source, target); +} + async function runProcessTracked({ cmd, args, @@ -492,7 +506,7 @@ async function ripAndEncode(options) { // ── Phase 2: Encode WAVs to target format ───────────────────────────────── if (format === 'wav') { - // Just move WAV files to output dir with proper names + // Keep RAW WAVs in place and copy them to the final output structure. for (let i = 0; i < tracksToRip.length; i++) { assertNotCancelled(isCancelled); const track = tracksToRip[i]; @@ -508,8 +522,8 @@ async function ripAndEncode(options) { percent: 50 + ((i / tracksToRip.length) * 50) }); ensureDir(path.dirname(outFile)); - log('info', `Promptkette [Move ${i + 1}/${tracksToRip.length}]: mv ${quoteShellArg(wavFile)} ${quoteShellArg(outFile)}`); - fs.renameSync(wavFile, outFile); + log('info', `Promptkette [Copy ${i + 1}/${tracksToRip.length}]: cp ${quoteShellArg(wavFile)} ${quoteShellArg(outFile)}`); + copyFilePreservingRaw(wavFile, outFile); onProgress && onProgress({ phase: 'encode', trackEvent: 'complete', diff --git a/backend/src/services/hardwareMonitorService.js b/backend/src/services/hardwareMonitorService.js index 7b50aeb..ccec11d 100644 --- a/backend/src/services/hardwareMonitorService.js +++ b/backend/src/services/hardwareMonitorService.js @@ -421,7 +421,7 @@ class HardwareMonitorService { } else { addPath('raw_dir', 'RAW-Verzeichnis', blurayRawPath || dvdRawPath || sourceMap.raw_dir); } - addPath('raw_dir_cd', 'CD-Verzeichnis', cdRawPath || sourceMap.raw_dir_cd); + addPath('raw_dir_cd', 'CD RAW-Ordner', cdRawPath || sourceMap.raw_dir_cd); if (blurayMoviePath && dvdMoviePath && blurayMoviePath !== dvdMoviePath) { addPath('movie_dir_bluray', 'Movie-Verzeichnis (Blu-ray)', blurayMoviePath); diff --git a/backend/src/services/historyService.js b/backend/src/services/historyService.js index 8996161..15b1e9d 100644 --- a/backend/src/services/historyService.js +++ b/backend/src/services/historyService.js @@ -21,7 +21,7 @@ function parseJsonSafe(raw, fallback = null) { const PROCESS_LOG_TAIL_MAX_BYTES = 1024 * 1024; const processLogStreams = new Map(); -const PROFILE_PATH_SUFFIXES = ['bluray', 'dvd', 'other']; +const PROFILE_PATH_SUFFIXES = ['bluray', 'dvd', 'cd', 'other']; const RAW_INCOMPLETE_PREFIX = 'Incomplete_'; const RAW_RIP_COMPLETE_PREFIX = 'Rip_Complete_'; @@ -356,12 +356,46 @@ function toProcessLogStreamKey(jobId) { return String(Math.trunc(normalizedId)); } -function resolveEffectiveRawPath(storedPath, rawDir) { +function resolveEffectiveRawPath(storedPath, rawDir, extraDirs = []) { const stored = String(storedPath || '').trim(); - if (!stored || !rawDir) return stored; + if (!stored) return stored; const folderName = path.basename(stored); if (!folderName) return stored; - return path.join(String(rawDir).trim(), folderName); + + const candidates = []; + const seen = new Set(); + const pushCandidate = (candidatePath) => { + const normalized = String(candidatePath || '').trim(); + if (!normalized) { + return; + } + const comparable = normalizeComparablePath(normalized); + if (!comparable || seen.has(comparable)) { + return; + } + seen.add(comparable); + candidates.push(normalized); + }; + + pushCandidate(stored); + if (rawDir) { + pushCandidate(path.join(String(rawDir).trim(), folderName)); + } + for (const extraDir of Array.isArray(extraDirs) ? extraDirs : []) { + pushCandidate(path.join(String(extraDir || '').trim(), folderName)); + } + + for (const candidate of candidates) { + try { + if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) { + return candidate; + } + } catch (_error) { + // ignore fs errors and continue with fallbacks + } + } + + return rawDir ? path.join(String(rawDir).trim(), folderName) : stored; } function resolveEffectiveOutputPath(storedPath, movieDir) { @@ -406,11 +440,11 @@ function resolveEffectiveStoragePathsForJob(settings = null, job = {}, parsed = const rawDir = String(effectiveSettings?.raw_dir || '').trim(); const configuredMovieDir = String(effectiveSettings?.movie_dir || '').trim(); const movieDir = mediaType === 'cd' ? rawDir : configuredMovieDir; - const effectiveRawPath = mediaType === 'cd' - ? (job?.raw_path || null) - : (rawDir && job?.raw_path - ? resolveEffectiveRawPath(job.raw_path, rawDir) - : (job?.raw_path || null)); + const rawLookupDirs = getConfiguredMediaPathList(settings || {}, 'raw_dir') + .filter((candidate) => normalizeComparablePath(candidate) !== normalizeComparablePath(rawDir)); + const effectiveRawPath = job?.raw_path + ? resolveEffectiveRawPath(job.raw_path, rawDir, rawLookupDirs) + : (job?.raw_path || null); const effectiveOutputPath = mediaType === 'cd' ? (job?.output_path || null) : (configuredMovieDir && job?.output_path diff --git a/backend/src/services/pipelineService.js b/backend/src/services/pipelineService.js index ef6b371..fb239ee 100644 --- a/backend/src/services/pipelineService.js +++ b/backend/src/services/pipelineService.js @@ -3694,6 +3694,46 @@ class PipelineService extends EventEmitter { return existingDirectories[0]; } + buildRawPathLookupConfig(settingsMap = {}, mediaProfile = null) { + const sourceMap = settingsMap && typeof settingsMap === 'object' ? settingsMap : {}; + const normalizedMediaProfile = normalizeMediaProfile(mediaProfile); + const effectiveSettings = settingsService.resolveEffectiveToolSettings(sourceMap, normalizedMediaProfile); + const preferredDefaultRawDir = normalizedMediaProfile === 'cd' + ? settingsService.DEFAULT_CD_DIR + : settingsService.DEFAULT_RAW_DIR; + const uniqueRawDirs = Array.from( + new Set( + [ + effectiveSettings?.raw_dir, + sourceMap?.raw_dir, + sourceMap?.raw_dir_bluray, + sourceMap?.raw_dir_dvd, + sourceMap?.raw_dir_cd, + preferredDefaultRawDir, + settingsService.DEFAULT_RAW_DIR, + settingsService.DEFAULT_CD_DIR + ] + .map((item) => String(item || '').trim()) + .filter(Boolean) + ) + ); + + return { + effectiveSettings, + rawBaseDir: uniqueRawDirs[0] || String(preferredDefaultRawDir || '').trim() || null, + rawExtraDirs: uniqueRawDirs.slice(1) + }; + } + + resolveCurrentRawPathForSettings(settingsMap = {}, mediaProfile = null, storedRawPath = null) { + const stored = String(storedRawPath || '').trim(); + if (!stored) { + return null; + } + const { rawBaseDir, rawExtraDirs } = this.buildRawPathLookupConfig(settingsMap, mediaProfile); + return this.resolveCurrentRawPath(rawBaseDir, stored, rawExtraDirs); + } + async migrateRawFolderNamingOnStartup(db) { const settings = await settingsService.getSettingsMap(); const rawBaseDir = String(settings?.raw_dir || settingsService.DEFAULT_RAW_DIR || '').trim(); @@ -5385,15 +5425,17 @@ class PipelineService extends EventEmitter { }; } + const existingPlan = this.safeParseJson(job.encode_plan_json); const refreshSettings = await settingsService.getSettingsMap(); - const refreshRawBaseDir = settingsService.DEFAULT_RAW_DIR; - const refreshRawExtraDirs = [ - refreshSettings?.raw_dir_bluray, - refreshSettings?.raw_dir_dvd - ].map((d) => String(d || '').trim()).filter(Boolean); - const resolvedRefreshRawPath = job.raw_path - ? this.resolveCurrentRawPath(refreshRawBaseDir, job.raw_path, refreshRawExtraDirs) - : null; + const refreshMediaProfile = this.resolveMediaProfileForJob(job, { + encodePlan: existingPlan, + rawPath: job.raw_path + }); + const resolvedRefreshRawPath = this.resolveCurrentRawPathForSettings( + refreshSettings, + refreshMediaProfile, + job.raw_path + ); if (!resolvedRefreshRawPath) { return { @@ -5409,7 +5451,6 @@ class PipelineService extends EventEmitter { 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; @@ -7339,14 +7380,11 @@ class PipelineService extends EventEmitter { encodePlan: confirmedPlan }); const confirmSettings = await settingsService.getEffectiveSettingsMap(readyMediaProfile); - const confirmRawBaseDir = String(confirmSettings?.raw_dir || '').trim(); - const confirmRawExtraDirs = [ - confirmSettings?.raw_dir_bluray, - confirmSettings?.raw_dir_dvd - ].map((d) => String(d || '').trim()).filter(Boolean); - const resolvedConfirmRawPath = job.raw_path - ? this.resolveCurrentRawPath(confirmRawBaseDir, job.raw_path, confirmRawExtraDirs) - : null; + const resolvedConfirmRawPath = this.resolveCurrentRawPathForSettings( + confirmSettings, + readyMediaProfile, + job.raw_path + ); const activeConfirmRawPath = resolvedConfirmRawPath || String(job.raw_path || '').trim() || null; let inputPath = isPreRipMode @@ -7460,13 +7498,16 @@ class PipelineService extends EventEmitter { throw error; } + const reencodeMediaProfile = this.resolveMediaProfileForJob(sourceJob, { + makemkvInfo: mkInfo, + rawPath: sourceJob.raw_path + }); const reencodeSettings = await settingsService.getSettingsMap(); - const reencodeRawBaseDir = settingsService.DEFAULT_RAW_DIR; - const reencodeRawExtraDirs = [ - reencodeSettings?.raw_dir_bluray, - reencodeSettings?.raw_dir_dvd - ].map((d) => String(d || '').trim()).filter(Boolean); - const resolvedReencodeRawPath = this.resolveCurrentRawPath(reencodeRawBaseDir, sourceJob.raw_path, reencodeRawExtraDirs); + const resolvedReencodeRawPath = this.resolveCurrentRawPathForSettings( + reencodeSettings, + reencodeMediaProfile, + sourceJob.raw_path + ); if (!resolvedReencodeRawPath) { const error = new Error(`Re-Encode nicht möglich: RAW-Pfad existiert nicht (${sourceJob.raw_path}).`); error.statusCode = 400; @@ -8339,14 +8380,7 @@ class PipelineService extends EventEmitter { rawPath: job.raw_path }); const settings = await settingsService.getEffectiveSettingsMap(mediaProfile); - const rawBaseDir = String(settings.raw_dir || '').trim(); - const rawExtraDirs = [ - settings.raw_dir_bluray, - settings.raw_dir_dvd - ].map((item) => String(item || '').trim()).filter(Boolean); - const resolvedRawPath = job.raw_path - ? this.resolveCurrentRawPath(rawBaseDir, job.raw_path, rawExtraDirs) - : null; + const resolvedRawPath = this.resolveCurrentRawPathForSettings(settings, mediaProfile, job.raw_path); const activeRawPath = resolvedRawPath || String(job.raw_path || '').trim() || null; if (activeRawPath && normalizeComparablePath(activeRawPath) !== normalizeComparablePath(job.raw_path)) { await historyService.updateJob(jobId, { raw_path: activeRawPath }); @@ -9251,14 +9285,15 @@ class PipelineService extends EventEmitter { }; } else { const retrySettings = await settingsService.getEffectiveSettingsMap(mediaProfile); - const retryRawBaseDir = String(retrySettings?.raw_dir || '').trim(); - const retryRawExtraDirs = [ - retrySettings?.raw_dir_bluray, - retrySettings?.raw_dir_dvd - ].map((dirPath) => String(dirPath || '').trim()).filter(Boolean); - const resolvedOldRawPath = sourceJob.raw_path - ? this.resolveCurrentRawPath(retryRawBaseDir, sourceJob.raw_path, retryRawExtraDirs) - : null; + const { rawBaseDir: retryRawBaseDir, rawExtraDirs: retryRawExtraDirs } = this.buildRawPathLookupConfig( + retrySettings, + mediaProfile + ); + const resolvedOldRawPath = this.resolveCurrentRawPathForSettings( + retrySettings, + mediaProfile, + sourceJob.raw_path + ); if (resolvedOldRawPath) { const oldRawFolderName = path.basename(resolvedOldRawPath); @@ -9419,15 +9454,13 @@ class PipelineService extends EventEmitter { const mode = String(encodePlan?.mode || 'rip').trim().toLowerCase(); const isPreRipMode = mode === 'pre_rip' || Boolean(encodePlan?.preRip); const reviewConfirmed = Boolean(Number(job.encode_review_confirmed || 0) || encodePlan?.reviewConfirmed); - const resumeSettings = await settingsService.getEffectiveSettingsMap(this.resolveMediaProfileForJob(job, { encodePlan })); - const resumeRawBaseDir = String(resumeSettings?.raw_dir || '').trim(); - const resumeRawExtraDirs = [ - resumeSettings?.raw_dir_bluray, - resumeSettings?.raw_dir_dvd - ].map((d) => String(d || '').trim()).filter(Boolean); - const resolvedResumeRawPath = job.raw_path - ? this.resolveCurrentRawPath(resumeRawBaseDir, job.raw_path, resumeRawExtraDirs) - : null; + const readyMediaProfile = this.resolveMediaProfileForJob(job, { encodePlan }); + const resumeSettings = await settingsService.getEffectiveSettingsMap(readyMediaProfile); + const resolvedResumeRawPath = this.resolveCurrentRawPathForSettings( + resumeSettings, + readyMediaProfile, + job.raw_path + ); const activeResumeRawPath = resolvedResumeRawPath || String(job.raw_path || '').trim() || null; let inputPath = isPreRipMode @@ -9463,10 +9496,6 @@ class PipelineService extends EventEmitter { imdbId: job.imdb_id || null, poster: job.poster_url || null }; - const readyMediaProfile = this.resolveMediaProfileForJob(job, { - encodePlan - }); - await this.setState('READY_TO_ENCODE', { activeJobId: jobId, progress: 0, @@ -9613,14 +9642,11 @@ class PipelineService extends EventEmitter { encodePlan: restartPlan }); const restartSettings = await settingsService.getEffectiveSettingsMap(readyMediaProfile); - const restartRawBaseDir = String(restartSettings?.raw_dir || '').trim(); - const restartRawExtraDirs = [ - restartSettings?.raw_dir_bluray, - restartSettings?.raw_dir_dvd - ].map((d) => String(d || '').trim()).filter(Boolean); - const resolvedRestartRawPath = job.raw_path - ? this.resolveCurrentRawPath(restartRawBaseDir, job.raw_path, restartRawExtraDirs) - : null; + const resolvedRestartRawPath = this.resolveCurrentRawPathForSettings( + restartSettings, + readyMediaProfile, + job.raw_path + ); const activeRestartRawPath = resolvedRestartRawPath || String(job.raw_path || '').trim() || null; let inputPath = isPreRipMode @@ -9761,13 +9787,19 @@ class PipelineService extends EventEmitter { throw error; } + const reviewMakemkvInfo = this.safeParseJson(sourceJob.makemkv_info_json); + const reviewEncodePlan = this.safeParseJson(sourceJob.encode_plan_json); + const reviewMediaProfile = this.resolveMediaProfileForJob(sourceJob, { + makemkvInfo: reviewMakemkvInfo, + encodePlan: reviewEncodePlan, + rawPath: sourceJob.raw_path + }); const reviewSettings = await settingsService.getSettingsMap(); - const reviewRawBaseDir = settingsService.DEFAULT_RAW_DIR; - const reviewRawExtraDirs = [ - reviewSettings?.raw_dir_bluray, - reviewSettings?.raw_dir_dvd - ].map((d) => String(d || '').trim()).filter(Boolean); - const resolvedReviewRawPath = this.resolveCurrentRawPath(reviewRawBaseDir, sourceJob.raw_path, reviewRawExtraDirs); + const resolvedReviewRawPath = this.resolveCurrentRawPathForSettings( + reviewSettings, + reviewMediaProfile, + sourceJob.raw_path + ); if (!resolvedReviewRawPath) { const error = new Error(`Review-Neustart nicht möglich: RAW-Pfad existiert nicht (${sourceJob.raw_path}).`); error.statusCode = 400; diff --git a/frontend/src/components/DynamicSettingsForm.jsx b/frontend/src/components/DynamicSettingsForm.jsx index da9bd65..0ad4db9 100644 --- a/frontend/src/components/DynamicSettingsForm.jsx +++ b/frontend/src/components/DynamicSettingsForm.jsx @@ -380,7 +380,7 @@ function PathCategoryTab({ settings, values, errors, dirtyKeys, onChange, effect const blurayMovies = ep.bluray?.movies || defaultMovies; const dvdRaw = ep.dvd?.raw || defaultRaw; const dvdMovies = ep.dvd?.movies || defaultMovies; - const cdOutput = ep.cd?.raw || defaultCd; + const cdRaw = ep.cd?.raw || defaultCd; const isDefault = (path, def) => path === def; @@ -424,10 +424,10 @@ function PathCategoryTab({ settings, values, errors, dirtyKeys, onChange, effect
{cdOutput}
- {isDefault(cdOutput, defaultCd) && Standard}
+ {cdRaw}
+ {isDefault(cdRaw, defaultCd) && Standard}