0.13.0 DVD Series
This commit is contained in:
@@ -6,6 +6,7 @@ import blurayIndicatorIcon from '../assets/media-bluray.svg';
|
||||
import discIndicatorIcon from '../assets/media-disc.svg';
|
||||
import otherIndicatorIcon from '../assets/media-other.svg';
|
||||
import { getProcessStatusLabel, getStatusLabel } from '../utils/statusPresentation';
|
||||
import { isSeriesDvdJob } from '../utils/jobTaxonomy';
|
||||
|
||||
const CD_FORMAT_LABELS = {
|
||||
flac: 'FLAC',
|
||||
@@ -190,6 +191,98 @@ function buildExecutedHandBrakeCommand(handbrakeInfo) {
|
||||
return `${cmd} ${args.map((arg) => shellQuote(arg)).join(' ')}`.trim();
|
||||
}
|
||||
|
||||
function normalizeSeriesEpisodeStatus(status) {
|
||||
const normalized = String(status || '').trim().toUpperCase();
|
||||
if (['QUEUED', 'RUNNING', 'FINISHED', 'ERROR', 'CANCELLED'].includes(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
return 'QUEUED';
|
||||
}
|
||||
|
||||
function seriesEpisodeStatusMeta(status) {
|
||||
const normalized = normalizeSeriesEpisodeStatus(status);
|
||||
if (normalized === 'FINISHED') {
|
||||
return { label: 'Fertig', severity: 'success' };
|
||||
}
|
||||
if (normalized === 'RUNNING') {
|
||||
return { label: 'Läuft', severity: 'info' };
|
||||
}
|
||||
if (normalized === 'ERROR') {
|
||||
return { label: 'Fehler', severity: 'danger' };
|
||||
}
|
||||
if (normalized === 'CANCELLED') {
|
||||
return { label: 'Abgebrochen', severity: 'warning' };
|
||||
}
|
||||
return { label: 'Wartend', severity: 'secondary' };
|
||||
}
|
||||
|
||||
function formatDateTimeOrDash(value) {
|
||||
const text = String(value || '').trim();
|
||||
if (!text) {
|
||||
return '-';
|
||||
}
|
||||
const parsed = new Date(text);
|
||||
if (Number.isNaN(parsed.getTime())) {
|
||||
return text;
|
||||
}
|
||||
return parsed.toLocaleString();
|
||||
}
|
||||
|
||||
function mergeSeriesBatchEpisodes(job) {
|
||||
const planEpisodes = Array.isArray(job?.encodePlan?.seriesBatchEpisodes)
|
||||
? job.encodePlan.seriesBatchEpisodes
|
||||
: [];
|
||||
const hbEpisodes = Array.isArray(job?.handbrakeInfo?.seriesBatch?.episodes)
|
||||
? job.handbrakeInfo.seriesBatch.episodes
|
||||
: [];
|
||||
const map = new Map();
|
||||
|
||||
const append = (list, sourcePriority) => {
|
||||
for (const item of list) {
|
||||
if (!item || typeof item !== 'object') {
|
||||
continue;
|
||||
}
|
||||
const episodeIndex = normalizePositiveInteger(item?.episodeIndex);
|
||||
const titleId = normalizePositiveInteger(item?.titleId);
|
||||
const key = `${episodeIndex || 0}:${titleId || 0}`;
|
||||
const existing = map.get(key);
|
||||
if (!existing) {
|
||||
map.set(key, {
|
||||
...item,
|
||||
episodeIndex,
|
||||
titleId,
|
||||
sourcePriority
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (sourcePriority >= Number(existing.sourcePriority || 0)) {
|
||||
map.set(key, {
|
||||
...existing,
|
||||
...item,
|
||||
episodeIndex: episodeIndex || existing.episodeIndex || null,
|
||||
titleId: titleId || existing.titleId || null,
|
||||
sourcePriority
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
append(planEpisodes, 1);
|
||||
append(hbEpisodes, 2);
|
||||
|
||||
return Array.from(map.values())
|
||||
.map((entry, index) => ({
|
||||
...entry,
|
||||
episodeIndex: normalizePositiveInteger(entry?.episodeIndex) || (index + 1),
|
||||
titleId: normalizePositiveInteger(entry?.titleId) || null,
|
||||
progress: Number.isFinite(Number(entry?.progress))
|
||||
? Math.max(0, Math.min(100, Number(entry.progress)))
|
||||
: 0,
|
||||
status: normalizeSeriesEpisodeStatus(entry?.status)
|
||||
}))
|
||||
.sort((left, right) => left.episodeIndex - right.episodeIndex);
|
||||
}
|
||||
|
||||
function buildConfiguredScriptAndChainSelection(job) {
|
||||
const plan = job?.encodePlan && typeof job.encodePlan === 'object' ? job.encodePlan : {};
|
||||
const handbrakeInfo = job?.handbrakeInfo && typeof job.handbrakeInfo === 'object' ? job.handbrakeInfo : {};
|
||||
@@ -505,6 +598,147 @@ function omdbRottenTomatoesScore(omdbInfo) {
|
||||
return omdbField(entry?.Value);
|
||||
}
|
||||
|
||||
function normalizeMetadataProvider(value) {
|
||||
return String(value || '').trim().toLowerCase() || 'omdb';
|
||||
}
|
||||
|
||||
function resolveJobMetadataContext(job) {
|
||||
const mkInfo = job?.makemkvInfo && typeof job.makemkvInfo === 'object'
|
||||
? job.makemkvInfo
|
||||
: {};
|
||||
const analyzeContext = mkInfo?.analyzeContext && typeof mkInfo.analyzeContext === 'object'
|
||||
? mkInfo.analyzeContext
|
||||
: {};
|
||||
const analyzeSelectedMetadata = analyzeContext?.selectedMetadata && typeof analyzeContext.selectedMetadata === 'object'
|
||||
? analyzeContext.selectedMetadata
|
||||
: {};
|
||||
const selectedMetadata = mkInfo?.selectedMetadata && typeof mkInfo.selectedMetadata === 'object'
|
||||
? mkInfo.selectedMetadata
|
||||
: {};
|
||||
const encodeMetadata = job?.encodePlan?.metadata && typeof job.encodePlan.metadata === 'object'
|
||||
? job.encodePlan.metadata
|
||||
: {};
|
||||
const mergedSelectedMetadata = {
|
||||
...analyzeSelectedMetadata,
|
||||
...selectedMetadata,
|
||||
...encodeMetadata
|
||||
};
|
||||
const metadataProvider = normalizeMetadataProvider(
|
||||
mergedSelectedMetadata?.metadataProvider
|
||||
|| analyzeContext?.metadataProvider
|
||||
|| 'omdb'
|
||||
);
|
||||
return {
|
||||
analyzeContext,
|
||||
selectedMetadata: mergedSelectedMetadata,
|
||||
metadataProvider
|
||||
};
|
||||
}
|
||||
|
||||
function uniqueNameList(values, maxItems = 10) {
|
||||
const source = Array.isArray(values) ? values : [];
|
||||
const limit = Math.max(1, Number(maxItems || 10));
|
||||
const output = [];
|
||||
const seen = new Set();
|
||||
for (const item of source) {
|
||||
const name = String(item?.name || item || '').trim();
|
||||
if (!name) {
|
||||
continue;
|
||||
}
|
||||
const key = name.toLowerCase();
|
||||
if (seen.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
output.push(name);
|
||||
if (output.length >= limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function formatRuntimeLabel(value) {
|
||||
if (Array.isArray(value)) {
|
||||
const values = value
|
||||
.map((entry) => Number(entry))
|
||||
.filter((entry) => Number.isFinite(entry) && entry > 0)
|
||||
.map((entry) => Math.trunc(entry));
|
||||
if (values.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const min = Math.min(...values);
|
||||
const max = Math.max(...values);
|
||||
return min === max ? `${min} min` : `${min}-${max} min`;
|
||||
}
|
||||
const numeric = Number(value);
|
||||
if (Number.isFinite(numeric) && numeric > 0) {
|
||||
return `${Math.trunc(numeric)} min`;
|
||||
}
|
||||
const text = String(value || '').trim();
|
||||
return text || null;
|
||||
}
|
||||
|
||||
function resolveMetadataDetailsForDisplay(job, omdbInfo = {}) {
|
||||
const metadataContext = resolveJobMetadataContext(job);
|
||||
const provider = metadataContext.metadataProvider;
|
||||
const selectedMetadata = metadataContext.selectedMetadata;
|
||||
const tmdbDetails = selectedMetadata?.tmdbDetails && typeof selectedMetadata.tmdbDetails === 'object'
|
||||
? selectedMetadata.tmdbDetails
|
||||
: {};
|
||||
const isTmdbProvider = provider === 'tmdb' || provider === 'themoviedb';
|
||||
|
||||
if (isTmdbProvider) {
|
||||
const genre = uniqueNameList(tmdbDetails?.genres || tmdbDetails?.genre, 3).join(', ') || null;
|
||||
const actors = uniqueNameList(tmdbDetails?.seasonCast || tmdbDetails?.actors, 6).join(', ') || null;
|
||||
const director = uniqueNameList(tmdbDetails?.createdBy || tmdbDetails?.director, 3).join(', ') || null;
|
||||
const runtime = tmdbDetails?.seasonRuntime || tmdbDetails?.runtime || formatRuntimeLabel(selectedMetadata?.episodeRunTime);
|
||||
const ratingNumber = Number(tmdbDetails?.seasonVoteAverage ?? tmdbDetails?.voteAverage ?? selectedMetadata?.voteAverage);
|
||||
const tmdbRating = Number.isFinite(ratingNumber) && ratingNumber > 0
|
||||
? ratingNumber.toFixed(1)
|
||||
: null;
|
||||
const imdbId = String(selectedMetadata?.imdbId || tmdbDetails?.imdbId || '').trim() || null;
|
||||
const hasMatch = Boolean(selectedMetadata?.tmdbId || tmdbDetails?.tmdbId);
|
||||
|
||||
return {
|
||||
provider: 'tmdb',
|
||||
title: 'TMDb Details',
|
||||
matchLabel: 'TMDb Match',
|
||||
hasMatch,
|
||||
imdbId,
|
||||
director: director || '-',
|
||||
actors: actors || '-',
|
||||
runtime: runtime || '-',
|
||||
genre: genre || '-',
|
||||
tmdbRating: tmdbRating || '-'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
provider: 'omdb',
|
||||
title: 'OMDb Details',
|
||||
matchLabel: 'OMDb Match',
|
||||
hasMatch: Boolean(job?.selected_from_omdb),
|
||||
imdbId: String(job?.imdb_id || '').trim() || null,
|
||||
director: omdbField(omdbInfo?.Director),
|
||||
actors: omdbField(omdbInfo?.Actors),
|
||||
runtime: omdbField(omdbInfo?.Runtime),
|
||||
genre: omdbField(omdbInfo?.Genre),
|
||||
rottenTomatoes: omdbRottenTomatoesScore(omdbInfo),
|
||||
imdbRating: omdbField(omdbInfo?.imdbRating)
|
||||
};
|
||||
}
|
||||
|
||||
function resolveSeriesDiscNumber(job) {
|
||||
const metadataContext = resolveJobMetadataContext(job);
|
||||
return normalizePositiveInteger(
|
||||
metadataContext?.selectedMetadata?.discNumber
|
||||
?? metadataContext?.analyzeContext?.seriesLookupHint?.discNumber
|
||||
?? job?.encodePlan?.discNumber
|
||||
?? null
|
||||
);
|
||||
}
|
||||
|
||||
function BoolState({ value }) {
|
||||
const isTrue = Boolean(value);
|
||||
return isTrue ? (
|
||||
@@ -518,6 +752,33 @@ function BoolState({ value }) {
|
||||
);
|
||||
}
|
||||
|
||||
function TriState({ existing = 0, expected = 0, labels = {} }) {
|
||||
const safeExpected = Number.isFinite(Number(expected)) ? Number(expected) : 0;
|
||||
const safeExisting = Number.isFinite(Number(existing)) ? Number(existing) : 0;
|
||||
if (safeExpected <= 0) {
|
||||
return <BoolState value={safeExisting > 0} />;
|
||||
}
|
||||
if (safeExisting <= 0) {
|
||||
return (
|
||||
<span className="job-step-inline-no" title={labels.none || `Nein (${safeExisting}/${safeExpected})`}>
|
||||
<i className="pi pi-times-circle" aria-hidden="true" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (safeExisting < safeExpected) {
|
||||
return (
|
||||
<span className="job-step-inline-warn" title={labels.partial || `Teilweise (${safeExisting}/${safeExpected})`}>
|
||||
<i className="pi pi-exclamation-circle" aria-hidden="true" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span className="job-step-inline-ok" title={labels.full || `Ja (${safeExisting}/${safeExpected})`}>
|
||||
<i className="pi pi-check-circle" aria-hidden="true" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function resolveSelectionStateMeta({ selected, outcome }) {
|
||||
if (!selected) {
|
||||
return {
|
||||
@@ -616,6 +877,7 @@ export default function JobDetailDialog({
|
||||
logLoadingMode = null,
|
||||
onAssignOmdb,
|
||||
onAssignCdMetadata,
|
||||
onAcknowledgeError,
|
||||
onResumeReady,
|
||||
onRestartEncode,
|
||||
onRestartReview,
|
||||
@@ -631,6 +893,7 @@ export default function JobDetailDialog({
|
||||
isQueued = false,
|
||||
omdbAssignBusy = false,
|
||||
cdMetadataAssignBusy = false,
|
||||
acknowledgeErrorBusy = false,
|
||||
actionBusy = false,
|
||||
cancelBusy = false,
|
||||
reencodeBusy = false,
|
||||
@@ -639,16 +902,39 @@ export default function JobDetailDialog({
|
||||
downloadFolderBusyPath = null
|
||||
}) {
|
||||
const mkDone = Boolean(job?.ripSuccessful) || !job?.makemkvInfo || job?.makemkvInfo?.status === 'SUCCESS';
|
||||
// RAW-Import aus Datenbank, der noch nie encodiert wurde → nur "Neu einlesen" erlaubt
|
||||
const isUnencodedOrphanImport = Boolean(
|
||||
job?.makemkvInfo?.source === 'orphan_raw_import' && !job?.handbrakeInfo
|
||||
);
|
||||
const statusUpper = String(job?.status || '').trim().toUpperCase();
|
||||
const running = ['ANALYZING', 'RIPPING', 'MEDIAINFO_CHECK', 'ENCODING', 'CD_RIPPING', 'CD_ENCODING'].includes(statusUpper);
|
||||
const softCancelable = ['READY_TO_START', 'READY_TO_ENCODE', 'METADATA_SELECTION', 'WAITING_FOR_USER_DECISION', 'CD_METADATA_SELECTION', 'CD_READY_TO_RIP'].includes(statusUpper);
|
||||
const showCancelAction = (running || softCancelable) && typeof onCancel === 'function';
|
||||
const showFinalLog = !running;
|
||||
const mediaType = resolveMediaType(job);
|
||||
const isDvdSeries = mediaType === 'dvd' && isSeriesDvdJob(job);
|
||||
const seriesBatchEpisodes = isDvdSeries ? mergeSeriesBatchEpisodes(job) : [];
|
||||
const seriesEpisodeAssignments = isDvdSeries && job?.encodePlan?.episodeAssignments && typeof job.encodePlan.episodeAssignments === 'object'
|
||||
? job.encodePlan.episodeAssignments
|
||||
: {};
|
||||
const seriesBatchSummary = seriesBatchEpisodes.reduce((acc, episode) => {
|
||||
const status = normalizeSeriesEpisodeStatus(episode?.status);
|
||||
if (status === 'FINISHED') {
|
||||
acc.finished += 1;
|
||||
} else if (status === 'ERROR') {
|
||||
acc.error += 1;
|
||||
} else if (status === 'CANCELLED') {
|
||||
acc.cancelled += 1;
|
||||
} else if (status === 'RUNNING') {
|
||||
acc.running += 1;
|
||||
} else {
|
||||
acc.queued += 1;
|
||||
}
|
||||
return acc;
|
||||
}, {
|
||||
total: seriesBatchEpisodes.length,
|
||||
finished: 0,
|
||||
error: 0,
|
||||
cancelled: 0,
|
||||
running: 0,
|
||||
queued: 0
|
||||
});
|
||||
const isCd = mediaType === 'cd';
|
||||
const isAudiobook = mediaType === 'audiobook';
|
||||
const isConverter = mediaType === 'converter';
|
||||
@@ -811,7 +1097,7 @@ export default function JobDetailDialog({
|
||||
const mediaTypeLabel = mediaType === 'bluray'
|
||||
? 'Blu-ray'
|
||||
: mediaType === 'dvd'
|
||||
? 'DVD'
|
||||
? (isDvdSeries ? 'DVD Serie' : 'DVD')
|
||||
: isCd
|
||||
? 'Audio CD'
|
||||
: (isAudiobook ? 'Audiobook' : (isConverter ? `Converter ${converterMediaTypeLabel}` : 'Sonstiges Medium'));
|
||||
@@ -823,6 +1109,27 @@ export default function JobDetailDialog({
|
||||
const mediaTypeAlt = mediaTypeLabel;
|
||||
const statusMeta = statusBadgeMeta(job?.status, queueLocked);
|
||||
const omdbInfo = job?.omdbInfo && typeof job.omdbInfo === 'object' ? job.omdbInfo : {};
|
||||
const metadataContext = resolveJobMetadataContext(job);
|
||||
const metadataDetails = resolveMetadataDetailsForDisplay(job, omdbInfo);
|
||||
const seriesDiscNumber = isDvdSeries ? resolveSeriesDiscNumber(job) : null;
|
||||
const seriesDiscLabel = seriesDiscNumber ? `Disk ${seriesDiscNumber}` : 'Disk unbekannt';
|
||||
const childJobs = Array.isArray(job?.children) ? job.children : [];
|
||||
const isSeriesContainer = isDvdSeries && String(job?.job_kind || '').trim().toLowerCase() === 'dvd_series_container';
|
||||
const seriesChildSummary = job?.seriesChildSummary || null;
|
||||
const seriesBackupSummary = isSeriesContainer ? seriesChildSummary?.backup : null;
|
||||
const seriesEncodeSummary = isSeriesContainer ? seriesChildSummary?.encode : null;
|
||||
const displayedImdbId = metadataDetails?.imdbId || job?.imdb_id || null;
|
||||
const metadataJsonTitle = metadataDetails.provider === 'tmdb' ? 'TMDb Info' : 'OMDb Info';
|
||||
const metadataJsonValue = metadataDetails.provider === 'tmdb'
|
||||
? (metadataContext?.selectedMetadata?.tmdbDetails || metadataContext?.selectedMetadata || null)
|
||||
: job?.omdbInfo;
|
||||
const metadataAssignProvider = isDvdSeries
|
||||
? 'tmdb'
|
||||
: (metadataDetails.provider === 'tmdb' ? 'tmdb' : 'omdb');
|
||||
const metadataAssignButtonLabel = metadataAssignProvider === 'tmdb' ? 'TMDb neu zuweisen' : 'OMDb neu zuweisen';
|
||||
const metadataAssignActionDescription = metadataAssignProvider === 'tmdb'
|
||||
? 'Öffnet die TMDb-Suche, um Serien-Metadaten (Titel, Staffel, Episoden, Poster) neu zuzuweisen.'
|
||||
: 'Öffnet die OMDb-Suche, um Film-Metadaten (Titel, Jahr, Poster, IMDb-ID) neu zuzuweisen.';
|
||||
const configuredSelection = buildConfiguredScriptAndChainSelection(job);
|
||||
const hasConfiguredSelection = configuredSelection.preScriptIds.length > 0
|
||||
|| configuredSelection.postScriptIds.length > 0
|
||||
@@ -832,8 +1139,13 @@ export default function JobDetailDialog({
|
||||
? job.encodePlan.userPreset
|
||||
: null;
|
||||
const reviewSelectedEncodeTitleId = job?.encodePlan?.encodeInputTitleId ?? null;
|
||||
const encodePlanTitles = Array.isArray(job?.encodePlan?.titles) ? job.encodePlan.titles : [];
|
||||
const selectedEncodeTitles = encodePlanTitles.filter(
|
||||
(title) => title.selectedForEncode || title.encodeInput || String(title.id) === String(reviewSelectedEncodeTitleId)
|
||||
);
|
||||
const executedHandBrakeCommand = buildExecutedHandBrakeCommand(job?.handbrakeInfo);
|
||||
const canDownloadRaw = Boolean(job?.raw_path && job?.rawStatus?.exists && typeof onDownloadArchive === 'function');
|
||||
const resolvedRawPath = job?.rawStatus?.path || job?.raw_path || null;
|
||||
const canDownloadRaw = Boolean(resolvedRawPath && job?.rawStatus?.exists && typeof onDownloadArchive === 'function');
|
||||
const canDownloadOutput = Boolean(job?.output_path && job?.outputStatus?.exists && typeof onDownloadArchive === 'function');
|
||||
const outputFolders = Array.isArray(job?.outputFolders) ? job.outputFolders : [];
|
||||
const hasAnyOutputFolder = outputFolders.length > 0 || Boolean(job?.outputStatus?.exists);
|
||||
@@ -979,12 +1291,24 @@ export default function JobDetailDialog({
|
||||
) : (
|
||||
<>
|
||||
<section className="job-meta-block job-meta-block-film">
|
||||
<h4>{isAudiobook ? 'Audiobook-Infos' : 'Film-Infos'}</h4>
|
||||
<h4>{isAudiobook ? 'Audiobook-Infos' : (isDvdSeries ? 'Serieninfos' : 'Film-Infos')}</h4>
|
||||
<div className="job-meta-list">
|
||||
<div className="job-meta-item">
|
||||
<strong>Titel:</strong>
|
||||
<span>{job.title || job.detected_title || '-'}</span>
|
||||
</div>
|
||||
{isDvdSeries ? (
|
||||
<div className="job-meta-item">
|
||||
<strong>Staffel:</strong>
|
||||
<span>{metadataContext?.selectedMetadata?.seasonNumber ?? '-'}</span>
|
||||
</div>
|
||||
) : null}
|
||||
{isDvdSeries ? (
|
||||
<div className="job-meta-item">
|
||||
<strong>{isSeriesContainer ? 'Container' : 'Disk'}:</strong>
|
||||
<span>{isSeriesContainer ? 'Ja' : (seriesDiscNumber ? `Disk ${seriesDiscNumber}` : '-')}</span>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="job-meta-item">
|
||||
<strong>Jahr:</strong>
|
||||
<span>{job.year || '-'}</span>
|
||||
@@ -1024,11 +1348,17 @@ export default function JobDetailDialog({
|
||||
<>
|
||||
<div className="job-meta-item">
|
||||
<strong>IMDb:</strong>
|
||||
<span>{job.imdb_id || '-'}</span>
|
||||
<span>{displayedImdbId || '-'}</span>
|
||||
</div>
|
||||
{isDvdSeries ? (
|
||||
<div className="job-meta-item">
|
||||
<strong>TMDb:</strong>
|
||||
<span>{metadataContext?.selectedMetadata?.tmdbId || '-'}</span>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="job-meta-item">
|
||||
<strong>OMDb Match:</strong>
|
||||
<BoolState value={job.selected_from_omdb} />
|
||||
<strong>{metadataDetails.matchLabel}:</strong>
|
||||
<BoolState value={metadataDetails.hasMatch} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -1044,32 +1374,41 @@ export default function JobDetailDialog({
|
||||
|
||||
{!isAudiobook ? (
|
||||
<section className="job-meta-block job-meta-block-film">
|
||||
<h4>OMDb Details</h4>
|
||||
<h4>{metadataDetails.title}</h4>
|
||||
<div className="job-meta-list">
|
||||
<div className="job-meta-item">
|
||||
<strong>Regisseur:</strong>
|
||||
<span>{omdbField(omdbInfo?.Director)}</span>
|
||||
<span>{omdbField(metadataDetails.director)}</span>
|
||||
</div>
|
||||
<div className="job-meta-item">
|
||||
<strong>Schauspieler:</strong>
|
||||
<span>{omdbField(omdbInfo?.Actors)}</span>
|
||||
<span>{omdbField(metadataDetails.actors)}</span>
|
||||
</div>
|
||||
<div className="job-meta-item">
|
||||
<strong>Laufzeit:</strong>
|
||||
<span>{omdbField(omdbInfo?.Runtime)}</span>
|
||||
<span>{omdbField(metadataDetails.runtime)}</span>
|
||||
</div>
|
||||
<div className="job-meta-item">
|
||||
<strong>Genre:</strong>
|
||||
<span>{omdbField(omdbInfo?.Genre)}</span>
|
||||
</div>
|
||||
<div className="job-meta-item">
|
||||
<strong>Rotten Tomatoes:</strong>
|
||||
<span>{omdbRottenTomatoesScore(omdbInfo)}</span>
|
||||
</div>
|
||||
<div className="job-meta-item">
|
||||
<strong>imdbRating:</strong>
|
||||
<span>{omdbField(omdbInfo?.imdbRating)}</span>
|
||||
<span>{omdbField(metadataDetails.genre)}</span>
|
||||
</div>
|
||||
{metadataDetails.provider === 'tmdb' ? (
|
||||
<div className="job-meta-item">
|
||||
<strong>TMDb Rating:</strong>
|
||||
<span>{omdbField(metadataDetails.tmdbRating)}</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="job-meta-item">
|
||||
<strong>Rotten Tomatoes:</strong>
|
||||
<span>{omdbField(metadataDetails.rottenTomatoes)}</span>
|
||||
</div>
|
||||
<div className="job-meta-item">
|
||||
<strong>imdbRating:</strong>
|
||||
<span>{omdbField(metadataDetails.imdbRating)}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
@@ -1101,9 +1440,17 @@ export default function JobDetailDialog({
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span><strong>{isAudiobook || isConverter ? 'Import:' : 'Backup:'}</strong> <BoolState value={job?.backupSuccess} /></span>
|
||||
<span><strong>{isAudiobook || isConverter ? 'Import:' : 'Backup:'}</strong> {isSeriesContainer && seriesBackupSummary ? (
|
||||
<TriState existing={seriesBackupSummary.existing} expected={seriesBackupSummary.expected} />
|
||||
) : (
|
||||
<BoolState value={job?.backupSuccess} />
|
||||
)}</span>
|
||||
<span className="job-infos-sep">|</span>
|
||||
<span><strong>Encode:</strong> <BoolState value={job?.encodeSuccess} /></span>
|
||||
<span><strong>Encode:</strong> {isSeriesContainer && seriesEncodeSummary ? (
|
||||
<TriState existing={seriesEncodeSummary.existing} expected={seriesEncodeSummary.expected} />
|
||||
) : (
|
||||
<BoolState value={job?.encodeSuccess} />
|
||||
)}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -1115,11 +1462,29 @@ export default function JobDetailDialog({
|
||||
{/* Zeile 3+4: Pfade */}
|
||||
<PathField
|
||||
label={isCd ? 'WAV:' : (isConverter ? 'Input:' : 'RAW:')}
|
||||
value={isCd ? (job.raw_path || job.output_path) : job.raw_path}
|
||||
value={isSeriesContainer ? null : (isCd ? (job.raw_path || job.output_path) : resolvedRawPath)}
|
||||
onDownload={canDownloadRaw ? () => onDownloadArchive?.(job, 'raw') : null}
|
||||
downloadDisabled={!canDownloadRaw}
|
||||
downloadLoading={downloadBusyTarget === 'raw'}
|
||||
/>
|
||||
{isSeriesContainer && childJobs.length > 0 ? (
|
||||
childJobs.map((child) => {
|
||||
const childDiscNumber = resolveSeriesDiscNumber(child);
|
||||
const childLabel = childDiscNumber ? `Disk ${childDiscNumber} RAW:` : 'Disk RAW:';
|
||||
const childRawPath = child?.raw_path || child?.output_path || null;
|
||||
const canDownloadChildRaw = Boolean(childRawPath && child?.rawStatus?.exists && typeof onDownloadArchive === 'function');
|
||||
return (
|
||||
<PathField
|
||||
key={`child-raw-${child.id}`}
|
||||
label={childLabel}
|
||||
value={childRawPath}
|
||||
onDownload={canDownloadChildRaw ? () => onDownloadArchive?.(child, 'raw') : null}
|
||||
downloadDisabled={!canDownloadChildRaw}
|
||||
downloadLoading={downloadBusyTarget === 'raw'}
|
||||
/>
|
||||
);
|
||||
})
|
||||
) : null}
|
||||
{outputFolders.length > 0 ? (
|
||||
outputFolders.map((folder, idx) => {
|
||||
const folderPath = String(folder?.output_path || '').trim();
|
||||
@@ -1149,7 +1514,23 @@ export default function JobDetailDialog({
|
||||
/>
|
||||
)}
|
||||
{job.error_message ? (
|
||||
<div><strong>Fehler:</strong> {job.error_message}</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', flexWrap: 'wrap' }}>
|
||||
<span><strong>Fehler:</strong> {job.error_message}</span>
|
||||
{typeof onAcknowledgeError === 'function' ? (
|
||||
<Button
|
||||
icon="pi pi-check-circle"
|
||||
rounded
|
||||
text
|
||||
size="small"
|
||||
severity="success"
|
||||
title="Fehler quittieren"
|
||||
aria-label="Fehler quittieren"
|
||||
onClick={() => onAcknowledgeError(job)}
|
||||
loading={acknowledgeErrorBusy}
|
||||
disabled={acknowledgeErrorBusy}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
@@ -1219,6 +1600,119 @@ export default function JobDetailDialog({
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{isDvdSeries && seriesBatchEpisodes.length > 0 ? (
|
||||
<section className="job-meta-block job-meta-block-full">
|
||||
<h4>Serien-Episoden</h4>
|
||||
<div className="series-batch-progress-wrap">
|
||||
<div className="series-batch-head">
|
||||
<div>
|
||||
<strong>Gesamt:</strong>{' '}
|
||||
{`${seriesBatchSummary.finished}/${seriesBatchSummary.total} fertig`}
|
||||
{seriesBatchSummary.running > 0 ? ` | laufend: ${seriesBatchSummary.running}` : ''}
|
||||
{seriesBatchSummary.queued > 0 ? ` | wartend: ${seriesBatchSummary.queued}` : ''}
|
||||
{seriesBatchSummary.error > 0 ? ` | fehler: ${seriesBatchSummary.error}` : ''}
|
||||
{seriesBatchSummary.cancelled > 0 ? ` | abgebrochen: ${seriesBatchSummary.cancelled}` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div className="series-batch-episodes">
|
||||
{seriesBatchEpisodes.map((episode) => {
|
||||
const assignment = seriesEpisodeAssignments[String(episode?.titleId)]
|
||||
|| seriesEpisodeAssignments[Number(episode?.titleId)]
|
||||
|| null;
|
||||
const statusMeta = seriesEpisodeStatusMeta(episode?.status);
|
||||
const episodeLabel = String(
|
||||
episode?.label
|
||||
|| assignment?.episodeTitle
|
||||
|| `Episode ${episode?.episodeIndex || '?'}`
|
||||
).trim();
|
||||
const episodeCode = (() => {
|
||||
const seasonNo = normalizePositiveInteger(assignment?.seasonNumber ?? episode?.seasonNumber);
|
||||
const episodeNoRaw = Number(assignment?.episodeNumber ?? episode?.episodeNumber);
|
||||
const episodeNo = Number.isFinite(episodeNoRaw) && episodeNoRaw > 0
|
||||
? episodeNoRaw
|
||||
: null;
|
||||
if (!seasonNo && !episodeNo) {
|
||||
return null;
|
||||
}
|
||||
const seasonToken = seasonNo ? String(seasonNo).padStart(2, '0') : '--';
|
||||
const episodeToken = episodeNo
|
||||
? (
|
||||
Number.isInteger(episodeNo)
|
||||
? String(Math.trunc(episodeNo)).padStart(2, '0')
|
||||
: String(episodeNo)
|
||||
)
|
||||
: '--';
|
||||
return `S${seasonToken}E${episodeToken}`;
|
||||
})();
|
||||
const trackSelection = episode?.trackSelection && typeof episode.trackSelection === 'object'
|
||||
? episode.trackSelection
|
||||
: null;
|
||||
const audioTrackIds = Array.isArray(trackSelection?.audioTrackIds) ? trackSelection.audioTrackIds : [];
|
||||
const subtitleTrackIds = Array.isArray(trackSelection?.subtitleTrackIds) ? trackSelection.subtitleTrackIds : [];
|
||||
const subtitleForcedIndexes = Array.isArray(trackSelection?.subtitleForcedTrackIndexes)
|
||||
? trackSelection.subtitleForcedTrackIndexes
|
||||
: [];
|
||||
const episodeCommand = buildExecutedHandBrakeCommand(episode?.handbrakeInfo);
|
||||
const boundedProgress = Number.isFinite(Number(episode?.progress))
|
||||
? Math.max(0, Math.min(100, Number(episode.progress)))
|
||||
: 0;
|
||||
return (
|
||||
<details key={`series-episode-${episode.episodeIndex}-${episode.titleId || 'na'}`} className="episode-track-accordion">
|
||||
<summary className="series-batch-episode-head">
|
||||
<span className="series-batch-episode-title">
|
||||
{`#${episode?.episodeIndex || '?'} | ${episodeLabel}${episodeCode ? ` | ${episodeCode}` : ''}${seriesDiscNumber ? ` | Disk ${seriesDiscNumber}` : ''}`}
|
||||
</span>
|
||||
<Tag value={statusMeta.label} severity={statusMeta.severity} />
|
||||
</summary>
|
||||
<div className="track-group">
|
||||
<div className="track-item">
|
||||
<span><strong>Titel-ID:</strong> {episode?.titleId || '-'}</span>
|
||||
</div>
|
||||
<div className="track-item">
|
||||
<span><strong>Fortschritt:</strong> {`${boundedProgress.toFixed(2)}%`}</span>
|
||||
</div>
|
||||
<div className="track-item">
|
||||
<span><strong>Gestartet:</strong> {formatDateTimeOrDash(episode?.startedAt)}</span>
|
||||
</div>
|
||||
<div className="track-item">
|
||||
<span><strong>Beendet:</strong> {formatDateTimeOrDash(episode?.finishedAt)}</span>
|
||||
</div>
|
||||
<div className="track-item">
|
||||
<span><strong>Audio-Spuren:</strong> {audioTrackIds.length > 0 ? audioTrackIds.join(', ') : '-'}</span>
|
||||
</div>
|
||||
<div className="track-item">
|
||||
<span><strong>Subtitle-Spuren:</strong> {subtitleTrackIds.length > 0 ? subtitleTrackIds.join(', ') : '-'}</span>
|
||||
</div>
|
||||
<div className="track-item">
|
||||
<span><strong>Subtitle Forced-Index:</strong> {subtitleForcedIndexes.length > 0 ? subtitleForcedIndexes.join(', ') : '-'}</span>
|
||||
</div>
|
||||
{episode?.outputPath ? (
|
||||
<div className="track-item">
|
||||
<span><strong>Output:</strong> {episode.outputPath}</span>
|
||||
</div>
|
||||
) : null}
|
||||
{episode?.error ? (
|
||||
<div className="track-item">
|
||||
<span><strong>Fehler:</strong> {episode.error}</span>
|
||||
</div>
|
||||
) : null}
|
||||
{episodeCommand ? (
|
||||
<div className="track-item">
|
||||
<span><strong>Command:</strong> {episodeCommand}</span>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="track-item">
|
||||
<small className="track-action-note">Hinweis: Alle Episoden-Logs liegen im Hauptjob-Log.</small>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{!isCd && !isAudiobook && !isConverter && (hasConfiguredSelection || encodePlanUserPreset || job.encodePlan?.minLengthMinutes != null || Array.isArray(job.encodePlan?.titles)) ? (
|
||||
<section className="job-meta-block job-meta-block-full">
|
||||
<h4>Encode-Konfiguration</h4>
|
||||
@@ -1257,12 +1751,74 @@ export default function JobDetailDialog({
|
||||
</div>
|
||||
) : null}
|
||||
{/* Spurauswahl – read-only */}
|
||||
{job.encodePlan && Array.isArray(job.encodePlan.titles) && job.encodePlan.titles.length > 0 ? (
|
||||
<div className="spurauswahl-block">
|
||||
<div className="spurauswahl-label">Spurauswahl</div>
|
||||
{job.encodePlan.titles
|
||||
.filter((t) => t.selectedForEncode || t.encodeInput || String(t.id) === String(reviewSelectedEncodeTitleId))
|
||||
.map((title) => {
|
||||
{selectedEncodeTitles.length > 0 ? (
|
||||
isDvdSeries ? (
|
||||
<details className="episode-track-accordion">
|
||||
<summary className="series-batch-episode-head">
|
||||
<span className="series-batch-episode-title">{seriesDiscLabel}</span>
|
||||
</summary>
|
||||
<div className="spurauswahl-block">
|
||||
<div className="spurauswahl-label">Spurauswahl</div>
|
||||
{selectedEncodeTitles.map((title) => {
|
||||
const audioTracks = Array.isArray(title.audioTracks) ? title.audioTracks : [];
|
||||
const subtitleTracks = Array.isArray(title.subtitleTracks) ? title.subtitleTracks : [];
|
||||
return (
|
||||
<div key={title.id} className="track-title-block">
|
||||
<div className="track-title-row">
|
||||
<span>#{title.id} | {title.fileName} | {title.durationMinutes != null ? `${Number(title.durationMinutes).toFixed(2)} min` : '-'}</span>
|
||||
</div>
|
||||
<div className="track-groups-row">
|
||||
{audioTracks.length > 0 ? (
|
||||
<div className="track-group">
|
||||
<div className="track-group-label">Tonspuren (Titel #{title.id})</div>
|
||||
{audioTracks.map((track) => {
|
||||
const lang = trackLang(track.language || track.languageLabel);
|
||||
const codec = trackCodec('audio', track.format, track.description || track.title);
|
||||
const chLayout = trackChLayout(track.channels);
|
||||
let displayText = `#${track.id} | ${lang} | ${codec}`;
|
||||
if (chLayout) displayText += ` | ${chLayout}`;
|
||||
const actionInfo = track.selectedForEncode
|
||||
? (String(track.encodePreviewSummary || track.encodeActionSummary || '').trim() || 'Copy')
|
||||
: 'Nicht übernommen';
|
||||
return (
|
||||
<div key={track.id} className="track-item">
|
||||
<span>{displayText}</span>
|
||||
<small className="track-action-note">Encode: {actionInfo}</small>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
{subtitleTracks.length > 0 ? (
|
||||
<div className="track-group">
|
||||
<div className="track-group-label">Subtitles (Titel #{title.id})</div>
|
||||
{subtitleTracks.map((track) => {
|
||||
const lang = trackLang(track.language || track.languageLabel);
|
||||
const codec = trackCodec('subtitle', track.format);
|
||||
const displayText = `#${track.id} | ${lang} | ${codec}`;
|
||||
const rawAction = String(track.subtitlePreviewSummary || track.subtitleActionSummary || '').trim();
|
||||
const actionInfo = track.selectedForEncode
|
||||
? (/^nicht übernommen$/i.test(rawAction) ? 'Übernehmen' : (rawAction || 'Übernehmen'))
|
||||
: 'Nicht übernommen';
|
||||
return (
|
||||
<div key={track.id} className="track-item">
|
||||
<span>{displayText}</span>
|
||||
<small className="track-action-note">Encode: {actionInfo}</small>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</details>
|
||||
) : (
|
||||
<div className="spurauswahl-block">
|
||||
<div className="spurauswahl-label">Spurauswahl</div>
|
||||
{selectedEncodeTitles.map((title) => {
|
||||
const audioTracks = Array.isArray(title.audioTracks) ? title.audioTracks : [];
|
||||
const subtitleTracks = Array.isArray(title.subtitleTracks) ? title.subtitleTracks : [];
|
||||
return (
|
||||
@@ -1315,9 +1871,9 @@ export default function JobDetailDialog({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
) : null}
|
||||
</section>
|
||||
) : null}
|
||||
@@ -1421,15 +1977,55 @@ export default function JobDetailDialog({
|
||||
|
||||
<section className="job-meta-block job-meta-block-full">
|
||||
<h4>Logs</h4>
|
||||
<div className="job-json-grid">
|
||||
{!isCd && !isAudiobook && !isConverter ? <JsonView title="OMDb Info" value={job.omdbInfo} /> : null}
|
||||
{isCd ? <JsonView title="MusicBrainz" value={job.makemkvInfo?.selectedMetadata ?? null} /> : null}
|
||||
{isAudiobook ? <JsonView title="Metadaten" value={job.handbrakeInfo?.metadata ?? job.makemkvInfo?.selectedMetadata ?? null} /> : null}
|
||||
<JsonView title={isCd ? 'cdparanoia Info' : (isAudiobook ? 'Audiobook Info' : (isConverter ? 'Converter Analyze Info' : 'MakeMKV Info'))} value={job.makemkvInfo} />
|
||||
{!isCd && !isAudiobook ? <JsonView title="Mediainfo Info" value={job.mediainfoInfo} /> : null}
|
||||
<JsonView title={isCd ? 'Rip-Plan' : (isConverter ? 'Converter Plan' : 'Encode Plan')} value={job.encodePlan} />
|
||||
<JsonView title={isCd ? 'Rip-Info' : (isAudiobook ? 'FFmpeg Info' : (isConverter ? 'Converter Encode Info' : 'HandBrake Info'))} value={job.handbrakeInfo} />
|
||||
</div>
|
||||
{isSeriesContainer && childJobs.length > 0 ? (
|
||||
<>
|
||||
<div className="job-json-grid">
|
||||
{!isCd && !isAudiobook && !isConverter ? <JsonView title={metadataJsonTitle} value={metadataJsonValue} /> : null}
|
||||
</div>
|
||||
{childJobs.map((child) => {
|
||||
const childDiscNumber = resolveSeriesDiscNumber(child);
|
||||
const childDiscLabel = childDiscNumber ? `Disk ${childDiscNumber}` : 'Disk unbekannt';
|
||||
return (
|
||||
<details key={`child-log-${child.id}`} className="episode-track-accordion">
|
||||
<summary className="series-batch-episode-head">
|
||||
<span className="series-batch-episode-title">{childDiscLabel}</span>
|
||||
</summary>
|
||||
<div className="job-json-grid">
|
||||
<JsonView title={isCd ? 'cdparanoia Info' : (isAudiobook ? 'Audiobook Info' : (isConverter ? 'Converter Analyze Info' : 'MakeMKV Info'))} value={child.makemkvInfo} />
|
||||
{!isCd && !isAudiobook ? <JsonView title="Mediainfo Info" value={child.mediainfoInfo} /> : null}
|
||||
<JsonView title={isCd ? 'Rip-Plan' : (isConverter ? 'Converter Plan' : 'Encode Plan')} value={child.encodePlan} />
|
||||
<JsonView title={isCd ? 'Rip-Info' : (isAudiobook ? 'FFmpeg Info' : (isConverter ? 'Converter Encode Info' : 'HandBrake Info'))} value={child.handbrakeInfo} />
|
||||
</div>
|
||||
</details>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
) : isDvdSeries ? (
|
||||
<details className="episode-track-accordion">
|
||||
<summary className="series-batch-episode-head">
|
||||
<span className="series-batch-episode-title">{seriesDiscLabel}</span>
|
||||
</summary>
|
||||
<div className="job-json-grid">
|
||||
{!isCd && !isAudiobook && !isConverter ? <JsonView title={metadataJsonTitle} value={metadataJsonValue} /> : null}
|
||||
{isCd ? <JsonView title="MusicBrainz" value={job.makemkvInfo?.selectedMetadata ?? null} /> : null}
|
||||
{isAudiobook ? <JsonView title="Metadaten" value={job.handbrakeInfo?.metadata ?? job.makemkvInfo?.selectedMetadata ?? null} /> : null}
|
||||
<JsonView title={isCd ? 'cdparanoia Info' : (isAudiobook ? 'Audiobook Info' : (isConverter ? 'Converter Analyze Info' : 'MakeMKV Info'))} value={job.makemkvInfo} />
|
||||
{!isCd && !isAudiobook ? <JsonView title="Mediainfo Info" value={job.mediainfoInfo} /> : null}
|
||||
<JsonView title={isCd ? 'Rip-Plan' : (isConverter ? 'Converter Plan' : 'Encode Plan')} value={job.encodePlan} />
|
||||
<JsonView title={isCd ? 'Rip-Info' : (isAudiobook ? 'FFmpeg Info' : (isConverter ? 'Converter Encode Info' : 'HandBrake Info'))} value={job.handbrakeInfo} />
|
||||
</div>
|
||||
</details>
|
||||
) : (
|
||||
<div className="job-json-grid">
|
||||
{!isCd && !isAudiobook && !isConverter ? <JsonView title={metadataJsonTitle} value={metadataJsonValue} /> : null}
|
||||
{isCd ? <JsonView title="MusicBrainz" value={job.makemkvInfo?.selectedMetadata ?? null} /> : null}
|
||||
{isAudiobook ? <JsonView title="Metadaten" value={job.handbrakeInfo?.metadata ?? job.makemkvInfo?.selectedMetadata ?? null} /> : null}
|
||||
<JsonView title={isCd ? 'cdparanoia Info' : (isAudiobook ? 'Audiobook Info' : (isConverter ? 'Converter Analyze Info' : 'MakeMKV Info'))} value={job.makemkvInfo} />
|
||||
{!isCd && !isAudiobook ? <JsonView title="Mediainfo Info" value={job.mediainfoInfo} /> : null}
|
||||
<JsonView title={isCd ? 'Rip-Plan' : (isConverter ? 'Converter Plan' : 'Encode Plan')} value={job.encodePlan} />
|
||||
<JsonView title={isCd ? 'Rip-Info' : (isAudiobook ? 'FFmpeg Info' : (isConverter ? 'Converter Encode Info' : 'HandBrake Info'))} value={job.handbrakeInfo} />
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
|
||||
@@ -1461,7 +2057,24 @@ export default function JobDetailDialog({
|
||||
{logTruncated ? <small>(gekürzt auf letzte 800 Zeilen)</small> : null}
|
||||
</div>
|
||||
{logLoaded ? (
|
||||
<pre className="log-box">{job.log || ''}</pre>
|
||||
isSeriesContainer && childJobs.length > 0 ? (
|
||||
<div className="log-box">
|
||||
{childJobs.map((child) => {
|
||||
const childDiscNumber = resolveSeriesDiscNumber(child);
|
||||
const childDiscLabel = childDiscNumber ? `Disk ${childDiscNumber}` : 'Disk unbekannt';
|
||||
return (
|
||||
<details key={`child-log-text-${child.id}`} className="episode-track-accordion">
|
||||
<summary className="series-batch-episode-head">
|
||||
<span className="series-batch-episode-title">{childDiscLabel}</span>
|
||||
</summary>
|
||||
<pre>{child.log || ''}</pre>
|
||||
</details>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<pre className="log-box">{job.log || ''}</pre>
|
||||
)
|
||||
) : (
|
||||
<p>Log nicht vorgeladen. Über die Buttons oben laden.</p>
|
||||
)}
|
||||
@@ -1595,7 +2208,7 @@ export default function JobDetailDialog({
|
||||
size="small"
|
||||
onClick={() => onRestartReview?.(job)}
|
||||
loading={actionBusy}
|
||||
disabled={!canRestartReview || isUnencodedOrphanImport}
|
||||
disabled={!canRestartReview}
|
||||
/>
|
||||
<span className="action-desc">{isAudiobook
|
||||
? 'Öffnet die Kapitel-Auswahl erneut, um Kapitel anzupassen bevor der Encode startet.'
|
||||
@@ -1610,9 +2223,9 @@ export default function JobDetailDialog({
|
||||
icon="pi pi-sync"
|
||||
severity="info"
|
||||
size="small"
|
||||
onClick={() => isUnencodedOrphanImport ? onRestartReview?.(job) : onReencode?.(job)}
|
||||
loading={isUnencodedOrphanImport ? actionBusy : reencodeBusy}
|
||||
disabled={isUnencodedOrphanImport ? !canRestartReview : !canReencode}
|
||||
onClick={() => onReencode?.(job)}
|
||||
loading={reencodeBusy}
|
||||
disabled={!canReencode}
|
||||
/>
|
||||
<span className="action-desc">{isAudiobook
|
||||
? 'Liest die AAX-Datei neu ein und öffnet danach die Kapitel-Auswahl. Sinnvoll wenn sich die Quelldatei geändert hat.'
|
||||
@@ -1629,7 +2242,7 @@ export default function JobDetailDialog({
|
||||
<div className="actions-group-label"><i className="pi pi-search" /> Metadaten</div>
|
||||
<div className="action-item">
|
||||
<Button
|
||||
label="OMDB neu zuweisen"
|
||||
label={metadataAssignButtonLabel}
|
||||
icon="pi pi-search"
|
||||
severity="secondary"
|
||||
outlined
|
||||
@@ -1638,7 +2251,7 @@ export default function JobDetailDialog({
|
||||
loading={omdbAssignBusy}
|
||||
disabled={running}
|
||||
/>
|
||||
<span className="action-desc">Öffnet die OMDB-Suche, um Film-Metadaten (Titel, Jahr, Poster, IMDb-ID) neu zuzuweisen.</span>
|
||||
<span className="action-desc">{metadataAssignActionDescription}</span>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
Reference in New Issue
Block a user