Steps 7 and 8: torrents, OPML, and polish

librqbit replaces the vendored BitTorrent 4.2.1 tree. Torrents download
in place because seeding serves the files it downloaded, so the planned
stage-then-move would have broken it. The stall budget now also covers
magnet metadata resolution, which otherwise never returns against a dead
swarm and wedged the scan.

Adds add/rm/import/export, tracing setup, systemd units and README.

A successful swarm download is unverified: no reachable peers here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RPyeapneuXrCdojsaiXGbe
This commit is contained in:
2026-09-09 21:02:22 +00:00
parent b12e0c46dd
commit 833c07240b
10 changed files with 651 additions and 47 deletions

View File

@@ -13,8 +13,9 @@ The full design and step list live in the plan file at
- [x] **4. `download.rs`** — downloads, filters, dedupe.
- [x] **5. `retention.rs`** — oldest-first quota + age reaper.
- [x] **6. `ipc.rs` + daemon** — UDS JSON-lines server, TTL scheduler, CLI-proxies-to-daemon.
- [ ] **7. `torrent.rs`** — librqbit, seed to ratio/time, stall abort. *Done when:* smoke 5 passes.
- [ ] **8. OPML + polish** — import/export, add/rm/status, tracing setup, systemd unit, README.
- [x] **7. `torrent.rs`** — librqbit, seed to ratio/time, stall abort. (swarm download unverified —
see the step 7 entry)
- [x] **8. OPML + polish** — import/export, add/rm/status, tracing setup, systemd units, README.
## Smoke tests
@@ -29,6 +30,54 @@ The full design and step list live in the plan file at
---
## 2026-09-09 — Step 8: OPML and polish
`ipx add <url>` fetches the feed to name it from its own title (`Accidental Tech Podcast` ->
`accidental-tech-podcast`); a feed that cannot be reached is still added, named from its URL, rather
than refused. `ipx rm` leaves downloads and history alone, so re-adding a feed does not re-pull its
back catalogue. `ipx import`/`export` walk nested OPML folder outlines and skip URLs already
subscribed. `tracing` logs to stderr, `IPX_LOG` sets the filter. `contrib/` has a systemd user unit
for the daemon, plus a timer and one-shot service for the no-daemon style (with the caveat that
without a daemon there is no socket for a UI).
Verified: `cargo test` 27/27. Round-trip — exported 3 feeds with real titles, removed one,
re-imported: exactly the missing one came back, no duplicates, config still mode 0600. Quickstart
from a genuinely empty home with no env overrides: `list` on a missing config, `add`, `fetch`
(1 downloaded, 1 explicit skipped, 1 torrent 404), `list`; every path resolved under `$HOME`.
---
## 2026-09-09 — Step 7: torrent.rs
`src/torrent.rs`: a librqbit `Session` started lazily on first torrent (binding ports and starting a
DHT for a config that has never seen a torrent would be rude). `.torrent` URLs and magnets both go
through `AddTorrent::from_url`. Progress is polled once a second and reported through the same
`Progress` events HTTP downloads use.
Downloads go **straight into the feed folder** rather than staging and moving. The plan said move
then seed, which cannot work — seeding serves the files it downloaded, so moving them first breaks
it. In-place also removes a copy the original had to do.
Seeding stops at `seed_ratio` or `seed_time_mins`, whichever comes first, then the torrent is
released from the session (files kept).
**Bug the smoke test caught:** the stall budget only covered the download loop, but resolving a
magnet's metadata happens inside `add_torrent`, which against a dead swarm never returns — a
torrent nobody seeds wedged the scan indefinitely. `add_torrent` is now wrapped in the same budget.
Verified: `cargo test` 27/27 (ratio incl. the divide-by-zero case, stall_mins = 0 not meaning
"abort instantly"). Routing: with `enabled = false` a torrent enclosure is marked
`skipped/torrents disabled` and never attempted. Session startup works here. Stall abort measured
end to end: with `stall_mins = 1`, a dead magnet failed at 20:58:23 -> 20:59:24, exactly 61s, and
the row recorded `error / no metadata after 1 minutes, gave up`.
**Not verified: an actual successful swarm download.** This sandbox has no reachable peers, so
smoke 5's happy path — payload lands, seeding stops at the ratio — has not been run. The code paths
either side of it are tested; the swarm itself needs a real network. Worth running once against a
live torrent feed before trusting it.
---
## 2026-09-09 — Step 6: ipc.rs + daemon
`src/ipc.rs`: `Event` and `Command` as serde-tagged enums (`{"ev":...}` / `{"cmd":...}`), one JSON