diff --git a/package.json b/package.json index e33f4b6..1007187 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "real-debrid-downloader", - "version": "1.6.24", + "version": "1.6.25", "description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)", "main": "build/main/main/main.js", "author": "Sucukdeluxe", diff --git a/src/main/debrid.ts b/src/main/debrid.ts index a4785e7..1c457b4 100644 --- a/src/main/debrid.ts +++ b/src/main/debrid.ts @@ -317,7 +317,7 @@ async function runWithConcurrency(items: T[], concurrency: number, worker: (i let index = 0; let firstError: unknown = null; const next = (): T | undefined => { - if (index >= items.length) { + if (firstError || index >= items.length) { return undefined; } const item = items[index]; diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index 42c620b..093a252 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -2669,6 +2669,7 @@ export class DownloadManager extends EventEmitter { } public skipItems(itemIds: string[]): void { + const affectedPackageIds = new Set(); for (const itemId of itemIds) { const item = this.session.items[itemId]; if (!item) continue; @@ -2680,6 +2681,11 @@ export class DownloadManager extends EventEmitter { this.retryAfterByItem.delete(itemId); this.retryStateByItem.delete(itemId); this.recordRunOutcome(itemId, "cancelled"); + affectedPackageIds.add(item.packageId); + } + for (const pkgId of affectedPackageIds) { + const pkg = this.session.packages[pkgId]; + if (pkg) this.refreshPackageStatus(pkg); } this.persistSoon(); this.emitState(); @@ -3328,13 +3334,17 @@ export class DownloadManager extends EventEmitter { pkg.itemIds = pkg.itemIds.filter((id) => id !== itemId); this.releaseTargetPath(itemId); this.dropItemContribution(itemId); - delete this.session.items[itemId]; - this.itemCount = Math.max(0, this.itemCount - 1); this.retryAfterByItem.delete(itemId); + this.retryStateByItem.delete(itemId); removed += 1; } if (pkg.itemIds.length === 0) { - this.removePackageFromSession(pkgId, []); + this.removePackageFromSession(pkgId, completedItemIds); + } else { + for (const itemId of completedItemIds) { + delete this.session.items[itemId]; + this.itemCount = Math.max(0, this.itemCount - 1); + } } } else if (policy === "package_done" || policy === "on_start") { const allCompleted = pkg.itemIds.every((id) => { @@ -6301,7 +6311,8 @@ export class DownloadManager extends EventEmitter { logger.warn(`Hybrid-Extract Fehler: pkg=${pkg.name}, reason=${compactErrorText(error)}`); const errorAt = nowMs(); for (const entry of hybridItems) { - if (entry.fullStatus === "Entpacken - Ausstehend" || entry.fullStatus === "Entpacken - Warten auf Parts") { + if (isExtractedLabel(entry.fullStatus || "")) continue; + if (/^Entpacken\b/i.test(entry.fullStatus || "") || entry.fullStatus === "Entpacken - Ausstehend" || entry.fullStatus === "Entpacken - Warten auf Parts") { entry.fullStatus = `Entpacken - Error`; entry.updatedAt = errorAt; } @@ -6838,7 +6849,7 @@ export class DownloadManager extends EventEmitter { } const paused = this.session.running && this.session.paused; - const currentSpeedBps = paused ? 0 : this.speedBytesLastWindow / SPEED_WINDOW_SECONDS; + const currentSpeedBps = !this.session.running || paused ? 0 : this.speedBytesLastWindow / SPEED_WINDOW_SECONDS; let maxSpeed = 0; for (let i = this.speedEventsHead; i < this.speedEvents.length; i += 1) { diff --git a/src/main/extractor.ts b/src/main/extractor.ts index 1fac6dc..e50ccf0 100644 --- a/src/main/extractor.ts +++ b/src/main/extractor.ts @@ -1674,6 +1674,11 @@ export function collectArchiveCleanupTargets(sourceArchivePath: string, director return Array.from(targets); } + // Tar compound archives (.tar.gz, .tar.bz2, .tar.xz, .tgz, .tbz2, .txz) + if (/\.(?:tar\.(?:gz|bz2|xz)|tgz|tbz2|txz)$/i.test(fileName)) { + return Array.from(targets); + } + // Generic .NNN split files (HJSplit etc.) const genericSplit = fileName.match(/^(.*)\.(\d{3})$/i); if (genericSplit) { @@ -1994,6 +1999,8 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{ if (!sig) { logger.info(`Generische Split-Datei übersprungen (keine Archiv-Signatur): ${archiveName}`); extracted += 1; + resumeCompleted.add(archiveResumeKey); + extractedArchives.push(archivePath); clearInterval(pulseTimer); return; } @@ -2127,8 +2134,12 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{ try { await extractSingleArchive(queue[idx]); } catch (error) { - if (isExtractAbortError(String(error))) { - abortError = error instanceof Error ? error : new Error(String(error)); + const errText = String(error); + if (errText.includes("noextractor:skipped")) { + break; // handled by noExtractorEncountered flag after the pool + } + if (isExtractAbortError(errText)) { + abortError = error instanceof Error ? error : new Error(errText); break; } // Non-abort errors are already handled inside extractSingleArchive diff --git a/src/main/storage.ts b/src/main/storage.ts index 33af16e..b3e6a8f 100644 --- a/src/main/storage.ts +++ b/src/main/storage.ts @@ -548,6 +548,7 @@ export function loadSession(paths: StoragePaths): SessionState { } export function saveSession(paths: StoragePaths, session: SessionState): void { + syncSaveGeneration += 1; ensureBaseDir(paths.baseDir); if (fs.existsSync(paths.sessionFile)) { try { @@ -569,16 +570,26 @@ export function saveSession(paths: StoragePaths, session: SessionState): void { let asyncSaveRunning = false; let asyncSaveQueued: { paths: StoragePaths; payload: string } | null = null; +let syncSaveGeneration = 0; -async function writeSessionPayload(paths: StoragePaths, payload: string): Promise { +async function writeSessionPayload(paths: StoragePaths, payload: string, generation: number): Promise { await fs.promises.mkdir(paths.baseDir, { recursive: true }); await fsp.copyFile(paths.sessionFile, sessionBackupPath(paths.sessionFile)).catch(() => {}); const tempPath = sessionTempPath(paths.sessionFile, "async"); await fsp.writeFile(tempPath, payload, "utf8"); + // If a synchronous save occurred after this async save started, discard the stale write + if (generation < syncSaveGeneration) { + await fsp.rm(tempPath, { force: true }).catch(() => {}); + return; + } try { await fsp.rename(tempPath, paths.sessionFile); } catch (renameError: unknown) { if (renameError && typeof renameError === "object" && "code" in renameError && (renameError as NodeJS.ErrnoException).code === "EXDEV") { + if (generation < syncSaveGeneration) { + await fsp.rm(tempPath, { force: true }).catch(() => {}); + return; + } await fsp.copyFile(tempPath, paths.sessionFile); await fsp.rm(tempPath, { force: true }).catch(() => {}); } else { @@ -593,8 +604,9 @@ async function saveSessionPayloadAsync(paths: StoragePaths, payload: string): Pr return; } asyncSaveRunning = true; + const gen = syncSaveGeneration; try { - await writeSessionPayload(paths, payload); + await writeSessionPayload(paths, payload, gen); } catch (error) { logger.error(`Async Session-Save fehlgeschlagen: ${String(error)}`); } finally { @@ -610,6 +622,7 @@ async function saveSessionPayloadAsync(paths: StoragePaths, payload: string): Pr export function cancelPendingAsyncSaves(): void { asyncSaveQueued = null; asyncSettingsSaveQueued = null; + syncSaveGeneration += 1; } export async function saveSessionAsync(paths: StoragePaths, session: SessionState): Promise { @@ -637,7 +650,8 @@ function normalizeHistoryEntry(raw: unknown, index: number): HistoryEntry | null completedAt: clampNumber(entry.completedAt, Date.now(), 0, Number.MAX_SAFE_INTEGER), durationSeconds: clampNumber(entry.durationSeconds, 0, 0, Number.MAX_SAFE_INTEGER), status: entry.status === "deleted" ? "deleted" : "completed", - outputDir: asText(entry.outputDir) + outputDir: asText(entry.outputDir), + urls: Array.isArray(entry.urls) ? (entry.urls as unknown[]).map(String).filter(Boolean) : undefined }; } diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 60f1aed..acef83f 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -473,6 +473,7 @@ export function App(): ReactElement { const [settingsDirty, setSettingsDirty] = useState(false); const settingsDirtyRef = useRef(false); const settingsDraftRevisionRef = useRef(0); + const panelDirtyRevisionRef = useRef(0); const latestStateRef = useRef(null); const snapshotRef = useRef(snapshot); snapshotRef.current = snapshot; @@ -645,6 +646,7 @@ export function App(): ReactElement { } setSettingsDraft(state.settings); settingsDirtyRef.current = false; + panelDirtyRevisionRef.current = 0; setSettingsDirty(false); applyTheme(state.settings.theme); if (state.settings.autoUpdateCheck) { @@ -1044,6 +1046,7 @@ export function App(): ReactElement { if (settingsDraftRevisionRef.current === revisionAtStart) { setSettingsDraft(result); settingsDirtyRef.current = false; + panelDirtyRevisionRef.current = 0; setSettingsDirty(false); } return result; @@ -1290,18 +1293,21 @@ export function App(): ReactElement { const setBool = (key: keyof AppSettings, value: boolean): void => { settingsDraftRevisionRef.current += 1; + panelDirtyRevisionRef.current += 1; settingsDirtyRef.current = true; setSettingsDirty(true); setSettingsDraft((prev) => ({ ...prev, [key]: value })); }; const setText = (key: keyof AppSettings, value: string): void => { settingsDraftRevisionRef.current += 1; + panelDirtyRevisionRef.current += 1; settingsDirtyRef.current = true; setSettingsDirty(true); setSettingsDraft((prev) => ({ ...prev, [key]: value })); }; const setNum = (key: keyof AppSettings, value: number): void => { settingsDraftRevisionRef.current += 1; + panelDirtyRevisionRef.current += 1; settingsDirtyRef.current = true; setSettingsDirty(true); setSettingsDraft((prev) => ({ ...prev, [key]: value })); @@ -1309,6 +1315,7 @@ export function App(): ReactElement { const setSpeedLimitMbps = (value: number): void => { const mbps = Number.isFinite(value) ? Math.max(0, value) : 0; settingsDraftRevisionRef.current += 1; + panelDirtyRevisionRef.current += 1; settingsDirtyRef.current = true; setSettingsDirty(true); setSettingsDraft((prev) => ({ ...prev, speedLimitKbps: Math.floor(mbps * 1024) })); @@ -1434,16 +1441,20 @@ export function App(): ReactElement { }, []); const onPackageFinishEdit = useCallback((packageId: string, currentName: string, nextName: string): void => { + let shouldRename = false; setEditingPackageId((prev) => { if (prev !== packageId) return prev; // already finished (e.g. blur after Enter key) + shouldRename = true; + return null; + }); + if (shouldRename) { const normalized = nextName.trim(); if (normalized && normalized !== currentName.trim()) { void window.rd.renamePackage(packageId, normalized).catch((error) => { showToast(`Umbenennen fehlgeschlagen: ${String(error)}`, 2400); }); } - return null; - }); + } }, [showToast]); const onPackageToggleCollapse = useCallback((packageId: string): void => { @@ -1691,6 +1702,7 @@ export function App(): ReactElement { const addSchedule = (): void => { settingsDraftRevisionRef.current += 1; + panelDirtyRevisionRef.current += 1; settingsDirtyRef.current = true; setSettingsDirty(true); setSettingsDraft((prev) => ({ @@ -1700,6 +1712,7 @@ export function App(): ReactElement { }; const removeSchedule = (idx: number): void => { settingsDraftRevisionRef.current += 1; + panelDirtyRevisionRef.current += 1; settingsDirtyRef.current = true; setSettingsDirty(true); setSettingsDraft((prev) => ({ @@ -1709,6 +1722,7 @@ export function App(): ReactElement { }; const updateSchedule = (idx: number, field: keyof BandwidthScheduleEntry, value: number | boolean): void => { settingsDraftRevisionRef.current += 1; + panelDirtyRevisionRef.current += 1; settingsDirtyRef.current = true; setSettingsDirty(true); setSettingsDraft((prev) => ({ @@ -2086,7 +2100,7 @@ export function App(): ReactElement { settingsDirtyRef.current = true; const rev = ++settingsDraftRevisionRef.current; setSettingsDraft((prev) => ({ ...prev, maxParallel: val })); - void window.rd.updateSettings({ maxParallel: val }).finally(() => { if (settingsDraftRevisionRef.current === rev) settingsDirtyRef.current = false; }); + void window.rd.updateSettings({ maxParallel: val }).finally(() => { if (settingsDraftRevisionRef.current === rev && panelDirtyRevisionRef.current === 0) settingsDirtyRef.current = false; }); }} />
@@ -2095,14 +2109,14 @@ export function App(): ReactElement { settingsDirtyRef.current = true; const rev = ++settingsDraftRevisionRef.current; setSettingsDraft((prev) => ({ ...prev, maxParallel: val })); - void window.rd.updateSettings({ maxParallel: val }).finally(() => { if (settingsDraftRevisionRef.current === rev) settingsDirtyRef.current = false; }); + void window.rd.updateSettings({ maxParallel: val }).finally(() => { if (settingsDraftRevisionRef.current === rev && panelDirtyRevisionRef.current === 0) settingsDirtyRef.current = false; }); }}>▲
@@ -2117,7 +2131,7 @@ export function App(): ReactElement { settingsDirtyRef.current = true; const rev = ++settingsDraftRevisionRef.current; setSettingsDraft((prev) => ({ ...prev, speedLimitEnabled: next })); - void window.rd.updateSettings({ speedLimitEnabled: next }).finally(() => { if (settingsDraftRevisionRef.current === rev) settingsDirtyRef.current = false; }); + void window.rd.updateSettings({ speedLimitEnabled: next }).finally(() => { if (settingsDraftRevisionRef.current === rev && panelDirtyRevisionRef.current === 0) settingsDirtyRef.current = false; }); }} />
@@ -2138,7 +2152,7 @@ export function App(): ReactElement { settingsDirtyRef.current = true; const rev = ++settingsDraftRevisionRef.current; setSettingsDraft((prev) => ({ ...prev, speedLimitKbps: kbps })); - void window.rd.updateSettings({ speedLimitKbps: kbps }).finally(() => { if (settingsDraftRevisionRef.current === rev) settingsDirtyRef.current = false; }); + void window.rd.updateSettings({ speedLimitKbps: kbps }).finally(() => { if (settingsDraftRevisionRef.current === rev && panelDirtyRevisionRef.current === 0) settingsDirtyRef.current = false; }); setSpeedLimitInput(formatMbpsInputFromKbps(kbps)); }} /> @@ -2149,7 +2163,7 @@ export function App(): ReactElement { settingsDirtyRef.current = true; const rev = ++settingsDraftRevisionRef.current; setSettingsDraft((prev) => ({ ...prev, speedLimitKbps: next })); - void window.rd.updateSettings({ speedLimitKbps: next }).finally(() => { if (settingsDraftRevisionRef.current === rev) settingsDirtyRef.current = false; }); + void window.rd.updateSettings({ speedLimitKbps: next }).finally(() => { if (settingsDraftRevisionRef.current === rev && panelDirtyRevisionRef.current === 0) settingsDirtyRef.current = false; }); setSpeedLimitInput(formatMbpsInputFromKbps(next)); }}>▲
@@ -2534,7 +2548,7 @@ export function App(): ReactElement { {tab === "statistics" && (
-

Session-Ubersicht

+

Session-Übersicht

Aktuelle Geschwindigkeit @@ -2648,6 +2662,7 @@ export function App(): ReactElement {