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]
|
## [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
|
||||||
|
|
||||||
|
|||||||
Generated
+2
-2
@@ -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
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
Reference in New Issue
Block a user