0.15.0-2 Misc Fixes

This commit is contained in:
2026-04-24 10:17:55 +00:00
parent 3e5840f329
commit 730bc50715
19 changed files with 1481 additions and 139 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "ripster-backend", "name": "ripster-backend",
"version": "0.15.0-1", "version": "0.15.0-2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ripster-backend", "name": "ripster-backend",
"version": "0.15.0-1", "version": "0.15.0-2",
"dependencies": { "dependencies": {
"archiver": "^7.0.1", "archiver": "^7.0.1",
"cors": "^2.8.5", "cors": "^2.8.5",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ripster-backend", "name": "ripster-backend",
"version": "0.15.0-1", "version": "0.15.0-2",
"private": true, "private": true,
"type": "commonjs", "type": "commonjs",
"scripts": { "scripts": {
+2
View File
@@ -770,6 +770,8 @@ async function migrateOutputTemplates(db) {
async function removeDeprecatedSettings(db) { async function removeDeprecatedSettings(db) {
const deprecatedKeys = [ const deprecatedKeys = [
'pushover_notify_disc_detected', 'pushover_notify_disc_detected',
'pushover_notify_cron_success',
'pushover_notify_cron_error',
'mediainfo_extra_args', 'mediainfo_extra_args',
'makemkv_rip_mode', 'makemkv_rip_mode',
'makemkv_analyze_extra_args', 'makemkv_analyze_extra_args',
+86 -4
View File
@@ -591,6 +591,32 @@ function toProcessLogPath(jobId) {
return path.join(getJobLogDir(), `job-${Math.trunc(normalizedId)}.process.log`); return path.join(getJobLogDir(), `job-${Math.trunc(normalizedId)}.process.log`);
} }
function buildArchivedProcessLogPath(sourceJobId, options = {}) {
const normalizedId = Number(sourceJobId);
if (!Number.isFinite(normalizedId) || normalizedId <= 0) {
return null;
}
const targetJobId = Number(options?.replacementJobId);
const safeReason = String(options?.reason || 'retired')
.trim()
.toLowerCase()
.replace(/[^a-z0-9_-]+/g, '-')
.replace(/^-+|-+$/g, '')
|| 'retired';
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const archiveDir = path.join(getJobLogDir(), 'archived');
const targetSuffix = Number.isFinite(targetJobId) && targetJobId > 0
? `.to-${Math.trunc(targetJobId)}`
: '';
return {
archiveDir,
archivePath: path.join(
archiveDir,
`job-${Math.trunc(normalizedId)}.process.${safeReason}${targetSuffix}.${timestamp}.log`
)
};
}
function hasProcessLogFile(jobId) { function hasProcessLogFile(jobId) {
const filePath = toProcessLogPath(jobId); const filePath = toProcessLogPath(jobId);
return Boolean(filePath && fs.existsSync(filePath)); return Boolean(filePath && fs.existsSync(filePath));
@@ -2944,7 +2970,17 @@ class HistoryService {
} }
await this.closeProcessLog(fromJobId); await this.closeProcessLog(fromJobId);
this._deleteProcessLogFile(fromJobId); const archivedProcessLogPath = this._archiveProcessLogFile(fromJobId, {
replacementJobId: toJobId,
reason
});
if (archivedProcessLogPath) {
this.appendProcessLog(
toJobId,
'SYSTEM',
`Vorheriger Prozess-Log von Job #${fromJobId} archiviert: ${archivedProcessLogPath}`
);
}
logger.warn('job:retired', { logger.warn('job:retired', {
sourceJobId: fromJobId, sourceJobId: fromJobId,
@@ -2957,7 +2993,8 @@ class HistoryService {
retired: true, retired: true,
sourceJobId: fromJobId, sourceJobId: fromJobId,
replacementJobId: toJobId, replacementJobId: toJobId,
reason reason,
archivedProcessLogPath: archivedProcessLogPath || null
}; };
} }
@@ -6708,6 +6745,33 @@ class HistoryService {
} }
} }
_archiveProcessLogFile(jobId, options = {}) {
const processLogPath = toProcessLogPath(jobId);
if (!processLogPath || !fs.existsSync(processLogPath)) {
return null;
}
const archiveTarget = buildArchivedProcessLogPath(jobId, options);
if (!archiveTarget?.archivePath) {
return null;
}
try {
fs.mkdirSync(archiveTarget.archiveDir, { recursive: true });
fs.renameSync(processLogPath, archiveTarget.archivePath);
return archiveTarget.archivePath;
} catch (error) {
logger.warn('job:process-log:archive-failed', {
jobId,
fromPath: processLogPath,
toPath: archiveTarget.archivePath,
reason: String(options?.reason || '').trim() || null,
replacementJobId: Number(options?.replacementJobId) || null,
error: error?.message || String(error)
});
return null;
}
}
async deleteJobFiles(jobId, target = 'both', options = {}) { async deleteJobFiles(jobId, target = 'both', options = {}) {
const allowedTargets = new Set(['raw', 'movie', 'both']); const allowedTargets = new Set(['raw', 'movie', 'both']);
if (!allowedTargets.has(target)) { if (!allowedTargets.has(target)) {
@@ -6992,9 +7056,12 @@ class HistoryService {
movieCandidatePathForTracking = outputPath; movieCandidatePathForTracking = outputPath;
} else { } else {
const parentDir = normalizeComparablePath(path.dirname(outputPath)); const parentDir = normalizeComparablePath(path.dirname(outputPath));
const parentDirName = String(path.basename(parentDir || '') || '').trim();
const isIncompleteMergeParentDir = /^incomplete_merge_.+_job_\d+\s*$/i.test(parentDirName);
// Converter jobs output a single file — never delete the parent dir // Converter jobs output a single file — never delete the parent dir
const isConverterJob = resolvedPaths.mediaType === 'converter'; const isConverterJob = resolvedPaths.mediaType === 'converter';
const canDeleteParentDir = !isConverterJob const canDeleteParentDir = !isConverterJob
&& !isIncompleteMergeParentDir
&& parentDir && parentDir
&& parentDir !== movieRoot && parentDir !== movieRoot
&& isPathInside(movieRoot, parentDir) && isPathInside(movieRoot, parentDir)
@@ -7388,11 +7455,26 @@ class HistoryService {
}; };
const collectIncompleteMoviePathsFromPreview = (preview) => { const collectIncompleteMoviePathsFromPreview = (preview) => {
const rows = Array.isArray(preview?.pathCandidates?.movie) ? preview.pathCandidates.movie : []; const rows = Array.isArray(preview?.pathCandidates?.movie) ? preview.pathCandidates.movie : [];
const incompleteJobPattern = /(^|[\\/])incomplete_job-\d+([\\/]|$)/i;
const incompleteMergePattern = /(^|[\\/])incomplete_merge_[^\\/]+_job_\d+([\\/]|$)/i;
return rows return rows
.filter((row) => Boolean(row?.exists)) .filter((row) => Boolean(row?.exists))
.filter((row) => {
const candidatePath = String(row?.path || '').trim();
if (!candidatePath) {
return false;
}
if (incompleteJobPattern.test(candidatePath)) {
return true;
}
if (incompleteMergePattern.test(candidatePath)) {
// Shared multipart merge folders must never be auto-selected as a whole directory.
return Boolean(row?.isFile);
}
return false;
})
.map((row) => String(row?.path || '').trim()) .map((row) => String(row?.path || '').trim())
.filter((candidatePath) => Boolean(candidatePath)) .filter(Boolean);
.filter((candidatePath) => /(^|[\\/])incomplete_job-\d+([\\/]|$)/i.test(candidatePath));
}; };
const cleanupIncompleteMovieFoldersForJobs = async (jobRows = [], ownerJobIds = []) => { const cleanupIncompleteMovieFoldersForJobs = async (jobRows = [], ownerJobIds = []) => {
const normalizedOwnerJobIds = Array.from(new Set( const normalizedOwnerJobIds = Array.from(new Set(
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "ripster-frontend", "name": "ripster-frontend",
"version": "0.15.0-1", "version": "0.15.0-2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ripster-frontend", "name": "ripster-frontend",
"version": "0.15.0-1", "version": "0.15.0-2",
"dependencies": { "dependencies": {
"primeicons": "^7.0.0", "primeicons": "^7.0.0",
"primereact": "^10.9.2", "primereact": "^10.9.2",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ripster-frontend", "name": "ripster-frontend",
"version": "0.15.0-1", "version": "0.15.0-2",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
+3 -3
View File
@@ -602,15 +602,15 @@ function App() {
<div <div
className="app-upload-banner-progress" className="app-upload-banner-progress"
aria-label={`Audiobook Upload ${Math.round(uploadProgress)} Prozent`} aria-label={`Audiobook Upload ${Math.trunc(uploadProgress)} Prozent`}
> >
<ProgressBar value={uploadProgress} showValue={false} /> <ProgressBar value={uploadProgress} showValue={false} />
<small> <small>
{uploadPhase === 'processing' {uploadPhase === 'processing'
? `100% | ${uploadBytesLabel || 'Upload abgeschlossen'}` ? `100% | ${uploadBytesLabel || 'Upload abgeschlossen'}`
: uploadBytesLabel : uploadBytesLabel
? `${Math.round(uploadProgress)}% | ${uploadBytesLabel}` ? `${Math.trunc(uploadProgress)}% | ${uploadBytesLabel}`
: `${Math.round(uploadProgress)}%`} : `${Math.trunc(uploadProgress)}%`}
</small> </small>
</div> </div>
@@ -297,9 +297,9 @@ export default function AudiobookConfigPanel({
</div> </div>
{isRunning ? ( {isRunning ? (
<div className="ripper-job-row-progress" aria-label={`Audiobook Fortschritt ${Math.round(progress)}%`}> <div className="ripper-job-row-progress" aria-label={`Audiobook Fortschritt ${Math.trunc(progress)}%`}>
<ProgressBar value={progress} showValue={false} /> <ProgressBar value={progress} showValue={false} />
<small>{Math.round(progress)}%{currentChapter ? ` | Kapitel ${currentChapter.index}/${currentChapter.total}: ${currentChapter.title}` : ''}</small> <small>{Math.trunc(progress)}%{currentChapter ? ` | Kapitel ${currentChapter.index}/${currentChapter.total}: ${currentChapter.title}` : ''}</small>
</div> </div>
) : null} ) : null}
@@ -182,15 +182,15 @@ export default function AudiobookUploadPanel({
) : null} ) : null}
<div <div
className="ripper-job-row-progress audiobook-upload-progress" className="ripper-job-row-progress audiobook-upload-progress"
aria-label={`Audiobook Upload ${Math.round(progress)} Prozent`} aria-label={`Audiobook Upload ${Math.trunc(progress)} Prozent`}
> >
<ProgressBar value={progress} showValue={false} /> <ProgressBar value={progress} showValue={false} />
<small> <small>
{phase === 'processing' {phase === 'processing'
? '100% | Upload fertig, Job wird vorbereitet ...' ? '100% | Upload fertig, Job wird vorbereitet ...'
: totalBytes > 0 : totalBytes > 0
? `${Math.round(progress)}% | ${formatBytes(loadedBytes)} / ${formatBytes(totalBytes)}` ? `${Math.trunc(progress)}% | ${formatBytes(loadedBytes)} / ${formatBytes(totalBytes)}`
: `${Math.round(progress)}%`} : `${Math.trunc(progress)}%`}
</small> </small>
</div> </div>
</div> </div>
+5 -8
View File
@@ -115,11 +115,8 @@ function formatProgressLabel(value) {
return '0%'; return '0%';
} }
const clamped = Math.max(0, Math.min(100, parsed)); const clamped = Math.max(0, Math.min(100, parsed));
const rounded = Math.round(clamped * 10) / 10; const whole = Math.trunc(clamped);
if (Number.isInteger(rounded)) { return `${whole}%`;
return `${rounded}%`;
}
return `${rounded.toFixed(1)}%`;
} }
function normalizeTrackStageStatus(value) { function normalizeTrackStageStatus(value) {
@@ -617,7 +614,7 @@ export default function CdRipConfigPanel({
}).length; }).length;
const progress = Number(pipeline?.progress ?? 0); const progress = Number(pipeline?.progress ?? 0);
const clampedProgress = Math.max(0, Math.min(100, progress)); const clampedProgress = Math.max(0, Math.min(100, progress));
const roundedProgress = Math.round(clampedProgress * 10) / 10; const wholeProgress = Math.trunc(clampedProgress);
const eta = String(pipeline?.eta || '').trim(); const eta = String(pipeline?.eta || '').trim();
const statusText = String(pipeline?.statusText || '').trim(); const statusText = String(pipeline?.statusText || '').trim();
const stateLabel = getStatusLabel(state); const stateLabel = getStatusLabel(state);
@@ -793,12 +790,12 @@ export default function CdRipConfigPanel({
{isRipping ? ( {isRipping ? (
<div className="progress-wrap"> <div className="progress-wrap">
<ProgressBar <ProgressBar
value={roundedProgress} value={wholeProgress}
showValue showValue
displayValueTemplate={(value) => formatProgressLabel(value)} displayValueTemplate={(value) => formatProgressLabel(value)}
/> />
<small> <small>
{`Fortschritt: ${formatProgressLabel(roundedProgress)} | ${livePhaseLabel} ${completedRipCount}/${selectedTrackRows.length || effectiveTrackRows.length} Rip | ${completedEncodeCount}/${selectedTrackRows.length || effectiveTrackRows.length} Encode`} {`Fortschritt: ${formatProgressLabel(wholeProgress)} | ${livePhaseLabel} ${completedRipCount}/${selectedTrackRows.length || effectiveTrackRows.length} Rip | ${completedEncodeCount}/${selectedTrackRows.length || effectiveTrackRows.length} Encode`}
</small> </small>
<small>{eta ? `ETA ${eta}` : 'ETA unbekannt'}</small> <small>{eta ? `ETA ${eta}` : 'ETA unbekannt'}</small>
</div> </div>
+1 -1
View File
@@ -1071,7 +1071,7 @@ export default function ConverterJobCard({
const liveProgress = jobProgress?.progress ?? null; const liveProgress = jobProgress?.progress ?? null;
const liveEta = jobProgress?.eta || null; const liveEta = jobProgress?.eta || null;
const liveStatusText = jobProgress?.statusText || null; const liveStatusText = jobProgress?.statusText || null;
const progressValue = liveProgress !== null ? Math.round(liveProgress) : null; const progressValue = liveProgress !== null ? Math.trunc(liveProgress) : null;
// ── Audio details (for active/terminal state) ───────────────────────────── // ── Audio details (for active/terminal state) ─────────────────────────────
const audioTracks = isAudio ? deriveAudioTracks(plan) : []; const audioTracks = isAudio ? deriveAudioTracks(plan) : [];
@@ -125,7 +125,7 @@ export default function ConverterUploadPanel({ onUploaded, allowedExtensions = n
// XHR für Upload-Fortschritt // XHR für Upload-Fortschritt
const result = await uploadWithProgress(formData, (pct) => { const result = await uploadWithProgress(formData, (pct) => {
setProgress(pct); setProgress(pct);
setStatusText(`Upload: ${Math.round(pct)}%`); setStatusText(`Upload: ${Math.trunc(pct)}%`);
}); });
setPhase('done'); setPhase('done');
+1 -1
View File
@@ -2001,7 +2001,7 @@ export default function JobDetailDialog({
<span><strong>Titel-ID:</strong> {episode?.titleId || '-'}</span> <span><strong>Titel-ID:</strong> {episode?.titleId || '-'}</span>
</div> </div>
<div className="track-item"> <div className="track-item">
<span><strong>Fortschritt:</strong> {`${boundedProgress.toFixed(2)}%`}</span> <span><strong>Fortschritt:</strong> {`${Math.trunc(boundedProgress)}%`}</span>
</div> </div>
<div className="track-item"> <div className="track-item">
<span><strong>Gestartet:</strong> {formatDateTimeOrDash(episode?.startedAt)}</span> <span><strong>Gestartet:</strong> {formatDateTimeOrDash(episode?.startedAt)}</span>
+33 -6
View File
@@ -660,12 +660,12 @@ function normalizeSeriesBatchProgress(value, fallback = 0) {
return Math.max(0, Math.min(100, parsed)); return Math.max(0, Math.min(100, parsed));
} }
function formatPercentTwoDecimals(value) { function formatPercentNoDecimals(value) {
const parsed = Number(value); const parsed = Number(value);
if (!Number.isFinite(parsed)) { if (!Number.isFinite(parsed)) {
return '0.00%'; return '0%';
} }
return `${parsed.toFixed(2)}%`; return `${Math.trunc(Math.max(0, Math.min(100, parsed)))}%`;
} }
function getSeriesBatchTagSeverity(status) { function getSeriesBatchTagSeverity(status) {
@@ -2528,6 +2528,15 @@ export default function PipelineStatusCard({
const manualDecisionState = pipeline?.context?.manualDecisionState || null; const manualDecisionState = pipeline?.context?.manualDecisionState || null;
const rawDecisionRequiredBeforeStart = state === 'WAITING_FOR_USER_DECISION' && manualDecisionState === 'waiting_for_raw_decision'; const rawDecisionRequiredBeforeStart = state === 'WAITING_FOR_USER_DECISION' && manualDecisionState === 'waiting_for_raw_decision';
const rawDecisionOptions = pipeline?.context?.rawDecisionOptions && typeof pipeline.context.rawDecisionOptions === 'object'
? pipeline.context.rawDecisionOptions
: null;
const rawDecisionAllowsMultipartMergeWithOrphan = Boolean(
rawDecisionRequiredBeforeStart
&& rawDecisionOptions?.allowMultipartMergeWithOrphan
);
const rawDecisionOrphanDiscNumber = normalizePositiveInt(rawDecisionOptions?.orphanDiscNumber);
const rawDecisionCurrentDiscNumber = normalizePositiveInt(rawDecisionOptions?.currentDiscNumber);
const playlistDecisionRequiredBeforeStart = state === 'WAITING_FOR_USER_DECISION' && !handBrakeTitleDecisionRequired && !rawDecisionRequiredBeforeStart; const playlistDecisionRequiredBeforeStart = state === 'WAITING_FOR_USER_DECISION' && !handBrakeTitleDecisionRequired && !rawDecisionRequiredBeforeStart;
const isSeriesDvdReview = useMemo(() => { const isSeriesDvdReview = useMemo(() => {
if (jobMediaProfile !== 'dvd' && jobMediaProfile !== 'bluray') { if (jobMediaProfile !== 'dvd' && jobMediaProfile !== 'bluray') {
@@ -2867,7 +2876,7 @@ export default function PipelineStatusCard({
: (pipeline?.eta || null); : (pipeline?.eta || null);
const primaryStatusText = hasSeriesBatchProgress const primaryStatusText = hasSeriesBatchProgress
? ( ? (
`Serien-Encoding ${primaryProgressValue.toFixed(2)}%` `Serien-Encoding ${formatPercentNoDecimals(primaryProgressValue)}`
+ `${runningSeriesEpisode?.title ? ` - ${runningSeriesEpisode.title}` : ''}` + `${runningSeriesEpisode?.title ? ` - ${runningSeriesEpisode.title}` : ''}`
) )
: (pipeline?.statusText || 'Bereit'); : (pipeline?.statusText || 'Bereit');
@@ -3365,7 +3374,7 @@ export default function PipelineStatusCard({
<ProgressBar <ProgressBar
value={primaryProgressValue} value={primaryProgressValue}
showValue showValue
displayValueTemplate={(value) => formatPercentTwoDecimals(value)} displayValueTemplate={(value) => formatPercentNoDecimals(value)}
/> />
<small> <small>
{primaryEta {primaryEta
@@ -3404,7 +3413,7 @@ export default function PipelineStatusCard({
<Tag value={getStatusLabel(child.status)} severity={getSeriesBatchTagSeverity(child.status)} /> <Tag value={getStatusLabel(child.status)} severity={getSeriesBatchTagSeverity(child.status)} />
</div> </div>
<ProgressBar value={child.progress} showValue={false} style={{ height: '5px' }} /> <ProgressBar value={child.progress} showValue={false} style={{ height: '5px' }} />
<small>{child.eta ? `${Math.round(child.progress)}% | ETA ${child.eta}` : `${Math.round(child.progress)}%`}</small> <small>{child.eta ? `${formatPercentNoDecimals(child.progress)} | ETA ${child.eta}` : `${formatPercentNoDecimals(child.progress)}`}</small>
</div> </div>
))} ))}
</div> </div>
@@ -3669,6 +3678,14 @@ export default function PipelineStatusCard({
<small> <small>
Vorhandenes Raw zur Disk gefunden. Wie soll weiter verfahren werden? Vorhandenes Raw zur Disk gefunden. Wie soll weiter verfahren werden?
</small> </small>
{rawDecisionAllowsMultipartMergeWithOrphan ? (
<small className="muted-inline mt-2">
Optional: Orphan-RAW als eigene Disc in einen Multipart-Container uebernehmen
{(rawDecisionOrphanDiscNumber || rawDecisionCurrentDiscNumber)
? ` (Orphan Disc ${rawDecisionOrphanDiscNumber || '?'} | Laufwerk Disc ${rawDecisionCurrentDiscNumber || '?'})`
: ''}.
</small>
) : null}
<div className="actions-row mt-3"> <div className="actions-row mt-3">
<Button <Button
label="mit RAW weiter machen" label="mit RAW weiter machen"
@@ -3677,6 +3694,16 @@ export default function PipelineStatusCard({
onClick={() => onSubmitRawDecision?.(retryJobId, 'continue')} onClick={() => onSubmitRawDecision?.(retryJobId, 'continue')}
loading={busy} loading={busy}
/> />
{rawDecisionAllowsMultipartMergeWithOrphan ? (
<Button
label="Multipart movie (Orphan + LW)"
icon="pi pi-sitemap"
severity="info"
outlined
onClick={() => onSubmitRawDecision?.(retryJobId, 'multipart_orphan_merge')}
loading={busy}
/>
) : null}
<Button <Button
label="RAW löschen und Rip erneut starten" label="RAW löschen und Rip erneut starten"
icon="pi pi-trash" icon="pi pi-trash"
+33 -11
View File
@@ -56,6 +56,23 @@ const CD_FORMAT_LABELS = {
ogg: 'Ogg Vorbis' ogg: 'Ogg Vorbis'
}; };
const MULTIPART_CONTAINER_LIVE_STATUSES = new Set([
'ANALYZING',
'METADATA_SELECTION',
'WAITING_FOR_USER_DECISION',
'READY_TO_START',
'MEDIAINFO_CHECK',
'READY_TO_ENCODE',
'RIPPING',
'ENCODING',
'POST_ENCODE_SCRIPTS',
'CD_METADATA_SELECTION',
'CD_READY_TO_RIP',
'CD_ANALYZING',
'CD_RIPPING',
'CD_ENCODING'
]);
function normalizePositiveInteger(value) { function normalizePositiveInteger(value) {
const parsed = Number(value); const parsed = Number(value);
if (!Number.isFinite(parsed) || parsed <= 0) { if (!Number.isFinite(parsed) || parsed <= 0) {
@@ -1774,6 +1791,18 @@ export default function HistoryPage({ refreshToken = 0 }) {
}; };
const renderStatusTag = (row) => { const renderStatusTag = (row) => {
const normalizedStatus = normalizeStatus(row?.status || row?.last_state);
const rowId = normalizeJobId(row?.id);
const isQueued = Boolean(rowId && queuedJobIdSet.has(rowId));
const renderDefaultStatusTag = () => (
<div className="history-status-tag-wrap">
<Tag
value={getStatusLabel(row?.status || row?.last_state, { queued: isQueued })}
severity={getStatusSeverity(normalizedStatus, { queued: isQueued })}
/>
</div>
);
if (isMultipartContainerHistoryRow(row)) { if (isMultipartContainerHistoryRow(row)) {
const mergeSummary = row?.seriesChildSummary?.merge && typeof row.seriesChildSummary.merge === 'object' const mergeSummary = row?.seriesChildSummary?.merge && typeof row.seriesChildSummary.merge === 'object'
? row.seriesChildSummary.merge ? row.seriesChildSummary.merge
@@ -1792,6 +1821,9 @@ export default function HistoryPage({ refreshToken = 0 }) {
</div> </div>
); );
} }
if (MULTIPART_CONTAINER_LIVE_STATUSES.has(normalizedStatus)) {
return renderDefaultStatusTag();
}
if (mergeState === 'done' || isCompleted) { if (mergeState === 'done' || isCompleted) {
return ( return (
<div className="history-status-tag-wrap"> <div className="history-status-tag-wrap">
@@ -1830,17 +1862,7 @@ export default function HistoryPage({ refreshToken = 0 }) {
</div> </div>
); );
} }
const normalizedStatus = normalizeStatus(row?.status); return renderDefaultStatusTag();
const rowId = normalizeJobId(row?.id);
const isQueued = Boolean(rowId && queuedJobIdSet.has(rowId));
return (
<div className="history-status-tag-wrap">
<Tag
value={getStatusLabel(row?.status, { queued: isQueued })}
severity={getStatusSeverity(normalizedStatus, { queued: isQueued })}
/>
</div>
);
}; };
const renderPoster = (row, className = 'history-dv-poster') => { const renderPoster = (row, className = 'history-dv-poster') => {
+32 -9
View File
@@ -87,7 +87,16 @@ function isIncompleteOutputPath(outputPath) {
if (!normalized) { if (!normalized) {
return false; return false;
} }
return /(^|\/)incomplete_job-\d+(\/|$)/i.test(normalized); return /(^|\/)incomplete_job-\d+(\/|$)/i.test(normalized)
|| /(^|\/)incomplete_merge_[^/]+_job_\d+(\/|$)/i.test(normalized);
}
function isIncompleteMergeOutputPath(outputPath) {
const normalized = normalizePathForMatch(outputPath);
if (!normalized) {
return false;
}
return /(^|\/)incomplete_merge_[^/]+_job_\d+(\/|$)/i.test(normalized);
} }
function getIncompleteOutputSelectionPaths(outputPath) { function getIncompleteOutputSelectionPaths(outputPath) {
@@ -96,20 +105,28 @@ function getIncompleteOutputSelectionPaths(outputPath) {
return []; return [];
} }
const segments = normalized.split('/').filter(Boolean); const segments = normalized.split('/').filter(Boolean);
const incompleteIndex = segments.findIndex((segment) => /^incomplete_job-\d+$/i.test(segment)); const incompleteJobIndex = segments.findIndex((segment) => /^incomplete_job-\d+$/i.test(segment));
if (incompleteIndex < 0) { if (incompleteJobIndex >= 0) {
return []; const folderPath = `/${segments.slice(0, incompleteJobIndex + 1).join('/')}`;
}
const folderPath = `/${segments.slice(0, incompleteIndex + 1).join('/')}`;
return Array.from(new Set([normalized, folderPath])); return Array.from(new Set([normalized, folderPath]));
} }
const incompleteMergeIndex = segments.findIndex((segment) => /^incomplete_merge_[^/]+_job_\d+$/i.test(segment));
if (incompleteMergeIndex >= 0) {
const mergeFolderPath = `/${segments.slice(0, incompleteMergeIndex + 1).join('/')}`;
const normalizedLooksLikeFolder = incompleteMergeIndex === (segments.length - 1);
return normalizedLooksLikeFolder
? [mergeFolderPath]
: [normalized];
}
return [];
}
function formatPercent(value, digits = 1) { function formatPercent(value) {
const parsed = Number(value); const parsed = Number(value);
if (!Number.isFinite(parsed)) { if (!Number.isFinite(parsed)) {
return 'n/a'; return 'n/a';
} }
return `${parsed.toFixed(digits)}%`; return `${Math.trunc(parsed)}%`;
} }
function formatTemperature(value) { function formatTemperature(value) {
@@ -2338,6 +2355,12 @@ export default function RipperPage({
incompleteMovieSelectionPaths = Array.from(new Set( incompleteMovieSelectionPaths = Array.from(new Set(
movieCandidates movieCandidates
.filter((candidate) => Boolean(candidate?.exists) && isIncompleteOutputPath(candidate?.path)) .filter((candidate) => Boolean(candidate?.exists) && isIncompleteOutputPath(candidate?.path))
.filter((candidate) => {
if (!isIncompleteMergeOutputPath(candidate?.path)) {
return true;
}
return Boolean(candidate?.isFile);
})
.map((candidate) => String(candidate?.path || '').trim()) .map((candidate) => String(candidate?.path || '').trim())
.filter(Boolean) .filter(Boolean)
)); ));
@@ -3384,7 +3407,7 @@ export default function RipperPage({
const clampedProgress = Number.isFinite(rawProgress) const clampedProgress = Number.isFinite(rawProgress)
? Math.max(0, Math.min(100, rawProgress)) ? Math.max(0, Math.min(100, rawProgress))
: 0; : 0;
const progressLabel = `${Math.round(clampedProgress)}%`; const progressLabel = `${Math.trunc(clampedProgress)}%`;
const etaLabel = isCancelledState ? '' : String( const etaLabel = isCancelledState ? '' : String(
hasSeriesBatchProgress hasSeriesBatchProgress
? (runningSeriesChild?.eta || pipelineForJob?.eta || '') ? (runningSeriesChild?.eta || pipelineForJob?.eta || '')
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "ripster", "name": "ripster",
"version": "0.15.0-1", "version": "0.15.0-2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ripster", "name": "ripster",
"version": "0.15.0-1", "version": "0.15.0-2",
"devDependencies": { "devDependencies": {
"concurrently": "^9.1.2" "concurrently": "^9.1.2"
} }
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "ripster", "name": "ripster",
"private": true, "private": true,
"version": "0.15.0-1", "version": "0.15.0-2",
"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",