Release v1.4.37 with DLC filenames, instant delete, version display

- Parse <filename> 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 <noreply@anthropic.com>
This commit is contained in:
Sucukdeluxe 2026-03-01 02:12:06 +01:00
parent a0c58aad2c
commit d491c21b97
6 changed files with 101 additions and 12 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "real-debrid-downloader", "name": "real-debrid-downloader",
"version": "1.4.36", "version": "1.4.37",
"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",

View File

@ -165,7 +165,8 @@ export class AppController {
const packages = await importDlcContainers(filePaths); const packages = await importDlcContainers(filePaths);
const merged: ParsedPackageInput[] = packages.map((pkg) => ({ const merged: ParsedPackageInput[] = packages.map((pkg) => ({
name: pkg.name, name: pkg.name,
links: pkg.links links: pkg.links,
...(pkg.fileNames ? { fileNames: pkg.fileNames } : {})
})); }));
const result = this.manager.addPackages(merged); const result = this.manager.addPackages(merged);
return result; return result;

View File

@ -94,24 +94,60 @@ function parsePackagesFromDlcXml(xml: string): ParsedPackageInput[] {
} }
const links: string[] = []; const links: string[] = [];
const urlRegex = /<url>(.*?)<\/url>/gi; const fileNames: string[] = [];
for (let um = urlRegex.exec(packageBody); um; um = urlRegex.exec(packageBody)) { const fileRegex = /<file>([\s\S]*?)<\/file>/gi;
for (let fm = fileRegex.exec(packageBody); fm; fm = fileRegex.exec(packageBody)) {
const fileBody = fm[1] || "";
const urlMatch = fileBody.match(/<url>(.*?)<\/url>/i);
if (!urlMatch) {
continue;
}
try { try {
const url = Buffer.from((um[1] || "").trim(), "base64").toString("utf8").trim(); const url = Buffer.from((urlMatch[1] || "").trim(), "base64").toString("utf8").trim();
if (isHttpLink(url)) { if (!isHttpLink(url)) {
links.push(url); continue;
} }
let fileName = "";
const fnMatch = fileBody.match(/<filename>(.*?)<\/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 { } catch {
// skip broken entries // skip broken entries
} }
} }
if (links.length === 0) {
const urlRegex = /<url>(.*?)<\/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 uniqueLinks = uniquePreserveOrder(links);
const hasFileNames = fileNames.some((fn) => fn.length > 0);
if (uniqueLinks.length > 0) { if (uniqueLinks.length > 0) {
packages.push({ const pkg: ParsedPackageInput = {
name: sanitizeFilename(packageName || inferPackageNameFromLinks(uniqueLinks) || `Paket-${packages.length + 1}`), name: sanitizeFilename(packageName || inferPackageNameFromLinks(uniqueLinks) || `Paket-${packages.length + 1}`),
links: uniqueLinks links: uniqueLinks
}); };
if (hasFileNames) {
pkg.fileNames = fileNames;
}
packages.push(pkg);
} }
} }

View File

@ -974,9 +974,11 @@ export class DownloadManager extends EventEmitter {
updatedAt: nowMs() updatedAt: nowMs()
}; };
for (const link of links) { for (let linkIdx = 0; linkIdx < links.length; linkIdx += 1) {
const link = links[linkIdx];
const itemId = uuidv4(); const itemId = uuidv4();
const fileName = filenameFromUrl(link); const hintName = pkg.fileNames?.[linkIdx];
const fileName = (hintName && !looksLikeOpaqueFilename(hintName)) ? sanitizeFilename(hintName) : filenameFromUrl(link);
const item: DownloadItem = { const item: DownloadItem = {
id: itemId, id: itemId,
packageId, packageId,

View File

@ -150,6 +150,7 @@ function parseMbpsInput(value: string): number | null {
export function App(): ReactElement { export function App(): ReactElement {
const [snapshot, setSnapshot] = useState<UiSnapshot>(emptySnapshot); const [snapshot, setSnapshot] = useState<UiSnapshot>(emptySnapshot);
const [appVersion, setAppVersion] = useState("");
const [tab, setTab] = useState<Tab>("collector"); const [tab, setTab] = useState<Tab>("collector");
const [statusToast, setStatusToast] = useState(""); const [statusToast, setStatusToast] = useState("");
const [settingsDraft, setSettingsDraft] = useState<AppSettings>(emptySnapshot().settings); const [settingsDraft, setSettingsDraft] = useState<AppSettings>(emptySnapshot().settings);
@ -260,6 +261,7 @@ export function App(): ReactElement {
useEffect(() => { useEffect(() => {
let unsubscribe: (() => void) | null = null; let unsubscribe: (() => void) | null = null;
let unsubClipboard: (() => 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) => { void window.rd.getSnapshot().then((state) => {
if (!mountedRef.current) { if (!mountedRef.current) {
return; return;
@ -974,6 +976,27 @@ export function App(): ReactElement {
}, []); }, []);
const onPackageCancel = useCallback((packageId: string): void => { 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) => { void window.rd.cancelPackage(packageId).catch((error) => {
showToast(`Paket-Löschung fehlgeschlagen: ${String(error)}`, 2400); showToast(`Paket-Löschung fehlgeschlagen: ${String(error)}`, 2400);
}); });
@ -994,6 +1017,32 @@ export function App(): ReactElement {
}, [showToast]); }, [showToast]);
const onPackageRemoveItem = useCallback((itemId: string): void => { 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) => { void window.rd.removeItem(itemId).catch((error) => {
showToast(`Entfernen fehlgeschlagen: ${String(error)}`, 2400); showToast(`Entfernen fehlgeschlagen: ${String(error)}`, 2400);
}); });
@ -1097,7 +1146,7 @@ export function App(): ReactElement {
<header className="top-header"> <header className="top-header">
<div className="header-spacer" /> <div className="header-spacer" />
<div className="title-block"> <div className="title-block">
<h1>Multi Debrid Downloader</h1> <h1>Multi Debrid Downloader{appVersion ? ` v${appVersion}` : ""}</h1>
</div> </div>
<div className="metrics"> <div className="metrics">
<div>{snapshot.speedText}</div> <div>{snapshot.speedText}</div>

View File

@ -135,6 +135,7 @@ export interface DownloadSummary {
export interface ParsedPackageInput { export interface ParsedPackageInput {
name: string; name: string;
links: string[]; links: string[];
fileNames?: string[];
} }
export interface ContainerImportResult { export interface ContainerImportResult {