release: prepare v2.0.83
This commit is contained in:
@@ -4,6 +4,16 @@ All notable changes to Multi-Debrid Downloader are documented in this file.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2.0.83] - 2026-09-01
|
||||
|
||||
### Bounded CRC recovery
|
||||
|
||||
- Treat an extractor-confirmed CRC failure as a damaged download even when the file size and archive signature are valid.
|
||||
- Discard every affected archive part, clear the previous provider assignment, and request a fresh direct link for one clean automatic redownload.
|
||||
- Persist the one-redownload limit across application restarts so an upstream service repeatedly returning the same damaged content cannot create an endless download loop.
|
||||
- Clear the recovery limit on a manual item or package reset while continuing to leave valid signed archives untouched for wrong-password failures.
|
||||
- Keep extraction, password discovery, and extractor fallback behavior unchanged.
|
||||
|
||||
## [2.0.82] - 2026-09-01
|
||||
|
||||
### Startup work folders
|
||||
|
||||
+17
-5
@@ -10,12 +10,12 @@ Diese Datei hält den verifizierten technischen Arbeitsstand fest. Sie enthält
|
||||
|
||||
- Verifiziert am: 1. September 2026, Europe/Berlin
|
||||
- Lokaler Pfad: `C:\Users\Sascha\Desktop\Claude & ChatGPT Projekte\Multi-Debrid-Downloader`
|
||||
- Arbeitsbranch: `release/v2.0.82`
|
||||
- Quellbasis: `release/v2.0.81`
|
||||
- Arbeitsbranch: `release/v2.0.83`
|
||||
- Quellbasis: `release/v2.0.82`
|
||||
- Release-Tag: `v2.0.82`
|
||||
- Baseline-Commit: `d58cfba817bd73d015c3c295e593082becc3211f`
|
||||
- Hotfix-Basis: `5cee459d0bcc5c19d8d6483948a036a43c9d91b1`
|
||||
- Paketversion: `2.0.82`
|
||||
- Baseline-Commit: `f3f2f60ece9322bfaddeb8f47d07830bb8fe273a`
|
||||
- Hotfix-Basis: `f3f2f60ece9322bfaddeb8f47d07830bb8fe273a`
|
||||
- Paketversion: `2.0.83`
|
||||
- Letztes Release: `Multi-Debrid-Downloader v2.0.82`, veröffentlicht am 1. September 2026 auf GitHub und Forgejo
|
||||
- Runtime-Voraussetzung: Node.js `>=20`; lokal verifiziert mit Node.js `24.19.0` und npm `11.17.0`
|
||||
|
||||
@@ -183,6 +183,15 @@ Diese Datei hält den verifizierten technischen Arbeitsstand fest. Sie enthält
|
||||
- „Sitzung“ und „Verbleibend“ stammen weiterhin aus demselben 750-Millisekunden-Snapshot. Bei Restmengen ab einem TiB wurde die sichtbare TB-Präzision von vier auf fünf Nachkommastellen erhöht: Ein Anzeigeschritt sinkt dadurch von rund 105 MiB auf rund 10,5 MiB und bleibt auch bei großen Queues sichtbar im gleichen Takt.
|
||||
- Download-, Entpack-, Passwort- und Nachbearbeitungslogik wurden nicht verändert.
|
||||
|
||||
## Änderungen in v2.0.83
|
||||
|
||||
- Die bereitgestellte intakte RAR-Datei und die MDD-Datei besitzen beide exakt 808.969.900 Byte, aber unterschiedliche SHA-256-Werte. UnRAR bestätigt ausschließlich in der MDD-Datei einen Prüfsummenfehler; 8.197 Bytes unterscheiden sich in einem kompakten Bereich ab Byte 106.576.
|
||||
- Eine direkte Range-Anfrage ohne MDD und ohne Proxy an den von Real-Debrid erzeugten Direktlink lieferte exakt die fehlerhaften Bytes der MDD-Datei. Damit sind Proxy-Segmentierung, lokaler Schreibpfad und Extractor als Erzeuger dieses konkreten Schadens ausgeschlossen; der aktive Real-Debrid-Direktlink selbst lieferte beschädigten Inhalt.
|
||||
- Der bisherige Recovery-Guard setzte Dateien mit korrekter Größe und gültigem RAR-Kopf trotz eines durch beide Extractor-Pfade bestätigten CRC-Fehlers nicht neu in die Queue. Eine gültige Signatur schützt jedoch nur den Archivkopf und erkennt beschädigte Nutzdaten nicht.
|
||||
- Ein entsprechend bestätigter CRC-Fehler verwirft nun alle betroffenen Archivteile, setzt Größe und Providerbindung zurück und fordert genau einen vollständigen Neudownload über einen frisch erzeugten Direktlink an.
|
||||
- Der Einmal-Schutz wird pro Item in der Session gespeichert und überlebt einen Programmneustart. Liefert der Upstream erneut einen CRC-Fehler, bleibt der zweite Fehler sichtbar, statt eine Endlosschleife zu erzeugen. Ein manueller Item- oder Paket-Reset löscht den Schutz und erlaubt einen neuen bewussten Versuch.
|
||||
- Echte Passwortfehler bei Dateien mit korrekter Größe und gültiger Archivsignatur lösen weiterhin keinen unnötigen Neudownload aus. Die Entpack-, Passwort- und Fallbacklogik selbst wurde nicht verändert.
|
||||
|
||||
## Start-, Build- und Testbefehle
|
||||
|
||||
```powershell
|
||||
@@ -214,6 +223,9 @@ npm exec -- tsc --noEmit
|
||||
|
||||
## Verifizierungen vom 1. September 2026
|
||||
|
||||
- Kandidat für `v2.0.83`: 392 von 392 fokussierten Download-Manager- und Storage-Tests erfolgreich. Die neuen Regressionen beweisen den einmaligen Neudownload bei CRC trotz gültiger RAR-Signatur, den persistenten Schutz vor einem zweiten automatischen Neudownload, die unveränderte Passwortfehler-Abgrenzung sowie das Löschen des Schutzes beim manuellen Reset.
|
||||
- Vollständiger Clientlauf des Kandidaten: 143 Testdateien erfolgreich, 1 optionale JVM-Testdatei übersprungen; 2.725 Tests erfolgreich und 4 übersprungen. Nur die zwei bekannten Windows-Symlink-Fixtures endeten mangels Berechtigung vor ihrer Produktassertion mit `EPERM`. Alle 265 Download-Manager- und 84 Extractor-Tests sind vollständig grün.
|
||||
- TypeScript-Prüfung, Main-Build, Renderer-Produktionsbuild, Node-Self-Check und alle 16 Backup-API-Tests sind erfolgreich. Die bekannte Vite-Warnung betrifft weiterhin den rund 581 KiB großen Renderer-Chunk.
|
||||
- Kandidat für `v2.0.82`: TypeScript-Prüfung und 616 fokussierte Ordner-, Storage-, Renderer-, Einstellungs-, Übersetzungs-, Account- und Downloadansichtstests erfolgreich. Die Ordnerregressionen bestätigen den ausgeschalteten Standard, selektive Zielerstellung, den Erhalt vorhandener Dateien und die Fehlerisolation zwischen Zielen.
|
||||
- Vollständiger Clientlauf: 143 Testdateien erfolgreich, 1 optionale JVM-Testdatei übersprungen; 2.725 Tests erfolgreich und 4 übersprungen. Nur die zwei bekannten Windows-Symlink-Fixtures endeten mangels Berechtigung vor ihrer Produktassertion mit `EPERM`. Die unveränderten 265 Download-Manager- und 84 Entpacktests sind vollständig grün.
|
||||
- Backup-API: 16 von 16 Tests erfolgreich.
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "multi-debrid-downloader",
|
||||
"version": "2.0.82",
|
||||
"version": "2.0.83",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "multi-debrid-downloader",
|
||||
"version": "2.0.82",
|
||||
"version": "2.0.83",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"adm-zip": "0.6.0",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "multi-debrid-downloader",
|
||||
"version": "2.0.82",
|
||||
"version": "2.0.83",
|
||||
"description": "Desktop downloader",
|
||||
"main": "build/main/main/main.js",
|
||||
"author": "Sucukdeluxe",
|
||||
|
||||
@@ -3704,6 +3704,7 @@ export class DownloadManager extends EventEmitter {
|
||||
item.progressPercent = 0;
|
||||
item.resumable = true;
|
||||
item.attempts = 0;
|
||||
delete item.archiveRecoveryRedownloads;
|
||||
item.lastError = "";
|
||||
item.fullStatus = "Wartet";
|
||||
item.provider = null;
|
||||
@@ -6068,6 +6069,7 @@ export class DownloadManager extends EventEmitter {
|
||||
item.speedBps = 0;
|
||||
item.attempts = 0;
|
||||
item.retries = 0;
|
||||
delete item.archiveRecoveryRedownloads;
|
||||
item.lastError = "";
|
||||
item.resumable = true;
|
||||
item.targetPath = "";
|
||||
@@ -6150,6 +6152,7 @@ export class DownloadManager extends EventEmitter {
|
||||
item.speedBps = 0;
|
||||
item.attempts = 0;
|
||||
item.retries = 0;
|
||||
delete item.archiveRecoveryRedownloads;
|
||||
item.lastError = "";
|
||||
item.resumable = true;
|
||||
item.targetPath = "";
|
||||
@@ -7821,42 +7824,64 @@ export class DownloadManager extends EventEmitter {
|
||||
|
||||
const inspectedArchiveItems = archiveItems
|
||||
.map((item) => ({ item, state: inspectPackageItemDiskState(pkg, item) }));
|
||||
const recoveryRedownloads = Math.max(
|
||||
0,
|
||||
...archiveItems.map((item) => Math.max(0, Math.floor(Number(item.archiveRecoveryRedownloads) || 0)))
|
||||
);
|
||||
if (recoveryRedownloads >= 1) {
|
||||
logger.warn(
|
||||
`Auto-Recovery (${scope}): ${failure.archiveName} nicht erneut heruntergeladen - ` +
|
||||
`automatischer Archiv-Neudownload bereits versucht`
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const corruptArchiveItems = inspectedArchiveItems
|
||||
.filter(({ state }) => state.reason !== "ok");
|
||||
|
||||
if (corruptArchiveItems.length === 0) {
|
||||
const firstPart = inspectedArchiveItems.find(({ state }) => state.diskPath);
|
||||
let hasValidSignature = false;
|
||||
if (firstPart?.state.diskPath) {
|
||||
try {
|
||||
const fd = fs.openSync(firstPart.state.diskPath, "r");
|
||||
try {
|
||||
const header = Buffer.alloc(8);
|
||||
fs.readSync(fd, header, 0, 8, 0);
|
||||
hasValidSignature =
|
||||
(header[0] === 0x52 && header[1] === 0x61 && header[2] === 0x72 && header[3] === 0x21 && header[4] === 0x1a && header[5] === 0x07) ||
|
||||
(header[0] === 0x37 && header[1] === 0x7a && header[2] === 0xbc && header[3] === 0xaf) ||
|
||||
(header[0] === 0x50 && header[1] === 0x4b && header[2] === 0x03 && header[3] === 0x04);
|
||||
} finally {
|
||||
fs.closeSync(fd);
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
|
||||
if (hasValidSignature) {
|
||||
if (failure.category === "crc_error") {
|
||||
logger.warn(
|
||||
`Auto-Recovery (${scope}): ${failure.archiveName} uebersprungen - ` +
|
||||
`Dateien haben korrekte Groesse und gueltige Archiv-Signatur, ` +
|
||||
`wahrscheinlicher Passwort-/Extractor-Fall statt defektem Download`
|
||||
`Auto-Recovery (${scope}): ${failure.archiveName} meldet CRC-Fehler trotz korrekter Größe und Archiv-Signatur, ` +
|
||||
`erzwinge einmaligen Neu-Download aller ${archiveItems.length} Parts`
|
||||
);
|
||||
return 0;
|
||||
corruptArchiveItems.push(...inspectedArchiveItems);
|
||||
}
|
||||
|
||||
logger.warn(
|
||||
`Auto-Recovery (${scope}): ${failure.archiveName} - Dateien korrekte Groesse aber ungueltige Archiv-Signatur, ` +
|
||||
`erzwinge Re-Download aller ${archiveItems.length} Parts`
|
||||
);
|
||||
corruptArchiveItems.push(...inspectedArchiveItems);
|
||||
if (failure.category !== "crc_error") {
|
||||
const firstPart = inspectedArchiveItems.find(({ state }) => state.diskPath);
|
||||
let hasValidSignature = false;
|
||||
if (firstPart?.state.diskPath) {
|
||||
try {
|
||||
const fd = fs.openSync(firstPart.state.diskPath, "r");
|
||||
try {
|
||||
const header = Buffer.alloc(8);
|
||||
fs.readSync(fd, header, 0, 8, 0);
|
||||
hasValidSignature =
|
||||
(header[0] === 0x52 && header[1] === 0x61 && header[2] === 0x72 && header[3] === 0x21 && header[4] === 0x1a && header[5] === 0x07) ||
|
||||
(header[0] === 0x37 && header[1] === 0x7a && header[2] === 0xbc && header[3] === 0xaf) ||
|
||||
(header[0] === 0x50 && header[1] === 0x4b && header[2] === 0x03 && header[3] === 0x04);
|
||||
} finally {
|
||||
fs.closeSync(fd);
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
|
||||
if (hasValidSignature) {
|
||||
logger.warn(
|
||||
`Auto-Recovery (${scope}): ${failure.archiveName} übersprungen - ` +
|
||||
`Dateien haben korrekte Größe und gültige Archiv-Signatur, ` +
|
||||
`wahrscheinlicher Passwort-/Extractor-Fall statt defektem Download`
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
|
||||
logger.warn(
|
||||
`Auto-Recovery (${scope}): ${failure.archiveName} - Dateien korrekte Größe aber ungültige Archiv-Signatur, ` +
|
||||
`erzwinge Neu-Download aller ${archiveItems.length} Parts`
|
||||
);
|
||||
corruptArchiveItems.push(...inspectedArchiveItems);
|
||||
}
|
||||
}
|
||||
|
||||
const queuedAt = nowMs();
|
||||
@@ -7875,9 +7900,15 @@ export class DownloadManager extends EventEmitter {
|
||||
item.targetPath = "";
|
||||
item.status = "queued";
|
||||
item.attempts = 0;
|
||||
item.archiveRecoveryRedownloads = recoveryRedownloads + 1;
|
||||
item.downloadedBytes = 0;
|
||||
item.totalBytes = null;
|
||||
item.progressPercent = 0;
|
||||
item.speedBps = 0;
|
||||
item.provider = null;
|
||||
item.providerLabel = undefined;
|
||||
item.providerAccountId = undefined;
|
||||
item.providerAccountLabel = undefined;
|
||||
item.lastError = failure.errorText;
|
||||
item.fullStatus = reason;
|
||||
item.updatedAt = queuedAt;
|
||||
|
||||
@@ -1040,6 +1040,9 @@ export function normalizeLoadedSession(raw: unknown): SessionState {
|
||||
metadataRenameTargetPath: asText(item.metadataRenameTargetPath) || undefined,
|
||||
resumable: item.resumable === undefined ? true : Boolean(item.resumable),
|
||||
attempts: clampNumber(item.attempts, 0, 0, 10_000),
|
||||
archiveRecoveryRedownloads: item.archiveRecoveryRedownloads === undefined
|
||||
? undefined
|
||||
: clampNumber(item.archiveRecoveryRedownloads, 0, 0, 1),
|
||||
lastError: asText(item.lastError),
|
||||
fullStatus: asText(item.fullStatus),
|
||||
onlineStatus: VALID_ONLINE_STATUSES.has(onlineStatusRaw) ? onlineStatusRaw as "online" | "offline" | "checking" : undefined,
|
||||
|
||||
@@ -493,6 +493,7 @@ export interface DownloadItem {
|
||||
metadataRenameTargetPath?: string;
|
||||
resumable: boolean;
|
||||
attempts: number;
|
||||
archiveRecoveryRedownloads?: number;
|
||||
lastError: string;
|
||||
fullStatus: string;
|
||||
createdAt: number;
|
||||
|
||||
@@ -6513,7 +6513,7 @@ describe("download manager", () => {
|
||||
expect(fs.existsSync(path.join(outputDir, archiveNames[1]!))).toBe(false);
|
||||
});
|
||||
|
||||
it("does not requeue archive parts on CRC error when file has valid RAR signature (wrong password)", () => {
|
||||
it("requeues archive parts once on CRC error even when size and RAR signature are valid", () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||
tempDirs.push(root);
|
||||
|
||||
@@ -6594,12 +6594,71 @@ describe("download manager", () => {
|
||||
"hybrid"
|
||||
);
|
||||
|
||||
expect(changed).toBe(0);
|
||||
expect(changed).toBe(2);
|
||||
for (const itemId of itemIds) {
|
||||
const item = session.items[itemId]!;
|
||||
expect(item.status).toBe("completed");
|
||||
expect(item.targetPath).toContain(".rar");
|
||||
expect(item.downloadedBytes).toBe(archiveSize);
|
||||
expect(item.status).toBe("queued");
|
||||
expect(item.targetPath).toBe("");
|
||||
expect(item.downloadedBytes).toBe(0);
|
||||
expect(item.totalBytes).toBeNull();
|
||||
expect(item.archiveRecoveryRedownloads).toBe(1);
|
||||
expect(item.provider).toBeNull();
|
||||
}
|
||||
|
||||
for (const [index, archiveName] of archiveNames.entries()) {
|
||||
const targetPath = path.join(outputDir, archiveName);
|
||||
const content = Buffer.alloc(archiveSize, 0);
|
||||
Buffer.from([0x52, 0x61, 0x72, 0x21, 0x1a, 0x07, 0x01, 0x00]).copy(content);
|
||||
fs.writeFileSync(targetPath, content);
|
||||
const item = session.items[itemIds[index]!]!;
|
||||
item.status = "completed";
|
||||
item.targetPath = targetPath;
|
||||
item.downloadedBytes = archiveSize;
|
||||
item.totalBytes = archiveSize;
|
||||
item.progressPercent = 100;
|
||||
item.provider = "realdebrid";
|
||||
}
|
||||
|
||||
const repeatedChanged = (manager as any).autoRecoverArchiveCrcFailure(
|
||||
session.packages[packageId],
|
||||
itemIds.map((itemId) => session.items[itemId]!),
|
||||
{
|
||||
archiveName: "show.s01e01.part1.rar",
|
||||
errorText: "Checksum error after redownload",
|
||||
category: "crc_error",
|
||||
suggestRedownload: true,
|
||||
jvmFailureReason: "7z-Fehler: CRCERROR"
|
||||
},
|
||||
"full"
|
||||
);
|
||||
|
||||
expect(repeatedChanged).toBe(0);
|
||||
for (const [index, itemId] of itemIds.entries()) {
|
||||
expect(session.items[itemId]?.status).toBe("completed");
|
||||
expect(session.items[itemId]?.archiveRecoveryRedownloads).toBe(1);
|
||||
expect(fs.existsSync(path.join(outputDir, archiveNames[index]!))).toBe(true);
|
||||
}
|
||||
|
||||
for (const itemId of itemIds) {
|
||||
delete session.items[itemId]?.archiveRecoveryRedownloads;
|
||||
}
|
||||
const wrongPasswordChanged = (manager as any).autoRecoverArchiveCrcFailure(
|
||||
session.packages[packageId],
|
||||
itemIds.map((itemId) => session.items[itemId]!),
|
||||
{
|
||||
archiveName: "show.s01e01.part1.rar",
|
||||
errorText: "Wrong password",
|
||||
category: "wrong_password",
|
||||
suggestRedownload: true,
|
||||
jvmFailureReason: "Can not open encrypted archive"
|
||||
},
|
||||
"full"
|
||||
);
|
||||
|
||||
expect(wrongPasswordChanged).toBe(0);
|
||||
for (const itemId of itemIds) {
|
||||
expect(session.items[itemId]?.status).toBe("completed");
|
||||
expect(session.items[itemId]?.archiveRecoveryRedownloads).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -8840,6 +8899,7 @@ describe("download manager", () => {
|
||||
targetPath: path.join(root, "downloads", "reset-extraction", `${itemId}.part1.rar`),
|
||||
resumable: true,
|
||||
attempts: 1,
|
||||
archiveRecoveryRedownloads: 1,
|
||||
lastError: "Unerwartetes Dateiende",
|
||||
fullStatus: `Entpack-Fehler [${itemId}.part1.rar]: Unerwartetes Dateiende`,
|
||||
onlineStatus: "online",
|
||||
@@ -8876,6 +8936,7 @@ describe("download manager", () => {
|
||||
fullStatus: "Wartet",
|
||||
onlineStatus: "online"
|
||||
}));
|
||||
expect(snapshot.items[itemId].archiveRecoveryRedownloads).toBeUndefined();
|
||||
}
|
||||
|
||||
await manager.resetPackage(packageId);
|
||||
|
||||
@@ -138,6 +138,7 @@ describe("settings storage", () => {
|
||||
status: "completed",
|
||||
fileName: "deepbrid.bin",
|
||||
targetPath: path.join(outputDir, "deepbrid.bin"),
|
||||
archiveRecoveryRedownloads: 99,
|
||||
createdAt: 1,
|
||||
updatedAt: 2
|
||||
}
|
||||
@@ -151,6 +152,7 @@ describe("settings storage", () => {
|
||||
}, 0);
|
||||
|
||||
expect(session.items["item-deepbrid"]?.provider).toBe("deepbrid");
|
||||
expect(session.items["item-deepbrid"]?.archiveRecoveryRedownloads).toBe(1);
|
||||
expect(session.packages["pkg-deepbrid"]?.cleanedProviders).toEqual(["deepbrid"]);
|
||||
expect(history?.provider).toBe("deepbrid");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user