From 2f031dc350016f499835070d42819d96f3281443 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Sat, 22 Aug 2026 20:17:58 +0200 Subject: [PATCH] fix(collector): restore direct DLC drag imports Resolve dropped file paths through Electron webUtils, send dragged DLC containers directly to Downloads, and keep explicit collector imports bounded so metadata enrichment cannot leave the interface permanently busy. --- src/main/collector-inspection.ts | 27 +++++++++++++++++++---- src/preload/preload.ts | 5 +++-- src/renderer/App.tsx | 16 +++++++++++--- src/renderer/collector-drop.ts | 24 ++++++++++++++++++++ src/shared/preload-api.ts | 3 ++- tests/account-preload.test.ts | 14 ++++++++++-- tests/collector-drop.test.ts | 35 ++++++++++++++++++++++++++++++ tests/collector-inspection.test.ts | 15 +++++++++++++ tests/visual/mock-electron-api.ts | 1 + 9 files changed, 128 insertions(+), 12 deletions(-) create mode 100644 src/renderer/collector-drop.ts create mode 100644 tests/collector-drop.test.ts diff --git a/src/main/collector-inspection.ts b/src/main/collector-inspection.ts index 1c64290..50283b1 100644 --- a/src/main/collector-inspection.ts +++ b/src/main/collector-inspection.ts @@ -14,6 +14,7 @@ interface CollectorInspectionDependencies { checkRapidgator?: typeof checkRapidgatorOnline; resolveFilenames?: (links: string[]) => Promise>; createId?: (prefix: "package" | "link") => string; + inspectionTimeoutMs?: number; } interface SourceLink { @@ -64,6 +65,17 @@ async function runWithConcurrency(items: T[], limit: number, worker: (item: T await Promise.all(runners); } +async function waitForCollectorMetadata(tasks: Promise[], timeoutMs: number): Promise { + const all = Promise.all(tasks).then(() => undefined); + let timer: ReturnType | null = null; + const timeout = new Promise((resolve) => { + timer = setTimeout(resolve, Math.max(1, timeoutMs)); + timer.unref?.(); + }); + await Promise.race([all, timeout]); + if (timer) clearTimeout(timer); +} + function countInputLines(rawText: string): { invalidCount: number; duplicateCount: number } { const seen = new Set(); let invalidCount = 0; @@ -172,7 +184,11 @@ export async function inspectCollectorPackages( const checkRapidgator = dependencies.checkRapidgator ?? checkRapidgatorOnline; const resolveFilenames = dependencies.resolveFilenames ?? ((urls) => new DebridService(settings).resolveFilenames(urls)); - const oneFichierPromise = checkOneFichier(oneFichierLinks).catch(() => new Map()); + let oneFichierResults = new Map(); + let genericResults = new Map(); + const oneFichierPromise = checkOneFichier(oneFichierLinks) + .then((results) => { oneFichierResults = results; }) + .catch(() => undefined); const rapidgatorPromise = runWithConcurrency(rapidgatorLinks, 8, async (url) => { const result = await checkRapidgator(url).catch(() => null); const link = linksByUrl.get(url); @@ -192,10 +208,13 @@ export async function inspectCollectorPackages( if (result.fileSizeBytes !== null && result.fileSizeBytes >= 0) link.fileSizeBytes = result.fileSizeBytes; }); const genericPromise = genericLinks.length > 0 - ? resolveFilenames(genericLinks).catch(() => new Map()) - : Promise.resolve(new Map()); + ? resolveFilenames(genericLinks).then((results) => { genericResults = results; }).catch(() => undefined) + : Promise.resolve(); - const [oneFichierResults, genericResults] = await Promise.all([oneFichierPromise, genericPromise, rapidgatorPromise, ddownloadPromise]).then(([one, generic]) => [one, generic] as const); + await waitForCollectorMetadata( + [oneFichierPromise, genericPromise, rapidgatorPromise, ddownloadPromise], + dependencies.inspectionTimeoutMs ?? 30_000 + ); for (const [url, result] of oneFichierResults) { const link = linksByUrl.get(url); if (!link) continue; diff --git a/src/preload/preload.ts b/src/preload/preload.ts index 66bae99..145c5d0 100644 --- a/src/preload/preload.ts +++ b/src/preload/preload.ts @@ -1,4 +1,4 @@ -import { contextBridge, ipcRenderer } from "electron"; +import { contextBridge, ipcRenderer, webUtils } from "electron"; import { AddLinksPayload, AccountCheckScope, @@ -56,7 +56,8 @@ const api: ElectronApi = { ipcRenderer.invoke(IPC_CHANNELS.INSPECT_COLLECTOR_TEXT, request), inspectCollectorContainers: (filePaths: string[], addedAt: number): Promise => ipcRenderer.invoke(IPC_CHANNELS.INSPECT_COLLECTOR_CONTAINERS, filePaths, addedAt), - getStartConflicts: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.GET_START_CONFLICTS), + getPathForDroppedFile: (file: File): string => webUtils.getPathForFile(file), + getStartConflicts: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.GET_START_CONFLICTS), resolveStartConflict: (packageId: string, policy: DuplicatePolicy): Promise => ipcRenderer.invoke(IPC_CHANNELS.RESOLVE_START_CONFLICT, packageId, policy), clearAll: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.CLEAR_ALL), diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 969f412..89f991b 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -49,6 +49,7 @@ import type { AccountEditState, AccountEditTarget, AccountKind, AccountService, import { ACCOUNT_SERVICE_ICONS } from "./account-service-icons"; import { DOWNLOAD_SPEED_MAX_SAMPLES, updateDownloadSpeedHistory } from "./download-speed-state"; import { createUiLocalizer, normalizeLanguage } from "./i18n"; +import { importDroppedDlcFiles } from "./collector-drop"; import { runLocalBackupExport, runLocalBackupImport, type BackupPassphraseMode } from "./backup-flow"; import type { DownloadSpeedHistoryState } from "./download-speed-state"; import { extractHoster, formatDateTime, formatSpeedMbps, humanSize, providerLabels } from "./download-format"; @@ -3649,11 +3650,20 @@ export function App(): ReactElement { const hasUri = event.dataTransfer.types.includes("text/uri-list"); if (!hasFiles && !hasUri) { return; } const files = Array.from(event.dataTransfer.files ?? []) as File[]; - const dlc = files.filter((f) => f.name.toLowerCase().endsWith(".dlc")).map((f) => (f as unknown as { path?: string }).path).filter((v): v is string => !!v); + const dlcFiles = files.filter((file) => file.name.toLowerCase().endsWith(".dlc")); const importFiles = files.filter((f) => /\.(json|txt)$/i.test(f.name)); const droppedText = event.dataTransfer.getData("text/plain") || event.dataTransfer.getData("text/uri-list") || ""; - if (dlc.length > 0) { - await enqueueCollectorInspection(() => window.rd.inspectCollectorContainers(dlc, Date.now())); + if (dlcFiles.length > 0) { + await performQuickAction(async () => { + const existingIds = new Set(Object.keys(snapshotRef.current.session.packages)); + const result = await importDroppedDlcFiles(dlcFiles, window.rd.getPathForDroppedFile, window.rd.addContainers); + if (!result || result.addedLinks === 0) throw new Error("Keine gültigen Links in der DLC-Datei gefunden"); + setTab("downloads"); + showToast(`Importiert: ${result.addedPackages} Paket(e), ${result.addedLinks} Link(s)`); + if (snapshotRef.current.settings.collapseNewPackages) await collapseNewPackages(existingIds); + }, (error) => { + showToast(`DLC-Import fehlgeschlagen: ${String(error)}`, 3000); + }); } else if (importFiles.length > 0) { const { queueJson, linkText } = await readCollectorImportFiles(importFiles); await importQueueJsonTexts(queueJson); diff --git a/src/renderer/collector-drop.ts b/src/renderer/collector-drop.ts new file mode 100644 index 0000000..3f70fd2 --- /dev/null +++ b/src/renderer/collector-drop.ts @@ -0,0 +1,24 @@ +export function resolveDroppedDlcPaths( + files: ReadonlyArray, + getPathForFile: (file: File) => string +): string[] { + const paths: string[] = []; + for (const file of files) { + if (!file.name.toLowerCase().endsWith(".dlc")) continue; + try { + const filePath = String(getPathForFile(file) || "").trim(); + if (filePath) paths.push(filePath); + } catch { + } + } + return paths; +} + +export async function importDroppedDlcFiles( + files: ReadonlyArray, + getPathForFile: (file: File) => string, + addContainers: (filePaths: string[]) => Promise +): Promise { + const filePaths = resolveDroppedDlcPaths(files, getPathForFile); + return filePaths.length > 0 ? addContainers(filePaths) : null; +} diff --git a/src/shared/preload-api.ts b/src/shared/preload-api.ts index ffca3e0..250722b 100644 --- a/src/shared/preload-api.ts +++ b/src/shared/preload-api.ts @@ -82,7 +82,8 @@ export interface ElectronApi { addContainers: (filePaths: string[]) => Promise<{ addedPackages: number; addedLinks: number }>; inspectCollectorText: (request: CollectorInspectionRequest) => Promise; inspectCollectorContainers: (filePaths: string[], addedAt: number) => Promise; - getStartConflicts: () => Promise; + getPathForDroppedFile: (file: File) => string; + getStartConflicts: () => Promise; resolveStartConflict: (packageId: string, policy: DuplicatePolicy) => Promise; clearAll: () => Promise; start: () => Promise; diff --git a/tests/account-preload.test.ts b/tests/account-preload.test.ts index de41ca9..d6e6eef 100644 --- a/tests/account-preload.test.ts +++ b/tests/account-preload.test.ts @@ -4,7 +4,8 @@ import type { ElectronApi } from "../src/shared/preload-api"; const electron = vi.hoisted(() => ({ api: undefined as ElectronApi | undefined, - invoke: vi.fn<(...args: unknown[]) => Promise>(async () => undefined) + invoke: vi.fn<(...args: unknown[]) => Promise>(async () => undefined), + getPathForFile: vi.fn(() => "C:\\Imports\\dropped.dlc") })); vi.mock("electron", () => ({ @@ -18,7 +19,8 @@ vi.mock("electron", () => ({ on: vi.fn(), removeListener: vi.fn(), send: vi.fn() - } + }, + webUtils: { getPathForFile: electron.getPathForFile } })); describe("account preload contract", () => { @@ -122,4 +124,12 @@ describe("account preload contract", () => { [IPC_CHANNELS.INSPECT_COLLECTOR_CONTAINERS, ["C:\\Imports\\sample.dlc"], 5678] ]); }); + + it("resolves dropped files through Electron webUtils without IPC", () => { + const file = { name: "dropped.dlc" } as File; + + expect(electron.api?.getPathForDroppedFile(file)).toBe("C:\\Imports\\dropped.dlc"); + expect(electron.getPathForFile).toHaveBeenCalledWith(file); + expect(electron.invoke).not.toHaveBeenCalled(); + }); }); diff --git a/tests/collector-drop.test.ts b/tests/collector-drop.test.ts new file mode 100644 index 0000000..ec2f90b --- /dev/null +++ b/tests/collector-drop.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it, vi } from "vitest"; +import { importDroppedDlcFiles, resolveDroppedDlcPaths } from "../src/renderer/collector-drop"; + +describe("collector DLC drop", () => { + it("resolves DLC paths through the preload bridge instead of File.path", () => { + const first = { name: "first.dlc" } as File; + const ignored = { name: "notes.txt" } as File; + const second = { name: "SECOND.DLC" } as File; + const getPath = vi.fn((file: File) => file === first ? "C:\\Drops\\first.dlc" : "C:\\Drops\\second.dlc"); + + expect(resolveDroppedDlcPaths([first, ignored, second], getPath)).toEqual([ + "C:\\Drops\\first.dlc", + "C:\\Drops\\second.dlc" + ]); + expect(getPath.mock.calls.map(([file]) => file)).toEqual([first, second]); + }); + + it("ignores files whose native path cannot be resolved", () => { + const file = { name: "broken.dlc" } as File; + + expect(resolveDroppedDlcPaths([file], () => "")).toEqual([]); + expect(resolveDroppedDlcPaths([file], () => { throw new Error("unavailable"); })).toEqual([]); + }); + + it("imports dropped DLC files directly into Downloads without collector analysis", async () => { + const file = { name: "package.dlc" } as File; + const addContainers = vi.fn(async () => ({ addedPackages: 2, addedLinks: 16 })); + + await expect(importDroppedDlcFiles([file], () => "C:\\Drops\\package.dlc", addContainers)).resolves.toEqual({ + addedPackages: 2, + addedLinks: 16 + }); + expect(addContainers).toHaveBeenCalledWith(["C:\\Drops\\package.dlc"]); + }); +}); diff --git a/tests/collector-inspection.test.ts b/tests/collector-inspection.test.ts index bda717f..8c3b8dd 100644 --- a/tests/collector-inspection.test.ts +++ b/tests/collector-inspection.test.ts @@ -100,6 +100,21 @@ describe("collector inspection", () => { })); }); + it("returns visible unknown links when metadata analysis reaches its deadline", async () => { + const link = "https://1fichier.com/?slow123"; + const result = await inspectCollectorText({ rawText: link, addedAt: 3200 }, defaultSettings(), { + checkOneFichier: async () => new Promise(() => {}), + inspectionTimeoutMs: 10 + }); + + expect(result.packages).toHaveLength(1); + expect(result.packages[0].links).toEqual([expect.objectContaining({ + url: link, + availability: "unknown", + status: "unknown" + })]); + }); + it("resolves DDownload metadata before grouping without using a debrid account", async () => { const link = "https://ddownload.com/ntwscdw62gyb"; let genericResolverCalls = 0; diff --git a/tests/visual/mock-electron-api.ts b/tests/visual/mock-electron-api.ts index 922aeb8..a06f8db 100644 --- a/tests/visual/mock-electron-api.ts +++ b/tests/visual/mock-electron-api.ts @@ -61,6 +61,7 @@ export function createVisualElectronApi( addContainers: async () => ({ addedPackages: 0, addedLinks: 0 }), inspectCollectorText: async () => clone(fixture.collector), inspectCollectorContainers: async () => clone(fixture.collector), + getPathForDroppedFile: () => "", getStartConflicts: async () => [], resolveStartConflict: async (_packageId, policy) => ({ skipped: policy === "skip",