import { Dialog } from 'primereact/dialog';
import { Button } from 'primereact/button';
import MediaInfoReviewPanel from './MediaInfoReviewPanel';
import blurayIndicatorIcon from '../assets/media-bluray.svg';
import discIndicatorIcon from '../assets/media-disc.svg';
import otherIndicatorIcon from '../assets/media-other.svg';
import { getStatusLabel } from '../utils/statusPresentation';
const CD_FORMAT_LABELS = {
flac: 'FLAC',
wav: 'WAV',
mp3: 'MP3',
opus: 'Opus',
ogg: 'Ogg Vorbis'
};
function JsonView({ title, value }) {
return (
{title}
{value ? JSON.stringify(value, null, 2) : '-'}
);
}
function ScriptResultRow({ result }) {
const status = String(result?.status || '').toUpperCase();
const isSuccess = status === 'SUCCESS';
const isError = status === 'ERROR';
const icon = isSuccess ? 'pi-check-circle' : isError ? 'pi-times-circle' : 'pi-minus-circle';
const tone = isSuccess ? 'success' : isError ? 'danger' : 'warning';
return (
{result?.scriptName || result?.chainName || `#${result?.scriptId ?? result?.chainId ?? '?'}`}
{status}
{result?.error ? {result.error} : null}
);
}
function ScriptSummarySection({ title, summary }) {
if (!summary || summary.configured === 0) return null;
const results = Array.isArray(summary.results) ? summary.results : [];
return (
{title}:
{summary.succeeded > 0 ? {summary.succeeded} OK : null}
{summary.failed > 0 ? {summary.failed} Fehler : null}
{summary.skipped > 0 ? {summary.skipped} übersprungen : null}
{results.length > 0 ? (
{results.map((r, i) => )}
) : null}
);
}
function normalizeIdList(values) {
const list = Array.isArray(values) ? values : [];
const seen = new Set();
const output = [];
for (const value of list) {
const parsed = Number(value);
if (!Number.isFinite(parsed) || parsed <= 0) {
continue;
}
const id = Math.trunc(parsed);
const key = String(id);
if (seen.has(key)) {
continue;
}
seen.add(key);
output.push(id);
}
return output;
}
function normalizePositiveInteger(value) {
const parsed = Number(value);
if (!Number.isFinite(parsed) || parsed <= 0) {
return null;
}
return Math.trunc(parsed);
}
function formatDurationSeconds(totalSeconds) {
const parsed = Number(totalSeconds);
if (!Number.isFinite(parsed) || parsed <= 0) {
return null;
}
const rounded = Math.max(0, Math.trunc(parsed));
const hours = Math.floor(rounded / 3600);
const minutes = Math.floor((rounded % 3600) / 60);
const seconds = rounded % 60;
if (hours > 0) {
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
}
return `${minutes}:${String(seconds).padStart(2, '0')}`;
}
function shellQuote(value) {
const raw = String(value ?? '');
if (raw.length === 0) {
return "''";
}
if (/^[A-Za-z0-9_./:=,+-]+$/.test(raw)) {
return raw;
}
return `'${raw.replace(/'/g, `'"'"'`)}'`;
}
function buildExecutedHandBrakeCommand(handbrakeInfo) {
const cmd = String(handbrakeInfo?.cmd || '').trim();
const args = Array.isArray(handbrakeInfo?.args) ? handbrakeInfo.args : [];
if (!cmd) {
return null;
}
return `${cmd} ${args.map((arg) => shellQuote(arg)).join(' ')}`.trim();
}
function buildConfiguredScriptAndChainSelection(job) {
const plan = job?.encodePlan && typeof job.encodePlan === 'object' ? job.encodePlan : {};
const handbrakeInfo = job?.handbrakeInfo && typeof job.handbrakeInfo === 'object' ? job.handbrakeInfo : {};
const scriptNameById = new Map();
const chainNameById = new Map();
const addScriptHint = (idValue, nameValue) => {
const id = normalizeIdList([idValue])[0] || null;
const name = String(nameValue || '').trim();
if (!id || !name || scriptNameById.has(id)) {
return;
}
scriptNameById.set(id, name);
};
const addChainHint = (idValue, nameValue) => {
const id = normalizeIdList([idValue])[0] || null;
const name = String(nameValue || '').trim();
if (!id || !name || chainNameById.has(id)) {
return;
}
chainNameById.set(id, name);
};
const addScriptHintsFromList = (list) => {
for (const item of (Array.isArray(list) ? list : [])) {
addScriptHint(item?.id ?? item?.scriptId, item?.name ?? item?.scriptName);
}
};
const addChainHintsFromList = (list) => {
for (const item of (Array.isArray(list) ? list : [])) {
addChainHint(item?.id ?? item?.chainId, item?.name ?? item?.chainName);
}
};
addScriptHintsFromList(plan?.preEncodeScripts);
addScriptHintsFromList(plan?.postEncodeScripts);
addChainHintsFromList(plan?.preEncodeChains);
addChainHintsFromList(plan?.postEncodeChains);
const scriptSummaries = [handbrakeInfo?.preEncodeScripts, handbrakeInfo?.postEncodeScripts];
for (const summary of scriptSummaries) {
const results = Array.isArray(summary?.results) ? summary.results : [];
for (const result of results) {
addScriptHint(result?.scriptId, result?.scriptName);
addChainHint(result?.chainId, result?.chainName);
}
}
const preScriptIds = normalizeIdList([
...(Array.isArray(plan?.preEncodeScriptIds) ? plan.preEncodeScriptIds : []),
...(Array.isArray(plan?.preEncodeScripts) ? plan.preEncodeScripts.map((item) => item?.id ?? item?.scriptId) : [])
]);
const postScriptIds = normalizeIdList([
...(Array.isArray(plan?.postEncodeScriptIds) ? plan.postEncodeScriptIds : []),
...(Array.isArray(plan?.postEncodeScripts) ? plan.postEncodeScripts.map((item) => item?.id ?? item?.scriptId) : [])
]);
const preChainIds = normalizeIdList([
...(Array.isArray(plan?.preEncodeChainIds) ? plan.preEncodeChainIds : []),
...(Array.isArray(plan?.preEncodeChains) ? plan.preEncodeChains.map((item) => item?.id ?? item?.chainId) : [])
]);
const postChainIds = normalizeIdList([
...(Array.isArray(plan?.postEncodeChainIds) ? plan.postEncodeChainIds : []),
...(Array.isArray(plan?.postEncodeChains) ? plan.postEncodeChains.map((item) => item?.id ?? item?.chainId) : [])
]);
return {
preScriptIds,
postScriptIds,
preChainIds,
postChainIds,
preScripts: preScriptIds.map((id) => scriptNameById.get(id) || `Skript #${id}`),
postScripts: postScriptIds.map((id) => scriptNameById.get(id) || `Skript #${id}`),
preChains: preChainIds.map((id) => chainNameById.get(id) || `Kette #${id}`),
postChains: postChainIds.map((id) => chainNameById.get(id) || `Kette #${id}`),
scriptCatalog: Array.from(scriptNameById.entries()).map(([id, name]) => ({ id, name })),
chainCatalog: Array.from(chainNameById.entries()).map(([id, name]) => ({ id, name }))
};
}
function resolveMediaType(job) {
const encodePlan = job?.encodePlan && typeof job.encodePlan === 'object' ? job.encodePlan : null;
const candidates = [
job?.mediaType,
job?.media_type,
job?.mediaProfile,
job?.media_profile,
encodePlan?.mediaProfile,
job?.makemkvInfo?.analyzeContext?.mediaProfile,
job?.makemkvInfo?.mediaProfile,
job?.mediainfoInfo?.mediaProfile
];
for (const candidate of candidates) {
const raw = String(candidate || '').trim().toLowerCase();
if (!raw) {
continue;
}
if (['bluray', 'blu-ray', 'blu_ray', 'bd', 'bdmv', 'bdrom', 'bd-rom', 'bd-r', 'bd-re'].includes(raw)) {
return 'bluray';
}
if (['dvd', 'disc', 'dvdvideo', 'dvd-video', 'dvdrom', 'dvd-rom', 'video_ts', 'iso9660'].includes(raw)) {
return 'dvd';
}
if (['cd', 'audio_cd', 'audio cd'].includes(raw)) {
return 'cd';
}
}
const statusCandidates = [job?.status, job?.last_state, job?.makemkvInfo?.lastState];
if (statusCandidates.some((v) => String(v || '').trim().toUpperCase().startsWith('CD_'))) {
return 'cd';
}
const planFormat = String(encodePlan?.format || '').trim().toLowerCase();
const hasCdTracksInPlan = Array.isArray(encodePlan?.selectedTracks) && encodePlan.selectedTracks.length > 0;
if (hasCdTracksInPlan && ['flac', 'wav', 'mp3', 'opus', 'ogg'].includes(planFormat)) {
return 'cd';
}
if (String(job?.handbrakeInfo?.mode || '').trim().toLowerCase() === 'cd_rip') {
return 'cd';
}
if (Array.isArray(job?.makemkvInfo?.tracks) && job.makemkvInfo.tracks.length > 0) {
return 'cd';
}
return 'other';
}
function resolveCdDetails(job) {
const encodePlan = job?.encodePlan && typeof job.encodePlan === 'object' ? job.encodePlan : {};
const makemkvInfo = job?.makemkvInfo && typeof job.makemkvInfo === 'object' ? job.makemkvInfo : {};
const selectedMetadata = makemkvInfo?.selectedMetadata && typeof makemkvInfo.selectedMetadata === 'object'
? makemkvInfo.selectedMetadata
: {};
const tracksSource = Array.isArray(makemkvInfo?.tracks) && makemkvInfo.tracks.length > 0
? makemkvInfo.tracks
: (Array.isArray(encodePlan?.tracks) ? encodePlan.tracks : []);
const tracks = tracksSource
.map((track) => {
const position = normalizePositiveInteger(track?.position);
if (!position) {
return null;
}
return { ...track, position, selected: track?.selected !== false };
})
.filter(Boolean);
const selectedTracksFromPlan = Array.isArray(encodePlan?.selectedTracks)
? encodePlan.selectedTracks.map((v) => normalizePositiveInteger(v)).filter(Boolean)
: [];
const selectedTrackPositions = selectedTracksFromPlan.length > 0
? selectedTracksFromPlan
: tracks.filter((t) => t.selected !== false).map((t) => t.position);
const fallbackArtist = tracks.map((t) => String(t?.artist || '').trim()).find(Boolean) || null;
const fallbackAlbum = tracks.map((t) => String(t?.album || '').trim()).find(Boolean) || null;
const totalDurationSec = tracks.reduce((sum, t) => {
const ms = Number(t?.durationMs);
const sec = Number(t?.durationSec);
if (Number.isFinite(ms) && ms > 0) {
return sum + ms / 1000;
}
if (Number.isFinite(sec) && sec > 0) {
return sum + sec;
}
return sum;
}, 0);
const format = String(encodePlan?.format || '').trim().toLowerCase();
const mbId = String(
selectedMetadata?.mbId
|| selectedMetadata?.musicBrainzId
|| selectedMetadata?.musicbrainzId
|| selectedMetadata?.mbid
|| ''
).trim() || null;
return {
artist: String(selectedMetadata?.artist || '').trim() || fallbackArtist || null,
album: String(selectedMetadata?.album || '').trim() || fallbackAlbum || null,
trackCount: tracks.length,
selectedTrackCount: selectedTrackPositions.length,
format,
formatLabel: format ? (CD_FORMAT_LABELS[format] || format.toUpperCase()) : null,
totalDurationLabel: formatDurationSeconds(totalDurationSec),
mbId
};
}
function statusBadgeMeta(status, queued = false) {
const normalized = String(status || '').trim().toUpperCase();
const label = getStatusLabel(normalized, { queued });
if (queued) {
return { label, icon: 'pi-list', tone: 'info' };
}
if (normalized === 'FINISHED') {
return { label, icon: 'pi-check-circle', tone: 'success' };
}
if (normalized === 'ERROR') {
return { label, icon: 'pi-times-circle', tone: 'danger' };
}
if (normalized === 'CANCELLED') {
return { label, icon: 'pi-ban', tone: 'warning' };
}
if (normalized === 'READY_TO_ENCODE' || normalized === 'READY_TO_START') {
return { label, icon: 'pi-play-circle', tone: 'info' };
}
if (normalized === 'WAITING_FOR_USER_DECISION') {
return { label, icon: 'pi-exclamation-circle', tone: 'warning' };
}
if (normalized === 'METADATA_SELECTION') {
return { label, icon: 'pi-list', tone: 'warning' };
}
if (normalized === 'ANALYZING') {
return { label, icon: 'pi-search', tone: 'warning' };
}
if (normalized === 'RIPPING') {
return { label, icon: 'pi-download', tone: 'warning' };
}
if (normalized === 'MEDIAINFO_CHECK') {
return { label, icon: 'pi-sliders-h', tone: 'warning' };
}
if (normalized === 'ENCODING') {
return { label, icon: 'pi-cog', tone: 'warning' };
}
return { label: label || '-', icon: 'pi-info-circle', tone: 'secondary' };
}
function omdbField(value) {
const raw = String(value || '').trim();
return raw || '-';
}
function omdbRottenTomatoesScore(omdbInfo) {
const ratings = Array.isArray(omdbInfo?.Ratings) ? omdbInfo.Ratings : [];
const entry = ratings.find((item) => String(item?.Source || '').trim().toLowerCase() === 'rotten tomatoes');
return omdbField(entry?.Value);
}
function BoolState({ value }) {
const isTrue = Boolean(value);
return isTrue ? (
) : (
);
}
export default function JobDetailDialog({
visible,
job,
onHide,
detailLoading = false,
onLoadLog,
logLoadingMode = null,
onAssignOmdb,
onAssignCdMetadata,
onResumeReady,
onRestartEncode,
onRestartReview,
onReencode,
onRetry,
onDeleteFiles,
onDeleteEntry,
onRemoveFromQueue,
isQueued = false,
omdbAssignBusy = false,
cdMetadataAssignBusy = false,
actionBusy = false,
reencodeBusy = false,
deleteEntryBusy = false
}) {
const mkDone = Boolean(job?.ripSuccessful) || !job?.makemkvInfo || job?.makemkvInfo?.status === 'SUCCESS';
const running = ['ANALYZING', 'RIPPING', 'MEDIAINFO_CHECK', 'ENCODING'].includes(job?.status);
const showFinalLog = !running;
const canReencode = !!(job?.rawStatus?.exists && job?.rawStatus?.isEmpty !== true && mkDone && !running);
const canResumeReady = Boolean(
(String(job?.status || '').trim().toUpperCase() === 'READY_TO_ENCODE' || String(job?.last_state || '').trim().toUpperCase() === 'READY_TO_ENCODE')
&& !running
&& typeof onResumeReady === 'function'
);
const hasConfirmedPlan = Boolean(
job?.encodePlan
&& Array.isArray(job?.encodePlan?.titles)
&& job?.encodePlan?.titles.length > 0
&& Number(job?.encode_review_confirmed || 0) === 1
);
const hasRestartInput = Boolean(job?.encode_input_path || job?.raw_path || job?.encodePlan?.encodeInputPath);
const canRestartEncode = Boolean(hasConfirmedPlan && hasRestartInput && !running);
const canRestartReview = Boolean(
job?.rawStatus?.exists
&& job?.rawStatus?.isEmpty !== true
&& !running
&& typeof onRestartReview === 'function'
);
const canDeleteEntry = !running && typeof onDeleteEntry === 'function';
const queueLocked = Boolean(isQueued && job?.id);
const logCount = Number(job?.log_count || 0);
const logMeta = job?.logMeta && typeof job.logMeta === 'object' ? job.logMeta : null;
const logLoaded = Boolean(logMeta?.loaded) || Boolean(job?.log);
const logTruncated = Boolean(logMeta?.truncated);
const mediaType = resolveMediaType(job);
const isCd = mediaType === 'cd';
const cdDetails = isCd ? resolveCdDetails(job) : null;
const canRetry = isCd && !running && typeof onRetry === 'function';
const mediaTypeLabel = mediaType === 'bluray'
? 'Blu-ray'
: mediaType === 'dvd'
? 'DVD'
: isCd
? 'Audio CD'
: 'Sonstiges Medium';
const mediaTypeIcon = mediaType === 'bluray'
? blurayIndicatorIcon
: mediaType === 'dvd'
? discIndicatorIcon
: otherIndicatorIcon;
const mediaTypeAlt = mediaTypeLabel;
const statusMeta = statusBadgeMeta(job?.status, queueLocked);
const omdbInfo = job?.omdbInfo && typeof job.omdbInfo === 'object' ? job.omdbInfo : {};
const configuredSelection = buildConfiguredScriptAndChainSelection(job);
const hasConfiguredSelection = configuredSelection.preScriptIds.length > 0
|| configuredSelection.postScriptIds.length > 0
|| configuredSelection.preChainIds.length > 0
|| configuredSelection.postChainIds.length > 0;
const reviewPreEncodeItems = [
...configuredSelection.preScriptIds.map((id) => ({ type: 'script', id })),
...configuredSelection.preChainIds.map((id) => ({ type: 'chain', id }))
];
const reviewPostEncodeItems = [
...configuredSelection.postScriptIds.map((id) => ({ type: 'script', id })),
...configuredSelection.postChainIds.map((id) => ({ type: 'chain', id }))
];
const encodePlanUserPreset = job?.encodePlan?.userPreset && typeof job.encodePlan.userPreset === 'object'
? job.encodePlan.userPreset
: null;
const encodePlanUserPresetId = Number(encodePlanUserPreset?.id);
const reviewUserPresets = encodePlanUserPreset ? [encodePlanUserPreset] : [];
const executedHandBrakeCommand = buildExecutedHandBrakeCommand(job?.handbrakeInfo);
return (
);
}