Smooth download speed and time remaining
This commit is contained in:
1 parent
68c7dd76f2
commit
596caf57ee
4 files changed
+65
-36
No files matched your search
@@ -3,11 +3,12 @@ import { element } from "./dom.js";
|
||||
import { destinationSelect, setAutoExtractDefault, setupDownloadForm, updateSubfolderState } from "./download-form.js";
|
||||
import { formatBytes } from "./format.js";
|
||||
import { recentDestinations, renderHistory } from "./history.js";
|
||||
import { isActive, matchesFilter, renderJobs, updateSpeeds } from "./jobs.js";
|
||||
import { isActive, matchesFilter, renderJobs } from "./jobs.js";
|
||||
import { fillLibrarySelect } from "./libraries.js";
|
||||
import { setupLogin } from "./login.js";
|
||||
import { collectTransitions, notifyTransitions } from "./notify.js";
|
||||
import { loadSettings, openSettings, setupSettings } from "./settings.js";
|
||||
import { updateSpeeds } from "./speed.js";
|
||||
import { renderTree, topLevelSelection } from "./tree.js";
|
||||
|
||||
const pollIntervalMs = 1500;
|
||||
|
||||
+18
-8
@@ -22,14 +22,24 @@ export function formatBytes(bytes) {
|
||||
return `${value.toFixed(unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
export function formatDuration(seconds) {
|
||||
const total = Math.max(0, Math.round(seconds));
|
||||
const hours = Math.floor(total / 3600);
|
||||
const minutes = Math.floor((total % 3600) / 60);
|
||||
const remainingSeconds = total % 60;
|
||||
if (hours > 0) return `${hours}h ${minutes}m`;
|
||||
if (minutes > 0) return `${minutes}m ${remainingSeconds}s`;
|
||||
return `${remainingSeconds}s`;
|
||||
function roundTo(value, step) {
|
||||
return Math.max(step, Math.round(value / step) * step);
|
||||
}
|
||||
|
||||
export function formatRemaining(seconds) {
|
||||
if (!Number.isFinite(seconds) || seconds <= 0) return "almost done";
|
||||
if (seconds < 60) return `${roundTo(seconds, 5)}s`;
|
||||
if (seconds < 600) {
|
||||
const rounded = roundTo(seconds, 15);
|
||||
const minutes = Math.floor(rounded / 60);
|
||||
const remainingSeconds = rounded % 60;
|
||||
return remainingSeconds === 0 ? `${minutes}m` : `${minutes}m ${remainingSeconds}s`;
|
||||
}
|
||||
if (seconds < 3600) return `${roundTo(seconds / 60, 1)}m`;
|
||||
const totalMinutes = roundTo(seconds / 60, 5);
|
||||
const hours = Math.floor(totalMinutes / 60);
|
||||
const minutes = totalMinutes % 60;
|
||||
return minutes === 0 ? `${hours}h` : `${hours}h ${minutes}m`;
|
||||
}
|
||||
|
||||
export function formatDate(value) {
|
||||
|
||||
+6
-27
@@ -1,10 +1,9 @@
|
||||
import { button, element } from "./dom.js";
|
||||
import { formatBytes, formatDuration, percentOf, statusLabels } from "./format.js";
|
||||
import { formatBytes, formatRemaining, percentOf, statusLabels } from "./format.js";
|
||||
import { speedOf } from "./speed.js";
|
||||
|
||||
const activeStatuses = new Set(["queued", "downloading", "extracting", "moving"]);
|
||||
const pausableStatuses = new Set(["queued", "downloading"]);
|
||||
const speedSmoothing = 0.3;
|
||||
const speedSamples = new Map();
|
||||
|
||||
export function isActive(job) {
|
||||
return activeStatuses.has(job.status);
|
||||
@@ -16,26 +15,6 @@ export function matchesFilter(job, filter) {
|
||||
return job.status === filter;
|
||||
}
|
||||
|
||||
export function updateSpeeds(jobs) {
|
||||
const now = performance.now();
|
||||
const seenIds = new Set();
|
||||
|
||||
for (const job of jobs) {
|
||||
seenIds.add(job.id);
|
||||
const previous = speedSamples.get(job.id);
|
||||
let speed = 0;
|
||||
if (previous && job.bytesDone >= previous.bytes && now > previous.time) {
|
||||
const instantSpeed = ((job.bytesDone - previous.bytes) * 1000) / (now - previous.time);
|
||||
speed = previous.speed > 0 ? previous.speed + speedSmoothing * (instantSpeed - previous.speed) : instantSpeed;
|
||||
}
|
||||
speedSamples.set(job.id, { bytes: job.bytesDone, time: now, speed });
|
||||
}
|
||||
|
||||
for (const id of speedSamples.keys()) {
|
||||
if (!seenIds.has(id)) speedSamples.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
function displayRank(job) {
|
||||
if (isActive(job) && !job.queuePosition) return 0;
|
||||
if (job.queuePosition) return 1;
|
||||
@@ -110,11 +89,11 @@ function progressText(job, percent) {
|
||||
}
|
||||
if (percent !== null) parts.push(`${percent.toFixed(1)}%`);
|
||||
|
||||
const sample = speedSamples.get(job.id);
|
||||
if (isActive(job) && sample && sample.speed > 0) {
|
||||
parts.push(`${formatBytes(sample.speed)}/s`);
|
||||
const speed = speedOf(job.id);
|
||||
if (isActive(job) && speed > 0) {
|
||||
parts.push(`${formatBytes(speed)}/s`);
|
||||
if (job.bytesTotal > job.bytesDone) {
|
||||
parts.push(`${formatDuration((job.bytesTotal - job.bytesDone) / sample.speed)} left`);
|
||||
parts.push(`${formatRemaining((job.bytesTotal - job.bytesDone) / speed)} left`);
|
||||
}
|
||||
}
|
||||
return parts.join(" | ");
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
const windowMs = 30000;
|
||||
const minimumSpanMs = 4000;
|
||||
const samplesByJob = new Map();
|
||||
|
||||
export function updateSpeeds(jobs) {
|
||||
const now = performance.now();
|
||||
const seenIds = new Set();
|
||||
|
||||
for (const job of jobs) {
|
||||
seenIds.add(job.id);
|
||||
let samples = samplesByJob.get(job.id) || [];
|
||||
const last = samples[samples.length - 1];
|
||||
const progressWentBack = last && job.bytesDone < last.bytes;
|
||||
const notTransferring = job.status !== "downloading" && job.status !== "extracting" && job.status !== "moving";
|
||||
if (progressWentBack || notTransferring || (last && last.status !== job.status)) {
|
||||
samples = [];
|
||||
}
|
||||
|
||||
samples.push({ bytes: job.bytesDone, time: now, status: job.status });
|
||||
while (samples.length > 2 && now - samples[1].time >= windowMs) {
|
||||
samples.shift();
|
||||
}
|
||||
samplesByJob.set(job.id, samples);
|
||||
}
|
||||
|
||||
for (const id of samplesByJob.keys()) {
|
||||
if (!seenIds.has(id)) samplesByJob.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
export function speedOf(jobId) {
|
||||
const samples = samplesByJob.get(jobId);
|
||||
if (!samples || samples.length < 2) return 0;
|
||||
const first = samples[0];
|
||||
const last = samples[samples.length - 1];
|
||||
const spanMs = last.time - first.time;
|
||||
if (spanMs < minimumSpanMs) return 0;
|
||||
return ((last.bytes - first.bytes) * 1000) / spanMs;
|
||||
}
|
||||
Reference in new issue
Block a user