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:
@@ -4,6 +4,8 @@ All notable changes to Multi-Debrid Downloader are documented in this file.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2.0.55] - 2026-08-22
|
||||
|
||||
### 1Fichier imports
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
- 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
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "multi-debrid-downloader",
|
||||
"version": "2.0.54",
|
||||
"version": "2.0.55",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "multi-debrid-downloader",
|
||||
"version": "2.0.54",
|
||||
"version": "2.0.55",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"adm-zip": "0.6.0",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "multi-debrid-downloader",
|
||||
"version": "2.0.54",
|
||||
"version": "2.0.55",
|
||||
"description": "Desktop downloader",
|
||||
"main": "build/main/main/main.js",
|
||||
"author": "Sucukdeluxe",
|
||||
|
||||
+65
-13
@@ -837,7 +837,29 @@ export function getSnapshotRenderDelay(itemCount: number, running: boolean, acti
|
||||
if (activeTab !== "downloads") delay = Math.max(delay, 800);
|
||||
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 }[] = [
|
||||
{ id: "rapidgator", label: "Rapidgator" },
|
||||
{ id: "uploaded", label: "Uploaded" },
|
||||
@@ -3498,9 +3520,35 @@ export function App(): ReactElement {
|
||||
});
|
||||
};
|
||||
|
||||
onImportDlcRef.current = onImportDlc;
|
||||
|
||||
const onDrop = async (event: DragEvent<HTMLElement>): Promise<void> => {
|
||||
onImportDlcRef.current = onImportDlc;
|
||||
|
||||
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();
|
||||
dragDepthRef.current = 0;
|
||||
dragOverRef.current = false;
|
||||
@@ -3515,8 +3563,9 @@ export function App(): ReactElement {
|
||||
if (dlc.length > 0) {
|
||||
await enqueueCollectorInspection(() => window.rd.inspectCollectorContainers(dlc, Date.now()));
|
||||
} else if (importFiles.length > 0) {
|
||||
const importedText = (await Promise.all(importFiles.map((file) => file.text()))).join("\n");
|
||||
await inspectCollectorRawText(importedText);
|
||||
const { queueJson, linkText } = await readCollectorImportFiles(importFiles);
|
||||
await importQueueJsonTexts(queueJson);
|
||||
if (linkText.trim()) await inspectCollectorRawText(linkText);
|
||||
} else if (droppedText.trim()) {
|
||||
await inspectCollectorRawText(droppedText);
|
||||
}
|
||||
@@ -3566,7 +3615,9 @@ export function App(): ReactElement {
|
||||
return;
|
||||
}
|
||||
releasePickerBusy();
|
||||
await inspectCollectorRawText(await file.text());
|
||||
const { queueJson, linkText } = await readCollectorImportFiles([file]);
|
||||
await importQueueJsonTexts(queueJson);
|
||||
if (linkText.trim()) await inspectCollectorRawText(linkText);
|
||||
};
|
||||
|
||||
clearImportQueueFocusListener();
|
||||
@@ -3673,6 +3724,7 @@ export function App(): ReactElement {
|
||||
|
||||
const openCollectorInput = (): void => {
|
||||
setCollectorError("");
|
||||
setTab("collector");
|
||||
setCollectorInput({ draft: "" });
|
||||
};
|
||||
|
||||
@@ -4478,11 +4530,11 @@ export function App(): ReactElement {
|
||||
void window.rd.quit();
|
||||
return;
|
||||
}
|
||||
if (!e.shiftKey && e.key.toLowerCase() === "l") {
|
||||
if (inInput) return;
|
||||
e.preventDefault();
|
||||
setTab("collector");
|
||||
setOpenMenu(null);
|
||||
if (!e.shiftKey && e.key.toLowerCase() === "l") {
|
||||
if (inInput) return;
|
||||
e.preventDefault();
|
||||
openCollectorInput();
|
||||
setOpenMenu(null);
|
||||
return;
|
||||
}
|
||||
if (!e.shiftKey && e.key.toLowerCase() === "p") {
|
||||
@@ -5613,7 +5665,7 @@ export function App(): ReactElement {
|
||||
Datei
|
||||
</button>
|
||||
<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 className="shortcut">Strg+L</span>
|
||||
</button>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { AvatarMenu, getAvatarMenuKeyboardAction } from "../src/renderer/shell/A
|
||||
import { AppHeader } from "../src/renderer/shell/AppHeader";
|
||||
import { AppShell } from "../src/renderer/shell/AppShell";
|
||||
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", () => {
|
||||
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"');
|
||||
});
|
||||
|
||||
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", () => {
|
||||
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");
|
||||
|
||||
Reference in New Issue
Block a user