Release v1.4.33 with DLC import and stats hotfixes
Some checks are pending
Build and Release / build (push) Waiting to run
Some checks are pending
Build and Release / build (push) Waiting to run
This commit is contained in:
parent
ff1036563a
commit
edbfba6663
30
CHANGELOG.md
30
CHANGELOG.md
@ -2,6 +2,36 @@
|
|||||||
|
|
||||||
Alle nennenswerten Aenderungen werden in dieser Datei dokumentiert.
|
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
|
## 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.
|
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",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.4.32",
|
"version": "1.4.33",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.4.32",
|
"version": "1.4.33",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.4.32",
|
"version": "1.4.33",
|
||||||
"description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)",
|
"description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)",
|
||||||
"main": "build/main/main/main.js",
|
"main": "build/main/main/main.js",
|
||||||
"author": "Sucukdeluxe",
|
"author": "Sucukdeluxe",
|
||||||
|
|||||||
@ -7,6 +7,11 @@ import { ParsedPackageInput } from "../shared/types";
|
|||||||
|
|
||||||
const MAX_DLC_FILE_BYTES = 8 * 1024 * 1024;
|
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 {
|
function decodeDcryptPayload(responseText: string): unknown {
|
||||||
let text = String(responseText || "").trim();
|
let text = String(responseText || "").trim();
|
||||||
const m = text.match(/<textarea[^>]*>([\s\S]*?)<\/textarea>/i);
|
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[]> {
|
export async function importDlcContainers(filePaths: string[]): Promise<ParsedPackageInput[]> {
|
||||||
const out: ParsedPackageInput[] = [];
|
const out: ParsedPackageInput[] = [];
|
||||||
|
const failures: string[] = [];
|
||||||
|
let sawDlc = false;
|
||||||
for (const filePath of filePaths) {
|
for (const filePath of filePaths) {
|
||||||
if (path.extname(filePath).toLowerCase() !== ".dlc") {
|
if (path.extname(filePath).toLowerCase() !== ".dlc") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
sawDlc = true;
|
||||||
let packages: ParsedPackageInput[] = [];
|
let packages: ParsedPackageInput[] = [];
|
||||||
|
let fileFailed = false;
|
||||||
|
let fileFailureReasons: string[] = [];
|
||||||
try {
|
try {
|
||||||
packages = await decryptDlcLocal(filePath);
|
packages = await decryptDlcLocal(filePath);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (/zu groß|ungültig/i.test(compactErrorText(error))) {
|
if (isContainerSizeValidationError(error)) {
|
||||||
|
failures.push(`${path.basename(filePath)}: ${compactErrorText(error)}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
fileFailed = true;
|
||||||
|
fileFailureReasons.push(`lokal: ${compactErrorText(error)}`);
|
||||||
packages = [];
|
packages = [];
|
||||||
}
|
}
|
||||||
if (packages.length === 0) {
|
if (packages.length === 0) {
|
||||||
try {
|
try {
|
||||||
packages = await decryptDlcViaDcrypt(filePath);
|
packages = await decryptDlcViaDcrypt(filePath);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (/zu groß|ungültig/i.test(compactErrorText(error))) {
|
if (isContainerSizeValidationError(error)) {
|
||||||
|
failures.push(`${path.basename(filePath)}: ${compactErrorText(error)}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
fileFailed = true;
|
||||||
|
fileFailureReasons.push(`dcrypt: ${compactErrorText(error)}`);
|
||||||
packages = [];
|
packages = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (packages.length === 0 && fileFailed) {
|
||||||
|
failures.push(`${path.basename(filePath)}: ${fileFailureReasons.join("; ")}`);
|
||||||
|
}
|
||||||
out.push(...packages);
|
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;
|
return out;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -681,6 +681,8 @@ export class DownloadManager extends EventEmitter {
|
|||||||
return this.statsCache;
|
return this.statsCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.resetSessionTotalsIfQueueEmpty();
|
||||||
|
|
||||||
let totalDownloaded = 0;
|
let totalDownloaded = 0;
|
||||||
let totalFiles = 0;
|
let totalFiles = 0;
|
||||||
for (const item of Object.values(this.session.items)) {
|
for (const item of Object.values(this.session.items)) {
|
||||||
@ -714,6 +716,25 @@ export class DownloadManager extends EventEmitter {
|
|||||||
return stats;
|
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 {
|
public renamePackage(packageId: string, newName: string): void {
|
||||||
const pkg = this.session.packages[packageId];
|
const pkg = this.session.packages[packageId];
|
||||||
if (!pkg) {
|
if (!pkg) {
|
||||||
@ -922,6 +943,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.nonResumableActive = 0;
|
this.nonResumableActive = 0;
|
||||||
this.retryAfterByItem.clear();
|
this.retryAfterByItem.clear();
|
||||||
this.retryStateByItem.clear();
|
this.retryStateByItem.clear();
|
||||||
|
this.resetSessionTotalsIfQueueEmpty();
|
||||||
this.persistNow();
|
this.persistNow();
|
||||||
this.emitState(true);
|
this.emitState(true);
|
||||||
}
|
}
|
||||||
@ -1966,6 +1988,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
pkg.status = "completed";
|
pkg.status = "completed";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.resetSessionTotalsIfQueueEmpty();
|
||||||
this.persistSoon();
|
this.persistSoon();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2371,6 +2394,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.runPackageIds.delete(packageId);
|
this.runPackageIds.delete(packageId);
|
||||||
this.runCompletedPackages.delete(packageId);
|
this.runCompletedPackages.delete(packageId);
|
||||||
this.hybridExtractRequeue.delete(packageId);
|
this.hybridExtractRequeue.delete(packageId);
|
||||||
|
this.resetSessionTotalsIfQueueEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ensureScheduler(): Promise<void> {
|
private async ensureScheduler(): Promise<void> {
|
||||||
|
|||||||
@ -703,7 +703,11 @@ export function App(): ReactElement {
|
|||||||
if (files.length === 0) { return; }
|
if (files.length === 0) { return; }
|
||||||
await persistDraftSettings();
|
await persistDraftSettings();
|
||||||
const result = await window.rd.addContainers(files);
|
const result = await window.rd.addContainers(files);
|
||||||
|
if (result.addedLinks > 0) {
|
||||||
showToast(`DLC importiert: ${result.addedPackages} Paket(e), ${result.addedLinks} Link(s)`);
|
showToast(`DLC importiert: ${result.addedPackages} Paket(e), ${result.addedLinks} Link(s)`);
|
||||||
|
} else {
|
||||||
|
showToast("Keine gültigen Links in den DLC-Dateien gefunden", 3000);
|
||||||
|
}
|
||||||
}, (error) => {
|
}, (error) => {
|
||||||
showToast(`Fehler beim DLC-Import: ${String(error)}`, 2600);
|
showToast(`Fehler beim DLC-Import: ${String(error)}`, 2600);
|
||||||
});
|
});
|
||||||
@ -721,7 +725,11 @@ export function App(): ReactElement {
|
|||||||
await performQuickAction(async () => {
|
await performQuickAction(async () => {
|
||||||
await persistDraftSettings();
|
await persistDraftSettings();
|
||||||
const result = await window.rd.addContainers(dlc);
|
const result = await window.rd.addContainers(dlc);
|
||||||
|
if (result.addedLinks > 0) {
|
||||||
showToast(`Drag-and-Drop: ${result.addedPackages} Paket(e), ${result.addedLinks} Link(s)`);
|
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) => {
|
}, (error) => {
|
||||||
showToast(`Fehler bei Drag-and-Drop: ${String(error)}`, 2600);
|
showToast(`Fehler bei Drag-and-Drop: ${String(error)}`, 2600);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -78,4 +78,43 @@ describe("container", () => {
|
|||||||
// Should have tried both!
|
// Should have tried both!
|
||||||
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
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();
|
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 () => {
|
it("does not start a run when queue is empty", 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