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:
parent
a0c58aad2c
commit
d491c21b97
@ -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",
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user