diff --git a/package.json b/package.json index c2c7b37..eb44e7a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "real-debrid-downloader", - "version": "1.4.76", + "version": "1.4.77", "description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)", "main": "build/main/main/main.js", "author": "Sucukdeluxe", diff --git a/src/main/debug-server.ts b/src/main/debug-server.ts index 20dd07a..c754409 100644 --- a/src/main/debug-server.ts +++ b/src/main/debug-server.ts @@ -5,7 +5,7 @@ import { logger, getLogFilePath } from "./logger"; import type { DownloadManager } from "./download-manager"; const DEFAULT_PORT = 9868; -const MAX_LOG_LINES = 500; +const MAX_LOG_LINES = 10000; let server: http.Server | null = null; let manager: DownloadManager | null = null; @@ -95,7 +95,12 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi if (pathname === "/log") { const count = Math.min(Number(url.searchParams.get("lines") || "100"), MAX_LOG_LINES); - const lines = readLogTail(count); + const grep = url.searchParams.get("grep") || ""; + let lines = readLogTail(count); + if (grep) { + const pattern = grep.toLowerCase(); + lines = lines.filter((l) => l.toLowerCase().includes(pattern)); + } jsonResponse(res, 200, { lines, count: lines.length }); return; } @@ -196,13 +201,51 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi return; } + if (pathname === "/session") { + if (!manager) { + jsonResponse(res, 503, { error: "Manager not initialized" }); + return; + } + const snapshot = manager.getSnapshot(); + const pkg = url.searchParams.get("package"); + if (pkg) { + const pkgLower = pkg.toLowerCase(); + const matchedPkg = Object.values(snapshot.session.packages) + .find((p) => p.name.toLowerCase().includes(pkgLower)); + if (matchedPkg) { + const ids = new Set(matchedPkg.itemIds); + const pkgItems = Object.values(snapshot.session.items) + .filter((i) => ids.has(i.id)); + jsonResponse(res, 200, { + package: matchedPkg, + items: pkgItems + }); + return; + } + } + jsonResponse(res, 200, { + running: snapshot.session.running, + paused: snapshot.session.paused, + packageCount: Object.keys(snapshot.session.packages).length, + itemCount: Object.keys(snapshot.session.items).length, + packages: Object.values(snapshot.session.packages).map((p) => ({ + id: p.id, + name: p.name, + status: p.status, + items: p.itemIds.length + })) + }); + return; + } + jsonResponse(res, 404, { error: "Not found", endpoints: [ "GET /health", - "GET /log?lines=100", + "GET /log?lines=100&grep=keyword", "GET /status", - "GET /items?status=downloading&package=Bloodline" + "GET /items?status=downloading&package=Bloodline", + "GET /session?package=Criminal" ] }); } diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index 282b5f5..53a2c4f 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -4472,20 +4472,59 @@ export class DownloadManager extends EventEmitter { return ready; } + // Build lookup: pathKey → item status for pending items + const pendingItemStatus = new Map(); + for (const itemId of pkg.itemIds) { + const item = this.session.items[itemId]; + if (item && item.targetPath && item.status !== "completed") { + pendingItemStatus.set(pathKey(item.targetPath), item.status); + } + } + for (const candidate of candidates) { const partsOnDisk = collectArchiveCleanupTargets(candidate, dirFiles); const allPartsCompleted = partsOnDisk.every((part) => completedPaths.has(pathKey(part))); - if (!allPartsCompleted) { + if (allPartsCompleted) { + const hasUnstartedParts = [...pendingPaths].some((pendingPath) => { + const pendingName = path.basename(pendingPath).toLowerCase(); + const candidateStem = path.basename(candidate).toLowerCase(); + return this.looksLikeArchivePart(pendingName, candidateStem); + }); + if (hasUnstartedParts) { + continue; + } + ready.add(pathKey(candidate)); continue; } - const hasUnstartedParts = [...pendingPaths].some((pendingPath) => { - const pendingName = path.basename(pendingPath).toLowerCase(); - const candidateStem = path.basename(candidate).toLowerCase(); - return this.looksLikeArchivePart(pendingName, candidateStem); + + // Disk-fallback: if all parts exist on disk but some items lack "completed" status, + // allow extraction if none of those parts are actively downloading/validating. + // This handles items that finished downloading but whose status was not updated. + const missingParts = partsOnDisk.filter((part) => !completedPaths.has(pathKey(part))); + let allMissingExistOnDisk = true; + for (const part of missingParts) { + try { + const stat = fs.statSync(part); + if (stat.size <= 0) { + allMissingExistOnDisk = false; + break; + } + } catch { + allMissingExistOnDisk = false; + break; + } + } + if (!allMissingExistOnDisk) { + continue; + } + const anyActivelyProcessing = missingParts.some((part) => { + const status = pendingItemStatus.get(pathKey(part)); + return status === "downloading" || status === "validating" || status === "integrity_check"; }); - if (hasUnstartedParts) { + if (anyActivelyProcessing) { continue; } + logger.info(`Hybrid-Extract Disk-Fallback: ${path.basename(candidate)} (${missingParts.length} Part(s) auf Disk ohne completed-Status)`); ready.add(pathKey(candidate)); }