0.9.1-5 Minor fixes

This commit is contained in:
2026-03-14 10:06:59 +00:00
parent c3875803ff
commit 6378a9a4cb
9 changed files with 155 additions and 40 deletions

View File

@@ -1,12 +1,12 @@
{ {
"name": "ripster-backend", "name": "ripster-backend",
"version": "0.9.1-4", "version": "0.9.1-5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ripster-backend", "name": "ripster-backend",
"version": "0.9.1-4", "version": "0.9.1-5",
"dependencies": { "dependencies": {
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",

View File

@@ -1,6 +1,6 @@
{ {
"name": "ripster-backend", "name": "ripster-backend",
"version": "0.9.1-4", "version": "0.9.1-5",
"private": true, "private": true,
"type": "commonjs", "type": "commonjs",
"scripts": { "scripts": {

View File

@@ -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({ onProgress && onProgress({
phase: 'encode', phase: 'encode',
trackEvent: 'complete', trackEvent: 'complete',
@@ -615,6 +626,7 @@ function buildEncodeArgs(format, opts, track, meta, wavFile, outFile) {
cmd: 'flac', cmd: 'flac',
args: [ args: [
`--compression-level-${clampedLevel}`, `--compression-level-${clampedLevel}`,
'--no-delete-input-file', // flac deletes input WAV by default; keep RAW folder filled
'--tag', `TITLE=${trackTitle}`, '--tag', `TITLE=${trackTitle}`,
'--tag', `ARTIST=${artist}`, '--tag', `ARTIST=${artist}`,
'--tag', `ALBUM=${album}`, '--tag', `ALBUM=${album}`,

View File

@@ -1,12 +1,12 @@
{ {
"name": "ripster-frontend", "name": "ripster-frontend",
"version": "0.9.1-4", "version": "0.9.1-5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ripster-frontend", "name": "ripster-frontend",
"version": "0.9.1-4", "version": "0.9.1-5",
"dependencies": { "dependencies": {
"primeicons": "^7.0.0", "primeicons": "^7.0.0",
"primereact": "^10.9.2", "primereact": "^10.9.2",

View File

@@ -1,6 +1,6 @@
{ {
"name": "ripster-frontend", "name": "ripster-frontend",
"version": "0.9.1-4", "version": "0.9.1-5",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {

View File

@@ -532,6 +532,84 @@ function resolveAudioEncoderPreviewLabel(track, encoderToken, copyMask, fallback
return `Transcode (${normalizedToken})`; 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) { function buildAudioActionPreviewSummary(track, selectedIndex, audioSelector) {
const selector = audioSelector && typeof audioSelector === 'object' ? audioSelector : {}; const selector = audioSelector && typeof audioSelector === 'object' ? audioSelector : {};
const availableEncoders = Array.isArray(selector.encoders) ? selector.encoders : []; const availableEncoders = Array.isArray(selector.encoders) ? selector.encoders : [];
@@ -759,6 +837,9 @@ export default function MediaInfoReviewPanel({
const effectivePresetOverride = selectedUserPreset const effectivePresetOverride = selectedUserPreset
? { handbrakePreset: selectedUserPreset.handbrakePreset || '', extraArgs: selectedUserPreset.extraArgs || '' } ? { handbrakePreset: selectedUserPreset.handbrakePreset || '', extraArgs: selectedUserPreset.extraArgs || '' }
: null; : null;
const effectiveAudioSelector = effectivePresetOverride?.extraArgs
? parseAudioSelectorFromArgs(effectivePresetOverride.extraArgs, review?.selectors?.audio)
: (review?.selectors?.audio || null);
const hasUserPresets = normalizedUserPresets.length > 0; const hasUserPresets = normalizedUserPresets.length > 0;
const allowUserPresetSelection = hasUserPresets && typeof onUserPresetChange === 'function' && allowEncodeItemSelection; const allowUserPresetSelection = hasUserPresets && typeof onUserPresetChange === 'function' && allowEncodeItemSelection;
@@ -843,10 +924,10 @@ export default function MediaInfoReviewPanel({
<div><strong>Preset-Profil:</strong> {effectivePresetOverride ? 'user-preset' : (review.selectors?.presetProfileSource || '-')}</div> <div><strong>Preset-Profil:</strong> {effectivePresetOverride ? 'user-preset' : (review.selectors?.presetProfileSource || '-')}</div>
<div><strong>MIN_LENGTH_MINUTES:</strong> {review.minLengthMinutes}</div> <div><strong>MIN_LENGTH_MINUTES:</strong> {review.minLengthMinutes}</div>
<div><strong>Encode Input:</strong> {encodeInputTitle?.fileName || '-'}</div> <div><strong>Encode Input:</strong> {encodeInputTitle?.fileName || '-'}</div>
<div><strong>Audio Auswahl:</strong> {review.selectors?.audio?.mode || '-'}</div> <div><strong>Audio Auswahl:</strong> {effectiveAudioSelector?.mode || '-'}</div>
<div><strong>Audio Encoder:</strong> {(review.selectors?.audio?.encoders || []).join(', ') || 'Preset-Default'}</div> <div><strong>Audio Encoder:</strong> {(effectiveAudioSelector?.encoders || []).join(', ') || 'Preset-Default'}</div>
<div><strong>Audio Copy-Mask:</strong> {(review.selectors?.audio?.copyMask || []).join(', ') || '-'}</div> <div><strong>Audio Copy-Mask:</strong> {(effectiveAudioSelector?.copyMask || []).join(', ') || '-'}</div>
<div><strong>Audio Fallback:</strong> {review.selectors?.audio?.fallbackEncoder || '-'}</div> <div><strong>Audio Fallback:</strong> {effectiveAudioSelector?.fallbackEncoder || '-'}</div>
<div><strong>Subtitle Auswahl:</strong> {review.selectors?.subtitle?.mode || '-'}</div> <div><strong>Subtitle Auswahl:</strong> {review.selectors?.subtitle?.mode || '-'}</div>
<div><strong>Subtitle Flags:</strong> {review.selectors?.subtitle?.forcedOnly ? 'forced-only' : '-'}{review.selectors?.subtitle?.burnBehavior === 'first' ? ' + burned(first)' : ''}</div> <div><strong>Subtitle Flags:</strong> {review.selectors?.subtitle?.forcedOnly ? 'forced-only' : '-'}{review.selectors?.subtitle?.burnBehavior === 'first' ? ' + burned(first)' : ''}</div>
</div> </div>
@@ -1206,7 +1287,7 @@ export default function MediaInfoReviewPanel({
type="audio" type="audio"
allowSelection={allowTrackSelectionForTitle} allowSelection={allowTrackSelectionForTitle}
selectedTrackIds={selectedAudioTrackIds} selectedTrackIds={selectedAudioTrackIds}
audioSelector={review?.selectors?.audio || null} audioSelector={effectiveAudioSelector}
onToggleTrack={(trackId, checked) => { onToggleTrack={(trackId, checked) => {
if (!allowTrackSelectionForTitle || typeof onTrackSelectionChange !== 'function') { if (!allowTrackSelectionForTitle || typeof onTrackSelectionChange !== 'function') {
return; return;

View File

@@ -667,7 +667,7 @@ export default function HistoryPage() {
const confirmDeleteEntry = async (target) => { const confirmDeleteEntry = async (target) => {
const normalizedTarget = String(target || '').trim().toLowerCase(); const normalizedTarget = String(target || '').trim().toLowerCase();
if (!['raw', 'movie', 'both'].includes(normalizedTarget)) { if (!['raw', 'movie', 'both', 'none'].includes(normalizedTarget)) {
return; return;
} }
const jobId = Number(deleteEntryDialogRow?.id || 0); const jobId = Number(deleteEntryDialogRow?.id || 0);
@@ -686,10 +686,13 @@ export default function HistoryPage() {
const rawDirs = Number(fileSummary?.raw?.dirsRemoved || 0); const rawDirs = Number(fileSummary?.raw?.dirsRemoved || 0);
const movieDirs = Number(fileSummary?.movie?.dirsRemoved || 0); const movieDirs = Number(fileSummary?.movie?.dirsRemoved || 0);
const detail = normalizedTarget === 'none'
? `${deletedJobIds.length || 1} Eintrag/Einträge entfernt (Dateien bleiben erhalten)`
: `${deletedJobIds.length || 1} Eintrag/Einträge entfernt | RAW: ${rawFiles} Dateien, ${rawDirs} Ordner | ${deleteEntryOutputShortLabel}: ${movieFiles} Dateien, ${movieDirs} Ordner`;
toastRef.current?.show({ toastRef.current?.show({
severity: 'success', severity: 'success',
summary: 'Historie gelöscht', summary: 'Historie gelöscht',
detail: `${deletedJobIds.length || 1} Eintrag/Einträge entfernt | RAW: ${rawFiles} Dateien, ${rawDirs} Ordner | ${deleteEntryOutputShortLabel}: ${movieFiles} Dateien, ${movieDirs} Ordner`, detail,
life: 5000 life: 5000
}); });
@@ -1124,36 +1127,46 @@ export default function HistoryPage() {
<div> <div>
<h4>{`RAW (${previewRawExisting.length}/${previewRawPaths.length})`}</h4> <h4>{`RAW (${previewRawExisting.length}/${previewRawPaths.length})`}</h4>
{previewRawPaths.length > 0 ? ( {previewRawPaths.length > 0 ? (() => {
<ul className="history-delete-preview-list"> const display = previewRawPaths.filter(p => p.exists).length > 0
{previewRawPaths.map((item) => ( ? previewRawPaths.filter(p => p.exists)
<li key={`delete-raw-${item.path}`}> : previewRawPaths.slice(0, 1);
<span className={item.exists ? 'exists-yes' : 'exists-no'}> return (
{item.exists ? 'vorhanden' : 'nicht gefunden'} <ul className="history-delete-preview-list">
</span> {display.map((item) => (
{' '}| {item.path} <li key={`delete-raw-${item.path}`}>
</li> <span className={item.exists ? 'exists-yes' : 'exists-no'}>
))} {item.exists ? 'vorhanden' : 'nicht gefunden'}
</ul> </span>
) : ( {' '}| {item.path}
</li>
))}
</ul>
);
})() : (
<small className="history-dv-subtle">Keine RAW-Pfade.</small> <small className="history-dv-subtle">Keine RAW-Pfade.</small>
)} )}
</div> </div>
<div> <div>
<h4>{`${deleteEntryOutputShortLabel} (${previewMovieExisting.length}/${previewMoviePaths.length})`}</h4> <h4>{`${deleteEntryOutputShortLabel} (${previewMovieExisting.length}/${previewMoviePaths.length})`}</h4>
{previewMoviePaths.length > 0 ? ( {previewMoviePaths.length > 0 ? (() => {
<ul className="history-delete-preview-list"> const display = previewMoviePaths.filter(p => p.exists).length > 0
{previewMoviePaths.map((item) => ( ? previewMoviePaths.filter(p => p.exists)
<li key={`delete-movie-${item.path}`}> : previewMoviePaths.slice(0, 1);
<span className={item.exists ? 'exists-yes' : 'exists-no'}> return (
{item.exists ? 'vorhanden' : 'nicht gefunden'} <ul className="history-delete-preview-list">
</span> {display.map((item) => (
{' '}| {item.path} <li key={`delete-movie-${item.path}`}>
</li> <span className={item.exists ? 'exists-yes' : 'exists-no'}>
))} {item.exists ? 'vorhanden' : 'nicht gefunden'}
</ul> </span>
) : ( {' '}| {item.path}
</li>
))}
</ul>
);
})() : (
<small className="history-dv-subtle">Keine Movie-Pfade.</small> <small className="history-dv-subtle">Keine Movie-Pfade.</small>
)} )}
</div> </div>
@@ -1187,6 +1200,15 @@ export default function HistoryPage() {
loading={deleteEntryTargetBusy === 'both'} loading={deleteEntryTargetBusy === 'both'}
disabled={deleteTargetActionsDisabled} disabled={deleteTargetActionsDisabled}
/> />
<Button
label="Nur Eintrag löschen"
icon="pi pi-database"
severity="secondary"
outlined
onClick={() => confirmDeleteEntry('none')}
loading={deleteEntryTargetBusy === 'none'}
disabled={deleteTargetActionsDisabled}
/>
<Button <Button
label="Abbrechen" label="Abbrechen"
severity="secondary" severity="secondary"

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "ripster", "name": "ripster",
"version": "0.9.1-4", "version": "0.9.1-5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ripster", "name": "ripster",
"version": "0.9.1-4", "version": "0.9.1-5",
"devDependencies": { "devDependencies": {
"concurrently": "^9.1.2" "concurrently": "^9.1.2"
} }

View File

@@ -1,7 +1,7 @@
{ {
"name": "ripster", "name": "ripster",
"private": true, "private": true,
"version": "0.9.1-4", "version": "0.9.1-5",
"scripts": { "scripts": {
"dev": "concurrently \"npm run dev --prefix backend\" \"npm run dev --prefix frontend\"", "dev": "concurrently \"npm run dev --prefix backend\" \"npm run dev --prefix frontend\"",
"dev:backend": "npm run dev --prefix backend", "dev:backend": "npm run dev --prefix backend",