0.13.1-6 Series Fix
This commit is contained in:
@@ -78,7 +78,10 @@ function stripEpisodePartSuffix(value) {
|
||||
if (!source) {
|
||||
return '';
|
||||
}
|
||||
const suffixPattern = /(?:\s*[-–—:.,]?\s*)(?:\(?\s*(?:teil|part|pt)\s*(?:\d{1,2}|[ivxlcdm]{1,6})(?:\s*(?:\/|von|of)\s*(?:\d{1,2}|[ivxlcdm]{1,6}))?\s*\)?)\s*$/i;
|
||||
// Output cleanup for multi-episode labels:
|
||||
// remove "Teil/Part" markers and pure numeric/roman suffixes in
|
||||
// parentheses like "(1)" / "(2)".
|
||||
const suffixPattern = /(?:\s*[-–—:.,]?\s*)(?:(?:\(?\s*(?:teil|part|pt)\s*(?:\d{1,2}|[ivxlcdm]{1,6})(?:\s*(?:\/|von|of)\s*(?:\d{1,2}|[ivxlcdm]{1,6}))?\s*\)?)|\(\s*(?:\d{1,2}|[ivxlcdm]{1,6})\s*\))\s*$/i;
|
||||
let normalized = source;
|
||||
let changed = false;
|
||||
for (let i = 0; i < 2; i += 1) {
|
||||
|
||||
@@ -14,6 +14,19 @@ export default function MetadataSelectionDialog({
|
||||
onSearch,
|
||||
busy
|
||||
}) {
|
||||
const normalizeWorkflowKind = (value) => {
|
||||
const raw = String(value || '').trim().toLowerCase();
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
if (['film', 'movie', 'feature'].includes(raw)) {
|
||||
return 'film';
|
||||
}
|
||||
if (['series', 'tv', 'season', 'episode'].includes(raw)) {
|
||||
return 'series';
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const parseDiscNumber = (rawValue) => {
|
||||
const text = String(rawValue ?? '').trim();
|
||||
if (!/^\d+$/.test(text)) {
|
||||
@@ -37,7 +50,22 @@ export default function MetadataSelectionDialog({
|
||||
const [submitError, setSubmitError] = useState('');
|
||||
const [seasonFilter, setSeasonFilter] = useState('');
|
||||
const [discValidationError, setDiscValidationError] = useState('');
|
||||
const contextMetadataProvider = String(context?.metadataProvider || 'omdb').trim().toLowerCase() || 'omdb';
|
||||
const contextWorkflowKind = normalizeWorkflowKind(
|
||||
context?.selectedMetadata?.workflowKind
|
||||
|| context?.workflowKind
|
||||
|| null
|
||||
);
|
||||
const contextSelectedMetadataProvider = String(
|
||||
context?.selectedMetadata?.metadataProvider
|
||||
|| ''
|
||||
).trim().toLowerCase();
|
||||
const contextMetadataProvider = (
|
||||
contextWorkflowKind === 'series'
|
||||
? 'tmdb'
|
||||
: (contextWorkflowKind === 'film'
|
||||
? 'omdb'
|
||||
: (contextSelectedMetadataProvider || String(context?.metadataProvider || 'omdb').trim().toLowerCase()))
|
||||
) || 'omdb';
|
||||
const [selectedMetadataProvider, setSelectedMetadataProvider] = useState(contextMetadataProvider);
|
||||
const mediaProfile = String(
|
||||
context?.mediaProfile
|
||||
@@ -196,6 +224,7 @@ export default function MetadataSelectionDialog({
|
||||
poster: selected.poster && selected.poster !== 'N/A' ? selected.poster : null,
|
||||
fromOmdb: metadataProvider === 'omdb',
|
||||
metadataProvider,
|
||||
workflowKind: metadataProvider === 'tmdb' ? 'series' : 'film',
|
||||
providerId: selected.providerId || null,
|
||||
tmdbId: selected.tmdbId || null,
|
||||
metadataKind: selected.metadataKind || null,
|
||||
@@ -213,6 +242,7 @@ export default function MetadataSelectionDialog({
|
||||
poster: null,
|
||||
fromOmdb: false,
|
||||
metadataProvider,
|
||||
workflowKind: metadataProvider === 'tmdb' ? 'series' : 'film',
|
||||
seasonNumber: null,
|
||||
...(effectiveDiscNumber ? { discNumber: effectiveDiscNumber } : {})
|
||||
};
|
||||
|
||||
@@ -810,7 +810,10 @@ function stripEpisodePartSuffix(value) {
|
||||
if (!source) {
|
||||
return '';
|
||||
}
|
||||
const suffixPattern = /(?:\s*[-–—:.,]?\s*)(?:\(?\s*(?:teil|part|pt)\s*(?:\d{1,2}|[ivxlcdm]{1,6})(?:\s*(?:\/|von|of)\s*(?:\d{1,2}|[ivxlcdm]{1,6}))?\s*\)?)\s*$/i;
|
||||
// Output cleanup for multi-episode labels:
|
||||
// remove "Teil/Part" markers and pure numeric/roman suffixes in
|
||||
// parentheses like "(1)" / "(2)".
|
||||
const suffixPattern = /(?:\s*[-–—:.,]?\s*)(?:(?:\(?\s*(?:teil|part|pt)\s*(?:\d{1,2}|[ivxlcdm]{1,6})(?:\s*(?:\/|von|of)\s*(?:\d{1,2}|[ivxlcdm]{1,6}))?\s*\)?)|\(\s*(?:\d{1,2}|[ivxlcdm]{1,6})\s*\))\s*$/i;
|
||||
let normalized = source;
|
||||
let changed = false;
|
||||
for (let i = 0; i < 2; i += 1) {
|
||||
@@ -1943,15 +1946,49 @@ export default function PipelineStatusCard({
|
||||
? pipeline.context.handBrakeDecisionSummary
|
||||
: null;
|
||||
const ambiguousLongestTitleId = normalizeTitleId(handBrakeDecisionSummary?.longestCandidateTitleId);
|
||||
const handBrakeDecisionCandidateTitleIds = useMemo(() => normalizeTitleIdList(
|
||||
(Array.isArray(pipeline?.context?.handBrakeTitleCandidates)
|
||||
? pipeline.context.handBrakeTitleCandidates
|
||||
: []
|
||||
).map((item) => item?.handBrakeTitleId)
|
||||
), [pipeline?.context?.handBrakeTitleCandidates]);
|
||||
const handBrakeDecisionSelectableTitleIds = useMemo(() => {
|
||||
const explicit = normalizeTitleIdList(pipeline?.context?.handBrakeDecisionSelectableTitleIds);
|
||||
if (explicit.length > 0) {
|
||||
return explicit;
|
||||
}
|
||||
if (isSeriesPlayAllDecisionMode && ambiguousLongestTitleId) {
|
||||
return [ambiguousLongestTitleId];
|
||||
}
|
||||
return handBrakeDecisionCandidateTitleIds;
|
||||
}, [
|
||||
isSeriesPlayAllDecisionMode,
|
||||
ambiguousLongestTitleId,
|
||||
handBrakeDecisionCandidateTitleIds,
|
||||
pipeline?.context?.handBrakeDecisionSelectableTitleIds
|
||||
]);
|
||||
const handBrakeDecisionSelectableTitleIdSet = useMemo(
|
||||
() => new Set(handBrakeDecisionSelectableTitleIds.map((id) => Number(id))),
|
||||
[handBrakeDecisionSelectableTitleIds]
|
||||
);
|
||||
|
||||
const waitingHandBrakeTitleRows = useMemo(() => {
|
||||
const raw = Array.isArray(pipeline?.context?.handBrakeTitleCandidates)
|
||||
const rawCandidates = Array.isArray(pipeline?.context?.handBrakeTitleCandidates)
|
||||
? pipeline.context.handBrakeTitleCandidates
|
||||
: [];
|
||||
return raw
|
||||
const rawAllTitles = isSeriesPlayAllDecisionMode && Array.isArray(pipeline?.context?.handBrakeAllTitles)
|
||||
? pipeline.context.handBrakeAllTitles
|
||||
: [];
|
||||
const sourceRows = rawAllTitles.length > 0 ? rawAllTitles : rawCandidates;
|
||||
const candidateIdSet = new Set(
|
||||
normalizeTitleIdList(rawCandidates.map((item) => item?.handBrakeTitleId)).map((id) => Number(id))
|
||||
);
|
||||
return sourceRows
|
||||
.map((item) => {
|
||||
const titleId = Number(item?.handBrakeTitleId);
|
||||
if (!Number.isFinite(titleId) || titleId <= 0) return null;
|
||||
const titleId = normalizeTitleId(item?.handBrakeTitleId);
|
||||
if (!titleId) {
|
||||
return null;
|
||||
}
|
||||
const durationSeconds = Number(item?.durationSeconds || 0);
|
||||
const minutes = Math.floor(durationSeconds / 60);
|
||||
const seconds = durationSeconds % 60;
|
||||
@@ -1963,12 +2000,23 @@ export default function PipelineStatusCard({
|
||||
durationMinutes: Number(item?.durationMinutes || 0),
|
||||
audioTrackCount: Number(item?.audioTrackCount || 0),
|
||||
subtitleTrackCount: Number(item?.subtitleTrackCount || 0),
|
||||
sizeBytes: Number(item?.sizeBytes || 0)
|
||||
sizeBytes: Number(item?.sizeBytes || 0),
|
||||
decisionCandidate: candidateIdSet.has(Number(titleId))
|
||||
};
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => a.handBrakeTitleId - b.handBrakeTitleId);
|
||||
}, [pipeline?.context?.handBrakeTitleCandidates]);
|
||||
}, [
|
||||
isSeriesPlayAllDecisionMode,
|
||||
pipeline?.context?.handBrakeAllTitles,
|
||||
pipeline?.context?.handBrakeTitleCandidates
|
||||
]);
|
||||
const visibleWaitingHandBrakeTitleRows = useMemo(() => {
|
||||
if (!isSeriesPlayAllDecisionMode) {
|
||||
return waitingHandBrakeTitleRows;
|
||||
}
|
||||
return waitingHandBrakeTitleRows.filter((row) => row.decisionCandidate);
|
||||
}, [isSeriesPlayAllDecisionMode, waitingHandBrakeTitleRows]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!handBrakeTitleDecisionRequired) {
|
||||
@@ -1991,11 +2039,24 @@ export default function PipelineStatusCard({
|
||||
setSelectedHandBrakeTitleIdsState([Math.trunc(singleContextId)]);
|
||||
return;
|
||||
}
|
||||
const best = waitingHandBrakeTitleRows[0]?.handBrakeTitleId || null;
|
||||
setSelectedHandBrakeTitleIdsState(best ? [best] : []);
|
||||
if (isSeriesPlayAllDecisionMode) {
|
||||
const lockedDefaultIds = waitingHandBrakeTitleRows
|
||||
.filter((row) => row.decisionCandidate && !handBrakeDecisionSelectableTitleIdSet.has(Number(row.handBrakeTitleId)))
|
||||
.map((row) => row.handBrakeTitleId);
|
||||
if (lockedDefaultIds.length > 0) {
|
||||
setSelectedHandBrakeTitleIdsState(normalizeTitleIdList(lockedDefaultIds));
|
||||
return;
|
||||
}
|
||||
}
|
||||
const firstSelectable = waitingHandBrakeTitleRows.find((row) => (
|
||||
!isSeriesPlayAllDecisionMode || handBrakeDecisionSelectableTitleIdSet.has(Number(row.handBrakeTitleId))
|
||||
))?.handBrakeTitleId || null;
|
||||
setSelectedHandBrakeTitleIdsState(firstSelectable ? [firstSelectable] : []);
|
||||
}, [
|
||||
handBrakeTitleDecisionRequired,
|
||||
isSeriesPlayAllDecisionMode,
|
||||
retryJobId,
|
||||
handBrakeDecisionSelectableTitleIdSet,
|
||||
waitingHandBrakeTitleRows,
|
||||
pipeline?.context?.selectedHandBrakeTitleId,
|
||||
pipeline?.context?.selectedHandBrakeTitleIds
|
||||
@@ -3087,40 +3148,58 @@ export default function PipelineStatusCard({
|
||||
<h3>{isSeriesPlayAllDecisionMode ? 'Serien-Grenzfall: PlayAll oder Doppelfolge?' : 'DVD Titel-Auswahl erforderlich'}</h3>
|
||||
<small>
|
||||
{isSeriesPlayAllDecisionMode
|
||||
? 'Grenzfall erkannt: Der längste Titel passt zeitlich zur Summe der kürzeren Episoden. Bitte entscheiden, ob er als PlayAll ausgeschlossen oder als Doppelfolge mit encodiert werden soll.'
|
||||
? 'Min. ein Titel passt zeitlich zur Summe der kürzeren Episoden. Bitte durch Auswahl entscheiden, ob PlayAll Multi-Episode (nicht anhaken) oder Doppelfolge (anhaken).'
|
||||
: 'Mehrere DVD-Titel gefunden, die die Mindestlänge erfüllen. Bitte einen oder mehrere Episoden-Titel auswählen.'}
|
||||
</small>
|
||||
{waitingHandBrakeTitleRows.length > 0 ? (
|
||||
{visibleWaitingHandBrakeTitleRows.length > 0 ? (
|
||||
<div className="playlist-decision-list">
|
||||
{waitingHandBrakeTitleRows.map((row) => (
|
||||
<div key={row.handBrakeTitleId} className="playlist-decision-item">
|
||||
<label className="readonly-check-row">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedHandBrakeTitleIdsState.includes(row.handBrakeTitleId)}
|
||||
disabled={queueLocked}
|
||||
onChange={() => {
|
||||
setSelectedHandBrakeTitleIdsState((prev) => {
|
||||
const list = Array.isArray(prev) ? prev : [];
|
||||
if (list.includes(row.handBrakeTitleId)) {
|
||||
return list.filter((value) => value !== row.handBrakeTitleId);
|
||||
{visibleWaitingHandBrakeTitleRows.map((row) => {
|
||||
const isDecisionSelectable = !isSeriesPlayAllDecisionMode
|
||||
|| handBrakeDecisionSelectableTitleIdSet.has(Number(row.handBrakeTitleId));
|
||||
const isAmbiguousDecisionTitle = isSeriesPlayAllDecisionMode
|
||||
&& isDecisionSelectable
|
||||
&& (
|
||||
Number(row.handBrakeTitleId) === Number(ambiguousLongestTitleId)
|
||||
|| handBrakeDecisionSelectableTitleIdSet.has(Number(row.handBrakeTitleId))
|
||||
);
|
||||
const isRecognizedLocked = isSeriesPlayAllDecisionMode
|
||||
&& row.decisionCandidate
|
||||
&& !isDecisionSelectable;
|
||||
const isOutOfDecisionScope = isSeriesPlayAllDecisionMode
|
||||
&& !row.decisionCandidate;
|
||||
return (
|
||||
<div key={row.handBrakeTitleId} className="playlist-decision-item">
|
||||
<label className="readonly-check-row">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedHandBrakeTitleIdsState.includes(row.handBrakeTitleId)}
|
||||
disabled={queueLocked || !isDecisionSelectable}
|
||||
onChange={() => {
|
||||
if (!isDecisionSelectable) {
|
||||
return;
|
||||
}
|
||||
return [...list, row.handBrakeTitleId];
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<span>
|
||||
{`Titel -t ${row.handBrakeTitleId}`}
|
||||
{isSeriesPlayAllDecisionMode && ambiguousLongestTitleId && row.handBrakeTitleId === ambiguousLongestTitleId
|
||||
? ' | Längster Titel (PlayAll-/Doppelfolge-Kandidat)'
|
||||
: ''}
|
||||
{row.durationLabel ? ` | ${row.durationLabel}` : ''}
|
||||
{row.audioTrackCount > 0 ? ` | Audio: ${row.audioTrackCount}` : ''}
|
||||
{row.subtitleTrackCount > 0 ? ` | Untertitel: ${row.subtitleTrackCount}` : ''}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
setSelectedHandBrakeTitleIdsState((prev) => {
|
||||
const list = Array.isArray(prev) ? prev : [];
|
||||
if (list.includes(row.handBrakeTitleId)) {
|
||||
return list.filter((value) => value !== row.handBrakeTitleId);
|
||||
}
|
||||
return [...list, row.handBrakeTitleId];
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<span>
|
||||
{isAmbiguousDecisionTitle ? <strong>{`Titel -t ${row.handBrakeTitleId}`}</strong> : `Titel -t ${row.handBrakeTitleId}`}
|
||||
{isAmbiguousDecisionTitle ? ' | Grenzfalltitel (PlayAll/Doppelfolge)' : ''}
|
||||
{isRecognizedLocked ? ' | automatisch erkannt (fix)' : ''}
|
||||
{isOutOfDecisionScope ? ' | nicht entscheidungsrelevant' : ''}
|
||||
{row.durationLabel ? ` | ${row.durationLabel}` : ''}
|
||||
{row.audioTrackCount > 0 ? ` | Audio: ${row.audioTrackCount}` : ''}
|
||||
{row.subtitleTrackCount > 0 ? ` | Untertitel: ${row.subtitleTrackCount}` : ''}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<small>Keine Kandidaten verfügbar. Bitte Analyse erneut ausführen.</small>
|
||||
@@ -3156,7 +3235,7 @@ export default function PipelineStatusCard({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{(state === 'READY_TO_ENCODE' || state === 'MEDIAINFO_CHECK' || mediaInfoReview) ? (
|
||||
{(state === 'READY_TO_ENCODE' || state === 'MEDIAINFO_CHECK' || mediaInfoReview) && !(handBrakeTitleDecisionRequired && isSeriesPlayAllDecisionMode) ? (
|
||||
<div className="mediainfo-review-block">
|
||||
{!isDvdTitleSelectionRequired ? <h3>Titel-/Spurprüfung</h3> : null}
|
||||
{state === 'READY_TO_ENCODE' && !queueLocked && !isDvdTitleSelectionRequired ? (
|
||||
|
||||
@@ -1112,8 +1112,20 @@ export default function HistoryPage({ refreshToken = 0 }) {
|
||||
: (makemkvInfo?.selectedMetadata && typeof makemkvInfo.selectedMetadata === 'object'
|
||||
? makemkvInfo.selectedMetadata
|
||||
: {});
|
||||
const workflowKindRaw = String(
|
||||
selectedMetadata?.workflowKind
|
||||
|| analyzeContext?.workflowKind
|
||||
|| ''
|
||||
).trim().toLowerCase();
|
||||
const workflowKind = ['film', 'movie', 'feature'].includes(workflowKindRaw)
|
||||
? 'film'
|
||||
: (['series', 'tv', 'season', 'episode'].includes(workflowKindRaw) ? 'series' : null);
|
||||
const isSeriesDvd = resolveMediaType(row) === 'dvd' && isSeriesDvdJob(row);
|
||||
const metadataProvider = isSeriesDvd ? 'tmdb' : 'omdb';
|
||||
const metadataProvider = workflowKind === 'series'
|
||||
? 'tmdb'
|
||||
: (workflowKind === 'film'
|
||||
? 'omdb'
|
||||
: (String(selectedMetadata?.metadataProvider || analyzeContext?.metadataProvider || (isSeriesDvd ? 'tmdb' : 'omdb')).trim().toLowerCase() || 'omdb'));
|
||||
const seasonNumber = selectedMetadata?.seasonNumber ?? analyzeContext?.seriesLookupHint?.seasonNumber ?? null;
|
||||
const discNumber = selectedMetadata?.discNumber ?? analyzeContext?.seriesLookupHint?.discNumber ?? null;
|
||||
const context = {
|
||||
|
||||
@@ -1352,7 +1352,7 @@ export default function RipperPage({
|
||||
jobId: normalizedJobId,
|
||||
detectedTitle: context?.detectedTitle || job?.detected_title || selectedMetadata?.title || '',
|
||||
selectedMetadata,
|
||||
metadataProvider: context?.metadataProvider || selectedMetadata?.metadataProvider || 'omdb',
|
||||
metadataProvider: selectedMetadata?.metadataProvider || context?.metadataProvider || 'omdb',
|
||||
metadataCandidates: Array.isArray(context?.metadataCandidates) ? context.metadataCandidates : [],
|
||||
seriesAnalysis: context?.seriesAnalysis || null,
|
||||
seriesLookupHint: context?.seriesLookupHint || null,
|
||||
@@ -1382,7 +1382,7 @@ export default function RipperPage({
|
||||
seasonNumber: null,
|
||||
discNumber: null
|
||||
},
|
||||
metadataProvider: currentContext?.metadataProvider || currentContext?.selectedMetadata?.metadataProvider || 'omdb',
|
||||
metadataProvider: currentContext?.selectedMetadata?.metadataProvider || currentContext?.metadataProvider || 'omdb',
|
||||
metadataCandidates: Array.isArray(currentContext?.metadataCandidates) ? currentContext.metadataCandidates : [],
|
||||
seriesAnalysis: currentContext?.seriesAnalysis || null,
|
||||
seriesLookupHint: currentContext?.seriesLookupHint || null,
|
||||
|
||||
@@ -217,6 +217,17 @@ export function isSeriesDvdJob(job) {
|
||||
: (job?.makemkvInfo?.selectedMetadata && typeof job.makemkvInfo.selectedMetadata === 'object'
|
||||
? job.makemkvInfo.selectedMetadata
|
||||
: {});
|
||||
const workflowKind = String(
|
||||
selectedMetadata?.workflowKind
|
||||
|| analyzeContext?.workflowKind
|
||||
|| ''
|
||||
).trim().toLowerCase();
|
||||
if (['film', 'movie', 'feature'].includes(workflowKind)) {
|
||||
return false;
|
||||
}
|
||||
if (['series', 'tv', 'season', 'episode'].includes(workflowKind)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const metadataProvider = String(
|
||||
analyzeContext?.metadataProvider
|
||||
|
||||
Reference in New Issue
Block a user