New Aria2 JSON-RPC adapter, alongside qBittorrent / Transmission / Deluge. Aria2's
RPC (default :6800/jsonrpc) maps cleanly onto the uniform adapter contract:
- the --rpc-secret token leads every call as "token:<secret>" (no username — the
secret uses the existing password field),
- addUri returns a GID (our torrent id); tellStatus → TorrentStatus with state
mapping (active→downloading, or seeding once the payload is complete; waiting→
queued; etc.),
- remove picks forceRemove vs removeDownloadResult by status, and (since aria2
doesn't delete files on remove) unlinks the file paths itself for delete_files,
- bare-host URLs get /jsonrpc appended.
Wired into adapter_for_type + the Settings dropdown (with a help note: port 6800,
secret in the Password field). All adapter methods go through the same interface,
so the stall/orphan handling and downloads pipeline work unchanged.
Tests (9): registry wiring, state mapping (incl. active→seeding), token-prefixed
params, /jsonrpc fixup, status parse (+ name fallback, no div-by-zero). 126 torrent
tests green; ruff clean.
A bare host like '192.168.1.5:8080' or 'qbittorrent.lan:8080' (no scheme)
is what users naturally type, but requests then raises 'No connection
adapters were found for ...' — it can't pick an http/https adapter, and a
bare host:port even gets misparsed as scheme=host. This surfaced as the
generic 'qbittorrent probe failed' with a 'login error: No connection
adapters were found' in the logs.
Add normalize_client_url() in torrent_clients/base: default a missing scheme
to http:// (+ trim trailing slash), and route all three adapters'
_load_config through it. Transmission normalizes the base before appending
/transmission/rpc.
Tests: normalizer unit cases + per-adapter regression (bare host -> http://).
Note: usenet adapters (sabnzbd/nzbget) share the same pattern and need the
same treatment in a follow-up.
qBittorrent 5.2.0 changed /api/v2/auth/login to return HTTP 204 (No Content)
on success instead of HTTP 200 with body 'Ok.'. The adapter required the body
to equal 'Ok.', so every login on 5.2.0+ failed with 'HTTP 204 body=' — the
connection probe and all torrent actions were broken.
Treat login as successful on the SID auth cookie and/or a success body: 'Ok.'
(<=5.1) or an empty HTTP 204 (>=5.2.0). Still reject bad creds, which
qBittorrent reports as HTTP 200 + 'Fails.' (not a 4xx).
Tests: 204-empty -> success, SID-cookie+empty-body -> success, 'Fails.' (even
with a stale cookie) -> failure.
Live testing surfaced: every download attempt failed with
'Torrent client refused the URL', but qBit was actually accepting
the add request fine. The bug was in our hash-lookup strategy.
qBittorrent's /api/v2/torrents/add returns 200 'Ok.' regardless
of whether the URL was actually valid / accepted / registered.
The previous code then queried /torrents/info?category=soulsync
to find the just-added torrent — but qBit hadn't categorised
the new torrent yet on the first poll, AND a fresh install has
no 'soulsync' category configured, so the lookup returned empty
and the adapter reported failure for every working download.
New strategy:
- Snapshot every torrent hash qBit currently tracks BEFORE
posting to /add.
- POST /add, accept its (uninformative) 200 OK.
- Poll the all-torrents list for up to 5 seconds, looking for a
hash that wasn't present in the before-snapshot. First new
hash wins.
The diff strategy works the same for /add with urls= (HTTP URL /
magnet) and /add with files= (raw .torrent upload), so both
paths now share the same _all_hashes + _poll_for_new_hash
helpers. Adds a warning log when qBit returns an unexpected
body and an error log when no new hash appears (so future
investigation has breadcrumbs).
- core/torrent_clients/transmission.py: rename unused loop var
`attempt` to `_attempt` in the session-id renegotiation loop
(B007 — loop var not used in body).
- core/image_cache.py: log the cleanup exception instead of
swallowing it silently (S110 — bare try/except/pass). debug
level since a failed tmp unlink is non-fatal; the outer
``raise`` still propagates the original error.
Full ruff sweep clean.
Second commit in the torrent + usenet rollout. SoulSync now speaks
three different BitTorrent client APIs through one uniform adapter
contract — picks the active client by config and dispatches the same
verbs to whichever backend the user uses. Each adapter handles its
own auth quirk (qBit cookie + CSRF Referer, Transmission session-id
renegotiation, Deluge JSON-RPC session) and maps native state
strings onto a shared 7-value set so the rest of the app stays
client-agnostic.
- core/torrent_clients/base.py: TorrentClientAdapter Protocol +
TorrentStatus dataclass. Eight verbs: is_configured, check_connection,
add_torrent (URL/magnet), add_torrent_file (raw bytes), get_status,
get_all, remove, pause, resume.
- core/torrent_clients/__init__.py: adapter_for_type factory +
get_active_adapter that reads torrent_client.type each call so
settings changes take effect without restart.
- core/torrent_clients/qbittorrent.py: WebUI v2 adapter. Cookie auth
via /api/v2/auth/login, transparent 403 re-login, Referer header
to satisfy qBit's CSRF guard. add_torrent returns the just-added
hash via /torrents/info sort=added_on (qBit's add endpoint doesn't
echo the hash).
- core/torrent_clients/transmission.py: RPC adapter. Auto-resolves
bare host URLs to /transmission/rpc, handles the 409 + new
X-Transmission-Session-Id renegotiation transparently, accepts
HTTP basic auth. add_torrent_file base64-encodes payload per spec.
- core/torrent_clients/deluge.py: Deluge 2.x JSON-RPC adapter.
Password-only auth, distinguishes magnet vs HTTP URL at the RPC
method layer, applies category via Label plugin (best-effort —
label plugin is optional).
- core/connection_test.py: 'torrent_client' branch picks the right
adapter, runs check_connection, surfaces a per-client error
message.
- config/settings.py: torrent_client.{type, url, username, password,
category, save_path} defaults + torrent_client.password in the
encrypted-at-rest secrets list.
- web_server.py: 'torrent_client' added to the /api/settings POST
allow-list so saved config persists.
- webui/index.html: new Torrent Client panel on the Indexers &
Downloaders tab — client-type dropdown, URL, username, password,
category, optional save path, Test Connection.
- webui/static/settings.js: load/save wiring + testTorrentClientConnection.
- webui/static/helper.js: WHATS_NEW + VERSION_MODAL_SECTIONS entry.