diff --git a/src/main/constants.ts b/src/main/constants.ts index c3baeb2..055595a 100644 --- a/src/main/constants.ts +++ b/src/main/constants.ts @@ -21,7 +21,8 @@ export const WRITE_BUFFER_SIZE = 512 * 1024; // 512 KB write buffer (JDown export const WRITE_FLUSH_TIMEOUT_MS = 2000; // 2s flush timeout export const ALLOCATION_UNIT_SIZE = 4096; // 4 KB NTFS alignment export const STREAM_HIGH_WATER_MARK = 512 * 1024; // 512 KB stream buffer — lower than before (2 MB) so backpressure triggers sooner when disk is slow -export const DISK_BUSY_THRESHOLD_MS = 300; // Show "Warte auf Festplatte" if writableLength > 0 for this long +export const DISK_BUSY_THRESHOLD_MS = 300; // Internal detection threshold for disk backpressure +export const DISK_BUSY_STATUS_THRESHOLD_MS = 500; // Delay UI/log display for brief disk-write spikes export const SAMPLE_DIR_NAMES = new Set(["sample", "samples"]); export const SAMPLE_VIDEO_EXTENSIONS = new Set([".mkv", ".mp4", ".avi", ".mov", ".wmv", ".m4v", ".ts", ".m2ts", ".webm"]); diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index 00820be..264ef66 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -29,7 +29,7 @@ import { getProviderUsageDayKey, isProviderDailyLimitReached } from "../shared/provider-daily-limits"; -import { REQUEST_RETRIES, SAMPLE_VIDEO_EXTENSIONS, SPEED_WINDOW_SECONDS, WRITE_BUFFER_SIZE, WRITE_FLUSH_TIMEOUT_MS, ALLOCATION_UNIT_SIZE, STREAM_HIGH_WATER_MARK, DISK_BUSY_THRESHOLD_MS } from "./constants"; +import { REQUEST_RETRIES, SAMPLE_VIDEO_EXTENSIONS, SPEED_WINDOW_SECONDS, WRITE_BUFFER_SIZE, WRITE_FLUSH_TIMEOUT_MS, ALLOCATION_UNIT_SIZE, STREAM_HIGH_WATER_MARK, DISK_BUSY_THRESHOLD_MS, DISK_BUSY_STATUS_THRESHOLD_MS } from "./constants"; // Reference counter for NODE_TLS_REJECT_UNAUTHORIZED to avoid race conditions // when multiple parallel downloads need TLS verification disabled (e.g. DDownload). @@ -7216,6 +7216,13 @@ export class DownloadManager extends EventEmitter { const drainTimeoutMs = Math.max(30000, Math.min(300000, stallTimeoutMs > 0 ? stallTimeoutMs * 12 : 120000)); let lastDiskBusyEmitAt = 0; let diskBusySince = 0; // timestamp when writableLength first became > 0 + const diskBusyStatusVisible = (nowTick: number): boolean => { + const blockedSince = active.blockedOnDiskSince || 0; + if (blockedSince > 0 && nowTick - blockedSince >= DISK_BUSY_STATUS_THRESHOLD_MS) { + return true; + } + return diskBusySince > 0 && nowTick - diskBusySince >= DISK_BUSY_STATUS_THRESHOLD_MS; + }; const waitDrain = (): Promise => new Promise((resolve, reject) => { if (active.abortController.signal.aborted) { @@ -7227,7 +7234,7 @@ export class DownloadManager extends EventEmitter { active.blockedOnDiskSince = nowMs(); if (item.status !== "paused" && !this.session.paused) { const nowTick = nowMs(); - if (nowTick - lastDiskBusyEmitAt >= 1200) { + if (diskBusyStatusVisible(nowTick) && nowTick - lastDiskBusyEmitAt >= 1200) { item.status = "downloading"; item.speedBps = 0; item.fullStatus = `Warte auf Festplatte (${label})`; @@ -7342,7 +7349,7 @@ export class DownloadManager extends EventEmitter { if (item.status === "paused" || this.session.paused) { return; } - if (nowTick - lastIdleEmitAt >= idlePulseMs) { + if (diskBusyStatusVisible(nowTick) && nowTick - lastIdleEmitAt >= idlePulseMs) { item.status = "downloading"; item.speedBps = 0; item.fullStatus = `Warte auf Festplatte (${label})`; @@ -7469,7 +7476,7 @@ export class DownloadManager extends EventEmitter { if (stream.writableLength > 0) { if (diskBusySince === 0) diskBusySince = nowMs(); const busyMs = nowMs() - diskBusySince; - if (busyMs >= DISK_BUSY_THRESHOLD_MS && item.status !== "paused" && !this.session.paused) { + if (busyMs >= DISK_BUSY_STATUS_THRESHOLD_MS && item.status !== "paused" && !this.session.paused) { const nowTick = nowMs(); if (nowTick - lastDiskBusyEmitAt >= 1200) { item.status = "downloading"; @@ -7537,7 +7544,7 @@ export class DownloadManager extends EventEmitter { item.downloadedBytes = written; item.progressPercent = item.totalBytes ? Math.max(0, Math.min(100, Math.floor((written / item.totalBytes) * 100))) : 0; // Keep "Warte auf Festplatte" label if disk is busy; otherwise show normal status - const diskBusy = diskBusySince > 0 && nowMs() - diskBusySince >= DISK_BUSY_THRESHOLD_MS; + const diskBusy = diskBusyStatusVisible(nowMs()); if (diskBusy) { item.speedBps = 0; item.fullStatus = `Warte auf Festplatte (${label})`; diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 7a846a2..82e0165 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -3959,6 +3959,7 @@ export function App(): ReactElement { editingName={editingName} collapsed={collapsedPackages[pkg.id] ?? false} hideExtractedItems={snapshot.settings.hideExtractedItems} + sessionRunning={snapshot.session.running} selectedIds={selectedIds} columnOrder={columnOrder} gridTemplate={gridTemplate} @@ -5476,6 +5477,7 @@ interface PackageCardProps { editingName: string; collapsed: boolean; hideExtractedItems: boolean; + sessionRunning: boolean; selectedIds: Set; columnOrder: string[]; gridTemplate: string; @@ -5497,7 +5499,7 @@ interface PackageCardProps { onDragEnd: () => void; } -const PackageCard = memo(function PackageCard({ pkg, items, packageSpeed, stripeVariant, isFirst, isLast, isEditing, editingName, collapsed, hideExtractedItems, selectedIds, columnOrder, gridTemplate, onSelect, onSelectMouseDown, onSelectMouseEnter, onStartEdit, onFinishEdit, onEditChange, onToggleCollapse, onCancel, onMoveUp, onMoveDown, onToggle, onRemoveItem, onContextMenu, onDragStart, onDrop, onDragEnd }: PackageCardProps): ReactElement { +const PackageCard = memo(function PackageCard({ pkg, items, packageSpeed, stripeVariant, isFirst, isLast, isEditing, editingName, collapsed, hideExtractedItems, sessionRunning, selectedIds, columnOrder, gridTemplate, onSelect, onSelectMouseDown, onSelectMouseEnter, onStartEdit, onFinishEdit, onEditChange, onToggleCollapse, onCancel, onMoveUp, onMoveDown, onToggle, onRemoveItem, onContextMenu, onDragStart, onDrop, onDragEnd }: PackageCardProps): ReactElement { const done = items.filter((item) => item.status === "completed").length; const failed = items.filter((item) => item.status === "failed").length; const cancelled = items.filter((item) => item.status === "cancelled").length; @@ -5533,6 +5535,23 @@ const PackageCard = memo(function PackageCard({ pkg, items, packageSpeed, stripe if (e.key === "Escape") { onFinishEdit(pkg.id, pkg.name, pkg.name); } }; + const getDisplayedItemStatus = (item: DownloadItem): string => { + const statusText = String(item.fullStatus || "").trim(); + if (sessionRunning) { + return statusText; + } + if (item.status !== "queued" && item.status !== "reconnect_wait") { + return statusText; + } + if (statusText === "Paket gestoppt") { + return statusText; + } + if (/^Entpacken\b/i.test(statusText) || /^Entpackt\b/i.test(statusText) || /^Entpack-Fehler\b/i.test(statusText) || /^Fertig\b/i.test(statusText)) { + return statusText; + } + return ""; + }; + return (
{item.providerLabel || (item.provider ? providerLabels[item.provider] : "")}; case "prio": return ; case "status": return ( - 0 ? `${item.fullStatus} ? R${item.retries}` : item.fullStatus}> - {item.fullStatus} + { + const displayStatus = getDisplayedItemStatus(item); + if (!displayStatus) { + return ""; + } + return item.retries > 0 ? `${displayStatus} ? R${item.retries}` : displayStatus; + })()}> + {getDisplayedItemStatus(item)} ); case "speed": return {item.speedBps > 0 ? formatSpeedMbps(item.speedBps) : ""}; diff --git a/tests/download-manager.test.ts b/tests/download-manager.test.ts index 27e2ff0..21275f0 100644 --- a/tests/download-manager.test.ts +++ b/tests/download-manager.test.ts @@ -1722,14 +1722,7 @@ describe("download manager", () => { ); manager.addPackages([{ name: "drain-stall", links: ["https://dummy/drain-stall"] }]); - const queuedSnapshot = manager.getSnapshot(); - const packageId = queuedSnapshot.session.packageOrder[0] || ""; - const itemId = queuedSnapshot.session.packages[packageId]?.itemIds[0] || ""; manager.start(); - await waitFor(() => { - const status = manager.getSnapshot().session.items[itemId]?.fullStatus || ""; - return status.includes("Warte auf Festplatte"); - }, 12000); await waitFor(() => !manager.getSnapshot().session.running, 40000); const item = Object.values(manager.getSnapshot().session.items)[0];