0.13.1 DVD Series rc1

This commit is contained in:
2026-04-11 21:26:28 +00:00
parent 1f8e6d3ce8
commit bc193db50a
14 changed files with 387 additions and 30 deletions
+178 -2
View File
@@ -485,6 +485,77 @@ function normalizePositiveInt(value) {
return Math.trunc(parsed);
}
function normalizeEpisodeReference(entry = {}) {
const source = entry && typeof entry === 'object' ? entry : {};
const episodeId = normalizePositiveInt(source?.episodeId ?? source?.id ?? null);
const episodeNumber = normalizePositiveInt(source?.episodeNumber ?? source?.number ?? null);
const seasonNumber = normalizePositiveInt(source?.seasonNumber ?? source?.season ?? null);
if (!episodeId && !episodeNumber) {
return null;
}
return {
episodeId,
episodeNumber,
seasonNumber
};
}
function buildEpisodeReferenceKey(entry = {}) {
const normalized = normalizeEpisodeReference(entry);
if (!normalized) {
return null;
}
if (normalized.episodeId) {
return `id:${normalized.episodeId}`;
}
if (normalized.seasonNumber && normalized.episodeNumber) {
return `se:${normalized.seasonNumber}:${normalized.episodeNumber}`;
}
return normalized.episodeNumber ? `ep:${normalized.episodeNumber}` : null;
}
function isSeriesBatchEpisodePlan(plan = null) {
const source = plan && typeof plan === 'object' ? plan : {};
if (Boolean(source?.seriesBatchChild) || Boolean(source?.seriesBatchVirtualEpisode)) {
return true;
}
const parentJobId = normalizePositiveInt(source?.seriesBatchParentJobId);
const childIndex = normalizePositiveInt(source?.seriesBatchChildIndex);
const childCount = normalizePositiveInt(source?.seriesBatchChildCount);
const titleId = normalizeTitleId(source?.seriesBatchTitleId);
return Boolean(parentJobId && (childIndex || childCount || titleId));
}
function collectEpisodeReferencesFromPlan(plan = null) {
const source = plan && typeof plan === 'object' ? plan : {};
const refs = [];
const assignments = source?.episodeAssignments && typeof source.episodeAssignments === 'object'
? Object.values(source.episodeAssignments)
: [];
for (const assignment of assignments) {
const normalized = normalizeEpisodeReference(assignment);
if (normalized) {
refs.push(normalized);
}
}
const titles = Array.isArray(source?.titles) ? source.titles : [];
for (const title of titles) {
if (!Boolean(title?.selectedForEncode) && !Boolean(title?.encodeInput)) {
continue;
}
const normalized = normalizeEpisodeReference({
episodeId: title?.episodeId,
episodeNumber: title?.episodeNumber,
seasonNumber: title?.seasonNumber
});
if (normalized) {
refs.push(normalized);
}
}
return refs;
}
function normalizeSeriesLanguage(value) {
const raw = String(value || '').trim();
if (!raw) {
@@ -730,6 +801,7 @@ export default function PipelineStatusCard({
const [trackSelectionByTitle, setTrackSelectionByTitle] = useState({});
const [episodeAssignmentsByTitle, setEpisodeAssignmentsByTitle] = useState({});
const [selectedEpisodeFillStart, setSelectedEpisodeFillStart] = useState(null);
const [containerUsedEpisodeKeys, setContainerUsedEpisodeKeys] = useState([]);
const [settingsMap, setSettingsMap] = useState({});
const [presetDisplayMap, setPresetDisplayMap] = useState({});
const [scriptCatalog, setScriptCatalog] = useState([]);
@@ -748,7 +820,7 @@ export default function PipelineStatusCard({
const converterConfigSaveTimeoutRef = useRef(null);
const selectedMetadataEpisodes = useMemo(() => {
const rows = Array.isArray(selectedMetadata?.episodes) ? selectedMetadata.episodes : [];
return rows
const normalizedRows = rows
.map((item) => {
const episodeId = Number(item?.id || 0);
const episodeNumber = Number(item?.number ?? item?.episodeNumber ?? 0);
@@ -777,7 +849,15 @@ export default function PipelineStatusCard({
const bId = Number.isFinite(b.episodeId) ? b.episodeId : Number.MAX_SAFE_INTEGER;
return aId - bId;
});
}, [selectedMetadata?.episodes]);
if (!Array.isArray(containerUsedEpisodeKeys) || containerUsedEpisodeKeys.length === 0) {
return normalizedRows;
}
const usedKeySet = new Set(containerUsedEpisodeKeys);
return normalizedRows.filter((episode) => {
const key = buildEpisodeReferenceKey(episode);
return !key || !usedKeySet.has(key);
});
}, [selectedMetadata?.episodes, containerUsedEpisodeKeys]);
useEffect(() => {
let cancelled = false;
@@ -1333,6 +1413,102 @@ export default function PipelineStatusCard({
const metadataProvider = String(selectedMetadata?.metadataProvider || '').trim().toLowerCase();
return hasSeasonNumber || hasEpisodeList || metadataProvider === 'tmdb' || metadataProvider === 'themoviedb';
}, [jobMediaProfile, selectedMetadata?.seasonNumber, selectedMetadata?.episodes, selectedMetadata?.metadataProvider]);
useEffect(() => {
let cancelled = false;
const loadContainerUsedEpisodes = async () => {
if (!isSeriesDvdReview || !retryJobId) {
if (!cancelled) {
setContainerUsedEpisodeKeys([]);
}
return;
}
try {
const currentResponse = await api.getJob(retryJobId, { lite: true, forceRefresh: true });
if (cancelled) {
return;
}
const currentJob = currentResponse?.job && typeof currentResponse.job === 'object'
? currentResponse.job
: null;
const currentJobId = normalizeJobId(currentJob?.id || retryJobId);
const normalizedCurrentKind = String(currentJob?.job_kind || '').trim().toLowerCase();
const containerJobId = normalizedCurrentKind === 'dvd_series_container'
? normalizeJobId(currentJob?.id)
: normalizeJobId(currentJob?.parent_job_id);
if (!containerJobId) {
if (!cancelled) {
setContainerUsedEpisodeKeys([]);
}
return;
}
const containerResponse = await api.getJob(containerJobId, { lite: true, forceRefresh: true });
if (cancelled) {
return;
}
const containerJob = containerResponse?.job && typeof containerResponse.job === 'object'
? containerResponse.job
: null;
const children = Array.isArray(containerJob?.children) ? containerJob.children : [];
const usedKeys = new Set();
for (const child of children) {
const childId = normalizeJobId(child?.id);
if (currentJobId && childId && currentJobId === childId) {
continue;
}
if (isSeriesBatchEpisodePlan(child?.encodePlan)) {
continue;
}
const refs = collectEpisodeReferencesFromPlan(child?.encodePlan);
for (const ref of refs) {
const key = buildEpisodeReferenceKey(ref);
if (key) {
usedKeys.add(key);
}
}
const batchEpisodes = Array.isArray(child?.handbrakeInfo?.seriesBatch?.episodes)
? child.handbrakeInfo.seriesBatch.episodes
: [];
for (const episode of batchEpisodes) {
const key = buildEpisodeReferenceKey({
episodeId: episode?.episodeId,
episodeNumber: episode?.episodeNumber,
seasonNumber: episode?.seasonNumber
});
if (key) {
usedKeys.add(key);
}
}
}
if (!cancelled) {
setContainerUsedEpisodeKeys(Array.from(usedKeys));
}
} catch (_error) {
if (!cancelled) {
setContainerUsedEpisodeKeys([]);
}
}
};
void loadContainerUsedEpisodes();
return () => {
cancelled = true;
};
}, [
isSeriesDvdReview,
retryJobId,
mediaInfoReview?.generatedAt,
mediaInfoReview?.reviewConfirmedAt,
selectedMetadata?.tmdbId,
selectedMetadata?.seasonNumber
]);
const seriesBatchContext = useMemo(() => {
const raw = pipeline?.context?.seriesBatch;
if (!raw || typeof raw !== 'object') {