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