Release v1.4.33 with DLC import and stats hotfixes
This commit is contained in:
parent
ff1036563a
commit
df3600669e
30
CHANGELOG.md
30
CHANGELOG.md
@ -2,6 +2,36 @@
|
||||
|
||||
Alle nennenswerten Aenderungen werden in dieser Datei dokumentiert.
|
||||
|
||||
## 1.4.33 - 2026-03-02
|
||||
|
||||
Hotfix-Release fuer zwei reale Produktionsprobleme: falsche Gesamt-Statistik bei leerer Queue und stilles DLC-Import-Failure bei Drag-and-Drop.
|
||||
|
||||
### Fixes
|
||||
|
||||
- **Stats-Anzeige korrigiert ("Gesamt" bei leerer Queue):**
|
||||
- Wenn keine Pakete/Items mehr vorhanden sind, werden persistierte Run-Bytes und Run-Timestamps jetzt sauber auf 0 zurueckgesetzt.
|
||||
- Dadurch verschwindet die Ghost-Anzeige wie z. B. `Gesamt: 19.99 GB` bei `Pakete: 0 / Dateien: 0`.
|
||||
- Reset greift in den relevanten Pfaden (`getStats`, `clearAll`, Paket-Entfernung, Startup-Normalisierung).
|
||||
|
||||
- **DLC Drag-and-Drop Import gehaertet:**
|
||||
- Lokale DLC-Fehler wie `Ungültiges DLC-Padding` blockieren den Fallback zu dcrypt nicht mehr.
|
||||
- Oversize/invalid-size DLCs werden weiterhin defensiv behandelt, aber valide Dateien im gleichen Batch werden nicht mehr still geschluckt.
|
||||
- Wenn alle DLC-Imports fehlschlagen, wird jetzt ein klarer Fehler mit Ursache geworfen statt still `0 Paket(e), 0 Link(s)` zu melden.
|
||||
|
||||
- **UI-Rueckmeldung verbessert:**
|
||||
- Bei DLC-Import mit `0` Treffern zeigt die UI jetzt eine klare Meldung (`Keine gültigen Links in den DLC-Dateien gefunden`) statt eines irrefuehrenden Erfolgs-Toast.
|
||||
|
||||
### Tests
|
||||
|
||||
- Neue/erweiterte Tests fuer:
|
||||
- Reset von `totalDownloadedBytes`/Stats bei leerer Queue.
|
||||
- DLC-Fallback-Pfad bei lokalen Decrypt-Exceptions.
|
||||
- Fehlerausgabe bei vollstaendig fehlgeschlagenem DLC-Import.
|
||||
- Validierung:
|
||||
- `npx tsc --noEmit` erfolgreich
|
||||
- `npm test` erfolgreich (`283/283`)
|
||||
- `npm run self-check` erfolgreich
|
||||
|
||||
## 1.4.32 - 2026-03-01
|
||||
|
||||
Diese Version erweitert den Auto-Renamer stark fuer reale Scene-/TV-Release-Strukturen (nested und flat) und fuehrt eine intensive Renamer-Regression mit zusaetzlichen Edge-Case- und Stress-Checks ein.
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "real-debrid-downloader",
|
||||
"version": "1.4.32",
|
||||
"version": "1.4.33",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "real-debrid-downloader",
|
||||
"version": "1.4.32",
|
||||
"version": "1.4.33",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"adm-zip": "^0.5.16",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "real-debrid-downloader",
|
||||
"version": "1.4.32",
|
||||
"version": "1.4.33",
|
||||
"description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)",
|
||||
"main": "build/main/main/main.js",
|
||||
"author": "Sucukdeluxe",
|
||||
|
||||
@ -7,6 +7,11 @@ import { ParsedPackageInput } from "../shared/types";
|
||||
|
||||
const MAX_DLC_FILE_BYTES = 8 * 1024 * 1024;
|
||||
|
||||
function isContainerSizeValidationError(error: unknown): boolean {
|
||||
const text = compactErrorText(error);
|
||||
return /zu groß/i.test(text) || /DLC-Datei ungültig oder zu groß/i.test(text);
|
||||
}
|
||||
|
||||
function decodeDcryptPayload(responseText: string): unknown {
|
||||
let text = String(responseText || "").trim();
|
||||
const m = text.match(/<textarea[^>]*>([\s\S]*?)<\/textarea>/i);
|
||||
@ -187,30 +192,51 @@ async function decryptDlcViaDcrypt(filePath: string): Promise<ParsedPackageInput
|
||||
|
||||
export async function importDlcContainers(filePaths: string[]): Promise<ParsedPackageInput[]> {
|
||||
const out: ParsedPackageInput[] = [];
|
||||
const failures: string[] = [];
|
||||
let sawDlc = false;
|
||||
for (const filePath of filePaths) {
|
||||
if (path.extname(filePath).toLowerCase() !== ".dlc") {
|
||||
continue;
|
||||
}
|
||||
sawDlc = true;
|
||||
let packages: ParsedPackageInput[] = [];
|
||||
let fileFailed = false;
|
||||
let fileFailureReasons: string[] = [];
|
||||
try {
|
||||
packages = await decryptDlcLocal(filePath);
|
||||
} catch (error) {
|
||||
if (/zu groß|ungültig/i.test(compactErrorText(error))) {
|
||||
if (isContainerSizeValidationError(error)) {
|
||||
failures.push(`${path.basename(filePath)}: ${compactErrorText(error)}`);
|
||||
continue;
|
||||
}
|
||||
fileFailed = true;
|
||||
fileFailureReasons.push(`lokal: ${compactErrorText(error)}`);
|
||||
packages = [];
|
||||
}
|
||||
if (packages.length === 0) {
|
||||
try {
|
||||
packages = await decryptDlcViaDcrypt(filePath);
|
||||
} catch (error) {
|
||||
if (/zu groß|ungültig/i.test(compactErrorText(error))) {
|
||||
if (isContainerSizeValidationError(error)) {
|
||||
failures.push(`${path.basename(filePath)}: ${compactErrorText(error)}`);
|
||||
continue;
|
||||
}
|
||||
fileFailed = true;
|
||||
fileFailureReasons.push(`dcrypt: ${compactErrorText(error)}`);
|
||||
packages = [];
|
||||
}
|
||||
}
|
||||
if (packages.length === 0 && fileFailed) {
|
||||
failures.push(`${path.basename(filePath)}: ${fileFailureReasons.join("; ")}`);
|
||||
}
|
||||
out.push(...packages);
|
||||
}
|
||||
|
||||
if (out.length === 0 && sawDlc && failures.length > 0) {
|
||||
const details = failures.slice(0, 2).join(" | ");
|
||||
const suffix = failures.length > 2 ? ` (+${failures.length - 2} weitere)` : "";
|
||||
throw new Error(`DLC konnte nicht importiert werden: ${details}${suffix}`);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
@ -681,6 +681,8 @@ export class DownloadManager extends EventEmitter {
|
||||
return this.statsCache;
|
||||
}
|
||||
|
||||
this.resetSessionTotalsIfQueueEmpty();
|
||||
|
||||
let totalDownloaded = 0;
|
||||
let totalFiles = 0;
|
||||
for (const item of Object.values(this.session.items)) {
|
||||
@ -714,6 +716,25 @@ export class DownloadManager extends EventEmitter {
|
||||
return stats;
|
||||
}
|
||||
|
||||
private resetSessionTotalsIfQueueEmpty(): void {
|
||||
if (this.itemCount > 0 || this.session.packageOrder.length > 0) {
|
||||
return;
|
||||
}
|
||||
if (Object.keys(this.session.items).length > 0 || Object.keys(this.session.packages).length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.session.totalDownloadedBytes = 0;
|
||||
this.session.runStartedAt = 0;
|
||||
this.lastGlobalProgressBytes = 0;
|
||||
this.lastGlobalProgressAt = nowMs();
|
||||
this.speedEvents = [];
|
||||
this.speedEventsHead = 0;
|
||||
this.speedBytesLastWindow = 0;
|
||||
this.statsCache = null;
|
||||
this.statsCacheAt = 0;
|
||||
}
|
||||
|
||||
public renamePackage(packageId: string, newName: string): void {
|
||||
const pkg = this.session.packages[packageId];
|
||||
if (!pkg) {
|
||||
@ -922,6 +943,7 @@ export class DownloadManager extends EventEmitter {
|
||||
this.nonResumableActive = 0;
|
||||
this.retryAfterByItem.clear();
|
||||
this.retryStateByItem.clear();
|
||||
this.resetSessionTotalsIfQueueEmpty();
|
||||
this.persistNow();
|
||||
this.emitState(true);
|
||||
}
|
||||
@ -1966,6 +1988,7 @@ export class DownloadManager extends EventEmitter {
|
||||
pkg.status = "completed";
|
||||
}
|
||||
}
|
||||
this.resetSessionTotalsIfQueueEmpty();
|
||||
this.persistSoon();
|
||||
}
|
||||
|
||||
@ -2371,6 +2394,7 @@ export class DownloadManager extends EventEmitter {
|
||||
this.runPackageIds.delete(packageId);
|
||||
this.runCompletedPackages.delete(packageId);
|
||||
this.hybridExtractRequeue.delete(packageId);
|
||||
this.resetSessionTotalsIfQueueEmpty();
|
||||
}
|
||||
|
||||
private async ensureScheduler(): Promise<void> {
|
||||
|
||||
@ -703,7 +703,11 @@ export function App(): ReactElement {
|
||||
if (files.length === 0) { return; }
|
||||
await persistDraftSettings();
|
||||
const result = await window.rd.addContainers(files);
|
||||
showToast(`DLC importiert: ${result.addedPackages} Paket(e), ${result.addedLinks} Link(s)`);
|
||||
if (result.addedLinks > 0) {
|
||||
showToast(`DLC importiert: ${result.addedPackages} Paket(e), ${result.addedLinks} Link(s)`);
|
||||
} else {
|
||||
showToast("Keine gültigen Links in den DLC-Dateien gefunden", 3000);
|
||||
}
|
||||
}, (error) => {
|
||||
showToast(`Fehler beim DLC-Import: ${String(error)}`, 2600);
|
||||
});
|
||||
@ -721,7 +725,11 @@ export function App(): ReactElement {
|
||||
await performQuickAction(async () => {
|
||||
await persistDraftSettings();
|
||||
const result = await window.rd.addContainers(dlc);
|
||||
showToast(`Drag-and-Drop: ${result.addedPackages} Paket(e), ${result.addedLinks} Link(s)`);
|
||||
if (result.addedLinks > 0) {
|
||||
showToast(`Drag-and-Drop: ${result.addedPackages} Paket(e), ${result.addedLinks} Link(s)`);
|
||||
} else {
|
||||
showToast("Keine gültigen Links in den DLC-Dateien gefunden", 3000);
|
||||
}
|
||||
}, (error) => {
|
||||
showToast(`Fehler bei Drag-and-Drop: ${String(error)}`, 2600);
|
||||
});
|
||||
|
||||
@ -78,4 +78,43 @@ describe("container", () => {
|
||||
// Should have tried both!
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("falls back to dcrypt when local decryption throws invalid padding", async () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dlc-"));
|
||||
tempDirs.push(dir);
|
||||
const filePath = path.join(dir, "invalid-local.dlc");
|
||||
fs.writeFileSync(filePath, "X".repeat(120));
|
||||
|
||||
const fetchSpy = vi.fn(async (url: string | URL | Request) => {
|
||||
const urlStr = String(url);
|
||||
if (urlStr.includes("service.jdownloader.org")) {
|
||||
return new Response(`<rc>${Buffer.alloc(16).toString("base64")}</rc>`, { status: 200 });
|
||||
}
|
||||
return new Response("http://example.com/fallback1", { status: 200 });
|
||||
});
|
||||
globalThis.fetch = fetchSpy as unknown as typeof fetch;
|
||||
|
||||
const result = await importDlcContainers([filePath]);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].links).toEqual(["http://example.com/fallback1"]);
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("throws clear error when all dlc imports fail", async () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dlc-"));
|
||||
tempDirs.push(dir);
|
||||
const filePath = path.join(dir, "broken.dlc");
|
||||
fs.writeFileSync(filePath, Buffer.from("not a valid dlc payload at all"));
|
||||
|
||||
const fetchSpy = vi.fn(async (url: string | URL | Request) => {
|
||||
const urlStr = String(url);
|
||||
if (urlStr.includes("service.jdownloader.org")) {
|
||||
return new Response("", { status: 404 });
|
||||
}
|
||||
return new Response("upstream failure", { status: 500 });
|
||||
});
|
||||
globalThis.fetch = fetchSpy as unknown as typeof fetch;
|
||||
|
||||
await expect(importDlcContainers([filePath])).rejects.toThrow(/DLC konnte nicht importiert werden/i);
|
||||
});
|
||||
});
|
||||
|
||||
@ -2399,6 +2399,103 @@ describe("download manager", () => {
|
||||
expect(summary).toBeNull();
|
||||
});
|
||||
|
||||
it("shows zero total when queue is empty despite stale persisted bytes", () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||
tempDirs.push(root);
|
||||
|
||||
const session = emptySession();
|
||||
session.totalDownloadedBytes = 19.99 * 1024 * 1024 * 1024;
|
||||
session.runStartedAt = Date.now() - 5 * 60 * 1000;
|
||||
|
||||
const manager = new DownloadManager(
|
||||
{
|
||||
...defaultSettings(),
|
||||
token: "rd-token",
|
||||
outputDir: path.join(root, "downloads"),
|
||||
extractDir: path.join(root, "extract"),
|
||||
autoExtract: false
|
||||
},
|
||||
session,
|
||||
createStoragePaths(path.join(root, "state"))
|
||||
);
|
||||
|
||||
const snapshot = manager.getSnapshot();
|
||||
expect(snapshot.stats.totalPackages).toBe(0);
|
||||
expect(snapshot.stats.totalFiles).toBe(0);
|
||||
expect(snapshot.stats.totalDownloaded).toBe(0);
|
||||
expect(snapshot.session.totalDownloadedBytes).toBe(0);
|
||||
expect(snapshot.session.runStartedAt).toBe(0);
|
||||
});
|
||||
|
||||
it("clearAll resets total bytes and stats", () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||
tempDirs.push(root);
|
||||
|
||||
const session = emptySession();
|
||||
const packageId = "pkg-clear";
|
||||
const itemId = "item-clear";
|
||||
const now = Date.now() - 1000;
|
||||
const outputDir = path.join(root, "downloads", "pkg-clear");
|
||||
const extractDir = path.join(root, "extract", "pkg-clear");
|
||||
const targetPath = path.join(outputDir, "episode.mkv");
|
||||
|
||||
session.packageOrder = [packageId];
|
||||
session.packages[packageId] = {
|
||||
id: packageId,
|
||||
name: "pkg-clear",
|
||||
outputDir,
|
||||
extractDir,
|
||||
status: "completed",
|
||||
itemIds: [itemId],
|
||||
cancelled: false,
|
||||
enabled: true,
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
};
|
||||
session.items[itemId] = {
|
||||
id: itemId,
|
||||
packageId,
|
||||
url: "https://dummy/item-clear",
|
||||
provider: "realdebrid",
|
||||
status: "completed",
|
||||
retries: 0,
|
||||
speedBps: 0,
|
||||
downloadedBytes: 1024,
|
||||
totalBytes: 1024,
|
||||
progressPercent: 100,
|
||||
fileName: "episode.mkv",
|
||||
targetPath,
|
||||
resumable: true,
|
||||
attempts: 1,
|
||||
lastError: "",
|
||||
fullStatus: "Fertig (1 KB)",
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
};
|
||||
session.totalDownloadedBytes = 1024;
|
||||
session.runStartedAt = now;
|
||||
|
||||
const manager = new DownloadManager(
|
||||
{
|
||||
...defaultSettings(),
|
||||
token: "rd-token",
|
||||
outputDir: path.join(root, "downloads"),
|
||||
extractDir: path.join(root, "extract"),
|
||||
autoExtract: false
|
||||
},
|
||||
session,
|
||||
createStoragePaths(path.join(root, "state"))
|
||||
);
|
||||
|
||||
manager.clearAll();
|
||||
const snapshot = manager.getSnapshot();
|
||||
expect(snapshot.stats.totalPackages).toBe(0);
|
||||
expect(snapshot.stats.totalFiles).toBe(0);
|
||||
expect(snapshot.stats.totalDownloaded).toBe(0);
|
||||
expect(snapshot.session.totalDownloadedBytes).toBe(0);
|
||||
expect(snapshot.session.runStartedAt).toBe(0);
|
||||
});
|
||||
|
||||
it("does not start a run when queue is empty", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||
tempDirs.push(root);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user