0.13.0-1 Series Flow

This commit is contained in:
2026-04-11 19:11:31 +00:00
parent 974b7bfad3
commit 1f8e6d3ce8
10 changed files with 1138 additions and 163 deletions
+137 -19
View File
@@ -905,11 +905,27 @@ function enrichJobRow(job, settings = null, options = {}) {
const backupSuccess = ripSuccessful;
const finishedWithOutput = normalizedJobStatus === 'FINISHED'
&& (includeFsChecks ? Boolean(outputStatus?.exists) : true);
const seriesBatchSummary = handbrakeInfo?.seriesBatch && typeof handbrakeInfo.seriesBatch === 'object'
? handbrakeInfo.seriesBatch
: null;
const seriesBatchMode = String(handbrakeInfo?.mode || '').trim().toLowerCase() === 'series_batch';
const seriesBatchTotal = Number(seriesBatchSummary?.totalCount || 0);
const seriesBatchFinished = Number(seriesBatchSummary?.finishedCount || 0);
const seriesBatchErrors = Number(seriesBatchSummary?.errorCount || 0);
const seriesBatchCancelled = Number(seriesBatchSummary?.cancelledCount || 0);
const seriesBatchEncodeSuccess = seriesBatchMode
&& seriesBatchTotal > 0
&& seriesBatchFinished >= seriesBatchTotal
&& seriesBatchErrors <= 0
&& seriesBatchCancelled <= 0;
const encodeSuccess = directoryLikeOutput
? finishedWithOutput
: (mediaType === 'converter'
? finishedWithOutput
: String(handbrakeInfo?.status || '').trim().toUpperCase() === 'SUCCESS');
: (
seriesBatchEncodeSuccess
|| (mediaType === 'converter'
? finishedWithOutput
: String(handbrakeInfo?.status || '').trim().toUpperCase() === 'SUCCESS')
);
return {
...job,
@@ -3629,6 +3645,7 @@ class HistoryService {
});
const seriesCandidateIds = seriesCandidates.map((job) => normalizeJobIdValue(job?.id)).filter(Boolean);
let childRows = [];
let nestedChildRows = [];
let outputFolders = [];
if (containerIds.length > 0) {
const placeholders = containerIds.map(() => '?').join(', ');
@@ -3639,15 +3656,46 @@ class HistoryService {
const childIds = childRows.map((row) => normalizeJobIdValue(row?.id)).filter(Boolean);
if (childIds.length > 0) {
const childPlaceholders = childIds.map(() => '?').join(', ');
nestedChildRows = await db.all(
`SELECT id, parent_job_id, output_path, raw_path, handbrake_info_json, status, encode_plan_json, makemkv_info_json, rip_successful FROM jobs WHERE parent_job_id IN (${childPlaceholders})`,
childIds
);
}
const summaryJobIds = Array.from(new Set([
...childRows.map((row) => normalizeJobIdValue(row?.id)).filter(Boolean),
...nestedChildRows.map((row) => normalizeJobIdValue(row?.id)).filter(Boolean)
]));
if (summaryJobIds.length > 0) {
const childPlaceholders = summaryJobIds.map(() => '?').join(', ');
outputFolders = await db.all(
`SELECT job_id, output_path FROM job_output_folders WHERE job_id IN (${childPlaceholders})`,
childIds
summaryJobIds
);
}
}
const allContainerChildRows = [...childRows, ...nestedChildRows];
const directChildRowById = new Map();
for (const row of childRows) {
const childId = normalizeJobIdValue(row?.id);
if (childId) {
directChildRowById.set(childId, row);
}
}
const nestedRowsByParentId = new Map();
for (const row of nestedChildRows) {
const parentId = normalizeJobIdValue(row?.parent_job_id);
if (!parentId) {
continue;
}
if (!nestedRowsByParentId.has(parentId)) {
nestedRowsByParentId.set(parentId, []);
}
nestedRowsByParentId.get(parentId).push(row);
}
const childOutputMap = new Map();
for (const child of childRows) {
for (const child of allContainerChildRows) {
const childId = normalizeJobIdValue(child?.id);
if (!childId) {
continue;
@@ -3701,9 +3749,12 @@ class HistoryService {
let expectedFromPlans = 0;
const seenOutputs = new Set();
let existingCount = 0;
let existingCountFromSeriesBatch = 0;
const totalChildren = childIds.length;
for (const childId of childIds) {
const childRow = childRows.find((row) => normalizeJobIdValue(row?.id) === childId) || null;
const childRow = directChildRowById.get(childId) || null;
const descendantRows = nestedRowsByParentId.get(childId) || [];
const relatedRows = [childRow, ...descendantRows].filter(Boolean);
if (childRow) {
const rawPath = String(childRow?.raw_path || '').trim();
if (rawPath) {
@@ -3722,32 +3773,87 @@ class HistoryService {
if (ripSuccessful) {
backupSuccessCount += 1;
}
let selectedCount = 0;
const childPlan = parseJsonSafe(childRow?.encode_plan_json, null);
if (childPlan && typeof childPlan === 'object') {
const titles = Array.isArray(childPlan?.titles) ? childPlan.titles : [];
const selectedCount = titles.filter((title) => title?.selectedForEncode || title?.encodeInput).length;
if (selectedCount > 0) {
expectedFromPlans += selectedCount;
}
selectedCount = titles.filter((title) => title?.selectedForEncode || title?.encodeInput).length;
}
const handbrakeInfo = parseJsonSafe(childRow?.handbrake_info_json, null);
const hbStatus = String(handbrakeInfo?.status || '').trim().toUpperCase();
if (hbStatus === 'SUCCESS') {
encodeSuccessAny = true;
encodeSuccessCount += 1;
const seriesBatchSummary = handbrakeInfo?.seriesBatch && typeof handbrakeInfo.seriesBatch === 'object'
? handbrakeInfo.seriesBatch
: null;
const seriesBatchTotal = Number(seriesBatchSummary?.totalCount || 0);
const seriesBatchFinished = Number(seriesBatchSummary?.finishedCount || 0);
const seriesBatchErrors = Number(seriesBatchSummary?.errorCount || 0);
const seriesBatchCancelled = Number(seriesBatchSummary?.cancelledCount || 0);
const expectedFromNested = descendantRows.length > 0 ? descendantRows.length : 0;
const expectedFromChild = seriesBatchTotal > 0
? seriesBatchTotal
: (selectedCount > 0 ? selectedCount : expectedFromNested);
if (expectedFromChild > 0) {
expectedFromPlans += expectedFromChild;
}
if (seriesBatchFinished > 0) {
existingCountFromSeriesBatch += seriesBatchFinished;
}
let childEncodeSuccess = hbStatus === 'SUCCESS';
if (
!childEncodeSuccess
&& seriesBatchTotal > 0
&& seriesBatchFinished >= seriesBatchTotal
&& seriesBatchErrors <= 0
&& seriesBatchCancelled <= 0
) {
childEncodeSuccess = true;
}
if (!childEncodeSuccess && descendantRows.length > 0) {
let finishedDescendants = 0;
let errorDescendants = 0;
let cancelledDescendants = 0;
for (const row of descendantRows) {
const rowState = String(row?.status || row?.last_state || '').trim().toUpperCase();
if (rowState === 'FINISHED') {
finishedDescendants += 1;
} else if (rowState === 'ERROR') {
errorDescendants += 1;
} else if (rowState === 'CANCELLED') {
cancelledDescendants += 1;
}
}
childEncodeSuccess = finishedDescendants >= descendantRows.length
&& errorDescendants === 0
&& cancelledDescendants === 0;
}
if (childEncodeSuccess) {
encodeSuccessAny = true;
encodeSuccessCount += 1;
}
} else if (descendantRows.length > 0) {
expectedFromPlans += descendantRows.length;
}
const outputs = Array.from(childOutputMap.get(childId) || []);
for (const outputPath of outputs) {
if (!outputPath || seenOutputs.has(outputPath)) {
for (const relatedRow of relatedRows) {
const relatedRowId = normalizeJobIdValue(relatedRow?.id);
if (!relatedRowId) {
continue;
}
seenOutputs.add(outputPath);
if (!includeFsChecks || fs.existsSync(outputPath)) {
existingCount += 1;
const outputs = Array.from(childOutputMap.get(relatedRowId) || []);
for (const outputPath of outputs) {
if (!outputPath || seenOutputs.has(outputPath)) {
continue;
}
seenOutputs.add(outputPath);
if (!includeFsChecks || fs.existsSync(outputPath)) {
existingCount += 1;
}
}
}
}
if (existingCountFromSeriesBatch > existingCount) {
existingCount = existingCountFromSeriesBatch;
}
if (!encodeSuccessAny && existingCount > 0) {
encodeSuccessAny = true;
if (encodeSuccessCount === 0 && totalChildren > 0) {
@@ -5102,6 +5208,18 @@ class HistoryService {
?? analyzeContext?.seriesLookupHint?.discNumber
?? null
);
const resolvedJobMediaType = inferMediaType(
job,
makemkvInfo,
job?.mediainfo_info_json,
job?.encode_plan_json,
job?.handbrake_info_json
);
if (resolvedJobMediaType === 'dvd' && discNumber === null) {
const error = new Error('Serien-DVD erkannt: Disk-Nummer ist Pflicht (Start bei 1).');
error.statusCode = 400;
throw error;
}
let metadataKind = manualMetadataKind
|| String(
existingSelectedMetadata?.metadataKind
File diff suppressed because it is too large Load Diff