feat(history): complete package lifecycle telemetry

Persist separate download, extraction, remux, post-processing, and total durations together with accurate file, byte, archive, part, output, and phase failure results. Keep download item counts independent from extraction and remux outcomes, distinguish partial and cancelled packages, and expose additive failure counters and fixed categories in history and Discord notifications. Track archive operations across parallel work, aborts, restarts, nested multipart sets, and skipped split files while deriving generation outputs from privacy-safe signatures and merging overlapping extraction intervals.
This commit is contained in:
Sucukdeluxe
2026-08-24 20:59:44 +02:00
parent 7fe438ba5d
commit e8046f3fa3
16 changed files with 1139 additions and 150 deletions
+69 -7
View File
@@ -152,7 +152,7 @@ describe("extractor", () => {
expect(targets.has(other)).toBe(false);
});
it("extracts archives in natural episode order", async () => {
it("extracts archives in natural episode order", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-extract-"));
tempDirs.push(root);
const packageDir = path.join(root, "pkg");
@@ -194,8 +194,38 @@ describe("extractor", () => {
"Show.S01E01.zip",
"Show.S01E02.zip",
"Show.S01E10.zip"
]);
});
]);
});
it("includes the normalized archive path in archive progress updates", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-extract-progress-path-"));
tempDirs.push(root);
const packageDir = path.join(root, "pkg");
const targetDir = path.join(root, "out");
fs.mkdirSync(packageDir, { recursive: true });
const archivePath = path.join(packageDir, "episode.zip");
const zip = new AdmZip();
zip.addFile("episode.txt", Buffer.from("episode"));
zip.writeZip(archivePath);
const progressPaths: string[] = [];
await extractPackageArchives({
packageDir,
targetDir,
cleanupMode: "none",
conflictMode: "overwrite",
removeLinks: false,
removeSamples: false,
onProgress: (update) => {
if (update.archiveName === "episode.zip") {
progressPaths.push(String(update.archivePath || ""));
}
}
});
expect(progressPaths.length).toBeGreaterThan(0);
expect(new Set(progressPaths)).toEqual(new Set([path.resolve(archivePath)]));
});
it("deletes split zip companion parts when cleanup is enabled", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-extract-"));
@@ -849,7 +879,7 @@ describe("extractor", () => {
expect(targets.has(other)).toBe(false);
});
it("does NOT delete a non-archive .00x family that sits beside a real archive (no-signature data-loss guard)", async () => {
it("does NOT delete a non-archive .00x family that sits beside a real archive (no-signature data-loss guard)", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-split-noarch-"));
tempDirs.push(root);
const packageDir = path.join(root, "pkg");
@@ -880,9 +910,41 @@ describe("extractor", () => {
expect(fs.existsSync(path.join(targetDir, "release.txt"))).toBe(true);
expect(fs.existsSync(d001)).toBe(true);
expect(fs.existsSync(d002)).toBe(true);
expect(fs.existsSync(d003)).toBe(true);
});
});
expect(fs.existsSync(d003)).toBe(true);
});
it("emits terminal skipped progress for a generic split without an archive signature", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-split-skip-progress-"));
tempDirs.push(root);
const packageDir = path.join(root, "pkg");
const targetDir = path.join(root, "out");
fs.mkdirSync(packageDir, { recursive: true });
const archivePath = path.join(packageDir, "data.001");
fs.writeFileSync(archivePath, "plain data without archive signature", "utf8");
const progress: Array<{ archiveDone?: boolean; archiveSkipped?: boolean; archivePath?: string }> = [];
await extractPackageArchives({
packageDir,
targetDir,
cleanupMode: "none",
conflictMode: "overwrite",
removeLinks: false,
removeSamples: false,
onlyArchives: new Set([process.platform === "win32" ? path.resolve(archivePath).toLowerCase() : path.resolve(archivePath)]),
onProgress: (update) => {
if (update.archiveName === "data.001") {
progress.push(update);
}
}
});
expect(progress.at(-1)).toMatchObject({
archiveDone: true,
archiveSkipped: true,
archivePath: path.resolve(archivePath)
});
});
});
describe("detectArchiveSignature", () => {
it("detects RAR signature", async () => {