release: prepare v2.0.55 metadata preview

Publish pre-download metadata resolution for 1Fichier and DDownload together with the rebuilt package-oriented link collector. Restore structured queue JSON imports and direct Ctrl+L/File-menu access to link analysis before creating release artifacts.
This commit is contained in:
Sucukdeluxe
2026-08-22 03:03:19 +02:00
parent b03f6ee1bc
commit 6a8a3828b8
5 changed files with 110 additions and 17 deletions
+4
View File
@@ -4,6 +4,8 @@ All notable changes to Multi-Debrid Downloader are documented in this file.
## [Unreleased] ## [Unreleased]
## [2.0.55] - 2026-08-22
### 1Fichier imports ### 1Fichier imports
- Resolve original 1Fichier filenames, exact sizes, and availability in batches before downloads start. - Resolve original 1Fichier filenames, exact sizes, and availability in batches before downloads start.
@@ -23,6 +25,8 @@ All notable changes to Multi-Debrid Downloader are documented in this file.
- Rebuilt the link collector as a package-oriented preview with expandable file rows, resolved metadata, availability filters, stable selection, and selected or complete transfer to Downloads. - Rebuilt the link collector as a package-oriented preview with expandable file rows, resolved metadata, availability filters, stable selection, and selected or complete transfer to Downloads.
- Route pasted links, clipboard detections, text files, drag-and-drop, and DLC containers through inspection before they enter the download queue. - Route pasted links, clipboard detections, text files, drag-and-drop, and DLC containers through inspection before they enter the download queue.
- Show known hosters as icons with full-name tooltips and a text fallback when an icon cannot be loaded. - Show known hosters as icons with full-name tooltips and a text fallback when an icon cannot be loaded.
- Restore exported queue JSON through the structured queue importer while keeping text files in the link collector preview.
- Open the link analysis dialog directly from Ctrl+L and the File menu.
## [2.0.54] - 2026-08-21 ## [2.0.54] - 2026-08-21
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "multi-debrid-downloader", "name": "multi-debrid-downloader",
"version": "2.0.54", "version": "2.0.55",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "multi-debrid-downloader", "name": "multi-debrid-downloader",
"version": "2.0.54", "version": "2.0.55",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"adm-zip": "0.6.0", "adm-zip": "0.6.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "multi-debrid-downloader", "name": "multi-debrid-downloader",
"version": "2.0.54", "version": "2.0.55",
"description": "Desktop downloader", "description": "Desktop downloader",
"main": "build/main/main/main.js", "main": "build/main/main/main.js",
"author": "Sucukdeluxe", "author": "Sucukdeluxe",
+65 -13
View File
@@ -837,7 +837,29 @@ export function getSnapshotRenderDelay(itemCount: number, running: boolean, acti
if (activeTab !== "downloads") delay = Math.max(delay, 800); if (activeTab !== "downloads") delay = Math.max(delay, 800);
return delay; return delay;
} }
export function classifyCollectorImportFileName(fileName: string): "queue" | "links" | "unsupported" {
const normalized = String(fileName || "").trim().toLowerCase();
if (normalized.endsWith(".json")) return "queue";
if (normalized.endsWith(".txt")) return "links";
return "unsupported";
}
export async function readCollectorImportFiles(
files: ReadonlyArray<{ name: string; text: () => Promise<string> }>
): Promise<{ queueJson: string[]; linkText: string }> {
const queueJson: string[] = [];
const linkText: string[] = [];
for (const file of files) {
const kind = classifyCollectorImportFileName(file.name);
if (kind === "unsupported") continue;
const text = await file.text();
if (kind === "queue") queueJson.push(text);
else linkText.push(text);
}
return { queueJson, linkText: linkText.join("\n") };
}
const KNOWN_HOSTERS: { id: string; label: string }[] = [ const KNOWN_HOSTERS: { id: string; label: string }[] = [
{ id: "rapidgator", label: "Rapidgator" }, { id: "rapidgator", label: "Rapidgator" },
{ id: "uploaded", label: "Uploaded" }, { id: "uploaded", label: "Uploaded" },
@@ -3498,9 +3520,35 @@ export function App(): ReactElement {
}); });
}; };
onImportDlcRef.current = onImportDlc; onImportDlcRef.current = onImportDlc;
const onDrop = async (event: DragEvent<HTMLElement>): Promise<void> => { const importQueueJsonTexts = async (queueJson: string[]): Promise<void> => {
if (queueJson.length === 0) return;
setCollectorError("");
await performQuickAction(async () => {
await persistDraftSettings();
const existingIds = new Set(Object.keys(snapshotRef.current.session.packages));
let addedPackages = 0;
let addedLinks = 0;
for (const json of queueJson) {
const result = await window.rd.importQueue(json);
addedPackages += result.addedPackages;
addedLinks += result.addedLinks;
}
if (addedLinks === 0) {
setCollectorError("Keine gültigen Links in der Queue-Datei gefunden");
showToast("Keine gültigen Links in der Queue-Datei gefunden", 3000);
return;
}
showToast(`Importiert: ${addedPackages} Paket(e), ${addedLinks} Link(s)`);
if (snapshotRef.current.settings.collapseNewPackages) await collapseNewPackages(existingIds);
}, (error) => {
setCollectorError(`Import fehlgeschlagen: ${String(error)}`);
showToast(`Import fehlgeschlagen: ${String(error)}`, 2600);
});
};
const onDrop = async (event: DragEvent<HTMLElement>): Promise<void> => {
event.preventDefault(); event.preventDefault();
dragDepthRef.current = 0; dragDepthRef.current = 0;
dragOverRef.current = false; dragOverRef.current = false;
@@ -3515,8 +3563,9 @@ export function App(): ReactElement {
if (dlc.length > 0) { if (dlc.length > 0) {
await enqueueCollectorInspection(() => window.rd.inspectCollectorContainers(dlc, Date.now())); await enqueueCollectorInspection(() => window.rd.inspectCollectorContainers(dlc, Date.now()));
} else if (importFiles.length > 0) { } else if (importFiles.length > 0) {
const importedText = (await Promise.all(importFiles.map((file) => file.text()))).join("\n"); const { queueJson, linkText } = await readCollectorImportFiles(importFiles);
await inspectCollectorRawText(importedText); await importQueueJsonTexts(queueJson);
if (linkText.trim()) await inspectCollectorRawText(linkText);
} else if (droppedText.trim()) { } else if (droppedText.trim()) {
await inspectCollectorRawText(droppedText); await inspectCollectorRawText(droppedText);
} }
@@ -3566,7 +3615,9 @@ export function App(): ReactElement {
return; return;
} }
releasePickerBusy(); releasePickerBusy();
await inspectCollectorRawText(await file.text()); const { queueJson, linkText } = await readCollectorImportFiles([file]);
await importQueueJsonTexts(queueJson);
if (linkText.trim()) await inspectCollectorRawText(linkText);
}; };
clearImportQueueFocusListener(); clearImportQueueFocusListener();
@@ -3673,6 +3724,7 @@ export function App(): ReactElement {
const openCollectorInput = (): void => { const openCollectorInput = (): void => {
setCollectorError(""); setCollectorError("");
setTab("collector");
setCollectorInput({ draft: "" }); setCollectorInput({ draft: "" });
}; };
@@ -4478,11 +4530,11 @@ export function App(): ReactElement {
void window.rd.quit(); void window.rd.quit();
return; return;
} }
if (!e.shiftKey && e.key.toLowerCase() === "l") { if (!e.shiftKey && e.key.toLowerCase() === "l") {
if (inInput) return; if (inInput) return;
e.preventDefault(); e.preventDefault();
setTab("collector"); openCollectorInput();
setOpenMenu(null); setOpenMenu(null);
return; return;
} }
if (!e.shiftKey && e.key.toLowerCase() === "p") { if (!e.shiftKey && e.key.toLowerCase() === "p") {
@@ -5613,7 +5665,7 @@ export function App(): ReactElement {
Datei Datei
</button> </button>
<div aria-hidden={openMenu !== "datei"} className={`menu-dropdown${openMenu === "datei" ? " is-open" : ""}`}> <div aria-hidden={openMenu !== "datei"} className={`menu-dropdown${openMenu === "datei" ? " is-open" : ""}`}>
<button className="menu-dropdown-item" onClick={() => { closeMenus(); setTab("collector"); }}> <button className="menu-dropdown-item" onClick={() => { closeMenus(); openCollectorInput(); }}>
<span>Text mit Links analysieren</span> <span>Text mit Links analysieren</span>
<span className="shortcut">Strg+L</span> <span className="shortcut">Strg+L</span>
</button> </button>
+38 -1
View File
@@ -5,7 +5,7 @@ import { AvatarMenu, getAvatarMenuKeyboardAction } from "../src/renderer/shell/A
import { AppHeader } from "../src/renderer/shell/AppHeader"; import { AppHeader } from "../src/renderer/shell/AppHeader";
import { AppShell } from "../src/renderer/shell/AppShell"; import { AppShell } from "../src/renderer/shell/AppShell";
import { buildMainNavigation } from "../src/renderer/shell/shell-model"; import { buildMainNavigation } from "../src/renderer/shell/shell-model";
import { getSnapshotRenderDelay } from "../src/renderer/App"; import { classifyCollectorImportFileName, getSnapshotRenderDelay, readCollectorImportFiles } from "../src/renderer/App";
describe("desktop shell", () => { describe("desktop shell", () => {
it("uses keyboard-focusable controls for every copy target", () => { it("uses keyboard-focusable controls for every copy target", () => {
@@ -37,6 +37,43 @@ describe("desktop shell", () => {
expect(removal).toContain('title: "Ausgewählte Links löschen"'); expect(removal).toContain('title: "Ausgewählte Links löschen"');
}); });
it("routes exported queue JSON back through the queue importer while text files stay in the collector", async () => {
expect(classifyCollectorImportFileName("queue.json")).toBe("queue");
expect(classifyCollectorImportFileName("LINKS.TXT")).toBe("links");
expect(classifyCollectorImportFileName("archive.zip")).toBe("unsupported");
const files = await readCollectorImportFiles([
{ name: "queue.json", text: async () => '{"version":1,"packages":[]}' },
{ name: "links.txt", text: async () => "https://example.test/file" },
{ name: "ignored.zip", text: async () => "not-read" }
]);
expect(files).toEqual({
queueJson: ['{"version":1,"packages":[]}'],
linkText: "https://example.test/file"
});
const source = readFileSync(new URL("../src/renderer/App.tsx", import.meta.url), "utf8");
const picker = source.slice(source.indexOf("const onImportQueue ="), source.indexOf("const setBool ="));
const drop = source.slice(source.indexOf("const onDrop ="), source.indexOf("const onExportQueue ="));
expect(picker).toContain("readCollectorImportFiles([file])");
expect(picker).toContain("importQueueJsonTexts(queueJson)");
expect(drop).toContain("readCollectorImportFiles(importFiles)");
expect(drop).toContain("importQueueJsonTexts(queueJson)");
});
it("opens the link analysis dialog from Ctrl+L and the File menu", () => {
const source = readFileSync(new URL("../src/renderer/App.tsx", import.meta.url), "utf8");
const shortcutStart = source.indexOf('e.key.toLowerCase() === "l"');
const shortcut = source.slice(shortcutStart, source.indexOf('e.key.toLowerCase() === "p"', shortcutStart));
const menuStart = source.indexOf("Text mit Links analysieren");
const menu = source.slice(source.lastIndexOf("<button", menuStart), source.indexOf("</button>", menuStart));
expect(shortcut).toContain("openCollectorInput()");
expect(shortcut).not.toContain('setTab("collector")');
expect(menu).toContain("openCollectorInput()");
});
it("places the delete confirmation opt-out below the right-aligned actions", () => { it("places the delete confirmation opt-out below the right-aligned actions", () => {
const source = readFileSync(new URL("../src/renderer/views/downloads/DeleteConfirmationDialog.tsx", import.meta.url), "utf8"); const source = readFileSync(new URL("../src/renderer/views/downloads/DeleteConfirmationDialog.tsx", import.meta.url), "utf8");
const css = readFileSync(new URL("../src/renderer/styles.css", import.meta.url), "utf8"); const css = readFileSync(new URL("../src/renderer/styles.css", import.meta.url), "utf8");