Release v1.4.34 with DLC import package-name and 413 fixes
- Use DLC filename as package name in dcrypt fallback instead of inferring from individual URLs (fixes mangled package names) - Add paste endpoint fallback when dcrypt upload returns 413 - Split decryptDlcViaDcrypt into tryDcryptUpload/tryDcryptPaste - Add DCRYPT_PASTE_URL constant - Expand container tests for 413 fallback and dual-failure scenarios Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
edbfba6663
commit
778124312c
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.4.33",
|
"version": "1.4.34",
|
||||||
"description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)",
|
"description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)",
|
||||||
"main": "build/main/main/main.js",
|
"main": "build/main/main/main.js",
|
||||||
"author": "Sucukdeluxe",
|
"author": "Sucukdeluxe",
|
||||||
|
|||||||
@ -8,6 +8,7 @@ export const APP_VERSION: string = packageJson.version;
|
|||||||
export const API_BASE_URL = "https://api.real-debrid.com/rest/1.0";
|
export const API_BASE_URL = "https://api.real-debrid.com/rest/1.0";
|
||||||
|
|
||||||
export const DCRYPT_UPLOAD_URL = "https://dcrypt.it/decrypt/upload";
|
export const DCRYPT_UPLOAD_URL = "https://dcrypt.it/decrypt/upload";
|
||||||
|
export const DCRYPT_PASTE_URL = "https://dcrypt.it/decrypt/paste";
|
||||||
export const DLC_SERVICE_URL = "https://service.jdownloader.org/dlcrypt/service.php?srcType=dlc&destType=pylo&data={KEY}";
|
export const DLC_SERVICE_URL = "https://service.jdownloader.org/dlcrypt/service.php?srcType=dlc&destType=pylo&data={KEY}";
|
||||||
export const DLC_AES_KEY = Buffer.from("cb99b5cbc24db398", "utf8");
|
export const DLC_AES_KEY = Buffer.from("cb99b5cbc24db398", "utf8");
|
||||||
export const DLC_AES_IV = Buffer.from("9bc24cb995cb8db3", "utf8");
|
export const DLC_AES_IV = Buffer.from("9bc24cb995cb8db3", "utf8");
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
import { DCRYPT_UPLOAD_URL, DLC_AES_IV, DLC_AES_KEY, DLC_SERVICE_URL } from "./constants";
|
import { DCRYPT_PASTE_URL, DCRYPT_UPLOAD_URL, DLC_AES_IV, DLC_AES_KEY, DLC_SERVICE_URL } from "./constants";
|
||||||
import { compactErrorText, inferPackageNameFromLinks, isHttpLink, sanitizeFilename, uniquePreserveOrder } from "./utils";
|
import { compactErrorText, inferPackageNameFromLinks, isHttpLink, sanitizeFilename, uniquePreserveOrder } from "./utils";
|
||||||
import { ParsedPackageInput } from "../shared/types";
|
import { ParsedPackageInput } from "../shared/types";
|
||||||
|
|
||||||
@ -162,9 +162,17 @@ async function decryptDlcLocal(filePath: string): Promise<ParsedPackageInput[]>
|
|||||||
return parsePackagesFromDlcXml(xmlData);
|
return parsePackagesFromDlcXml(xmlData);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function decryptDlcViaDcrypt(filePath: string): Promise<ParsedPackageInput[]> {
|
function extractLinksFromResponse(text: string): string[] {
|
||||||
const fileName = path.basename(filePath);
|
const payload = decodeDcryptPayload(text);
|
||||||
const blob = new Blob([new Uint8Array(readDlcFileWithLimit(filePath))]);
|
let links = extractUrlsRecursive(payload);
|
||||||
|
if (links.length === 0) {
|
||||||
|
links = extractUrlsRecursive(text);
|
||||||
|
}
|
||||||
|
return uniquePreserveOrder(links.filter((l) => isHttpLink(l)));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tryDcryptUpload(fileContent: Buffer, fileName: string): Promise<string[] | null> {
|
||||||
|
const blob = new Blob([new Uint8Array(fileContent)]);
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
form.set("dlcfile", blob, fileName);
|
form.set("dlcfile", blob, fileName);
|
||||||
|
|
||||||
@ -172,22 +180,44 @@ async function decryptDlcViaDcrypt(filePath: string): Promise<ParsedPackageInput
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
body: form
|
body: form
|
||||||
});
|
});
|
||||||
|
if (response.status === 413) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(compactErrorText(text));
|
throw new Error(compactErrorText(text));
|
||||||
}
|
}
|
||||||
const payload = decodeDcryptPayload(text);
|
return extractLinksFromResponse(text);
|
||||||
let packages = extractPackagesFromPayload(payload);
|
}
|
||||||
if (packages.length === 1) {
|
|
||||||
const regrouped = groupLinksByName(packages[0].links);
|
async function tryDcryptPaste(fileContent: Buffer): Promise<string[]> {
|
||||||
if (regrouped.length > 1) {
|
const form = new FormData();
|
||||||
packages = regrouped;
|
form.set("content", fileContent.toString("ascii"));
|
||||||
|
|
||||||
|
const response = await fetch(DCRYPT_PASTE_URL, {
|
||||||
|
method: "POST",
|
||||||
|
body: form
|
||||||
|
});
|
||||||
|
const text = await response.text();
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(compactErrorText(text));
|
||||||
}
|
}
|
||||||
|
return extractLinksFromResponse(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function decryptDlcViaDcrypt(filePath: string): Promise<ParsedPackageInput[]> {
|
||||||
|
const fileContent = readDlcFileWithLimit(filePath);
|
||||||
|
const fileName = path.basename(filePath);
|
||||||
|
const packageName = sanitizeFilename(path.basename(filePath, ".dlc")) || "Paket";
|
||||||
|
|
||||||
|
let links = await tryDcryptUpload(fileContent, fileName);
|
||||||
|
if (links === null) {
|
||||||
|
links = await tryDcryptPaste(fileContent);
|
||||||
}
|
}
|
||||||
if (packages.length === 0) {
|
if (links.length === 0) {
|
||||||
packages = groupLinksByName(extractUrlsRecursive(text));
|
return [];
|
||||||
}
|
}
|
||||||
return packages;
|
return [{ name: packageName, links }];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function importDlcContainers(filePaths: string[]): Promise<ParsedPackageInput[]> {
|
export async function importDlcContainers(filePaths: string[]): Promise<ParsedPackageInput[]> {
|
||||||
|
|||||||
@ -27,18 +27,21 @@ describe("container", () => {
|
|||||||
// Just needs to be short enough to pass file limits but fail parsing, triggering dcrypt fallback
|
// Just needs to be short enough to pass file limits but fail parsing, triggering dcrypt fallback
|
||||||
fs.writeFileSync(validFilePath, Buffer.from("Valid but not real DLC content..."));
|
fs.writeFileSync(validFilePath, Buffer.from("Valid but not real DLC content..."));
|
||||||
|
|
||||||
const fetchSpy = vi.fn(async () => {
|
const fetchSpy = vi.fn(async (url: string | URL | Request) => {
|
||||||
// Mock dcrypt response for valid.dlc
|
const urlStr = String(url);
|
||||||
|
if (urlStr.includes("dcrypt.it/decrypt/upload")) {
|
||||||
return new Response("http://example.com/file1.rar\nhttp://example.com/file2.rar", { status: 200 });
|
return new Response("http://example.com/file1.rar\nhttp://example.com/file2.rar", { status: 200 });
|
||||||
|
}
|
||||||
|
return new Response("", { status: 404 });
|
||||||
});
|
});
|
||||||
globalThis.fetch = fetchSpy as unknown as typeof fetch;
|
globalThis.fetch = fetchSpy as unknown as typeof fetch;
|
||||||
|
|
||||||
const result = await importDlcContainers([oversizedFilePath, validFilePath]);
|
const result = await importDlcContainers([oversizedFilePath, validFilePath]);
|
||||||
|
|
||||||
// Expect the oversized to be silently skipped, and valid to be parsed into 2 packages (one per link name)
|
// Expect the oversized to be silently skipped, and valid to be parsed into 1 package with DLC filename
|
||||||
expect(result).toHaveLength(2);
|
expect(result).toHaveLength(1);
|
||||||
expect(result[0].links).toEqual(["http://example.com/file1.rar"]);
|
expect(result[0].name).toBe("valid");
|
||||||
expect(result[1].links).toEqual(["http://example.com/file2.rar"]);
|
expect(result[0].links).toEqual(["http://example.com/file1.rar", "http://example.com/file2.rar"]);
|
||||||
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -62,18 +65,21 @@ describe("container", () => {
|
|||||||
|
|
||||||
const fetchSpy = vi.fn(async (url: string | URL | Request) => {
|
const fetchSpy = vi.fn(async (url: string | URL | Request) => {
|
||||||
const urlStr = String(url);
|
const urlStr = String(url);
|
||||||
if (urlStr.includes("rc")) {
|
if (urlStr.includes("service.jdownloader.org")) {
|
||||||
// Mock local RC service failure (returning 404 or empty string)
|
// Mock local RC service failure (returning 404)
|
||||||
return new Response("", { status: 404 });
|
return new Response("", { status: 404 });
|
||||||
} else {
|
}
|
||||||
|
if (urlStr.includes("dcrypt.it/decrypt/upload")) {
|
||||||
// Mock dcrypt fallback success
|
// Mock dcrypt fallback success
|
||||||
return new Response("http://fallback.com/1", { status: 200 });
|
return new Response("http://fallback.com/1", { status: 200 });
|
||||||
}
|
}
|
||||||
|
return new Response("", { status: 404 });
|
||||||
});
|
});
|
||||||
globalThis.fetch = fetchSpy as unknown as typeof fetch;
|
globalThis.fetch = fetchSpy as unknown as typeof fetch;
|
||||||
|
|
||||||
const result = await importDlcContainers([filePath]);
|
const result = await importDlcContainers([filePath]);
|
||||||
expect(result).toHaveLength(1);
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0].name).toBe("fallback");
|
||||||
expect(result[0].links).toEqual(["http://fallback.com/1"]);
|
expect(result[0].links).toEqual(["http://fallback.com/1"]);
|
||||||
// Should have tried both!
|
// Should have tried both!
|
||||||
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
||||||
@ -90,16 +96,73 @@ describe("container", () => {
|
|||||||
if (urlStr.includes("service.jdownloader.org")) {
|
if (urlStr.includes("service.jdownloader.org")) {
|
||||||
return new Response(`<rc>${Buffer.alloc(16).toString("base64")}</rc>`, { status: 200 });
|
return new Response(`<rc>${Buffer.alloc(16).toString("base64")}</rc>`, { status: 200 });
|
||||||
}
|
}
|
||||||
|
if (urlStr.includes("dcrypt.it/decrypt/upload")) {
|
||||||
return new Response("http://example.com/fallback1", { status: 200 });
|
return new Response("http://example.com/fallback1", { status: 200 });
|
||||||
|
}
|
||||||
|
return new Response("", { status: 404 });
|
||||||
});
|
});
|
||||||
globalThis.fetch = fetchSpy as unknown as typeof fetch;
|
globalThis.fetch = fetchSpy as unknown as typeof fetch;
|
||||||
|
|
||||||
const result = await importDlcContainers([filePath]);
|
const result = await importDlcContainers([filePath]);
|
||||||
expect(result).toHaveLength(1);
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0].name).toBe("invalid-local");
|
||||||
expect(result[0].links).toEqual(["http://example.com/fallback1"]);
|
expect(result[0].links).toEqual(["http://example.com/fallback1"]);
|
||||||
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("falls back to paste endpoint when upload returns 413", async () => {
|
||||||
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dlc-"));
|
||||||
|
tempDirs.push(dir);
|
||||||
|
const filePath = path.join(dir, "big-dlc.dlc");
|
||||||
|
fs.writeFileSync(filePath, Buffer.alloc(100, 1).toString("base64"));
|
||||||
|
|
||||||
|
const fetchSpy = vi.fn(async (url: string | URL | Request) => {
|
||||||
|
const urlStr = String(url);
|
||||||
|
if (urlStr.includes("service.jdownloader.org")) {
|
||||||
|
return new Response("", { status: 404 });
|
||||||
|
}
|
||||||
|
if (urlStr.includes("dcrypt.it/decrypt/upload")) {
|
||||||
|
return new Response("Request Entity Too Large", { status: 413 });
|
||||||
|
}
|
||||||
|
if (urlStr.includes("dcrypt.it/decrypt/paste")) {
|
||||||
|
return new Response("http://paste-fallback.com/file1.rar\nhttp://paste-fallback.com/file2.rar", { status: 200 });
|
||||||
|
}
|
||||||
|
return new Response("", { status: 404 });
|
||||||
|
});
|
||||||
|
globalThis.fetch = fetchSpy as unknown as typeof fetch;
|
||||||
|
|
||||||
|
const result = await importDlcContainers([filePath]);
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0].name).toBe("big-dlc");
|
||||||
|
expect(result[0].links).toEqual(["http://paste-fallback.com/file1.rar", "http://paste-fallback.com/file2.rar"]);
|
||||||
|
// local RC + upload + paste = 3 calls
|
||||||
|
expect(fetchSpy).toHaveBeenCalledTimes(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws when both dcrypt endpoints fail", async () => {
|
||||||
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dlc-"));
|
||||||
|
tempDirs.push(dir);
|
||||||
|
const filePath = path.join(dir, "doomed.dlc");
|
||||||
|
fs.writeFileSync(filePath, Buffer.from("not a valid dlc payload at all"));
|
||||||
|
|
||||||
|
const fetchSpy = vi.fn(async (url: string | URL | Request) => {
|
||||||
|
const urlStr = String(url);
|
||||||
|
if (urlStr.includes("service.jdownloader.org")) {
|
||||||
|
return new Response("", { status: 404 });
|
||||||
|
}
|
||||||
|
if (urlStr.includes("dcrypt.it/decrypt/upload")) {
|
||||||
|
return new Response("Request Entity Too Large", { status: 413 });
|
||||||
|
}
|
||||||
|
if (urlStr.includes("dcrypt.it/decrypt/paste")) {
|
||||||
|
return new Response("paste failure", { status: 500 });
|
||||||
|
}
|
||||||
|
return new Response("", { status: 500 });
|
||||||
|
});
|
||||||
|
globalThis.fetch = fetchSpy as unknown as typeof fetch;
|
||||||
|
|
||||||
|
await expect(importDlcContainers([filePath])).rejects.toThrow(/DLC konnte nicht importiert werden/i);
|
||||||
|
});
|
||||||
|
|
||||||
it("throws clear error when all dlc imports fail", async () => {
|
it("throws clear error when all dlc imports fail", async () => {
|
||||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dlc-"));
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dlc-"));
|
||||||
tempDirs.push(dir);
|
tempDirs.push(dir);
|
||||||
@ -111,7 +174,10 @@ describe("container", () => {
|
|||||||
if (urlStr.includes("service.jdownloader.org")) {
|
if (urlStr.includes("service.jdownloader.org")) {
|
||||||
return new Response("", { status: 404 });
|
return new Response("", { status: 404 });
|
||||||
}
|
}
|
||||||
|
if (urlStr.includes("dcrypt.it/decrypt/upload")) {
|
||||||
return new Response("upstream failure", { status: 500 });
|
return new Response("upstream failure", { status: 500 });
|
||||||
|
}
|
||||||
|
return new Response("", { status: 500 });
|
||||||
});
|
});
|
||||||
globalThis.fetch = fetchSpy as unknown as typeof fetch;
|
globalThis.fetch = fetchSpy as unknown as typeof fetch;
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user