0.12.0-16 misc fixes

This commit is contained in:
2026-03-30 14:37:23 +00:00
parent 76b020d5c0
commit 5284dd2a43
30 changed files with 6955 additions and 896 deletions
+255 -16
View File
@@ -15,6 +15,14 @@ function normalizeTitleId(value) {
return Math.trunc(parsed);
}
function normalizeJobId(value) {
const parsed = Number(value);
if (!Number.isFinite(parsed) || parsed <= 0) {
return null;
}
return Math.trunc(parsed);
}
function normalizePlaylistId(value) {
const raw = String(value || '').trim().toLowerCase();
if (!raw) {
@@ -155,10 +163,119 @@ function isBurnedSubtitleTrack(track) {
);
}
function isDuplicateSubtitleTrack(track) {
return Boolean(track?.duplicate);
}
function normalizeSubtitleLanguage(value) {
const raw = String(value || '').trim().toLowerCase();
if (!raw) {
return 'und';
}
if (raw.length >= 3) {
return raw.slice(0, 3);
}
return raw;
}
function normalizeSubtitleVariantSelectionMap(rawSelection) {
const map = {};
if (!rawSelection || typeof rawSelection !== 'object') {
return map;
}
for (const [language, value] of Object.entries(rawSelection)) {
const normalizedLanguage = normalizeSubtitleLanguage(language);
const full = Boolean(value?.full);
const forced = Boolean(value?.forced);
map[normalizedLanguage] = { full, forced };
}
return map;
}
function normalizeSubtitleLanguageOrder(rawOrder = []) {
const list = Array.isArray(rawOrder) ? rawOrder : [];
const seen = new Set();
const output = [];
for (const value of list) {
const language = normalizeSubtitleLanguage(value);
if (!language || seen.has(language)) {
continue;
}
seen.add(language);
output.push(language);
}
return output;
}
function resolveSubtitleTrackVariantFlags(track) {
const subtitleType = String(track?.subtitleType || '').trim().toLowerCase();
const isForcedOnly = Boolean(
track?.isForcedOnly
?? track?.subtitlePreviewForcedOnly
?? track?.forcedOnly
?? subtitleType === 'forced'
);
const fullHasForced = !isForcedOnly && Boolean(
track?.fullHasForced
?? track?.subtitleFullHasForced
?? track?.hasForcedVariant
);
return { isForcedOnly, fullHasForced };
}
function buildDefaultSubtitleVariantSelection(subtitleTracks, selectedTracks) {
const tracks = Array.isArray(subtitleTracks) ? subtitleTracks : [];
const selected = Array.isArray(selectedTracks) ? selectedTracks : [];
const orderedTracks = [...tracks].sort((a, b) => {
const aId = normalizeTrackId(a?.id) ?? Number.MAX_SAFE_INTEGER;
const bId = normalizeTrackId(b?.id) ?? Number.MAX_SAFE_INTEGER;
if (aId !== bId) {
return aId - bId;
}
return String(a?.language || a?.languageLabel || '').localeCompare(String(b?.language || b?.languageLabel || ''));
});
const order = [];
const orderSeen = new Set();
for (const track of orderedTracks) {
const language = normalizeSubtitleLanguage(track?.language || track?.languageLabel || 'und');
if (!orderSeen.has(language)) {
orderSeen.add(language);
order.push(language);
}
}
const selectionMap = {};
for (const track of selected) {
if (isBurnedSubtitleTrack(track) || isDuplicateSubtitleTrack(track)) {
continue;
}
const language = normalizeSubtitleLanguage(track?.language || track?.languageLabel || 'und');
if (!selectionMap[language]) {
selectionMap[language] = { full: false, forced: false };
}
const flags = resolveSubtitleTrackVariantFlags(track);
if (flags.isForcedOnly) {
selectionMap[language].forced = true;
} else {
selectionMap[language].full = true;
}
}
return {
subtitleVariantSelection: selectionMap,
subtitleLanguageOrder: order
};
}
function buildDefaultTrackSelection(review) {
const titles = Array.isArray(review?.titles) ? review.titles : [];
const selection = {};
const reviewEncodeInputTitleId = normalizeTitleId(review?.encodeInputTitleId);
const manualSelection = review?.manualTrackSelection && typeof review.manualTrackSelection === 'object'
? review.manualTrackSelection
: null;
const manualTitleId = normalizeTitleId(manualSelection?.titleId);
for (const title of titles) {
const titleId = normalizeTitleId(title?.id);
@@ -179,16 +296,49 @@ function buildDefaultTrackSelection(review) {
const subtitleSelectionSource = isEncodeInputTitle
? subtitleTracks.filter((track) => Boolean(track?.selectedForEncode))
: subtitleTracks.filter((track) => Boolean(track?.selectedByRule));
const defaultSubtitleVariantSelection = buildDefaultSubtitleVariantSelection(
subtitleTracks,
subtitleSelectionSource
);
const manualSelectionMatchesTitle = Boolean(
isEncodeInputTitle
&& manualSelection
&& (!manualTitleId || manualTitleId === titleId)
);
const manualAudioTrackIds = manualSelectionMatchesTitle
? normalizeTrackIdList(manualSelection?.audioTrackIds || [])
: [];
const manualSubtitleTrackIds = manualSelectionMatchesTitle
? normalizeTrackIdList(
Array.isArray(manualSelection?.subtitleTrackIdsOrdered)
? manualSelection.subtitleTrackIdsOrdered
: manualSelection?.subtitleTrackIds || []
)
: [];
const manualSubtitleVariantSelection = manualSelectionMatchesTitle
? normalizeSubtitleVariantSelectionMap(manualSelection?.subtitleVariantSelection || {})
: {};
const manualSubtitleLanguageOrder = manualSelectionMatchesTitle
? normalizeSubtitleLanguageOrder(manualSelection?.subtitleLanguageOrder || [])
: [];
selection[titleId] = {
audioTrackIds: normalizeTrackIdList(
audioSelectionSource.map((track) => track?.id)
),
subtitleTrackIds: normalizeTrackIdList(
subtitleSelectionSource
.filter((track) => !isBurnedSubtitleTrack(track))
.map((track) => track?.id)
)
audioTrackIds: manualAudioTrackIds.length > 0
? manualAudioTrackIds
: normalizeTrackIdList(audioSelectionSource.map((track) => track?.id)),
subtitleTrackIds: manualSubtitleTrackIds.length > 0
? manualSubtitleTrackIds
: normalizeTrackIdList(
subtitleSelectionSource
.filter((track) => !isBurnedSubtitleTrack(track) && !isDuplicateSubtitleTrack(track))
.map((track) => track?.id)
),
subtitleVariantSelection: Object.keys(manualSubtitleVariantSelection).length > 0
? manualSubtitleVariantSelection
: defaultSubtitleVariantSelection.subtitleVariantSelection,
subtitleLanguageOrder: manualSubtitleLanguageOrder.length > 0
? manualSubtitleLanguageOrder
: defaultSubtitleVariantSelection.subtitleLanguageOrder
};
}
@@ -197,7 +347,12 @@ function buildDefaultTrackSelection(review) {
function defaultTrackSelectionForTitle(review, titleId) {
const defaults = buildDefaultTrackSelection(review);
return defaults[titleId] || defaults[String(titleId)] || { audioTrackIds: [], subtitleTrackIds: [] };
return defaults[titleId] || defaults[String(titleId)] || {
audioTrackIds: [],
subtitleTrackIds: [],
subtitleVariantSelection: {},
subtitleLanguageOrder: []
};
}
function buildSettingsMap(categories) {
@@ -299,6 +454,7 @@ function buildOutputPathPreview(settings, mediaProfile, metadata, fallbackJobId
}
export default function PipelineStatusCard({
jobId = null,
pipeline,
onAnalyze,
onReanalyze,
@@ -321,7 +477,9 @@ export default function PipelineStatusCard({
const stateLabel = getStatusLabel(state);
const progress = Number(pipeline?.progress || 0);
const running = state === 'ANALYZING' || state === 'RIPPING' || state === 'ENCODING' || state === 'MEDIAINFO_CHECK';
const retryJobId = pipeline?.context?.jobId;
const explicitJobId = normalizeJobId(jobId);
const contextJobId = normalizeJobId(pipeline?.context?.jobId);
const retryJobId = explicitJobId || contextJobId;
const queueLocked = Boolean(isQueued && retryJobId);
const selectedMetadata = pipeline?.context?.selectedMetadata || null;
const mediaInfoReview = pipeline?.context?.mediaInfoReview || null;
@@ -435,7 +593,12 @@ export default function PipelineStatusCard({
}
const defaults = buildDefaultTrackSelection(mediaInfoReview);
const fallback = defaults[currentTitleId] || { audioTrackIds: [], subtitleTrackIds: [] };
const fallback = defaults[currentTitleId] || {
audioTrackIds: [],
subtitleTrackIds: [],
subtitleVariantSelection: {},
subtitleLanguageOrder: []
};
return {
...prev,
[currentTitleId]: fallback
@@ -639,7 +802,12 @@ export default function PipelineStatusCard({
: null;
const fallbackSelection = encodeTitleId
? defaultTrackSelectionForTitle(mediaInfoReview, encodeTitleId)
: { audioTrackIds: [], subtitleTrackIds: [] };
: {
audioTrackIds: [],
subtitleTrackIds: [],
subtitleVariantSelection: {},
subtitleLanguageOrder: []
};
const effectiveSelection = selectionEntry || fallbackSelection;
const encodeTitle = encodeTitleId
? (Array.isArray(mediaInfoReview?.titles)
@@ -648,17 +816,39 @@ export default function PipelineStatusCard({
: null;
const blockedSubtitleTrackIds = new Set(
(Array.isArray(encodeTitle?.subtitleTracks) ? encodeTitle.subtitleTracks : [])
.filter((track) => isBurnedSubtitleTrack(track))
.filter((track) => isBurnedSubtitleTrack(track) || isDuplicateSubtitleTrack(track))
.map((track) => normalizeTrackId(track?.id))
.filter((id) => id !== null)
.map((id) => String(id))
);
const availableSubtitleLanguages = new Set(
(Array.isArray(encodeTitle?.subtitleTracks) ? encodeTitle.subtitleTracks : [])
.filter((track) => !isBurnedSubtitleTrack(track) && !isDuplicateSubtitleTrack(track))
.map((track) => normalizeSubtitleLanguage(track?.language || track?.languageLabel || 'und'))
);
const normalizedVariantSelection = normalizeSubtitleVariantSelectionMap(effectiveSelection?.subtitleVariantSelection || {});
const filteredVariantSelection = {};
for (const [language, value] of Object.entries(normalizedVariantSelection)) {
if (!availableSubtitleLanguages.has(language)) {
continue;
}
filteredVariantSelection[language] = {
full: Boolean(value?.full),
forced: Boolean(value?.forced)
};
}
const normalizedLanguageOrder = normalizeSubtitleLanguageOrder(effectiveSelection?.subtitleLanguageOrder || [])
.filter((language) => availableSubtitleLanguages.has(language));
const hasExplicitVariantSelection = Object.keys(filteredVariantSelection).length > 0;
const filteredSubtitleTrackIds = normalizeTrackIdList(effectiveSelection?.subtitleTrackIds || [])
.filter((id) => !blockedSubtitleTrackIds.has(String(id)));
const selectedTrackSelection = encodeTitleId
? {
[encodeTitleId]: {
audioTrackIds: normalizeTrackIdList(effectiveSelection?.audioTrackIds || []),
subtitleTrackIds: normalizeTrackIdList(effectiveSelection?.subtitleTrackIds || [])
.filter((id) => !blockedSubtitleTrackIds.has(String(id)))
subtitleTrackIds: hasExplicitVariantSelection ? [] : filteredSubtitleTrackIds,
subtitleVariantSelection: filteredVariantSelection,
subtitleLanguageOrder: normalizedLanguageOrder
}
}
: null;
@@ -1032,7 +1222,9 @@ export default function PipelineStatusCard({
setTrackSelectionByTitle((prev) => {
const current = prev?.[normalizedTitleId] || prev?.[String(normalizedTitleId)] || {
audioTrackIds: [],
subtitleTrackIds: []
subtitleTrackIds: [],
subtitleVariantSelection: {},
subtitleLanguageOrder: []
};
const key = trackType === 'subtitle' ? 'subtitleTrackIds' : 'audioTrackIds';
const existing = normalizeTrackIdList(current?.[key] || []);
@@ -1049,6 +1241,53 @@ export default function PipelineStatusCard({
};
});
}}
onSubtitleVariantSelectionChange={(titleId, language, variant, checked) => {
const normalizedTitleId = normalizeTitleId(titleId);
const normalizedLanguage = normalizeSubtitleLanguage(language);
const normalizedVariant = String(variant || '').trim().toLowerCase() === 'forced' ? 'forced' : 'full';
if (!normalizedTitleId || !normalizedLanguage) {
return;
}
setTrackSelectionByTitle((prev) => {
const current = prev?.[normalizedTitleId] || prev?.[String(normalizedTitleId)] || {
audioTrackIds: [],
subtitleTrackIds: [],
subtitleVariantSelection: {},
subtitleLanguageOrder: []
};
const currentMap = normalizeSubtitleVariantSelectionMap(current?.subtitleVariantSelection || {});
const nextMap = { ...currentMap };
const currentEntry = nextMap[normalizedLanguage] || { full: false, forced: false };
nextMap[normalizedLanguage] = {
...currentEntry,
[normalizedVariant]: Boolean(checked)
};
const existingOrder = normalizeSubtitleLanguageOrder(current?.subtitleLanguageOrder || []);
const fallbackOrder = normalizeSubtitleLanguageOrder(
defaultTrackSelectionForTitle(mediaInfoReview, normalizedTitleId)?.subtitleLanguageOrder || []
);
let nextOrder = existingOrder.length > 0
? existingOrder
: fallbackOrder;
if (nextOrder.length === 0) {
nextOrder = [normalizedLanguage];
} else if (!nextOrder.includes(normalizedLanguage) && fallbackOrder.includes(normalizedLanguage)) {
nextOrder = fallbackOrder;
} else if (!nextOrder.includes(normalizedLanguage)) {
nextOrder = [...nextOrder, normalizedLanguage];
}
return {
...prev,
[normalizedTitleId]: {
...current,
subtitleVariantSelection: nextMap,
subtitleLanguageOrder: nextOrder
}
};
});
}}
availableScripts={scriptCatalog}
availableChains={chainCatalog}
preEncodeItems={preEncodeItems}