0.13.0 DVD Series
This commit is contained in:
@@ -21,7 +21,7 @@ import otherIndicatorIcon from '../assets/media-other.svg';
|
||||
import audiobookIndicatorIcon from '../assets/media-audiobook.svg';
|
||||
import { getStatusLabel, getStatusSeverity, normalizeStatus } from '../utils/statusPresentation';
|
||||
import { confirmModal } from '../utils/confirmModal';
|
||||
import { isConverterJob, resolveMediaType as resolveCentralMediaType } from '../utils/jobTaxonomy';
|
||||
import { isConverterJob, isSeriesDvdJob, resolveMediaType as resolveCentralMediaType } from '../utils/jobTaxonomy';
|
||||
|
||||
const processingStates = ['ANALYZING', 'RIPPING', 'MEDIAINFO_CHECK', 'ENCODING', 'CD_ANALYZING', 'CD_RIPPING', 'CD_ENCODING'];
|
||||
const driveActiveStates = ['ANALYZING', 'RIPPING', 'MEDIAINFO_CHECK', 'CD_ANALYZING', 'CD_RIPPING'];
|
||||
@@ -543,11 +543,18 @@ function resolveMediaType(job) {
|
||||
|
||||
function mediaIndicatorMeta(job) {
|
||||
const mediaType = resolveMediaType(job);
|
||||
const isSeriesDvd = mediaType === 'dvd' && isSeriesDvdJob(job);
|
||||
if (mediaType === 'bluray') {
|
||||
return { mediaType, src: blurayIndicatorIcon, alt: 'Blu-ray', title: 'Blu-ray' };
|
||||
}
|
||||
if (mediaType === 'dvd') {
|
||||
return { mediaType, src: discIndicatorIcon, alt: 'DVD', title: 'DVD' };
|
||||
return {
|
||||
mediaType,
|
||||
src: discIndicatorIcon,
|
||||
alt: isSeriesDvd ? 'DVD Serie' : 'DVD',
|
||||
title: isSeriesDvd ? 'DVD Serie' : 'DVD',
|
||||
isSeriesDvd
|
||||
};
|
||||
}
|
||||
if (mediaType === 'cd') {
|
||||
return { mediaType, src: otherIndicatorIcon, alt: 'Audio CD', title: 'Audio CD' };
|
||||
@@ -748,7 +755,16 @@ function buildPipelineFromJob(job, currentPipeline, currentPipelineJobId) {
|
||||
mbId: resolvedCdMbId,
|
||||
coverUrl: resolvedCdCoverUrl,
|
||||
imdbId: job?.imdb_id || null,
|
||||
poster: job?.poster_url || resolvedCdCoverUrl || null
|
||||
poster: job?.poster_url || resolvedCdCoverUrl || null,
|
||||
metadataProvider: analyzeContext?.metadataProvider || 'omdb',
|
||||
providerId: analyzeContext?.selectedMetadata?.providerId || null,
|
||||
tmdbId: analyzeContext?.selectedMetadata?.tmdbId || null,
|
||||
metadataKind: analyzeContext?.selectedMetadata?.metadataKind || null,
|
||||
seasonNumber: analyzeContext?.selectedMetadata?.seasonNumber || analyzeContext?.seriesLookupHint?.seasonNumber || null,
|
||||
seasonName: analyzeContext?.selectedMetadata?.seasonName || null,
|
||||
episodeCount: analyzeContext?.selectedMetadata?.episodeCount || 0,
|
||||
episodes: Array.isArray(analyzeContext?.selectedMetadata?.episodes) ? analyzeContext.selectedMetadata.episodes : [],
|
||||
discNumber: analyzeContext?.selectedMetadata?.discNumber || analyzeContext?.seriesLookupHint?.discNumber || null
|
||||
};
|
||||
const mode = String(encodePlan?.mode || 'rip').trim().toLowerCase();
|
||||
const isPreRip = mode === 'pre_rip' || Boolean(encodePlan?.preRip);
|
||||
@@ -836,6 +852,10 @@ function buildPipelineFromJob(job, currentPipeline, currentPipelineJobId) {
|
||||
: [],
|
||||
selectedPlaylist: analyzeContext.selectedPlaylist || null,
|
||||
selectedTitleId: analyzeContext.selectedTitleId ?? null,
|
||||
metadataProvider: analyzeContext.metadataProvider || 'omdb',
|
||||
metadataCandidates: Array.isArray(analyzeContext.metadataCandidates) ? analyzeContext.metadataCandidates : [],
|
||||
seriesAnalysis: analyzeContext.seriesAnalysis || null,
|
||||
seriesLookupHint: analyzeContext.seriesLookupHint || null,
|
||||
omdbCandidates: [],
|
||||
canRestartEncodeFromLastSettings,
|
||||
canRestartReviewFromRaw
|
||||
@@ -1048,7 +1068,8 @@ export default function RipperPage({
|
||||
api.getJobs({
|
||||
statuses: Array.from(ripperStatuses),
|
||||
limit: 160,
|
||||
lite: true
|
||||
lite: true,
|
||||
includeChildren: true
|
||||
}),
|
||||
api.getPipelineQueue()
|
||||
]);
|
||||
@@ -1062,6 +1083,9 @@ export default function RipperPage({
|
||||
if (isConverterJob(job)) {
|
||||
return false;
|
||||
}
|
||||
if (String(job?.job_kind || '').trim().toLowerCase() === 'dvd_series_container') {
|
||||
return false;
|
||||
}
|
||||
const normalizedStatus = String(job?.status || '').trim().toUpperCase();
|
||||
if (!ripperStatuses.has(normalizedStatus)) {
|
||||
return false;
|
||||
@@ -1113,8 +1137,12 @@ export default function RipperPage({
|
||||
seen.add(String(id));
|
||||
deduped.push(job);
|
||||
}
|
||||
const filteredJobs = deduped.filter((job) => {
|
||||
const isSeriesBatchChild = Boolean(job?.encodePlan?.seriesBatchChild);
|
||||
return !isSeriesBatchChild;
|
||||
});
|
||||
|
||||
setRipperJobs(deduped);
|
||||
setRipperJobs(filteredJobs);
|
||||
|
||||
// Prüfen ob Audiobook-Jobs auf Activation Bytes warten
|
||||
try {
|
||||
@@ -1314,13 +1342,20 @@ export default function RipperPage({
|
||||
title: job?.title || job?.detected_title || context?.detectedTitle || '',
|
||||
year: job?.year || null,
|
||||
imdbId: job?.imdb_id || null,
|
||||
poster: job?.poster_url || null
|
||||
poster: job?.poster_url || null,
|
||||
metadataProvider: context?.metadataProvider || 'omdb',
|
||||
seasonNumber: context?.seriesLookupHint?.seasonNumber || null,
|
||||
discNumber: context?.seriesLookupHint?.discNumber || null
|
||||
};
|
||||
return {
|
||||
...context,
|
||||
jobId: normalizedJobId,
|
||||
detectedTitle: context?.detectedTitle || job?.detected_title || selectedMetadata?.title || '',
|
||||
selectedMetadata,
|
||||
metadataProvider: context?.metadataProvider || selectedMetadata?.metadataProvider || 'omdb',
|
||||
metadataCandidates: Array.isArray(context?.metadataCandidates) ? context.metadataCandidates : [],
|
||||
seriesAnalysis: context?.seriesAnalysis || null,
|
||||
seriesLookupHint: context?.seriesLookupHint || null,
|
||||
omdbCandidates: Array.isArray(context?.omdbCandidates) ? context.omdbCandidates : []
|
||||
};
|
||||
};
|
||||
@@ -1342,8 +1377,15 @@ export default function RipperPage({
|
||||
title: currentContext?.detectedTitle || '',
|
||||
year: null,
|
||||
imdbId: null,
|
||||
poster: null
|
||||
poster: null,
|
||||
metadataProvider: currentContext?.metadataProvider || 'omdb',
|
||||
seasonNumber: currentContext?.seriesLookupHint?.seasonNumber || null,
|
||||
discNumber: currentContext?.seriesLookupHint?.discNumber || null
|
||||
},
|
||||
metadataProvider: currentContext?.metadataProvider || currentContext?.selectedMetadata?.metadataProvider || 'omdb',
|
||||
metadataCandidates: Array.isArray(currentContext?.metadataCandidates) ? currentContext.metadataCandidates : [],
|
||||
seriesAnalysis: currentContext?.seriesAnalysis || null,
|
||||
seriesLookupHint: currentContext?.seriesLookupHint || null,
|
||||
omdbCandidates: Array.isArray(currentContext?.omdbCandidates) ? currentContext.omdbCandidates : []
|
||||
};
|
||||
}
|
||||
@@ -1409,8 +1451,16 @@ export default function RipperPage({
|
||||
title: response?.result?.detectedTitle || '',
|
||||
year: null,
|
||||
imdbId: null,
|
||||
poster: null
|
||||
poster: null,
|
||||
metadataProvider: response?.result?.metadataProvider || 'omdb',
|
||||
seasonNumber: response?.result?.seriesLookupHint?.seasonNumber || null
|
||||
},
|
||||
metadataProvider: response?.result?.metadataProvider || 'omdb',
|
||||
metadataCandidates: Array.isArray(response?.result?.metadataCandidates)
|
||||
? response.result.metadataCandidates
|
||||
: [],
|
||||
seriesAnalysis: response?.result?.seriesAnalysis || null,
|
||||
seriesLookupHint: response?.result?.seriesLookupHint || null,
|
||||
omdbCandidates: Array.isArray(response?.result?.omdbCandidates)
|
||||
? response.result.omdbCandidates
|
||||
: []
|
||||
@@ -1443,8 +1493,16 @@ export default function RipperPage({
|
||||
title: response?.result?.detectedTitle || '',
|
||||
year: null,
|
||||
imdbId: null,
|
||||
poster: null
|
||||
poster: null,
|
||||
metadataProvider: response?.result?.metadataProvider || 'omdb',
|
||||
seasonNumber: response?.result?.seriesLookupHint?.seasonNumber || null
|
||||
},
|
||||
metadataProvider: response?.result?.metadataProvider || 'omdb',
|
||||
metadataCandidates: Array.isArray(response?.result?.metadataCandidates)
|
||||
? response.result.metadataCandidates
|
||||
: [],
|
||||
seriesAnalysis: response?.result?.seriesAnalysis || null,
|
||||
seriesLookupHint: response?.result?.seriesLookupHint || null,
|
||||
omdbCandidates: Array.isArray(response?.result?.omdbCandidates)
|
||||
? response.result.omdbCandidates
|
||||
: []
|
||||
@@ -1672,7 +1730,9 @@ export default function RipperPage({
|
||||
if (startOptions.ensureConfirmed) {
|
||||
const confirmPayload = {
|
||||
selectedEncodeTitleId: startOptions.selectedEncodeTitleId ?? null,
|
||||
selectedEncodeTitleIds: startOptions.selectedEncodeTitleIds ?? null,
|
||||
selectedTrackSelection: startOptions.selectedTrackSelection ?? null,
|
||||
episodeAssignments: startOptions.episodeAssignments ?? null,
|
||||
skipPipelineStateUpdate: true
|
||||
};
|
||||
if (startOptions.selectedPostEncodeScriptIds !== undefined) {
|
||||
@@ -1773,16 +1833,26 @@ export default function RipperPage({
|
||||
selectedTrackSelection = null,
|
||||
selectedPostEncodeScriptIds = undefined,
|
||||
selectedUserPresetId = undefined,
|
||||
selectedHandBrakePreset = undefined
|
||||
selectedHandBrakePreset = undefined,
|
||||
selectedEncodeTitleIds = undefined,
|
||||
episodeAssignments = undefined
|
||||
) => {
|
||||
const normalizedJobId = normalizeJobId(jobId);
|
||||
if (normalizedJobId) setJobBusy(normalizedJobId, true);
|
||||
try {
|
||||
const payload = {
|
||||
selectedEncodeTitleId,
|
||||
selectedTrackSelection,
|
||||
selectedPostEncodeScriptIds
|
||||
selectedTrackSelection
|
||||
};
|
||||
if (selectedEncodeTitleIds !== undefined) {
|
||||
payload.selectedEncodeTitleIds = selectedEncodeTitleIds;
|
||||
}
|
||||
if (selectedPostEncodeScriptIds !== undefined) {
|
||||
payload.selectedPostEncodeScriptIds = selectedPostEncodeScriptIds;
|
||||
}
|
||||
if (episodeAssignments !== undefined) {
|
||||
payload.episodeAssignments = episodeAssignments;
|
||||
}
|
||||
if (selectedUserPresetId !== undefined) {
|
||||
payload.selectedUserPresetId = selectedUserPresetId;
|
||||
}
|
||||
@@ -1818,13 +1888,19 @@ export default function RipperPage({
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectHandBrakeTitle = async (jobId, selectedHandBrakeTitleId = null) => {
|
||||
const handleSelectHandBrakeTitle = async (jobId, selectedHandBrakeTitleIds = null) => {
|
||||
const normalizedJobId = normalizeJobId(jobId);
|
||||
if (normalizedJobId) setJobBusy(normalizedJobId, true);
|
||||
try {
|
||||
const normalizedIds = (Array.isArray(selectedHandBrakeTitleIds) ? selectedHandBrakeTitleIds : [selectedHandBrakeTitleIds])
|
||||
.map((value) => Number(value))
|
||||
.filter((value) => Number.isFinite(value) && value > 0)
|
||||
.map((value) => Math.trunc(value));
|
||||
const dedupedIds = Array.from(new Set(normalizedIds));
|
||||
await api.selectMetadata({
|
||||
jobId,
|
||||
selectedHandBrakeTitleId: selectedHandBrakeTitleId || null
|
||||
selectedHandBrakeTitleId: dedupedIds[0] || null,
|
||||
selectedHandBrakeTitleIds: dedupedIds
|
||||
});
|
||||
await refreshPipeline();
|
||||
await loadRipperJobs();
|
||||
@@ -1914,9 +1990,12 @@ export default function RipperPage({
|
||||
}
|
||||
};
|
||||
|
||||
const handleRestartEncodeWithLastSettings = async (jobId) => {
|
||||
const handleRestartEncodeWithLastSettings = async (jobId, restartOptions = {}) => {
|
||||
const job = ripperJobs.find((item) => normalizeJobId(item?.id) === normalizeJobId(jobId)) || null;
|
||||
const title = job?.title || job?.detected_title || `Job #${jobId}`;
|
||||
const restartMode = String(restartOptions?.restartMode || 'all').trim().toLowerCase() === 'from_abort'
|
||||
? 'from_abort'
|
||||
: 'all';
|
||||
if (job?.encodeSuccess) {
|
||||
const confirmed = await confirmModal({
|
||||
header: 'Encode neu starten',
|
||||
@@ -1934,7 +2013,7 @@ export default function RipperPage({
|
||||
const normalizedJobId = normalizeJobId(jobId);
|
||||
if (normalizedJobId) setJobBusy(normalizedJobId, true);
|
||||
try {
|
||||
const response = await api.restartEncodeWithLastSettings(jobId);
|
||||
const response = await api.restartEncodeWithLastSettings(jobId, { restartMode });
|
||||
const result = getQueueActionResult(response);
|
||||
const replacementJobId = normalizeJobId(result?.jobId) || normalizedJobId;
|
||||
await refreshPipeline();
|
||||
@@ -2115,14 +2194,12 @@ export default function RipperPage({
|
||||
}
|
||||
};
|
||||
|
||||
const handleOmdbSearch = async (query) => {
|
||||
try {
|
||||
const response = await api.searchOmdb(query);
|
||||
return response.results || [];
|
||||
} catch (error) {
|
||||
showError(error);
|
||||
return [];
|
||||
}
|
||||
const handleMetadataSearch = async (query, options = {}) => {
|
||||
const provider = String(options?.metadataProvider || effectiveMetadataDialogContext?.metadataProvider || 'omdb').trim().toLowerCase() || 'omdb';
|
||||
const response = provider === 'tmdb'
|
||||
? await api.searchTmdbSeries(query)
|
||||
: await api.searchOmdb(query);
|
||||
return response.results || [];
|
||||
};
|
||||
|
||||
const doSelectMetadata = async (payload) => {
|
||||
@@ -2772,6 +2849,42 @@ export default function RipperPage({
|
||||
normalizedStatus === 'READY_TO_ENCODE'
|
||||
|| (mediaIndicator.mediaType === 'audiobook' && normalizedStatus === 'READY_TO_START')
|
||||
) && !isCurrentSession;
|
||||
const analyzeContext = getAnalyzeContext(job);
|
||||
const selectedMetadata = pipelineForJob?.context?.selectedMetadata && typeof pipelineForJob.context.selectedMetadata === 'object'
|
||||
? pipelineForJob.context.selectedMetadata
|
||||
: (analyzeContext?.selectedMetadata && typeof analyzeContext.selectedMetadata === 'object'
|
||||
? analyzeContext.selectedMetadata
|
||||
: (job?.makemkvInfo?.selectedMetadata && typeof job.makemkvInfo.selectedMetadata === 'object'
|
||||
? job.makemkvInfo.selectedMetadata
|
||||
: {}));
|
||||
const seriesSeasonNumber = mediaIndicator.isSeriesDvd
|
||||
? (selectedMetadata?.seasonNumber ?? analyzeContext?.seriesLookupHint?.seasonNumber ?? null)
|
||||
: null;
|
||||
const seriesTmdbId = mediaIndicator.isSeriesDvd
|
||||
? (selectedMetadata?.tmdbId ?? analyzeContext?.tmdbId ?? null)
|
||||
: null;
|
||||
const seriesDiscNumber = mediaIndicator.isSeriesDvd
|
||||
? (selectedMetadata?.discNumber ?? analyzeContext?.seriesLookupHint?.discNumber ?? null)
|
||||
: null;
|
||||
const isSeriesContainer = mediaIndicator.isSeriesDvd
|
||||
&& String(job?.job_kind || '').trim().toLowerCase() === 'dvd_series_container';
|
||||
const parentContainerId = normalizeJobId(job?.parent_job_id);
|
||||
const seriesTmdbText = String(seriesTmdbId ?? '').trim();
|
||||
const seriesSeasonLabel = mediaIndicator.isSeriesDvd && seriesSeasonNumber
|
||||
? ` Staffel ${seriesSeasonNumber}`
|
||||
: '';
|
||||
const seriesDiscLabel = mediaIndicator.isSeriesDvd && seriesDiscNumber
|
||||
? ` | Disk ${seriesDiscNumber}`
|
||||
: '';
|
||||
const seriesContainerLabel = isSeriesContainer
|
||||
? ' | Container'
|
||||
: (parentContainerId ? ` | Container #${parentContainerId}` : '');
|
||||
const seriesTmdbLabel = mediaIndicator.isSeriesDvd && seriesTmdbText && seriesTmdbText !== '0'
|
||||
? ` | TMDb ${seriesTmdbId}`
|
||||
: '';
|
||||
const seriesTitleSuffix = mediaIndicator.isSeriesDvd
|
||||
? `${seriesSeasonLabel}${seriesDiscLabel}${seriesContainerLabel}`
|
||||
: '';
|
||||
const mediaProfile = String(pipelineForJob?.context?.mediaProfile || '').trim().toLowerCase();
|
||||
const jobState = String(pipelineForJob?.state || normalizedStatus).trim().toUpperCase();
|
||||
const pipelineStage = String(pipelineForJob?.context?.stage || '').trim().toUpperCase();
|
||||
@@ -2784,12 +2897,45 @@ export default function RipperPage({
|
||||
const isAudiobookJob = mediaProfile === 'audiobook'
|
||||
|| mediaIndicator.mediaType === 'audiobook'
|
||||
|| String(pipelineForJob?.context?.mode || '').trim().toLowerCase() === 'audiobook';
|
||||
const rawProgress = Number(pipelineForJob?.progress ?? 0);
|
||||
const seriesBatchContext = pipelineForJob?.context?.seriesBatch && typeof pipelineForJob.context.seriesBatch === 'object'
|
||||
? pipelineForJob.context.seriesBatch
|
||||
: null;
|
||||
const seriesBatchChildren = Array.isArray(seriesBatchContext?.children) ? seriesBatchContext.children : [];
|
||||
const hasSeriesBatchProgress = seriesBatchChildren.length > 0 || Number(seriesBatchContext?.totalCount || 0) > 0;
|
||||
const seriesBatchTotalCountRaw = Number(seriesBatchContext?.totalCount || 0);
|
||||
const seriesBatchTotalCount = Number.isFinite(seriesBatchTotalCountRaw) && seriesBatchTotalCountRaw > 0
|
||||
? Math.trunc(seriesBatchTotalCountRaw)
|
||||
: seriesBatchChildren.length;
|
||||
const seriesBatchAggregateProgress = hasSeriesBatchProgress && seriesBatchTotalCount > 0
|
||||
? (
|
||||
seriesBatchChildren.reduce((sum, child) => {
|
||||
const status = String(child?.status || '').trim().toUpperCase();
|
||||
if (status === 'FINISHED') {
|
||||
return sum + 100;
|
||||
}
|
||||
const childProgress = Number(child?.progress || 0);
|
||||
return sum + (Number.isFinite(childProgress) ? Math.max(0, Math.min(100, childProgress)) : 0);
|
||||
}, 0) / seriesBatchTotalCount
|
||||
)
|
||||
: 0;
|
||||
const runningSeriesChild = hasSeriesBatchProgress
|
||||
? (
|
||||
seriesBatchChildren.find((child) => String(child?.status || '').trim().toUpperCase() === 'RUNNING')
|
||||
|| null
|
||||
)
|
||||
: null;
|
||||
const rawProgress = hasSeriesBatchProgress
|
||||
? seriesBatchAggregateProgress
|
||||
: Number(pipelineForJob?.progress ?? 0);
|
||||
const clampedProgress = Number.isFinite(rawProgress)
|
||||
? Math.max(0, Math.min(100, rawProgress))
|
||||
: 0;
|
||||
const progressLabel = `${Math.round(clampedProgress)}%`;
|
||||
const etaLabel = String(pipelineForJob?.eta || '').trim();
|
||||
const etaLabel = String(
|
||||
hasSeriesBatchProgress
|
||||
? (runningSeriesChild?.eta || pipelineForJob?.eta || '')
|
||||
: (pipelineForJob?.eta || '')
|
||||
).trim();
|
||||
|
||||
const audiobookMeta = pipelineForJob?.context?.selectedMetadata && typeof pipelineForJob.context.selectedMetadata === 'object'
|
||||
? pipelineForJob.context.selectedMetadata
|
||||
@@ -2814,8 +2960,12 @@ export default function RipperPage({
|
||||
/>
|
||||
<span>#{jobId} | {jobTitle}</span>
|
||||
</strong>
|
||||
{mediaIndicator.isSeriesDvd ? (
|
||||
<small className="ripper-job-subtitle">{seriesTitleSuffix || '-'}</small>
|
||||
) : null}
|
||||
<div className="ripper-job-badges">
|
||||
<Tag value={statusBadgeValue} severity={statusBadgeSeverity} />
|
||||
{mediaIndicator.isSeriesDvd ? <Tag value="DVD Serie" severity="secondary" /> : null}
|
||||
{isCurrentSession ? <Tag value="Aktive Session" severity="info" /> : null}
|
||||
{isResumable ? <Tag value="Fortsetzbar" severity="success" /> : null}
|
||||
{normalizedStatus === 'READY_TO_ENCODE'
|
||||
@@ -2934,21 +3084,30 @@ export default function RipperPage({
|
||||
<span>{jobTitle}</span>
|
||||
</strong>
|
||||
<small>
|
||||
#{jobId}
|
||||
{isAudiobookJob
|
||||
? (
|
||||
`${audiobookMeta?.author ? ` | ${audiobookMeta.author}` : ''}`
|
||||
+ `${audiobookMeta?.narrator ? ` | ${audiobookMeta.narrator}` : ''}`
|
||||
+ `${audiobookChapterCount > 0 ? ` | ${audiobookChapterCount} Kapitel` : ''}`
|
||||
)
|
||||
{mediaIndicator.isSeriesDvd
|
||||
? (seriesTitleSuffix || '-')
|
||||
: (
|
||||
`${job?.year ? ` | ${job.year}` : ''}`
|
||||
+ `${job?.imdb_id ? ` | ${job.imdb_id}` : ''}`
|
||||
`#${jobId}`
|
||||
+ (isAudiobookJob
|
||||
? (
|
||||
`${audiobookMeta?.author ? ` | ${audiobookMeta.author}` : ''}`
|
||||
+ `${audiobookMeta?.narrator ? ` | ${audiobookMeta.narrator}` : ''}`
|
||||
+ `${audiobookChapterCount > 0 ? ` | ${audiobookChapterCount} Kapitel` : ''}`
|
||||
)
|
||||
: (
|
||||
`${job?.year ? ` | ${job.year}` : ''}`
|
||||
+ (mediaIndicator.isSeriesDvd ? seriesSeasonLabel : '')
|
||||
+ (mediaIndicator.isSeriesDvd ? seriesDiscLabel : '')
|
||||
+ (mediaIndicator.isSeriesDvd ? seriesContainerLabel : '')
|
||||
+ (mediaIndicator.isSeriesDvd ? seriesTmdbLabel : '')
|
||||
+ (mediaIndicator.isSeriesDvd ? '' : `${job?.imdb_id ? ` | ${job.imdb_id}` : ''}`)
|
||||
))
|
||||
)}
|
||||
</small>
|
||||
</div>
|
||||
<div className="ripper-job-badges">
|
||||
<Tag value={statusBadgeValue} severity={statusBadgeSeverity} />
|
||||
{mediaIndicator.isSeriesDvd ? <Tag value="DVD Serie" severity="secondary" /> : null}
|
||||
{isCurrentSession ? <Tag value="Aktive Session" severity="info" /> : null}
|
||||
{isResumable ? <Tag value="Fortsetzbar" severity="success" /> : null}
|
||||
{normalizedStatus === 'READY_TO_ENCODE'
|
||||
@@ -2995,7 +3154,35 @@ export default function RipperPage({
|
||||
const detailKey = buildRunningQueueScriptKey(item?.jobId);
|
||||
const detailsExpanded = hasScriptSummary && expandedQueueScriptKeys.has(detailKey);
|
||||
const jobProgressEntry = pipeline?.jobProgress?.[String(item.jobId)] || null;
|
||||
const rawQueueProgress = Number(jobProgressEntry?.progress ?? 0);
|
||||
const seriesBatchContext = jobProgressEntry?.context?.seriesBatch && typeof jobProgressEntry.context.seriesBatch === 'object'
|
||||
? jobProgressEntry.context.seriesBatch
|
||||
: null;
|
||||
const seriesBatchChildren = Array.isArray(seriesBatchContext?.children) ? seriesBatchContext.children : [];
|
||||
const seriesBatchTotalCountRaw = Number(seriesBatchContext?.totalCount || 0);
|
||||
const seriesBatchTotalCount = Number.isFinite(seriesBatchTotalCountRaw) && seriesBatchTotalCountRaw > 0
|
||||
? Math.trunc(seriesBatchTotalCountRaw)
|
||||
: seriesBatchChildren.length;
|
||||
const seriesBatchOverallProgress = seriesBatchTotalCount > 0
|
||||
? (
|
||||
seriesBatchChildren.reduce((sum, child) => {
|
||||
const status = String(child?.status || '').trim().toUpperCase();
|
||||
if (status === 'FINISHED') {
|
||||
return sum + 100;
|
||||
}
|
||||
const childProgress = Number(child?.progress || 0);
|
||||
return sum + (Number.isFinite(childProgress) ? Math.max(0, Math.min(100, childProgress)) : 0);
|
||||
}, 0) / seriesBatchTotalCount
|
||||
)
|
||||
: null;
|
||||
const runningSeriesChild = seriesBatchChildren.find(
|
||||
(child) => String(child?.status || '').trim().toUpperCase() === 'RUNNING'
|
||||
) || null;
|
||||
const runningChildProgress = Number(runningSeriesChild?.progress ?? NaN);
|
||||
const rawQueueProgress = Number(
|
||||
Number.isFinite(runningChildProgress)
|
||||
? runningChildProgress
|
||||
: (jobProgressEntry?.progress ?? 0)
|
||||
);
|
||||
const clampedQueueProgress = Number.isFinite(rawQueueProgress) ? Math.max(0, Math.min(100, rawQueueProgress)) : 0;
|
||||
return (
|
||||
<div key={`running-${item.jobId}`} className="pipeline-queue-entry-wrap">
|
||||
@@ -3006,6 +3193,7 @@ export default function RipperPage({
|
||||
{item.hasScripts ? <i className="pi pi-code queue-job-tag" title="Skripte hinterlegt" /> : null}
|
||||
{item.hasChains ? <i className="pi pi-link queue-job-tag" title="Skriptketten hinterlegt" /> : null}
|
||||
</strong>
|
||||
{item.subtitle ? <small>{item.subtitle}</small> : null}
|
||||
<small>{getStatusLabel(item.status)}</small>
|
||||
{clampedQueueProgress > 0 && (
|
||||
<ProgressBar value={clampedQueueProgress} style={{ height: '5px' }} showValue={false} />
|
||||
@@ -3122,6 +3310,7 @@ export default function RipperPage({
|
||||
{item.hasScripts ? <i className="pi pi-code queue-job-tag" title="Skripte hinterlegt" /> : null}
|
||||
{item.hasChains ? <i className="pi pi-link queue-job-tag" title="Skriptketten hinterlegt" /> : null}
|
||||
</strong>
|
||||
{item.subtitle ? <small>{item.subtitle}</small> : null}
|
||||
<small>{item.actionLabel || item.action || '-'} | {getStatusLabel(item.status)}</small>
|
||||
</>
|
||||
)}
|
||||
@@ -3497,7 +3686,7 @@ export default function RipperPage({
|
||||
setMetadataDialogReassignMode(false);
|
||||
}}
|
||||
onSubmit={handleMetadataSubmit}
|
||||
onSearch={handleOmdbSearch}
|
||||
onSearch={handleMetadataSearch}
|
||||
busy={busy}
|
||||
/>
|
||||
|
||||
@@ -3521,31 +3710,60 @@ export default function RipperPage({
|
||||
style={{ width: '30rem', maxWidth: '96vw' }}
|
||||
modal
|
||||
>
|
||||
{(() => {
|
||||
const pendingPayload = duplicateJobDialog.pendingPayload || {};
|
||||
const metadataProvider = String(pendingPayload?.metadataProvider || '').trim().toLowerCase();
|
||||
const seasonNumber = Number(pendingPayload?.seasonNumber || 0) || 0;
|
||||
const tmdbId = Number(pendingPayload?.tmdbId || 0) || 0;
|
||||
const isSeriesContainerAssign = metadataProvider === 'tmdb' && seasonNumber > 0 && tmdbId > 0;
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
<strong>{duplicateJobDialog.existingJob?.title || duplicateJobDialog.pendingPayload?.title}</strong> ist bereits als Job #{duplicateJobDialog.existingJob?.id} in der Historie vorhanden.
|
||||
</p>
|
||||
<p>Neuen Job anlegen oder mit dem vorhandenen Eintrag weiterarbeiten?</p>
|
||||
<p>
|
||||
{isSeriesContainerAssign
|
||||
? 'Dieser Job kann dem vorhandenen Serien-Container automatisch zugewiesen werden.'
|
||||
: 'Neuen Job anlegen oder mit dem vorhandenen Eintrag weiterarbeiten?'}
|
||||
</p>
|
||||
<div className="dialog-actions">
|
||||
<Button
|
||||
label="Vorhandenen Job öffnen"
|
||||
icon="pi pi-history"
|
||||
onClick={() => {
|
||||
label={isSeriesContainerAssign ? 'Vorhandenem Container zuweisen' : 'Vorhandenen Job öffnen'}
|
||||
icon={isSeriesContainerAssign ? 'pi pi-sitemap' : 'pi pi-history'}
|
||||
onClick={async () => {
|
||||
const payload = duplicateJobDialog.pendingPayload;
|
||||
const jobId = duplicateJobDialog.existingJob?.id;
|
||||
setDuplicateJobDialog({ visible: false, existingJob: null, pendingPayload: null });
|
||||
if (isSeriesContainerAssign) {
|
||||
await doSelectMetadata(payload);
|
||||
return;
|
||||
}
|
||||
navigate(`/history?open=${jobId}`);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
label="Neuen Job anlegen"
|
||||
severity="secondary"
|
||||
outlined
|
||||
onClick={async () => {
|
||||
const payload = duplicateJobDialog.pendingPayload;
|
||||
setDuplicateJobDialog({ visible: false, existingJob: null, pendingPayload: null });
|
||||
await doSelectMetadata(payload);
|
||||
}}
|
||||
/>
|
||||
{isSeriesContainerAssign ? (
|
||||
<Button
|
||||
label="Abbrechen"
|
||||
severity="secondary"
|
||||
outlined
|
||||
onClick={() => setDuplicateJobDialog({ visible: false, existingJob: null, pendingPayload: null })}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
label="Neuen Job anlegen"
|
||||
severity="secondary"
|
||||
outlined
|
||||
onClick={async () => {
|
||||
const payload = duplicateJobDialog.pendingPayload;
|
||||
setDuplicateJobDialog({ visible: false, existingJob: null, pendingPayload: null });
|
||||
await doSelectMetadata(payload);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</Dialog>
|
||||
|
||||
<Dialog
|
||||
|
||||
Reference in New Issue
Block a user