No description
  • Go 95.8%
  • Nix 4.2%
Find a file
Matt 3bd50e143c observability: fill in the silent code paths
Debugging the "stuck at 50% with no logs" incident exposed how
little the shim says about what it's doing. The reconciler would
iterate hundreds of torrents and emit at most a handful of debug
skip lines; the transfer worker stayed silent between receiving
a job and "transfer complete" minutes later; the HTTP layer said
nothing about what *arr was asking for.

Add the following, all at sensible levels:

INFO (always on):
  * "reconcile: enqueue" — per torrent accepted for pulling
    (hash, name, size, remote_path, relpath). Low-rate, happens
    once per new torrent — makes it obvious at a glance which
    torrents the shim has decided to pull.
  * "transfer start" — when the worker dequeues and spawns rclone.
    Includes pid so operators can strace/kill the child.

DEBUG (log-level=debug):
  * "reconcile: skipping incomplete torrent" — previously silent.
    Now visible so you can tell when qbit hasn't finished yet.
  * "reconcile: skipping already-completed torrent" — previously
    silent. Visible so you can tell when a local marker exists.
  * "reconcile: tick done" — per-tick counters (upstream count,
    enqueued, each skip bucket). Turns the stream of per-torrent
    debug lines into a scoreboard.
  * "transfer progress" — every ~5s during an active rclone run
    (bytes, total, pct, speed, elapsed). Lets you see a long
    transfer is actually moving, and diagnose stalls that aren't
    ending in "rclone failed".
  * "http" — per-request line (method, path, status, bytes,
    duration, remote). *arr polls /torrents/info hot, so this
    is debug-only by design.
2026-04-22 14:01:54 -04:00
cmd/qbit-bridge app: composition root in internal/app.Run; main.go delegates 2026-04-19 12:13:20 -04:00
docs module: add services.qbit-bridge.logLevel option 2026-04-19 20:30:23 -04:00
internal observability: fill in the silent code paths 2026-04-22 14:01:54 -04:00
nix module: add services.qbit-bridge.logLevel option 2026-04-19 20:30:23 -04:00
.envrc nix: consolidate module under nix/ (module/ → nix/module.nix) 2026-04-19 14:08:47 -04:00
.gitignore gitignore: add /.worktrees for isolated dev workspaces 2026-04-19 11:31:38 -04:00
flake.lock add claude 2026-04-19 20:46:43 -04:00
flake.nix add claude 2026-04-19 20:46:43 -04:00
go.mod integration: scaffold tagged end-to-end test + test SSH key 2026-04-19 12:18:00 -04:00
go.sum integration: scaffold tagged end-to-end test + test SSH key 2026-04-19 12:18:00 -04:00
HANDOVER.md format: run treefmt (prettier on docs, gofmt on nix-adjacent files); commit flake.lock 2026-04-19 12:25:27 -04:00
README.md docs: configuration + deployment guide 2026-04-19 14:31:09 -04:00
renovate.json nix: consolidate module under nix/ (module/ → nix/module.nix) 2026-04-19 14:08:47 -04:00

qbit-bridge

A qBittorrent Web API shim that sits between *arr apps (Sonarr, Radarr, …) and a qBittorrent running on a remote seedbox. It proxies the API calls *arr makes, pulls completed torrents to the local host over SFTP (replacing sshfs / syncthing / rclone-mount), and rewrites /torrents/info so *arr sees one honest progress bar covering both the seedbox download and the seedbox→local transfer. No more "100% on seedbox, missing locally" gap.

Single-operator, single-seedbox tool. No web UI, no multi-user support.

Quick start

Add this flake as an input in your NixOS config:

# flake.nix of your system config
{
  inputs.qbit-bridge.url = "git+https://git.matt.you/matt/qbit-bridge";
  inputs.qbit-bridge.inputs.nixpkgs.follows = "nixpkgs";
  # ...
}

Enable the service on the host that will run the shim:

# host module
{
  imports = [ inputs.qbit-bridge.nixosModules.default ];

  services.qbit-bridge = {
    enable = true;

    upstreamUrl = "https://seedbox.example.com/qbittorrent";

    seedbox = {
      host = "seedbox.example.com";
      user = "matt";
      identityFile = "/run/secrets/qbit-bridge-ssh-key"; # sops-nix / whatever
    };

    outputDir = "/mnt/media/downloads";
  };
}

Point *arr at http://127.0.0.1:46881 instead of the seedbox directly. Enter any username/password — the shim accepts anything and forwards to upstream using upstreamUrl. See docs/configuration.md for the full option reference, *arr-side setup, and troubleshooting.

Outputs

  • packages.${system}.default — the shim binary.
  • nixosModules.default — the NixOS module. Auto-wires the package; just set services.qbit-bridge.enable = true.
  • checks.${system}.tests — runs the Go unit suite under nix flake check.

Local dev

nix develop
go test -race ./...

Integration test (requires Docker or Podman):

# with podman:
export DOCKER_HOST=unix:///run/user/$(id -u)/podman/podman.sock
export TESTCONTAINERS_RYUK_DISABLED=true
go test -tags=integration -race ./internal/integration/...

Design