diff --git a/src/main/notification-events.ts b/src/main/notification-events.ts index ed8c505..58b33a2 100644 --- a/src/main/notification-events.ts +++ b/src/main/notification-events.ts @@ -42,6 +42,7 @@ export interface RunResult { remuxFailures: number; downloadFailures: number; offlineFailures: number; + cleanupFailures: number; } export interface RunResultInput { @@ -289,10 +290,11 @@ export function buildRunResult(input: RunResultInput): RunResult { extractionDurationSeconds: sum((result) => result.extractionDurationSeconds), remuxDurationSeconds: sum((result) => result.remuxDurationSeconds), postProcessDurationSeconds: sum((result) => result.postProcessDurationSeconds), - extractionFailures: packages.reduce((total, result) => total + result.archiveOperations.filter((operation) => operation.status === "failed").length, 0), - remuxFailures: packages.reduce((total, result) => total + result.remuxOperations.filter((operation) => operation.status === "failed").length, 0), - downloadFailures: packages.filter((result) => result.failurePhase === "download").reduce((total, result) => total + result.failedFiles, 0), - offlineFailures: packages.filter((result) => /offline|not found|nicht gefunden/i.test(result.errorCategory)).reduce((total, result) => total + result.failedFiles, 0) + extractionFailures: sum((result) => result.extractionFailures), + remuxFailures: sum((result) => result.remuxFailures), + downloadFailures: sum((result) => result.downloadFailures), + offlineFailures: sum((result) => result.offlineFailures), + cleanupFailures: sum((result) => result.cleanupFailures) }; } @@ -323,7 +325,8 @@ export function buildRunNotificationEvent(result: RunResult): NotificationEvent { name: "Entpackfehler", value: String(result.extractionFailures), inline: true }, { name: "Remuxfehler", value: String(result.remuxFailures), inline: true }, { name: "Downloadfehler", value: String(result.downloadFailures), inline: true }, - { name: "Offline", value: String(result.offlineFailures), inline: true } + { name: "Offline", value: String(result.offlineFailures), inline: true }, + { name: "Cleanupfehler", value: String(result.cleanupFailures), inline: true } ] ); } diff --git a/src/main/package-telemetry.ts b/src/main/package-telemetry.ts index d1ec9e9..610cd05 100644 --- a/src/main/package-telemetry.ts +++ b/src/main/package-telemetry.ts @@ -123,7 +123,8 @@ export function finalizePackageResult(telemetry: PackageTelemetry): PackageResul .map((operation) => ({ ...operation, itemIds: [...operation.itemIds] })); const remuxOperations = (telemetry.remuxOperations ?? packageEntry.remuxOperations ?? []) .map((operation) => ({ ...operation })); - const completedDownloads = telemetry.items.filter((item) => item.status === "completed").length; + const cleanedCompletedDownloads = Math.max(0, Math.floor(finiteNonNegative(packageEntry.cleanedCompletedItemCount))); + const completedDownloads = cleanedCompletedDownloads + telemetry.items.filter((item) => item.status === "completed").length; const failedDownloads = telemetry.items.filter((item) => item.status === "failed"); const cancelledDownloads = telemetry.items.filter((item) => item.status === "cancelled").length; const failedArchives = archiveOperations.filter((operation) => operation.status === "failed").length; @@ -133,7 +134,12 @@ export function finalizePackageResult(telemetry: PackageTelemetry): PackageResul const audioStripFailures = Math.max(0, Math.floor(finiteNonNegative(packageEntry.audioStripSummary?.failed))); const remuxFailures = Math.max(failedRemuxOperations, audioStripFailures); const cleanupErrorCategory = String(telemetry.cleanupErrorCategory ?? packageEntry.cleanupErrorCategory ?? "").trim(); - const postProcessFailures = failedArchives + remuxFailures + (cleanupErrorCategory ? 1 : 0); + const downloadFailureCategories = failedDownloads.map((item) => + projectPackageFailureCategory("download", item.lastError || item.fullStatus) + ); + const offlineFailures = downloadFailureCategories.filter((category) => category === "Offline").length; + const cleanupFailures = cleanupErrorCategory ? 1 : 0; + const postProcessFailures = failedArchives + remuxFailures + cleanupFailures; const postProcessCancellations = cancelledArchives + cancelledRemuxOperations; const failedFiles = failedDownloads.length + postProcessFailures; const cancelledFiles = cancelledDownloads + postProcessCancellations; @@ -143,8 +149,10 @@ export function finalizePackageResult(telemetry: PackageTelemetry): PackageResul const postProcessStartedAt = finiteNonNegative(packageEntry.postProcessStartedAt); const postProcessCompletedAt = finiteNonNegative(packageEntry.postProcessCompletedAt); const completedAt = finiteNonNegative(packageEntry.terminalAt); - const downloadedBytes = telemetry.items.reduce((total, item) => total + finiteNonNegative(item.downloadedBytes), 0); - const totalBytes = telemetry.items.reduce((total, item) => total + finiteNonNegative(item.totalBytes ?? item.downloadedBytes), 0); + const downloadedBytes = finiteNonNegative(packageEntry.cleanedDownloadedBytes) + + telemetry.items.reduce((total, item) => total + finiteNonNegative(item.downloadedBytes), 0); + const totalBytes = finiteNonNegative(packageEntry.cleanedTotalBytes) + + telemetry.items.reduce((total, item) => total + finiteNonNegative(item.totalBytes ?? item.downloadedBytes), 0); const downloadDurationSeconds = durationSecondsBetween(startedAt, downloadEndedAt); const extractionDurationSeconds = sumOperationDurationSeconds(archiveOperations); const remuxDurationSeconds = sumOperationDurationSeconds(remuxOperations); @@ -177,6 +185,11 @@ export function finalizePackageResult(telemetry: PackageTelemetry): PackageResul successfulFiles, failedFiles, cancelledFiles, + downloadFailures: failedDownloads.length, + offlineFailures, + extractionFailures: failedArchives, + remuxFailures, + cleanupFailures, archiveCount: archiveOperations.length, partCount: archiveOperations.reduce((total, operation) => total + Math.max(0, Math.floor(finiteNonNegative(operation.partCount))), 0), outputCount: Math.max(0, Math.floor(finiteNonNegative(telemetry.outputCount ?? packageEntry.outputCount))), diff --git a/src/shared/types.ts b/src/shared/types.ts index 53eeee3..8f4506a 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -546,6 +546,11 @@ export interface PackageResult { successfulFiles: number; failedFiles: number; cancelledFiles: number; + downloadFailures: number; + offlineFailures: number; + extractionFailures: number; + remuxFailures: number; + cleanupFailures: number; archiveCount: number; partCount: number; outputCount: number; diff --git a/tests/notification-outbox.test.ts b/tests/notification-outbox.test.ts index 8a6802d..42cbabe 100644 --- a/tests/notification-outbox.test.ts +++ b/tests/notification-outbox.test.ts @@ -401,6 +401,11 @@ describe("NotificationOutbox", () => { successfulFiles: 0, failedFiles: 1, cancelledFiles: 0, + downloadFailures: 1, + offlineFailures: 0, + extractionFailures: 0, + remuxFailures: 0, + cleanupFailures: 0, archiveCount: 0, partCount: 0, outputCount: 0, diff --git a/tests/package-telemetry.test.ts b/tests/package-telemetry.test.ts index 1fb457f..9cbd4b0 100644 --- a/tests/package-telemetry.test.ts +++ b/tests/package-telemetry.test.ts @@ -11,6 +11,7 @@ import { durationSecondsBetween, finalizePackageResult } from "../src/main/package-telemetry"; +import { buildRunResult } from "../src/main/notification-events"; import { normalizeLoadedSession } from "../src/main/storage"; function packageEntry(overrides: Partial = {}): PackageEntry { @@ -174,6 +175,28 @@ describe("package lifecycle telemetry", () => { })); }); + it("includes immediately cleaned successes and bytes in a mixed package result", () => { + const failedItem = downloadItem("item-3", "failed"); + const result = finalizePackageResult(telemetry({ + package: packageEntry({ + status: "failed", + itemIds: [failedItem.id], + cleanedCompletedItemCount: 2, + cleanedDownloadedBytes: 2_000, + cleanedTotalBytes: 2_000 + }), + items: [failedItem] + })); + + expect(result).toEqual(expect.objectContaining({ + status: "partial", + successfulFiles: 2, + failedFiles: 1, + downloadedBytes: 2_000, + totalBytes: 3_000 + })); + }); + it("classifies a failed download without error text as a download-phase failure", () => { const item = { ...downloadItem("item-1", "failed"), lastError: "", fullStatus: "" }; const result = finalizePackageResult(telemetry({ @@ -319,6 +342,39 @@ describe("package lifecycle telemetry", () => { ]); }); + it("retains additive failure counters when cleanup is the primary failure phase", () => { + const failedItem = { ...downloadItem("item-1", "failed"), lastError: "Host ist offline" }; + const packageResult = finalizePackageResult(telemetry({ + items: [failedItem], + archiveOperations: [archiveOperation({ status: "failed", errorCategory: "archive-error" })], + remuxOperations: [remuxOperation({ status: "failed", errorCategory: "remux-error" })], + cleanupErrorCategory: "cleanup-error" + })); + const runResult = buildRunResult({ + id: "mixed-failures", + stopped: false, + startedAt: 1_000, + completedAt: 166_000, + packages: [packageResult] + }); + + expect(packageResult).toEqual(expect.objectContaining({ + failurePhase: "cleanup", + downloadFailures: 1, + offlineFailures: 1, + extractionFailures: 1, + remuxFailures: 1, + cleanupFailures: 1 + })); + expect(runResult).toEqual(expect.objectContaining({ + downloadFailures: 1, + offlineFailures: 1, + extractionFailures: 1, + remuxFailures: 1, + cleanupFailures: 1 + })); + }); + it("uses audio-strip outcomes when no individual remux operation was recorded", () => { const items = [downloadItem("item-1"), downloadItem("item-2")]; const result = finalizePackageResult(telemetry({