0.15.0-2 Misc Fixes
This commit is contained in:
Generated
+2
-2
@@ -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,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": {
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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
Generated
+2
-2
@@ -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,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": {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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') => {
|
||||||
|
|||||||
@@ -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 || '')
|
||||||
|
|||||||
Generated
+2
-2
@@ -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
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user