Fix: Start-Konflikt-Waechter friert nach Update nicht mehr die ganze Liste ein
Nach einem Auto-Update-Neustart blieb die komplette Warteschlange stehen
(running=false, hunderte Items wartend), obwohl Auto-Resume aktiv war.
Ursache war der Start-Konflikt-Waechter:
- getStartConflicts() meldete jedes Paket als Konflikt, dessen
paketspezifischer Entpack-Ordner bereits Dateien enthielt. Ein nur
teilweise heruntergeladenes Paket hat dort aber seine EIGENEN fertigen
Teile liegen (z.B. 15 von 26 fertig und entpackt) - das ist kein
Konflikt, sondern Wiederaufnahme.
- Der Auto-Resume-Waechter war binaer: ein einziger gemeldeter Konflikt
hat ALLE Pakete blockiert ("Auto-Resume uebersprungen: Start-Konflikte
erkannt"). Zwei teilfertige Pakete haben so 30 Pakete eingefroren.
Fix:
- Diskriminator in getStartConflicts(): ein Paket mit eigenen fertigen
("completed") Items zaehlt nicht mehr als Konflikt - die Dateien im
Entpack-Ordner sind seine eigene Ausgabe, kein Fremdbestand.
- Selektives Auto-Resume: start() akzeptiert excludePackageIds und laesst
diese Pakete aus dem Lauf-Set (runPackageIds, auf das der Scheduler
filtert). beginAutoResume() startet die sauberen Pakete und haelt nur
echte Konflikte zurueck, statt alles zu blockieren; die zurueckgehaltenen
Pakete werden im Log namentlich genannt.
Die beiden Aenderungen greifen ineinander: der Diskriminator garantiert,
dass ein als Konflikt gemeldetes Paket keine fertigen Teile hat, also auch
keine ausstehende Nachbearbeitung - das defensive runPackageIds.delete kann
daher keine Entpack-Arbeit haengen lassen.
Tests: Diskriminator (eigene Ausgabe nicht geflaggt / Fremdbestand
geflaggt) plus selektives Resume (ausgeschlossenes Paket bleibt aus dem
Lauf-Set, laeuft nicht an).
This commit is contained in:
@@ -12244,3 +12244,121 @@ describe("download manager", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("start conflict guard + selective resume", () => {
|
||||
function makeItem(id: string, packageId: string, status: string, fileName: string): any {
|
||||
return {
|
||||
id, packageId, url: `https://hoster.example/${id}`, provider: "realdebrid",
|
||||
status, retries: 0, speedBps: 0, downloadedBytes: status === "completed" ? 100 : 0,
|
||||
totalBytes: status === "completed" ? 100 : null, progressPercent: status === "completed" ? 100 : 0,
|
||||
fileName, targetPath: "", resumable: true, attempts: 0, lastError: "", fullStatus: "",
|
||||
createdAt: Date.now(), updatedAt: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
it("does not flag a partially-downloaded package whose extract dir holds its own completed output", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-startconflict-own-"));
|
||||
tempDirs.push(root);
|
||||
const storagePaths = createStoragePaths(path.join(root, "state"));
|
||||
initPackageLogs(storagePaths.baseDir);
|
||||
initItemLogs(storagePaths.baseDir);
|
||||
|
||||
const session = emptySession();
|
||||
const packageId = "pkg-own-output";
|
||||
const extractDir = path.join(root, "extract", "OwnOutput");
|
||||
fs.mkdirSync(extractDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(extractDir, "episode01.mkv"), Buffer.alloc(64, 7));
|
||||
session.packageOrder = [packageId];
|
||||
session.packages[packageId] = {
|
||||
id: packageId, name: "OwnOutput",
|
||||
outputDir: path.join(root, "downloads", "OwnOutput"), extractDir,
|
||||
status: "queued", itemIds: ["own-done", "own-pending"], cancelled: false, enabled: true,
|
||||
createdAt: Date.now(), updatedAt: Date.now()
|
||||
} as any;
|
||||
session.items["own-done"] = makeItem("own-done", packageId, "completed", "done.rar");
|
||||
session.items["own-pending"] = makeItem("own-pending", packageId, "queued", "pending.rar");
|
||||
|
||||
const manager = new DownloadManager(
|
||||
{ ...defaultSettings(), token: "rd-token", outputDir: path.join(root, "downloads"), extractDir: path.join(root, "extract") },
|
||||
session, storagePaths
|
||||
);
|
||||
|
||||
const conflicts = await manager.getStartConflicts();
|
||||
expect(conflicts.map((c) => c.packageId)).not.toContain(packageId);
|
||||
});
|
||||
|
||||
it("flags a fresh package when its package-specific extract dir already holds files and it has no completed items", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-startconflict-fresh-"));
|
||||
tempDirs.push(root);
|
||||
const storagePaths = createStoragePaths(path.join(root, "state"));
|
||||
initPackageLogs(storagePaths.baseDir);
|
||||
initItemLogs(storagePaths.baseDir);
|
||||
|
||||
const session = emptySession();
|
||||
const packageId = "pkg-fresh-conflict";
|
||||
const extractDir = path.join(root, "extract", "FreshConflict");
|
||||
fs.mkdirSync(extractDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(extractDir, "old-from-previous-run.mkv"), Buffer.alloc(64, 9));
|
||||
session.packageOrder = [packageId];
|
||||
session.packages[packageId] = {
|
||||
id: packageId, name: "FreshConflict",
|
||||
outputDir: path.join(root, "downloads", "FreshConflict"), extractDir,
|
||||
status: "queued", itemIds: ["fresh-pending"], cancelled: false, enabled: true,
|
||||
createdAt: Date.now(), updatedAt: Date.now()
|
||||
} as any;
|
||||
session.items["fresh-pending"] = makeItem("fresh-pending", packageId, "queued", "fresh.rar");
|
||||
|
||||
const manager = new DownloadManager(
|
||||
{ ...defaultSettings(), token: "rd-token", outputDir: path.join(root, "downloads"), extractDir: path.join(root, "extract") },
|
||||
session, storagePaths
|
||||
);
|
||||
|
||||
const conflicts = await manager.getStartConflicts();
|
||||
expect(conflicts.map((c) => c.packageId)).toContain(packageId);
|
||||
});
|
||||
|
||||
it("start() holds excluded packages out of the run set and runs the rest", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-selective-resume-"));
|
||||
tempDirs.push(root);
|
||||
const storagePaths = createStoragePaths(path.join(root, "state"));
|
||||
initPackageLogs(storagePaths.baseDir);
|
||||
initItemLogs(storagePaths.baseDir);
|
||||
|
||||
const session = emptySession();
|
||||
const runId = "pkg-run";
|
||||
const holdId = "pkg-hold";
|
||||
fs.mkdirSync(path.join(root, "downloads", "RunMe"), { recursive: true });
|
||||
fs.mkdirSync(path.join(root, "downloads", "HoldMe"), { recursive: true });
|
||||
session.packageOrder = [runId, holdId];
|
||||
session.packages[runId] = {
|
||||
id: runId, name: "RunMe",
|
||||
outputDir: path.join(root, "downloads", "RunMe"), extractDir: path.join(root, "extract", "RunMe"),
|
||||
status: "queued", itemIds: ["run-item"], cancelled: false, enabled: true,
|
||||
createdAt: Date.now(), updatedAt: Date.now()
|
||||
} as any;
|
||||
session.packages[holdId] = {
|
||||
id: holdId, name: "HoldMe",
|
||||
outputDir: path.join(root, "downloads", "HoldMe"), extractDir: path.join(root, "extract", "HoldMe"),
|
||||
status: "queued", itemIds: ["hold-item"], cancelled: false, enabled: true,
|
||||
createdAt: Date.now(), updatedAt: Date.now()
|
||||
} as any;
|
||||
session.items["run-item"] = makeItem("run-item", runId, "queued", "run.rar");
|
||||
session.items["hold-item"] = makeItem("hold-item", holdId, "queued", "hold.rar");
|
||||
|
||||
const manager = new DownloadManager(
|
||||
{ ...defaultSettings(), token: "rd-token", maxParallel: 2, outputDir: path.join(root, "downloads"), extractDir: path.join(root, "extract") },
|
||||
session, storagePaths
|
||||
);
|
||||
(manager as any).debridService.unrestrictLink = () => new Promise(() => {});
|
||||
|
||||
await manager.start({ excludePackageIds: new Set([holdId]) });
|
||||
|
||||
expect((manager as any).runPackageIds.has(runId)).toBe(true);
|
||||
expect((manager as any).runPackageIds.has(holdId)).toBe(false);
|
||||
expect((manager as any).runItemIds.has("run-item")).toBe(true);
|
||||
expect((manager as any).runItemIds.has("hold-item")).toBe(false);
|
||||
expect(session.items["hold-item"].status).toBe("queued");
|
||||
|
||||
manager.stop();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user