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:
parent
39c587c8b7
commit
ba144f323b
@ -123,6 +123,11 @@ export function getDebridLinkKeyRuntimeStateForTests(keyId: string): DebridLinkR
|
|||||||
return status ? status.state : null;
|
return status ? status.state : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getDebridLinkKeyCooldownStateForTests(keyId: string, now = Date.now()): { remainingMs: number; message: string } | null {
|
||||||
|
const state = getDebridLinkKeyCooldownState(keyId, now);
|
||||||
|
return state ? { remainingMs: state.remainingMs, message: state.message } : null;
|
||||||
|
}
|
||||||
|
|
||||||
function clearDebridLinkKeyCooldownState(keyId: string): void {
|
function clearDebridLinkKeyCooldownState(keyId: string): void {
|
||||||
debridLinkKeyCooldowns.delete(keyId);
|
debridLinkKeyCooldowns.delete(keyId);
|
||||||
debridLinkKeyCooldownDetails.delete(keyId);
|
debridLinkKeyCooldownDetails.delete(keyId);
|
||||||
@ -2787,6 +2792,23 @@ class DebridLinkClient {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
const failure = await this.classifyKeyFailure(error, apiKey, link, signal);
|
const failure = await this.classifyKeyFailure(error, apiKey, link, signal);
|
||||||
const elapsedMs = Date.now() - testStartedAt;
|
const elapsedMs = Date.now() - testStartedAt;
|
||||||
|
const abortText = compactErrorText(error).replace(/^Error:\s*/i, "");
|
||||||
|
if (/aborted/i.test(abortText) && !/timeout/i.test(abortText)) {
|
||||||
|
const ranLongEnough = elapsedMs >= getMegaDebridAbortMinRunMs();
|
||||||
|
if (ranLongEnough) {
|
||||||
|
setDebridLinkKeyCooldownState(apiKey.id, DEBRID_LINK_KEY_COOLDOWN_MS, `Abbruch/Timeout nach ${Math.ceil(elapsedMs / 1000)}s`, "temporary");
|
||||||
|
} else {
|
||||||
|
clearDebridLinkKeyCooldownState(apiKey.id);
|
||||||
|
}
|
||||||
|
failures.push(`Debrid-Link${keyLabel}: ${abortText}`);
|
||||||
|
logAccountRotation("WARN", providerName, rotationLabel, "TIMEOUT_COOLDOWN", {
|
||||||
|
elapsedMs,
|
||||||
|
reason: abortText,
|
||||||
|
cooldownSec: ranLongEnough ? Math.ceil(DEBRID_LINK_KEY_COOLDOWN_MS / 1000) : 0,
|
||||||
|
next: "naechster Key beim Retry"
|
||||||
|
});
|
||||||
|
throw new Error(`Debrid-Link${keyLabel}: ${abortText}`);
|
||||||
|
}
|
||||||
attemptedKeyFailures.push({
|
attemptedKeyFailures.push({
|
||||||
message: `Debrid-Link${keyLabel}: ${failure.message}`,
|
message: `Debrid-Link${keyLabel}: ${failure.message}`,
|
||||||
cooldownMs: failure.cooldownMs,
|
cooldownMs: failure.cooldownMs,
|
||||||
|
|||||||
@ -8976,7 +8976,6 @@ export class DownloadManager extends EventEmitter {
|
|||||||
if (item.attempts < maxAttempts) {
|
if (item.attempts < maxAttempts) {
|
||||||
item.status = "integrity_check";
|
item.status = "integrity_check";
|
||||||
item.progressPercent = 0;
|
item.progressPercent = 0;
|
||||||
this.dropItemContribution(item.id);
|
|
||||||
item.downloadedBytes = 0;
|
item.downloadedBytes = 0;
|
||||||
item.totalBytes = unrestricted.fileSize;
|
item.totalBytes = unrestricted.fileSize;
|
||||||
this.emitState();
|
this.emitState();
|
||||||
@ -9017,7 +9016,6 @@ export class DownloadManager extends EventEmitter {
|
|||||||
} catch {
|
} catch {
|
||||||
}
|
}
|
||||||
this.releaseTargetPath(item.id);
|
this.releaseTargetPath(item.id);
|
||||||
this.dropItemContribution(item.id);
|
|
||||||
item.downloadedBytes = 0;
|
item.downloadedBytes = 0;
|
||||||
item.progressPercent = 0;
|
item.progressPercent = 0;
|
||||||
item.totalBytes = (item.totalBytes || 0) > 0 ? item.totalBytes : null;
|
item.totalBytes = (item.totalBytes || 0) > 0 ? item.totalBytes : null;
|
||||||
@ -9993,6 +9991,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
if (previouslyContributed > 0) {
|
if (previouslyContributed > 0) {
|
||||||
this.session.totalDownloadedBytes = Math.max(0, this.session.totalDownloadedBytes - previouslyContributed);
|
this.session.totalDownloadedBytes = Math.max(0, this.session.totalDownloadedBytes - previouslyContributed);
|
||||||
this.sessionDownloadedBytes = Math.max(0, this.sessionDownloadedBytes - previouslyContributed);
|
this.sessionDownloadedBytes = Math.max(0, this.sessionDownloadedBytes - previouslyContributed);
|
||||||
|
this.settings.totalDownloadedAllTime = Math.max(0, Number(this.settings.totalDownloadedAllTime || 0) - previouslyContributed);
|
||||||
this.itemContributedBytes.set(active.itemId, 0);
|
this.itemContributedBytes.set(active.itemId, 0);
|
||||||
}
|
}
|
||||||
if (existingBytes > 0) {
|
if (existingBytes > 0) {
|
||||||
@ -10483,7 +10482,6 @@ export class DownloadManager extends EventEmitter {
|
|||||||
await fs.promises.rm(effectiveTargetPath, { force: true });
|
await fs.promises.rm(effectiveTargetPath, { force: true });
|
||||||
} catch { }
|
} catch { }
|
||||||
this.releaseTargetPath(active.itemId);
|
this.releaseTargetPath(active.itemId);
|
||||||
this.dropItemContribution(active.itemId);
|
|
||||||
item.downloadedBytes = 0;
|
item.downloadedBytes = 0;
|
||||||
item.progressPercent = 0;
|
item.progressPercent = 0;
|
||||||
throw new Error(`Download zu klein (${written} B) – Hoster-Fehlerseite?${snippet ? ` Inhalt: "${snippet}"` : ""}`);
|
throw new Error(`Download zu klein (${written} B) – Hoster-Fehlerseite?${snippet ? ` Inhalt: "${snippet}"` : ""}`);
|
||||||
|
|||||||
@ -120,6 +120,37 @@ MW-1 (HIGH, Web-Selbstcooldown) + SET-MIG-01 (MED, Legacy-Mega-Demotion). Gitea
|
|||||||
Dokumentiert-nicht-gefixt: DL-1, DL-CONCURRENCY-PILEUP, BYTE-DROP-RETRY-1, extractor.ts-Passwort-Cache-Seam.
|
Dokumentiert-nicht-gefixt: DL-1, DL-CONCURRENCY-PILEUP, BYTE-DROP-RETRY-1, extractor.ts-Passwort-Cache-Seam.
|
||||||
Beim Nutzer (nicht autonom): 60s-Failover-Kappung + gespiegelter Mega-API/Web-Schalter (entscheidungen-offen.md).
|
Beim Nutzer (nicht autonom): 60s-Failover-Kappung + gespiegelter Mega-API/Web-Schalter (entscheidungen-offen.md).
|
||||||
|
|
||||||
|
## Nutzer-Nachforderung (nach Goal-Abschluss): die 3 dokumentierten Funde DOCH umsetzen → v1.7.221
|
||||||
|
Nutzer: "dann mach das beides erstmal" (BYTE-DROP-RETRY-1 + DL-1 + DL-CONCURRENCY-PILEUP). Nicht relitigiert OB,
|
||||||
|
nur WIE (Advisor-gefuehrt, je rot-bewiesen, je full-suite gruen + tsc=6 single-pass).
|
||||||
|
- **BYTE-DROP-RETRY-1 (MED) GEFIXT.** Scope-Disziplin (Advisor): NUR Session-Counter + totalDownloadedAllTime,
|
||||||
|
NICHT recordProviderDownloadedBytes/providerDailyUsageBytes (das gated isProviderDailyLimited = Verhalten,
|
||||||
|
und ist nicht provider-keyed → naive Subtraktion wuerde den falschen Provider-Bucket korrumpieren → bewusst
|
||||||
|
ausgeklammert). Mechanismus (a): an den 3 bestaetigten rm-dann-frisch-Sites (Integrity 8979, too-small 9020,
|
||||||
|
tiny 10486) den `dropItemContribution`-Aufruf ENTFERNT → der itemContributedBytes-Eintrag ueberlebt → die
|
||||||
|
bestehende writeMode-"w"-Reconciliation (9991) subtrahiert ihn korrekt (selbst-korrigierend nach writeMode,
|
||||||
|
Append undercounted nicht). Plus: am selben Punkt (9991) `totalDownloadedAllTime -= previouslyContributed`
|
||||||
|
ergaenzt (spiegelt den Add bei 10311; All-Time wurde NIE subtrahiert → doppelte bei JEDEM frischen Re-Download,
|
||||||
|
nicht nur den dropItemContribution-Pfaden). KEINE 23-Site-Reklassifikation (Advisor: Provider-Usage off-limits
|
||||||
|
→ jede Restfehlklassifikation ist bounded Telemetrie). Guard-Test dl-mgr.test.ts:6152 (Completion-Removal behaelt
|
||||||
|
Session-Total) bleibt gruen. Rot-bewiesen NICHT-vakuum, BEIDE Beine einzeln: Integration durch echten
|
||||||
|
Integrity-Fail-Retry (.md5-Manifest, lokaler HTTP-Server serviert wrong-dann-correct), Assert session==1x UND
|
||||||
|
allTime==1x; Bein 1 (All-Time-Zeile raus) → allTime rot (2x) session gruen; Bein 2 (dropItemContribution zurueck)
|
||||||
|
→ session rot (2x).
|
||||||
|
- **DL-1 (LOW) GEFIXT.** Advisor revidierte den frueheren "braucht neue Oberflaeche"-Call: am Rotations-Catch (2789)
|
||||||
|
ist elapsedMs bereits da → Mega-Gate (2072) gespiegelt. abort-ohne-timeout + elapsedMs < getMegaDebridAbortMinRunMs()
|
||||||
|
→ KEIN Key-Cooldown (User-Cancel bestraft den Key nicht); ran-long-enough → DEBRID_LINK_KEY_COOLDOWN_MS (120s)
|
||||||
|
damit der Retry rotiert; throw bailt die Rotation (verhindert auch die zuvor moegliche Transport-Kaskade ueber
|
||||||
|
mehrere Keys bei aborted-Signal). Neuer Test-Getter getDebridLinkKeyCooldownStateForTests. Rot-bewiesen
|
||||||
|
(quick-cancel → null; ohne Fix 15s gesetzt; long-abort → >60s, ohne Fix 15s).
|
||||||
|
- **DL-CONCURRENCY-PILEUP (MED): untersucht → BEREITS STRUKTURELL GELOEST, kein Eingriff (Nutzer-Entscheidung
|
||||||
|
"Akzeptieren").** getSerializedValidatingLimit("megadebrid-web") = Anzahl nutzbarer (nicht-gecoolter) Accounts
|
||||||
|
(dl-mgr 8047-8055); shouldDelayStartForItem erzwingt es in der Kandidatenwahl (8526) → Ueberschuss-Konvertierungen
|
||||||
|
warten als "queued" im Scheduler, NICHT in den per-Account-Single-Flight-Queues; Depth-Spread (debrid 1981-1986)
|
||||||
|
verteilt die erlaubten 1-pro-Account. MW-1 haelt usableAccounts (= das Limit) korrekt hoch. Ein weiterer
|
||||||
|
Scheduler-Eingriff = redundant ODER schaedlich (Ueber-Admission = echter Pileup) → Advisor-4.-Bug-Risiko. Dem
|
||||||
|
Nutzer vorgelegt (AskUserQuestion) → "Akzeptieren, kein Eingriff".
|
||||||
|
|
||||||
## Runde 1 (laeuft)
|
## Runde 1 (laeuft)
|
||||||
- Discover+Verify-Workflow ueber 7 Subsysteme (scheduler-slots, unrestrict-retry, mega-rotation,
|
- Discover+Verify-Workflow ueber 7 Subsysteme (scheduler-slots, unrestrict-retry, mega-rotation,
|
||||||
classify-cooldown, mega-web-token, provider-chain-timeout, account-availability).
|
classify-cooldown, mega-web-token, provider-chain-timeout, account-availability).
|
||||||
|
|||||||
@ -63,6 +63,29 @@ ODER zwei unabhängige Pro-Modus-Schalter (mehr Kontrolle, aber UI + Migration n
|
|||||||
|
|
||||||
## Erledigt in dieser Runde (zur Info, kein Handlungsbedarf)
|
## Erledigt in dieser Runde (zur Info, kein Handlungsbedarf)
|
||||||
|
|
||||||
|
- **Download-Statistik zählt eine Datei nach einem Integritäts-/Zu-klein-Neuversuch nicht mehr doppelt:**
|
||||||
|
Schlug eine Datei die CRC-/Hash-Prüfung fehl (oder kam zu klein an) und wurde komplett neu geladen, wurde die
|
||||||
|
Dateigröße bisher pro Versuch erneut in die Statistik addiert — die Anzeige „insgesamt heruntergeladen" (Session
|
||||||
|
und Gesamt-Zähler) sowie die daraus berechnete Durchschnittsgeschwindigkeit waren dadurch bei flatterhaften Hostern
|
||||||
|
um die jeweilige Dateigröße aufgebläht (bei großen Archiven mit wiederholten CRC-Fehlern um mehrere GB). Jetzt zählt
|
||||||
|
jede gelieferte Datei genau einmal. Reine Anzeige-/Statistik-Korrektur — Slot-Vergabe, Tageslimits und der
|
||||||
|
Download-Ablauf waren nie betroffen (die Tageslimit-Zähler werden bewusst nicht angefasst, da sie die Provider-Auswahl
|
||||||
|
steuern und der echte Datenverkehr über die Leitung ging). Rot-bewiesener Test, beide Zähler einzeln geprüft.
|
||||||
|
|
||||||
|
- **Debrid-Link: ein abgebrochener Vorgang sperrt den Key nicht mehr unnötig:**
|
||||||
|
Wenn du einen Vorgang abgebrochen hast (oder der Gesamttimeout zuschlug), bevor echte Arbeit lief, bekam der
|
||||||
|
Debrid-Link-Key bisher trotzdem eine 15-Sekunden-Sperre — bei mehreren Abbrüchen in Folge konnte das sogar über
|
||||||
|
mehrere Keys kaskadieren und eine längere providerweite Sperre auslösen. Jetzt wird ein schneller Abbruch (vor der
|
||||||
|
Mindest-Laufzeit) nicht mehr als Key-Fehler gewertet: keine Sperre. Lief der Vorgang dagegen lange genug und brach
|
||||||
|
dann ab (echter langsamer/hängender Key), wird er weiterhin gesperrt, damit der nächste Versuch sauber auf den
|
||||||
|
nächsten Key rotiert. Spiegelt exakt das Verhalten, das es bei Mega-Debrid schon gibt. Rot-bewiesener Test.
|
||||||
|
|
||||||
|
- **Mega-Konvertierungs-Stau (geprüft, kein Eingriff nötig):** Die Zahl gleichzeitiger Mega-Umwandlungen ist bereits
|
||||||
|
auf die Anzahl nutzbarer Accounts gedeckelt — Überschuss wartet sauber im Scheduler statt sich in den Account-
|
||||||
|
Warteschlangen zu stapeln, und der oben beschriebene Mega-Web-Fix hält dieses Limit jetzt korrekt hoch. Ein
|
||||||
|
zusätzlicher Eingriff wäre überflüssig oder würde durch Über-Vergabe erst echten Stau erzeugen. Auf deine
|
||||||
|
Entscheidung hin daher bewusst NICHT verändert.
|
||||||
|
|
||||||
- **Alte Konfiguration: Mega-Debrid fällt nach einem Upgrade nicht mehr still aus der Provider-Reihenfolge:**
|
- **Alte Konfiguration: Mega-Debrid fällt nach einem Upgrade nicht mehr still aus der Provider-Reihenfolge:**
|
||||||
Eine Konfigurationsdatei, die noch von einer sehr alten Version (vor v1.6.90) stammt, kannte die getrennten
|
Eine Konfigurationsdatei, die noch von einer sehr alten Version (vor v1.6.90) stammt, kannte die getrennten
|
||||||
Mega-Debrid „API aktiv"/„Web aktiv"-Schalter noch nicht. Beim Laden wurden diese fehlenden Schalter still auf
|
Mega-Debrid „API aktiv"/„Web aktiv"-Schalter noch nicht. Beim Laden wurden diese fehlenden Schalter still auf
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { defaultSettings, REQUEST_RETRIES } from "../src/main/constants";
|
|||||||
import { parseDebridLinkApiKeys } from "../src/shared/debrid-link-keys";
|
import { parseDebridLinkApiKeys } from "../src/shared/debrid-link-keys";
|
||||||
import { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts";
|
import { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts";
|
||||||
import { getProviderUsageDayKey } from "../src/shared/provider-daily-limits";
|
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;
|
const originalFetch = globalThis.fetch;
|
||||||
|
|
||||||
@ -572,6 +572,78 @@ describe("debrid service", () => {
|
|||||||
expect(getDebridLinkKeyRuntimeStateForTests(key2Id)).toBe("ready");
|
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 () => {
|
it("treats bad Debrid-Link file passwords as fatal and does not rotate keys", async () => {
|
||||||
const settings = {
|
const settings = {
|
||||||
...defaultSettings(),
|
...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 () => {
|
it("requests a fresh direct link after repeated same-link download failures", 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