Fix stale extract pending states
This commit is contained in:
parent
ef26237b3e
commit
e6b8ea0abe
@ -589,6 +589,13 @@ function isExtractErrorLabel(statusText: string): boolean {
|
|||||||
|| /^entpacken\b.*\btimeout\b/i.test(text);
|
|| /^entpacken\b.*\btimeout\b/i.test(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isTransientExtractStatus(statusText: string): boolean {
|
||||||
|
const text = String(statusText || "").trim();
|
||||||
|
return /^entpacken\b/i.test(text)
|
||||||
|
|| /^passwort\b/i.test(text)
|
||||||
|
|| /^finalisieren\b/i.test(text);
|
||||||
|
}
|
||||||
|
|
||||||
function shouldAutoRetryExtraction(statusText: string): boolean {
|
function shouldAutoRetryExtraction(statusText: string): boolean {
|
||||||
return !isExtractedLabel(statusText) && !isExtractErrorLabel(statusText);
|
return !isExtractedLabel(statusText) && !isExtractErrorLabel(statusText);
|
||||||
}
|
}
|
||||||
@ -1102,11 +1109,13 @@ export function buildAutoRenameBaseNameFromFoldersWithOptions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function resolveArchiveItemsFromList(archiveName: string, items: DownloadItem[]): DownloadItem[] {
|
export function resolveArchiveItemsFromList(archiveName: string, items: DownloadItem[]): DownloadItem[] {
|
||||||
const entryLower = archiveName.toLowerCase();
|
const normalizeArchiveMatchName = (value: string): string =>
|
||||||
|
path.basename(String(value || "")).replace(/ \(\d+\)(?=\.[^.]+$)/, "");
|
||||||
|
const entryLower = normalizeArchiveMatchName(archiveName).toLowerCase();
|
||||||
|
|
||||||
// Helper: get item basename (try targetPath first, then fileName)
|
// Helper: get item basename (try targetPath first, then fileName)
|
||||||
const itemBaseName = (item: DownloadItem): string =>
|
const itemBaseName = (item: DownloadItem): string =>
|
||||||
path.basename(item.targetPath || item.fileName || "");
|
normalizeArchiveMatchName(item.targetPath || item.fileName || "");
|
||||||
|
|
||||||
// Try pattern-based matching first (for multipart archives)
|
// Try pattern-based matching first (for multipart archives)
|
||||||
let pattern: RegExp | null = null;
|
let pattern: RegExp | null = null;
|
||||||
@ -5296,9 +5305,11 @@ export class DownloadManager extends EventEmitter {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/^Entpacken\b/i.test(currentStatus) || /^Passwort\b/i.test(currentStatus) || /^Finalisieren\b/i.test(currentStatus)) {
|
if (isTransientExtractStatus(currentStatus)) {
|
||||||
const previousStatus = String(previousStatuses.get(entry.id) || "").trim();
|
const previousStatus = String(previousStatuses.get(entry.id) || "").trim();
|
||||||
entry.fullStatus = previousStatus || `Fertig (${humanSize(entry.downloadedBytes)})`;
|
entry.fullStatus = isTransientExtractStatus(previousStatus)
|
||||||
|
? `Fertig (${humanSize(entry.downloadedBytes)})`
|
||||||
|
: previousStatus || `Fertig (${humanSize(entry.downloadedBytes)})`;
|
||||||
entry.updatedAt = appliedAt;
|
entry.updatedAt = appliedAt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -9719,16 +9730,6 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.emitState();
|
this.emitState();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mark all items as pending before extraction starts
|
|
||||||
for (const entry of completedItems) {
|
|
||||||
if (!isExtractedLabel(entry.fullStatus)) {
|
|
||||||
preExtractStatuses.set(entry.id, String(entry.fullStatus || "").trim());
|
|
||||||
entry.fullStatus = "Entpacken - Ausstehend";
|
|
||||||
entry.updatedAt = nowMs();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.emitState();
|
|
||||||
|
|
||||||
const extractTimeoutMs = getPostExtractTimeoutMs();
|
const extractTimeoutMs = getPostExtractTimeoutMs();
|
||||||
const extractAbortController = new AbortController();
|
const extractAbortController = new AbortController();
|
||||||
let timedOut = false;
|
let timedOut = false;
|
||||||
@ -9774,6 +9775,23 @@ export class DownloadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fullArchiveSet = await this.findFullExtractArchiveSet(pkg, completedItems);
|
const fullArchiveSet = await this.findFullExtractArchiveSet(pkg, completedItems);
|
||||||
|
const fullExtractItemIds = new Set<string>();
|
||||||
|
for (const archivePath of fullArchiveSet) {
|
||||||
|
const archiveItems = resolveArchiveItems(path.basename(archivePath));
|
||||||
|
for (const entry of archiveItems) {
|
||||||
|
fullExtractItemIds.add(entry.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const pendingAt = nowMs();
|
||||||
|
for (const entry of completedItems) {
|
||||||
|
if (!fullExtractItemIds.has(entry.id) || isExtractedLabel(entry.fullStatus)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
preExtractStatuses.set(entry.id, String(entry.fullStatus || "").trim());
|
||||||
|
entry.fullStatus = "Entpacken - Ausstehend";
|
||||||
|
entry.updatedAt = pendingAt;
|
||||||
|
}
|
||||||
|
this.emitState();
|
||||||
const result = await extractPackageArchives({
|
const result = await extractPackageArchives({
|
||||||
packageDir: pkg.outputDir,
|
packageDir: pkg.outputDir,
|
||||||
targetDir: pkg.extractDir,
|
targetDir: pkg.extractDir,
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import http from "node:http";
|
|||||||
import { EventEmitter, once } from "node:events";
|
import { EventEmitter, once } from "node:events";
|
||||||
import AdmZip from "adm-zip";
|
import AdmZip from "adm-zip";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
import { DownloadManager, extractArchiveNameFromExtractorLogMessage, getAuthoritativeRealDebridTotal } from "../src/main/download-manager";
|
import { DownloadManager, extractArchiveNameFromExtractorLogMessage, getAuthoritativeRealDebridTotal, resolveArchiveItemsFromList } from "../src/main/download-manager";
|
||||||
import { defaultSettings } from "../src/main/constants";
|
import { defaultSettings } from "../src/main/constants";
|
||||||
import { parseDebridLinkApiKeys } from "../src/shared/debrid-link-keys";
|
import { parseDebridLinkApiKeys } from "../src/shared/debrid-link-keys";
|
||||||
import { getProviderUsageDayKey } from "../src/shared/provider-daily-limits";
|
import { getProviderUsageDayKey } from "../src/shared/provider-daily-limits";
|
||||||
@ -29,6 +29,20 @@ describe("extractArchiveNameFromExtractorLogMessage", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("resolveArchiveItemsFromList", () => {
|
||||||
|
it("includes duplicate-suffixed archive copies in multipart matches", () => {
|
||||||
|
const items = [
|
||||||
|
{ id: "dup-1", fileName: "show.s01e26.part1.rar" },
|
||||||
|
{ id: "dup-2", fileName: "show.s01e26.part1.rar", targetPath: "C:\\Downloads\\show.s01e26.part1 (1).rar" },
|
||||||
|
{ id: "dup-3", fileName: "show.s01e26.part2.rar" }
|
||||||
|
] as any[];
|
||||||
|
|
||||||
|
const resolved = resolveArchiveItemsFromList("show.s01e26.part1.rar", items);
|
||||||
|
|
||||||
|
expect(resolved.map((item) => item.id)).toEqual(["dup-1", "dup-2", "dup-3"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
async function waitFor(predicate: () => boolean, timeoutMs = 15000): Promise<void> {
|
async function waitFor(predicate: () => boolean, timeoutMs = 15000): Promise<void> {
|
||||||
const started = Date.now();
|
const started = Date.now();
|
||||||
while (!predicate()) {
|
while (!predicate()) {
|
||||||
@ -3904,6 +3918,54 @@ describe("download manager", () => {
|
|||||||
expect(completedItems[3].fullStatus).toBe("Fertig (200 MB)");
|
expect(completedItems[3].fullStatus).toBe("Fertig (200 MB)");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("clears stale pending extraction labels for untouched items when another archive fails", () => {
|
||||||
|
const createdAt = Date.now() - 10_000;
|
||||||
|
const completedItems = [
|
||||||
|
{
|
||||||
|
id: "stale-fail-item-1",
|
||||||
|
status: "completed",
|
||||||
|
fileName: "show.s01e01.part1.rar",
|
||||||
|
downloadedBytes: 100 * 1024 * 1024,
|
||||||
|
fullStatus: "Fertig (100 MB)",
|
||||||
|
updatedAt: createdAt
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "stale-fail-item-2",
|
||||||
|
status: "completed",
|
||||||
|
fileName: "show.s01e01.part2.rar",
|
||||||
|
downloadedBytes: 100 * 1024 * 1024,
|
||||||
|
fullStatus: "Fertig (100 MB)",
|
||||||
|
updatedAt: createdAt
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "stale-fail-item-3",
|
||||||
|
status: "completed",
|
||||||
|
fileName: "show.s01e05.part2.rar",
|
||||||
|
downloadedBytes: 180 * 1024 * 1024,
|
||||||
|
fullStatus: "Entpacken - Ausstehend",
|
||||||
|
updatedAt: createdAt
|
||||||
|
}
|
||||||
|
] as any[];
|
||||||
|
const previousStatuses = new Map<string, string>(completedItems.map((item: any) => [item.id, item.fullStatus]));
|
||||||
|
|
||||||
|
completedItems[0].fullStatus = "Entpacken - Error";
|
||||||
|
completedItems[1].fullStatus = "Entpacken - Error";
|
||||||
|
|
||||||
|
(DownloadManager.prototype as any).applyPackageExtractFailureStatuses.call(
|
||||||
|
{},
|
||||||
|
completedItems,
|
||||||
|
(archiveName: string) => resolveArchiveItemsFromList(archiveName, completedItems),
|
||||||
|
new Map([["show.s01e01.part1.rar", "Checksum error in the encrypted file"]]),
|
||||||
|
"Checksum error in the encrypted file",
|
||||||
|
previousStatuses,
|
||||||
|
createdAt + 5_000
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(completedItems[0].fullStatus).toBe("Entpack-Fehler: Checksum error in the encrypted file");
|
||||||
|
expect(completedItems[1].fullStatus).toBe("Entpack-Fehler: Checksum error in the encrypted file");
|
||||||
|
expect(completedItems[2].fullStatus).toBe("Fertig (180 MB)");
|
||||||
|
});
|
||||||
|
|
||||||
it("detects start conflicts when extract output already exists", async () => {
|
it("detects start conflicts when extract output already exists", async () => {
|
||||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||||
tempDirs.push(root);
|
tempDirs.push(root);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user