diff --git a/backend/package-lock.json b/backend/package-lock.json index fd16426..7b3b361 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,12 +1,12 @@ { "name": "ripster-backend", - "version": "0.9.1-4", + "version": "0.9.1-5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ripster-backend", - "version": "0.9.1-4", + "version": "0.9.1-5", "dependencies": { "cors": "^2.8.5", "dotenv": "^16.4.7", diff --git a/backend/package.json b/backend/package.json index e2ea817..cc86f03 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "ripster-backend", - "version": "0.9.1-4", + "version": "0.9.1-5", "private": true, "type": "commonjs", "scripts": { diff --git a/backend/src/services/cdRipService.js b/backend/src/services/cdRipService.js index 45f0b8d..e966030 100644 --- a/backend/src/services/cdRipService.js +++ b/backend/src/services/cdRipService.js @@ -585,6 +585,17 @@ async function ripAndEncode(options) { ); } + // Safety net: some encoders (e.g. older flac without --no-delete-input-file) may remove + // the source WAV. Restore it from the encoded output so the RAW folder stays filled. + if (!fs.existsSync(wavFile) && fs.existsSync(outFile)) { + try { + fs.copyFileSync(outFile, wavFile); + log('info', `Track ${track.position}: WAV-Quelldatei vom Encoder gelöscht – aus Output wiederhergestellt.`); + } catch (restoreErr) { + log('warn', `Track ${track.position}: WAV-Wiederherstellung fehlgeschlagen: ${restoreErr.message}`); + } + } + onProgress && onProgress({ phase: 'encode', trackEvent: 'complete', @@ -615,6 +626,7 @@ function buildEncodeArgs(format, opts, track, meta, wavFile, outFile) { cmd: 'flac', args: [ `--compression-level-${clampedLevel}`, + '--no-delete-input-file', // flac deletes input WAV by default; keep RAW folder filled '--tag', `TITLE=${trackTitle}`, '--tag', `ARTIST=${artist}`, '--tag', `ALBUM=${album}`, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a00a739..1cfeaf4 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "ripster-frontend", - "version": "0.9.1-4", + "version": "0.9.1-5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ripster-frontend", - "version": "0.9.1-4", + "version": "0.9.1-5", "dependencies": { "primeicons": "^7.0.0", "primereact": "^10.9.2", diff --git a/frontend/package.json b/frontend/package.json index 5922f74..6c98de8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "ripster-frontend", - "version": "0.9.1-4", + "version": "0.9.1-5", "private": true, "type": "module", "scripts": { diff --git a/frontend/src/components/MediaInfoReviewPanel.jsx b/frontend/src/components/MediaInfoReviewPanel.jsx index 25147e7..3b09168 100644 --- a/frontend/src/components/MediaInfoReviewPanel.jsx +++ b/frontend/src/components/MediaInfoReviewPanel.jsx @@ -532,6 +532,84 @@ function resolveAudioEncoderPreviewLabel(track, encoderToken, copyMask, fallback return `Transcode (${normalizedToken})`; } +function parseAudioSelectorFromArgs(extraArgsString, baseSelector) { + const base = baseSelector && typeof baseSelector === 'object' ? baseSelector : {}; + const args = String(extraArgsString || '').trim(); + if (!args) { + return base; + } + + // Tokenize: split on whitespace but respect quoted strings + const tokens = []; + let current = ''; + let inQuote = null; + for (let i = 0; i < args.length; i++) { + const ch = args[i]; + if (inQuote) { + if (ch === inQuote) { + inQuote = null; + } else { + current += ch; + } + } else if (ch === '"' || ch === "'") { + inQuote = ch; + } else if (ch === ' ' || ch === '\t') { + if (current) { + tokens.push(current); + current = ''; + } + } else { + current += ch; + } + } + if (current) { + tokens.push(current); + } + + const result = { ...base }; + + const getNext = (i) => (i + 1 < tokens.length ? tokens[i + 1] : null); + + for (let i = 0; i < tokens.length; i++) { + const token = tokens[i]; + + // Support both --flag=value and --flag value + const eqIdx = token.indexOf('='); + const flag = eqIdx !== -1 ? token.slice(0, eqIdx) : token; + const inlineVal = eqIdx !== -1 ? token.slice(eqIdx + 1) : null; + + const getValue = () => { + if (inlineVal !== null) return inlineVal; + const next = getNext(i); + if (next && !next.startsWith('-')) { + i++; + return next; + } + return null; + }; + + if (flag === '--aencoder') { + const val = getValue(); + if (val) { + result.encoders = val.split(',').map((s) => s.trim()).filter(Boolean); + result.encoderSource = 'args'; + } + } else if (flag === '--audio-copy-mask') { + const val = getValue(); + if (val) { + result.copyMask = val.split(',').map((s) => s.trim()).filter(Boolean); + } + } else if (flag === '--audio-fallback') { + const val = getValue(); + if (val) { + result.fallbackEncoder = val.trim(); + } + } + } + + return result; +} + function buildAudioActionPreviewSummary(track, selectedIndex, audioSelector) { const selector = audioSelector && typeof audioSelector === 'object' ? audioSelector : {}; const availableEncoders = Array.isArray(selector.encoders) ? selector.encoders : []; @@ -759,6 +837,9 @@ export default function MediaInfoReviewPanel({ const effectivePresetOverride = selectedUserPreset ? { handbrakePreset: selectedUserPreset.handbrakePreset || '', extraArgs: selectedUserPreset.extraArgs || '' } : null; + const effectiveAudioSelector = effectivePresetOverride?.extraArgs + ? parseAudioSelectorFromArgs(effectivePresetOverride.extraArgs, review?.selectors?.audio) + : (review?.selectors?.audio || null); const hasUserPresets = normalizedUserPresets.length > 0; const allowUserPresetSelection = hasUserPresets && typeof onUserPresetChange === 'function' && allowEncodeItemSelection; @@ -843,10 +924,10 @@ export default function MediaInfoReviewPanel({