0.13.1-10 Misc fixes

This commit is contained in:
2026-04-17 08:04:52 +00:00
parent f25b6e1181
commit bebe9eac8c
19 changed files with 979 additions and 2155 deletions
+178 -29
View File
@@ -669,15 +669,16 @@ function formatTemplateTwoDigit(value) {
return String(value || '').trim() || String(numeric);
}
function resolveEpisodeAssignmentRange(assignment = null, title = null, fallbackEpisodeNumber = 1) {
function resolveEpisodeAssignmentRange(assignment = null, title = null, fallbackEpisodeNumber = 1, options = {}) {
const assignmentSource = assignment && typeof assignment === 'object' ? assignment : {};
const titleSource = title && typeof title === 'object' ? title : {};
const allowTitleEpisodeNumber = options?.allowTitleEpisodeNumber !== false;
const fallbackNumber = normalizePositiveInt(fallbackEpisodeNumber) || 1;
const episodeStart = normalizePositiveInt(
assignmentSource?.episodeNumberStart
?? assignmentSource?.episodeNoStart
?? assignmentSource?.episodeNumber
?? titleSource?.episodeNumber
?? (allowTitleEpisodeNumber ? titleSource?.episodeNumber : null)
?? fallbackNumber
) || fallbackNumber;
const explicitEpisodeEnd = normalizePositiveInt(
@@ -939,7 +940,135 @@ function resolveEffectiveEpisodeRangeForTitle(assignment = null, title = null, a
};
}
function buildDvdSeriesEpisodeOutputPathPreview(settings, metadata, title, assignment = null, fallbackJobId = null, allTitles = []) {
function resolveSeriesTitleOrderForPreview(allTitles = [], selectedTitleIds = [], targetTitleId = null) {
const explicitSelectedIds = normalizeTitleIdList(selectedTitleIds);
const fallbackSelectedIds = explicitSelectedIds.length > 0
? explicitSelectedIds
: normalizeTitleIdList(
(Array.isArray(allTitles) ? allTitles : [])
.filter((item) => Boolean(item?.selectedForEncode || item?.encodeInput))
.map((item) => item?.id)
);
let ordered = fallbackSelectedIds;
const normalizedTarget = normalizeTitleId(targetTitleId);
if (normalizedTarget && !ordered.some((id) => Number(id) === Number(normalizedTarget))) {
ordered = [...ordered, Number(normalizedTarget)];
}
if (ordered.length === 0 && normalizedTarget) {
ordered = [Number(normalizedTarget)];
}
return ordered;
}
function resolveEpisodeRangeForTitleFromAssignments({
assignmentByTitle = {},
allTitles = [],
selectedTitleIds = [],
targetTitle = null,
fallbackEpisodeNumber = 1
}) {
const titles = Array.isArray(allTitles) ? allTitles : [];
const titlesById = new Map();
for (const item of titles) {
const id = normalizeTitleId(item?.id);
if (!id) {
continue;
}
titlesById.set(Number(id), item);
}
const targetTitleId = normalizeTitleId(targetTitle?.id || null);
const orderedTitleIds = resolveSeriesTitleOrderForPreview(titles, selectedTitleIds, targetTitleId);
const fallbackStart = normalizePositiveInt(fallbackEpisodeNumber) || 1;
let cursor = fallbackStart;
let targetRange = null;
for (const rawId of orderedTitleIds) {
const currentId = normalizeTitleId(rawId);
if (!currentId) {
continue;
}
const assignment = assignmentByTitle?.[currentId] || assignmentByTitle?.[String(currentId)] || null;
const currentTitle = titlesById.get(Number(currentId)) || null;
const hasExplicitRange = hasExplicitEpisodeAssignmentRange(assignment);
const range = resolveEpisodeAssignmentRange(
assignment,
currentTitle,
cursor,
{ allowTitleEpisodeNumber: hasExplicitRange }
);
const normalizedEnd = normalizePositiveInt(range?.end);
if (normalizedEnd !== null) {
cursor = normalizedEnd + 1;
}
if (targetTitleId && Number(currentId) === Number(targetTitleId)) {
targetRange = range;
break;
}
}
if (!targetRange) {
const assignment = targetTitleId
? (assignmentByTitle?.[targetTitleId] || assignmentByTitle?.[String(targetTitleId)] || null)
: null;
const hasExplicitRange = hasExplicitEpisodeAssignmentRange(assignment);
targetRange = resolveEpisodeAssignmentRange(
assignment,
targetTitle,
cursor,
{ allowTitleEpisodeNumber: hasExplicitRange }
);
}
return targetRange;
}
function templateUsesLeadingZeroForToken(template, token) {
const source = String(template || '').trim();
const normalizedToken = String(token || '').trim();
if (!source || !normalizedToken) {
return false;
}
const pattern = new RegExp(`\\{\\s*0\\s*:\\s*${normalizedToken}\\s*\\}`, 'i');
return pattern.test(source);
}
function formatEpisodeNumberByTemplate(rawValue, template, token = 'episodeNr') {
const numeric = Number(rawValue);
if (!Number.isFinite(numeric) || numeric <= 0) {
return String(rawValue || '').trim() || '0';
}
if (!Number.isInteger(numeric)) {
return String(numeric);
}
const normalized = String(Math.trunc(numeric));
if (templateUsesLeadingZeroForToken(template, token)) {
return normalized.padStart(2, '0');
}
return normalized;
}
function buildFallbackEpisodeTitleForOutput(episodeRange = null, template = '') {
const start = normalizePositiveInt(episodeRange?.start) || 1;
const end = normalizePositiveInt(episodeRange?.end) || start;
if (end > start) {
const startToken = formatEpisodeNumberByTemplate(start, template, 'episodeNr');
const endToken = formatEpisodeNumberByTemplate(end, template, 'episodeNr');
return `Folge ${startToken}-${endToken}`;
}
const token = formatEpisodeNumberByTemplate(start, template, 'episodeNr');
return `Folge ${token}`;
}
function buildDvdSeriesEpisodeOutputPathPreview(
settings,
metadata,
title,
assignment = null,
fallbackJobId = null,
allTitles = [],
assignmentByTitle = {},
selectedTitleIds = []
) {
const seriesRootDir = String(
settings?.series_dir_dvd
|| settings?.movie_dir_dvd
@@ -953,15 +1082,28 @@ function buildDvdSeriesEpisodeOutputPathPreview(settings, metadata, title, assig
const seasonNumber = normalizePositiveInt(
assignment?.seasonNumber ?? metadata?.seasonNumber ?? title?.seasonNumber ?? 1
) || 1;
const normalizedSelectedIds = normalizeTitleIdList(selectedTitleIds);
const targetTitleId = normalizeTitleId(title?.id);
const fallbackEpisodeNumber = targetTitleId && normalizedSelectedIds.length > 0
? (normalizedSelectedIds.findIndex((id) => Number(id) === Number(targetTitleId)) + 1)
: 1;
const inferredRange = resolveEpisodeRangeForTitleFromAssignments({
assignmentByTitle,
allTitles,
selectedTitleIds: normalizedSelectedIds,
targetTitle: title,
fallbackEpisodeNumber: fallbackEpisodeNumber > 0 ? fallbackEpisodeNumber : 1
});
const episodeRange = resolveEffectiveEpisodeRangeForTitle(
assignment,
assignment || {},
title,
allTitles,
title?.id || 1
inferredRange?.start || (fallbackEpisodeNumber > 0 ? fallbackEpisodeNumber : 1)
);
const episodeNumber = episodeRange.start;
const episodeRangeToken = buildEpisodeRangeToken(episodeRange.start, episodeRange.end);
const partsToken = buildEpisodePartsToken(episodeRange);
const effectiveEpisodeRange = episodeRange;
const episodeNumber = effectiveEpisodeRange.start;
const episodeRangeToken = buildEpisodeRangeToken(effectiveEpisodeRange.start, effectiveEpisodeRange.end);
const partsToken = buildEpisodePartsToken(effectiveEpisodeRange);
const discNumber = normalizePositiveInt(metadata?.discNumber ?? assignment?.discNumber ?? null);
const seriesTitleRaw = String(
metadata?.title
@@ -969,16 +1111,6 @@ function buildDvdSeriesEpisodeOutputPathPreview(settings, metadata, title, assig
|| (fallbackJobId ? `job-${fallbackJobId}` : 'series')
).trim();
const seriesTitle = seriesTitleRaw || 'series';
const episodeTitleRaw = String(
assignment?.episodeTitle
|| title?.episodeTitle
|| title?.fileName
|| `Episode ${episodeNumber}`
).trim();
const episodeTitle = normalizeEpisodeTitleForOutput(
episodeTitleRaw || `Episode ${episodeRangeToken}`,
episodeRange
);
const year = String(metadata?.year || new Date().getFullYear()).trim();
const language = normalizeSeriesLanguage(settings?.dvd_series_language || metadata?.language || null);
@@ -992,21 +1124,28 @@ function buildDvdSeriesEpisodeOutputPathPreview(settings, metadata, title, assig
).trim();
const singleTemplate = singleTemplateRaw || '{seriesTitle}/Staffel {seasonNr}/S{seasonNr}E{episodeNr} - {episodeTitle}';
const multiTemplate = multiTemplateRaw || '{seriesTitle}/Staffel {seasonNr}/S{0:seasonNr}E{episodeRange} - {episodeTitle} (Teil {parts})';
const template = episodeRange.isMulti ? multiTemplate : singleTemplate;
const template = effectiveEpisodeRange.isMulti ? multiTemplate : singleTemplate;
const fallbackEpisodeTitle = buildFallbackEpisodeTitleForOutput(effectiveEpisodeRange, template);
const episodeTitleRaw = String(
assignment?.episodeTitle
|| title?.episodeTitle
|| fallbackEpisodeTitle
).trim();
const rendered = renderTemplate(template, {
seriesTitle,
seasonNr: seasonNumber,
seasonNo: String(seasonNumber),
episodeNr: episodeNumber,
episodeNo: String(episodeNumber),
episodeNoStart: episodeRange.start,
episodeNoEnd: episodeRange.end,
episodeNumberStart: episodeRange.start,
episodeNumberEnd: episodeRange.end,
episodeNoStart: effectiveEpisodeRange.start,
episodeNoEnd: effectiveEpisodeRange.end,
episodeNumberStart: effectiveEpisodeRange.start,
episodeNumberEnd: effectiveEpisodeRange.end,
episodeRange: episodeRangeToken,
episodeSpan: episodeRange.span,
episodeSpan: effectiveEpisodeRange.span,
parts: partsToken,
episodeTitle,
episodeTitle: normalizeEpisodeTitleForOutput(
episodeTitleRaw || fallbackEpisodeTitle,
effectiveEpisodeRange
) || fallbackEpisodeTitle,
discNr: discNumber || '',
year,
language
@@ -2380,14 +2519,24 @@ export default function PipelineStatusCard({
title,
assignment,
retryJobId,
Array.isArray(mediaInfoReview?.titles) ? mediaInfoReview.titles : []
Array.isArray(mediaInfoReview?.titles) ? mediaInfoReview.titles : [],
episodeAssignmentsByTitle,
selectedEncodeTitleIds
);
if (path) {
map[titleId] = path;
}
}
return map;
}, [isSeriesDvdReview, mediaInfoReview?.titles, episodeAssignmentsByTitle, settingsMap, selectedMetadata, retryJobId]);
}, [
isSeriesDvdReview,
mediaInfoReview?.titles,
episodeAssignmentsByTitle,
settingsMap,
selectedMetadata,
retryJobId,
selectedEncodeTitleIds
]);
const multiEpisodeReviewWarnings = useMemo(() => {
if (!isSeriesDvdReview) {
return [];