arr_top v0.3.2

arrtop — a top-like TUI for the Sonarr/Radarr download+import pipeline

arrtop

A top-style terminal UI for the Sonarr/Radarr download → import pipeline. It shows the queue sorted by what's actively importing, with real progress bars — including the one the *arr API can't give you: import (copy) progress.

Status: usable. The data layer (typed queue poller), config + CLI, logging, the import disk-watch, and the full-screen live TUI are in place. On a terminal arrtop runs the live view; piped/redirected (or with --once) it prints a one-shot snapshot with a live IMPORT% column.

Why it exists

The *arr API tells you a lot about the queue for free — trackedDownloadState (downloading/importPending/importing), and size/sizeleft/timeleft for a download progress bar. But once a download completes and the *arr is copying the file into the library, the API reports only "importing" with sizeleft: 0 and hasFile: falseno copy progress at all. On a cross-filesystem import (e.g. a remote/seedbox source), that copy can take an hour with zero feedback.

arrtop fills that gap by reading the destination file directly:

queue (API)         → sort by trackedDownloadState; download bars from size/sizeleft
importing item      → movie.path / series.path (API: the destination folder)
                    → queue.size              (API: the copy's final size = the target)
                    → destination file size   (disk: the one number only disk knows)
import %            = file bytes / target
done                = movie.hasFile flips true (movieFile.path populates)

The import disk-watch

For a row whose state is importing, ArrTop::ImportWatch.progress turns the API's destination folder plus copy target (queue.size, carried on the row as import_target) into a live copy percentage read straight off disk:

  • Recursive walk. The destination is a folder — Radarr's movie folder, or Sonarr's series folder where the file lands in a Season NN/ subfolder — so the walk descends into subfolders to collect every regular file.
  • No Dir.glob (metacharacter gotcha). Real *arr folder names embed glob metacharacters — Jurassic Park (1993) {tmdb-329} [Bluray-1080p] — that Dir.glob would interpret as patterns instead of literal path segments. The walk is therefore manual (Dir.children + File.info), and every filesystem call is rescued so a file that vanishes mid-copy or an unreadable subdirectory is skipped, not fatal.
  • Most-recent-mtime selection. Among the video files (.mkv .mp4 .avi .m4v .ts .m2ts .mov .wmv .mpg .mpeg .webm .flv), it picks the one with the newest modification time — the file actively being written. This deliberately beats a largest-size heuristic: during an upgrade the old full file sits beside the new partial one, and "largest" would wrongly pick the old file.
  • import% = file bytes / target, clamped to [0, 100] (the growing file can momentarily overshoot the target).
  • Off-host degrades to (unknown), never crashes. When the folder does not exist or cannot be read (arrtop running off the *arr host), or the import has not created the file yet, progress returns nil and the CLI shows in the IMPORT% column. Non-importing rows show there too.

Runs on the *arr host (important)

The import bar reads the file the *arr is writing, so it must run where that write is happening — on the Sonarr/Radarr host. Watching the same file over NFS from another client only updates at the writer's flush cadence (coarse, stepped) regardless of client cache settings; only the writing host's kernel reports the growing size in real time. A lighter API-only mode (download bars + import state, no copy %) can run anywhere.

Configuration

arrtop reads a list of Sonarr/Radarr backends from a config file in YAML or JSON (the same shape in both — see config.example.yaml / config.example.json):

refresh: 2s                 # TUI redraw interval; optional (default 2s)
backends:
  - name: sonarr
    type: sonarr          # sonarr | radarr (lowercase)
    url: http://localhost:8989
    api_key: YOUR_SONARR_API_KEY
  - name: radarr
    type: radarr
    url: http://localhost:7878
    api_key: YOUR_RADARR_API_KEY

refresh accepts <int>s, <int>ms, or a bare integer (seconds); it sets how often the live view redraws (it also wakes instantly on a keypress). The config path is resolved in this order:

  1. -c/--config <path>
  2. the ARR_TOP_CONFIG environment variable
  3. the first of ./config.yaml, ./config.yml, ./config.json that exists
  4. the first of /etc/arr_top/config.yaml, /etc/arr_top/config.yml, /etc/arr_top/config.json that exists

A local ./config.* therefore overrides a system-wide one. The .deb/.rpm packages ship /etc/arr_top/config.yaml.example; copy it to /etc/arr_top/config.yaml (drop the .example) to use the system-wide location. If none resolves, arrtop prints an error and exits non-zero. The file extension picks the parser (.yaml/.yml → YAML, .json → JSON; anything else tries YAML then JSON). Every backend needs a non-blank name, url, api_key, and a recognized type (sonarr or radarr); validation reports all problems at once.

Logging goes to stderr at the Info level (so the TUI can own stdout). A configurable level lands in a later phase.

arrtop --version
arrtop --help
arrtop --config /etc/arrtop/config.yaml
arrtop --once                 # one-shot snapshot, skip the live view

The live view (TUI)

Run arrtop on a terminal and it opens a full-screen, top-style live view: one line per queue item — state, title, a progress bar (the live import copy bar for importing rows, the download bar otherwise), the percentage, and an ETA — under a header summarizing the counts. An unreachable *arr shows as a red line rather than silently vanishing.

  • Keys: q (or Q, or Ctrl-C) quits.
  • Refresh: redraws every refresh (config; default 2s), and instantly on any keypress.
  • Auto-resize: the layout follows the terminal size on each redraw (no SIGWINCH needed).
  • Terminal-safe: the terminal is always restored — alt screen left, cursor shown, cooked mode — on quit, SIGINT/SIGTERM, an uncaught exception, and at process exit. It never leaves your terminal in raw mode.

Snapshot fallback. When stdout is not a terminal (piped or redirected — e.g. CI, arrtop > out.txt, arrtop | head), or when you pass --once/-1, arrtop prints a single plain-text table instead of entering the TUI. Snapshot output tolerates a closed pipe, so arrtop | head exits quietly.

Build

The binary must be built with -Dpreview_mt (arrtop's fibers run across threads), so always build through the Makefile:

make build        # → bin/arrtop  (debug, -Dpreview_mt)
make release      # optimized
make run ARGS=…   # build + run
make test         # crystal spec
make check lint   # format check + ameba

Requires Crystal >= 1.20.2. Dependencies are the public sonarr and radarr shards.

Repository

arr_top

Owner
Statistic
  • 0
  • 0
  • 1
  • 0
  • 3
  • about 1 hour ago
  • July 20, 2026
License

MIT License

Links
Synced at

Wed, 22 Jul 2026 22:32:35 GMT

Languages