0.13.1-2 Series RC

This commit is contained in:
2026-04-13 07:57:48 +00:00
parent 850f05453c
commit b2d8dffbf4
7 changed files with 146 additions and 64 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "ripster-frontend",
"version": "0.13.1-1",
"version": "0.13.1-2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ripster-frontend",
"version": "0.13.1-1",
"version": "0.13.1-2",
"dependencies": {
"primeicons": "^7.0.0",
"primereact": "^10.9.2",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ripster-frontend",
"version": "0.13.1-1",
"version": "0.13.1-2",
"private": true,
"type": "module",
"scripts": {
+137 -55
View File
@@ -515,6 +515,24 @@ function buildEpisodeReferenceKey(entry = {}) {
return normalized.episodeNumber ? `ep:${normalized.episodeNumber}` : null;
}
function collectEpisodeReferenceKeys(entry = {}) {
const normalized = normalizeEpisodeReference(entry);
if (!normalized) {
return [];
}
const keys = [];
if (normalized.episodeId) {
keys.push(`id:${normalized.episodeId}`);
}
if (normalized.seasonNumber && normalized.episodeNumber) {
keys.push(`se:${normalized.seasonNumber}:${normalized.episodeNumber}`);
}
if (normalized.episodeNumber) {
keys.push(`ep:${normalized.episodeNumber}`);
}
return Array.from(new Set(keys));
}
function isSeriesBatchEpisodePlan(plan = null) {
const source = plan && typeof plan === 'object' ? plan : {};
if (Boolean(source?.seriesBatchChild) || Boolean(source?.seriesBatchVirtualEpisode)) {
@@ -1218,8 +1236,11 @@ export default function PipelineStatusCard({
}
const usedKeySet = new Set(containerUsedEpisodeKeys);
return normalizedRows.filter((episode) => {
const key = buildEpisodeReferenceKey(episode);
return !key || !usedKeySet.has(key);
const keys = collectEpisodeReferenceKeys(episode);
if (keys.length === 0) {
return true;
}
return keys.every((key) => !usedKeySet.has(key));
});
}, [selectedMetadata?.episodes, containerSeasonEpisodes, containerUsedEpisodeKeys]);
@@ -1411,40 +1432,38 @@ export default function PipelineStatusCard({
});
}, [selectedEncodeTitleId, selectedEncodeTitleIds, mediaInfoReview?.generatedAt]);
const applyEpisodeFillFrom = (startEpisodeRef = null) => {
const resolveEpisodeFillContext = () => {
const episodeRows = Array.isArray(selectedMetadataEpisodes) ? selectedMetadataEpisodes : [];
if (episodeRows.length === 0) {
return;
}
const reviewTitles = Array.isArray(mediaInfoReview?.titles) ? mediaInfoReview.titles : [];
const selectedTitleIds = normalizeTitleIdList([
...normalizeTitleIdList(selectedEncodeTitleIds),
...(Array.isArray(mediaInfoReview?.selectedTitleIds) ? mediaInfoReview.selectedTitleIds : []),
...reviewTitles.filter((title) => Boolean(title?.selectedForEncode)).map((title) => title?.id)
]);
if (selectedTitleIds.length === 0) {
return;
}
const orderedTitles = (Array.isArray(mediaInfoReview?.titles) ? mediaInfoReview.titles : [])
const orderedTitles = reviewTitles
.map((title) => ({
id: normalizeTitleId(title?.id),
title
}))
.filter((row) => row.id !== null);
const orderedSelectedTitleRows = orderedTitles.filter((row) => selectedTitleIds.includes(row.id));
const orderedSelectedTitleRows = selectedTitleIds.length > 0
? orderedTitles.filter((row) => selectedTitleIds.includes(row.id))
: [];
const fallbackSelectedTitleRows = orderedSelectedTitleRows.length > 0
? orderedSelectedTitleRows
: orderedTitles.filter((row) => selectedTitleIds.includes(row.id));
if (fallbackSelectedTitleRows.length === 0) {
return;
}
const rawFillRef = String(startEpisodeRef || '').trim();
const normalizedRefMatch = rawFillRef.match(/^fill:([^:]+):title:\d+$/i);
const normalizedRef = normalizedRefMatch && normalizedRefMatch[1]
? String(normalizedRefMatch[1]).trim()
: rawFillRef;
return {
episodeRows,
reviewTitles,
selectedTitleIds,
fallbackSelectedTitleRows
};
};
const allReviewTitles = fallbackSelectedTitleRows.map((item) => item?.title).filter(Boolean);
const buildGroupedEpisodeUnits = (episodeRows = [], selectedTitleRows = [], reviewTitles = []) => {
const rows = Array.isArray(episodeRows) ? episodeRows : [];
const selectedRows = Array.isArray(selectedTitleRows) ? selectedTitleRows : [];
const allReviewTitles = Array.isArray(reviewTitles) ? reviewTitles : [];
const normalizeEpisodeStart = (episode) => normalizePositiveInt(
episode?.episodeNumberStart
?? episode?.episodeNoStart
@@ -1472,10 +1491,10 @@ export default function PipelineStatusCard({
return start;
};
const titleAnchorsByEpisodeIndex = new Map();
if (episodeRows.length > 0) {
if (rows.length > 0) {
let episodeAnchorCursor = 0;
for (const selectedTitleRow of fallbackSelectedTitleRows) {
if (episodeAnchorCursor >= episodeRows.length) {
for (const selectedTitleRow of selectedRows) {
if (episodeAnchorCursor >= rows.length) {
break;
}
const explicitSpan = normalizePositiveInt(
@@ -1485,7 +1504,7 @@ export default function PipelineStatusCard({
);
const estimatedSpan = estimateSeriesEpisodeSpanForTitle(selectedTitleRow?.title, allReviewTitles);
const span = Math.max(1, explicitSpan || 1, estimatedSpan || 1);
const safeSpan = Math.max(1, Math.min(span, episodeRows.length - episodeAnchorCursor));
const safeSpan = Math.max(1, Math.min(span, rows.length - episodeAnchorCursor));
titleAnchorsByEpisodeIndex.set(episodeAnchorCursor, { span: safeSpan });
episodeAnchorCursor += safeSpan;
}
@@ -1493,8 +1512,8 @@ export default function PipelineStatusCard({
const groupedEpisodeUnits = [];
let sourceEpisodeIndex = 0;
while (sourceEpisodeIndex < episodeRows.length) {
const startRow = episodeRows[sourceEpisodeIndex];
while (sourceEpisodeIndex < rows.length) {
const startRow = rows[sourceEpisodeIndex];
const rangeStart = normalizeEpisodeStart(startRow)
?? normalizePositiveInt(startRow?.episodeNumber)
?? null;
@@ -1522,8 +1541,8 @@ export default function PipelineStatusCard({
const anchor = titleAnchorsByEpisodeIndex.get(sourceEpisodeIndex) || null;
const anchorSpan = normalizePositiveInt(anchor?.span) || 1;
if (anchorSpan > 1) {
const endIndex = Math.min(episodeRows.length - 1, sourceEpisodeIndex + anchorSpan - 1);
const endRow = episodeRows[endIndex] || startRow;
const endIndex = Math.min(rows.length - 1, sourceEpisodeIndex + anchorSpan - 1);
const endRow = rows[endIndex] || startRow;
const endRowStart = normalizeEpisodeStart(endRow)
?? normalizePositiveInt(endRow?.episodeNumber)
?? null;
@@ -1555,6 +1574,29 @@ export default function PipelineStatusCard({
sourceEpisodeIndex += 1;
}
return groupedEpisodeUnits;
};
const applyEpisodeFillFrom = (startEpisodeRef = null) => {
const { episodeRows, reviewTitles, selectedTitleIds, fallbackSelectedTitleRows } = resolveEpisodeFillContext();
if (episodeRows.length === 0 || selectedTitleIds.length === 0) {
return;
}
if (fallbackSelectedTitleRows.length === 0) {
return;
}
const rawFillRef = String(startEpisodeRef || '').trim();
const normalizedRefMatch = rawFillRef.match(/^fill:([^:]+):title:\d+$/i);
const normalizedRef = normalizedRefMatch && normalizedRefMatch[1]
? String(normalizedRefMatch[1]).trim()
: rawFillRef;
const groupedEpisodeUnits = buildGroupedEpisodeUnits(
episodeRows,
fallbackSelectedTitleRows,
reviewTitles
);
let startUnitIndex = 0;
if (normalizedRef) {
const foundUnitIndex = groupedEpisodeUnits.findIndex((unit) =>
@@ -1596,7 +1638,7 @@ export default function PipelineStatusCard({
&& rangeEnd >= rangeStart
)
? Math.max(1, Math.trunc(rangeEnd - rangeStart + 1))
: Math.max(1, consumedRows);
: Math.max(1, normalizePositiveInt(unit?.consumedRows) || 1);
const rangeToken = buildEpisodeRangeToken(
rangeStart,
rangeEnd
@@ -1647,6 +1689,31 @@ export default function PipelineStatusCard({
}));
};
const episodeFillOptions = useMemo(() => {
const { episodeRows, reviewTitles, fallbackSelectedTitleRows } = resolveEpisodeFillContext();
if (!Array.isArray(episodeRows) || episodeRows.length === 0) {
return [];
}
const groupedUnits = buildGroupedEpisodeUnits(episodeRows, fallbackSelectedTitleRows, reviewTitles);
return groupedUnits
.map((unit) => {
const startRow = unit?.startRow || null;
const value = startRow?.episodeIdStart
|| startRow?.episodeId
|| startRow?.episodeNumberStart
|| startRow?.episodeNumber
|| null;
if (!value) {
return null;
}
return {
value: String(value),
startRef: String(value)
};
})
.filter(Boolean);
}, [selectedMetadataEpisodes, selectedEncodeTitleIds, mediaInfoReview?.titles, mediaInfoReview?.selectedTitleIds]);
useEffect(() => {
if (!Array.isArray(selectedMetadataEpisodes) || selectedMetadataEpisodes.length === 0) {
return;
@@ -1661,17 +1728,32 @@ export default function PipelineStatusCard({
if (hasAnyEpisodeAssignment) {
return;
}
const defaultStartRef = selectedMetadataEpisodes[0]?.episodeIdStart
|| selectedMetadataEpisodes[0]?.episodeId
|| selectedMetadataEpisodes[0]?.episodeNumberStart
|| selectedMetadataEpisodes[0]?.episodeNumber
|| null;
if (!Array.isArray(episodeFillOptions) || episodeFillOptions.length === 0) {
return;
}
const normalizedSelection = String(selectedEpisodeFillStart || '').trim();
const hasValidSelection = Boolean(
normalizedSelection
&& episodeFillOptions.some((option) => String(option?.value || '').trim() === normalizedSelection)
);
const defaultStartRef = hasValidSelection
? normalizedSelection
: (episodeFillOptions[0]?.value || episodeFillOptions[0]?.startRef || null);
if (!defaultStartRef) {
return;
}
setSelectedEpisodeFillStart(String(defaultStartRef));
if (!hasValidSelection) {
setSelectedEpisodeFillStart(String(defaultStartRef));
}
applyEpisodeFillFrom(defaultStartRef);
}, [selectedMetadataEpisodes, selectedEncodeTitleIds, mediaInfoReview?.generatedAt, episodeAssignmentsByTitle]);
}, [
selectedMetadataEpisodes,
selectedEncodeTitleIds,
mediaInfoReview?.generatedAt,
episodeAssignmentsByTitle,
episodeFillOptions,
selectedEpisodeFillStart
]);
const reviewPlaylistDecisionRequired = Boolean(mediaInfoReview?.playlistDecisionRequired);
const hasSelectedEncodeTitle = normalizeTitleIdList(selectedEncodeTitleIds).length > 0;
@@ -2046,8 +2128,8 @@ export default function PipelineStatusCard({
const refs = collectEpisodeReferencesFromPlan(child?.encodePlan);
for (const ref of refs) {
const key = buildEpisodeReferenceKey(ref);
if (key) {
const keys = collectEpisodeReferenceKeys(ref);
for (const key of keys) {
usedKeys.add(key);
}
}
@@ -2056,12 +2138,12 @@ export default function PipelineStatusCard({
? child.handbrakeInfo.seriesBatch.episodes
: [];
for (const episode of batchEpisodes) {
const key = buildEpisodeReferenceKey({
const keys = collectEpisodeReferenceKeys({
episodeId: episode?.episodeId,
episodeNumber: episode?.episodeNumber,
seasonNumber: episode?.seasonNumber
});
if (key) {
for (const key of keys) {
usedKeys.add(key);
}
}
@@ -2327,15 +2409,17 @@ export default function PipelineStatusCard({
.map((row) => String(row?.episodeTitle || '').trim())
.filter(Boolean)
.join(' + ');
const normalizedTitle = normalizeEpisodeTitleForOutput(
sourceTitle || String(title?.episodeTitle || '').trim() || '(ohne Titel)',
{
start: startNumber || 1,
end: endNumber || startNumber || 1,
span: safeSpan,
isMulti
}
);
const normalizedTitle = sourceTitle
? normalizeEpisodeTitleForOutput(
sourceTitle,
{
start: startNumber || 1,
end: endNumber || startNumber || 1,
span: safeSpan,
isMulti
}
)
: '';
titleAnchorSuggestionById.set(titleId, {
suggestedTitle: normalizedTitle || '(ohne Titel)',
isMulti
@@ -2370,6 +2454,7 @@ export default function PipelineStatusCard({
span: effectiveSpan,
isMulti: effectiveSpan > 1
};
const suggestedTitle = String(titleAnchorSuggestionById.get(titleId)?.suggestedTitle || '').trim();
return {
titleId,
episodeTitle: String(
@@ -2378,12 +2463,9 @@ export default function PipelineStatusCard({
|| title?.fileName
|| ''
).trim() || '(ohne Titel)',
suggestedTitle: (
String(titleAnchorSuggestionById.get(titleId)?.suggestedTitle || '').trim()
|| normalizeEpisodeTitleForOutput(
String(title?.episodeTitle || '').trim() || '(ohne Titel)',
suggestedRange
)
suggestedTitle: suggestedTitle || normalizeEpisodeTitleForOutput(
String(assignment?.episodeTitle || title?.episodeTitle || '').trim() || '(ohne Titel)',
suggestedRange
)
};
})