mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-07 09:53:17 +00:00
579a8f7f80
- add durable Torrent telemetry, availability, sharing, relocation, and web-seed workflows\n- fence queue ownership, lifecycle recovery, persistence, and native control races\n- add localized UI, generated bindings, regression coverage, and smoke validation
129 lines
4.3 KiB
TypeScript
129 lines
4.3 KiB
TypeScript
import type { DownloadItem } from '../bindings/DownloadItem';
|
|
import type { DownloadProgressEvent } from '../bindings/DownloadProgressEvent';
|
|
import { isTransferActiveStatus } from './downloads';
|
|
|
|
export interface DownloadSummary {
|
|
itemCount: number;
|
|
activeCount: number;
|
|
downloadedBytes: number | null;
|
|
remainingBytes: number | null;
|
|
remainingIsEstimated: boolean;
|
|
}
|
|
|
|
type ProgressMap = Readonly<Record<string, DownloadProgressEvent | undefined>>;
|
|
|
|
const usableBytes = (value: number | null | undefined): number | undefined =>
|
|
typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
|
|
const isFreshDownloadStatus = (status: DownloadItem['status']): boolean =>
|
|
status === 'ready' ||
|
|
status === 'staged' ||
|
|
status === 'queued' ||
|
|
status === 'downloading' ||
|
|
status === 'seeding' ||
|
|
status === 'processing' ||
|
|
status === 'verifying' ||
|
|
status === 'retrying' ||
|
|
status === 'moving';
|
|
|
|
const hasPositiveProgress = (download: DownloadItem): boolean =>
|
|
typeof download.fraction === 'number' &&
|
|
Number.isFinite(download.fraction) &&
|
|
download.fraction > 0;
|
|
|
|
const canInferNoDownloadedBytes = (download: DownloadItem): boolean =>
|
|
!hasPositiveProgress(download) &&
|
|
(isFreshDownloadStatus(download.status) || download.status === 'paused');
|
|
|
|
const effectiveByteState = (
|
|
download: DownloadItem,
|
|
progress: DownloadProgressEvent | undefined
|
|
): { downloadedBytes?: number; totalBytes?: number; totalIsEstimate: boolean } => {
|
|
const usesStoredMediaTotal = download.isMedia === true && progress && !progress.size_is_final;
|
|
const storedTotalIsEstimate = download.totalIsEstimate === true ||
|
|
download.size?.trim().startsWith('~') === true;
|
|
const totalBytes = usesStoredMediaTotal
|
|
? usableBytes(download.totalBytes)
|
|
: usableBytes(progress?.total_bytes) ?? usableBytes(download.totalBytes);
|
|
const downloadedBytes = download.status === 'completed'
|
|
? usableBytes(download.downloadedBytes) ??
|
|
usableBytes(progress?.downloaded_bytes) ??
|
|
totalBytes
|
|
: usableBytes(progress?.downloaded_bytes) ??
|
|
usableBytes(download.downloadedBytes) ??
|
|
(canInferNoDownloadedBytes(download) ? 0 : undefined);
|
|
const totalIsEstimate = usesStoredMediaTotal
|
|
? storedTotalIsEstimate
|
|
: (progress?.total_is_estimate ?? storedTotalIsEstimate) === true;
|
|
|
|
return { downloadedBytes, totalBytes, totalIsEstimate };
|
|
};
|
|
|
|
export const summarizeDownloads = (
|
|
downloads: readonly DownloadItem[],
|
|
progressMap: ProgressMap = {}
|
|
): DownloadSummary => {
|
|
if (downloads.length === 0) {
|
|
return {
|
|
itemCount: 0,
|
|
activeCount: 0,
|
|
downloadedBytes: null,
|
|
remainingBytes: null,
|
|
remainingIsEstimated: false,
|
|
};
|
|
}
|
|
|
|
let downloadedBytes = 0;
|
|
let remainingBytes = 0;
|
|
let downloadedKnown = true;
|
|
let remainingKnown = true;
|
|
let remainingIsEstimated = false;
|
|
let activeCount = 0;
|
|
|
|
for (const download of downloads) {
|
|
const state = effectiveByteState(download, progressMap[download.id]);
|
|
if (isTransferActiveStatus(download.status) || download.status === 'moving') activeCount += 1;
|
|
if (state.downloadedBytes === undefined) {
|
|
downloadedKnown = false;
|
|
} else {
|
|
const nextDownloadedBytes = downloadedBytes + state.downloadedBytes;
|
|
if (Number.isFinite(nextDownloadedBytes)) {
|
|
downloadedBytes = nextDownloadedBytes;
|
|
} else {
|
|
downloadedKnown = false;
|
|
}
|
|
}
|
|
|
|
if (download.status === 'completed') {
|
|
// A completed row is terminal even when a delayed progress event still
|
|
// carries an old partial denominator. Never expose that stale value as
|
|
// remaining work in the aggregate status bar.
|
|
continue;
|
|
}
|
|
|
|
if (
|
|
state.totalBytes === undefined ||
|
|
state.downloadedBytes === undefined ||
|
|
state.downloadedBytes > state.totalBytes
|
|
) {
|
|
remainingKnown = false;
|
|
} else {
|
|
const nextRemainingBytes = remainingBytes + Math.max(0, state.totalBytes - state.downloadedBytes);
|
|
if (Number.isFinite(nextRemainingBytes)) {
|
|
remainingBytes = nextRemainingBytes;
|
|
remainingIsEstimated ||= state.totalIsEstimate;
|
|
} else {
|
|
remainingKnown = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
itemCount: downloads.length,
|
|
activeCount,
|
|
downloadedBytes: downloadedKnown ? downloadedBytes : null,
|
|
remainingBytes: remainingKnown ? remainingBytes : null,
|
|
remainingIsEstimated,
|
|
};
|
|
};
|