# 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](https://git.sdf1.net/rays/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 ```sh 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 ``` 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 [--folder X] [--keywords a,b]` | subscribe; the id comes from the feed title | | `ipx rm ` | 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 ` | 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 ```toml [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 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. ```sh $ 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](LICENSE).