import { afterEach, describe, expect, it, vi } from "vitest"; import { defaultSettings, REQUEST_RETRIES } from "../src/main/constants"; import { DebridService, extractRapidgatorFilenameFromHtml, filenameFromRapidgatorUrlPath, normalizeResolvedFilename } from "../src/main/debrid"; const originalFetch = globalThis.fetch; afterEach(() => { globalThis.fetch = originalFetch; vi.restoreAllMocks(); }); describe("debrid service", () => { it("falls back to Mega web when Real-Debrid fails", async () => { const settings = { ...defaultSettings(), token: "rd-token", megaLogin: "user", megaPassword: "pass", bestToken: "", providerPrimary: "realdebrid" as const, providerSecondary: "megadebrid" as const, providerTertiary: "bestdebrid" as const, autoProviderFallback: true }; globalThis.fetch = (async (input: RequestInfo | URL): Promise => { const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; if (url.includes("api.real-debrid.com/rest/1.0/unrestrict/link")) { return new Response(JSON.stringify({ error: "traffic_limit" }), { status: 403, headers: { "Content-Type": "application/json" } }); } return new Response("not-found", { status: 404 }); }) as typeof fetch; const megaWeb = vi.fn(async () => ({ fileName: "file.bin", directUrl: "https://mega-web.example/file.bin", fileSize: null, retriesUsed: 0 })); const service = new DebridService(settings, { megaWebUnrestrict: megaWeb }); const result = await service.unrestrictLink("https://rapidgator.net/file/example.part1.rar.html"); expect(result.provider).toBe("megadebrid"); expect(result.directUrl).toBe("https://mega-web.example/file.bin"); expect(megaWeb).toHaveBeenCalledTimes(1); }); it("does not fallback when auto fallback is disabled", async () => { const settings = { ...defaultSettings(), token: "rd-token", megaLogin: "user", megaPassword: "pass", providerPrimary: "realdebrid" as const, providerSecondary: "megadebrid" as const, providerTertiary: "bestdebrid" as const, autoProviderFallback: false }; globalThis.fetch = (async (input: RequestInfo | URL): Promise => { const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; if (url.includes("api.real-debrid.com/rest/1.0/unrestrict/link")) { return new Response("traffic exhausted", { status: 429 }); } return new Response("not-found", { status: 404 }); }) as typeof fetch; const megaWeb = vi.fn(async () => ({ fileName: "unused.bin", directUrl: "https://unused", fileSize: null, retriesUsed: 0 })); const service = new DebridService(settings, { megaWebUnrestrict: megaWeb }); await expect(service.unrestrictLink("https://rapidgator.net/file/example.part2.rar.html")).rejects.toThrow(); expect(megaWeb).toHaveBeenCalledTimes(0); }); it("uses BestDebrid auth header without token query fallback", async () => { const settings = { ...defaultSettings(), token: "", bestToken: "best-token", providerPrimary: "bestdebrid" as const, providerSecondary: "realdebrid" as const, providerTertiary: "megadebrid" as const, autoProviderFallback: true }; const calledUrls: string[] = []; globalThis.fetch = (async (input: RequestInfo | URL): Promise => { const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; calledUrls.push(url); if (url.includes("/api/v1/generateLink?link=")) { return new Response(JSON.stringify({ download: "https://best.example/file.bin", filename: "file.bin", filesize: 2048 }), { status: 200, headers: { "Content-Type": "application/json" } }); } return new Response("not-found", { status: 404 }); }) as typeof fetch; const service = new DebridService(settings); const result = await service.unrestrictLink("https://rapidgator.net/file/example.part3.rar.html"); expect(result.provider).toBe("bestdebrid"); expect(result.fileSize).toBe(2048); expect(calledUrls.some((url) => url.includes("auth="))).toBe(false); }); it("sends Bearer auth header to BestDebrid", async () => { const settings = { ...defaultSettings(), token: "", bestToken: "best-token", providerPrimary: "bestdebrid" as const, providerSecondary: "none" as const, providerTertiary: "none" as const, autoProviderFallback: true }; let authHeader = ""; globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit): Promise => { const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; if (url.includes("/api/v1/generateLink?link=")) { const headers = init?.headers; if (headers instanceof Headers) { authHeader = headers.get("Authorization") || ""; } else if (Array.isArray(headers)) { const tuple = headers.find(([key]) => key.toLowerCase() === "authorization"); authHeader = tuple?.[1] || ""; } else { authHeader = String((headers as Record | undefined)?.Authorization || ""); } return new Response(JSON.stringify({ download: "https://best.example/file.bin", filename: "file.bin", filesize: 42 }), { status: 200, headers: { "Content-Type": "application/json" } }); } return new Response("not-found", { status: 404 }); }) as typeof fetch; const service = new DebridService(settings); const result = await service.unrestrictLink("https://hoster.example/file/abc"); expect(result.provider).toBe("bestdebrid"); expect(authHeader).toBe("Bearer best-token"); }); it("does not retry BestDebrid auth failures (401)", async () => { const settings = { ...defaultSettings(), token: "", bestToken: "best-token", providerPrimary: "bestdebrid" as const, providerSecondary: "none" as const, providerTertiary: "none" as const, autoProviderFallback: true }; let calls = 0; globalThis.fetch = (async (input: RequestInfo | URL): Promise => { const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; if (url.includes("/api/v1/generateLink?link=")) { calls += 1; return new Response(JSON.stringify({ message: "Unauthorized" }), { status: 401, headers: { "Content-Type": "application/json" } }); } return new Response("not-found", { status: 404 }); }) as typeof fetch; const service = new DebridService(settings); await expect(service.unrestrictLink("https://hoster.example/file/no-retry")).rejects.toThrow(); expect(calls).toBe(1); }); it("does not retry AllDebrid auth failures (403)", async () => { const settings = { ...defaultSettings(), allDebridToken: "ad-token", providerPrimary: "alldebrid" as const, providerSecondary: "none" as const, providerTertiary: "none" as const, autoProviderFallback: true }; let calls = 0; globalThis.fetch = (async (input: RequestInfo | URL): Promise => { const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; if (url.includes("api.alldebrid.com/v4/link/unlock")) { calls += 1; return new Response(JSON.stringify({ status: "error", error: { message: "forbidden" } }), { status: 403, headers: { "Content-Type": "application/json" } }); } return new Response("not-found", { status: 404 }); }) as typeof fetch; const service = new DebridService(settings); await expect(service.unrestrictLink("https://hoster.example/file/no-retry-ad")).rejects.toThrow(); expect(calls).toBe(1); }); it("supports AllDebrid unlock", async () => { const settings = { ...defaultSettings(), token: "", bestToken: "", allDebridToken: "ad-token", providerPrimary: "alldebrid" as const, providerSecondary: "realdebrid" as const, providerTertiary: "megadebrid" as const, autoProviderFallback: true }; globalThis.fetch = (async (input: RequestInfo | URL): Promise => { const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; if (url.includes("api.alldebrid.com/v4/link/unlock")) { return new Response(JSON.stringify({ status: "success", data: { link: "https://alldebrid.example/file.bin", filename: "file.bin", filesize: 4096 } }), { status: 200, headers: { "Content-Type": "application/json" } }); } return new Response("not-found", { status: 404 }); }) as typeof fetch; const service = new DebridService(settings); const result = await service.unrestrictLink("https://rapidgator.net/file/example.part4.rar.html"); expect(result.provider).toBe("alldebrid"); expect(result.directUrl).toBe("https://alldebrid.example/file.bin"); expect(result.fileSize).toBe(4096); }); it("treats MegaDebrid as not configured when web fallback callback is unavailable", async () => { const settings = { ...defaultSettings(), megaLogin: "user", megaPassword: "pass", providerPrimary: "megadebrid" as const, providerSecondary: "none" as const, providerTertiary: "none" as const, autoProviderFallback: false }; const service = new DebridService(settings); await expect(service.unrestrictLink("https://rapidgator.net/file/missing-mega-web")).rejects.toThrow(/nicht konfiguriert/i); }); it("uses Mega web path exclusively", async () => { const settings = { ...defaultSettings(), token: "", bestToken: "", allDebridToken: "", megaLogin: "user", megaPassword: "pass", providerPrimary: "megadebrid" as const, providerSecondary: "megadebrid" as const, providerTertiary: "megadebrid" as const, autoProviderFallback: true }; const fetchSpy = vi.fn(async () => new Response("not-found", { status: 404 })); globalThis.fetch = fetchSpy as unknown as typeof fetch; const megaWeb = vi.fn(async () => ({ fileName: "from-web.rar", directUrl: "https://www11.unrestrict.link/download/file/abc/from-web.rar", fileSize: null, retriesUsed: 0 })); const service = new DebridService(settings, { megaWebUnrestrict: megaWeb }); const result = await service.unrestrictLink("https://rapidgator.net/file/abc/from-web.rar.html"); expect(result.provider).toBe("megadebrid"); expect(result.directUrl).toContain("unrestrict.link/download/file/"); expect(megaWeb).toHaveBeenCalledTimes(1); expect(fetchSpy).toHaveBeenCalledTimes(0); }); it("aborts Mega web unrestrict when caller signal is cancelled", async () => { const settings = { ...defaultSettings(), token: "", bestToken: "", allDebridToken: "", megaLogin: "user", megaPassword: "pass", providerPrimary: "megadebrid" as const, providerSecondary: "none" as const, providerTertiary: "none" as const, autoProviderFallback: false }; const megaWeb = vi.fn((_link: string, signal?: AbortSignal): Promise => new Promise((_, reject) => { const onAbort = (): void => reject(new Error("aborted:mega-web-test")); if (signal?.aborted) { onAbort(); return; } signal?.addEventListener("abort", onAbort, { once: true }); })); const service = new DebridService(settings, { megaWebUnrestrict: megaWeb }); const controller = new AbortController(); const abortTimer = setTimeout(() => { controller.abort("test"); }, 200); try { await expect(service.unrestrictLink("https://rapidgator.net/file/abort-mega-web", controller.signal)).rejects.toThrow(/aborted/i); expect(megaWeb).toHaveBeenCalledTimes(1); expect(megaWeb.mock.calls[0]?.[1]).toBe(controller.signal); } finally { clearTimeout(abortTimer); } }); it("respects provider selection and does not append hidden providers", async () => { const settings = { ...defaultSettings(), token: "", bestToken: "", allDebridToken: "ad-token", megaLogin: "user", megaPassword: "pass", providerPrimary: "megadebrid" as const, providerSecondary: "megadebrid" as const, providerTertiary: "megadebrid" as const, autoProviderFallback: true }; let allDebridCalls = 0; globalThis.fetch = (async (input: RequestInfo | URL): Promise => { const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; if (url.includes("api.alldebrid.com/v4/link/unlock")) { allDebridCalls += 1; return new Response(JSON.stringify({ status: "success", data: { link: "https://alldebrid.example/file.bin" } }), { status: 200, headers: { "Content-Type": "application/json" } }); } return new Response("not-found", { status: 404 }); }) as typeof fetch; const megaWeb = vi.fn(async () => null); const service = new DebridService(settings, { megaWebUnrestrict: megaWeb }); await expect(service.unrestrictLink("https://rapidgator.net/file/example.part5.rar.html")).rejects.toThrow(); expect(allDebridCalls).toBe(0); }); it("does not use secondary provider when fallback is disabled and primary is missing", async () => { const settings = { ...defaultSettings(), token: "", megaLogin: "user", megaPassword: "pass", providerPrimary: "realdebrid" as const, providerSecondary: "megadebrid" as const, providerTertiary: "none" as const, autoProviderFallback: false }; const megaWeb = vi.fn(async () => ({ fileName: "should-not-run.bin", directUrl: "https://unused", fileSize: null, retriesUsed: 0 })); const service = new DebridService(settings, { megaWebUnrestrict: megaWeb }); await expect(service.unrestrictLink("https://rapidgator.net/file/example.part5.rar.html")).rejects.toThrow(/nicht konfiguriert/i); expect(megaWeb).toHaveBeenCalledTimes(0); }); it("allows disabling secondary and tertiary providers", async () => { const settings = { ...defaultSettings(), token: "rd-token", megaLogin: "user", megaPassword: "pass", providerPrimary: "realdebrid" as const, providerSecondary: "none" as const, providerTertiary: "none" as const, autoProviderFallback: true }; globalThis.fetch = (async (input: RequestInfo | URL): Promise => { const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; if (url.includes("api.real-debrid.com/rest/1.0/unrestrict/link")) { return new Response(JSON.stringify({ error: "traffic_limit" }), { status: 403, headers: { "Content-Type": "application/json" } }); } return new Response("not-found", { status: 404 }); }) as typeof fetch; const megaWeb = vi.fn(async () => ({ fileName: "unused.bin", directUrl: "https://unused", fileSize: null, retriesUsed: 0 })); const service = new DebridService(settings, { megaWebUnrestrict: megaWeb }); await expect(service.unrestrictLink("https://rapidgator.net/file/example.part6.rar.html")).rejects.toThrow(); expect(megaWeb).toHaveBeenCalledTimes(0); }); it("resolves rapidgator filename from page when provider returns hash", async () => { const settings = { ...defaultSettings(), token: "rd-token", providerPrimary: "realdebrid" as const, providerSecondary: "none" as const, providerTertiary: "none" as const, autoProviderFallback: true }; globalThis.fetch = (async (input: RequestInfo | URL): Promise => { const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; if (url.includes("api.real-debrid.com/rest/1.0/unrestrict/link")) { return new Response(JSON.stringify({ download: "https://cdn.example/file.bin", filename: "6f09df2984fe01378537c7cd8d7fa7ce", filesize: 2048 }), { status: 200, headers: { "Content-Type": "application/json" } }); } if (url.includes("rapidgator.net/file/6f09df2984fe01378537c7cd8d7fa7ce")) { return new Response("download file Banshee.S04E01.German.DL.720p.part01.rar - Rapidgator", { status: 200, headers: { "Content-Type": "text/html" } }); } return new Response("not-found", { status: 404 }); }) as typeof fetch; const service = new DebridService(settings); const result = await service.unrestrictLink("https://rapidgator.net/file/6f09df2984fe01378537c7cd8d7fa7ce"); expect(result.provider).toBe("realdebrid"); expect(result.fileName).toBe("Banshee.S04E01.German.DL.720p.part01.rar"); }); it("resolves filenames for rg.to links", async () => { const settings = { ...defaultSettings(), allDebridToken: "" }; const link = "https://rg.to/file/685cec6dcc1837dc725755fc9c726dd9"; globalThis.fetch = (async (input: RequestInfo | URL): Promise => { const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; if (url === link) { return new Response("Download file Bulletproof.S01E01.German.DL.DD20.Synced.720p.AmazonHD.h264-GDR.part01.rar", { status: 200, headers: { "Content-Type": "text/html" } }); } return new Response("not-found", { status: 404 }); }) as typeof fetch; const service = new DebridService(settings); const resolved = await service.resolveFilenames([link]); expect(resolved.get(link)).toBe("Bulletproof.S01E01.German.DL.DD20.Synced.720p.AmazonHD.h264-GDR.part01.rar"); }); it("does not unrestrict non-rapidgator links during filename scan", async () => { const settings = { ...defaultSettings(), token: "rd-token", providerPrimary: "realdebrid" as const, providerSecondary: "none" as const, providerTertiary: "none" as const, autoProviderFallback: true, allDebridToken: "" }; const linkFromPage = "https://rapidgator.net/file/11111111111111111111111111111111"; const linkFromProvider = "https://hoster.example/file/22222222222222222222222222222222"; let unrestrictCalls = 0; globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit): Promise => { const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; if (url === linkFromPage) { return new Response("Download file from-page.part1.rar", { status: 200, headers: { "Content-Type": "text/html" } }); } if (url.includes("api.real-debrid.com/rest/1.0/unrestrict/link")) { unrestrictCalls += 1; const body = init?.body; const bodyText = body instanceof URLSearchParams ? body.toString() : String(body || ""); const linkValue = new URLSearchParams(bodyText).get("link") || ""; if (linkValue === linkFromProvider) { return new Response(JSON.stringify({ download: "https://cdn.example/from-provider", filename: "from-provider.part2.rar", filesize: 1024 }), { status: 200, headers: { "Content-Type": "application/json" } }); } } return new Response("not-found", { status: 404 }); }) as typeof fetch; const service = new DebridService(settings); const events: Array<{ link: string; fileName: string }> = []; const resolved = await service.resolveFilenames([linkFromPage, linkFromProvider], (link, fileName) => { events.push({ link, fileName }); }); expect(resolved.get(linkFromPage)).toBe("from-page.part1.rar"); expect(resolved.has(linkFromProvider)).toBe(false); expect(unrestrictCalls).toBe(0); expect(events).toEqual(expect.arrayContaining([ { link: linkFromPage, fileName: "from-page.part1.rar" } ])); }); it("does not unrestrict rapidgator links during filename scan after page lookup miss", async () => { const settings = { ...defaultSettings(), token: "rd-token", providerPrimary: "realdebrid" as const, providerSecondary: "none" as const, providerTertiary: "none" as const, allDebridToken: "" }; const link = "https://rapidgator.net/file/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; let unrestrictCalls = 0; globalThis.fetch = (async (input: RequestInfo | URL): Promise => { const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; if (url.includes("api.real-debrid.com/rest/1.0/unrestrict/link")) { unrestrictCalls += 1; return new Response(JSON.stringify({ error: "should-not-be-called" }), { status: 500, headers: { "Content-Type": "application/json" } }); } if (url === link) { return new Response("not found", { status: 404 }); } return new Response("not-found", { status: 404 }); }) as typeof fetch; const service = new DebridService(settings); const resolved = await service.resolveFilenames([link]); expect(resolved.size).toBe(0); expect(unrestrictCalls).toBe(0); }); it("maps AllDebrid filename infos by index when response link is missing", async () => { const settings = { ...defaultSettings(), token: "", bestToken: "", allDebridToken: "ad-token", providerPrimary: "realdebrid" as const, providerSecondary: "none" as const, providerTertiary: "none" as const, autoProviderFallback: true }; const linkA = "https://rapidgator.net/file/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; const linkB = "https://rapidgator.net/file/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; globalThis.fetch = (async (input: RequestInfo | URL): Promise => { const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; if (url.includes("api.alldebrid.com/v4/link/infos")) { return new Response(JSON.stringify({ status: "success", data: { infos: [ { filename: "wrong-a.mkv" }, { filename: "wrong-b.mkv" } ] } }), { status: 200, headers: { "Content-Type": "application/json" } }); } if (url === linkA || url === linkB) { return new Response("no title", { status: 404 }); } return new Response("not-found", { status: 404 }); }) as typeof fetch; const service = new DebridService(settings); const resolved = await service.resolveFilenames([linkA, linkB]); expect(resolved.get(linkA)).toBe("wrong-a.mkv"); expect(resolved.get(linkB)).toBe("wrong-b.mkv"); expect(resolved.size).toBe(2); }); it("retries AllDebrid filename infos after transient server error", async () => { const settings = { ...defaultSettings(), allDebridToken: "ad-token" }; const link = "https://rapidgator.net/file/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; let infoCalls = 0; globalThis.fetch = (async (input: RequestInfo | URL): Promise => { const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; if (url.includes("api.alldebrid.com/v4/link/infos")) { infoCalls += 1; if (infoCalls === 1) { return new Response("temporary error", { status: 500 }); } return new Response(JSON.stringify({ status: "success", data: { infos: [ { link, filename: "resolved-from-infos.mkv" } ] } }), { status: 200, headers: { "Content-Type": "application/json" } }); } return new Response("not-found", { status: 404 }); }) as typeof fetch; const service = new DebridService(settings); const resolved = await service.resolveFilenames([link]); expect(resolved.get(link)).toBe("resolved-from-infos.mkv"); expect(infoCalls).toBe(2); }); it("retries AllDebrid filename infos when HTML challenge is returned", async () => { const settings = { ...defaultSettings(), allDebridToken: "ad-token" }; const link = "https://rapidgator.net/file/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; let infoCalls = 0; let pageCalls = 0; globalThis.fetch = (async (input: RequestInfo | URL): Promise => { const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; if (url.includes("api.alldebrid.com/v4/link/infos")) { infoCalls += 1; return new Response("cf challenge", { status: 200, headers: { "Content-Type": "text/html" } }); } if (url === link) { pageCalls += 1; } return new Response("not-found", { status: 404 }); }) as typeof fetch; const service = new DebridService(settings); const resolved = await service.resolveFilenames([link]); expect(resolved.size).toBe(0); expect(infoCalls).toBe(REQUEST_RETRIES); expect(pageCalls).toBe(1); }); }); describe("normalizeResolvedFilename", () => { it("strips HTML entities", () => { expect(normalizeResolvedFilename("Show.S01E01.German.DL.720p.part01.rar")).toBe("Show.S01E01.German.DL.720p.part01.rar"); expect(normalizeResolvedFilename("File&Name.part1.rar")).toBe("File&Name.part1.rar"); expect(normalizeResolvedFilename("File"Name".part1.rar")).toBe('File"Name".part1.rar'); }); it("strips HTML tags and collapses whitespace", () => { // Tags are replaced by spaces, then multiple spaces collapsed const result = normalizeResolvedFilename("Show.S01E01.part01.rar"); expect(result).toBe("Show.S01E01 .part01.rar"); // Entity decoding happens before tag removal, so <...> becomes <...> then gets stripped const entityTagResult = normalizeResolvedFilename("File<Tag>.part1.rar"); expect(entityTagResult).toBe("File .part1.rar"); }); it("strips 'download file' prefix", () => { expect(normalizeResolvedFilename("Download file Show.S01E01.part01.rar")).toBe("Show.S01E01.part01.rar"); expect(normalizeResolvedFilename("download file Movie.2024.mkv")).toBe("Movie.2024.mkv"); }); it("strips Rapidgator suffix", () => { expect(normalizeResolvedFilename("Show.S01E01.part01.rar - Rapidgator")).toBe("Show.S01E01.part01.rar"); expect(normalizeResolvedFilename("Movie.mkv | Rapidgator.net")).toBe("Movie.mkv"); }); it("returns empty for opaque or non-filename values", () => { expect(normalizeResolvedFilename("")).toBe(""); expect(normalizeResolvedFilename("just some text")).toBe(""); expect(normalizeResolvedFilename("e51f6809bb6ca615601f5ac5db433737")).toBe(""); expect(normalizeResolvedFilename("download.bin")).toBe(""); }); it("handles combined transforms", () => { // "Download file" prefix stripped, & decoded to &, "- Rapidgator" suffix stripped expect(normalizeResolvedFilename("Download file Show.S01E01.part01.rar - Rapidgator")) .toBe("Show.S01E01.part01.rar"); }); }); describe("filenameFromRapidgatorUrlPath", () => { it("extracts filename from standard rapidgator URL", () => { expect(filenameFromRapidgatorUrlPath("https://rapidgator.net/file/abc123/Show.S01E01.part01.rar.html")) .toBe("Show.S01E01.part01.rar"); }); it("extracts filename without .html suffix", () => { expect(filenameFromRapidgatorUrlPath("https://rapidgator.net/file/abc123/Movie.2024.mkv")) .toBe("Movie.2024.mkv"); }); it("returns empty for hash-only URL paths", () => { expect(filenameFromRapidgatorUrlPath("https://rapidgator.net/file/e51f6809bb6ca615601f5ac5db433737")) .toBe(""); }); it("returns empty for invalid URLs", () => { expect(filenameFromRapidgatorUrlPath("not-a-url")).toBe(""); expect(filenameFromRapidgatorUrlPath("")).toBe(""); }); it("handles URL-encoded path segments", () => { expect(filenameFromRapidgatorUrlPath("https://rapidgator.net/file/id/Show%20Name.S01E01.part01.rar.html")) .toBe("Show Name.S01E01.part01.rar"); }); }); describe("extractRapidgatorFilenameFromHtml", () => { it("extracts filename from title tag", () => { const html = "Download file Show.S01E01.German.DL.720p.part01.rar - Rapidgator"; expect(extractRapidgatorFilenameFromHtml(html)).toBe("Show.S01E01.German.DL.720p.part01.rar"); }); it("extracts filename from og:title meta tag", () => { const html = ''; expect(extractRapidgatorFilenameFromHtml(html)).toBe("Movie.2024.German.DL.1080p.mkv"); }); it("extracts filename from reversed og:title attribute order", () => { const html = ''; expect(extractRapidgatorFilenameFromHtml(html)).toBe("Movie.2024.German.DL.1080p.mkv"); }); it("returns empty for HTML without recognizable filenames", () => { const html = "Rapidgator: Fast, Pair and UnlimitedNo file here"; expect(extractRapidgatorFilenameFromHtml(html)).toBe(""); }); it("returns empty for empty HTML", () => { expect(extractRapidgatorFilenameFromHtml("")).toBe(""); }); it("ignores broad body text that is not a labeled filename", () => { const html = "Please download file now from mirror.mkv"; expect(extractRapidgatorFilenameFromHtml(html)).toBe(""); }); it("extracts from File name label in page body", () => { const html = 'File name: Show.S02E03.720p.part01.rar'; expect(extractRapidgatorFilenameFromHtml(html)).toBe("Show.S02E03.720p.part01.rar"); }); });