0.13.1-6 Series Fix

This commit is contained in:
2026-04-14 07:08:59 +00:00
parent cac6442b5b
commit d462de8263
15 changed files with 408 additions and 66 deletions
+119 -40
View File
@@ -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 ? (