Fix hybrid extraction skipping archives when item status stuck
Some checks are pending
Build and Release / build (push) Waiting to run
Some checks are pending
Build and Release / build (push) Waiting to run
Add disk-fallback to findReadyArchiveSets: when all archive parts physically exist on disk with non-zero size and none are actively downloading/validating, consider the archive ready for extraction. This fixes episodes being skipped when a download item's status was not updated to "completed" despite the file being fully written. Also improve debug server: raise log limit to 10000 lines, add grep filter, add /session endpoint for raw session data. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9f589439a1
commit
d8a53dcea6
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.4.76",
|
"version": "1.4.77",
|
||||||
"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",
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { logger, getLogFilePath } from "./logger";
|
|||||||
import type { DownloadManager } from "./download-manager";
|
import type { DownloadManager } from "./download-manager";
|
||||||
|
|
||||||
const DEFAULT_PORT = 9868;
|
const DEFAULT_PORT = 9868;
|
||||||
const MAX_LOG_LINES = 500;
|
const MAX_LOG_LINES = 10000;
|
||||||
|
|
||||||
let server: http.Server | null = null;
|
let server: http.Server | null = null;
|
||||||
let manager: DownloadManager | null = null;
|
let manager: DownloadManager | null = null;
|
||||||
@ -95,7 +95,12 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
|
|||||||
|
|
||||||
if (pathname === "/log") {
|
if (pathname === "/log") {
|
||||||
const count = Math.min(Number(url.searchParams.get("lines") || "100"), MAX_LOG_LINES);
|
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 });
|
jsonResponse(res, 200, { lines, count: lines.length });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -196,13 +201,51 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
|
|||||||
return;
|
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, {
|
jsonResponse(res, 404, {
|
||||||
error: "Not found",
|
error: "Not found",
|
||||||
endpoints: [
|
endpoints: [
|
||||||
"GET /health",
|
"GET /health",
|
||||||
"GET /log?lines=100",
|
"GET /log?lines=100&grep=keyword",
|
||||||
"GET /status",
|
"GET /status",
|
||||||
"GET /items?status=downloading&package=Bloodline"
|
"GET /items?status=downloading&package=Bloodline",
|
||||||
|
"GET /session?package=Criminal"
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4472,20 +4472,59 @@ export class DownloadManager extends EventEmitter {
|
|||||||
return ready;
|
return ready;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build lookup: pathKey → item status for pending items
|
||||||
|
const pendingItemStatus = new Map<string, string>();
|
||||||
|
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) {
|
for (const candidate of candidates) {
|
||||||
const partsOnDisk = collectArchiveCleanupTargets(candidate, dirFiles);
|
const partsOnDisk = collectArchiveCleanupTargets(candidate, dirFiles);
|
||||||
const allPartsCompleted = partsOnDisk.every((part) => completedPaths.has(pathKey(part)));
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
const hasUnstartedParts = [...pendingPaths].some((pendingPath) => {
|
|
||||||
const pendingName = path.basename(pendingPath).toLowerCase();
|
// Disk-fallback: if all parts exist on disk but some items lack "completed" status,
|
||||||
const candidateStem = path.basename(candidate).toLowerCase();
|
// allow extraction if none of those parts are actively downloading/validating.
|
||||||
return this.looksLikeArchivePart(pendingName, candidateStem);
|
// 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;
|
continue;
|
||||||
}
|
}
|
||||||
|
logger.info(`Hybrid-Extract Disk-Fallback: ${path.basename(candidate)} (${missingParts.length} Part(s) auf Disk ohne completed-Status)`);
|
||||||
ready.add(pathKey(candidate));
|
ready.add(pathKey(candidate));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user