fix: align live speed and split package reset

This commit is contained in:
Sucukdeluxe
2026-08-31 18:21:58 +02:00
parent 217b7b026e
commit f14d166898
9 changed files with 270 additions and 85 deletions
+90 -71
View File
@@ -1874,7 +1874,7 @@ export class DownloadManager extends EventEmitter {
private persistTimer: NodeJS.Timeout | null = null;
private speedEvents: Array<{ at: number; bytes: number; pid: string }> = [];
private speedEvents: Array<{ at: number; bytes: number; pid: string; itemId: string }> = [];
private summary: DownloadSummary | null = null;
@@ -1883,7 +1883,9 @@ export class DownloadManager extends EventEmitter {
private stateEmitTimer: NodeJS.Timeout | null = null;
private lastStateEmitAt = 0;
private speedBytesLastWindow = 0;
private speedBytesLastWindow = 0;
private speedBytesPerItem = new Map<string, number>();
private sessionDownloadedBytes = 0;
private sessionCompletedFiles = 0;
@@ -2808,7 +2810,15 @@ export class DownloadManager extends EventEmitter {
const reconnectMs = Math.max(0, this.session.reconnectUntil - now);
const snapshotSession = cloneSession(this.session);
const snapshotSession = cloneSession(this.session);
for (const [itemId, item] of Object.entries(snapshotSession.items)) {
const speedBps = this.session.running && !paused && item.status === "downloading"
? Math.floor((this.speedBytesPerItem.get(item.id) ?? 0) / SPEED_WINDOW_SECONDS)
: 0;
if (item.speedBps !== speedBps) {
snapshotSession.items[itemId] = { ...item, speedBps };
}
}
let rendererState: ReturnType<typeof createRendererState>;
if (this.settingsSnapshotCache && now - this.settingsSnapshotCacheAt < 400) {
rendererState = this.settingsSnapshotCache;
@@ -2964,10 +2974,11 @@ export class DownloadManager extends EventEmitter {
changed = true;
}
if (this.speedEvents.length > 0 || this.speedBytesLastWindow !== 0 || this.speedBytesPerPackage.size > 0) {
this.speedEvents = [];
this.speedBytesLastWindow = 0;
this.speedBytesPerPackage.clear();
this.speedEventsHead = 0;
this.speedEvents = [];
this.speedBytesLastWindow = 0;
this.speedBytesPerPackage.clear();
this.speedBytesPerItem.clear();
this.speedEventsHead = 0;
changed = true;
}
if (changed) {
@@ -3070,10 +3081,11 @@ export class DownloadManager extends EventEmitter {
this.lastGlobalProgressBytes = 0;
this.lastGlobalProgressAt = now;
this.speedEvents = [];
this.speedEventsHead = 0;
this.speedBytesLastWindow = 0;
this.speedBytesPerPackage.clear();
this.summary = null;
this.speedEventsHead = 0;
this.speedBytesLastWindow = 0;
this.speedBytesPerPackage.clear();
this.speedBytesPerItem.clear();
this.summary = null;
this.invalidateStatsCache();
this.persistSoon();
this.emitState(true);
@@ -3390,10 +3402,11 @@ export class DownloadManager extends EventEmitter {
this.claimedTargetPathByItem.clear();
this.itemContributedBytes.clear();
this.speedEvents = [];
this.speedEventsHead = 0;
this.speedBytesLastWindow = 0;
this.speedBytesPerPackage.clear();
this.packagePostProcessTasks.clear();
this.speedEventsHead = 0;
this.speedBytesLastWindow = 0;
this.speedBytesPerPackage.clear();
this.speedBytesPerItem.clear();
this.packagePostProcessTasks.clear();
this.packagePostProcessAbortControllers.clear();
this.packageDeferredPostProcessAbortControllers.clear();
this.packageDeferredPostProcessTasks.clear();
@@ -6339,11 +6352,12 @@ export class DownloadManager extends EventEmitter {
this.session.summaryText = "";
this.session.reconnectUntil = 0;
this.session.reconnectReason = "";
this.speedEvents = [];
this.speedBytesLastWindow = 0;
this.speedBytesPerPackage.clear();
this.speedEventsHead = 0;
this.lastGlobalProgressBytes = 0;
this.speedEvents = [];
this.speedBytesLastWindow = 0;
this.speedBytesPerPackage.clear();
this.speedBytesPerItem.clear();
this.speedEventsHead = 0;
this.lastGlobalProgressBytes = 0;
this.lastGlobalProgressAt = nowMs();
this.lastReconnectMarkAt = 0;
this.consecutiveReconnects = 0;
@@ -6452,11 +6466,12 @@ export class DownloadManager extends EventEmitter {
this.session.summaryText = "";
this.session.reconnectUntil = 0;
this.session.reconnectReason = "";
this.speedEvents = [];
this.speedBytesLastWindow = 0;
this.speedBytesPerPackage.clear();
this.speedEventsHead = 0;
this.lastGlobalProgressBytes = 0;
this.speedEvents = [];
this.speedBytesLastWindow = 0;
this.speedBytesPerPackage.clear();
this.speedBytesPerItem.clear();
this.speedEventsHead = 0;
this.lastGlobalProgressBytes = 0;
this.lastGlobalProgressAt = nowMs();
this.lastReconnectMarkAt = 0;
this.consecutiveReconnects = 0;
@@ -6569,11 +6584,12 @@ export class DownloadManager extends EventEmitter {
this.session.summaryText = "";
this.session.reconnectUntil = 0;
this.session.reconnectReason = "";
this.speedEvents = [];
this.speedBytesLastWindow = 0;
this.speedBytesPerPackage.clear();
this.speedEventsHead = 0;
this.lastGlobalProgressBytes = 0;
this.speedEvents = [];
this.speedBytesLastWindow = 0;
this.speedBytesPerPackage.clear();
this.speedBytesPerItem.clear();
this.speedEventsHead = 0;
this.lastGlobalProgressBytes = 0;
this.lastGlobalProgressAt = nowMs();
this.summary = null;
this.nonResumableActive = 0;
@@ -6610,9 +6626,10 @@ export class DownloadManager extends EventEmitter {
this.session.reconnectReason = "";
this.lastReconnectMarkAt = 0;
this.consecutiveReconnects = 0;
this.speedEvents = [];
this.speedBytesLastWindow = 0;
this.speedBytesPerPackage.clear();
this.speedEvents = [];
this.speedBytesLastWindow = 0;
this.speedBytesPerPackage.clear();
this.speedBytesPerItem.clear();
this.speedEventsHead = 0;
this.lastGlobalProgressBytes = 0;
this.lastGlobalProgressAt = nowMs();
@@ -6652,11 +6669,12 @@ export class DownloadManager extends EventEmitter {
this.retryStateByItem.clear();
this.lastGlobalProgressBytes = this.session.totalDownloadedBytes;
this.lastGlobalProgressAt = nowMs();
this.speedEvents = [];
this.speedBytesLastWindow = 0;
this.speedBytesPerPackage.clear();
this.speedEventsHead = 0;
if (!keepExtraction) {
this.speedEvents = [];
this.speedBytesLastWindow = 0;
this.speedBytesPerPackage.clear();
this.speedBytesPerItem.clear();
this.speedEventsHead = 0;
if (!keepExtraction) {
this.abortPostProcessing("stop");
for (const waiter of this.packagePostProcessWaiters) { waiter.resolve(); }
this.packagePostProcessWaiters = [];
@@ -6774,11 +6792,12 @@ export class DownloadManager extends EventEmitter {
}
}
this.speedEvents = [];
this.speedBytesLastWindow = 0;
this.speedBytesPerPackage.clear();
this.speedEventsHead = 0;
this.runItemIds.clear();
this.speedEvents = [];
this.speedBytesLastWindow = 0;
this.speedBytesPerPackage.clear();
this.speedBytesPerItem.clear();
this.speedEventsHead = 0;
this.runItemIds.clear();
this.runPackageIds.clear();
this.runOutcomes.clear();
this.runCompletedPackages.clear();
@@ -6815,10 +6834,11 @@ export class DownloadManager extends EventEmitter {
this.session.paused = !this.session.paused;
if (!wasPaused && this.session.paused) {
this.speedEvents = [];
this.speedBytesLastWindow = 0;
this.speedBytesPerPackage.clear();
this.speedEventsHead = 0;
this.speedEvents = [];
this.speedBytesLastWindow = 0;
this.speedBytesPerPackage.clear();
this.speedBytesPerItem.clear();
this.speedEventsHead = 0;
}
if (wasPaused && !this.session.paused) {
@@ -7198,17 +7218,20 @@ export class DownloadManager extends EventEmitter {
}, emitDelay);
}
private speedEventsHead = 0;
private speedBytesPerPackage = new Map<string, number>();
private speedEventsHead = 0;
private speedBytesPerPackage = new Map<string, number>();
private pruneSpeedEvents(now: number): void {
const cutoff = now - SPEED_WINDOW_SECONDS * 1000;
while (this.speedEventsHead < this.speedEvents.length && this.speedEvents[this.speedEventsHead].at < cutoff) {
const ev = this.speedEvents[this.speedEventsHead];
this.speedBytesLastWindow = Math.max(0, this.speedBytesLastWindow - ev.bytes);
const pkgBytes = (this.speedBytesPerPackage.get(ev.pid) ?? 0) - ev.bytes;
if (pkgBytes <= 0) this.speedBytesPerPackage.delete(ev.pid);
else this.speedBytesPerPackage.set(ev.pid, pkgBytes);
const pkgBytes = (this.speedBytesPerPackage.get(ev.pid) ?? 0) - ev.bytes;
if (pkgBytes <= 0) this.speedBytesPerPackage.delete(ev.pid);
else this.speedBytesPerPackage.set(ev.pid, pkgBytes);
const itemBytes = (this.speedBytesPerItem.get(ev.itemId) ?? 0) - ev.bytes;
if (itemBytes <= 0) this.speedBytesPerItem.delete(ev.itemId);
else this.speedBytesPerItem.set(ev.itemId, itemBytes);
this.speedEventsHead += 1;
}
if (this.speedEventsHead > 200) {
@@ -7219,7 +7242,7 @@ export class DownloadManager extends EventEmitter {
private lastSpeedPruneAt = 0;
private recordSpeed(bytes: number, packageId: string = ""): void {
private recordSpeed(bytes: number, packageId: string = "", itemId: string = ""): void {
const now = nowMs();
if (!Number.isFinite(bytes) || bytes <= 0) {
return;
@@ -7231,13 +7254,14 @@ export class DownloadManager extends EventEmitter {
}
const bucket = now - (now % 120);
const last = this.speedEvents[this.speedEvents.length - 1];
if (last && last.at === bucket && last.pid === packageId) {
last.bytes += bytes;
} else {
this.speedEvents.push({ at: bucket, bytes, pid: packageId });
}
this.speedBytesLastWindow += bytes;
this.speedBytesPerPackage.set(packageId, (this.speedBytesPerPackage.get(packageId) ?? 0) + bytes);
if (last && last.at === bucket && last.pid === packageId && last.itemId === itemId) {
last.bytes += bytes;
} else {
this.speedEvents.push({ at: bucket, bytes, pid: packageId, itemId });
}
this.speedBytesLastWindow += bytes;
this.speedBytesPerPackage.set(packageId, (this.speedBytesPerPackage.get(packageId) ?? 0) + bytes);
this.speedBytesPerItem.set(itemId, (this.speedBytesPerItem.get(itemId) ?? 0) + bytes);
if (now - this.lastSpeedPruneAt >= 1500) {
this.pruneSpeedEvents(now);
this.lastSpeedPruneAt = now;
@@ -11024,8 +11048,6 @@ export class DownloadManager extends EventEmitter {
origin_http_error: "Downloadserver hat die Segmentanfrage abgelehnt",
segment_failed: "Proxy-Segmente konnten nicht vollständig geladen werden"
};
let proxyWindowBytes = 0;
let proxyWindowStartedAt = nowMs();
let lastProxyUiEmitAt = 0;
item.fullStatus = `Proxy-Download startet (Gesamtlimit ${this.settings.proxyConnectionsPerDownload})`;
item.updatedAt = nowMs();
@@ -11058,14 +11080,13 @@ export class DownloadManager extends EventEmitter {
}
},
onTrafficBytes: (bytes) => {
proxyWindowBytes += bytes;
this.recordProviderDownloadedBytes(
item.provider,
bytes,
item.providerAccountId,
item.providerAccountLabel
);
this.recordSpeed(bytes, item.packageId);
this.recordSpeed(bytes, item.packageId, active.itemId);
},
onProgress: (deltaBytes, downloadedBytes, totalBytes) => {
this.session.totalDownloadedBytes = Math.max(0, this.session.totalDownloadedBytes + deltaBytes);
@@ -11076,19 +11097,16 @@ export class DownloadManager extends EventEmitter {
Math.max(0, (this.itemContributedBytes.get(active.itemId) || 0) + deltaBytes)
);
const nowTick = nowMs();
const elapsed = Math.max((nowTick - proxyWindowStartedAt) / 1000, 0.2);
item.status = this.session.paused ? "paused" : "downloading";
item.downloadedBytes = downloadedBytes;
item.totalBytes = totalBytes;
item.progressPercent = Math.max(0, Math.min(100, Math.floor((downloadedBytes / totalBytes) * 100)));
item.speedBps = this.session.paused ? 0 : Math.max(0, Math.floor(proxyWindowBytes / elapsed));
item.speedBps = this.session.paused
? 0
: Math.floor((this.speedBytesPerItem.get(active.itemId) ?? 0) / SPEED_WINDOW_SECONDS);
item.fullStatus = this.session.paused
? "Pausiert"
: `Proxy-Download läuft (Gesamtlimit ${this.settings.proxyConnectionsPerDownload})`;
if (elapsed >= 0.5) {
proxyWindowStartedAt = nowTick;
proxyWindowBytes = 0;
}
if (nowTick - lastProxyUiEmitAt >= DOWNLOAD_LIVE_UPDATE_INTERVAL_MS || deltaBytes < 0) {
item.updatedAt = nowTick;
this.emitState();
@@ -11751,7 +11769,7 @@ export class DownloadManager extends EventEmitter {
item.providerAccountLabel
);
this.itemContributedBytes.set(active.itemId, (this.itemContributedBytes.get(active.itemId) || 0) + buffer.length);
this.recordSpeed(buffer.length, item.packageId);
this.recordSpeed(buffer.length, item.packageId, active.itemId);
throughputWindowBytes += buffer.length;
if (completionPlan.canFinishEarly && completionPlan.expectedTotal && written >= completionPlan.expectedTotal) {
@@ -14630,6 +14648,7 @@ export class DownloadManager extends EventEmitter {
this.speedEventsHead = 0;
this.speedBytesLastWindow = 0;
this.speedBytesPerPackage.clear();
this.speedBytesPerItem.clear();
this.resetGlobalSpeedLimitState();
this.nonResumableActive = 0;
this.lastGlobalProgressBytes = this.session.totalDownloadedBytes;
+17 -8
View File
@@ -111,7 +111,7 @@ import {
StatisticsSidebarStatus,
type StatisticsViewActions
} from "./views/statistics/StatisticsView";
import { buildDownloadsViewModel, formatDownloadEta, formatRemainingDownloadBytes, formatRemainingDownloadTooltip, getDownloadQueueStatusMetrics, getDownloadSpeedBps, type DownloadDisplayMode, type DownloadSidebarFilter } from "./views/downloads/downloads-model";
import { buildDownloadsViewModel, formatDownloadEta, formatRemainingDownloadBytes, formatRemainingDownloadTooltip, getDownloadQueueStatusMetrics, getDownloadSpeedBps, getPackageErrorItemIds, isDownloadItemError, type DownloadDisplayMode, type DownloadSidebarFilter } from "./views/downloads/downloads-model";
import { downloadColumnDefinitions, type DownloadSortColumn } from "./views/downloads/DownloadsTable";
import { DeleteConfirmationDialog } from "./views/downloads/DeleteConfirmationDialog";
import { beginDownloadColumnDrag, clearDownloadColumnDrag, commitDownloadColumnDrag, createDownloadColumnOrderPersistence, DOWNLOAD_COLUMN_MOVE_DURATION_MS, updateDownloadColumnDrag, type DownloadColumnDragSession, type DownloadColumnOrderPersistence } from "./views/downloads/column-drag";
@@ -5684,7 +5684,7 @@ export function App(): ReactElement {
},
onResetErrors: () => {
const failedIds = Object.values(snapshot.session.items)
.filter((item) => item.status === "failed")
.filter(isDownloadItemError)
.map((item) => item.id);
if (failedIds.length === 0) {
return;
@@ -7119,12 +7119,21 @@ export function App(): ReactElement {
else { executeDeleteSelection(ids); }
}}>Ausgewählte Dateien entfernen ({selectedItemIds.length})</button>
)}
{hasPackages && !contextMenu.itemId && (
<button className="ctx-menu-item" onClick={() => {
for (const id of selectedPackageIds) void window.rd.resetPackage(id).catch(() => {});
setContextMenu(null);
}}>Zurücksetzen{multi ? ` (${selectedPackageIds.length})` : ""}</button>
)}
{hasPackages && !contextMenu.itemId && (() => {
const errorItemIds = getPackageErrorItemIds(selectedPackageIds, snapshot.session.packages, snapshot.session.items);
return (
<>
<button className="ctx-menu-item" disabled={errorItemIds.length === 0} onClick={() => {
if (errorItemIds.length > 0) void window.rd.resetItems(errorItemIds).catch(() => {});
setContextMenu(null);
}}>Nur fehlerhafte Dateien zurücksetzen</button>
<button className="ctx-menu-item" onClick={() => {
for (const id of selectedPackageIds) void window.rd.resetPackage(id).catch(() => {});
setContextMenu(null);
}}>{multi ? "Ausgewählte Pakete vollständig zurücksetzen" : "Gesamtes Paket zurücksetzen"}</button>
</>
);
})()}
{contextMenu.itemId && (
<button className="ctx-menu-item" onClick={() => {
const itemIds = multi ? selectedItemIds : [contextMenu.itemId!];
+1 -1
View File
@@ -81,7 +81,7 @@ const pairs = [
["Sitzungszähler und Ergebnisse der aktuellen Queue werden angezeigt.", "Session counters and results for the current queue are shown."], ["Sitzung zurücksetzen", "Reset session"], ["Gesamt zurücksetzen", "Reset total"], ["Fehler zurücksetzen", "Reset errors"],
["Bandbreitenverlauf", "Bandwidth history"], ["Bandbreitenverlauf der letzten 60 Sekunden", "Bandwidth history for the last 60 seconds"], ["Provider", "Provider"], ["Daten", "Data"], ["Ergebnisse", "Results"],
["Nie", "Never"], ["Sofort", "Immediately"], ["Beim App-Start", "On app startup"], ["Sobald Paket fertig ist", "When package completes"], ["Überschreiben", "Overwrite"], ["Überspringen", "Skip"], ["Nachfragen", "Ask"],
["Abbrechen", "Cancel"], ["Speichern", "Save"], ["Schließen", "Close"], ["Löschen", "Delete"], ["Suchen", "Search"], ["Zurücksetzen", "Reset"], ["Testen", "Test"], ["Öffnen", "Open"],
["Abbrechen", "Cancel"], ["Speichern", "Save"], ["Schließen", "Close"], ["Löschen", "Delete"], ["Suchen", "Search"], ["Zurücksetzen", "Reset"], ["Nur fehlerhafte Dateien zurücksetzen", "Reset failed files only"], ["Gesamtes Paket zurücksetzen", "Reset entire package"], ["Ausgewählte Pakete vollständig zurücksetzen", "Reset selected packages completely"], ["Testen", "Test"], ["Öffnen", "Open"],
["Noch keine Downloads", "No downloads yet"], ["Füge Links hinzu, um den ersten Download zu starten.", "Add links to start the first download."], ["Keine passenden Downloads", "No matching downloads"], ["Alle anzeigen", "Show all"],
["Neue Sammlung", "New collection"], ["Linksammler-Aktionen", "Link collector actions"], ["Linksammler-Filter", "Link collector filters"], ["Links erfassen", "Capture links"], ["DLC importieren", "Import DLC"], ["Datei importieren", "Import file"],
["Pakete:", "Packages:"], ["Links:", "Links:"], ["Downloads übergeben", "Transfer downloads"], ["Analyse läuft im Hintergrund", "Analysis is running in the background"], ["Name, URL oder Hoster", "Name, URL, or host"], ["Teilweise online", "Partially online"],
@@ -193,13 +193,31 @@ function matchesQuery(value: string | undefined, query: string): boolean {
return Boolean(value?.toLocaleLowerCase("de-DE").includes(query));
}
function isExtractFailure(item: DownloadItem): boolean {
export function isDownloadItemError(item: DownloadItem): boolean {
const status = item.fullStatus.trim();
return /^(?:Entpack(?:-|\s*)Fehler\b|Entpacken\b.*(?:\bFehler\b|\bError\b|fehlgeschlagen)|Extraction\b.*(?:\bError\b|failed))/i.test(status);
return item.status === "failed"
|| /^(?:Entpack(?:-|\s*)Fehler\b|Entpacken\b.*(?:\bFehler\b|\bError\b|fehlgeschlagen)|Extraction\b.*(?:\bError\b|failed))/i.test(status);
}
export function getPackageErrorItemIds(
packageIds: readonly string[],
packages: Record<string, PackageEntry>,
items: Record<string, DownloadItem>
): string[] {
const errorItemIds = new Set<string>();
for (const packageId of packageIds) {
const pkg = packages[packageId];
if (!pkg) continue;
for (const itemId of pkg.itemIds) {
const item = items[itemId];
if (item && isDownloadItemError(item)) errorItemIds.add(itemId);
}
}
return [...errorItemIds];
}
function classifyDownloadItem(item: DownloadItem, pkg: PackageEntry): DownloadSidebarFilter {
if (item.status === "failed" || isExtractFailure(item)) return "failed";
if (isDownloadItemError(item)) return "failed";
if (item.status === "cancelled") return "all";
if (isExtracted(item)) return "completed";
if ((pkg.status === "extracting" || pkg.status === "integrity_check") && item.status === "completed") return "active";