Compare commits
No commits in common. "50bc6084194d7158d45475e6a97c6672a3f6f08a" and "39c587c8b79aefe18553458b12d50e1f96a68ef4" have entirely different histories.
50bc608419
...
39c587c8b7
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.7.221",
|
"version": "1.7.220",
|
||||||
"description": "Desktop downloader",
|
"description": "Desktop downloader",
|
||||||
"main": "build/main/main/main.js",
|
"main": "build/main/main/main.js",
|
||||||
"author": "Sucukdeluxe",
|
"author": "Sucukdeluxe",
|
||||||
|
|||||||
@ -123,11 +123,6 @@ 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);
|
||||||
@ -2792,23 +2787,6 @@ 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,6 +8976,7 @@ 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();
|
||||||
@ -9016,6 +9017,7 @@ 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;
|
||||||
@ -9991,7 +9993,6 @@ 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) {
|
||||||
@ -10482,6 +10483,7 @@ 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,37 +120,6 @@ 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,29 +63,6 @@ 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, getDebridLinkKeyCooldownStateForTests, 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, 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,78 +572,6 @@ 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,100 +1373,6 @@ 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