Compare commits

..

No commits in common. "c4c0110f84cc2ef79a0e4efb5e080729fa7416be" and "fd2cb724a38393b8fba44a09b7b5ea36e3ac7857" have entirely different histories.

6 changed files with 34 additions and 122 deletions

View File

@ -1,6 +1,6 @@
{
"name": "real-debrid-downloader",
"version": "1.7.173",
"version": "1.7.172",
"description": "Desktop downloader",
"main": "build/main/main/main.js",
"author": "Sucukdeluxe",

View File

@ -103,7 +103,7 @@ export class AppController {
this.allDebridWebFallback = new AllDebridWebFallback(() => this.settings.rememberToken);
this.bestDebridWebFallback = new BestDebridWebFallback(() => this.settings.rememberToken);
this.manager = new DownloadManager(this.settings, session, this.storagePaths, {
megaWebUnrestrict: (link: string, signal?: AbortSignal, account?: { login: string; password: string }) => this.megaWebFallback.unrestrict(link, signal, account),
megaWebUnrestrict: (link: string, signal?: AbortSignal) => this.megaWebFallback.unrestrict(link, signal),
allDebridWebUnrestrict: (link: string, signal?: AbortSignal) => this.allDebridWebFallback.unrestrict(link, signal),
realDebridWebUnrestrict: (link: string, signal?: AbortSignal) => this.realDebridWebFallback.unrestrict(link, signal),
bestDebridWebUnrestrict: (link: string, signal?: AbortSignal) => this.bestDebridWebFallback.unrestrict(link, signal),

View File

@ -412,7 +412,7 @@ interface ProviderUnrestrictedLink extends UnrestrictedLink {
providerLabel: string;
}
export type MegaWebUnrestrictor = (link: string, signal?: AbortSignal, account?: { login: string; password: string }) => Promise<UnrestrictedLink | null>;
export type MegaWebUnrestrictor = (link: string, signal?: AbortSignal) => Promise<UnrestrictedLink | null>;
export type AllDebridWebUnrestrictor = (link: string, signal?: AbortSignal) => Promise<UnrestrictedLink | null>;
export type RealDebridWebUnrestrictor = (link: string, signal?: AbortSignal) => Promise<UnrestrictedLink | null>;
export type BestDebridWebUnrestrictor = (link: string, signal?: AbortSignal) => Promise<UnrestrictedLink | null>;
@ -1849,7 +1849,7 @@ class MegaDebridClient {
if (signal?.aborted) {
throw new Error("aborted:debrid");
}
const web = await this.megaWebUnrestrict(link, signal, { login: this.login, password: this.password }).catch((error) => {
const web = await this.megaWebUnrestrict(link, signal).catch((error) => {
lastError = compactErrorText(error);
return null;
});

View File

@ -228,45 +228,43 @@ export class MegaWebFallback {
private getCredentials: () => MegaCredentials;
// Per-Login Session-Cache: login(lowercase) → { cookie, setAt }. Multi-Account-
// Rotation: jeder Account nutzt SEINE eigene Session. Frueher gab es nur EINE
// geteilte Cookie-Session → der Web-Unrestrict lief fuer JEDEN rotierten Account mit
// den Creds des ersten/Legacy-Accounts (settings.megaLogin); der naechste Account
// wurde nie wirklich verwendet (Rotation war wirkungslos).
private sessions = new Map<string, { cookie: string; setAt: number }>();
private cookie = "";
private cookieSetAt = 0;
public constructor(getCredentials: () => MegaCredentials) {
this.getCredentials = getCredentials;
}
public async unrestrict(
link: string,
signal?: AbortSignal,
account?: { login: string; password: string }
): Promise<UnrestrictedLink | null> {
public async unrestrict(link: string, signal?: AbortSignal): Promise<UnrestrictedLink | null> {
const overallSignal = withTimeoutSignal(signal, 180000);
return this.runExclusive(async () => {
throwIfAborted(overallSignal);
// Per-Account-Creds aus der Rotation bevorzugen; sonst Legacy-Default.
const creds = (account && account.login.trim() && account.password.trim())
? account
: this.getCredentials();
const creds = this.getCredentials();
if (!creds.login.trim() || !creds.password.trim()) {
return null;
}
const key = creds.login.trim().toLowerCase();
let cookie = await this.ensureSession(key, creds.login, creds.password, overallSignal);
let generated = await this.generate(link, cookie, overallSignal);
if (!this.cookie || Date.now() - this.cookieSetAt > 20 * 60 * 1000) {
await this.login(creds.login, creds.password, overallSignal);
}
const generated = await this.generate(link, overallSignal);
if (!generated) {
// Session evtl. abgelaufen → fuer DIESEN Login neu einloggen + einmal erneut.
this.sessions.delete(key);
cookie = await this.ensureSession(key, creds.login, creds.password, overallSignal);
generated = await this.generate(link, cookie, overallSignal);
if (!generated) {
this.cookie = "";
await this.login(creds.login, creds.password, overallSignal);
const retry = await this.generate(link, overallSignal);
if (!retry) {
return null;
}
return {
directUrl: retry.directUrl,
fileName: retry.fileName || filenameFromUrl(link),
fileSize: null,
retriesUsed: 0
};
}
return {
directUrl: generated.directUrl,
fileName: generated.fileName || filenameFromUrl(link),
@ -276,20 +274,9 @@ export class MegaWebFallback {
}, overallSignal);
}
/** Liefert ein gueltiges Session-Cookie fuer den gegebenen Login (aus Cache oder
* via frischem Login). Cache-TTL 20 min. */
private async ensureSession(key: string, login: string, password: string, signal?: AbortSignal): Promise<string> {
const existing = this.sessions.get(key);
if (existing && existing.cookie && Date.now() - existing.setAt <= 20 * 60 * 1000) {
return existing.cookie;
}
const cookie = await this.login(login, password, signal);
this.sessions.set(key, { cookie, setAt: Date.now() });
return cookie;
}
public invalidateSession(): void {
this.sessions.clear();
this.cookie = "";
this.cookieSetAt = 0;
}
private async runExclusive<T>(job: () => Promise<T>, signal?: AbortSignal): Promise<T> {
@ -308,7 +295,7 @@ export class MegaWebFallback {
return raceWithAbort(run, signal);
}
private async login(login: string, password: string, signal?: AbortSignal): Promise<string> {
private async login(login: string, password: string, signal?: AbortSignal): Promise<void> {
throwIfAborted(signal);
const response = await fetch(LOGIN_URL, {
method: "POST",
@ -345,17 +332,18 @@ export class MegaWebFallback {
throw new Error("Mega-Web Login ungültig oder Session blockiert");
}
return cookie;
this.cookie = cookie;
this.cookieSetAt = Date.now();
}
private async generate(link: string, cookie: string, signal?: AbortSignal): Promise<{ directUrl: string; fileName: string } | null> {
private async generate(link: string, signal?: AbortSignal): Promise<{ directUrl: string; fileName: string } | null> {
throwIfAborted(signal);
const page = await fetch(DEBRID_URL, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "Mozilla/5.0",
Cookie: cookie,
Cookie: this.cookie,
Referer: DEBRID_REFERER
},
body: new URLSearchParams({
@ -387,7 +375,7 @@ export class MegaWebFallback {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "Mozilla/5.0",
Cookie: cookie,
Cookie: this.cookie,
Referer: DEBRID_REFERER
},
body: new URLSearchParams({
@ -445,7 +433,7 @@ export class MegaWebFallback {
}
public dispose(): void {
this.sessions.clear();
this.cookie = "";
}
}

View File

@ -1522,50 +1522,6 @@ describe("debrid service", () => {
expect(calls).toBe(2);
}, 20000);
it("passes each account's OWN credentials to the Mega web unrestrict during rotation", async () => {
// Echter Root-Cause (Support-Bundle): der Web-Pfad nutzte fuer JEDEN rotierten
// Account die Creds des ersten/Legacy-Accounts → "Account 2" lief in Wahrheit mit
// Account-1-Login → der zweite (funktionierende) Account wurde nie verwendet.
// Jetzt muss jeder Account-Versuch SEINE eigenen Creds an den Web-Unrestrict reichen.
const settings = {
...defaultSettings(),
token: "",
bestToken: "",
allDebridToken: "",
megaLogin: "user1",
megaPassword: "pass1",
megaCredentials: "user1:pass1\nuser2:pass2",
megaDebridPreferApi: false,
providerOrder: [] as const,
providerPrimary: "megadebrid" as const,
providerSecondary: "none" as const,
providerTertiary: "none" as const,
autoProviderFallback: false
};
globalThis.fetch = (async () => new Response("error", { status: 500 })) as typeof fetch;
const accountsSeen: Array<string | undefined> = [];
const megaWeb = vi.fn(async (_link: string, _signal: AbortSignal | undefined, account?: { login: string; password: string }) => {
accountsSeen.push(account?.login);
if (account?.login === "user1") {
// Account 1 am Tageslimit.
throw new Error("Mega-Web: Kein Server für diesen Hoster verfügbar. Bitte versuchen Sie es später noch einmal.");
}
// Account 2 (eigene Creds) loest auf.
return { fileName: "ok.rar", directUrl: "https://mega-web.example/ok.rar", fileSize: null, retriesUsed: 0 };
});
const service = new DebridService(settings, { megaWebUnrestrict: megaWeb });
const result = await service.unrestrictLink("https://rapidgator.net/file/per-account-creds");
// Jeder Account wurde mit SEINEM eigenen Login angesprochen (nicht 2x user1).
expect(accountsSeen).toContain("user1");
expect(accountsSeen).toContain("user2");
// Und der funktionierende Account 2 loest auf.
expect((result as { sourceAccountId?: string }).sourceAccountId).toBe(getMegaDebridAccountId("user2"));
expect(result.directUrl).toBe("https://mega-web.example/ok.rar");
}, 20000);
it("respects provider selection and does not append hidden providers", async () => {
const settings = {
...defaultSettings(),

View File

@ -89,38 +89,6 @@ describe("mega-web-fallback", () => {
expect(ajaxCalls).toBe(1);
});
it("logs in with the per-account credentials passed to unrestrict, not the default", async () => {
const loginsUsed: string[] = [];
globalThis.fetch = vi.fn(async (url: string | URL | Request, opts?: { body?: unknown }) => {
const urlStr = String(url);
if (urlStr.includes("form=login")) {
const params = new URLSearchParams(String(opts?.body ?? ""));
loginsUsed.push(params.get("login") || "");
const headers = new Headers();
headers.append("set-cookie", "session=goodcookie; path=/");
return new Response("", { headers, status: 200 });
}
if (urlStr.includes("page=debrideur")) {
return new Response('<form id="debridForm"></form>', { status: 200 });
}
if (urlStr.includes("form=debrid")) {
return new Response(`<div class="acp-box"><h3>Link: https://mega.debrid/l1</h3><a href="javascript:processDebrid(1,'code1',0)">d</a></div>`, { status: 200 });
}
if (urlStr.includes("ajax=debrid")) {
return new Response(JSON.stringify({ link: "https://mega.direct/ok" }), { status: 200 });
}
return new Response("Not found", { status: 404 });
}) as unknown as typeof fetch;
// getCredentials liefert den DEFAULT/Legacy-Account ...
const fallback = new MegaWebFallback(() => ({ login: "defaultacc", password: "defpw" }));
// ... aber die Rotation übergibt explizit Account 2 — DESSEN Login MUSS verwendet werden.
const result = await fallback.unrestrict("https://mega.debrid/l1", undefined, { login: "account2", password: "pw2" });
expect(result?.directUrl).toBe("https://mega.direct/ok");
expect(loginsUsed).toContain("account2");
expect(loginsUsed).not.toContain("defaultacc");
});
it("throws if login fails to set cookie", async () => {
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
const urlStr = String(url);