From 3ef2ee732af56960973bfbbd7d0525aa8d5afde9 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Fri, 27 Feb 2026 04:40:21 +0100 Subject: [PATCH] Move provider settings to tab and improve DLC filename resolution --- package-lock.json | 4 +- package.json | 2 +- src/main/constants.ts | 2 +- src/main/debrid.ts | 108 +++++++++++++++++++++++++++++++++++ src/main/download-manager.ts | 51 +++++++++++++++++ src/renderer/App.tsx | 68 +++++++++++----------- 6 files changed, 197 insertions(+), 38 deletions(-) diff --git a/package-lock.json b/package-lock.json index 94ebda7..923d065 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "real-debrid-downloader", - "version": "1.1.15", + "version": "1.1.16", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "real-debrid-downloader", - "version": "1.1.15", + "version": "1.1.16", "license": "MIT", "dependencies": { "adm-zip": "^0.5.16", diff --git a/package.json b/package.json index 4d39e46..c4f4e81 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "real-debrid-downloader", - "version": "1.1.15", + "version": "1.1.16", "description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)", "main": "build/main/main/main.js", "author": "Sucukdeluxe", diff --git a/src/main/constants.ts b/src/main/constants.ts index 3583a77..4d2a9e9 100644 --- a/src/main/constants.ts +++ b/src/main/constants.ts @@ -3,7 +3,7 @@ import os from "node:os"; import { AppSettings } from "../shared/types"; export const APP_NAME = "Debrid Download Manager"; -export const APP_VERSION = "1.1.15"; +export const APP_VERSION = "1.1.16"; export const API_BASE_URL = "https://api.real-debrid.com/rest/1.0"; export const DCRYPT_UPLOAD_URL = "https://dcrypt.it/decrypt/upload"; diff --git a/src/main/debrid.ts b/src/main/debrid.ts index d873141..49f4352 100644 --- a/src/main/debrid.ts +++ b/src/main/debrid.ts @@ -24,6 +24,16 @@ type BestDebridRequest = { useAuthHeader: boolean; }; +function canonicalLink(link: string): string { + try { + const parsed = new URL(link); + const query = parsed.searchParams.toString(); + return `${parsed.hostname}${parsed.pathname}${query ? `?${query}` : ""}`.toLowerCase(); + } catch { + return link.trim().toLowerCase(); + } +} + function shouldRetryStatus(status: number): boolean { return status === 429 || status >= 500; } @@ -262,6 +272,79 @@ class AllDebridClient { this.token = token; } + public async getLinkInfos(links: string[]): Promise> { + const result = new Map(); + const canonicalToInput = new Map(); + const uniqueLinks: string[] = []; + + for (const link of links) { + const trimmed = link.trim(); + if (!trimmed) { + continue; + } + const canonical = canonicalLink(trimmed); + if (canonicalToInput.has(canonical)) { + continue; + } + canonicalToInput.set(canonical, trimmed); + uniqueLinks.push(trimmed); + } + + for (let index = 0; index < uniqueLinks.length; index += 32) { + const chunk = uniqueLinks.slice(index, index + 32); + const body = new URLSearchParams(); + for (const link of chunk) { + body.append("link[]", link); + } + + const response = await fetch(`${ALL_DEBRID_API_BASE}/link/infos`, { + method: "POST", + headers: { + Authorization: `Bearer ${this.token}`, + "Content-Type": "application/x-www-form-urlencoded", + "User-Agent": "RD-Node-Downloader/1.1.15" + }, + body + }); + + const text = await response.text(); + const payload = asRecord(parseJson(text)); + if (!response.ok) { + throw new Error(parseError(response.status, text, payload)); + } + + const status = pickString(payload, ["status"]); + if (status && status.toLowerCase() === "error") { + const errorObj = asRecord(payload?.error); + throw new Error(pickString(errorObj, ["message", "code"]) || "AllDebrid API error"); + } + + const data = asRecord(payload?.data); + const infos = Array.isArray(data?.infos) ? data.infos : []; + for (let i = 0; i < infos.length; i += 1) { + const info = asRecord(infos[i]); + if (!info) { + continue; + } + const fileName = pickString(info, ["filename", "fileName"]); + if (!fileName) { + continue; + } + + const responseLink = pickString(info, ["link"]); + const byResponse = canonicalToInput.get(canonicalLink(responseLink)); + const byIndex = chunk[i] || ""; + const original = byResponse || byIndex; + if (!original) { + continue; + } + result.set(original, fileName); + } + } + + return result; + } + public async unrestrictLink(link: string): Promise { let lastError = ""; for (let attempt = 1; attempt <= REQUEST_RETRIES; attempt += 1) { @@ -337,6 +420,31 @@ export class DebridService { this.allDebridClient = new AllDebridClient(next.allDebridToken); } + public async resolveFilenames(links: string[]): Promise> { + const unresolved = links.filter((link) => filenameFromUrl(link) === "download.bin"); + if (unresolved.length === 0) { + return new Map(); + } + + const token = this.settings.allDebridToken.trim(); + if (!token) { + return new Map(); + } + + try { + const infos = await this.allDebridClient.getLinkInfos(unresolved); + const clean = new Map(); + for (const [link, fileName] of infos.entries()) { + if (fileName.trim() && fileName.trim().toLowerCase() !== "download.bin") { + clean.set(link, fileName.trim()); + } + } + return clean; + } catch { + return new Map(); + } + } + public async unrestrictLink(link: string): Promise { const order = uniqueProviderOrder([ this.settings.providerPrimary, diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index ea10431..e65104b 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -165,6 +165,7 @@ export class DownloadManager extends EventEmitter { public addPackages(packages: ParsedPackageInput[]): { addedPackages: number; addedLinks: number } { let addedPackages = 0; let addedLinks = 0; + const unresolvedByLink = new Map(); for (const pkg of packages) { const links = pkg.links.filter((link) => !!link.trim()); if (links.length === 0) { @@ -211,6 +212,11 @@ export class DownloadManager extends EventEmitter { }; packageEntry.itemIds.push(itemId); this.session.items[itemId] = item; + if (fileName === "download.bin") { + const existing = unresolvedByLink.get(link) ?? []; + existing.push(itemId); + unresolvedByLink.set(link, existing); + } addedLinks += 1; } @@ -221,9 +227,54 @@ export class DownloadManager extends EventEmitter { this.persistSoon(); this.emitState(); + if (unresolvedByLink.size > 0) { + void this.resolveQueuedFilenames(unresolvedByLink); + } return { addedPackages, addedLinks }; } + private async resolveQueuedFilenames(unresolvedByLink: Map): Promise { + try { + const resolved = await this.debridService.resolveFilenames(Array.from(unresolvedByLink.keys())); + if (resolved.size === 0) { + return; + } + + let changed = false; + for (const [link, itemIds] of unresolvedByLink.entries()) { + const fileName = resolved.get(link); + if (!fileName || fileName.toLowerCase() === "download.bin") { + continue; + } + const normalized = sanitizeFilename(fileName); + if (!normalized || normalized.toLowerCase() === "download.bin") { + continue; + } + + for (const itemId of itemIds) { + const item = this.session.items[itemId]; + if (!item) { + continue; + } + if (item.fileName !== "download.bin") { + continue; + } + item.fileName = normalized; + item.targetPath = path.join(this.session.packages[item.packageId]?.outputDir || this.settings.outputDir, normalized); + item.updatedAt = nowMs(); + changed = true; + } + } + + if (changed) { + this.persistSoon(); + this.emitState(); + } + } catch (error) { + logger.warn(`Dateinamen-Resolve fehlgeschlagen: ${compactErrorText(error)}`); + } + } + public cancelPackage(packageId: string): void { const pkg = this.session.packages[packageId]; if (!pkg) { diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 5a4b26c..108a93e 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -235,6 +235,40 @@ export function App(): ReactElement {
{tab === "collector" && (
+
+

Linksammler

+
+ + +
+