Fix: Statistik-Doppelzaehlung bei Retry + Debrid-Link Key-Cooldown bei Abbruch (Nutzer-Nachforderung Audit)
BYTE-DROP-RETRY-1 (MED): Integritaets-/Zu-klein-/Tiny-Neuversuche zaehlten die volle Dateigroesse pro Versuch erneut in die Byte-Statistik. Ursache: die 3 rm-dann-frisch-Sites (Integrity-Fail 8979, too-small 9020, tiny 10486) riefen dropItemContribution() auf, das den itemContributedBytes-Eintrag loescht, den die einzige Reconciliation (9991, writeMode 'w') zum Subtrahieren braucht -> Subtraktion tot -> Re-Download addiert erneut. Fix (Mechanismus a): dropItemContribution an diesen 3 Sites entfernt, der Eintrag ueberlebt, 9991 subtrahiert ihn korrekt (selbst-korrigierend nach writeMode). Zusaetzlich am selben Punkt totalDownloadedAllTime subtrahiert (wurde nie subtrahiert -> doppelte bei JEDEM frischen Re-Download). BEWUSST ausgeklammert: recordProviderDownloadedBytes/ providerDailyUsageBytes, da diese isProviderDailyLimited (= Verhalten) steuern und nicht provider-keyed sind. Reine Telemetrie-Korrektur, kein Slot/Admission betroffen. DL-1 (LOW): Ein abort-ohne-timeout (User-Cancel) setzte am DebridLink-Rotations- Catch via classifyKeyFailure einen 15s-Key-Cooldown (und konnte ueber Keys zu einer providerweiten Kaskade fuehren). Fix: Mega-Gate (2072) am Catch (2789) gespiegelt - abort-ohne-timeout + elapsedMs < getMegaDebridAbortMinRunMs() -> kein Cooldown; ran-long-enough -> 120s (Retry rotiert); throw bailt die Rotation. DL-CONCURRENCY-PILEUP (MED): untersucht -> bereits strukturell geloest (getSerializedValidatingLimit = nutzbare Accounts + shouldDelayStartForItem 8526; MW-1 haelt das Limit hoch). Kein Eingriff (Nutzer-Entscheidung). Je rot-bewiesener Test (per Temp-Revert, nicht-vakuum; BYTE-DROP beide Beine einzeln). Volle Suite 893 gruen, tsc unveraendert (6 vorbestehende Fehler).
This commit is contained in:
+73
-1
@@ -3,7 +3,7 @@ import { defaultSettings, REQUEST_RETRIES } from "../src/main/constants";
|
||||
import { parseDebridLinkApiKeys } from "../src/shared/debrid-link-keys";
|
||||
import { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts";
|
||||
import { getProviderUsageDayKey } from "../src/shared/provider-daily-limits";
|
||||
import { classifyMegaDebridAccountFailureForTests, clearMegaDebridEmptyResponseStreak, DebridService, extractRapidgatorFilenameFromHtml, fetchAllDebridHostInfo, fetchDebridLinkHostLimits, filenameFromRapidgatorUrlPath, getDebridLinkKeyRuntimeStateForTests, getMegaDebridAccountCooldownState, leadProviderChainWith, MEGA_DEBRID_EMPTY_STREAK_UNTIL_RESTART, MEGA_DEBRID_STICKY_LINKS, normalizeResolvedFilename, primeMegaDebridUntilRestartForTests, recordMegaDebridEmptyResponseStreak, resetDebridLinkRuntimeStateForTests, resetMegaDebridRuntimeStateForTests } from "../src/main/debrid";
|
||||
import { classifyMegaDebridAccountFailureForTests, clearMegaDebridEmptyResponseStreak, DebridService, extractRapidgatorFilenameFromHtml, fetchAllDebridHostInfo, fetchDebridLinkHostLimits, filenameFromRapidgatorUrlPath, getDebridLinkKeyCooldownStateForTests, getDebridLinkKeyRuntimeStateForTests, getMegaDebridAccountCooldownState, leadProviderChainWith, MEGA_DEBRID_EMPTY_STREAK_UNTIL_RESTART, MEGA_DEBRID_STICKY_LINKS, normalizeResolvedFilename, primeMegaDebridUntilRestartForTests, recordMegaDebridEmptyResponseStreak, resetDebridLinkRuntimeStateForTests, resetMegaDebridRuntimeStateForTests } from "../src/main/debrid";
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
@@ -572,6 +572,78 @@ describe("debrid service", () => {
|
||||
expect(getDebridLinkKeyRuntimeStateForTests(key2Id)).toBe("ready");
|
||||
});
|
||||
|
||||
it("does NOT cool down a Debrid-Link key on a quick user-cancel abort (below the min-run threshold)", async () => {
|
||||
const settings = {
|
||||
...defaultSettings(),
|
||||
token: "",
|
||||
bestToken: "",
|
||||
allDebridToken: "",
|
||||
megaLogin: "",
|
||||
megaPassword: "",
|
||||
megaCredentials: "",
|
||||
debridLinkApiKeys: "dl-key-one",
|
||||
providerOrder: ["debridlink"] as const,
|
||||
providerPrimary: "debridlink" as const,
|
||||
providerSecondary: "none" as const,
|
||||
providerTertiary: "none" as const,
|
||||
autoProviderFallback: false
|
||||
};
|
||||
const controller = new AbortController();
|
||||
globalThis.fetch = (async (input: RequestInfo | URL): Promise<Response> => {
|
||||
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
||||
if (url.includes("/downloader/add")) {
|
||||
controller.abort();
|
||||
throw new Error("aborted");
|
||||
}
|
||||
return new Response("not-found", { status: 404 });
|
||||
}) as typeof fetch;
|
||||
|
||||
const keyId = parseDebridLinkApiKeys("dl-key-one")[0].id;
|
||||
const service = new DebridService(settings);
|
||||
await expect(
|
||||
service.unrestrictLink("https://rapidgator.net/file/dl-quick-cancel", controller.signal)
|
||||
).rejects.toThrow();
|
||||
|
||||
expect(getDebridLinkKeyCooldownStateForTests(keyId)).toBeNull();
|
||||
});
|
||||
|
||||
it("cools down a Debrid-Link key on an abort that ran long enough (retry rotates to the next key)", async () => {
|
||||
process.env.RD_MEGA_ABORT_MIN_RUN_MS = "0";
|
||||
const settings = {
|
||||
...defaultSettings(),
|
||||
token: "",
|
||||
bestToken: "",
|
||||
allDebridToken: "",
|
||||
megaLogin: "",
|
||||
megaPassword: "",
|
||||
megaCredentials: "",
|
||||
debridLinkApiKeys: "dl-key-one",
|
||||
providerOrder: ["debridlink"] as const,
|
||||
providerPrimary: "debridlink" as const,
|
||||
providerSecondary: "none" as const,
|
||||
providerTertiary: "none" as const,
|
||||
autoProviderFallback: false
|
||||
};
|
||||
const controller = new AbortController();
|
||||
globalThis.fetch = (async (input: RequestInfo | URL): Promise<Response> => {
|
||||
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
||||
if (url.includes("/downloader/add")) {
|
||||
controller.abort();
|
||||
throw new Error("aborted");
|
||||
}
|
||||
return new Response("not-found", { status: 404 });
|
||||
}) as typeof fetch;
|
||||
|
||||
const keyId = parseDebridLinkApiKeys("dl-key-one")[0].id;
|
||||
const service = new DebridService(settings);
|
||||
await expect(
|
||||
service.unrestrictLink("https://rapidgator.net/file/dl-long-abort", controller.signal)
|
||||
).rejects.toThrow();
|
||||
|
||||
const cooldown = getDebridLinkKeyCooldownStateForTests(keyId);
|
||||
expect(cooldown?.remainingMs ?? 0).toBeGreaterThan(60_000);
|
||||
});
|
||||
|
||||
it("treats bad Debrid-Link file passwords as fatal and does not rotate keys", async () => {
|
||||
const settings = {
|
||||
...defaultSettings(),
|
||||
|
||||
@@ -1373,6 +1373,100 @@ describe("download manager", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("counts a file once in session and all-time totals across an integrity-fail retry (no telemetry double-count)", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-byteacct-"));
|
||||
tempDirs.push(root);
|
||||
const size = 512 * 1024;
|
||||
const correct = Buffer.alloc(size, 0);
|
||||
for (let i = 0; i < size; i += 1) {
|
||||
correct[i] = (i * 17 + 3) & 0xff;
|
||||
}
|
||||
const wrong = Buffer.alloc(size, 0);
|
||||
for (let i = 0; i < size; i += 1) {
|
||||
wrong[i] = (i * 17 + 99) & 0xff;
|
||||
}
|
||||
const md5 = crypto.createHash("md5").update(correct).digest("hex");
|
||||
|
||||
const pkgDir = path.join(root, "downloads", "byteacct");
|
||||
fs.mkdirSync(pkgDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(pkgDir, "byteacct.md5"), `${md5} *movie.mkv\n`, "utf8");
|
||||
|
||||
let fullServes = 0;
|
||||
const server = http.createServer((req, res) => {
|
||||
const range = String(req.headers.range || "");
|
||||
const m = range.match(/bytes=(\d+)-/i);
|
||||
const start = m ? Number(m[1]) : 0;
|
||||
if (start === 0) {
|
||||
fullServes += 1;
|
||||
}
|
||||
const body = fullServes <= 1 ? wrong : correct;
|
||||
const chunk = body.subarray(start);
|
||||
if (start > 0) {
|
||||
res.statusCode = 206;
|
||||
res.setHeader("Content-Range", `bytes ${start}-${body.length - 1}/${body.length}`);
|
||||
} else {
|
||||
res.statusCode = 200;
|
||||
}
|
||||
res.setHeader("Accept-Ranges", "bytes");
|
||||
res.setHeader("Content-Length", String(chunk.length));
|
||||
res.end(chunk);
|
||||
});
|
||||
|
||||
server.listen(0, "127.0.0.1");
|
||||
await once(server, "listening");
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") {
|
||||
throw new Error("server address unavailable");
|
||||
}
|
||||
const base = `http://127.0.0.1:${address.port}`;
|
||||
|
||||
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("/unrestrict/link")) {
|
||||
return new Response(
|
||||
JSON.stringify({ download: `${base}/file`, filename: "movie.mkv", filesize: size }),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
return originalFetch(input, init);
|
||||
};
|
||||
|
||||
try {
|
||||
const manager = new DownloadManager(
|
||||
{
|
||||
...defaultSettings(),
|
||||
token: "rd-token",
|
||||
outputDir: path.join(root, "downloads"),
|
||||
extractDir: path.join(root, "extract"),
|
||||
autoExtract: false,
|
||||
autoReconnect: false,
|
||||
enableIntegrityCheck: true,
|
||||
totalDownloadedAllTime: 0
|
||||
},
|
||||
emptySession(),
|
||||
createStoragePaths(path.join(root, "state"))
|
||||
);
|
||||
|
||||
manager.addPackages([{ name: "byteacct", links: ["https://dummy/byteacct"] }]);
|
||||
await manager.start();
|
||||
await waitFor(() => !manager.getSnapshot().session.running, 30000);
|
||||
|
||||
const item = Object.values(manager.getSnapshot().session.items)[0];
|
||||
expect(item?.status).toBe("completed");
|
||||
expect(fullServes).toBeGreaterThanOrEqual(2);
|
||||
|
||||
const internal = manager as unknown as {
|
||||
session: { totalDownloadedBytes: number };
|
||||
settings: { totalDownloadedAllTime: number };
|
||||
};
|
||||
expect(internal.session.totalDownloadedBytes).toBe(size);
|
||||
expect(internal.settings.totalDownloadedAllTime).toBe(size);
|
||||
} finally {
|
||||
server.close();
|
||||
await once(server, "close");
|
||||
}
|
||||
}, 40000);
|
||||
|
||||
it("requests a fresh direct link after repeated same-link download failures", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||
tempDirs.push(root);
|
||||
|
||||
Reference in New Issue
Block a user