ArrTop::ImportRateTracker
Best-effort import ETA: estimates how long an in-progress copy has left by
measuring how fast the destination file is growing between two ImportWatch
readings.
The *arr API gives no copy progress and no copy rate for an import, and the
download timeleft/eta no longer apply once the download is done. So the
ETA is derived here from arrtop's own successive disk readings: bytes gained
÷ wall time between samples = bytes/sec, and remaining ÷ rate = the ETA.
State is keyed by an opaque per-copy key — the destination file path
(ImportProgress#file), NOT the folder. A Sonarr season pack lands every
episode in ONE folder, so a folder key would compare different episodes'
byte counts frame-to-frame and manufacture an astronomical bogus rate; keying
by file gives each episode its own sample history, so only a genuinely
growing file yields a positive rate. It returns nil until it has two
samples for a key, and resets (nil) when the watched file shrinks — a
new/replaced file — so a stale rate is never shown.
Timing uses Time.instant (a monotonic clock that never jumps backward). The
instant-based bookkeeping stays inside #eta; the actual math lives in the
pure class methods .rate/.eta_from, which take plain Int64/Time::Span
deltas so they can be unit-tested without fabricating clock readings.
Constructors
Class methods
Pure ETA from raw deltas: remaining_bytes ÷ rate, or nil when the rate
is unknown or zero (a stall, no time elapsed, or a file reset) or nothing
remains. A 0-rate stall gives NO ETA — we never divide by zero (Infinity).
Pure bytes/sec from a byte delta over a time delta. A no-growth interval
(delta_bytes == 0) with elapsed time is a measured stall and reports
0.0, not nil. Returns nil only when time did not advance
(seconds <= 0) or the file shrank/reset (delta_bytes < 0) — those stay
"unknown".
Instance methods
The estimated time remaining for the copy under key (its destination file
path), or nil when it cannot be computed yet: fewer than two samples, no
measurable time or byte gain between them (a stall), the file shrank
(reset), or the target is already reached. Records the latest sample on
every call. Delegates to #measure so it never double-samples.
Records one sample under key (the copy's destination file path) and returns both the current copy rate (bytes/sec) and the ETA from that single update — so a caller that wants both (the per-row bar's ETA and the header's aggregate speed) pays for only one sample per frame, not two.
Both are nil until a second sample exists for that key, and both reset
to nil on a zero-time delta or a shrunk file. A no-growth interval is a
measured stall: the rate reports 0.0 (a copy that isn't advancing,
not "unknown"), while the eta stays nil (nothing divides by a 0 rate).
The ETA is additionally nil once nothing remains to copy.