Fix Debrid-Link retry recovery
This commit is contained in:
parent
a892c1ad8f
commit
c1a4d8037f
@ -76,6 +76,21 @@ function readSupportHistory() {
|
|||||||
return loadHistory(getStoragePaths());
|
return loadHistory(getStoragePaths());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractDebugClientIp(req: http.IncomingMessage): string {
|
||||||
|
const forwarded = req.headers["x-forwarded-for"];
|
||||||
|
const forwardedValue = Array.isArray(forwarded) ? forwarded[0] : forwarded;
|
||||||
|
const forwardedIp = String(forwardedValue || "").split(",")[0]?.trim();
|
||||||
|
if (forwardedIp) {
|
||||||
|
return forwardedIp;
|
||||||
|
}
|
||||||
|
const realIp = String(req.headers["x-real-ip"] || "").trim();
|
||||||
|
if (realIp) {
|
||||||
|
return realIp;
|
||||||
|
}
|
||||||
|
const remote = String(req.socket.remoteAddress || req.socket.address()?.address || "").trim();
|
||||||
|
return remote.replace(/^::ffff:/i, "");
|
||||||
|
}
|
||||||
|
|
||||||
function getAiManifestPath(baseDir: string = runtimeBaseDir): string {
|
function getAiManifestPath(baseDir: string = runtimeBaseDir): string {
|
||||||
return path.join(baseDir, AI_MANIFEST_FILE);
|
return path.join(baseDir, AI_MANIFEST_FILE);
|
||||||
}
|
}
|
||||||
@ -431,7 +446,8 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
|
|||||||
if (traceConfig.enabled && traceConfig.logDebugRequests) {
|
if (traceConfig.enabled && traceConfig.logDebugRequests) {
|
||||||
logTraceEvent("INFO", "debug-http", "Request", {
|
logTraceEvent("INFO", "debug-http", "Request", {
|
||||||
method: req.method || "GET",
|
method: req.method || "GET",
|
||||||
url: sanitizeRequestUrlForTrace(req.url || "/")
|
url: sanitizeRequestUrlForTrace(req.url || "/"),
|
||||||
|
clientIp: extractDebugClientIp(req)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -449,7 +465,8 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
|
|||||||
if (traceConfig.enabled && traceConfig.logDebugRequests) {
|
if (traceConfig.enabled && traceConfig.logDebugRequests) {
|
||||||
logTraceEvent("WARN", "debug-http", "Unauthorized request", {
|
logTraceEvent("WARN", "debug-http", "Unauthorized request", {
|
||||||
method: req.method || "GET",
|
method: req.method || "GET",
|
||||||
url: sanitizeRequestUrlForTrace(req.url || "/")
|
url: sanitizeRequestUrlForTrace(req.url || "/"),
|
||||||
|
clientIp: extractDebugClientIp(req)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
jsonResponse(res, 401, { error: "Unauthorized" });
|
jsonResponse(res, 401, { error: "Unauthorized" });
|
||||||
|
|||||||
@ -434,6 +434,11 @@ function shouldPreflightFinalizeItemFromDisk(item: DownloadItem): boolean {
|
|||||||
const text = `${item.fullStatus || ""} ${item.lastError || ""}`.toLowerCase();
|
const text = `${item.fullStatus || ""} ${item.lastError || ""}`.toLowerCase();
|
||||||
return text.includes("resume-link erneuern")
|
return text.includes("resume-link erneuern")
|
||||||
|| text.includes("resume link erneuern")
|
|| text.includes("resume link erneuern")
|
||||||
|
|| text.includes("direktlink erneuern")
|
||||||
|
|| text.includes("direktlink erschöpft")
|
||||||
|
|| text.includes("direct_link_retry_exhausted")
|
||||||
|
|| text.includes("download_underflow")
|
||||||
|
|| text.includes("resume_download_underflow")
|
||||||
|| text.includes("range_ignored_on_resume")
|
|| text.includes("range_ignored_on_resume")
|
||||||
|| text.includes("server ignorierte range");
|
|| text.includes("server ignorierte range");
|
||||||
}
|
}
|
||||||
@ -8142,16 +8147,21 @@ export class DownloadManager extends EventEmitter {
|
|||||||
if (response.status === 416 && existingBytes > 0) {
|
if (response.status === 416 && existingBytes > 0) {
|
||||||
await response.arrayBuffer().catch(() => undefined);
|
await response.arrayBuffer().catch(() => undefined);
|
||||||
const rangeTotal = parseContentRangeTotal(response.headers.get("content-range"));
|
const rangeTotal = parseContentRangeTotal(response.headers.get("content-range"));
|
||||||
const expectedTotal = knownTotal && knownTotal > 0 ? knownTotal : rangeTotal;
|
const expectedTotal = rangeTotal && rangeTotal > 0
|
||||||
if (expectedTotal && existingBytes === expectedTotal) {
|
? rangeTotal
|
||||||
item.totalBytes = expectedTotal;
|
: (knownTotal && knownTotal > 0 ? knownTotal : null);
|
||||||
|
const closeEnoughToExpected = expectedTotal != null
|
||||||
|
&& Math.abs(existingBytes - expectedTotal) <= ALLOCATION_UNIT_SIZE;
|
||||||
|
if (expectedTotal != null && closeEnoughToExpected) {
|
||||||
|
const finalizedTotal = Math.max(existingBytes, expectedTotal);
|
||||||
|
item.totalBytes = finalizedTotal;
|
||||||
item.downloadedBytes = existingBytes;
|
item.downloadedBytes = existingBytes;
|
||||||
item.progressPercent = 100;
|
item.progressPercent = 100;
|
||||||
item.speedBps = 0;
|
item.speedBps = 0;
|
||||||
item.updatedAt = nowMs();
|
item.updatedAt = nowMs();
|
||||||
logAttemptEvent("INFO", "HTTP 416 als vollständig behandelt", {
|
logAttemptEvent("INFO", "HTTP 416 als vollständig behandelt", {
|
||||||
existingBytes,
|
existingBytes,
|
||||||
expectedTotal
|
expectedTotal: finalizedTotal
|
||||||
});
|
});
|
||||||
return { resumable: true };
|
return { resumable: true };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -405,6 +405,22 @@ describe("debug-server", () => {
|
|||||||
expect(payload.supportBundle?.estimatedEntries).toBeGreaterThan(0);
|
expect(payload.supportBundle?.estimatedEntries).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("writes the client IP into the debug trace log", async () => {
|
||||||
|
const fixture = await createFixture();
|
||||||
|
const response = await fetch(`${fixture.baseUrl}/health?token=${fixture.token}`, {
|
||||||
|
headers: {
|
||||||
|
"X-Forwarded-For": "159.195.63.46"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(response.ok).toBe(true);
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||||
|
const traceLogPath = getTraceLogPath();
|
||||||
|
expect(traceLogPath).toBeTruthy();
|
||||||
|
const traceText = fs.readFileSync(traceLogPath!, "utf8");
|
||||||
|
expect(traceText).toContain("clientIp=159.195.63.46");
|
||||||
|
});
|
||||||
|
|
||||||
it("serves package details and package log by package query", async () => {
|
it("serves package details and package log by package query", async () => {
|
||||||
const fixture = await createFixture();
|
const fixture = await createFixture();
|
||||||
|
|
||||||
|
|||||||
@ -1332,6 +1332,92 @@ describe("download manager", () => {
|
|||||||
expect(unrestrictCalls).toBe(0);
|
expect(unrestrictCalls).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("completes Debrid-Link direct-link retries from disk during start preflight", async () => {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||||
|
tempDirs.push(root);
|
||||||
|
const binary = Buffer.alloc(60 * 1024, 23);
|
||||||
|
const pkgDir = path.join(root, "downloads", "queued-directlink-complete");
|
||||||
|
fs.mkdirSync(pkgDir, { recursive: true });
|
||||||
|
const targetPath = path.join(pkgDir, "queued-directlink-complete.part10.rar");
|
||||||
|
fs.writeFileSync(targetPath, binary);
|
||||||
|
let unrestrictCalls = 0;
|
||||||
|
|
||||||
|
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
||||||
|
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
||||||
|
if (url.includes("debrid-link.com/api/v2/downloader/add")) {
|
||||||
|
unrestrictCalls += 1;
|
||||||
|
throw new Error(`unexpected debrid-link unrestrict ${url}`);
|
||||||
|
}
|
||||||
|
return originalFetch(input, init);
|
||||||
|
};
|
||||||
|
|
||||||
|
const session = emptySession();
|
||||||
|
const packageId = "queued-directlink-complete-pkg";
|
||||||
|
const itemId = "queued-directlink-complete-item";
|
||||||
|
const createdAt = Date.now() - 10_000;
|
||||||
|
|
||||||
|
session.packageOrder = [packageId];
|
||||||
|
session.packages[packageId] = {
|
||||||
|
id: packageId,
|
||||||
|
name: "queued-directlink-complete",
|
||||||
|
outputDir: pkgDir,
|
||||||
|
extractDir: path.join(root, "extract", "queued-directlink-complete"),
|
||||||
|
status: "queued",
|
||||||
|
itemIds: [itemId],
|
||||||
|
cancelled: false,
|
||||||
|
enabled: true,
|
||||||
|
createdAt,
|
||||||
|
updatedAt: createdAt
|
||||||
|
};
|
||||||
|
session.items[itemId] = {
|
||||||
|
id: itemId,
|
||||||
|
packageId,
|
||||||
|
url: "https://dummy/queued-directlink-complete",
|
||||||
|
provider: "debridlink",
|
||||||
|
status: "queued",
|
||||||
|
retries: 14,
|
||||||
|
speedBps: 0,
|
||||||
|
downloadedBytes: 0,
|
||||||
|
totalBytes: binary.length,
|
||||||
|
progressPercent: 0,
|
||||||
|
fileName: "queued-directlink-complete.part10.rar",
|
||||||
|
targetPath,
|
||||||
|
resumable: true,
|
||||||
|
attempts: 0,
|
||||||
|
lastError: "direct_link_retry_exhausted:HTTP 416",
|
||||||
|
fullStatus: "Direktlink erneuern, Retry 15/inf",
|
||||||
|
createdAt,
|
||||||
|
updatedAt: createdAt
|
||||||
|
};
|
||||||
|
|
||||||
|
const manager = new DownloadManager(
|
||||||
|
{
|
||||||
|
...defaultSettings(),
|
||||||
|
debridLinkApiKeys: "dl-test-key",
|
||||||
|
providerOrder: ["debridlink"],
|
||||||
|
providerPrimary: "debridlink",
|
||||||
|
providerSecondary: "none",
|
||||||
|
providerTertiary: "none",
|
||||||
|
outputDir: path.join(root, "downloads"),
|
||||||
|
extractDir: path.join(root, "extract"),
|
||||||
|
retryLimit: 0,
|
||||||
|
autoExtract: false
|
||||||
|
},
|
||||||
|
session,
|
||||||
|
createStoragePaths(path.join(root, "state"))
|
||||||
|
);
|
||||||
|
|
||||||
|
await manager.start();
|
||||||
|
await waitFor(() => !manager.getSnapshot().session.running, 12000);
|
||||||
|
|
||||||
|
const item = manager.getSnapshot().session.items[itemId];
|
||||||
|
expect(item?.status).toBe("completed");
|
||||||
|
expect(item?.progressPercent).toBe(100);
|
||||||
|
expect(item?.downloadedBytes).toBe(binary.length);
|
||||||
|
expect(item?.fullStatus).toContain("Fertig");
|
||||||
|
expect(unrestrictCalls).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
it("retries direct-link exhaustion caused by HTTP 416 in-session and then completes", async () => {
|
it("retries direct-link exhaustion caused by HTTP 416 in-session and then completes", 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