0.16.1-7 Release-Bugfixes
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Button } from 'primereact/button';
|
||||
import { Card } from 'primereact/card';
|
||||
import { Toast } from 'primereact/toast';
|
||||
@@ -10,7 +9,6 @@ import { api } from '../api/client';
|
||||
import { useWebSocket } from '../hooks/useWebSocket';
|
||||
import AudiobookUploadPanel from '../components/AudiobookUploadPanel';
|
||||
import AudiobookConfigPanel from '../components/AudiobookConfigPanel';
|
||||
import AudiobookOutputExplorer from '../components/AudiobookOutputExplorer';
|
||||
import { getStatusLabel, getStatusSeverity, normalizeStatus } from '../utils/statusPresentation';
|
||||
import { resolveMediaType } from '../utils/jobTaxonomy';
|
||||
import { confirmModal } from '../utils/confirmModal';
|
||||
@@ -25,6 +23,44 @@ function normalizeJobId(value) {
|
||||
return Math.trunc(parsed);
|
||||
}
|
||||
|
||||
function hasOwn(source, key) {
|
||||
return source != null && Object.prototype.hasOwnProperty.call(source, key);
|
||||
}
|
||||
|
||||
function normalizeLiveProgressForJob(job) {
|
||||
const live = job?.liveProgress && typeof job.liveProgress === 'object' ? job.liveProgress : null;
|
||||
if (!live) {
|
||||
return null;
|
||||
}
|
||||
const liveContext = live?.context && typeof live.context === 'object' ? live.context : {};
|
||||
return {
|
||||
progress: live?.progress ?? null,
|
||||
eta: live?.eta ?? null,
|
||||
statusText: live?.statusText ?? null,
|
||||
state: live?.state ?? null,
|
||||
currentChapter: liveContext?.currentChapter && typeof liveContext.currentChapter === 'object'
|
||||
? liveContext.currentChapter
|
||||
: null,
|
||||
completedChapterCount: Number.isFinite(Number(liveContext?.completedChapterCount))
|
||||
? Number(liveContext.completedChapterCount)
|
||||
: null
|
||||
};
|
||||
}
|
||||
|
||||
function extractUploadJobIdFromResponse(response) {
|
||||
const payload = response && typeof response === 'object' ? response : {};
|
||||
const result = payload?.result && typeof payload.result === 'object' ? payload.result : {};
|
||||
return (
|
||||
normalizeJobId(result?.jobId)
|
||||
|| normalizeJobId(payload?.jobId)
|
||||
|| normalizeJobId(result?.id)
|
||||
|| normalizeJobId(payload?.id)
|
||||
|| normalizeJobId(result?.job?.id)
|
||||
|| normalizeJobId(payload?.job?.id)
|
||||
|| null
|
||||
);
|
||||
}
|
||||
|
||||
function parseEncodePlan(job) {
|
||||
if (job?.encodePlan && typeof job.encodePlan === 'object') {
|
||||
return job.encodePlan;
|
||||
@@ -71,6 +107,22 @@ function resolveAudiobookMetadata(job, encodePlan) {
|
||||
};
|
||||
}
|
||||
|
||||
function buildAudiobookJobMetaLine(jobId, metadata = {}, fallbackYear = null) {
|
||||
const author = String(metadata?.author || metadata?.artist || '').trim() || null;
|
||||
const narrator = String(metadata?.narrator || '').trim() || null;
|
||||
const chapterCount = Array.isArray(metadata?.chapters) ? metadata.chapters.length : 0;
|
||||
const year = Number.isFinite(Number(metadata?.year))
|
||||
? Math.trunc(Number(metadata.year))
|
||||
: (Number.isFinite(Number(fallbackYear)) ? Math.trunc(Number(fallbackYear)) : null);
|
||||
return (
|
||||
`#${jobId}`
|
||||
+ (author ? ` | ${author}` : '')
|
||||
+ (narrator ? ` | ${narrator}` : '')
|
||||
+ (chapterCount > 0 ? ` | ${chapterCount} Kapitel` : '')
|
||||
+ (year ? ` | ${year}` : '')
|
||||
);
|
||||
}
|
||||
|
||||
function buildAudiobookPipeline(job, progress = null) {
|
||||
const encodePlan = parseEncodePlan(job);
|
||||
const selectedMetadata = resolveAudiobookMetadata(job, encodePlan);
|
||||
@@ -93,7 +145,13 @@ function buildAudiobookPipeline(job, progress = null) {
|
||||
format: String(encodePlan?.format || job?.handbrakeInfo?.format || 'mp3').trim().toLowerCase() || 'mp3',
|
||||
formatOptions: encodePlan?.formatOptions && typeof encodePlan.formatOptions === 'object'
|
||||
? encodePlan.formatOptions
|
||||
: {}
|
||||
: {},
|
||||
preEncodeScriptIds: Array.isArray(encodePlan?.preEncodeScriptIds) ? encodePlan.preEncodeScriptIds : [],
|
||||
postEncodeScriptIds: Array.isArray(encodePlan?.postEncodeScriptIds) ? encodePlan.postEncodeScriptIds : [],
|
||||
preEncodeChainIds: Array.isArray(encodePlan?.preEncodeChainIds) ? encodePlan.preEncodeChainIds : [],
|
||||
postEncodeChainIds: Array.isArray(encodePlan?.postEncodeChainIds) ? encodePlan.postEncodeChainIds : [],
|
||||
preEncodeItems: Array.isArray(encodePlan?.preEncodeItems) ? encodePlan.preEncodeItems : [],
|
||||
postEncodeItems: Array.isArray(encodePlan?.postEncodeItems) ? encodePlan.postEncodeItems : []
|
||||
},
|
||||
mediaInfoReview: reviewData,
|
||||
outputPath: String(job?.output_path || encodePlan?.outputPath || '').trim() || null,
|
||||
@@ -109,16 +167,15 @@ function buildAudiobookPipeline(job, progress = null) {
|
||||
|
||||
export default function AudiobooksPage({
|
||||
audiobookUpload,
|
||||
onAudiobookUpload
|
||||
onAudiobookUpload,
|
||||
onCancelAudiobookUpload = null
|
||||
}) {
|
||||
const toastRef = useRef(null);
|
||||
const navigate = useNavigate();
|
||||
const [jobs, setJobs] = useState([]);
|
||||
const [loadingJobs, setLoadingJobs] = useState(false);
|
||||
const [expandedJobId, setExpandedJobId] = useState(undefined);
|
||||
const [jobProgress, setJobProgress] = useState({});
|
||||
const [actionBusyJobIds, setActionBusyJobIds] = useState(() => new Set());
|
||||
const explorerRefreshToken = 0;
|
||||
|
||||
const setActionBusy = (jobId, busy) => {
|
||||
const normalizedJobId = normalizeJobId(jobId);
|
||||
@@ -146,21 +203,32 @@ export default function AudiobooksPage({
|
||||
.sort((left, right) => Number(right?.id || 0) - Number(left?.id || 0));
|
||||
setJobs(audiobookRows);
|
||||
setJobProgress((prev) => {
|
||||
const next = { ...prev };
|
||||
const next = {};
|
||||
for (const job of audiobookRows) {
|
||||
const jobId = normalizeJobId(job?.id);
|
||||
if (!jobId) {
|
||||
continue;
|
||||
}
|
||||
const status = String(job?.status || '').trim().toUpperCase();
|
||||
if (!['ANALYZING', 'ENCODING'].includes(status)) {
|
||||
delete next[jobId];
|
||||
const isLiveStatus = ['ANALYZING', 'ENCODING'].includes(status);
|
||||
if (!isLiveStatus) {
|
||||
continue;
|
||||
}
|
||||
const serverProgress = normalizeLiveProgressForJob(job);
|
||||
if (serverProgress) {
|
||||
next[jobId] = serverProgress;
|
||||
continue;
|
||||
}
|
||||
if (prev?.[jobId] && typeof prev[jobId] === 'object') {
|
||||
next[jobId] = prev[jobId];
|
||||
}
|
||||
}
|
||||
return next;
|
||||
});
|
||||
return audiobookRows;
|
||||
} catch (error) {
|
||||
console.error('AudiobooksPage load jobs error:', error);
|
||||
return [];
|
||||
} finally {
|
||||
setLoadingJobs(false);
|
||||
}
|
||||
@@ -183,17 +251,31 @@ export default function AudiobooksPage({
|
||||
if (!jobId) {
|
||||
return;
|
||||
}
|
||||
setJobProgress((prev) => ({
|
||||
...prev,
|
||||
[jobId]: {
|
||||
progress: payload?.progress ?? null,
|
||||
eta: payload?.eta ?? null,
|
||||
statusText: payload?.statusText ?? null,
|
||||
state: payload?.state ?? null,
|
||||
currentChapter: payload?.contextPatch?.currentChapter ?? null,
|
||||
completedChapterCount: payload?.contextPatch?.completedChapterCount ?? null
|
||||
}
|
||||
}));
|
||||
setJobProgress((prev) => {
|
||||
const previousEntry = prev?.[jobId] && typeof prev[jobId] === 'object' ? prev[jobId] : {};
|
||||
const contextPatch = payload?.contextPatch && typeof payload.contextPatch === 'object'
|
||||
? payload.contextPatch
|
||||
: null;
|
||||
const nextCurrentChapter = hasOwn(contextPatch, 'currentChapter')
|
||||
? (contextPatch?.currentChapter ?? null)
|
||||
: (previousEntry?.currentChapter ?? null);
|
||||
const nextCompletedChapterCount = hasOwn(contextPatch, 'completedChapterCount')
|
||||
? (Number.isFinite(Number(contextPatch?.completedChapterCount))
|
||||
? Number(contextPatch.completedChapterCount)
|
||||
: null)
|
||||
: (previousEntry?.completedChapterCount ?? null);
|
||||
return {
|
||||
...prev,
|
||||
[jobId]: {
|
||||
progress: payload?.progress ?? previousEntry?.progress ?? null,
|
||||
eta: payload?.eta ?? previousEntry?.eta ?? null,
|
||||
statusText: payload?.statusText ?? previousEntry?.statusText ?? null,
|
||||
state: payload?.state ?? previousEntry?.state ?? null,
|
||||
currentChapter: nextCurrentChapter,
|
||||
completedChapterCount: nextCompletedChapterCount
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
if (
|
||||
message.type === 'PIPELINE_UPDATE'
|
||||
@@ -262,7 +344,7 @@ export default function AudiobooksPage({
|
||||
}
|
||||
setActionBusy(normalizedJobId, true);
|
||||
try {
|
||||
await api.cancelJob(normalizedJobId);
|
||||
await api.cancelPipeline(normalizedJobId);
|
||||
toastRef.current?.show({
|
||||
severity: 'info',
|
||||
summary: 'Abbruch angefordert',
|
||||
@@ -282,39 +364,6 @@ export default function AudiobooksPage({
|
||||
}
|
||||
};
|
||||
|
||||
const handleRetry = async (jobId) => {
|
||||
const normalizedJobId = normalizeJobId(jobId);
|
||||
if (!normalizedJobId) {
|
||||
return;
|
||||
}
|
||||
setActionBusy(normalizedJobId, true);
|
||||
try {
|
||||
const response = await api.retryJob(normalizedJobId);
|
||||
const retryJobId = normalizeJobId(response?.retryJob?.id);
|
||||
toastRef.current?.show({
|
||||
severity: 'success',
|
||||
summary: 'Retry erstellt',
|
||||
detail: retryJobId
|
||||
? `Neuer Job #${retryJobId} wurde angelegt.`
|
||||
: `Retry fuer Job #${normalizedJobId} wurde angelegt.`,
|
||||
life: 3200
|
||||
});
|
||||
await loadJobs();
|
||||
if (retryJobId) {
|
||||
setExpandedJobId(retryJobId);
|
||||
}
|
||||
} catch (error) {
|
||||
toastRef.current?.show({
|
||||
severity: 'error',
|
||||
summary: 'Retry fehlgeschlagen',
|
||||
detail: error?.message || 'Bitte Logs pruefen.',
|
||||
life: 4200
|
||||
});
|
||||
} finally {
|
||||
setActionBusy(normalizedJobId, false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (jobId) => {
|
||||
const normalizedJobId = normalizeJobId(jobId);
|
||||
if (!normalizedJobId) {
|
||||
@@ -357,11 +406,14 @@ export default function AudiobooksPage({
|
||||
}
|
||||
};
|
||||
|
||||
const handleUploaded = async (uploadedJobId) => {
|
||||
const normalizedJobId = normalizeJobId(uploadedJobId);
|
||||
await loadJobs();
|
||||
if (normalizedJobId) {
|
||||
setExpandedJobId(normalizedJobId);
|
||||
const handleUploaded = async (uploadedJobId, response = null) => {
|
||||
const normalizedJobId = normalizeJobId(uploadedJobId)
|
||||
|| extractUploadJobIdFromResponse(response);
|
||||
const rows = await loadJobs();
|
||||
const fallbackJobId = normalizeJobId(rows?.[0]?.id);
|
||||
const targetJobId = normalizedJobId || fallbackJobId;
|
||||
if (targetJobId) {
|
||||
setExpandedJobId(targetJobId);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -393,6 +445,7 @@ export default function AudiobooksPage({
|
||||
<AudiobookUploadPanel
|
||||
audiobookUpload={audiobookUpload}
|
||||
onAudiobookUpload={onAudiobookUpload}
|
||||
onCancelUpload={onCancelAudiobookUpload}
|
||||
onUploaded={handleUploaded}
|
||||
/>
|
||||
</Card>
|
||||
@@ -417,7 +470,17 @@ export default function AudiobooksPage({
|
||||
const progressValue = Number.isFinite(Number(pipelineForJob?.progress))
|
||||
? Math.max(0, Math.min(100, Number(pipelineForJob.progress)))
|
||||
: 0;
|
||||
const showProgress = ['ANALYZING', 'ENCODING'].includes(state) && progressValue > 0;
|
||||
const isRunning = ['ANALYZING', 'ENCODING'].includes(state);
|
||||
const showProgress = isRunning || progressValue > 0;
|
||||
const progressLabel = `${Math.trunc(progressValue)}%`;
|
||||
const etaLabel = String(pipelineForJob?.eta || '').trim();
|
||||
const currentChapter = pipelineForJob?.context?.currentChapter && typeof pipelineForJob.context.currentChapter === 'object'
|
||||
? pipelineForJob.context.currentChapter
|
||||
: null;
|
||||
const chapterLabel = currentChapter?.index && currentChapter?.total
|
||||
? ` | Kapitel ${currentChapter.index}/${currentChapter.total}${currentChapter.title ? `: ${currentChapter.title}` : ''}`
|
||||
: '';
|
||||
const metaLine = buildAudiobookJobMetaLine(jobId, pipelineForJob?.context?.selectedMetadata, job?.year);
|
||||
|
||||
if (!isExpanded) {
|
||||
return (
|
||||
@@ -427,25 +490,30 @@ export default function AudiobooksPage({
|
||||
className="ripper-job-row"
|
||||
onClick={() => setExpandedJobId(jobId)}
|
||||
>
|
||||
<div className="poster-thumb ripper-job-poster-fallback">Kein Cover</div>
|
||||
{(job?.poster_url && job.poster_url !== 'N/A') ? (
|
||||
<img src={job.poster_url} alt={title} className="poster-thumb" />
|
||||
) : (
|
||||
<div className="poster-thumb ripper-job-poster-fallback">Kein Cover</div>
|
||||
)}
|
||||
<div className="ripper-job-row-content">
|
||||
<div className="ripper-job-row-main">
|
||||
<strong className="ripper-job-title-line">
|
||||
<i className="pi pi-headphones media-indicator-icon" style={{ fontSize: '1rem', color: 'var(--rip-muted)' }} />
|
||||
<span>{title}</span>
|
||||
</strong>
|
||||
<small>#{jobId}</small>
|
||||
<small>{metaLine}</small>
|
||||
</div>
|
||||
<div className="ripper-job-badges">
|
||||
<Tag value={getStatusLabel(state)} severity={getStatusSeverity(normalizeStatus(state))} />
|
||||
<Tag value="Audiobook" severity="info" />
|
||||
</div>
|
||||
{showProgress ? (
|
||||
<div className="ripper-job-row-progress">
|
||||
<ProgressBar value={progressValue} showValue={false} />
|
||||
<small>{etaLabel ? `${progressLabel} | ETA ${etaLabel}` : `${progressLabel}${chapterLabel}`}</small>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<i className="pi pi-angle-down" aria-hidden="true" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -453,25 +521,22 @@ export default function AudiobooksPage({
|
||||
return (
|
||||
<div key={jobId} className="ripper-job-expanded">
|
||||
<div className="ripper-job-expanded-head">
|
||||
<div className="poster-thumb ripper-job-poster-fallback">Kein Cover</div>
|
||||
{(job?.poster_url && job.poster_url !== 'N/A') ? (
|
||||
<img src={job.poster_url} alt={title} className="poster-thumb" />
|
||||
) : (
|
||||
<div className="poster-thumb ripper-job-poster-fallback">Kein Cover</div>
|
||||
)}
|
||||
<div className="ripper-job-expanded-title">
|
||||
<strong className="ripper-job-title-line">
|
||||
<i className="pi pi-headphones media-indicator-icon" style={{ fontSize: '1rem', color: 'var(--rip-muted)' }} />
|
||||
<span>#{jobId} | {title}</span>
|
||||
<span>{title}</span>
|
||||
</strong>
|
||||
<small className="ripper-job-subtitle">{metaLine}</small>
|
||||
<div className="ripper-job-badges">
|
||||
<Tag value={getStatusLabel(state)} severity={getStatusSeverity(normalizeStatus(state))} />
|
||||
<Tag value="Audiobook" severity="info" />
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||
<Button
|
||||
label="Im Ripper"
|
||||
icon="pi pi-arrow-right"
|
||||
severity="secondary"
|
||||
outlined
|
||||
onClick={() => navigate('/ripper')}
|
||||
/>
|
||||
<Button
|
||||
label="Einklappen"
|
||||
icon="pi pi-angle-up"
|
||||
@@ -482,11 +547,16 @@ export default function AudiobooksPage({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{showProgress ? (
|
||||
<div className="ripper-job-row-progress">
|
||||
<ProgressBar value={progressValue} showValue={false} />
|
||||
<small>{etaLabel ? `${progressLabel} | ETA ${etaLabel}` : `${progressLabel}${chapterLabel}`}</small>
|
||||
</div>
|
||||
) : null}
|
||||
<AudiobookConfigPanel
|
||||
pipeline={pipelineForJob}
|
||||
onStart={(config) => void handleAudiobookStart(jobId, config)}
|
||||
onCancel={() => void handleCancel(jobId)}
|
||||
onRetry={() => void handleRetry(jobId)}
|
||||
onDeleteJob={() => void handleDelete(jobId)}
|
||||
busy={busy}
|
||||
/>
|
||||
@@ -496,13 +566,6 @@ export default function AudiobooksPage({
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="Audiobook Output Explorer"
|
||||
subTitle="Read-only Explorer auf dem Audiobook-Ausgabeordner"
|
||||
>
|
||||
<AudiobookOutputExplorer refreshToken={explorerRefreshToken} />
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user