rays 833c07240b 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
2026-09-09 21:02:22 +00:00
2026-09-09 20:34:09 +00:00
2026-09-09 19:34:25 +00:00
2026-09-09 20:38:30 +00:00
2026-09-09 20:38:30 +00:00
2026-09-09 19:34:25 +00:00

ipodderx-rs

A headless podcatcher: scans RSS/Atom feeds, downloads enclosures (HTTP and BitTorrent), files them into per-feed folders, and reaps old episodes to stay under a disk quota. Runs as a one-shot CLI or as a daemon with a Unix-socket JSON event stream for a UI to attach to.

Lineage

This is a modern Rust rewrite of ipodderx-core, the Python 2 engine behind iPodderX (2004-2008, Ray Slakinski & August Trometer), which was open-sourced under the MIT License in 2010.

What carries over: the feed scan and TTL handling, GUID/URL dedupe, per-feed and per-date download folders, keyword filters, the explicit-content filter, torrent enclosures, and "SmartSpace" -- the oldest-first disk quota reaper.

What does not: iTunes and iPhoto export via AppleScript, text-to-speech enclosures, the Windows WMP/COM paths, XML plists and Python pickles for state, the directory.iPodderX.com survey ping, 3DES-encrypted preferences, and the printMSG stdout protocol (replaced by a JSON-lines socket).

Quickstart

cargo install --path .

ipx add https://atp.fm/rss          # names the feed from its own title
ipx list
ipx fetch                           # scan now
ipx daemon                          # or run continuously, honouring each feed's <ttl>

Config lives at ~/.config/ipx/config.toml (mode 0600, since it may hold feed passwords); state at ~/.local/share/ipx/state.db. Override with IPX_CONFIG and IPX_DATA_DIR. Set IPX_LOG=ipx=debug for verbose logging on stderr.

Commands

command what it does
ipx add <url> [--folder X] [--keywords a,b] subscribe; the id comes from the feed title
ipx rm <feed> unsubscribe; downloads and history are kept
ipx list / ipx status subscriptions and their state
ipx fetch [FEED] [--force] scan; --force ignores the TTL
ipx reap [--dry-run] run retention now
ipx import/export <file.opml> move subscriptions in or out
ipx daemon scheduler plus the control socket

Any command with a wire form probes the socket first: if a daemon is running it does the work, and the CLI just renders the events it streams back. --local forces in-process execution.

Configuration

[general]
download_dir   = "~/Podcasts"
socket         = "/run/user/1000/ipx.sock"   # default: $XDG_RUNTIME_DIR/ipx.sock
interval_mins  = 60          # default poll; a feed's own <ttl> wins when longer
organize       = "feed"      # "feed" | "date"
max_total_gb   = 50          # 0 = unlimited
max_age_days   = 30          # 0 = keep forever

[torrent]
enabled        = true
seed_ratio     = 1.0         # stop seeding at this ratio ...
seed_time_mins = 60          # ... or after this long, whichever comes first
port_range     = "6881-6889"
stall_mins     = 30          # give up on a torrent making no progress

[feeds.atp]
url                = "https://atp.fm/rss"
folder             = "Accidental Tech Podcast"  # default: the feed title
keywords           = ["deep dive"]   # OR across keywords, AND within one
allow_explicit     = false
auto_download      = true
max_new_per_check  = 3        # the rest wait for the next scan
username           = "ray"                  # optional HTTP basic auth
password_env       = "IPX_ATP_PASS"         # or a literal `password`

Retention keeps files that are flagged in the database, and deletes read episodes before unread ones, oldest first.

Socket protocol

Newline-delimited JSON over a Unix socket, both directions.

$ printf '{"cmd":"fetch","force":true}\n' | socat - UNIX-CONNECT:$XDG_RUNTIME_DIR/ipx.sock
{"ev":"feed_start","feed":"atp"}
{"ev":"progress","feed":"atp","url":"...","file":"ep1.mp3","done":8192,"total":3000000}
{"ev":"download_done","feed":"atp","url":"...","path":"...","bytes":3000000}
{"ev":"feed_done","feed":"atp","new":1,"downloaded":1,"failed":0,"torrents":0}
{"ev":"scan_done","feeds":1}

Commands: fetch (optional feed, force), reap (optional dry_run), status. Events: feed_start, feed_skip, feed_done, feed_error, progress, download_done, download_error, torrent_deferred, reaped, reap_done, scan_done, status, error. scan_done, reap_done and status are terminal -- a client that asked for work stops there.

Progress is throttled to whole percents. The stream is a broadcast, so a client attached to a busy daemon also sees that daemon's other work.

Running it as a service

contrib/ has a systemd user unit for the daemon, and a timer plus one-shot service if you would rather run periodic scans with no daemon (in which case there is no socket for a UI to attach to).

License

MIT. See LICENSE.

Description
No description provided
Readme MIT 9.5 MiB
Languages
Rust 49%
JavaScript 27.3%
TypeScript 15.4%
CSS 6.8%
HTML 1.1%
Other 0.4%