From d491c21b972304e720d6ae2a57c7631fa86c4de1 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Sun, 1 Mar 2026 02:12:06 +0100 Subject: [PATCH] Release v1.4.37 with DLC filenames, instant delete, version display - Parse tags from DLC XML for proper file names instead of deriving from opaque URLs (fixes download.bin display) - Optimistic UI removal for package/item delete (instant feedback) - Show app version in header ("Multi Debrid Downloader vX.X.X") Co-Authored-By: Claude Opus 4.6 --- package.json | 2 +- src/main/app-controller.ts | 3 ++- src/main/container.ts | 50 ++++++++++++++++++++++++++++++----- src/main/download-manager.ts | 6 +++-- src/renderer/App.tsx | 51 +++++++++++++++++++++++++++++++++++- src/shared/types.ts | 1 + 6 files changed, 101 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index bccb8cc..394df61 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "real-debrid-downloader", - "version": "1.4.36", + "version": "1.4.37", "description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)", "main": "build/main/main/main.js", "author": "Sucukdeluxe", diff --git a/src/main/app-controller.ts b/src/main/app-controller.ts index eb671e5..e16d26e 100644 --- a/src/main/app-controller.ts +++ b/src/main/app-controller.ts @@ -165,7 +165,8 @@ export class AppController { const packages = await importDlcContainers(filePaths); const merged: ParsedPackageInput[] = packages.map((pkg) => ({ name: pkg.name, - links: pkg.links + links: pkg.links, + ...(pkg.fileNames ? { fileNames: pkg.fileNames } : {}) })); const result = this.manager.addPackages(merged); return result; diff --git a/src/main/container.ts b/src/main/container.ts index 2027134..d5043ea 100644 --- a/src/main/container.ts +++ b/src/main/container.ts @@ -94,24 +94,60 @@ function parsePackagesFromDlcXml(xml: string): ParsedPackageInput[] { } const links: string[] = []; - const urlRegex = /(.*?)<\/url>/gi; - for (let um = urlRegex.exec(packageBody); um; um = urlRegex.exec(packageBody)) { + const fileNames: string[] = []; + const fileRegex = /([\s\S]*?)<\/file>/gi; + for (let fm = fileRegex.exec(packageBody); fm; fm = fileRegex.exec(packageBody)) { + const fileBody = fm[1] || ""; + const urlMatch = fileBody.match(/(.*?)<\/url>/i); + if (!urlMatch) { + continue; + } try { - const url = Buffer.from((um[1] || "").trim(), "base64").toString("utf8").trim(); - if (isHttpLink(url)) { - links.push(url); + const url = Buffer.from((urlMatch[1] || "").trim(), "base64").toString("utf8").trim(); + if (!isHttpLink(url)) { + continue; } + let fileName = ""; + const fnMatch = fileBody.match(/(.*?)<\/filename>/i); + if (fnMatch?.[1]) { + try { + fileName = Buffer.from(fnMatch[1].trim(), "base64").toString("utf8").trim(); + } catch { + // ignore + } + } + links.push(url); + fileNames.push(sanitizeFilename(fileName)); } catch { // skip broken entries } } + if (links.length === 0) { + const urlRegex = /(.*?)<\/url>/gi; + for (let um = urlRegex.exec(packageBody); um; um = urlRegex.exec(packageBody)) { + try { + const url = Buffer.from((um[1] || "").trim(), "base64").toString("utf8").trim(); + if (isHttpLink(url)) { + links.push(url); + } + } catch { + // skip broken entries + } + } + } + const uniqueLinks = uniquePreserveOrder(links); + const hasFileNames = fileNames.some((fn) => fn.length > 0); if (uniqueLinks.length > 0) { - packages.push({ + const pkg: ParsedPackageInput = { name: sanitizeFilename(packageName || inferPackageNameFromLinks(uniqueLinks) || `Paket-${packages.length + 1}`), links: uniqueLinks - }); + }; + if (hasFileNames) { + pkg.fileNames = fileNames; + } + packages.push(pkg); } } diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index 0d1218b..2bf3be5 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -974,9 +974,11 @@ export class DownloadManager extends EventEmitter { updatedAt: nowMs() }; - for (const link of links) { + for (let linkIdx = 0; linkIdx < links.length; linkIdx += 1) { + const link = links[linkIdx]; const itemId = uuidv4(); - const fileName = filenameFromUrl(link); + const hintName = pkg.fileNames?.[linkIdx]; + const fileName = (hintName && !looksLikeOpaqueFilename(hintName)) ? sanitizeFilename(hintName) : filenameFromUrl(link); const item: DownloadItem = { id: itemId, packageId, diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index a65bdf6..6581293 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -150,6 +150,7 @@ function parseMbpsInput(value: string): number | null { export function App(): ReactElement { const [snapshot, setSnapshot] = useState(emptySnapshot); + const [appVersion, setAppVersion] = useState(""); const [tab, setTab] = useState("collector"); const [statusToast, setStatusToast] = useState(""); const [settingsDraft, setSettingsDraft] = useState(emptySnapshot().settings); @@ -260,6 +261,7 @@ export function App(): ReactElement { useEffect(() => { let unsubscribe: (() => void) | null = null; let unsubClipboard: (() => void) | null = null; + void window.rd.getVersion().then((v) => { if (mountedRef.current) { setAppVersion(v); } }).catch(() => undefined); void window.rd.getSnapshot().then((state) => { if (!mountedRef.current) { return; @@ -974,6 +976,27 @@ export function App(): ReactElement { }, []); const onPackageCancel = useCallback((packageId: string): void => { + setSnapshot((prev) => { + if (!prev) { return prev; } + const nextPackages = { ...prev.session.packages }; + const nextItems = { ...prev.session.items }; + const pkg = nextPackages[packageId]; + if (pkg) { + for (const itemId of pkg.itemIds) { + delete nextItems[itemId]; + } + delete nextPackages[packageId]; + } + return { + ...prev, + session: { + ...prev.session, + packages: nextPackages, + items: nextItems, + packageOrder: prev.session.packageOrder.filter((id) => id !== packageId) + } + }; + }); void window.rd.cancelPackage(packageId).catch((error) => { showToast(`Paket-Löschung fehlgeschlagen: ${String(error)}`, 2400); }); @@ -994,6 +1017,32 @@ export function App(): ReactElement { }, [showToast]); const onPackageRemoveItem = useCallback((itemId: string): void => { + setSnapshot((prev) => { + if (!prev) { return prev; } + const item = prev.session.items[itemId]; + if (!item) { return prev; } + const nextItems = { ...prev.session.items }; + delete nextItems[itemId]; + const nextPackages = { ...prev.session.packages }; + const pkg = nextPackages[item.packageId]; + if (pkg) { + const nextItemIds = pkg.itemIds.filter((id) => id !== itemId); + if (nextItemIds.length === 0) { + delete nextPackages[item.packageId]; + return { + ...prev, + session: { + ...prev.session, + packages: nextPackages, + items: nextItems, + packageOrder: prev.session.packageOrder.filter((id) => id !== item.packageId) + } + }; + } + nextPackages[item.packageId] = { ...pkg, itemIds: nextItemIds }; + } + return { ...prev, session: { ...prev.session, packages: nextPackages, items: nextItems } }; + }); void window.rd.removeItem(itemId).catch((error) => { showToast(`Entfernen fehlgeschlagen: ${String(error)}`, 2400); }); @@ -1097,7 +1146,7 @@ export function App(): ReactElement {
-

Multi Debrid Downloader

+

Multi Debrid Downloader{appVersion ? ` v${appVersion}` : ""}

{snapshot.speedText}
diff --git a/src/shared/types.ts b/src/shared/types.ts index 1175a6f..333d4c6 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -135,6 +135,7 @@ export interface DownloadSummary { export interface ParsedPackageInput { name: string; links: string[]; + fileNames?: string[]; } export interface ContainerImportResult {