ArrTop::TorrentSizes
Optional per-episode exact-size lookup backed by one or more configured download clients (currently qBittorrent).
A Sonarr season pack reports only the whole pack's size on every episode
row — there is no per-episode size in the *arr API — so arrtop otherwise
estimates each episode as pack_total / episode_count (see
TUI.effective_target). When a download client is configured, arrtop asks it
for the torrent's file list and uses the real size of each episode's
file as the import target instead.
Concurrency (-Dpreview_mt): the poller fiber calls #warm (the only method
that touches the network — it fetches and caches each torrent's file list),
while the UI fiber calls #exact_size (a pure cache read, never any I/O).
The cache is guarded by a Mutex so the two fibers never race. #warm is
fully rescued and NEVER raises, so an unreachable client can't wedge the
poller; every uncached/failed lookup falls back to the estimate.
Fully optional: build it from config.download_clients with .build, or get
a no-op with .disabled (#exact_size always nil, #warm does nothing).
Constants
Constructors
Builds a TorrentSizes from the configured clients (nil/empty ⇒ a no-op
instance). One lazy QBittorrent::Client is held per configured client,
keyed by its name; the fetcher looks the client up by name and calls
torrents/files, mapping the result to CachedFiles and rescuing every
failure to nil (→ the caller falls back to the estimate).
A no-op instance: no configured clients, so #warm does nothing and
#exact_size always returns nil (every caller falls back to the
estimate). Used when download_clients is absent from the config.
fetcher fetches a torrent's file list by (client_name, hash);
client_names is the set of configured download-client names (a row's
download_client must be in it to be looked up). Public so specs can inject
a fake/counting fetcher with no network.
Instance methods
Whether any download client is configured (⇒ whether this instance can ever return an exact size).
UI-fiber entry point (render): the exact byte size of row's own episode
file, read purely from the cache with no network. Returns nil — so the
caller falls back to the estimate — on any miss: non-episode row, no
matching configured client, hash not cached, missing season/episode, no
cached file matching the episode's SxxEyy token, or a matching file with a
nil size. Only episode rows get exact sizes; movies keep their behaviour.
Matching reuses the SAME approach as ImportWatch (a case-insensitive
S0*<season>(E\d+)*?E0*<episode>(\b|E) token, tolerant of zero-padding and
multi-episode files). When several cached files match (e.g. a .mkv beside
a same-named .nfo), the largest is chosen — the video, not a sidecar.
POLLER-fiber entry point (background): fetch and cache the file list for
each distinct torrent among rows whose download_client matches a
configured client and whose hash isn't cached yet. One files call returns
every file in the torrent, so a whole season pack (all episodes share one
download_id) costs a single query. Every fetch is rescued; a failure
leaves the hash uncached (→ the estimate). NEVER raises into the poller.