diff --git a/backend/package-lock.json b/backend/package-lock.json index 31e0e78..3f4607d 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,12 +1,12 @@ { "name": "ripster-backend", - "version": "0.13.1-1", + "version": "0.13.1-2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ripster-backend", - "version": "0.13.1-1", + "version": "0.13.1-2", "dependencies": { "archiver": "^7.0.1", "cors": "^2.8.5", diff --git a/backend/package.json b/backend/package.json index 6785597..398c2ad 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "ripster-backend", - "version": "0.13.1-1", + "version": "0.13.1-2", "private": true, "type": "commonjs", "scripts": { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d9b8b4f..bad41c8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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", diff --git a/frontend/package.json b/frontend/package.json index 1050186..6fa2346 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "ripster-frontend", - "version": "0.13.1-1", + "version": "0.13.1-2", "private": true, "type": "module", "scripts": { diff --git a/frontend/src/components/PipelineStatusCard.jsx b/frontend/src/components/PipelineStatusCard.jsx index 9aecf4a..f202f43 100644 --- a/frontend/src/components/PipelineStatusCard.jsx +++ b/frontend/src/components/PipelineStatusCard.jsx @@ -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 ) }; }) diff --git a/package-lock.json b/package-lock.json index 7f9c6f3..28ccea5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ripster", - "version": "0.13.1-1", + "version": "0.13.1-2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ripster", - "version": "0.13.1-1", + "version": "0.13.1-2", "devDependencies": { "concurrently": "^9.1.2" } diff --git a/package.json b/package.json index 1877977..32035ac 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ripster", "private": true, - "version": "0.13.1-1", + "version": "0.13.1-2", "scripts": { "dev": "concurrently \"npm run dev --prefix backend\" \"npm run dev --prefix frontend\"", "dev:backend": "npm run dev --prefix backend",