class

ArrTop::TorrentSizes

Inherits Reference < Object

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

Log = ::Log.for("arrtop.torrent_sizes")

Constructors

build(clients : Array(Config::DownloadClient) | Nil) : TorrentSizes

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).

Source
disabled

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.

Source
new(fetcher : Fetcher, client_names : Set(String))

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.

Source

Instance methods

enabled?

Whether any download client is configured (⇒ whether this instance can ever return an exact size).

Source
exact_size(row : QueueRow) : Int64 | Nil

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.

Source
warm(rows : Array(QueueRow)) : Nil

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.

Source

Nested types