From 7ac61ce64aa0c349def4c936009433ab5fa81089 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Fri, 27 Feb 2026 04:56:53 +0100 Subject: [PATCH] Fix provider selection persistence, queue naming, cancel removal, and update prompts --- package-lock.json | 4 +- package.json | 2 +- src/main/constants.ts | 2 +- src/main/debrid.ts | 107 ++++++++++++++++++++++++++++------- src/main/download-manager.ts | 84 +++++++++++++++++++-------- src/main/main.ts | 14 ++++- src/main/utils.ts | 12 +++- src/preload/preload.ts | 1 + src/renderer/App.tsx | 60 ++++++++++++++------ src/shared/ipc.ts | 1 + src/shared/preload-api.ts | 1 + tests/debrid.test.ts | 37 ++++++++++++ tests/self-check.ts | 9 ++- tests/utils.test.ts | 7 ++- 14 files changed, 267 insertions(+), 74 deletions(-) diff --git a/package-lock.json b/package-lock.json index 923d065..5bfe3ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "real-debrid-downloader", - "version": "1.1.16", + "version": "1.1.17", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "real-debrid-downloader", - "version": "1.1.16", + "version": "1.1.17", "license": "MIT", "dependencies": { "adm-zip": "^0.5.16", diff --git a/package.json b/package.json index c4f4e81..5e75634 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "real-debrid-downloader", - "version": "1.1.16", + "version": "1.1.17", "description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)", "main": "build/main/main/main.js", "author": "Sucukdeluxe", diff --git a/src/main/constants.ts b/src/main/constants.ts index 4d2a9e9..ec01871 100644 --- a/src/main/constants.ts +++ b/src/main/constants.ts @@ -3,7 +3,7 @@ import os from "node:os"; import { AppSettings } from "../shared/types"; export const APP_NAME = "Debrid Download Manager"; -export const APP_VERSION = "1.1.16"; +export const APP_VERSION = "1.1.17"; export const API_BASE_URL = "https://api.real-debrid.com/rest/1.0"; export const DCRYPT_UPLOAD_URL = "https://dcrypt.it/decrypt/upload"; diff --git a/src/main/debrid.ts b/src/main/debrid.ts index 49f4352..9b6cd14 100644 --- a/src/main/debrid.ts +++ b/src/main/debrid.ts @@ -1,7 +1,7 @@ import { AppSettings, DebridProvider } from "../shared/types"; import { REQUEST_RETRIES } from "./constants"; import { RealDebridClient, UnrestrictedLink } from "./realdebrid"; -import { compactErrorText, filenameFromUrl, sleep } from "./utils"; +import { compactErrorText, filenameFromUrl, looksLikeOpaqueFilename, sleep } from "./utils"; const MEGA_DEBRID_API = "https://www.mega-debrid.eu/api.php"; const BEST_DEBRID_API_BASE = "https://bestdebrid.com/api/v1"; @@ -108,6 +108,70 @@ function uniqueProviderOrder(order: DebridProvider[]): DebridProvider[] { return result; } +function isRapidgatorLink(link: string): boolean { + try { + return new URL(link).hostname.toLowerCase().includes("rapidgator.net"); + } catch { + return false; + } +} + +function decodeHtmlEntities(text: string): string { + return text + .replace(/&/g, "&") + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/</g, "<") + .replace(/>/g, ">"); +} + +async function runWithConcurrency(items: T[], concurrency: number, worker: (item: T) => Promise): Promise { + if (items.length === 0) { + return; + } + const size = Math.max(1, Math.min(concurrency, items.length)); + let index = 0; + const runners = Array.from({ length: size }, async () => { + while (index < items.length) { + const current = items[index]; + index += 1; + await worker(current); + } + }); + await Promise.all(runners); +} + +async function resolveRapidgatorFilename(link: string): Promise { + if (!isRapidgatorLink(link)) { + return ""; + } + try { + const response = await fetch(link, { + method: "GET", + headers: { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36" + } + }); + if (!response.ok) { + return ""; + } + const html = await response.text(); + const titleMatch = html.match(/([^<]+)<\/title>/i); + const title = decodeHtmlEntities((titleMatch?.[1] || "").trim()); + if (!title) { + return ""; + } + const preferred = title.match(/download\s+file\s+(.+)$/i)?.[1]?.trim() || title; + if (!preferred) { + return ""; + } + const withoutSuffix = preferred.replace(/\s*-\s*rapidgator.*$/i, "").trim(); + return withoutSuffix; + } catch { + return ""; + } +} + function buildBestDebridRequests(link: string, token: string): BestDebridRequest[] { const linkParam = encodeURIComponent(link); const authParam = encodeURIComponent(token); @@ -421,39 +485,42 @@ export class DebridService { } public async resolveFilenames(links: string[]): Promise<Map<string, string>> { - const unresolved = links.filter((link) => filenameFromUrl(link) === "download.bin"); + const unresolved = links.filter((link) => looksLikeOpaqueFilename(filenameFromUrl(link))); if (unresolved.length === 0) { return new Map<string, string>(); } + const clean = new Map<string, string>(); const token = this.settings.allDebridToken.trim(); - if (!token) { - return new Map<string, string>(); + if (token) { + try { + const infos = await this.allDebridClient.getLinkInfos(unresolved); + for (const [link, fileName] of infos.entries()) { + if (fileName.trim() && !looksLikeOpaqueFilename(fileName.trim())) { + clean.set(link, fileName.trim()); + } + } + } catch { + // ignore and continue with host page fallback + } } - try { - const infos = await this.allDebridClient.getLinkInfos(unresolved); - const clean = new Map<string, string>(); - for (const [link, fileName] of infos.entries()) { - if (fileName.trim() && fileName.trim().toLowerCase() !== "download.bin") { - clean.set(link, fileName.trim()); - } + const remaining = unresolved.filter((link) => !clean.has(link) && isRapidgatorLink(link)); + await runWithConcurrency(remaining, 6, async (link) => { + const fromPage = await resolveRapidgatorFilename(link); + if (fromPage && !looksLikeOpaqueFilename(fromPage)) { + clean.set(link, fromPage); } - return clean; - } catch { - return new Map<string, string>(); - } + }); + + return clean; } public async unrestrictLink(link: string): Promise<ProviderUnrestrictedLink> { const order = uniqueProviderOrder([ this.settings.providerPrimary, this.settings.providerSecondary, - this.settings.providerTertiary, - "realdebrid", - "megadebrid", - "bestdebrid", - "alldebrid" + this.settings.providerTertiary ]); let configuredFound = false; diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index e65104b..a2f7111 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -11,7 +11,7 @@ import { extractPackageArchives } from "./extractor"; import { validateFileAgainstManifest } from "./integrity"; import { logger } from "./logger"; import { StoragePaths, saveSession } from "./storage"; -import { compactErrorText, ensureDirPath, filenameFromUrl, formatEta, humanSize, nowMs, sanitizeFilename, sleep } from "./utils"; +import { compactErrorText, ensureDirPath, filenameFromUrl, formatEta, humanSize, looksLikeOpaqueFilename, nowMs, sanitizeFilename, sleep } from "./utils"; type ActiveTask = { itemId: string; @@ -99,6 +99,10 @@ export class DownloadManager extends EventEmitter { private nonResumableActive = 0; + private stateEmitTimer: NodeJS.Timeout | null = null; + + private speedBytesLastWindow = 0; + public constructor(settings: AppSettings, session: SessionState, storagePaths: StoragePaths) { super(); this.settings = settings; @@ -129,8 +133,8 @@ export class DownloadManager extends EventEmitter { public getSnapshot(): UiSnapshot { const now = nowMs(); - this.speedEvents = this.speedEvents.filter((event) => event.at >= now - 3000); - const speedBps = this.speedEvents.reduce((acc, event) => acc + event.bytes, 0) / 3; + this.pruneSpeedEvents(now); + const speedBps = this.speedBytesLastWindow / 3; const totalItems = Object.keys(this.session.items).length; const doneItems = Object.values(this.session.items).filter((item) => isFinishedStatus(item.status)).length; @@ -159,7 +163,7 @@ export class DownloadManager extends EventEmitter { this.session.summaryText = ""; this.summary = null; this.persistNow(); - this.emitState(); + this.emitState(true); } public addPackages(packages: ParsedPackageInput[]): { addedPackages: number; addedLinks: number } { @@ -212,7 +216,7 @@ export class DownloadManager extends EventEmitter { }; packageEntry.itemIds.push(itemId); this.session.items[itemId] = item; - if (fileName === "download.bin") { + if (looksLikeOpaqueFilename(fileName)) { const existing = unresolvedByLink.get(link) ?? []; existing.push(itemId); unresolvedByLink.set(link, existing); @@ -256,7 +260,7 @@ export class DownloadManager extends EventEmitter { if (!item) { continue; } - if (item.fileName !== "download.bin") { + if (!looksLikeOpaqueFilename(item.fileName)) { continue; } item.fileName = normalized; @@ -280,20 +284,13 @@ export class DownloadManager extends EventEmitter { if (!pkg) { return; } - pkg.cancelled = true; - pkg.status = "cancelled"; - pkg.updatedAt = nowMs(); + const itemIds = [...pkg.itemIds]; - for (const itemId of pkg.itemIds) { + for (const itemId of itemIds) { const item = this.session.items[itemId]; if (!item) { continue; } - if (item.status === "queued" || item.status === "validating" || item.status === "reconnect_wait") { - item.status = "cancelled"; - item.fullStatus = "Entfernt"; - item.updatedAt = nowMs(); - } const active = this.activeTasks.get(itemId); if (active) { active.abortReason = "cancel"; @@ -302,9 +299,10 @@ export class DownloadManager extends EventEmitter { } const removed = cleanupCancelledPackageArtifacts(pkg.outputDir); + this.removePackageFromSession(packageId, itemIds); logger.info(`Paket ${pkg.name} abgebrochen, ${removed} Artefakte gelöscht`); this.persistSoon(); - this.emitState(); + this.emitState(true); } public start(): void { @@ -316,7 +314,7 @@ export class DownloadManager extends EventEmitter { this.session.runStartedAt = this.session.runStartedAt || nowMs(); this.summary = null; this.persistSoon(); - this.emitState(); + this.emitState(true); this.ensureScheduler(); } @@ -328,7 +326,7 @@ export class DownloadManager extends EventEmitter { active.abortController.abort("stop"); } this.persistSoon(); - this.emitState(); + this.emitState(true); } public togglePause(): boolean { @@ -337,7 +335,7 @@ export class DownloadManager extends EventEmitter { } this.session.paused = !this.session.paused; this.persistSoon(); - this.emitState(); + this.emitState(true); return this.session.paused; } @@ -400,8 +398,46 @@ export class DownloadManager extends EventEmitter { saveSession(this.storagePaths, this.session); } - private emitState(): void { - this.emit("state", this.getSnapshot()); + private emitState(force = false): void { + if (force) { + if (this.stateEmitTimer) { + clearTimeout(this.stateEmitTimer); + this.stateEmitTimer = null; + } + this.emit("state", this.getSnapshot()); + return; + } + if (this.stateEmitTimer) { + return; + } + this.stateEmitTimer = setTimeout(() => { + this.stateEmitTimer = null; + this.emit("state", this.getSnapshot()); + }, 140); + } + + private pruneSpeedEvents(now: number): void { + while (this.speedEvents.length > 0 && this.speedEvents[0].at < now - 3000) { + const event = this.speedEvents.shift(); + if (event) { + this.speedBytesLastWindow = Math.max(0, this.speedBytesLastWindow - event.bytes); + } + } + } + + private recordSpeed(bytes: number): void { + const now = nowMs(); + this.speedEvents.push({ at: now, bytes }); + this.speedBytesLastWindow += bytes; + this.pruneSpeedEvents(now); + } + + private removePackageFromSession(packageId: string, itemIds: string[]): void { + for (const itemId of itemIds) { + delete this.session.items[itemId]; + } + delete this.session.packages[packageId]; + this.session.packageOrder = this.session.packageOrder.filter((id) => id !== packageId); } private async ensureScheduler(): Promise<void> { @@ -748,8 +784,7 @@ export class DownloadManager extends EventEmitter { written += buffer.length; windowBytes += buffer.length; this.session.totalDownloadedBytes += buffer.length; - this.speedEvents.push({ at: nowMs(), bytes: buffer.length }); - this.speedEvents = this.speedEvents.filter((event) => event.at >= nowMs() - 3000); + this.recordSpeed(buffer.length); const elapsed = Math.max((nowMs() - windowStarted) / 1000, 0.1); const speed = windowBytes / elapsed; @@ -801,7 +836,8 @@ export class DownloadManager extends EventEmitter { return; } - const globalBytes = this.speedEvents.reduce((acc, event) => acc + event.bytes, 0) + chunkBytes; + this.pruneSpeedEvents(now); + const globalBytes = this.speedBytesLastWindow + chunkBytes; const globalAllowed = bytesPerSecond * 3; if (globalBytes > globalAllowed) { await sleep(Math.min(250, Math.ceil(((globalBytes - globalAllowed) / bytesPerSecond) * 1000))); diff --git a/src/main/main.ts b/src/main/main.ts index b06fd74..cee7560 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -1,5 +1,5 @@ import path from "node:path"; -import { app, BrowserWindow, dialog, ipcMain, IpcMainInvokeEvent } from "electron"; +import { app, BrowserWindow, dialog, ipcMain, IpcMainInvokeEvent, shell } from "electron"; import { AddLinksPayload, AppSettings } from "../shared/types"; import { AppController } from "./app-controller"; import { IPC_CHANNELS } from "../shared/ipc"; @@ -41,6 +41,18 @@ function registerIpcHandlers(): void { ipcMain.handle(IPC_CHANNELS.GET_SNAPSHOT, () => controller.getSnapshot()); ipcMain.handle(IPC_CHANNELS.GET_VERSION, () => controller.getVersion()); ipcMain.handle(IPC_CHANNELS.CHECK_UPDATES, async () => controller.checkUpdates()); + ipcMain.handle(IPC_CHANNELS.OPEN_EXTERNAL, async (_event: IpcMainInvokeEvent, rawUrl: string) => { + try { + const parsed = new URL(String(rawUrl || "").trim()); + if (parsed.protocol !== "https:" && parsed.protocol !== "http:") { + return false; + } + await shell.openExternal(parsed.toString()); + return true; + } catch { + return false; + } + }); ipcMain.handle(IPC_CHANNELS.UPDATE_SETTINGS, (_event: IpcMainInvokeEvent, partial: Partial<AppSettings>) => controller.updateSettings(partial ?? {})); ipcMain.handle(IPC_CHANNELS.ADD_LINKS, (_event: IpcMainInvokeEvent, payload: AddLinksPayload) => controller.addLinks(payload)); ipcMain.handle(IPC_CHANNELS.ADD_CONTAINERS, async (_event: IpcMainInvokeEvent, filePaths: string[]) => controller.addContainers(filePaths ?? [])); diff --git a/src/main/utils.ts b/src/main/utils.ts index 7bfbb24..3e304db 100644 --- a/src/main/utils.ts +++ b/src/main/utils.ts @@ -62,15 +62,21 @@ export function filenameFromUrl(url: string): string { const normalized = decoded .replace(/\.(rar|zip|7z|tar|gz|bz2|xz|iso|part\d+\.rar|r\d{2})\.html$/i, ".$1") .replace(/\.(mp4|mkv|avi|mp3|flac|srt)\.html$/i, ".$1"); - if (/^[a-f0-9]{24,}$/i.test(normalized)) { - return "download.bin"; - } return sanitizeFilename(normalized || "download.bin"); } catch { return "download.bin"; } } +export function looksLikeOpaqueFilename(name: string): boolean { + const cleaned = sanitizeFilename(name || "").toLowerCase(); + if (!cleaned || cleaned === "download.bin") { + return true; + } + const parsed = path.parse(cleaned); + return /^[a-f0-9]{24,}$/i.test(parsed.name || cleaned); +} + export function inferPackageNameFromLinks(links: string[]): string { if (links.length === 0) { return "Paket"; diff --git a/src/preload/preload.ts b/src/preload/preload.ts index 6d3e765..0c63b9a 100644 --- a/src/preload/preload.ts +++ b/src/preload/preload.ts @@ -7,6 +7,7 @@ const api: ElectronApi = { getSnapshot: (): Promise<UiSnapshot> => ipcRenderer.invoke(IPC_CHANNELS.GET_SNAPSHOT), getVersion: (): Promise<string> => ipcRenderer.invoke(IPC_CHANNELS.GET_VERSION), checkUpdates: (): Promise<UpdateCheckResult> => ipcRenderer.invoke(IPC_CHANNELS.CHECK_UPDATES), + openExternal: (url: string): Promise<boolean> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_EXTERNAL, url), updateSettings: (settings: Partial<AppSettings>): Promise<AppSettings> => ipcRenderer.invoke(IPC_CHANNELS.UPDATE_SETTINGS, settings), addLinks: (payload: AddLinksPayload): Promise<{ addedPackages: number; addedLinks: number; invalidCount: number }> => ipcRenderer.invoke(IPC_CHANNELS.ADD_LINKS, payload), diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 108a93e..1d61571 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -1,5 +1,5 @@ import { DragEvent, ReactElement, useEffect, useMemo, useState } from "react"; -import type { AppSettings, DebridProvider, DownloadItem, PackageEntry, UiSnapshot } from "../shared/types"; +import type { AppSettings, DebridProvider, DownloadItem, PackageEntry, UiSnapshot, UpdateCheckResult } from "../shared/types"; type Tab = "collector" | "downloads" | "settings"; @@ -86,10 +86,7 @@ export function App(): ReactElement { setSettingsDraft(state.settings); if (state.settings.autoUpdateCheck) { void window.rd.checkUpdates().then((result) => { - if (result.updateAvailable) { - setStatusToast(`Update verfügbar: ${result.latestTag} (aktuell v${result.currentVersion})`); - setTimeout(() => setStatusToast(""), 3800); - } + void handleUpdateResult(result, "startup"); }); } }); @@ -107,6 +104,37 @@ export function App(): ReactElement { .map((id: string) => snapshot.session.packages[id]) .filter(Boolean), [snapshot]); + const handleUpdateResult = async (result: UpdateCheckResult, source: "manual" | "startup"): Promise<void> => { + if (result.error) { + if (source === "manual") { + setStatusToast(`Update-Check fehlgeschlagen: ${result.error}`); + setTimeout(() => setStatusToast(""), 2800); + } + return; + } + + if (!result.updateAvailable) { + if (source === "manual") { + setStatusToast(`Kein Update verfügbar (v${result.currentVersion})`); + setTimeout(() => setStatusToast(""), 2000); + } + return; + } + + const approved = window.confirm( + `Update verfügbar: ${result.latestTag} (aktuell v${result.currentVersion})\n\nJetzt Download-Seite öffnen?` + ); + if (!approved) { + setStatusToast(`Update verfügbar: ${result.latestTag}`); + setTimeout(() => setStatusToast(""), 2600); + return; + } + + const opened = await window.rd.openExternal(result.releaseUrl); + setStatusToast(opened ? "Download-Seite im Browser geöffnet" : "Konnte Download-Seite nicht öffnen"); + setTimeout(() => setStatusToast(""), 2600); + }; + const onSaveSettings = async (): Promise<void> => { await window.rd.updateSettings(settingsDraft); setStatusToast("Settings gespeichert"); @@ -115,18 +143,7 @@ export function App(): ReactElement { const onCheckUpdates = async (): Promise<void> => { const result = await window.rd.checkUpdates(); - if (result.error) { - setStatusToast(`Update-Check fehlgeschlagen: ${result.error}`); - setTimeout(() => setStatusToast(""), 2800); - return; - } - if (result.updateAvailable) { - setStatusToast(`Update verfügbar: ${result.latestTag} (aktuell v${result.currentVersion})`); - setTimeout(() => setStatusToast(""), 3200); - return; - } - setStatusToast(`Kein Update verfügbar (v${result.currentVersion})`); - setTimeout(() => setStatusToast(""), 2000); + await handleUpdateResult(result, "manual"); }; const onAddLinks = async (): Promise<void> => { @@ -193,7 +210,14 @@ export function App(): ReactElement { <section className="control-strip"> <div className="buttons"> - <button className="btn accent" disabled={!snapshot.canStart} onClick={() => window.rd.start()}>Start</button> + <button + className="btn accent" + disabled={!snapshot.canStart} + onClick={async () => { + await window.rd.updateSettings(settingsDraft); + await window.rd.start(); + }} + >Start</button> <button className="btn" disabled={!snapshot.canPause} onClick={() => window.rd.togglePause()}> {snapshot.session.paused ? "Resume" : "Pause"} </button> diff --git a/src/shared/ipc.ts b/src/shared/ipc.ts index c29cc32..a6c509a 100644 --- a/src/shared/ipc.ts +++ b/src/shared/ipc.ts @@ -2,6 +2,7 @@ export const IPC_CHANNELS = { GET_SNAPSHOT: "app:get-snapshot", GET_VERSION: "app:get-version", CHECK_UPDATES: "app:check-updates", + OPEN_EXTERNAL: "app:open-external", UPDATE_SETTINGS: "app:update-settings", ADD_LINKS: "queue:add-links", ADD_CONTAINERS: "queue:add-containers", diff --git a/src/shared/preload-api.ts b/src/shared/preload-api.ts index a64ccdd..7ec4ba9 100644 --- a/src/shared/preload-api.ts +++ b/src/shared/preload-api.ts @@ -4,6 +4,7 @@ export interface ElectronApi { getSnapshot: () => Promise<UiSnapshot>; getVersion: () => Promise<string>; checkUpdates: () => Promise<UpdateCheckResult>; + openExternal: (url: string) => Promise<boolean>; updateSettings: (settings: Partial<AppSettings>) => Promise<AppSettings>; addLinks: (payload: AddLinksPayload) => Promise<{ addedPackages: number; addedLinks: number; invalidCount: number }>; addContainers: (filePaths: string[]) => Promise<{ addedPackages: number; addedLinks: number }>; diff --git a/tests/debrid.test.ts b/tests/debrid.test.ts index b21c659..6f14c28 100644 --- a/tests/debrid.test.ts +++ b/tests/debrid.test.ts @@ -142,4 +142,41 @@ describe("debrid service", () => { expect(result.directUrl).toBe("https://alldebrid.example/file.bin"); expect(result.fileSize).toBe(4096); }); + + it("respects provider selection and does not append hidden fallback providers", async () => { + const settings = { + ...defaultSettings(), + token: "", + megaToken: "mega-token", + bestToken: "", + allDebridToken: "ad-token", + providerPrimary: "megadebrid" as const, + providerSecondary: "megadebrid" as const, + providerTertiary: "megadebrid" as const, + autoProviderFallback: true + }; + + let allDebridCalls = 0; + globalThis.fetch = (async (input: RequestInfo | URL): Promise<Response> => { + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + if (url.includes("mega-debrid.eu/api.php?action=getLink")) { + return new Response(JSON.stringify({ response_code: "error", response_text: "host unavailable" }), { + status: 200, + headers: { "Content-Type": "application/json" } + }); + } + if (url.includes("api.alldebrid.com/v4/link/unlock")) { + allDebridCalls += 1; + return new Response(JSON.stringify({ status: "success", data: { link: "https://alldebrid.example/file.bin" } }), { + status: 200, + headers: { "Content-Type": "application/json" } + }); + } + return new Response("not-found", { status: 404 }); + }) as typeof fetch; + + const service = new DebridService(settings); + await expect(service.unrestrictLink("https://rapidgator.net/file/example.part5.rar.html")).rejects.toThrow(); + expect(allDebridCalls).toBe(0); + }); }); diff --git a/tests/self-check.ts b/tests/self-check.ts index d3a22a0..794ba49 100644 --- a/tests/self-check.ts +++ b/tests/self-check.ts @@ -185,8 +185,13 @@ async function main(): Promise<void> { manager4.cancelPackage(pkgId); await waitFor(() => !manager4.getSnapshot().session.running || Object.values(manager4.getSnapshot().session.items).every((item) => item.status !== "downloading"), 15000); const cancelSnapshot = manager4.getSnapshot(); - const cancelItem = Object.values(cancelSnapshot.session.items)[0]; - assert(cancelItem?.status === "cancelled" || cancelItem?.status === "queued", "Paketabbruch nicht wirksam"); + const remainingItems = Object.values(cancelSnapshot.session.items); + if (remainingItems.length === 0) { + assert(cancelSnapshot.session.packageOrder.length === 0, "Abgebrochenes Paket wurde nicht entfernt"); + } else { + const cancelItem = remainingItems[0]; + assert(cancelItem?.status === "cancelled" || cancelItem?.status === "queued", "Paketabbruch nicht wirksam"); + } const packageDir = path.join(path.join(tempRoot, "downloads-cancel"), "cancel"); assert(!fs.existsSync(path.join(packageDir, "release.part1.rar")), "RAR-Artefakt wurde nicht gelöscht"); diff --git a/tests/utils.test.ts b/tests/utils.test.ts index 41d52da..01a0793 100644 --- a/tests/utils.test.ts +++ b/tests/utils.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { parsePackagesFromLinksText, isHttpLink, sanitizeFilename, formatEta, filenameFromUrl } from "../src/main/utils"; +import { parsePackagesFromLinksText, isHttpLink, sanitizeFilename, formatEta, filenameFromUrl, looksLikeOpaqueFilename } from "../src/main/utils"; describe("utils", () => { it("validates http links", () => { @@ -34,6 +34,9 @@ describe("utils", () => { it("normalizes filenames from links", () => { expect(filenameFromUrl("https://rapidgator.net/file/id/show.part1.rar.html")).toBe("show.part1.rar"); expect(filenameFromUrl("https://debrid.example/dl/abc?filename=Movie.S01E01.mkv")).toBe("Movie.S01E01.mkv"); - expect(filenameFromUrl("https://debrid.example/dl/e51f6809bb6ca615601f5ac5db433737")).toBe("download.bin"); + expect(filenameFromUrl("https://debrid.example/dl/e51f6809bb6ca615601f5ac5db433737")).toBe("e51f6809bb6ca615601f5ac5db433737"); + expect(looksLikeOpaqueFilename("download.bin")).toBe(true); + expect(looksLikeOpaqueFilename("e51f6809bb6ca615601f5ac5db433737")).toBe(true); + expect(looksLikeOpaqueFilename("movie.part1.rar")).toBe(false); }); });